NewPipe-app-android/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java

452 lines
18 KiB
Java

package org.schabi.newpipe.player.helper;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.SeekBar;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.SliderStrategy;
import static org.schabi.newpipe.player.BasePlayer.DEBUG;
public class PlaybackParameterDialog extends DialogFragment {
@NonNull private static final String TAG = "PlaybackParameterDialog";
// Minimum allowable range in ExoPlayer
public static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
public static final char STEP_UP_SIGN = '+';
public static final char STEP_DOWN_SIGN = '-';
public static final double STEP_ONE_PERCENT_VALUE = 0.01f;
public static final double STEP_FIVE_PERCENT_VALUE = 0.05f;
public static final double STEP_TEN_PERCENT_VALUE = 0.10f;
public static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f;
public static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f;
public static final double DEFAULT_TEMPO = 1.00f;
public static final double DEFAULT_PITCH = 1.00f;
public static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE;
public static final boolean DEFAULT_SKIP_SILENCE = false;
@NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
@NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
@NonNull private static final String TEMPO_KEY = "tempo_key";
@NonNull private static final String PITCH_KEY = "pitch_key";
@NonNull private static final String STEP_SIZE_KEY = "step_size_key";
public interface Callback {
void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch,
final boolean playbackSkipSilence);
}
@Nullable private Callback callback;
@NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic(
MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE,
/*centerAt=*/1.00f, /*sliderGranularity=*/10000);
private double initialTempo = DEFAULT_TEMPO;
private double initialPitch = DEFAULT_PITCH;
private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE;
private double tempo = DEFAULT_TEMPO;
private double pitch = DEFAULT_PITCH;
private double stepSize = DEFAULT_STEP;
@Nullable private SeekBar tempoSlider;
@Nullable private TextView tempoCurrentText;
@Nullable private TextView tempoStepDownText;
@Nullable private TextView tempoStepUpText;
@Nullable private SeekBar pitchSlider;
@Nullable private TextView pitchCurrentText;
@Nullable private TextView pitchStepDownText;
@Nullable private TextView pitchStepUpText;
@Nullable private CheckBox unhookingCheckbox;
@Nullable private CheckBox skipSilenceCheckbox;
public static PlaybackParameterDialog newInstance(final double playbackTempo,
final double playbackPitch,
final boolean playbackSkipSilence,
Callback callback) {
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
dialog.callback = callback;
dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch;
dialog.tempo = playbackTempo;
dialog.pitch = playbackPitch;
dialog.initialSkipSilence = playbackSkipSilence;
return dialog;
}
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof Callback) {
callback = (Callback) context;
} else if (callback == null) {
dismiss();
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO);
pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH);
stepSize = savedInstanceState.getDouble(STEP_SIZE_KEY, DEFAULT_STEP);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
outState.putDouble(TEMPO_KEY, getCurrentTempo());
outState.putDouble(PITCH_KEY, getCurrentPitch());
outState.putDouble(STEP_SIZE_KEY, getCurrentStepSize());
}
/*//////////////////////////////////////////////////////////////////////////
// Dialog
//////////////////////////////////////////////////////////////////////////*/
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
setupControlViews(view);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
.setTitle(R.string.playback_speed_control)
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence))
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE))
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
setCurrentPlaybackParameters());
return dialogBuilder.create();
}
/*//////////////////////////////////////////////////////////////////////////
// Control Views
//////////////////////////////////////////////////////////////////////////*/
private void setupControlViews(@NonNull View rootView) {
setupHookingControl(rootView);
setupSkipSilenceControl(rootView);
setupTempoControl(rootView);
setupPitchControl(rootView);
changeStepSize(stepSize);
setupStepSizeSelector(rootView);
}
private void setupTempoControl(@NonNull View rootView) {
tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
TextView tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText);
tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText);
tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
if (tempoCurrentText != null)
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
if (tempoMaximumText != null)
tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
if (tempoMinimumText != null)
tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
if (tempoSlider != null) {
tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
tempoSlider.setProgress(strategy.progressOf(tempo));
tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
}
}
private void setupPitchControl(@NonNull View rootView) {
pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
TextView pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
TextView pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText);
pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText);
pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
if (pitchCurrentText != null)
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
if (pitchMaximumText != null)
pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
if (pitchMinimumText != null)
pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
if (pitchSlider != null) {
pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
pitchSlider.setProgress(strategy.progressOf(pitch));
pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
}
}
private void setupHookingControl(@NonNull View rootView) {
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
if (unhookingCheckbox != null) {
unhookingCheckbox.setChecked(pitch != tempo);
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
if (isChecked) return;
// When unchecked, slide back to the minimum of current tempo or pitch
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
setSliders(minimum);
setCurrentPlaybackParameters();
});
}
}
private void setupSkipSilenceControl(@NonNull View rootView) {
skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox);
if (skipSilenceCheckbox != null) {
skipSilenceCheckbox.setChecked(initialSkipSilence);
skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) ->
setCurrentPlaybackParameters());
}
}
private void setupStepSizeSelector(@NonNull final View rootView) {
TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent);
TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent);
TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent);
TextView stepSizeTwentyFivePercentText = rootView.findViewById(R.id.stepSizeTwentyFivePercent);
TextView stepSizeOneHundredPercentText = rootView.findViewById(R.id.stepSizeOneHundredPercent);
if (stepSizeOnePercentText != null) {
stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE));
stepSizeOnePercentText.setOnClickListener(view ->
changeStepSize(STEP_ONE_PERCENT_VALUE));
}
if (stepSizeFivePercentText != null) {
stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE));
stepSizeFivePercentText.setOnClickListener(view ->
changeStepSize(STEP_FIVE_PERCENT_VALUE));
}
if (stepSizeTenPercentText != null) {
stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE));
stepSizeTenPercentText.setOnClickListener(view ->
changeStepSize(STEP_TEN_PERCENT_VALUE));
}
if (stepSizeTwentyFivePercentText != null) {
stepSizeTwentyFivePercentText.setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE));
stepSizeTwentyFivePercentText.setOnClickListener(view ->
changeStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE));
}
if (stepSizeOneHundredPercentText != null) {
stepSizeOneHundredPercentText.setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE));
stepSizeOneHundredPercentText.setOnClickListener(view ->
changeStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE));
}
}
private void changeStepSize(final double stepSize) {
this.stepSize = stepSize;
if (tempoStepUpText != null) {
tempoStepUpText.setText(getStepUpPercentString(stepSize));
tempoStepUpText.setOnClickListener(view -> {
onTempoSliderUpdated(getCurrentTempo() + stepSize);
setCurrentPlaybackParameters();
});
}
if (tempoStepDownText != null) {
tempoStepDownText.setText(getStepDownPercentString(stepSize));
tempoStepDownText.setOnClickListener(view -> {
onTempoSliderUpdated(getCurrentTempo() - stepSize);
setCurrentPlaybackParameters();
});
}
if (pitchStepUpText != null) {
pitchStepUpText.setText(getStepUpPercentString(stepSize));
pitchStepUpText.setOnClickListener(view -> {
onPitchSliderUpdated(getCurrentPitch() + stepSize);
setCurrentPlaybackParameters();
});
}
if (pitchStepDownText != null) {
pitchStepDownText.setText(getStepDownPercentString(stepSize));
pitchStepDownText.setOnClickListener(view -> {
onPitchSliderUpdated(getCurrentPitch() - stepSize);
setCurrentPlaybackParameters();
});
}
}
/*//////////////////////////////////////////////////////////////////////////
// Sliders
//////////////////////////////////////////////////////////////////////////*/
private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
final double currentTempo = strategy.valueOf(progress);
if (fromUser) {
onTempoSliderUpdated(currentTempo);
setCurrentPlaybackParameters();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
};
}
private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
final double currentPitch = strategy.valueOf(progress);
if (fromUser) { // this change is first in chain
onPitchSliderUpdated(currentPitch);
setCurrentPlaybackParameters();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
};
}
private void onTempoSliderUpdated(final double newTempo) {
if (unhookingCheckbox == null) return;
if (!unhookingCheckbox.isChecked()) {
setSliders(newTempo);
} else {
setTempoSlider(newTempo);
}
}
private void onPitchSliderUpdated(final double newPitch) {
if (unhookingCheckbox == null) return;
if (!unhookingCheckbox.isChecked()) {
setSliders(newPitch);
} else {
setPitchSlider(newPitch);
}
}
private void setSliders(final double newValue) {
setTempoSlider(newValue);
setPitchSlider(newValue);
}
private void setTempoSlider(final double newTempo) {
if (tempoSlider == null) return;
tempoSlider.setProgress(strategy.progressOf(newTempo));
}
private void setPitchSlider(final double newPitch) {
if (pitchSlider == null) return;
pitchSlider.setProgress(strategy.progressOf(newPitch));
}
/*//////////////////////////////////////////////////////////////////////////
// Helper
//////////////////////////////////////////////////////////////////////////*/
private void setCurrentPlaybackParameters() {
setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence());
}
private void setPlaybackParameters(final double tempo, final double pitch,
final boolean skipSilence) {
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
"tempo=[" + tempo + "], " +
"pitch=[" + pitch + "]");
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence);
}
}
private double getCurrentTempo() {
return tempoSlider == null ? tempo : strategy.valueOf(
tempoSlider.getProgress());
}
private double getCurrentPitch() {
return pitchSlider == null ? pitch : strategy.valueOf(
pitchSlider.getProgress());
}
private double getCurrentStepSize() {
return stepSize;
}
private boolean getCurrentSkipSilence() {
return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked();
}
@NonNull
private static String getStepUpPercentString(final double percent) {
return STEP_UP_SIGN + getPercentString(percent);
}
@NonNull
private static String getStepDownPercentString(final double percent) {
return STEP_DOWN_SIGN + getPercentString(percent);
}
@NonNull
private static String getPercentString(final double percent) {
return PlayerHelper.formatPitch(percent);
}
}