diff --git a/app/build.gradle b/app/build.gradle
index 9b2569a66..5c434c30c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -51,9 +51,10 @@ ext {
supportLibVersion = '27.1.0'
exoPlayerLibVersion = '2.7.1'
roomDbLibVersion = '1.0.0'
- leakCanaryVersion = '1.5.4'
- okHttpVersion = '1.5.0'
- icepickVersion = '3.2.0'
+ leakCanaryLibVersion = '1.5.4'
+ okHttpLibVersion = '1.5.0'
+ icepickLibVersion = '3.2.0'
+ stethoLibVersion = '1.5.0'
}
dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
@@ -81,8 +82,8 @@ dependencies {
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
- debugImplementation "com.facebook.stetho:stetho:$okHttpVersion"
- debugImplementation "com.facebook.stetho:stetho-urlconnection:$okHttpVersion"
+ debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion"
+ debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
debugImplementation 'com.android.support:multidex:1.0.3'
implementation 'io.reactivex.rxjava2:rxjava:2.1.10'
@@ -93,13 +94,13 @@ dependencies {
implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion"
annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion"
- implementation "frankiesardo:icepick:$icepickVersion"
- annotationProcessor "frankiesardo:icepick-processor:$icepickVersion"
+ implementation "frankiesardo:icepick:$icepickLibVersion"
+ annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion"
- debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion"
- betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
- releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
+ debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
+ betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
+ releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
- debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpVersion"
+ debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1be8c1f2c..1edd67d24 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,6 +28,12 @@
+
+
+
+
+
+
= currentTimeline.getWindowCount()) {
+ return false;
+ }
+
+ Timeline.Window timelineWindow = new Timeline.Window();
+ currentTimeline.getWindow(currentWindowIndex, timelineWindow);
+ return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition();
+ }
+
public boolean isPlaying() {
final int state = simpleExoPlayer.getPlaybackState();
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
index 90a4a8c9f..cbc4b8230 100644
--- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
@@ -19,7 +19,6 @@
package org.schabi.newpipe.player;
-import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -33,6 +32,7 @@ import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.DisplayMetrics;
@@ -49,6 +49,7 @@ import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView;
@@ -57,6 +58,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
+import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
@@ -87,7 +89,8 @@ import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
*
* @author mauriciocolli
*/
-public final class MainVideoPlayer extends Activity implements StateSaver.WriteRead {
+public final class MainVideoPlayer extends AppCompatActivity
+ implements StateSaver.WriteRead, PlaybackParameterDialog.Callback {
private static final String TAG = ".MainVideoPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
@@ -340,6 +343,15 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
}
}
+ ////////////////////////////////////////////////////////////////////////////
+ // Playback Parameters Listener
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
+ if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch);
+ }
+
///////////////////////////////////////////////////////////////////////////
@SuppressWarnings({"unused", "WeakerAccess"})
@@ -630,6 +642,12 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
showControlsThenHide();
}
+ @Override
+ public void onPlaybackSpeedClicked() {
+ PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch())
+ .show(getSupportFragmentManager(), TAG);
+ }
+
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar);
diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
index 50248891b..1f850944d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
@@ -31,6 +31,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
+import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
@@ -43,7 +44,8 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
public abstract class ServicePlayerActivity extends AppCompatActivity
- implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
+ implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
+ View.OnClickListener, PlaybackParameterDialog.Callback {
private boolean serviceBound;
private ServiceConnection serviceConnection;
@@ -57,8 +59,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
- private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61;
- private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
@@ -85,9 +85,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private ProgressBar progressBar;
private TextView playbackSpeedButton;
- private PopupMenu playbackSpeedPopupMenu;
private TextView playbackPitchButton;
- private PopupMenu playbackPitchPopupMenu;
////////////////////////////////////////////////////////////////////////////
// Abstracts
@@ -317,45 +315,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
shuffleButton.setOnClickListener(this);
playbackSpeedButton.setOnClickListener(this);
playbackPitchButton.setOnClickListener(this);
-
- playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton);
- playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton);
- buildPlaybackSpeedMenu();
- buildPlaybackPitchMenu();
- }
-
- private void buildPlaybackSpeedMenu() {
- if (playbackSpeedPopupMenu == null) return;
-
- playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID);
- for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) {
- final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i];
- final String formattedSpeed = formatSpeed(playbackSpeed);
- final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed);
- item.setOnMenuItemClickListener(menuItem -> {
- if (player == null) return false;
-
- player.setPlaybackSpeed(playbackSpeed);
- return true;
- });
- }
- }
-
- private void buildPlaybackPitchMenu() {
- if (playbackPitchPopupMenu == null) return;
-
- playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID);
- for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) {
- final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i];
- final String formattedPitch = formatPitch(playbackPitch);
- final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch);
- item.setOnMenuItemClickListener(menuItem -> {
- if (player == null) return false;
-
- player.setPlaybackPitch(playbackPitch);
- return true;
- });
- }
}
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
@@ -474,10 +433,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
player.onShuffleClicked();
} else if (view.getId() == playbackSpeedButton.getId()) {
- playbackSpeedPopupMenu.show();
+ PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
+ player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
} else if (view.getId() == playbackPitchButton.getId()) {
- playbackPitchPopupMenu.show();
+ PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
+ player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
} else if (view.getId() == metadata.getId()) {
scrollToSelected();
@@ -488,6 +449,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
}
+ ////////////////////////////////////////////////////////////////////////////
+ // Playback Parameters Listener
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
+ if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);
+ }
+
////////////////////////////////////////////////////////////////////////////
// Seekbar Listener
////////////////////////////////////////////////////////////////////////////
@@ -539,6 +509,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
progressSeekBar.setProgress(currentProgress);
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
}
+
+ if (player != null) {
+ progressLiveSync.setClickable(!player.isLiveEdge());
+ }
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
index aa896bb69..b019ea91e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
@@ -49,6 +49,7 @@ import android.widget.TextView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
@@ -523,6 +524,12 @@ public abstract class VideoPlayer extends BasePlayer
onTextTrackUpdate();
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ super.onPlaybackParametersChanged(playbackParameters);
+ playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed));
+ }
+
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (DEBUG) {
@@ -615,6 +622,7 @@ public abstract class VideoPlayer extends BasePlayer
if (DEBUG && bufferPercent % 20 == 0) { //Limit log
Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
}
+ playbackLiveSync.setClickable(!isLiveEdge());
}
@Override
@@ -718,7 +726,7 @@ public abstract class VideoPlayer extends BasePlayer
wasPlaying = simpleExoPlayer.getPlayWhenReady();
}
- private void onPlaybackSpeedClicked() {
+ public void onPlaybackSpeedClicked() {
if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called");
playbackSpeedPopupMenu.show();
isSomePopupMenuVisible = true;
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
new file mode 100644
index 000000000..8a0a8a86c
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -0,0 +1,348 @@
+package org.schabi.newpipe.player.helper;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.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 static org.schabi.newpipe.player.BasePlayer.DEBUG;
+
+public class PlaybackParameterDialog extends DialogFragment {
+ private static final String TAG = "PlaybackParameterDialog";
+
+ public static final float MINIMUM_PLAYBACK_VALUE = 0.25f;
+ public static final float MAXIMUM_PLAYBACK_VALUE = 3.00f;
+
+ public static final String STEP_UP_SIGN = "+";
+ public static final String STEP_DOWN_SIGN = "-";
+ public static final float PLAYBACK_STEP_VALUE = 0.05f;
+
+ public static final float NIGHTCORE_TEMPO = 1.20f;
+ public static final float NIGHTCORE_PITCH_LOWER = 1.15f;
+ public static final float NIGHTCORE_PITCH_UPPER = 1.25f;
+
+ public static final float DEFAULT_TEMPO = 1.00f;
+ public static final float DEFAULT_PITCH = 1.00f;
+
+ private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
+ private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
+
+ public interface Callback {
+ void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch);
+ }
+
+ private Callback callback;
+
+ private float initialTempo = DEFAULT_TEMPO;
+ private float initialPitch = DEFAULT_PITCH;
+
+ private SeekBar tempoSlider;
+ private TextView tempoMinimumText;
+ private TextView tempoMaximumText;
+ private TextView tempoCurrentText;
+ private TextView tempoStepDownText;
+ private TextView tempoStepUpText;
+
+ private SeekBar pitchSlider;
+ private TextView pitchMinimumText;
+ private TextView pitchMaximumText;
+ private TextView pitchCurrentText;
+ private TextView pitchStepDownText;
+ private TextView pitchStepUpText;
+
+ private CheckBox unhookingCheckbox;
+
+ private TextView nightCorePresetText;
+ private TextView resetPresetText;
+
+ public static PlaybackParameterDialog newInstance(final float playbackTempo,
+ final float playbackPitch) {
+ PlaybackParameterDialog dialog = new PlaybackParameterDialog();
+ dialog.initialTempo = playbackTempo;
+ dialog.initialPitch = playbackPitch;
+ return dialog;
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Lifecycle
+ //////////////////////////////////////////////////////////////////////////*/
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (context != null && context instanceof Callback) {
+ callback = (Callback) context;
+ } else {
+ dismiss();
+ }
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ initialTempo = savedInstanceState.getFloat(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
+ initialPitch = savedInstanceState.getFloat(INITIAL_PITCH_KEY, DEFAULT_PITCH);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putFloat(INITIAL_TEMPO_KEY, initialTempo);
+ outState.putFloat(INITIAL_PITCH_KEY, initialPitch);
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Dialog
+ //////////////////////////////////////////////////////////////////////////*/
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
+ setupView(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))
+ .setPositiveButton(R.string.finish, (dialogInterface, i) ->
+ setPlaybackParameters(getCurrentTempo(), getCurrentPitch()));
+
+ return dialogBuilder.create();
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Dialog Builder
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private void setupView(@NonNull View rootView) {
+ setupHookingControl(rootView);
+ setupTempoControl(rootView);
+ setupPitchControl(rootView);
+ setupPresetControl(rootView);
+ }
+
+ private void setupTempoControl(@NonNull View rootView) {
+ tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
+ tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
+ tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText);
+ tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText);
+ tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
+ tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
+
+ tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo));
+ tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
+ tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
+
+ tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
+ tempoStepUpText.setOnClickListener(view ->
+ setTempo(getCurrentTempo() + PLAYBACK_STEP_VALUE));
+
+ tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
+ tempoStepDownText.setOnClickListener(view ->
+ setTempo(getCurrentTempo() - PLAYBACK_STEP_VALUE));
+
+ tempoSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE));
+ tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialTempo));
+ tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
+ }
+
+ private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
+ return new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ final float currentTempo = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress);
+ if (fromUser) { // this change is first in chain
+ setTempo(currentTempo);
+ } else {
+ setPlaybackParameters(currentTempo, getCurrentPitch());
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // Do Nothing.
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // Do Nothing.
+ }
+ };
+ }
+
+ private void setupPitchControl(@NonNull View rootView) {
+ pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
+ pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
+ pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText);
+ pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText);
+ pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
+ pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
+
+ pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch));
+ pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
+ pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
+
+ pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
+ pitchStepUpText.setOnClickListener(view ->
+ setPitch(getCurrentPitch() + PLAYBACK_STEP_VALUE));
+
+ pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
+ pitchStepDownText.setOnClickListener(view ->
+ setPitch(getCurrentPitch() - PLAYBACK_STEP_VALUE));
+
+ pitchSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE));
+ pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialPitch));
+ pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
+ }
+
+ private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
+ return new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ final float currentPitch = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress);
+ if (fromUser) { // this change is first in chain
+ setPitch(currentPitch);
+ } else {
+ setPlaybackParameters(getCurrentTempo(), currentPitch);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // Do Nothing.
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // Do Nothing.
+ }
+ };
+ }
+
+ private void setupHookingControl(@NonNull View rootView) {
+ unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
+ unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
+ if (isChecked) return;
+ // When unchecked, slide back to the minimum of current tempo or pitch
+ final float minimum = Math.min(getCurrentPitch(), getCurrentTempo());
+ setSliders(minimum);
+ });
+ }
+
+ private void setupPresetControl(@NonNull View rootView) {
+ nightCorePresetText = rootView.findViewById(R.id.presetNightcore);
+ nightCorePresetText.setOnClickListener(view -> {
+ final float randomPitch = NIGHTCORE_PITCH_LOWER +
+ (float) Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER);
+
+ setTempoSlider(NIGHTCORE_TEMPO);
+ setPitchSlider(randomPitch);
+ });
+
+ resetPresetText = rootView.findViewById(R.id.presetReset);
+ resetPresetText.setOnClickListener(view -> {
+ setTempoSlider(DEFAULT_TEMPO);
+ setPitchSlider(DEFAULT_PITCH);
+ });
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Helper
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private void setTempo(final float newTempo) {
+ if (unhookingCheckbox == null) return;
+ if (!unhookingCheckbox.isChecked()) {
+ setSliders(newTempo);
+ } else {
+ setTempoSlider(newTempo);
+ }
+ }
+
+ private void setPitch(final float newPitch) {
+ if (unhookingCheckbox == null) return;
+ if (!unhookingCheckbox.isChecked()) {
+ setSliders(newPitch);
+ } else {
+ setPitchSlider(newPitch);
+ }
+ }
+
+ private void setSliders(final float newValue) {
+ setTempoSlider(newValue);
+ setPitchSlider(newValue);
+ }
+
+ private void setTempoSlider(final float newTempo) {
+ if (tempoSlider == null) return;
+ // seekbar doesn't register progress if it is the same as the existing progress
+ tempoSlider.setProgress(Integer.MAX_VALUE);
+ tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newTempo));
+ }
+
+ private void setPitchSlider(final float newPitch) {
+ if (pitchSlider == null) return;
+ pitchSlider.setProgress(Integer.MAX_VALUE);
+ pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newPitch));
+ }
+
+ private void setPlaybackParameters(final float tempo, final float pitch) {
+ 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(tempo, pitch);
+ }
+ }
+
+ private float getCurrentTempo() {
+ return tempoSlider == null ? initialTempo : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE,
+ tempoSlider.getProgress());
+ }
+
+ private float getCurrentPitch() {
+ return pitchSlider == null ? initialPitch : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE,
+ pitchSlider.getProgress());
+ }
+
+ /**
+ * Converts from zeroed float with a minimum offset to the nearest rounded slider
+ * equivalent integer
+ * */
+ private static int getSliderEquivalent(final float minimumValue, final float floatValue) {
+ return Math.round((floatValue - minimumValue) * 100f);
+ }
+
+ /**
+ * Converts from slider integer value to an equivalent float value with a given minimum offset
+ * */
+ private static float getSliderEquivalent(final float minimumValue, final int intValue) {
+ return ((float) intValue) / 100f + minimumValue;
+ }
+
+ private static String getStepUpPercentString(final float percent) {
+ return STEP_UP_SIGN + PlayerHelper.formatPitch(percent);
+ }
+
+ private static String getStepDownPercentString(final float percent) {
+ return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent);
+ }
+}
diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml
new file mode 100644
index 000000000..a8c6a5dcd
--- /dev/null
+++ b/app/src/main/res/layout/dialog_playback_parameter.xml
@@ -0,0 +1,313 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cd280ff02..effdeaaba 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -456,4 +456,12 @@
yourid, soundcloud.com/yourid
Keep in mind that this operation can be network expensive.\n\nDo you want to continue?
+
+
+ Playback Speed Control
+ Tempo
+ Pitch
+ Unhook (may cause distortion)
+ Nightcore
+ Default