summaryrefslogtreecommitdiffstats
path: root/variablespeed/src/com/android/ex/variablespeed
diff options
context:
space:
mode:
authorHugo Hudson <hugohudson@google.com>2011-07-14 23:31:17 +0100
committerHugo Hudson <hugohudson@google.com>2011-07-15 17:32:09 +0100
commitb83ad73794088498d6d38cd3b4fc9311f505d051 (patch)
tree678afd4df3c8fc56af51247f21ee3bc413b681e5 /variablespeed/src/com/android/ex/variablespeed
parent409fc12c707e39b55303251b728787ee5147b468 (diff)
downloadandroid_frameworks_ex-b83ad73794088498d6d38cd3b4fc9311f505d051.tar.gz
android_frameworks_ex-b83ad73794088498d6d38cd3b4fc9311f505d051.tar.bz2
android_frameworks_ex-b83ad73794088498d6d38cd3b4fc9311f505d051.zip
Initial check-in of variable speed playback library.
Contains an implementation of time-domain audio scaler, for pitch-invariant speed up and slow-down of audio. Contains wrapper library using OpenSLES to pump audio from encoded stream (mp3 file etc) through audio decoder then through time scaler and out to media player. This is written as a jni library with jni hooks to allow driving of this from the Java side. The other part of this cl is the Java wrapper. There is a new interface MediaPlayerProxy, containing a subset of the methods found on the MediaPlayer. The VariableSpeed class provides a concrete implementation of this interface adapting to the jni code. Change-Id: I518d8bf703488628c00730241a08ebfb67588ca6
Diffstat (limited to 'variablespeed/src/com/android/ex/variablespeed')
-rw-r--r--variablespeed/src/com/android/ex/variablespeed/EngineParameters.java173
-rw-r--r--variablespeed/src/com/android/ex/variablespeed/MediaPlayerDataSource.java68
-rw-r--r--variablespeed/src/com/android/ex/variablespeed/MediaPlayerProxy.java48
-rw-r--r--variablespeed/src/com/android/ex/variablespeed/SingleThreadedMediaPlayerProxy.java105
-rw-r--r--variablespeed/src/com/android/ex/variablespeed/VariableSpeed.java394
-rw-r--r--variablespeed/src/com/android/ex/variablespeed/VariableSpeedNative.java92
6 files changed, 880 insertions, 0 deletions
diff --git a/variablespeed/src/com/android/ex/variablespeed/EngineParameters.java b/variablespeed/src/com/android/ex/variablespeed/EngineParameters.java
new file mode 100644
index 0000000..60cbea6
--- /dev/null
+++ b/variablespeed/src/com/android/ex/variablespeed/EngineParameters.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2011 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.android.ex.variablespeed;
+
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * Encapsulates the parameters required to configure the audio engine.
+ * <p>
+ * You should not need to use this class directly, it exists for the benefit of
+ * this package and the classes contained therein.
+ */
+@Immutable
+/*package*/ final class EngineParameters {
+ private final int mChannels;
+ private final int mSampleRate;
+ private final int mTargetFrames;
+ private final int mMaxPlayBufferCount;
+ private final float mWindowDuration;
+ private final float mWindowOverlapDuration;
+ private final float mInitialRate;
+ private final int mDecodeBufferInitialSize;
+ private final int mDecodeBufferMaxSize;
+ private final int mStartPositionMillis;
+
+ public int getChannels() {
+ return mChannels;
+ }
+
+ public int getSampleRate() {
+ return mSampleRate;
+ }
+
+ public int getTargetFrames() {
+ return mTargetFrames;
+ }
+
+ public int getMaxPlayBufferCount() {
+ return mMaxPlayBufferCount;
+ }
+
+ public float getWindowDuration() {
+ return mWindowDuration;
+ }
+
+ public float getWindowOverlapDuration() {
+ return mWindowOverlapDuration;
+ }
+
+ public float getInitialRate() {
+ return mInitialRate;
+ }
+
+ public int getDecodeBufferInitialSize() {
+ return mDecodeBufferInitialSize;
+ }
+
+ public int getDecodeBufferMaxSize() {
+ return mDecodeBufferMaxSize;
+ }
+
+ public int getStartPositionMillis() {
+ return mStartPositionMillis;
+ }
+
+ private EngineParameters(int channels, int sampleRate, int targetFrames,
+ int maxPlayBufferCount, float windowDuration, float windowOverlapDuration,
+ float initialRate, int decodeBufferInitialSize, int decodeBufferMaxSize,
+ int startPositionMillis) {
+ mChannels = channels;
+ mSampleRate = sampleRate;
+ mTargetFrames = targetFrames;
+ mMaxPlayBufferCount = maxPlayBufferCount;
+ mWindowDuration = windowDuration;
+ mWindowOverlapDuration = windowOverlapDuration;
+ mInitialRate = initialRate;
+ mDecodeBufferInitialSize = decodeBufferInitialSize;
+ mDecodeBufferMaxSize = decodeBufferMaxSize;
+ mStartPositionMillis = startPositionMillis;
+ }
+
+ /**
+ * We use the builder pattern to construct an {@link EngineParameters}
+ * object.
+ * <p>
+ * This class is not thread safe, you should confine its use to one thread
+ * or provide your own synchronization.
+ */
+ @NotThreadSafe
+ public static class Builder {
+ private int mChannels = 2;
+ private int mSampleRate = 44100;
+ private int mTargetFrames = 1000;
+ private int mMaxPlayBufferCount = 2;
+ private float mWindowDuration = 0.08f;
+ private float mWindowOverlapDuration = 0.008f;
+ private float mInitialRate = 1.0f;
+ private int mDecodeBufferInitialSize = 5 * 1024;
+ private int mDecodeBufferMaxSize = 20 * 1024;
+ private int mStartPositionMillis = 0;
+
+ public EngineParameters build() {
+ return new EngineParameters(mChannels, mSampleRate, mTargetFrames, mMaxPlayBufferCount,
+ mWindowDuration, mWindowOverlapDuration, mInitialRate,
+ mDecodeBufferInitialSize, mDecodeBufferMaxSize, mStartPositionMillis);
+ }
+
+ public Builder channels(int channels) {
+ mChannels = channels;
+ return this;
+ }
+
+ public Builder sampleRate(int sampleRate) {
+ mSampleRate = sampleRate;
+ return this;
+ }
+
+ public Builder targetFrames(int targetFrames) {
+ mTargetFrames = targetFrames;
+ return this;
+ }
+
+ public Builder maxPlayBufferCount(int maxPlayBufferCount) {
+ mMaxPlayBufferCount = maxPlayBufferCount;
+ return this;
+ }
+
+ public Builder windowDuration(int windowDuration) {
+ mWindowDuration = windowDuration;
+ return this;
+ }
+
+ public Builder windowOverlapDuration(int windowOverlapDuration) {
+ mWindowOverlapDuration = windowOverlapDuration;
+ return this;
+ }
+
+ public Builder initialRate(float initialRate) {
+ mInitialRate = initialRate;
+ return this;
+ }
+
+ public Builder decodeBufferInitialSize(int decodeBufferInitialSize) {
+ mDecodeBufferInitialSize = decodeBufferInitialSize;
+ return this;
+ }
+
+ public Builder decodeBufferMaxSize(int decodeBufferMaxSize) {
+ mDecodeBufferMaxSize = decodeBufferMaxSize;
+ return this;
+ }
+
+ public Builder startPositionMillis(int startPositionMillis) {
+ mStartPositionMillis = startPositionMillis;
+ return this;
+ }
+ }
+}
diff --git a/variablespeed/src/com/android/ex/variablespeed/MediaPlayerDataSource.java b/variablespeed/src/com/android/ex/variablespeed/MediaPlayerDataSource.java
new file mode 100644
index 0000000..1c6a8cb
--- /dev/null
+++ b/variablespeed/src/com/android/ex/variablespeed/MediaPlayerDataSource.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2011 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.android.ex.variablespeed;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.net.Uri;
+
+import java.io.IOException;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Encapsulates the data source for a media player.
+ * <p>
+ * Is used to make the setting of the data source for a
+ * {@link android.media.MediaPlayer} easier, or the calling of the correct
+ * {@link VariableSpeedNative} method done correctly. You should not use this class
+ * directly, it is for the benefit of the {@link VariableSpeed} implementation.
+ */
+@Immutable
+/*package*/ class MediaPlayerDataSource {
+ private final Context mContext;
+ private final Uri mUri;
+ private final String mPath;
+
+ public MediaPlayerDataSource(Context context, Uri intentUri) {
+ mContext = context;
+ mUri = intentUri;
+ mPath = null;
+ }
+
+ public MediaPlayerDataSource(String path) {
+ mContext = null;
+ mUri = null;
+ mPath = path;
+ }
+
+ public void setAsSourceFor(MediaPlayer mediaPlayer) throws IOException {
+ if (mContext != null) {
+ mediaPlayer.setDataSource(mContext, mUri);
+ } else {
+ mediaPlayer.setDataSource(mPath);
+ }
+ }
+
+ public void playNative() throws IOException {
+ if (mContext != null) {
+ VariableSpeedNative.playFromContext(mContext, mUri);
+ } else {
+ VariableSpeedNative.playUri(mPath);
+ }
+ }
+}
diff --git a/variablespeed/src/com/android/ex/variablespeed/MediaPlayerProxy.java b/variablespeed/src/com/android/ex/variablespeed/MediaPlayerProxy.java
new file mode 100644
index 0000000..76492c1
--- /dev/null
+++ b/variablespeed/src/com/android/ex/variablespeed/MediaPlayerProxy.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 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.android.ex.variablespeed;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.net.Uri;
+
+import java.io.IOException;
+
+/**
+ * Interface that supports a subset of the operations on {@link android.media.MediaPlayer}.
+ *
+ * <p>This subset is arbitrarily defined - at the moment it is the subset that the voicemail
+ * playback requires.</p>
+ *
+ * <p>This interface exists to make alternate implementations to the standard media player
+ * swappable, as well as making it much easier to test code that directly uses a media player.
+ */
+public interface MediaPlayerProxy {
+ void setOnErrorListener(MediaPlayer.OnErrorListener listener);
+ void setOnCompletionListener(MediaPlayer.OnCompletionListener listener);
+ void release();
+ void reset();
+ void setDataSource(String path) throws IllegalStateException, IOException;
+ void setDataSource(Context context, Uri intentUri) throws IllegalStateException, IOException;
+ void prepare() throws IOException;
+ int getDuration();
+ void seekTo(int startPosition);
+ void start();
+ boolean isPlaying();
+ int getCurrentPosition();
+ void pause();
+}
diff --git a/variablespeed/src/com/android/ex/variablespeed/SingleThreadedMediaPlayerProxy.java b/variablespeed/src/com/android/ex/variablespeed/SingleThreadedMediaPlayerProxy.java
new file mode 100644
index 0000000..035bc0e
--- /dev/null
+++ b/variablespeed/src/com/android/ex/variablespeed/SingleThreadedMediaPlayerProxy.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2011 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.android.ex.variablespeed;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.net.Uri;
+
+import java.io.IOException;
+
+/**
+ * Simple wrapper around a {@link MediaPlayerProxy}, guaranteeing that every call made to the
+ * MediaPlayerProxy is single-threaded.
+ */
+public class SingleThreadedMediaPlayerProxy implements MediaPlayerProxy {
+ private final MediaPlayerProxy mDelegate;
+
+ public SingleThreadedMediaPlayerProxy(MediaPlayerProxy delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ public synchronized void setOnErrorListener(MediaPlayer.OnErrorListener listener) {
+ mDelegate.setOnErrorListener(listener);
+ }
+
+ @Override
+ public synchronized void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) {
+ mDelegate.setOnCompletionListener(listener);
+ }
+
+ @Override
+ public synchronized void release() {
+ mDelegate.release();
+ }
+
+ @Override
+ public synchronized void reset() {
+ mDelegate.reset();
+ }
+
+ @Override
+ public synchronized void setDataSource(String path) throws IllegalStateException, IOException {
+ mDelegate.setDataSource(path);
+ }
+
+ @Override
+ public synchronized void setDataSource(Context context, Uri intentUri)
+ throws IllegalStateException, IOException {
+ mDelegate.setDataSource(context, intentUri);
+ }
+
+ @Override
+ public synchronized void prepare() throws IOException {
+ mDelegate.prepare();
+ }
+
+ @Override
+ public synchronized int getDuration() {
+ return mDelegate.getDuration();
+ }
+
+ @Override
+ public synchronized void seekTo(int startPosition) {
+ mDelegate.seekTo(startPosition);
+ }
+
+ @Override
+ public synchronized void start() {
+ mDelegate.start();
+ }
+
+ @Override
+ public synchronized boolean isPlaying() {
+ return mDelegate.isPlaying();
+ }
+
+ @Override
+ public synchronized int getCurrentPosition() {
+ return mDelegate.getCurrentPosition();
+ }
+
+ public void setVariableSpeed(float rate) {
+ ((VariableSpeed) mDelegate).setVariableSpeed(rate);
+ }
+
+ @Override
+ public synchronized void pause() {
+ mDelegate.pause();
+ }
+}
diff --git a/variablespeed/src/com/android/ex/variablespeed/VariableSpeed.java b/variablespeed/src/com/android/ex/variablespeed/VariableSpeed.java
new file mode 100644
index 0000000..b3b3efb
--- /dev/null
+++ b/variablespeed/src/com/android/ex/variablespeed/VariableSpeed.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2011 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.android.ex.variablespeed;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.net.Uri;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * This class behaves in a similar fashion to the MediaPlayer, but by using
+ * native code it is able to use variable-speed playback.
+ * <p>
+ * This class is thread-safe. It's not yet perfect though, see the unit tests
+ * for details - there is insufficient testing for the concurrent logic. You are
+ * probably best advised to use thread confinment until the unit tests are more
+ * complete with regards to threading.
+ * <p>
+ * The easiest way to ensure that calls to this class are not made concurrently
+ * (besides only ever accessing it from one thread) is to wrap it in a
+ * {@link SingleThreadedMediaPlayerProxy}, designed just for this purpose.
+ */
+// TODO: There are a couple of NYI still to iron out in this file.
+@ThreadSafe
+public class VariableSpeed implements MediaPlayerProxy {
+ private final Executor mExecutor;
+ private final Object lock = new Object();
+ @GuardedBy("lock") private MediaPlayerDataSource mDataSource;
+ @GuardedBy("lock") private boolean mIsPrepared;
+ @GuardedBy("lock") private boolean mHasDuration;
+ @GuardedBy("lock") private boolean mHasStartedPlayback;
+ @GuardedBy("lock") private CountDownLatch mEngineInitializedLatch;
+ @GuardedBy("lock") private CountDownLatch mPlaybackFinishedLatch;
+ @GuardedBy("lock") private boolean mHasBeenReleased = true;
+ @GuardedBy("lock") private boolean mIsReadyToReUse = true;
+ @GuardedBy("lock") private boolean mSkipCompletionReport;
+ @GuardedBy("lock") private int mStartPosition;
+ @GuardedBy("lock") private float mCurrentPlaybackRate = 1.0f;
+ @GuardedBy("lock") private int mDuration;
+ @GuardedBy("lock") private MediaPlayer.OnCompletionListener mCompletionListener;
+
+ private VariableSpeed(Executor executor) throws UnsupportedOperationException {
+ mExecutor = executor;
+ try {
+ VariableSpeedNative.loadLibrary();
+ } catch (UnsatisfiedLinkError e) {
+ throw new UnsupportedOperationException("could not load library", e);
+ } catch (SecurityException e) {
+ throw new UnsupportedOperationException("could not load library", e);
+ }
+ reset();
+ }
+
+ public static MediaPlayerProxy createVariableSpeed(Executor executor)
+ throws UnsupportedOperationException {
+ return new SingleThreadedMediaPlayerProxy(new VariableSpeed(executor));
+ }
+
+ @Override
+ public void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) {
+ synchronized (lock) {
+ check(!mHasBeenReleased, "has been released, reset before use");
+ mCompletionListener = listener;
+ }
+ }
+
+ @Override
+ public void setOnErrorListener(MediaPlayer.OnErrorListener listener) {
+ synchronized (lock) {
+ check(!mHasBeenReleased, "has been released, reset before use");
+ // NYI
+ }
+ }
+
+ @Override
+ public void release() {
+ synchronized (lock) {
+ if (mHasBeenReleased) {
+ return;
+ }
+ mHasBeenReleased = true;
+ }
+ stopCurrentPlayback();
+ boolean requiresShutdown = false;
+ synchronized (lock) {
+ requiresShutdown = hasEngineBeenInitialized();
+ }
+ if (requiresShutdown) {
+ VariableSpeedNative.shutdownEngine();
+ }
+ synchronized (lock) {
+ mIsReadyToReUse = true;
+ }
+ }
+
+ private boolean hasEngineBeenInitialized() {
+ return mEngineInitializedLatch.getCount() <= 0;
+ }
+
+ private boolean hasPlaybackFinished() {
+ return mPlaybackFinishedLatch.getCount() <= 0;
+ }
+
+ /**
+ * Stops the current playback, returns once it has stopped.
+ */
+ private void stopCurrentPlayback() {
+ boolean isPlaying;
+ CountDownLatch engineInitializedLatch;
+ CountDownLatch playbackFinishedLatch;
+ synchronized (lock) {
+ isPlaying = mHasStartedPlayback && !hasPlaybackFinished();
+ engineInitializedLatch = mEngineInitializedLatch;
+ playbackFinishedLatch = mPlaybackFinishedLatch;
+ if (isPlaying) {
+ mSkipCompletionReport = true;
+ }
+ }
+ if (isPlaying) {
+ waitForLatch(engineInitializedLatch);
+ VariableSpeedNative.stopPlayback();
+ waitForLatch(playbackFinishedLatch);
+ }
+ }
+
+ private void waitForLatch(CountDownLatch latch) {
+ try {
+ boolean success = latch.await(10, TimeUnit.SECONDS);
+ if (!success) {
+ reportException(new TimeoutException("waited too long"));
+ }
+ } catch (InterruptedException e) {
+ // Preserve the interrupt status, though this is unexpected.
+ Thread.currentThread().interrupt();
+ reportException(e);
+ }
+ }
+
+ @Override
+ public void setDataSource(Context context, Uri intentUri) {
+ checkNotNull(context, "context");
+ checkNotNull(intentUri, "intentUri");
+ innerSetDataSource(new MediaPlayerDataSource(context, intentUri));
+ }
+
+ @Override
+ public void setDataSource(String path) {
+ checkNotNull(path, "path");
+ innerSetDataSource(new MediaPlayerDataSource(path));
+ }
+
+ private void innerSetDataSource(MediaPlayerDataSource source) {
+ checkNotNull(source, "source");
+ synchronized (lock) {
+ check(!mHasBeenReleased, "has been released, reset before use");
+ check(mDataSource == null, "cannot setDataSource more than once");
+ mDataSource = source;
+ }
+ }
+
+ @Override
+ public void reset() {
+ boolean requiresRelease;
+ synchronized (lock) {
+ requiresRelease = !mHasBeenReleased;
+ }
+ if (requiresRelease) {
+ release();
+ }
+ synchronized (lock) {
+ check(mHasBeenReleased && mIsReadyToReUse, "to re-use, must call reset after release");
+ mDataSource = null;
+ mIsPrepared = false;
+ mHasDuration = false;
+ mHasStartedPlayback = false;
+ mEngineInitializedLatch = new CountDownLatch(1);
+ mPlaybackFinishedLatch = new CountDownLatch(1);
+ mHasBeenReleased = false;
+ mIsReadyToReUse = false;
+ mSkipCompletionReport = false;
+ mStartPosition = 0;
+ mDuration = 0;
+ }
+ }
+
+ @Override
+ public void prepare() throws IOException {
+ MediaPlayerDataSource dataSource;
+ synchronized (lock) {
+ check(!mHasBeenReleased, "has been released, reset before use");
+ check(mDataSource != null, "must setDataSource before you prepare");
+ check(!mIsPrepared, "cannot prepare more than once");
+ mIsPrepared = true;
+ dataSource = mDataSource;
+ }
+ // NYI This should become another executable that we can wait on.
+ MediaPlayer mediaPlayer = new MediaPlayer();
+ dataSource.setAsSourceFor(mediaPlayer);
+ mediaPlayer.prepare();
+ synchronized (lock) {
+ check(!mHasDuration, "can't have duration, this is impossible");
+ mHasDuration = true;
+ mDuration = mediaPlayer.getDuration();
+ }
+ mediaPlayer.release();
+ }
+
+ @Override
+ public int getDuration() {
+ synchronized (lock) {
+ check(!mHasBeenReleased, "has been released, reset before use");
+ check(mHasDuration, "you haven't called prepare, can't get the duration");
+ return mDuration;
+ }
+ }
+
+ @Override
+ public void seekTo(int startPosition) {
+ boolean currentlyPlaying;
+ MediaPlayerDataSource dataSource;
+ synchronized (lock) {
+ check(!mHasBeenReleased, "has been released, reset before use");
+ check(mHasDuration, "you can't seek until you have prepared");
+ currentlyPlaying = mHasStartedPlayback && !hasPlaybackFinished();
+ mStartPosition = Math.min(startPosition, mDuration);
+ dataSource = mDataSource;
+ }
+ if (currentlyPlaying) {
+ stopAndStartPlayingAgain(dataSource);
+ }
+ }
+
+ private void stopAndStartPlayingAgain(MediaPlayerDataSource source) {
+ stopCurrentPlayback();
+ reset();
+ innerSetDataSource(source);
+ try {
+ prepare();
+ } catch (IOException e) {
+ reportException(e);
+ return;
+ }
+ start();
+ return;
+ }
+
+ private void reportException(Exception e) {
+ // NYI
+ e.printStackTrace(System.err);
+ }
+
+ @Override
+ public void start() {
+ MediaPlayerDataSource restartWithThisDataSource = null;
+ synchronized (lock) {
+ check(!mHasBeenReleased, "has been released, reset before use");
+ check(mIsPrepared, "must have prepared before you can start");
+ if (!mHasStartedPlayback) {
+ // Playback has not started. Start it.
+ mHasStartedPlayback = true;
+ // TODO: This will be dynamically calculated soon, waiting for a bugfix in media.
+ EngineParameters engineParameters = new EngineParameters.Builder()
+ .sampleRate(11025).channels(1)
+// .sampleRate(44100).channels(2)
+ .initialRate(mCurrentPlaybackRate)
+ .startPositionMillis(mStartPosition).build();
+ mExecutor.execute(new PlaybackRunnable(mDataSource, engineParameters));
+ } else {
+ // Playback has already started. Restart it, without holding the
+ // lock.
+ restartWithThisDataSource = mDataSource;
+ }
+ }
+ if (restartWithThisDataSource != null) {
+ stopAndStartPlayingAgain(restartWithThisDataSource);
+ }
+ }
+
+ /** A Runnable capable of driving the native audio playback methods. */
+ private final class PlaybackRunnable implements Runnable {
+ private final MediaPlayerDataSource mInnerSource;
+ private final EngineParameters mEngineParameters;
+
+ public PlaybackRunnable(MediaPlayerDataSource source, EngineParameters engineParameters) {
+ mInnerSource = source;
+ mEngineParameters = engineParameters;
+ }
+
+ @Override
+ public void run() {
+ synchronized (lock) {
+ VariableSpeedNative.initializeEngine(mEngineParameters);
+ mEngineInitializedLatch.countDown();
+ }
+ try {
+ VariableSpeedNative.startPlayback();
+ mInnerSource.playNative();
+ } catch (IOException e) {
+ // NYI exception handling.
+ }
+ MediaPlayer.OnCompletionListener completionListener;
+ boolean skipThisCompletionReport;
+ synchronized (lock) {
+ completionListener = mCompletionListener;
+ skipThisCompletionReport = mSkipCompletionReport;
+ mPlaybackFinishedLatch.countDown();
+ }
+ if (!skipThisCompletionReport && completionListener != null) {
+ completionListener.onCompletion(null);
+ }
+ // NYI exception handling.
+ }
+ }
+
+ @Override
+ public boolean isPlaying() {
+ synchronized (lock) {
+ return mHasStartedPlayback && !hasPlaybackFinished();
+ }
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ synchronized (lock) {
+ if (mHasBeenReleased) {
+ return 0;
+ }
+ if (!mHasStartedPlayback) {
+ return 0;
+ }
+ if (!hasEngineBeenInitialized()) {
+ return 0;
+ }
+ if (!hasPlaybackFinished()) {
+ return VariableSpeedNative.getCurrentPosition();
+ }
+ return mDuration;
+ }
+ }
+
+ @Override
+ public void pause() {
+ synchronized (lock) {
+ check(!mHasBeenReleased, "has been released, reset before use");
+ }
+ stopCurrentPlayback();
+ }
+
+ public void setVariableSpeed(float rate) {
+ // NYI are there situations in which the engine has been destroyed, so
+ // that this will segfault?
+ synchronized (lock) {
+ check(!mHasBeenReleased, "has been released, reset before use");
+ if (mHasStartedPlayback) {
+ VariableSpeedNative.setVariableSpeed(rate);
+ }
+ mCurrentPlaybackRate = rate;
+ }
+ }
+
+ private void check(boolean condition, String exception) {
+ if (!condition) {
+ throw new IllegalStateException(exception);
+ }
+ }
+
+ private void checkNotNull(Object argument, String argumentName) {
+ if (argument == null) {
+ throw new IllegalArgumentException(argumentName + " must not be null");
+ }
+ }
+}
diff --git a/variablespeed/src/com/android/ex/variablespeed/VariableSpeedNative.java b/variablespeed/src/com/android/ex/variablespeed/VariableSpeedNative.java
new file mode 100644
index 0000000..90fc49e
--- /dev/null
+++ b/variablespeed/src/com/android/ex/variablespeed/VariableSpeedNative.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 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.android.ex.variablespeed;
+
+import com.android.common.io.MoreCloseables;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.lang.reflect.Field;
+
+/**
+ * Provides all the native calls through to the underlying audio library.
+ * <p>
+ * You should not use this class directly. Prefer to use the {@link VariableSpeed}
+ * class instead.
+ */
+/*package*/ class VariableSpeedNative {
+ /*package*/ static void loadLibrary() throws UnsatisfiedLinkError, SecurityException {
+ System.loadLibrary("variablespeed");
+ }
+
+ /*package*/ static boolean playFromContext(Context context, Uri uri)
+ throws FileNotFoundException {
+ AssetFileDescriptor afd = context.getContentResolver().openAssetFileDescriptor(uri, "r");
+ try {
+ FileDescriptor fileDescriptor = afd.getFileDescriptor();
+ Field descriptorField = fileDescriptor.getClass().getDeclaredField("descriptor");
+ descriptorField.setAccessible(true);
+ int fd = descriptorField.getInt(fileDescriptor);
+ VariableSpeedNative.playFileDescriptor(fd, afd.getStartOffset(), afd.getLength());
+ return true;
+ } catch (SecurityException e) {
+ // Fall through.
+ } catch (NoSuchFieldException e) {
+ // Fall through.
+ } catch (IllegalArgumentException e) {
+ // Fall through.
+ } catch (IllegalAccessException e) {
+ // Fall through.
+ } finally {
+ MoreCloseables.closeQuietly(afd);
+ }
+ return false;
+ }
+
+ /*package*/ static native void playUri(String uri);
+
+ /*package*/ static native void playFileDescriptor(int fd, long offset, long length);
+
+ /*package*/ static native void setVariableSpeed(float speed);
+
+ /*package*/ static native void startPlayback();
+
+ /*package*/ static native void stopPlayback();
+
+ /*package*/ static native void shutdownEngine();
+
+ /*package*/ static native int getCurrentPosition();
+
+ /*package*/ static native int getTotalDuration();
+
+ /*package*/ static void initializeEngine(EngineParameters params) {
+ initializeEngine(params.getChannels(), params.getSampleRate(), params.getTargetFrames(),
+ params.getWindowDuration(), params.getWindowOverlapDuration(),
+ params.getMaxPlayBufferCount(), params.getInitialRate(),
+ params.getDecodeBufferInitialSize(), params.getDecodeBufferMaxSize(),
+ params.getStartPositionMillis());
+ }
+
+ private static native void initializeEngine(int channels, int sampleRate, int targetFrames,
+ float windowDuration, float windowOverlapDuration, int maxPlayBufferCount,
+ float initialRate, int decodeBufferInitialSize, int decodeBufferMaxSize,
+ int startPositionMillis);
+}