Change pitch by semitones

This commit is contained in:
martin 2021-12-21 18:17:48 +01:00
parent 96eb1425f8
commit 5514616372
4 changed files with 360 additions and 11 deletions

View File

@ -9,6 +9,7 @@ import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
@ -37,6 +38,7 @@ public class PlaybackParameterDialog extends DialogFragment {
private static final double DEFAULT_TEMPO = 1.00f;
private static final double DEFAULT_PITCH = 1.00f;
private static final int DEFAULT_SEMITONES = 0;
private static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE;
private static final boolean DEFAULT_SKIP_SILENCE = false;
@ -64,9 +66,11 @@ public class PlaybackParameterDialog extends DialogFragment {
private double initialTempo = DEFAULT_TEMPO;
private double initialPitch = DEFAULT_PITCH;
private int initialSemitones = DEFAULT_SEMITONES;
private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE;
private double tempo = DEFAULT_TEMPO;
private double pitch = DEFAULT_PITCH;
private int semitones = DEFAULT_SEMITONES;
private double stepSize = DEFAULT_STEP;
@Nullable
@ -78,6 +82,8 @@ public class PlaybackParameterDialog extends DialogFragment {
@Nullable
private TextView tempoStepUpText;
@Nullable
private RelativeLayout pitchControl;
@Nullable
private SeekBar pitchSlider;
@Nullable
private TextView pitchCurrentText;
@ -86,9 +92,23 @@ public class PlaybackParameterDialog extends DialogFragment {
@Nullable
private TextView pitchStepUpText;
@Nullable
private RelativeLayout semitoneControl;
@Nullable
private SeekBar semitoneSlider;
@Nullable
private TextView semitoneCurrentText;
@Nullable
private TextView semitoneStepDownText;
@Nullable
private TextView semitoneStepUpText;
@Nullable
private View separatorStepSizeSelector;
@Nullable
private CheckBox unhookingCheckbox;
@Nullable
private CheckBox skipSilenceCheckbox;
@Nullable
private CheckBox adjustBySemitonesCheckbox;
public static PlaybackParameterDialog newInstance(final double playbackTempo,
final double playbackPitch,
@ -101,6 +121,7 @@ public class PlaybackParameterDialog extends DialogFragment {
dialog.tempo = playbackTempo;
dialog.pitch = playbackPitch;
dialog.semitones = dialog.percentToSemitones(playbackPitch);
dialog.initialSkipSilence = playbackSkipSilence;
return dialog;
@ -127,9 +148,11 @@ public class PlaybackParameterDialog extends DialogFragment {
if (savedInstanceState != null) {
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
initialSemitones = percentToSemitones(initialPitch);
tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO);
pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH);
semitones = percentToSemitones(pitch);
stepSize = savedInstanceState.getDouble(STEP_SIZE_KEY, DEFAULT_STEP);
}
}
@ -160,9 +183,11 @@ public class PlaybackParameterDialog extends DialogFragment {
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence))
setPlaybackParameters(initialTempo, initialPitch,
initialSemitones, initialSkipSilence))
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE))
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH,
DEFAULT_SEMITONES, DEFAULT_SKIP_SILENCE))
.setPositiveButton(R.string.ok, (dialogInterface, i) ->
setCurrentPlaybackParameters());
@ -176,12 +201,47 @@ public class PlaybackParameterDialog extends DialogFragment {
private void setupControlViews(@NonNull final View rootView) {
setupHookingControl(rootView);
setupSkipSilenceControl(rootView);
setupAdjustBySemitonesControl(rootView);
setupTempoControl(rootView);
setupPitchControl(rootView);
setupSemitoneControl(rootView);
togglePitchSliderType(rootView);
setStepSize(stepSize);
setupStepSizeSelector(rootView);
}
private void togglePitchSliderType(@NonNull final View rootView) {
pitchControl = rootView.findViewById(R.id.pitchControl);
semitoneControl = rootView.findViewById(R.id.semitoneControl);
separatorStepSizeSelector = rootView.findViewById(R.id.separatorStepSizeSelector);
final RelativeLayout.LayoutParams params =
(RelativeLayout.LayoutParams) separatorStepSizeSelector.getLayoutParams();
if (pitchControl != null && semitoneControl != null && unhookingCheckbox != null) {
if (getCurrentAdjustBySemitones()) {
// replaces pitchControl slider with semitoneControl slider
pitchControl.setVisibility(View.GONE);
semitoneControl.setVisibility(View.VISIBLE);
params.addRule(RelativeLayout.BELOW, R.id.semitoneControl);
// forces unhook for semitones
unhookingCheckbox.setChecked(true);
unhookingCheckbox.setEnabled(false);
setupTempoStepSizeSelector(rootView);
} else {
semitoneControl.setVisibility(View.GONE);
pitchControl.setVisibility(View.VISIBLE);
params.addRule(RelativeLayout.BELOW, R.id.pitchControl);
// (re)enables hooking selection
unhookingCheckbox.setEnabled(true);
setupCombinedStepSizeSelector(rootView);
}
}
}
private void setupTempoControl(@NonNull final View rootView) {
@ -234,23 +294,40 @@ public class PlaybackParameterDialog extends DialogFragment {
}
}
private void setupSemitoneControl(@NonNull final View rootView) {
semitoneSlider = rootView.findViewById(R.id.semitoneSeekbar);
semitoneCurrentText = rootView.findViewById(R.id.semitoneCurrentText);
semitoneStepDownText = rootView.findViewById(R.id.semitoneStepDown);
semitoneStepUpText = rootView.findViewById(R.id.semitoneStepUp);
if (semitoneCurrentText != null) {
semitoneCurrentText.setText(getSignedSemitonesString(semitones));
}
if (semitoneSlider != null) {
setSemitoneSlider(semitones);
semitoneSlider.setOnSeekBarChangeListener(getOnSemitoneChangedListener());
}
}
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
// restores 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
// saves 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
// when unchecked, slides back to the minimum of current tempo or pitch
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
setSliders(minimum);
setCurrentPlaybackParameters();
@ -268,6 +345,42 @@ public class PlaybackParameterDialog extends DialogFragment {
}
}
private void setupAdjustBySemitonesControl(@NonNull final View rootView) {
adjustBySemitonesCheckbox = rootView.findViewById(R.id.adjustBySemitonesCheckbox);
if (adjustBySemitonesCheckbox != null) {
// restores whether semitone adjustment is used or not
adjustBySemitonesCheckbox.setChecked(PreferenceManager
.getDefaultSharedPreferences(requireContext())
.getBoolean(getString(R.string.playback_adjust_by_semitones_key), true));
// stores whether semitone adjustment is used or not
adjustBySemitonesCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit()
.putBoolean(getString(R.string.playback_adjust_by_semitones_key), isChecked)
.apply();
togglePitchSliderType(rootView);
if (isChecked) {
setPlaybackParameters(
getCurrentTempo(),
getCurrentPitch(),
percentToSemitones(getCurrentPitch()),
getCurrentSkipSilence()
);
setSemitoneSlider(percentToSemitones(getCurrentPitch()));
} else {
setPlaybackParameters(
getCurrentTempo(),
semitonesToPercent(getCurrentSemitones()),
getCurrentSemitones(),
getCurrentSkipSilence()
);
setPitchSlider(semitonesToPercent(getCurrentSemitones()));
}
});
}
}
private void setupStepSizeSelector(@NonNull final View rootView) {
final TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent);
final TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent);
@ -310,6 +423,22 @@ public class PlaybackParameterDialog extends DialogFragment {
}
}
private void setupTempoStepSizeSelector(@NonNull final View rootView) {
final TextView playbackStepTypeText = rootView.findViewById(R.id.playback_step_type);
if (playbackStepTypeText != null) {
playbackStepTypeText.setText(R.string.playback_tempo_step);
}
setupStepSizeSelector(rootView);
}
private void setupCombinedStepSizeSelector(@NonNull final View rootView) {
final TextView playbackStepTypeText = rootView.findViewById(R.id.playback_step_type);
if (playbackStepTypeText != null) {
playbackStepTypeText.setText(R.string.playback_step);
}
setupStepSizeSelector(rootView);
}
private void setStepSize(final double stepSize) {
this.stepSize = stepSize;
@ -344,6 +473,20 @@ public class PlaybackParameterDialog extends DialogFragment {
setCurrentPlaybackParameters();
});
}
if (semitoneStepDownText != null) {
semitoneStepDownText.setOnClickListener(view -> {
onSemitoneSliderUpdated(getCurrentSemitones() - 1);
setCurrentPlaybackParameters();
});
}
if (semitoneStepUpText != null) {
semitoneStepUpText.setOnClickListener(view -> {
onSemitoneSliderUpdated(getCurrentSemitones() + 1);
setCurrentPlaybackParameters();
});
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -398,6 +541,33 @@ public class PlaybackParameterDialog extends DialogFragment {
};
}
private SeekBar.OnSeekBarChangeListener getOnSemitoneChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
// semitone slider supplies values 0 to 25, subtraction by 12 is required
final int currentSemitones = progress - 12;
if (fromUser) { // this change is first in chain
onSemitoneSliderUpdated(currentSemitones);
// line below also saves semitones as pitch percentages
onPitchSliderUpdated(semitonesToPercent(currentSemitones));
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;
@ -420,6 +590,13 @@ public class PlaybackParameterDialog extends DialogFragment {
}
}
private void onSemitoneSliderUpdated(final int newSemitone) {
if (unhookingCheckbox == null) {
return;
}
setSemitoneSlider(newSemitone);
}
private void setSliders(final double newValue) {
setTempoSlider(newValue);
setPitchSlider(newValue);
@ -439,25 +616,49 @@ public class PlaybackParameterDialog extends DialogFragment {
pitchSlider.setProgress(strategy.progressOf(newPitch));
}
private void setSemitoneSlider(final int newSemitone) {
if (semitoneSlider == null) {
return;
}
semitoneSlider.setProgress(newSemitone + 12);
}
/*//////////////////////////////////////////////////////////////////////////
// Helper
//////////////////////////////////////////////////////////////////////////*/
private void setCurrentPlaybackParameters() {
setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence());
if (getCurrentAdjustBySemitones()) {
setPlaybackParameters(
getCurrentTempo(),
semitonesToPercent(getCurrentSemitones()),
getCurrentSemitones(),
getCurrentSkipSilence()
);
} else {
setPlaybackParameters(
getCurrentTempo(),
getCurrentPitch(),
percentToSemitones(getCurrentPitch()),
getCurrentSkipSilence()
);
}
}
private void setPlaybackParameters(final double newTempo, final double newPitch,
final boolean skipSilence) {
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
final int newSemitones, final boolean skipSilence) {
if (callback != null && tempoCurrentText != null
&& pitchCurrentText != null && semitoneCurrentText != null) {
if (DEBUG) {
Log.d(TAG, "Setting playback parameters to "
+ "tempo=[" + newTempo + "], "
+ "pitch=[" + newPitch + "]");
+ "pitch=[" + newPitch + "], "
+ "semitones=[" + newSemitones + "]");
}
tempoCurrentText.setText(PlayerHelper.formatSpeed(newTempo));
pitchCurrentText.setText(PlayerHelper.formatPitch(newPitch));
semitoneCurrentText.setText(getSignedSemitonesString(newSemitones));
callback.onPlaybackParameterChanged((float) newTempo, (float) newPitch, skipSilence);
}
}
@ -470,6 +671,11 @@ public class PlaybackParameterDialog extends DialogFragment {
return pitchSlider == null ? pitch : strategy.valueOf(pitchSlider.getProgress());
}
private int getCurrentSemitones() {
// semitoneSlider is absolute, that's why - 12
return semitoneSlider == null ? semitones : semitoneSlider.getProgress() - 12;
}
private double getCurrentStepSize() {
return stepSize;
}
@ -478,6 +684,10 @@ public class PlaybackParameterDialog extends DialogFragment {
return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked();
}
private boolean getCurrentAdjustBySemitones() {
return adjustBySemitonesCheckbox != null && adjustBySemitonesCheckbox.isChecked();
}
@NonNull
private static String getStepUpPercentString(final double percent) {
return STEP_UP_SIGN + getPercentString(percent);
@ -493,8 +703,21 @@ public class PlaybackParameterDialog extends DialogFragment {
return PlayerHelper.formatPitch(percent);
}
@NonNull
private static String getSignedSemitonesString(final int semitones) {
return semitones > 0 ? "+" + semitones : "" + semitones;
}
public interface Callback {
void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
boolean playbackSkipSilence);
}
public double semitonesToPercent(final int inSemitones) {
return Math.pow(2, inSemitones / 12.0);
}
public int percentToSemitones(final double inPercent) {
return (int) Math.round(12 * Math.log(inPercent) / Math.log(2));
}
}

View File

@ -261,11 +261,121 @@
tools:text="+5%" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/semitoneControl"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_below="@id/pitchControlText"
android:layout_marginTop="4dp"
android:orientation="horizontal">
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/semitoneStepDown"
android:layout_width="24dp"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:text="♭"
android:textColor="?attr/colorAccent"
android:textSize="24sp"
android:textStyle="bold"
tools:ignore="HardcodedText"
tools:text="♭" />
<RelativeLayout
android:id="@+id/semitoneDisplay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toStartOf="@+id/semitoneStepUp"
android:layout_toLeftOf="@+id/semitoneStepUp"
android:layout_toEndOf="@+id/semitoneStepDown"
android:layout_toRightOf="@+id/semitoneStepDown"
android:orientation="horizontal">
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/semitoneMinimumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:gravity="center"
android:text="-12"
android:textColor="?attr/colorAccent"
tools:ignore="HardcodedText"
tools:text="-12" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/semitoneCurrentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:gravity="center"
android:text="--"
android:textColor="?attr/colorAccent"
android:textStyle="bold"
tools:ignore="HardcodedText"
tools:text="0" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/semitoneMaximumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:gravity="center"
android:text="+12"
android:textColor="?attr/colorAccent"
tools:ignore="HardcodedText"
tools:text="+12" />
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/semitoneSeekbar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/semitoneCurrentText"
android:max="24"
android:paddingBottom="4dp"
android:progress="12" />
</RelativeLayout>
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/semitoneStepUp"
android:layout_width="24dp"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:text="♯"
android:textColor="?attr/colorAccent"
android:textSize="20sp"
android:textStyle="bold"
tools:ignore="HardcodedText"
tools:text="♯" />
</RelativeLayout>
<View
android:id="@+id/separatorStepSizeSelector"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/pitchControl"
android:layout_below="@+id/semitoneControl"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="12dp"
@ -280,6 +390,7 @@
android:orientation="horizontal">
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/playback_step_type"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
@ -380,6 +491,18 @@
android:clickable="true"
android:focusable="true"
android:text="@string/skip_silence_checkbox" />
<CheckBox
android:id="@+id/adjustBySemitonesCheckbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/skipSilenceCheckbox"
android:layout_centerHorizontal="true"
android:checked="false"
android:clickable="true"
android:focusable="true"
android:maxLines="1"
android:text="@string/adjust_by_semitones_checkbox" />
</LinearLayout>
<!-- END HERE -->

View File

@ -258,6 +258,7 @@
<string name="enable_playback_resume_key" translatable="false">enable_playback_resume</string>
<string name="enable_playback_state_lists_key" translatable="false">enable_playback_state_lists</string>
<string name="playback_unhook_key" translatable="false">playback_unhook_key</string>
<string name="playback_adjust_by_semitones_key" translatable="false">playback_adjust_by_semitones_key</string>
<string name="playback_speed_key" translatable="false">playback_speed_key</string>
<string name="playback_pitch_key" translatable="false">playback_pitch_key</string>
<string name="playback_skip_silence_key" translatable="false">playback_skip_silence_key</string>

View File

@ -503,7 +503,9 @@
<string name="playback_pitch">Pitch</string>
<string name="unhook_checkbox">Unhook (may cause distortion)</string>
<string name="skip_silence_checkbox">Fast-forward during silence</string>
<string name="adjust_by_semitones_checkbox">Adjust pitch by musical semitones</string>
<string name="playback_step">Step</string>
<string name="playback_tempo_step">Tempo step</string>
<string name="playback_reset">Reset</string>
<!-- GDPR dialog -->
<string name="start_accept_privacy_policy">In order to comply with the European General Data Protection Regulation (GDPR), we herby draw your attention to NewPipe\'s privacy policy. Please read it carefully.\nYou must accept it to send us the bug report.</string>