mirror of https://github.com/TeamNewPipe/NewPipe
502 lines
19 KiB
Java
502 lines
19 KiB
Java
package org.schabi.newpipe.player.helper;
|
|
|
|
import android.app.Dialog;
|
|
import android.content.Context;
|
|
import android.os.Bundle;
|
|
import android.util.Log;
|
|
import android.view.View;
|
|
import android.widget.CheckBox;
|
|
import android.widget.SeekBar;
|
|
import android.widget.TextView;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.appcompat.app.AlertDialog;
|
|
import androidx.fragment.app.DialogFragment;
|
|
import androidx.preference.PreferenceManager;
|
|
|
|
import org.schabi.newpipe.R;
|
|
import org.schabi.newpipe.util.SliderStrategy;
|
|
|
|
import static org.schabi.newpipe.player.Player.DEBUG;
|
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
|
|
public class PlaybackParameterDialog extends DialogFragment {
|
|
// Minimum allowable range in ExoPlayer
|
|
private static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
|
|
private static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
|
|
|
|
private static final char STEP_UP_SIGN = '+';
|
|
private static final char STEP_DOWN_SIGN = '-';
|
|
|
|
private static final double STEP_ONE_PERCENT_VALUE = 0.01f;
|
|
private static final double STEP_FIVE_PERCENT_VALUE = 0.05f;
|
|
private static final double STEP_TEN_PERCENT_VALUE = 0.10f;
|
|
private static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f;
|
|
private static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f;
|
|
|
|
private static final double DEFAULT_TEMPO = 1.00f;
|
|
private static final double DEFAULT_PITCH = 1.00f;
|
|
private static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE;
|
|
private static final boolean DEFAULT_SKIP_SILENCE = false;
|
|
|
|
@NonNull
|
|
private static final String TAG = "PlaybackParameterDialog";
|
|
@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";
|
|
|
|
@NonNull
|
|
private final SliderStrategy strategy = new SliderStrategy.Quadratic(
|
|
MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE,
|
|
/*centerAt=*/1.00f, /*sliderGranularity=*/10000);
|
|
|
|
@Nullable
|
|
private Callback callback;
|
|
|
|
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,
|
|
final Callback callback) {
|
|
final 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(final Context context) {
|
|
super.onAttach(context);
|
|
if (context instanceof Callback) {
|
|
callback = (Callback) context;
|
|
} else if (callback == null) {
|
|
dismiss();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
|
assureCorrectAppLanguage(getContext());
|
|
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(final 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 final Bundle savedInstanceState) {
|
|
assureCorrectAppLanguage(getContext());
|
|
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.ok, (dialogInterface, i) ->
|
|
setCurrentPlaybackParameters());
|
|
|
|
return dialogBuilder.create();
|
|
}
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Control Views
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private void setupControlViews(@NonNull final View rootView) {
|
|
setupHookingControl(rootView);
|
|
setupSkipSilenceControl(rootView);
|
|
|
|
setupTempoControl(rootView);
|
|
setupPitchControl(rootView);
|
|
|
|
setStepSize(stepSize);
|
|
setupStepSizeSelector(rootView);
|
|
}
|
|
|
|
private void setupTempoControl(@NonNull final View rootView) {
|
|
tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
|
|
final TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
|
|
final 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 final View rootView) {
|
|
pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
|
|
final TextView pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
|
|
final 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 final View rootView) {
|
|
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
|
|
if (unhookingCheckbox != null) {
|
|
// restore whether pitch and tempo are unhooked or not
|
|
unhookingCheckbox.setChecked(PreferenceManager
|
|
.getDefaultSharedPreferences(requireContext())
|
|
.getBoolean(getString(R.string.playback_unhook_key), true));
|
|
|
|
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
|
|
// save whether pitch and tempo are unhooked or not
|
|
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
.edit()
|
|
.putBoolean(getString(R.string.playback_unhook_key), isChecked)
|
|
.apply();
|
|
|
|
if (!isChecked) {
|
|
// 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 final 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) {
|
|
final TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent);
|
|
final TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent);
|
|
final TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent);
|
|
final TextView stepSizeTwentyFivePercentText = rootView
|
|
.findViewById(R.id.stepSizeTwentyFivePercent);
|
|
final TextView stepSizeOneHundredPercentText = rootView
|
|
.findViewById(R.id.stepSizeOneHundredPercent);
|
|
|
|
if (stepSizeOnePercentText != null) {
|
|
stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE));
|
|
stepSizeOnePercentText
|
|
.setOnClickListener(view -> setStepSize(STEP_ONE_PERCENT_VALUE));
|
|
}
|
|
|
|
if (stepSizeFivePercentText != null) {
|
|
stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE));
|
|
stepSizeFivePercentText
|
|
.setOnClickListener(view -> setStepSize(STEP_FIVE_PERCENT_VALUE));
|
|
}
|
|
|
|
if (stepSizeTenPercentText != null) {
|
|
stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE));
|
|
stepSizeTenPercentText
|
|
.setOnClickListener(view -> setStepSize(STEP_TEN_PERCENT_VALUE));
|
|
}
|
|
|
|
if (stepSizeTwentyFivePercentText != null) {
|
|
stepSizeTwentyFivePercentText
|
|
.setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE));
|
|
stepSizeTwentyFivePercentText
|
|
.setOnClickListener(view -> setStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE));
|
|
}
|
|
|
|
if (stepSizeOneHundredPercentText != null) {
|
|
stepSizeOneHundredPercentText
|
|
.setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE));
|
|
stepSizeOneHundredPercentText
|
|
.setOnClickListener(view -> setStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE));
|
|
}
|
|
}
|
|
|
|
private void setStepSize(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(final SeekBar seekBar, final int progress,
|
|
final boolean fromUser) {
|
|
final double currentTempo = strategy.valueOf(progress);
|
|
if (fromUser) {
|
|
onTempoSliderUpdated(currentTempo);
|
|
setCurrentPlaybackParameters();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStartTrackingTouch(final SeekBar seekBar) {
|
|
// Do Nothing.
|
|
}
|
|
|
|
@Override
|
|
public void onStopTrackingTouch(final SeekBar seekBar) {
|
|
// Do Nothing.
|
|
}
|
|
};
|
|
}
|
|
|
|
private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
|
|
return new SeekBar.OnSeekBarChangeListener() {
|
|
@Override
|
|
public void onProgressChanged(final SeekBar seekBar, final int progress,
|
|
final boolean fromUser) {
|
|
final double currentPitch = strategy.valueOf(progress);
|
|
if (fromUser) { // this change is first in chain
|
|
onPitchSliderUpdated(currentPitch);
|
|
setCurrentPlaybackParameters();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStartTrackingTouch(final SeekBar seekBar) {
|
|
// Do Nothing.
|
|
}
|
|
|
|
@Override
|
|
public void onStopTrackingTouch(final 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 newTempo, final double newPitch,
|
|
final boolean skipSilence) {
|
|
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Setting playback parameters to "
|
|
+ "tempo=[" + newTempo + "], "
|
|
+ "pitch=[" + newPitch + "]");
|
|
}
|
|
|
|
tempoCurrentText.setText(PlayerHelper.formatSpeed(newTempo));
|
|
pitchCurrentText.setText(PlayerHelper.formatPitch(newPitch));
|
|
callback.onPlaybackParameterChanged((float) newTempo, (float) newPitch, 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);
|
|
}
|
|
|
|
public interface Callback {
|
|
void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
|
|
boolean playbackSkipSilence);
|
|
}
|
|
}
|