diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java
index e39cd16743..79e0bcd693 100644
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java
+++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java
@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
@@ -99,6 +100,12 @@ import java.util.Locale;
Log.d(TAG, "positionDiscontinuity");
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ Log.d(TAG, "playbackParameters " + String.format(
+ "[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch));
+ }
+
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
int periodCount = timeline.getPeriodCount();
diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
index adb04eaa24..15433af92d 100644
--- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
+++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
@@ -34,6 +34,7 @@ import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
@@ -427,6 +428,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing.
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
index 990c470a93..21f01f0cca 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
@@ -22,6 +22,7 @@ import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
@@ -102,6 +103,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
// Do nothing.
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing.
diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
index 3e07186995..263934d982 100644
--- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
+++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
@@ -22,6 +22,7 @@ import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
@@ -102,6 +103,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
// Do nothing.
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing.
diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
index f888554e22..2647776b74 100644
--- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
+++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
@@ -23,6 +23,7 @@ import android.util.Log;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
@@ -134,6 +135,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
// Do nothing.
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing.
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
index 93c0a7dc11..daa845298b 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
@@ -140,6 +140,16 @@ public final class ExoPlayerTest extends TestCase {
return isCurrentStreamFinal() ? 60000030 : 60000000;
}
+ @Override
+ public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ return PlaybackParameters.DEFAULT;
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return PlaybackParameters.DEFAULT;
+ }
+
@Override
public boolean isEnded() {
// Allow playback to end once the final period is playing.
@@ -272,6 +282,11 @@ public final class ExoPlayerTest extends TestCase {
positionDiscontinuityCount++;
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
}
private static final class TimelineWindowDefinition {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java
index 6a1db191a0..29f8220037 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/C.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java
@@ -479,15 +479,6 @@ public final class C {
*/
public static final int MSG_SET_VOLUME = 2;
- /**
- * A type of a message that can be passed to an audio {@link Renderer} via
- * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
- * should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the
- * underlying {@link android.media.AudioTrack}. The message object should not be modified by the
- * caller after it has been passed.
- */
- public static final int MSG_SET_PLAYBACK_PARAMS = 3;
-
/**
* A type of a message that can be passed to an audio {@link Renderer} via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
@@ -500,7 +491,7 @@ public final class C {
* introduce a brief gap in audio output. Note also that tracks in the same audio session must
* share the same routing, so a new audio session id will be generated.
*/
- public static final int MSG_SET_STREAM_TYPE = 4;
+ public static final int MSG_SET_STREAM_TYPE = 3;
/**
* The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}
@@ -510,7 +501,7 @@ public final class C {
* Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is
* owned by a {@link android.view.SurfaceView}.
*/
- public static final int MSG_SET_SCALING_MODE = 5;
+ public static final int MSG_SET_SCALING_MODE = 4;
/**
* Applications or extensions may define custom {@code MSG_*} constants greater than or equal to
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index f7dbea851a..ab521e3733 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
@@ -168,6 +169,16 @@ public interface ExoPlayer {
*/
void onPositionDiscontinuity();
+ /**
+ * Called when the current playback parameters change. The playback parameters may change due to
+ * a call to {@link ExoPlayer#setPlaybackParameters(PlaybackParameters)}, or the player itself
+ * may change them (for example, if audio playback switches to passthrough mode, where speed
+ * adjustment is no longer possible).
+ *
+ * @param playbackParameters The playback parameters.
+ */
+ void onPlaybackParametersChanged(PlaybackParameters playbackParameters);
+
}
/**
@@ -340,6 +351,28 @@ public interface ExoPlayer {
*/
void seekTo(int windowIndex, long positionMs);
+ /**
+ * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the
+ * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment.
+ *
+ * Playback parameters changes may cause the player to buffer.
+ * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever
+ * the currently active playback parameters change. When that listener is called, the parameters
+ * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch
+ * may be out of range, in which case they are constrained to a set of permitted values. If it is
+ * not possible to change the playback parameters, the listener will not be invoked.
+ *
+ * @param playbackParameters The playback parameters, or {@code null} to use the defaults.
+ */
+ void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);
+
+ /**
+ * Returns the currently active playback parameters.
+ *
+ * @see EventListener#onPlaybackParametersChanged(PlaybackParameters)
+ */
+ PlaybackParameters getPlaybackParameters();
+
/**
* Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
* is to pause playback.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index d44d138091..7497ed0da9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -19,6 +19,7 @@ import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo;
@@ -57,6 +58,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private Object manifest;
private TrackGroupArray trackGroups;
private TrackSelectionArray trackSelections;
+ private PlaybackParameters playbackParameters;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
@@ -87,6 +89,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
period = new Timeline.Period();
trackGroups = TrackGroupArray.EMPTY;
trackSelections = emptyTrackSelections;
+ playbackParameters = PlaybackParameters.DEFAULT;
eventHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
@@ -196,6 +199,19 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
+ @Override
+ public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
+ if (playbackParameters == null) {
+ playbackParameters = PlaybackParameters.DEFAULT;
+ }
+ internalPlayer.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return playbackParameters;
+ }
+
@Override
public void stop() {
internalPlayer.stop();
@@ -376,6 +392,16 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
break;
}
+ case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: {
+ PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj;
+ if (!this.playbackParameters.equals(playbackParameters)) {
+ this.playbackParameters = playbackParameters;
+ for (EventListener listener : listeners) {
+ listener.onPlaybackParametersChanged(playbackParameters);
+ }
+ }
+ break;
+ }
case ExoPlayerImplInternal.MSG_ERROR: {
ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
for (EventListener listener : listeners) {
@@ -383,6 +409,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
break;
}
+ default:
+ throw new IllegalStateException();
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index e4c109e85b..f6fa0d39ac 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -96,20 +96,22 @@ import java.io.IOException;
public static final int MSG_SEEK_ACK = 4;
public static final int MSG_POSITION_DISCONTINUITY = 5;
public static final int MSG_SOURCE_INFO_REFRESHED = 6;
- public static final int MSG_ERROR = 7;
+ public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 7;
+ public static final int MSG_ERROR = 8;
// Internal messages
private static final int MSG_PREPARE = 0;
private static final int MSG_SET_PLAY_WHEN_READY = 1;
private static final int MSG_DO_SOME_WORK = 2;
private static final int MSG_SEEK_TO = 3;
- private static final int MSG_STOP = 4;
- private static final int MSG_RELEASE = 5;
- private static final int MSG_REFRESH_SOURCE_INFO = 6;
- private static final int MSG_PERIOD_PREPARED = 7;
- private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 8;
- private static final int MSG_TRACK_SELECTION_INVALIDATED = 9;
- private static final int MSG_CUSTOM = 10;
+ private static final int MSG_SET_PLAYBACK_PARAMETERS = 4;
+ private static final int MSG_STOP = 5;
+ private static final int MSG_RELEASE = 6;
+ private static final int MSG_REFRESH_SOURCE_INFO = 7;
+ private static final int MSG_PERIOD_PREPARED = 8;
+ private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9;
+ private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
+ private static final int MSG_CUSTOM = 11;
private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
@@ -143,6 +145,7 @@ import java.io.IOException;
private final Timeline.Period period;
private PlaybackInfo playbackInfo;
+ private PlaybackParameters playbackParameters;
private Renderer rendererMediaClockSource;
private MediaClock rendererMediaClock;
private MediaSource mediaSource;
@@ -188,6 +191,7 @@ import java.io.IOException;
window = new Timeline.Window();
period = new Timeline.Period();
trackSelector.init(this);
+ playbackParameters = PlaybackParameters.DEFAULT;
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
@@ -211,6 +215,10 @@ import java.io.IOException;
.sendToTarget();
}
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget();
+ }
+
public void stop() {
handler.sendEmptyMessage(MSG_STOP);
}
@@ -304,6 +312,10 @@ import java.io.IOException;
seekToInternal((SeekPosition) msg.obj);
return true;
}
+ case MSG_SET_PLAYBACK_PARAMETERS: {
+ setPlaybackParametersInternal((PlaybackParameters) msg.obj);
+ return true;
+ }
case MSG_STOP: {
stopInternal();
return true;
@@ -478,6 +490,19 @@ import java.io.IOException;
maybeThrowPeriodPrepareError();
}
+ // The standalone media clock never changes playback parameters, so just check the renderer.
+ if (rendererMediaClock != null) {
+ PlaybackParameters playbackParameters = rendererMediaClock.getPlaybackParameters();
+ if (!playbackParameters.equals(this.playbackParameters)) {
+ // TODO: Make LoadControl, period transition position projection, adaptive track selection
+ // and potentially any time-related code in renderers take into account the playback speed.
+ this.playbackParameters = playbackParameters;
+ standaloneMediaClock.synchronize(rendererMediaClock);
+ eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters)
+ .sendToTarget();
+ }
+ }
+
long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period)
.getDurationUs();
if (allRenderersEnded
@@ -646,6 +671,14 @@ import java.io.IOException;
}
}
+ private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
+ playbackParameters = rendererMediaClock != null
+ ? rendererMediaClock.setPlaybackParameters(playbackParameters)
+ : standaloneMediaClock.setPlaybackParameters(playbackParameters);
+ this.playbackParameters = playbackParameters;
+ eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget();
+ }
+
private void stopInternal() {
resetInternal(true);
loadControl.onStopped();
@@ -774,7 +807,7 @@ import java.io.IOException;
if (sampleStream == null) {
// The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take
// over timing responsibilities.
- standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
+ standaloneMediaClock.synchronize(rendererMediaClock);
}
rendererMediaClock = null;
rendererMediaClockSource = null;
@@ -1334,7 +1367,7 @@ import java.io.IOException;
// is final and it's not reading ahead.
if (renderer == rendererMediaClockSource) {
// Sync standaloneMediaClock so that it can take over timing responsibilities.
- standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
+ standaloneMediaClock.synchronize(rendererMediaClock);
rendererMediaClock = null;
rendererMediaClockSource = null;
}
@@ -1380,6 +1413,7 @@ import java.io.IOException;
}
rendererMediaClock = mediaClock;
rendererMediaClockSource = renderer;
+ rendererMediaClock.setPlaybackParameters(playbackParameters);
}
// Start the renderer if playing.
if (playing) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
new file mode 100644
index 0000000000..90aded7660
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+/**
+ * The parameters that apply to playback.
+ */
+public final class PlaybackParameters {
+
+ /**
+ * The default playback parameters: real-time playback with no pitch modification.
+ */
+ public static final PlaybackParameters DEFAULT = new PlaybackParameters(1f, 1f);
+
+ /**
+ * The factor by which playback will be sped up.
+ */
+ public final float speed;
+
+ /**
+ * The factor by which the audio pitch will be scaled.
+ */
+ public final float pitch;
+
+ private final int scaledUsPerMs;
+
+ /**
+ * Creates new playback parameters.
+ *
+ * @param speed The factor by which playback will be sped up.
+ * @param pitch The factor by which the audio pitch will be scaled.
+ */
+ public PlaybackParameters(float speed, float pitch) {
+ this.speed = speed;
+ this.pitch = pitch;
+ scaledUsPerMs = Math.round(speed * 1000f);
+ }
+
+ /**
+ * Scales the millisecond duration {@code timeMs} by the playback speed, returning the result in
+ * microseconds.
+ *
+ * @param timeMs The time to scale, in milliseconds.
+ * @return The scaled time, in microseconds.
+ */
+ public long getSpeedAdjustedDurationUs(long timeMs) {
+ return timeMs * scaledUsPerMs;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ PlaybackParameters other = (PlaybackParameters) obj;
+ return this.speed == other.speed && this.pitch == other.pitch;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + Float.floatToRawIntBits(speed);
+ result = 31 * result + Float.floatToRawIntBits(pitch);
+ return result;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index 3ce4937911..6ce6191905 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -22,6 +22,7 @@ import android.media.MediaCodec;
import android.media.PlaybackParams;
import android.os.Handler;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
@@ -145,7 +146,6 @@ public class SimpleExoPlayer implements ExoPlayer {
@C.StreamType
private int audioStreamType;
private float audioVolume;
- private PlaybackParamsHolder playbackParamsHolder;
protected SimpleExoPlayer(Context context, TrackSelector trackSelector, LoadControl loadControl,
DrmSessionManager drmSessionManager,
@@ -344,37 +344,20 @@ public class SimpleExoPlayer implements ExoPlayer {
/**
* Sets the {@link PlaybackParams} governing audio playback.
*
+ * @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}.
* @param params The {@link PlaybackParams}, or null to clear any previously set parameters.
*/
+ @Deprecated
@TargetApi(23)
- public void setPlaybackParams(PlaybackParams params) {
+ public void setPlaybackParams(@Nullable PlaybackParams params) {
+ PlaybackParameters playbackParameters;
if (params != null) {
- // The audio renderers will call this on the playback thread to ensure they can query
- // parameters without failure. We do the same up front, which is redundant except that it
- // ensures an immediate call to getPlaybackParams will retrieve the instance with defaults
- // allowed, rather than this change becoming visible sometime later once the audio renderers
- // receive the parameters.
params.allowDefaults();
- playbackParamsHolder = new PlaybackParamsHolder(params);
+ playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch());
} else {
- playbackParamsHolder = null;
+ playbackParameters = null;
}
- ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
- int count = 0;
- for (Renderer renderer : renderers) {
- if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
- messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_PLAYBACK_PARAMS, params);
- }
- }
- player.sendMessages(messages);
- }
-
- /**
- * Returns the {@link PlaybackParams} governing audio playback, or null if not set.
- */
- @TargetApi(23)
- public PlaybackParams getPlaybackParams() {
- return playbackParamsHolder == null ? null : playbackParamsHolder.params;
+ setPlaybackParameters(playbackParameters);
}
/**
@@ -519,6 +502,16 @@ public class SimpleExoPlayer implements ExoPlayer {
player.seekTo(windowIndex, positionMs);
}
+ @Override
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ player.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return player.getPlaybackParameters();
+ }
+
@Override
public void stop() {
player.stop();
@@ -1024,15 +1017,4 @@ public class SimpleExoPlayer implements ExoPlayer {
}
- @TargetApi(23)
- private static final class PlaybackParamsHolder {
-
- public final PlaybackParams params;
-
- public PlaybackParamsHolder(PlaybackParams params) {
- this.params = params;
- }
-
- }
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
index 3b8a1b8f39..d56f6a0d89 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
@@ -20,11 +20,11 @@ import android.annotation.TargetApi;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioTimestamp;
-import android.media.PlaybackParams;
import android.os.ConditionVariable;
import android.os.SystemClock;
import android.util.Log;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@@ -271,6 +271,7 @@ public final class AudioTrack {
private final AudioCapabilities audioCapabilities;
private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
+ private final SonicAudioProcessor sonicAudioProcessor;
private final AudioProcessor[] availableAudioProcessors;
private final Listener listener;
private final ConditionVariable releasingConditionVariable;
@@ -294,6 +295,7 @@ public final class AudioTrack {
private boolean passthrough;
private int bufferSize;
private long bufferSizeUs;
+ private PlaybackParameters playbackParameters;
private ByteBuffer avSyncHeader;
private int bytesUntilNextAvSync;
@@ -344,11 +346,6 @@ public final class AudioTrack {
public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors,
Listener listener) {
this.audioCapabilities = audioCapabilities;
- channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
- availableAudioProcessors = new AudioProcessor[audioProcessors.length + 2];
- availableAudioProcessors[0] = new ResamplingAudioProcessor();
- availableAudioProcessors[1] = channelMappingAudioProcessor;
- System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length);
this.listener = listener;
releasingConditionVariable = new ConditionVariable(true);
if (Util.SDK_INT >= 18) {
@@ -359,18 +356,24 @@ public final class AudioTrack {
// There's no guarantee this method exists. Do nothing.
}
}
- if (Util.SDK_INT >= 23) {
- audioTrackUtil = new AudioTrackUtilV23();
- } else if (Util.SDK_INT >= 19) {
+ if (Util.SDK_INT >= 19) {
audioTrackUtil = new AudioTrackUtilV19();
} else {
audioTrackUtil = new AudioTrackUtil();
}
+ channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
+ sonicAudioProcessor = new SonicAudioProcessor();
+ availableAudioProcessors = new AudioProcessor[3 + audioProcessors.length];
+ availableAudioProcessors[0] = new ResamplingAudioProcessor();
+ availableAudioProcessors[1] = channelMappingAudioProcessor;
+ System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length);
+ availableAudioProcessors[2 + audioProcessors.length] = sonicAudioProcessor;
playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
volume = 1.0f;
startMediaTimeState = START_NOT_SET;
streamType = C.STREAM_TYPE_DEFAULT;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ playbackParameters = PlaybackParameters.DEFAULT;
drainingAudioProcessorIndex = C.INDEX_UNSET;
this.audioProcessors = new AudioProcessor[0];
outputBuffers = new ByteBuffer[0];
@@ -408,33 +411,28 @@ public final class AudioTrack {
}
long systemClockUs = System.nanoTime() / 1000;
- long currentPositionUs;
+ long positionUs;
if (audioTimestampSet) {
- // How long ago in the past the audio timestamp is (negative if it's in the future).
- long presentationDiff = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000);
- // Fixes such difference if the playback speed is not real time speed.
- long actualSpeedPresentationDiff = (long) (presentationDiff
- * audioTrackUtil.getPlaybackSpeed());
- long framesDiff = durationUsToFrames(actualSpeedPresentationDiff);
- // The position of the frame that's currently being presented.
- long currentFramePosition = audioTrackUtil.getTimestampFramePosition() + framesDiff;
- currentPositionUs = framesToDurationUs(currentFramePosition) + startMediaTimeUs;
+ // Calculate the speed-adjusted position using the timestamp (which may be in the future).
+ long elapsedSinceTimestampUs = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000);
+ long elapsedSinceTimestampFrames = durationUsToFrames(elapsedSinceTimestampUs);
+ long elapsedFrames = audioTrackUtil.getTimestampFramePosition() + elapsedSinceTimestampFrames;
+ positionUs = framesToDurationUs(elapsedFrames);
} else {
if (playheadOffsetCount == 0) {
// The AudioTrack has started, but we don't have any samples to compute a smoothed position.
- currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs;
+ positionUs = audioTrackUtil.getPositionUs();
} else {
// getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the
// system clock (and a smoothed offset between it and the playhead position) so as to
// prevent jitter in the reported positions.
- currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs;
+ positionUs = systemClockUs + smoothedPlayheadOffsetUs;
}
if (!sourceEnded) {
- currentPositionUs -= latencyUs;
+ positionUs -= latencyUs;
}
}
-
- return currentPositionUs;
+ return startMediaTimeUs + scaleFrames(positionUs);
}
/**
@@ -481,10 +479,7 @@ public final class AudioTrack {
boolean flush = false;
if (!passthrough) {
pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount);
-
- // Reconfigure the audio processors.
channelMappingAudioProcessor.setChannelMap(outputChannels);
- ArrayList newAudioProcessors = new ArrayList<>();
for (AudioProcessor audioProcessor : availableAudioProcessors) {
try {
flush |= audioProcessor.configure(sampleRate, channelCount, encoding);
@@ -492,23 +487,12 @@ public final class AudioTrack {
throw new ConfigurationException(e);
}
if (audioProcessor.isActive()) {
- newAudioProcessors.add(audioProcessor);
channelCount = audioProcessor.getOutputChannelCount();
encoding = audioProcessor.getOutputEncoding();
- } else {
- audioProcessor.flush();
}
}
-
if (flush) {
- int count = newAudioProcessors.size();
- audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]);
- outputBuffers = new ByteBuffer[count];
- for (int i = 0; i < count; i++) {
- AudioProcessor audioProcessor = audioProcessors[i];
- audioProcessor.flush();
- outputBuffers[i] = audioProcessor.getOutput();
- }
+ resetAudioProcessors();
}
}
@@ -603,6 +587,28 @@ public final class AudioTrack {
: multipliedBufferSize;
}
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize);
+
+ // The old playback parameters may no longer be applicable so try to reset them now.
+ setPlaybackParameters(playbackParameters);
+ }
+
+ private void resetAudioProcessors() {
+ ArrayList newAudioProcessors = new ArrayList<>();
+ for (AudioProcessor audioProcessor : availableAudioProcessors) {
+ if (audioProcessor.isActive()) {
+ newAudioProcessors.add(audioProcessor);
+ } else {
+ audioProcessor.flush();
+ }
+ }
+ int count = newAudioProcessors.size();
+ audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]);
+ outputBuffers = new ByteBuffer[count];
+ for (int i = 0; i < count; i++) {
+ AudioProcessor audioProcessor = audioProcessors[i];
+ audioProcessor.flush();
+ outputBuffers[i] = audioProcessor.getOutput();
+ }
}
private void initialize() throws InitializationException {
@@ -940,15 +946,42 @@ public final class AudioTrack {
}
/**
- * Sets the playback parameters. Only available for {@link Util#SDK_INT} >= 23
+ * Attempts to set the playback parameters and returns the active playback parameters, which may
+ * differ from those passed in.
*
- * @param playbackParams The playback parameters to be used by the
- * {@link android.media.AudioTrack}.
- * @throws UnsupportedOperationException if the Playback Parameters are not supported. That is,
- * {@link Util#SDK_INT} < 23.
+ * @return The active playback parameters.
*/
- public void setPlaybackParams(PlaybackParams playbackParams) {
- audioTrackUtil.setPlaybackParams(playbackParams);
+ public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ if (passthrough) {
+ this.playbackParameters = PlaybackParameters.DEFAULT;
+ } else {
+ this.playbackParameters = new PlaybackParameters(
+ sonicAudioProcessor.setSpeed(playbackParameters.speed),
+ sonicAudioProcessor.setPitch(playbackParameters.pitch));
+ // TODO: Avoid resetting the track, so that speed/pitch changes are seamless.
+ // See [Internal: b/36542189].
+ reset();
+ // Setting the playback parameters never changes the output format, so it is not necessary to
+ // reconfigure the processors, though they may have become active/inactive.
+ resetAudioProcessors();
+ }
+ return this.playbackParameters;
+ }
+
+ /**
+ * Gets the {@link PlaybackParameters}.
+ */
+ public PlaybackParameters getPlaybackParameters() {
+ return playbackParameters;
+ }
+
+ /**
+ * Returns the number of input frames corresponding to the specified number of output frames,
+ * taking into account any internal playback speed adjustment.
+ */
+ private long scaleFrames(long outputFrameCount) {
+ return sonicAudioProcessor.isActive() ? sonicAudioProcessor.getInputFrames(outputFrameCount)
+ : outputFrameCount;
}
/**
@@ -1145,7 +1178,7 @@ public final class AudioTrack {
* Updates the audio track latency and playback position parameters.
*/
private void maybeSampleSyncParams() {
- long playbackPositionUs = audioTrackUtil.getPlaybackHeadPositionUs();
+ long playbackPositionUs = audioTrackUtil.getPositionUs();
if (playbackPositionUs == 0) {
// The AudioTrack hasn't output anything yet.
return;
@@ -1441,15 +1474,15 @@ public final class AudioTrack {
/**
* Stops the audio track in a way that ensures media written to it is played out in full, and
- * that {@link #getPlaybackHeadPosition()} and {@link #getPlaybackHeadPositionUs()} continue to
- * increment as the remaining media is played out.
+ * that {@link #getPlaybackHeadPosition()} and {@link #getPositionUs()} continue to increment as
+ * the remaining media is played out.
*
- * @param submittedFrames The total number of frames that have been submitted.
+ * @param writtenFrames The total number of frames that have been written.
*/
- public void handleEndOfStream(long submittedFrames) {
+ public void handleEndOfStream(long writtenFrames) {
stopPlaybackHeadPosition = getPlaybackHeadPosition();
stopTimestampUs = SystemClock.elapsedRealtime() * 1000;
- endPlaybackHeadPosition = submittedFrames;
+ endPlaybackHeadPosition = writtenFrames;
audioTrack.stop();
}
@@ -1471,8 +1504,7 @@ public final class AudioTrack {
* returns the playback head position as a long that will only wrap around if the value exceeds
* {@link Long#MAX_VALUE} (which in practice will never happen).
*
- * @return {@link android.media.AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack}
- * expressed as a long.
+ * @return The playback head position, in frames.
*/
public long getPlaybackHeadPosition() {
if (stopTimestampUs != C.TIME_UNSET) {
@@ -1507,9 +1539,9 @@ public final class AudioTrack {
}
/**
- * Returns {@link #getPlaybackHeadPosition()} expressed as microseconds.
+ * Returns the duration of played media since reconfiguration, in microseconds.
*/
- public long getPlaybackHeadPositionUs() {
+ public long getPositionUs() {
return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate;
}
@@ -1553,28 +1585,6 @@ public final class AudioTrack {
throw new UnsupportedOperationException();
}
- /**
- * Sets the Playback Parameters to be used by the underlying {@link android.media.AudioTrack}.
- *
- * @param playbackParams The playback parameters to be used by the
- * {@link android.media.AudioTrack}.
- * @throws UnsupportedOperationException If Playback Parameters are not supported
- * (i.e. {@link Util#SDK_INT} < 23).
- */
- public void setPlaybackParams(PlaybackParams playbackParams) {
- throw new UnsupportedOperationException();
- }
-
- /**
- * Returns the configured playback speed according to the used Playback Parameters. If these are
- * not supported, 1.0f(normal speed) is returned.
- *
- * @return The speed factor used by the underlying {@link android.media.AudioTrack}.
- */
- public float getPlaybackSpeed() {
- return 1.0f;
- }
-
}
@TargetApi(19)
@@ -1626,43 +1636,4 @@ public final class AudioTrack {
}
- @TargetApi(23)
- private static class AudioTrackUtilV23 extends AudioTrackUtilV19 {
-
- private PlaybackParams playbackParams;
- private float playbackSpeed;
-
- public AudioTrackUtilV23() {
- playbackSpeed = 1.0f;
- }
-
- @Override
- public void reconfigure(android.media.AudioTrack audioTrack,
- boolean needsPassthroughWorkaround) {
- super.reconfigure(audioTrack, needsPassthroughWorkaround);
- maybeApplyPlaybackParams();
- }
-
- @Override
- public void setPlaybackParams(PlaybackParams playbackParams) {
- playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams())
- .allowDefaults();
- this.playbackParams = playbackParams;
- playbackSpeed = playbackParams.getSpeed();
- maybeApplyPlaybackParams();
- }
-
- @Override
- public float getPlaybackSpeed() {
- return playbackSpeed;
- }
-
- private void maybeApplyPlaybackParams() {
- if (audioTrack != null && playbackParams != null) {
- audioTrack.setPlaybackParams(playbackParams);
- }
- }
-
- }
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
index e34068861d..48c7462b03 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -19,12 +19,12 @@ import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat;
-import android.media.PlaybackParams;
import android.media.audiofx.Virtualizer;
import android.os.Handler;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
@@ -345,6 +345,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return currentPositionUs;
}
+ @Override
+ public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ return audioTrack.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return audioTrack.getPlaybackParameters();
+ }
+
@Override
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs,
@@ -389,9 +399,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
case C.MSG_SET_VOLUME:
audioTrack.setVolume((Float) message);
break;
- case C.MSG_SET_PLAYBACK_PARAMS:
- audioTrack.setPlaybackParams((PlaybackParams) message);
- break;
case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message;
audioTrack.setStreamType(streamType);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
index 5594d9a90e..3a50a8244a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.audio;
-import android.media.PlaybackParams;
import android.media.audiofx.Virtualizer;
import android.os.Handler;
import android.os.Looper;
@@ -26,6 +25,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
@@ -434,6 +434,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return currentPositionUs;
}
+ @Override
+ public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ return audioTrack.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return audioTrack.getPlaybackParameters();
+ }
+
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
decoderCounters = new DecoderCounters();
@@ -585,9 +595,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
case C.MSG_SET_VOLUME:
audioTrack.setVolume((Float) message);
break;
- case C.MSG_SET_PLAYBACK_PARAMS:
- audioTrack.setPlaybackParams((PlaybackParams) message);
- break;
case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message;
audioTrack.setStreamType(streamType);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java
new file mode 100644
index 0000000000..40c52f13c2
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java
@@ -0,0 +1,817 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2010 Bill Cox, Sonic Library
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+/**
+ * Sonic audio time/pitch stretching library. Based on https://github.com/waywardgeek/sonic.
+ */
+/* package */ final class Sonic {
+
+ private static final int SONIC_MIN_PITCH = 65;
+ private static final int SONIC_MAX_PITCH = 400;
+ /* This is used to down-sample some inputs to improve speed */
+ private static final int SONIC_AMDF_FREQ = 4000;
+
+ private short[] inputBuffer;
+ private short[] outputBuffer;
+ private short[] pitchBuffer;
+ private short[] downSampleBuffer;
+ private float speed;
+ private float volume;
+ private float pitch;
+ private float rate;
+ private int oldRatePosition;
+ private int newRatePosition;
+ private boolean useChordPitch;
+ private int quality;
+ private int numChannels;
+ private int inputBufferSize;
+ private int pitchBufferSize;
+ private int outputBufferSize;
+ private int numInputSamples;
+ private int numOutputSamples;
+ private int numPitchSamples;
+ private int minPeriod;
+ private int maxPeriod;
+ private int maxRequired;
+ private int remainingInputToCopy;
+ private int sampleRate;
+ private int prevPeriod;
+ private int prevMinDiff;
+ private int minDiff;
+ private int maxDiff;
+
+ // Resize the array.
+ private short[] resize(short[] oldArray, int newLength) {
+ newLength *= numChannels;
+ short[] newArray = new short[newLength];
+ int length = Math.min(oldArray.length, newLength);
+
+ System.arraycopy(oldArray, 0, newArray, 0, length);
+ return newArray;
+ }
+
+ // Move samples from one array to another. May move samples down within an array, but not up.
+ private void move(short[] dest, int destPos, short[] source, int sourcePos, int numSamples) {
+ System.arraycopy(
+ source, sourcePos * numChannels, dest, destPos * numChannels, numSamples * numChannels);
+ }
+
+ // Scale the samples by the factor.
+ private void scaleSamples(short[] samples, int position, int numSamples, float volume) {
+ int fixedPointVolume = (int) (volume * 4096.0f);
+ int start = position * numChannels;
+ int stop = start + numSamples * numChannels;
+
+ for (int xSample = start; xSample < stop; xSample++) {
+ int value = (samples[xSample] * fixedPointVolume) >> 12;
+ if (value > 32767) {
+ value = 32767;
+ } else if (value < -32767) {
+ value = -32767;
+ }
+ samples[xSample] = (short) value;
+ }
+ }
+
+ // Get the speed of the stream.
+ public float getSpeed() {
+ return speed;
+ }
+
+ // Set the speed of the stream.
+ public void setSpeed(float speed) {
+ this.speed = speed;
+ }
+
+ // Get the pitch of the stream.
+ public float getPitch() {
+ return pitch;
+ }
+
+ // Set the pitch of the stream.
+ public void setPitch(float pitch) {
+ this.pitch = pitch;
+ }
+
+ // Get the rate of the stream.
+ public float getRate() {
+ return rate;
+ }
+
+ // Set the playback rate of the stream. This scales pitch and speed at the same time.
+ public void setRate(float rate) {
+ this.rate = rate;
+ this.oldRatePosition = 0;
+ this.newRatePosition = 0;
+ }
+
+ // Get the vocal chord pitch setting.
+ public boolean getChordPitch() {
+ return useChordPitch;
+ }
+
+ // Set the vocal chord mode for pitch computation. Default is off.
+ public void setChordPitch(boolean useChordPitch) {
+ this.useChordPitch = useChordPitch;
+ }
+
+ // Get the quality setting.
+ public int getQuality() {
+ return quality;
+ }
+
+ // Set the "quality". Default 0 is virtually as good as 1, but very much faster.
+ public void setQuality(int quality) {
+ this.quality = quality;
+ }
+
+ // Get the scaling factor of the stream.
+ public float getVolume() {
+ return volume;
+ }
+
+ // Set the scaling factor of the stream.
+ public void setVolume(float volume) {
+ this.volume = volume;
+ }
+
+ // Allocate stream buffers.
+ private void allocateStreamBuffers(int sampleRate, int numChannels) {
+ minPeriod = sampleRate / SONIC_MAX_PITCH;
+ maxPeriod = sampleRate / SONIC_MIN_PITCH;
+ maxRequired = 2 * maxPeriod;
+ inputBufferSize = maxRequired;
+ inputBuffer = new short[maxRequired * numChannels];
+ outputBufferSize = maxRequired;
+ outputBuffer = new short[maxRequired * numChannels];
+ pitchBufferSize = maxRequired;
+ pitchBuffer = new short[maxRequired * numChannels];
+ downSampleBuffer = new short[maxRequired];
+ this.sampleRate = sampleRate;
+ this.numChannels = numChannels;
+ oldRatePosition = 0;
+ newRatePosition = 0;
+ prevPeriod = 0;
+ }
+
+ // Create a sonic stream.
+ public Sonic(int sampleRate, int numChannels) {
+ allocateStreamBuffers(sampleRate, numChannels);
+ speed = 1.0f;
+ pitch = 1.0f;
+ volume = 1.0f;
+ rate = 1.0f;
+ oldRatePosition = 0;
+ newRatePosition = 0;
+ useChordPitch = false;
+ quality = 0;
+ }
+
+ // Get the sample rate of the stream.
+ public int getSampleRate() {
+ return sampleRate;
+ }
+
+ // Set the sample rate of the stream. This will cause samples buffered in the stream to be lost.
+ public void setSampleRate(int sampleRate) {
+ allocateStreamBuffers(sampleRate, numChannels);
+ }
+
+ // Get the number of channels.
+ public int getNumChannels() {
+ return numChannels;
+ }
+
+ // Set the num channels of the stream. This will cause samples buffered in the stream to be lost.
+ public void setNumChannels(int numChannels) {
+ allocateStreamBuffers(sampleRate, numChannels);
+ }
+
+ // Enlarge the output buffer if needed.
+ private void enlargeOutputBufferIfNeeded(int numSamples) {
+ if (numOutputSamples + numSamples > outputBufferSize) {
+ outputBufferSize += (outputBufferSize >> 1) + numSamples;
+ outputBuffer = resize(outputBuffer, outputBufferSize);
+ }
+ }
+
+ // Enlarge the input buffer if needed.
+ private void enlargeInputBufferIfNeeded(int numSamples) {
+ if (numInputSamples + numSamples > inputBufferSize) {
+ inputBufferSize += (inputBufferSize >> 1) + numSamples;
+ inputBuffer = resize(inputBuffer, inputBufferSize);
+ }
+ }
+
+ // Add the input samples to the input buffer.
+ private void addFloatSamplesToInputBuffer(float[] samples, int numSamples) {
+ if (numSamples == 0) {
+ return;
+ }
+ enlargeInputBufferIfNeeded(numSamples);
+ int xBuffer = numInputSamples * numChannels;
+ for (int xSample = 0; xSample < numSamples * numChannels; xSample++) {
+ inputBuffer[xBuffer++] = (short) (samples[xSample] * 32767.0f);
+ }
+ numInputSamples += numSamples;
+ }
+
+ // Add the input samples to the input buffer.
+ private void addShortSamplesToInputBuffer(short[] samples, int numSamples) {
+ if (numSamples == 0) {
+ return;
+ }
+ enlargeInputBufferIfNeeded(numSamples);
+ move(inputBuffer, numInputSamples, samples, 0, numSamples);
+ numInputSamples += numSamples;
+ }
+
+ // Add the input samples to the input buffer.
+ private void addUnsignedByteSamplesToInputBuffer(byte[] samples, int numSamples) {
+ short sample;
+
+ enlargeInputBufferIfNeeded(numSamples);
+ int xBuffer = numInputSamples * numChannels;
+ for (int xSample = 0; xSample < numSamples * numChannels; xSample++) {
+ sample = (short) ((samples[xSample] & 0xff) - 128); // Convert from unsigned to signed
+ inputBuffer[xBuffer++] = (short) (sample << 8);
+ }
+ numInputSamples += numSamples;
+ }
+
+ // Add the input samples to the input buffer. They must be 16-bit little-endian encoded in a byte
+ // array.
+ private void addBytesToInputBuffer(byte[] inBuffer, int numBytes) {
+ int numSamples = numBytes / (2 * numChannels);
+ short sample;
+
+ enlargeInputBufferIfNeeded(numSamples);
+ int xBuffer = numInputSamples * numChannels;
+ for (int xByte = 0; xByte + 1 < numBytes; xByte += 2) {
+ sample = (short) ((inBuffer[xByte] & 0xff) | (inBuffer[xByte + 1] << 8));
+ inputBuffer[xBuffer++] = sample;
+ }
+ numInputSamples += numSamples;
+ }
+
+ // Remove input samples that we have already processed.
+ private void removeInputSamples(int position) {
+ int remainingSamples = numInputSamples - position;
+
+ move(inputBuffer, 0, inputBuffer, position, remainingSamples);
+ numInputSamples = remainingSamples;
+ }
+
+ // Just copy from the array to the output buffer
+ private void copyToOutput(short[] samples, int position, int numSamples) {
+ enlargeOutputBufferIfNeeded(numSamples);
+ move(outputBuffer, numOutputSamples, samples, position, numSamples);
+ numOutputSamples += numSamples;
+ }
+
+ // Just copy from the input buffer to the output buffer. Return num samples copied.
+ private int copyInputToOutput(int position) {
+ int numSamples = remainingInputToCopy;
+
+ if (numSamples > maxRequired) {
+ numSamples = maxRequired;
+ }
+ copyToOutput(inputBuffer, position, numSamples);
+ remainingInputToCopy -= numSamples;
+ return numSamples;
+ }
+
+ // Read data out of the stream. Sometimes no data will be available, and zero
+ // is returned, which is not an error condition.
+ public int readFloatFromStream(float[] samples, int maxSamples) {
+ int numSamples = numOutputSamples;
+ int remainingSamples = 0;
+
+ if (numSamples == 0) {
+ return 0;
+ }
+ if (numSamples > maxSamples) {
+ remainingSamples = numSamples - maxSamples;
+ numSamples = maxSamples;
+ }
+ for (int xSample = 0; xSample < numSamples * numChannels; xSample++) {
+ samples[xSample++] = (outputBuffer[xSample]) / 32767.0f;
+ }
+ move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples);
+ numOutputSamples = remainingSamples;
+ return numSamples;
+ }
+
+ // Read short data out of the stream. Sometimes no data will be available, and zero
+ // is returned, which is not an error condition.
+ public int readShortFromStream(short[] samples, int maxSamples) {
+ int numSamples = numOutputSamples;
+ int remainingSamples = 0;
+
+ if (numSamples == 0) {
+ return 0;
+ }
+ if (numSamples > maxSamples) {
+ remainingSamples = numSamples - maxSamples;
+ numSamples = maxSamples;
+ }
+ move(samples, 0, outputBuffer, 0, numSamples);
+ move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples);
+ numOutputSamples = remainingSamples;
+ return numSamples;
+ }
+
+ // Read unsigned byte data out of the stream. Sometimes no data will be available, and zero
+ // is returned, which is not an error condition.
+ public int readBytesFromStream(byte[] outBuffer, int maxBytes) {
+ int maxSamples = maxBytes / (2 * numChannels);
+ int numSamples = numOutputSamples;
+ int remainingSamples = 0;
+
+ if (numSamples == 0 || maxSamples == 0) {
+ return 0;
+ }
+ if (numSamples > maxSamples) {
+ remainingSamples = numSamples - maxSamples;
+ numSamples = maxSamples;
+ }
+ for (int xSample = 0; xSample < numSamples * numChannels; xSample++) {
+ short sample = outputBuffer[xSample];
+ outBuffer[xSample << 1] = (byte) (sample & 0xff);
+ outBuffer[(xSample << 1) + 1] = (byte) (sample >> 8);
+ }
+ move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples);
+ numOutputSamples = remainingSamples;
+ return 2 * numSamples * numChannels;
+ }
+
+ // Force the sonic stream to generate output using whatever data it currently
+ // has. No extra delay will be added to the output, but flushing in the middle of
+ // words could introduce distortion.
+ public void flushStream() {
+ int remainingSamples = numInputSamples;
+ float s = speed / pitch;
+ float r = rate * pitch;
+ int expectedOutputSamples =
+ numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / r + 0.5f);
+
+ // Add enough silence to flush both input and pitch buffers.
+ enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired);
+ for (int xSample = 0; xSample < 2 * maxRequired * numChannels; xSample++) {
+ inputBuffer[remainingSamples * numChannels + xSample] = 0;
+ }
+ numInputSamples += 2 * maxRequired;
+ writeShortToStream(null, 0);
+ // Throw away any extra samples we generated due to the silence we added.
+ if (numOutputSamples > expectedOutputSamples) {
+ numOutputSamples = expectedOutputSamples;
+ }
+ // Empty input and pitch buffers.
+ numInputSamples = 0;
+ remainingInputToCopy = 0;
+ numPitchSamples = 0;
+ }
+
+ // Return the number of samples in the output buffer
+ public int samplesAvailable() {
+ return numOutputSamples;
+ }
+
+ // If skip is greater than one, average skip samples together and write them to
+ // the down-sample buffer. If numChannels is greater than one, mix the channels
+ // together as we down sample.
+ private void downSampleInput(short[] samples, int position, int skip) {
+ int numSamples = maxRequired / skip;
+ int samplesPerValue = numChannels * skip;
+ int value;
+
+ position *= numChannels;
+ for (int i = 0; i < numSamples; i++) {
+ value = 0;
+ for (int j = 0; j < samplesPerValue; j++) {
+ value += samples[position + i * samplesPerValue + j];
+ }
+ value /= samplesPerValue;
+ downSampleBuffer[i] = (short) value;
+ }
+ }
+
+ // Find the best frequency match in the range, and given a sample skip multiple.
+ // For now, just find the pitch of the first channel.
+ private int findPitchPeriodInRange(short[] samples, int position, int minPeriod, int maxPeriod) {
+ int bestPeriod = 0;
+ int worstPeriod = 255;
+ int minDiff = 1;
+ int maxDiff = 0;
+
+ position *= numChannels;
+ for (int period = minPeriod; period <= maxPeriod; period++) {
+ int diff = 0;
+ for (int i = 0; i < period; i++) {
+ short sVal = samples[position + i];
+ short pVal = samples[position + period + i];
+ diff += sVal >= pVal ? sVal - pVal : pVal - sVal;
+ }
+ // Note that the highest number of samples we add into diff will be less than 256, since we
+ // skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples
+ // without overflow.
+ if (diff * bestPeriod < minDiff * period) {
+ minDiff = diff;
+ bestPeriod = period;
+ }
+ if (diff * worstPeriod > maxDiff * period) {
+ maxDiff = diff;
+ worstPeriod = period;
+ }
+ }
+ this.minDiff = minDiff / bestPeriod;
+ this.maxDiff = maxDiff / worstPeriod;
+
+ return bestPeriod;
+ }
+
+ // At abrupt ends of voiced words, we can have pitch periods that are better
+ // approximated by the previous pitch period estimate. Try to detect this case.
+ private boolean prevPeriodBetter(int minDiff, int maxDiff, boolean preferNewPeriod) {
+ if (minDiff == 0 || prevPeriod == 0) {
+ return false;
+ }
+ if (preferNewPeriod) {
+ if (maxDiff > minDiff * 3) {
+ // Got a reasonable match this period
+ return false;
+ }
+ if (minDiff * 2 <= prevMinDiff * 3) {
+ // Mismatch is not that much greater this period
+ return false;
+ }
+ } else {
+ if (minDiff <= prevMinDiff) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Find the pitch period. This is a critical step, and we may have to try
+ // multiple ways to get a good answer. This version uses AMDF. To improve
+ // speed, we down sample by an integer factor get in the 11KHz range, and then
+ // do it again with a narrower frequency range without down sampling
+ private int findPitchPeriod(short[] samples, int position, boolean preferNewPeriod) {
+ int period;
+ int retPeriod;
+ int skip = 1;
+
+ if (sampleRate > SONIC_AMDF_FREQ && quality == 0) {
+ skip = sampleRate / SONIC_AMDF_FREQ;
+ }
+ if (numChannels == 1 && skip == 1) {
+ period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod);
+ } else {
+ downSampleInput(samples, position, skip);
+ period = findPitchPeriodInRange(downSampleBuffer, 0, minPeriod / skip, maxPeriod / skip);
+ if (skip != 1) {
+ period *= skip;
+ int minP = period - (skip << 2);
+ int maxP = period + (skip << 2);
+ if (minP < minPeriod) {
+ minP = minPeriod;
+ }
+ if (maxP > maxPeriod) {
+ maxP = maxPeriod;
+ }
+ if (numChannels == 1) {
+ period = findPitchPeriodInRange(samples, position, minP, maxP);
+ } else {
+ downSampleInput(samples, position, 1);
+ period = findPitchPeriodInRange(downSampleBuffer, 0, minP, maxP);
+ }
+ }
+ }
+ if (prevPeriodBetter(minDiff, maxDiff, preferNewPeriod)) {
+ retPeriod = prevPeriod;
+ } else {
+ retPeriod = period;
+ }
+ prevMinDiff = minDiff;
+ prevPeriod = period;
+ return retPeriod;
+ }
+
+ // Overlap two sound segments, ramp the volume of one down, while ramping the
+ // other one from zero up, and add them, storing the result at the output.
+ private static void overlapAdd(int numSamples, int numChannels, short[] out, int outPos,
+ short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) {
+ for (int i = 0; i < numChannels; i++) {
+ int o = outPos * numChannels + i;
+ int u = rampUpPos * numChannels + i;
+ int d = rampDownPos * numChannels + i;
+ for (int t = 0; t < numSamples; t++) {
+ out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * t) / numSamples);
+ o += numChannels;
+ d += numChannels;
+ u += numChannels;
+ }
+ }
+ }
+
+ // Overlap two sound segments, ramp the volume of one down, while ramping the
+ // other one from zero up, and add them, storing the result at the output.
+ private static void overlapAddWithSeparation(int numSamples, int numChannels, int separation,
+ short[] out, int outPos, short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) {
+ for (int i = 0; i < numChannels; i++) {
+ int o = outPos * numChannels + i;
+ int u = rampUpPos * numChannels + i;
+ int d = rampDownPos * numChannels + i;
+ for (int t = 0; t < numSamples + separation; t++) {
+ if (t < separation) {
+ out[o] = (short) (rampDown[d] * (numSamples - t) / numSamples);
+ d += numChannels;
+ } else if (t < numSamples) {
+ out[o] =
+ (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * (t - separation))
+ / numSamples);
+ d += numChannels;
+ u += numChannels;
+ } else {
+ out[o] = (short) (rampUp[u] * (t - separation) / numSamples);
+ u += numChannels;
+ }
+ o += numChannels;
+ }
+ }
+ }
+
+ // Just move the new samples in the output buffer to the pitch buffer
+ private void moveNewSamplesToPitchBuffer(int originalNumOutputSamples) {
+ int numSamples = numOutputSamples - originalNumOutputSamples;
+
+ if (numPitchSamples + numSamples > pitchBufferSize) {
+ pitchBufferSize += (pitchBufferSize >> 1) + numSamples;
+ pitchBuffer = resize(pitchBuffer, pitchBufferSize);
+ }
+ move(pitchBuffer, numPitchSamples, outputBuffer, originalNumOutputSamples, numSamples);
+ numOutputSamples = originalNumOutputSamples;
+ numPitchSamples += numSamples;
+ }
+
+ // Remove processed samples from the pitch buffer.
+ private void removePitchSamples(int numSamples) {
+ if (numSamples == 0) {
+ return;
+ }
+ move(pitchBuffer, 0, pitchBuffer, numSamples, numPitchSamples - numSamples);
+ numPitchSamples -= numSamples;
+ }
+
+ // Change the pitch. The latency this introduces could be reduced by looking at
+ // past samples to determine pitch, rather than future.
+ private void adjustPitch(int originalNumOutputSamples) {
+ int period;
+ int newPeriod;
+ int separation;
+ int position = 0;
+
+ if (numOutputSamples == originalNumOutputSamples) {
+ return;
+ }
+ moveNewSamplesToPitchBuffer(originalNumOutputSamples);
+ while (numPitchSamples - position >= maxRequired) {
+ period = findPitchPeriod(pitchBuffer, position, false);
+ newPeriod = (int) (period / pitch);
+ enlargeOutputBufferIfNeeded(newPeriod);
+ if (pitch >= 1.0f) {
+ overlapAdd(newPeriod, numChannels, outputBuffer, numOutputSamples, pitchBuffer, position,
+ pitchBuffer, position + period - newPeriod);
+ } else {
+ separation = newPeriod - period;
+ overlapAddWithSeparation(period, numChannels, separation, outputBuffer, numOutputSamples,
+ pitchBuffer, position, pitchBuffer, position);
+ }
+ numOutputSamples += newPeriod;
+ position += period;
+ }
+ removePitchSamples(position);
+ }
+
+ // Interpolate the new output sample.
+ private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) {
+ short left = in[inPos * numChannels];
+ short right = in[inPos * numChannels + numChannels];
+ int position = newRatePosition * oldSampleRate;
+ int leftPosition = oldRatePosition * newSampleRate;
+ int rightPosition = (oldRatePosition + 1) * newSampleRate;
+ int ratio = rightPosition - position;
+ int width = rightPosition - leftPosition;
+
+ return (short) ((ratio * left + (width - ratio) * right) / width);
+ }
+
+ // Change the rate.
+ private void adjustRate(float rate, int originalNumOutputSamples) {
+ int newSampleRate = (int) (sampleRate / rate);
+ int oldSampleRate = sampleRate;
+ int position;
+
+ // Set these values to help with the integer math
+ while (newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) {
+ newSampleRate >>= 1;
+ oldSampleRate >>= 1;
+ }
+ if (numOutputSamples == originalNumOutputSamples) {
+ return;
+ }
+ moveNewSamplesToPitchBuffer(originalNumOutputSamples);
+ // Leave at least one pitch sample in the buffer
+ for (position = 0; position < numPitchSamples - 1; position++) {
+ while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) {
+ enlargeOutputBufferIfNeeded(1);
+ for (int i = 0; i < numChannels; i++) {
+ outputBuffer[numOutputSamples * numChannels + i] =
+ interpolate(pitchBuffer, position + i, oldSampleRate, newSampleRate);
+ }
+ newRatePosition++;
+ numOutputSamples++;
+ }
+ oldRatePosition++;
+ if (oldRatePosition == oldSampleRate) {
+ oldRatePosition = 0;
+ if (newRatePosition != newSampleRate) {
+ System.out.printf("Assertion failed: newRatePosition != newSampleRate\n");
+ assert false;
+ }
+ newRatePosition = 0;
+ }
+ }
+ removePitchSamples(position);
+ }
+
+ // Skip over a pitch period, and copy period/speed samples to the output
+ private int skipPitchPeriod(short[] samples, int position, float speed, int period) {
+ int newSamples;
+
+ if (speed >= 2.0f) {
+ newSamples = (int) (period / (speed - 1.0f));
+ } else {
+ newSamples = period;
+ remainingInputToCopy = (int) (period * (2.0f - speed) / (speed - 1.0f));
+ }
+ enlargeOutputBufferIfNeeded(newSamples);
+ overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples, samples, position, samples,
+ position + period);
+ numOutputSamples += newSamples;
+ return newSamples;
+ }
+
+ // Insert a pitch period, and determine how much input to copy directly.
+ private int insertPitchPeriod(short[] samples, int position, float speed, int period) {
+ int newSamples;
+
+ if (speed < 0.5f) {
+ newSamples = (int) (period * speed / (1.0f - speed));
+ } else {
+ newSamples = period;
+ remainingInputToCopy = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed));
+ }
+ enlargeOutputBufferIfNeeded(period + newSamples);
+ move(outputBuffer, numOutputSamples, samples, position, period);
+ overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples,
+ position + period, samples, position);
+ numOutputSamples += period + newSamples;
+ return newSamples;
+ }
+
+ // Resample as many pitch periods as we have buffered on the input. Return 0 if
+ // we fail to resize an input or output buffer. Also scale the output by the volume.
+ private void changeSpeed(float speed) {
+ int numSamples = numInputSamples;
+ int position = 0;
+ int period;
+ int newSamples;
+
+ if (numInputSamples < maxRequired) {
+ return;
+ }
+ do {
+ if (remainingInputToCopy > 0) {
+ newSamples = copyInputToOutput(position);
+ position += newSamples;
+ } else {
+ period = findPitchPeriod(inputBuffer, position, true);
+ if (speed > 1.0) {
+ newSamples = skipPitchPeriod(inputBuffer, position, speed, period);
+ position += period + newSamples;
+ } else {
+ newSamples = insertPitchPeriod(inputBuffer, position, speed, period);
+ position += newSamples;
+ }
+ }
+ } while (position + maxRequired <= numSamples);
+ removeInputSamples(position);
+ }
+
+ // Resample as many pitch periods as we have buffered on the input. Scale the output by the
+ // volume.
+ private void processStreamInput() {
+ int originalNumOutputSamples = numOutputSamples;
+ float s = speed / pitch;
+ float r = rate;
+
+ if (!useChordPitch) {
+ r *= pitch;
+ }
+ if (s > 1.00001 || s < 0.99999) {
+ changeSpeed(s);
+ } else {
+ copyToOutput(inputBuffer, 0, numInputSamples);
+ numInputSamples = 0;
+ }
+ if (useChordPitch) {
+ if (pitch != 1.0f) {
+ adjustPitch(originalNumOutputSamples);
+ }
+ } else if (r != 1.0f) {
+ adjustRate(r, originalNumOutputSamples);
+ }
+ if (volume != 1.0f) {
+ // Adjust output volume.
+ scaleSamples(outputBuffer, originalNumOutputSamples,
+ numOutputSamples - originalNumOutputSamples, volume);
+ }
+ }
+
+ // Write floating point data to the input buffer and process it.
+ public void writeFloatToStream(float[] samples, int numSamples) {
+ addFloatSamplesToInputBuffer(samples, numSamples);
+ processStreamInput();
+ }
+
+ // Write the data to the input stream, and process it.
+ public void writeShortToStream(short[] samples, int numSamples) {
+ addShortSamplesToInputBuffer(samples, numSamples);
+ processStreamInput();
+ }
+
+ // Simple wrapper around sonicWriteFloatToStream that does the unsigned byte to short
+ // conversion for you.
+ public void writeUnsignedByteToStream(byte[] samples, int numSamples) {
+ addUnsignedByteSamplesToInputBuffer(samples, numSamples);
+ processStreamInput();
+ }
+
+ // Simple wrapper around sonicWriteBytesToStream that does the byte to 16-bit LE conversion.
+ public void writeBytesToStream(byte[] inBuffer, int numBytes) {
+ addBytesToInputBuffer(inBuffer, numBytes);
+ processStreamInput();
+ }
+
+ // This is a non-stream oriented interface to just change the speed of a sound sample
+ public static int changeFloatSpeed(float[] samples, int numSamples, float speed, float pitch,
+ float rate, float volume, boolean useChordPitch, int sampleRate, int numChannels) {
+ Sonic stream = new Sonic(sampleRate, numChannels);
+
+ stream.setSpeed(speed);
+ stream.setPitch(pitch);
+ stream.setRate(rate);
+ stream.setVolume(volume);
+ stream.setChordPitch(useChordPitch);
+ stream.writeFloatToStream(samples, numSamples);
+ stream.flushStream();
+ numSamples = stream.samplesAvailable();
+ stream.readFloatFromStream(samples, numSamples);
+ return numSamples;
+ }
+
+ /* This is a non-stream oriented interface to just change the speed of a sound sample */
+ public int sonicChangeShortSpeed(short[] samples, int numSamples, float speed, float pitch,
+ float rate, float volume, boolean useChordPitch, int sampleRate, int numChannels) {
+ Sonic stream = new Sonic(sampleRate, numChannels);
+
+ stream.setSpeed(speed);
+ stream.setPitch(pitch);
+ stream.setRate(rate);
+ stream.setVolume(volume);
+ stream.setChordPitch(useChordPitch);
+ stream.writeShortToStream(samples, numSamples);
+ stream.flushStream();
+ numSamples = stream.samplesAvailable();
+ stream.readShortFromStream(samples, numSamples);
+ return numSamples;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java
new file mode 100644
index 0000000000..ae1fae40c2
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.C.Encoding;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * An {@link AudioProcessor} that uses the Sonic library to modify the speed/pitch of audio.
+ */
+// TODO: Make public once it is possible to override AudioTrack's position calculations.
+/* package */ final class SonicAudioProcessor implements AudioProcessor {
+
+ /**
+ * The maximum allowed playback speed in {@link #setSpeed(float)}.
+ */
+ public static final float MAXIMUM_SPEED = 8.0f;
+ /**
+ * The minimum allowed playback speed in {@link #setSpeed(float)}.
+ */
+ public static final float MINIMUM_SPEED = 0.1f;
+ /**
+ * The maximum allowed pitch in {@link #setPitch(float)}.
+ */
+ public static final float MAXIMUM_PITCH = 8.0f;
+ /**
+ * The minimum allowed pitch in {@link #setPitch(float)}.
+ */
+ public static final float MINIMUM_PITCH = 0.1f;
+
+ /**
+ * The threshold below which the difference between two pitch/speed factors is negligible.
+ */
+ private static final float CLOSE_THRESHOLD = 0.01f;
+
+ private static final byte[] EMPTY_ARRAY = new byte[0];
+
+ private int channelCount;
+ private int sampleRateHz;
+
+ private Sonic sonic;
+ private float speed;
+ private float pitch;
+
+ private byte[] inputArray;
+ private ByteBuffer buffer;
+ private byte[] bufferArray;
+ private ByteBuffer outputBuffer;
+ private long inputBytes;
+ private long outputBytes;
+ private boolean inputEnded;
+
+ /**
+ * Creates a new Sonic audio processor.
+ */
+ public SonicAudioProcessor() {
+ speed = 1f;
+ pitch = 1f;
+ channelCount = Format.NO_VALUE;
+ sampleRateHz = Format.NO_VALUE;
+ buffer = EMPTY_BUFFER;
+ outputBuffer = EMPTY_BUFFER;
+ inputArray = EMPTY_ARRAY;
+ bufferArray = EMPTY_ARRAY;
+ }
+
+ /**
+ * Sets the playback speed. The new speed will take effect after a call to {@link #flush()}.
+ *
+ * @param speed The requested new playback speed.
+ * @return The actual new playback speed.
+ */
+ public float setSpeed(float speed) {
+ this.speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED);
+ return this.speed;
+ }
+
+ /**
+ * Sets the playback pitch. The new pitch will take effect after a call to {@link #flush()}.
+ *
+ * @param pitch The requested new pitch.
+ * @return The actual new pitch.
+ */
+ public float setPitch(float pitch) {
+ this.pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH);
+ return pitch;
+ }
+
+ /**
+ * Returns the number of input frames corresponding to the specified number of output frames.
+ */
+ public long getInputFrames(long outputFrames) {
+ // Sonic produces output data as soon as input is queued.
+ return outputBytes == 0 ? 0 : Util.scaleLargeTimestamp(outputFrames, inputBytes, outputBytes);
+ }
+
+ @Override
+ public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding)
+ throws UnhandledFormatException {
+ if (encoding != C.ENCODING_PCM_16BIT) {
+ throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
+ }
+ if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {
+ return false;
+ }
+ this.sampleRateHz = sampleRateHz;
+ this.channelCount = channelCount;
+ return true;
+ }
+
+ @Override
+ public boolean isActive() {
+ return Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD;
+ }
+
+ @Override
+ public int getOutputChannelCount() {
+ return channelCount;
+ }
+
+ @Override
+ public int getOutputEncoding() {
+ return C.ENCODING_PCM_16BIT;
+ }
+
+ @Override
+ public void queueInput(ByteBuffer inputBuffer) {
+ // TODO: Remove this extra copy.
+ int inputBytesToRead = inputBuffer.remaining();
+ if (inputArray == null || inputArray.length < inputBytesToRead) {
+ inputArray = new byte[inputBytesToRead];
+ }
+ inputBuffer.get(inputArray, 0, inputBytesToRead);
+ sonic.writeBytesToStream(inputArray, inputBytesToRead);
+ int outputSize = sonic.samplesAvailable() * channelCount * 2;
+ if (buffer.capacity() < outputSize) {
+ buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
+ bufferArray = new byte[outputSize];
+ } else {
+ buffer.clear();
+ }
+ inputBytes += inputBytesToRead;
+ int outputBytesRead = sonic.readBytesFromStream(bufferArray, outputSize);
+ buffer.put(bufferArray, 0, outputBytesRead);
+ buffer.flip();
+ outputBytes += outputSize;
+ outputBuffer = buffer;
+ }
+
+ @Override
+ public void queueEndOfStream() {
+ sonic.flushStream();
+ inputEnded = true;
+ }
+
+ @Override
+ public ByteBuffer getOutput() {
+ ByteBuffer outputBuffer = this.outputBuffer;
+ this.outputBuffer = EMPTY_BUFFER;
+ return outputBuffer;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return inputEnded && (sonic == null || sonic.samplesAvailable() == 0);
+ }
+
+ @Override
+ public void flush() {
+ sonic = new Sonic(sampleRateHz, channelCount);
+ sonic.setSpeed(speed);
+ sonic.setPitch(pitch);
+ outputBuffer = EMPTY_BUFFER;
+ inputBytes = 0;
+ outputBytes = 0;
+ inputEnded = false;
+ }
+
+ @Override
+ public void release() {
+ sonic = null;
+ buffer = EMPTY_BUFFER;
+ outputBuffer = EMPTY_BUFFER;
+ inputArray = EMPTY_ARRAY;
+ bufferArray = EMPTY_ARRAY;
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java
index 5f42a40e04..a10298e456 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.util;
+import com.google.android.exoplayer2.PlaybackParameters;
+
/**
* Tracks the progression of media time.
*/
@@ -25,4 +27,18 @@ public interface MediaClock {
*/
long getPositionUs();
+ /**
+ * Attempts to set the playback parameters and returns the active playback parameters, which may
+ * differ from those passed in.
+ *
+ * @param playbackParameters The playback parameters.
+ * @return The active playback parameters.
+ */
+ PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters);
+
+ /**
+ * Returns the active playback parameters.
+ */
+ PlaybackParameters getPlaybackParameters();
+
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java
index 0ae9b40869..5b8d117dd0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java
@@ -16,33 +16,34 @@
package com.google.android.exoplayer2.util;
import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.PlaybackParameters;
/**
- * A standalone {@link MediaClock}. The clock can be started, stopped and its time can be set and
- * retrieved. When started, this clock is based on {@link SystemClock#elapsedRealtime()}.
+ * A {@link MediaClock} whose position advances with real time based on the playback parameters when
+ * started.
*/
public final class StandaloneMediaClock implements MediaClock {
private boolean started;
+ private long baseUs;
+ private long baseElapsedMs;
+ private PlaybackParameters playbackParameters;
/**
- * The media time when the clock was last set or stopped.
+ * Creates a new standalone media clock.
*/
- private long positionUs;
-
- /**
- * The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs}
- * when the clock was last set or started.
- */
- private long deltaUs;
+ public StandaloneMediaClock() {
+ playbackParameters = PlaybackParameters.DEFAULT;
+ }
/**
* Starts the clock. Does nothing if the clock is already started.
*/
public void start() {
if (!started) {
+ baseElapsedMs = SystemClock.elapsedRealtime();
started = true;
- deltaUs = elapsedRealtimeMinus(positionUs);
}
}
@@ -51,26 +52,60 @@ public final class StandaloneMediaClock implements MediaClock {
*/
public void stop() {
if (started) {
- positionUs = elapsedRealtimeMinus(deltaUs);
+ setPositionUs(getPositionUs());
started = false;
}
}
/**
- * @param timeUs The position to set in microseconds.
+ * Sets the clock's position.
+ *
+ * @param positionUs The position to set in microseconds.
*/
- public void setPositionUs(long timeUs) {
- this.positionUs = timeUs;
- deltaUs = elapsedRealtimeMinus(timeUs);
+ public void setPositionUs(long positionUs) {
+ baseUs = positionUs;
+ if (started) {
+ baseElapsedMs = SystemClock.elapsedRealtime();
+ }
+ }
+
+ /**
+ * Synchronizes this clock with the current state of {@code clock}.
+ *
+ * @param clock The clock with which to synchronize.
+ */
+ public void synchronize(MediaClock clock) {
+ setPositionUs(clock.getPositionUs());
+ playbackParameters = clock.getPlaybackParameters();
}
@Override
public long getPositionUs() {
- return started ? elapsedRealtimeMinus(deltaUs) : positionUs;
+ long positionUs = baseUs;
+ if (started) {
+ long elapsedSinceBaseMs = SystemClock.elapsedRealtime() - baseElapsedMs;
+ if (playbackParameters.speed == 1f) {
+ positionUs += C.msToUs(elapsedSinceBaseMs);
+ } else {
+ positionUs += playbackParameters.getSpeedAdjustedDurationUs(elapsedSinceBaseMs);
+ }
+ }
+ return positionUs;
}
- private long elapsedRealtimeMinus(long toSubtractUs) {
- return SystemClock.elapsedRealtime() * 1000 - toSubtractUs;
+ @Override
+ public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ // Store the current position as the new base, in case the playback speed has changed.
+ if (started) {
+ setPositionUs(getPositionUs());
+ }
+ this.playbackParameters = playbackParameters;
+ return playbackParameters;
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return playbackParameters;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
index d9282700d7..e481066720 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
@@ -309,6 +309,18 @@ public final class Util {
return Math.max(min, Math.min(value, max));
}
+ /**
+ * Constrains a value to the specified bounds.
+ *
+ * @param value The value to constrain.
+ * @param min The lower bound.
+ * @param max The upper bound.
+ * @return The constrained value {@code Math.max(min, Math.min(value, max))}.
+ */
+ public static float constrainValue(float value, float min, float max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
/**
* Returns the index of the largest element in {@code array} that is less than (or optionally
* equal to) a specified {@code value}.
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java
index 1bf5b59a4a..38c7a5be9c 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java
@@ -19,6 +19,7 @@ import android.widget.TextView;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.decoder.DecoderCounters;
@@ -90,6 +91,11 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe
updateAndPost();
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing.
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
index 88a65589fc..757252297d 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java
@@ -29,6 +29,7 @@ import android.widget.TextView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@@ -766,6 +767,11 @@ public class PlaybackControlView extends FrameLayout {
updateProgress();
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
updateNavigation();
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java
index 2fa9c29fce..9d221be60a 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java
@@ -34,6 +34,7 @@ import android.widget.ImageView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.metadata.Metadata;
@@ -741,6 +742,11 @@ public final class SimpleExoPlayerView extends FrameLayout {
// Do nothing.
}
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing.
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java
index 87c55e9248..b13cfbdd15 100644
--- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java
+++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java
@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
@@ -223,6 +224,11 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen
// Do nothing.
}
+ @Override
+ public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ // Do nothing.
+ }
+
@Override
public final void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing.