From e885822a3484d5404fe6cce51cb0eb87cadccf4f Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Wed, 21 Mar 2018 00:11:54 -0700 Subject: [PATCH] -Added playback speed control dialog to allow full user control over player tempo and pitch parameters. -Changed tempo and pitch button in service player activity and tempo button in main video player to open speed control dialog. -Changed LIVE button to be no longer clickable when player position is at or beyond default position. -Changed main video player to use AppCompatActivity rather than Activity. -Fixed video player tempo button not updating when player speed parameters change. -Fixed player crashing on lower sdk versions due to no MediaButtonReceiver, added intent back to manifest. -Fixed inconsistent gradle library naming. -Fixed stetho dependencies incorrect version. --- app/build.gradle | 23 +- app/src/main/AndroidManifest.xml | 6 + .../org/schabi/newpipe/player/BasePlayer.java | 21 +- .../newpipe/player/MainVideoPlayer.java | 22 +- .../newpipe/player/ServicePlayerActivity.java | 66 +--- .../schabi/newpipe/player/VideoPlayer.java | 10 +- .../helper/PlaybackParameterDialog.java | 348 ++++++++++++++++++ .../res/layout/dialog_playback_parameter.xml | 313 ++++++++++++++++ app/src/main/res/values/strings.xml | 8 + 9 files changed, 755 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java create mode 100644 app/src/main/res/layout/dialog_playback_parameter.xml 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