summaryrefslogtreecommitdiffstats
path: root/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
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')
-rw-r--r--variablespeed/Android.mk40
-rw-r--r--variablespeed/jni/Android.mk53
-rw-r--r--variablespeed/jni/decode_buffer.cc86
-rw-r--r--variablespeed/jni/decode_buffer.h59
-rw-r--r--variablespeed/jni/hlogging.h32
-rw-r--r--variablespeed/jni/integral_types.h33
-rw-r--r--variablespeed/jni/jni_entry.cc95
-rw-r--r--variablespeed/jni/macros.h66
-rw-r--r--variablespeed/jni/no_synchronization.h50
-rw-r--r--variablespeed/jni/profile_timer.h52
-rw-r--r--variablespeed/jni/ring_buffer.cc152
-rw-r--r--variablespeed/jni/ring_buffer.h117
-rw-r--r--variablespeed/jni/sola_time_scaler.cc366
-rw-r--r--variablespeed/jni/sola_time_scaler.h165
-rw-r--r--variablespeed/jni/variablespeed.cc800
-rw-r--r--variablespeed/jni/variablespeed.h152
-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
22 files changed, 3198 insertions, 0 deletions
diff --git a/variablespeed/Android.mk b/variablespeed/Android.mk
new file mode 100644
index 0000000..d5acbe0
--- /dev/null
+++ b/variablespeed/Android.mk
@@ -0,0 +1,40 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+# Build the java code, shared library.
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := android-ex-variablespeed
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-common \
+ guava \
+
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# Build the native code, library.
+
+include $(CLEAR_VARS)
+
+include $(LOCAL_PATH)/jni/Android.mk
diff --git a/variablespeed/jni/Android.mk b/variablespeed/jni/Android.mk
new file mode 100644
index 0000000..52b83aa
--- /dev/null
+++ b/variablespeed/jni/Android.mk
@@ -0,0 +1,53 @@
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+# TODO: Remove the .cc extension, just .cpp.
+# TODO: Change module name to use underscores not hyphens.
+# TODO: Work out if the log and android libs are needed.
+
+# Add in extra warnings.
+LOCAL_CFLAGS += -Wall
+LOCAL_CPPFLAGS += -Wall
+
+LOCAL_CPP_EXTENSION := .cc
+LOCAL_MODULE := libvariablespeed
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := \
+ variablespeed.cc \
+ ring_buffer.cc \
+ sola_time_scaler.cc \
+ jni_entry.cc \
+ decode_buffer.cc \
+
+LOCAL_C_INCLUDES := \
+ system/media/wilhelm/include \
+ external/stlport/stlport \
+ bionic \
+
+LOCAL_SHARED_LIBRARIES := \
+ libOpenSLES \
+ libstlport \
+ libutils \
+ libcutils \
+
+LOCAL_LDLIBS := \
+ -lOpenSLES \
+ -llog \
+ -landroid \
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/variablespeed/jni/decode_buffer.cc b/variablespeed/jni/decode_buffer.cc
new file mode 100644
index 0000000..a08cfd6
--- /dev/null
+++ b/variablespeed/jni/decode_buffer.cc
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+#include <decode_buffer.h>
+
+namespace {
+
+static const size_t kNumberOfBytesPerSample = 2;
+
+} // namespace
+
+DecodeBuffer::DecodeBuffer(size_t sizeOfOneBuffer, size_t maxSize)
+ : sizeOfOneBuffer_(sizeOfOneBuffer), maxSize_(maxSize),
+ start_(0), end_(0), advancedCount_(0), data_() {
+ Clear();
+}
+
+DecodeBuffer::~DecodeBuffer() {
+ Clear();
+}
+
+size_t DecodeBuffer::GetSizeInBytes() const {
+ return kNumberOfBytesPerSample * (end_ - start_);
+}
+
+bool DecodeBuffer::IsTooLarge() const {
+ return GetSizeInBytes() > maxSize_;
+}
+
+void DecodeBuffer::AddData(int8_t* pointer, size_t lengthInBytes) {
+ for (size_t i = 0; i < lengthInBytes / kNumberOfBytesPerSample; ++i) {
+ PushValue(reinterpret_cast<int16*>(pointer)[i]);
+ }
+}
+
+void DecodeBuffer::Clear() {
+ while (data_.size() > 0) {
+ delete[] data_.front();
+ data_.erase(data_.begin());
+ }
+ start_ = 0;
+ end_ = 0;
+ advancedCount_ = 0;
+}
+
+size_t DecodeBuffer::GetTotalAdvancedCount() const {
+ return advancedCount_;
+}
+
+void DecodeBuffer::AdvanceHeadPointerShorts(size_t numberOfShorts) {
+ start_ += numberOfShorts;
+ while (start_ > sizeOfOneBuffer_) {
+ data_.push_back(data_.front());
+ data_.erase(data_.begin());
+ start_ -= sizeOfOneBuffer_;
+ end_ -= sizeOfOneBuffer_;
+ }
+ advancedCount_ += numberOfShorts;
+}
+
+void DecodeBuffer::PushValue(int16 value) {
+ size_t bufferIndex = end_ / sizeOfOneBuffer_;
+ if (bufferIndex >= data_.size()) {
+ data_.push_back(new int16[sizeOfOneBuffer_]);
+ }
+ data_.at(bufferIndex)[end_ % sizeOfOneBuffer_] = value;
+ ++end_;
+}
+
+int16 DecodeBuffer::GetAtIndex(size_t index) {
+ return data_.at((start_ + index) / sizeOfOneBuffer_)
+ [(start_ + index) % sizeOfOneBuffer_];
+}
diff --git a/variablespeed/jni/decode_buffer.h b/variablespeed/jni/decode_buffer.h
new file mode 100644
index 0000000..2f1aa71
--- /dev/null
+++ b/variablespeed/jni/decode_buffer.h
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_EX_VARIABLESPEED_JNI_DECODE_BUFFER_H_
+#define FRAMEWORKS_EX_VARIABLESPEED_JNI_DECODE_BUFFER_H_
+
+#include <integral_types.h>
+#include <macros.h>
+#include <stdlib.h>
+#include <vector>
+
+// DecodeBuffer is used to store arrays of int16 values for audio.
+//
+// This class is not thread-safe. You should provide your own
+// synchronization if you wish to use it from multiple threads.
+class DecodeBuffer {
+ public:
+ DecodeBuffer(size_t sizeOfOneBuffer, size_t maxSize);
+ virtual ~DecodeBuffer();
+ size_t GetSizeInBytes() const;
+ void AddData(int8_t* pointer, size_t lengthInBytes);
+ void Clear();
+ void AdvanceHeadPointerShorts(size_t numberOfShorts);
+ int16 GetAtIndex(size_t index);
+ bool IsTooLarge() const;
+ size_t GetTotalAdvancedCount() const;
+
+ private:
+ void PushValue(int16 value);
+
+ size_t sizeOfOneBuffer_;
+ size_t maxSize_;
+ size_t start_;
+ size_t end_;
+ size_t advancedCount_;
+ // This vector isn't ideal because we perform a number of queue-like
+ // operations: namely removing from the front and appending at the back.
+ // However we also need constant-time access to the elements of this
+ // vector, and therefore it's not good enough to use a std::queue.
+ // In practice this data structure choice doesn't seem to be a bottleneck.
+ std::vector<int16*> data_;
+
+ DISALLOW_COPY_AND_ASSIGN(DecodeBuffer);
+};
+
+#endif // FRAMEWORKS_EX_VARIABLESPEED_JNI_DECODE_BUFFER_H_
diff --git a/variablespeed/jni/hlogging.h b/variablespeed/jni/hlogging.h
new file mode 100644
index 0000000..056f5a3
--- /dev/null
+++ b/variablespeed/jni/hlogging.h
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_EX_VARIABLESPEED_JNI_HLOGGING_H_
+#define FRAMEWORKS_EX_VARIABLESPEED_JNI_HLOGGING_H_
+
+#include <integral_types.h>
+#include <macros.h>
+#include <android/log.h>
+
+// Simple logging macros.
+#define LOG_TAG "VariableSpeed"
+#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
+#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+
+#endif // FRAMEWORKS_EX_VARIABLESPEED_JNI_HLOGGING_H_
diff --git a/variablespeed/jni/integral_types.h b/variablespeed/jni/integral_types.h
new file mode 100644
index 0000000..8927d7c
--- /dev/null
+++ b/variablespeed/jni/integral_types.h
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_EX_VARIABLESPEED_JNI_INTEGRAL_TYPES_H_
+#define FRAMEWORKS_EX_VARIABLESPEED_JNI_INTEGRAL_TYPES_H_
+
+#include <cstring> // for size_t
+
+// Standard typedefs
+typedef signed char schar;
+typedef signed char int8;
+typedef short int16; // NOLINT
+typedef int int32;
+typedef long long int64; // NOLINT
+typedef unsigned char uint8;
+typedef unsigned short uint16; // NOLINT
+typedef unsigned int uint32;
+typedef unsigned long long uint64; // NOLINT
+
+#endif // FRAMEWORKS_EX_VARIABLESPEED_JNI_INTEGRAL_TYPES_H_
diff --git a/variablespeed/jni/jni_entry.cc b/variablespeed/jni/jni_entry.cc
new file mode 100644
index 0000000..d751b09
--- /dev/null
+++ b/variablespeed/jni/jni_entry.cc
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+#include <stdlib.h>
+#include <assert.h>
+
+#include <jni.h>
+#include <variablespeed.h>
+
+// Quick #define to make sure I get all the JNI method calls right.
+#define JNI_METHOD(x, y) \
+JNIEXPORT y JNICALL \
+Java_com_android_ex_variablespeed_VariableSpeedNative_##x
+
+class MethodLog {
+ public:
+ explicit MethodLog(const char* name) : name_(name) {
+ LOGD("+ %s", name);
+ }
+ virtual ~MethodLog() {
+ LOGD("- %s", name_);
+ }
+
+ private:
+ const char* name_;
+};
+
+extern "C" {
+JNI_METHOD(playFileDescriptor, void) (JNIEnv*, jclass, int fd, jlong offset,
+ jlong length) {
+ MethodLog _("playFileDescriptor");
+ AudioEngine::GetEngine()->PlayFileDescriptor(fd, offset, length);
+}
+
+JNI_METHOD(playUri, void) (JNIEnv* env, jclass, jstring uri) {
+ MethodLog _("playUri");
+ const char* utf8 = env->GetStringUTFChars(uri, NULL);
+ CHECK(NULL != utf8);
+ AudioEngine::GetEngine()->PlayUri(utf8);
+}
+
+JNI_METHOD(setVariableSpeed, void) (JNIEnv*, jclass, float speed) {
+ MethodLog _("setVariableSpeed");
+ AudioEngine::GetEngine()->SetVariableSpeed(speed);
+}
+
+JNI_METHOD(startPlayback, void) (JNIEnv*, jclass) {
+ MethodLog _("startPlayback");
+ AudioEngine::GetEngine()->RequestStart();
+}
+
+JNI_METHOD(stopPlayback, void) (JNIEnv*, jclass) {
+ MethodLog _("stopPlayback");
+ AudioEngine::GetEngine()->RequestStop();
+}
+
+JNI_METHOD(getCurrentPosition, int) (JNIEnv*, jclass) {
+ MethodLog _("getCurrentPosition");
+ return AudioEngine::GetEngine()->GetCurrentPosition();
+}
+
+JNI_METHOD(getTotalDuration, int) (JNIEnv*, jclass) {
+ MethodLog _("getTotalDuration");
+ return AudioEngine::GetEngine()->GetTotalDuration();
+}
+
+JNI_METHOD(initializeEngine, void) (JNIEnv*, jclass, int channels,
+ int sampleRate, int targetFrames, float windowDuration,
+ float windowOverlapDuration, size_t maxPlayBufferCount,
+ float initialRate, size_t decodeInitialSize, size_t decodeMaxSize,
+ size_t startPositionMillis) {
+ MethodLog _("initializeEngine");
+ AudioEngine::SetEngine(new AudioEngine(channels, sampleRate, targetFrames,
+ windowDuration, windowOverlapDuration, maxPlayBufferCount, initialRate,
+ decodeInitialSize, decodeMaxSize, startPositionMillis));
+}
+
+JNI_METHOD(shutdownEngine, void) (JNIEnv*, jclass) {
+ MethodLog _("shutdownEngine");
+ AudioEngine::DeleteEngine();
+}
+} // extern "C"
diff --git a/variablespeed/jni/macros.h b/variablespeed/jni/macros.h
new file mode 100644
index 0000000..c08d44b
--- /dev/null
+++ b/variablespeed/jni/macros.h
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_EX_VARIABLESPEED_JNI_MACROS_H_
+#define FRAMEWORKS_EX_VARIABLESPEED_JNI_MACROS_H_
+
+#include <hlogging.h>
+
+inline float min(float a, float b) {
+ return (a < b) ? a : b;
+}
+
+inline float max(float a, float b) {
+ return (a > b) ? a : b;
+}
+
+template <class ForwardIterator>
+ ForwardIterator min_element(ForwardIterator first, ForwardIterator last) {
+ ForwardIterator lowest = first;
+ if (first == last) return last;
+ while (++first != last)
+ if (*first < *lowest)
+ lowest = first;
+ return lowest;
+}
+
+// A macro to disallow the copy constructor and operator= functions
+// This should be used in the private: declarations for a class
+#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
+ TypeName(const TypeName&); \
+ void operator=(const TypeName&)
+
+#define CHECK(x) { \
+ if (!(x)) { \
+ LOGE("assertion failed: " #x); \
+ LOGE("file: %s line: %d", __FILE__, __LINE__); \
+ int* frob = NULL; \
+ *frob = 5; \
+ } \
+}
+
+template <class Dest, class Source>
+inline Dest bit_cast(const Source& source) {
+ // Compile time assertion: sizeof(Dest) == sizeof(Source)
+ // A compile error here means your Dest and Source have different sizes.
+ typedef char VerifySizesAreEqual [sizeof(Dest) == sizeof(Source) ? 1 : -1]; // NOLINT
+
+ Dest dest;
+ memcpy(&dest, &source, sizeof(dest));
+ return dest;
+}
+
+#endif // FRAMEWORKS_EX_VARIABLESPEED_JNI_MACROS_H_
diff --git a/variablespeed/jni/no_synchronization.h b/variablespeed/jni/no_synchronization.h
new file mode 100644
index 0000000..751ebd9
--- /dev/null
+++ b/variablespeed/jni/no_synchronization.h
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_EX_VARIABLESPEED_JNI_NO_SYNCHRONIZATION_H_
+#define FRAMEWORKS_EX_VARIABLESPEED_JNI_NO_SYNCHRONIZATION_H_
+
+#include <macros.h>
+
+// We don't need any synchronization at the moment.
+// The sola_time_scaler (which is the code that uses this mutex class) is
+// currently being used in a single-threaded manner, driven from the main
+// PlayFromThisSource method in variablespeed.
+// As such no locking is actually required, and so this class contains a
+// fake mutex that does nothing.
+
+class Mutex {
+ public:
+ Mutex() {}
+ virtual ~Mutex() {}
+ void Lock() {}
+ void Unlock() {}
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(Mutex);
+};
+
+class MutexLock {
+ public:
+ explicit MutexLock(Mutex* mu) : mu_(mu) {}
+ virtual ~MutexLock() {}
+
+ private:
+ Mutex* const mu_;
+ DISALLOW_COPY_AND_ASSIGN(MutexLock);
+};
+
+#endif // FRAMEWORKS_EX_VARIABLESPEED_JNI_NO_SYNCHRONIZATION_H_
diff --git a/variablespeed/jni/profile_timer.h b/variablespeed/jni/profile_timer.h
new file mode 100644
index 0000000..6844cb2
--- /dev/null
+++ b/variablespeed/jni/profile_timer.h
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_EX_VARIABLESPEED_JNI_PROFILE_TIMER_H_
+#define FRAMEWORKS_EX_VARIABLESPEED_JNI_PROFILE_TIMER_H_
+
+#include <hlogging.h>
+#include <time.h>
+
+#include <string>
+
+// Simple profiler for debugging method call duration.
+class Timer {
+ public:
+ Timer() : startTime_(clock()) {
+ }
+
+ virtual ~Timer() {
+ PrintElapsed("destructor");
+ }
+
+ void PrintElapsed(const char* message) {
+ clock_t endTime(clock());
+ LOGD("Timer(%s): %d ms", message,
+ static_cast<int>((endTime - startTime_) * 1000 / CLOCKS_PER_SEC));
+ }
+
+ size_t GetElapsed() {
+ clock_t endTime(clock());
+ return (endTime - startTime_) * 1000 / CLOCKS_PER_SEC;
+ }
+
+ private:
+ clock_t startTime_;
+
+ DISALLOW_COPY_AND_ASSIGN(Timer);
+};
+
+#endif // FRAMEWORKS_EX_VARIABLESPEED_JNI_PROFILE_TIMER_H_
diff --git a/variablespeed/jni/ring_buffer.cc b/variablespeed/jni/ring_buffer.cc
new file mode 100644
index 0000000..fb014d3
--- /dev/null
+++ b/variablespeed/jni/ring_buffer.cc
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+#include "ring_buffer.h"
+
+#include "integral_types.h"
+
+namespace video_editing {
+
+void RingBuffer::Init(int size, int num_channels, int num_readers) {
+ size_ = size;
+ num_channels_ = num_channels;
+ num_readers_ = num_readers;
+ temp_read_buffer_size_ = 1024;
+ initialized_ = true;
+ Reset();
+}
+
+RingBuffer::RingBuffer()
+ : initialized_(false), samples_(NULL),
+ num_readers_(0), temp_read_buffer_(NULL) {
+}
+
+RingBuffer::~RingBuffer() {
+ delete[] samples_;
+ delete[] temp_read_buffer_;
+}
+
+void RingBuffer::Reset() {
+ delete[] samples_;
+ samples_ = new float[size_ * num_channels_];
+ memset(samples_, 0,
+ size_ * num_channels_ * sizeof(samples_[0]));
+
+ temp_read_buffer_size_ = 1024;
+ delete[] temp_read_buffer_;
+ temp_read_buffer_ = new float[temp_read_buffer_size_ * num_channels_];
+ memset(temp_read_buffer_, 0,
+ temp_read_buffer_size_ * num_channels_ * sizeof(samples_[0]));
+ readers_.clear();
+ for (int i = 0; i < num_readers_; ++i) {
+ readers_.push_back(0LL);
+ }
+ head_logical_ = 0LL;
+ head_ = 0;
+}
+
+int RingBuffer::available(int reader) const {
+ return head_logical_ - readers_[reader];
+}
+
+int RingBuffer::overhead() const {
+ int64 tail = GetTail();
+ return tail + size_ - head_logical_;
+}
+
+int64 RingBuffer::GetTail() const {
+ return *min_element(readers_.begin(), readers_.end());
+}
+
+int64 RingBuffer::Tell(int reader) const {
+ return readers_[reader];
+}
+
+void RingBuffer::Seek(int reader, int64 position) {
+ readers_[reader] = position;
+}
+
+void RingBuffer::Write(const float* samples, int num_frames) {
+ if (!num_frames) {
+ return;
+ }
+ if (head_ + num_frames <= size_) {
+ memcpy(samples_ + head_ * num_channels_, samples,
+ num_frames * num_channels_ * sizeof(samples[0]));
+ head_ += num_frames;
+ } else {
+ int overhead = size_ - head_;
+ memcpy(samples_ + head_ * num_channels_, samples,
+ num_channels_ * overhead * sizeof(samples[0]));
+ head_ = num_frames - overhead;
+ memcpy(samples_, samples + overhead * num_channels_,
+ num_channels_ * head_ * sizeof(samples[0]));
+ }
+ head_logical_ += num_frames;
+}
+
+void RingBuffer::Copy(int reader, float* destination, int num_frames) const {
+ int pos = Tell(reader) % size_;
+ if (pos + num_frames <= size_) {
+ memcpy(destination, samples_ + pos * num_channels_,
+ num_channels_ * num_frames * sizeof(destination[0]));
+ } else {
+ int wrapped = size_ - pos;
+ memcpy(destination, samples_ + pos * num_channels_,
+ num_channels_ * wrapped * sizeof(destination[0]));
+ int remaining = num_frames - wrapped;
+ memcpy(destination + wrapped * num_channels_, samples_,
+ num_channels_ * remaining * sizeof(destination[0]));
+ }
+}
+
+float* RingBuffer::GetPointer(int reader, int num_frames) {
+ int pos = Tell(reader) % size_;
+ if (pos + num_frames <= size_) {
+ return samples_ + pos * num_channels_;
+ } else {
+ if (num_frames > temp_read_buffer_size_) {
+ temp_read_buffer_size_ = num_frames;
+ delete[] temp_read_buffer_;
+ temp_read_buffer_ =
+ new float[temp_read_buffer_size_ * num_channels_]; // NOLINT
+ }
+ Copy(reader, temp_read_buffer_, num_frames);
+ return temp_read_buffer_;
+ }
+}
+
+void RingBuffer::MergeBack(int reader, const float* source, int num_frames) {
+ // If the source pointer is not the temporary buffer,
+ // data updates were performed in place, so there is nothing to do.
+ // Otherwise, copy samples from the temp buffer back to the ring buffer.
+ if (source == temp_read_buffer_) {
+ int pos = Tell(reader) % size_;
+ if (pos + num_frames <= size_) {
+ memcpy(samples_ + (pos * num_channels_), source,
+ num_channels_ * num_frames * sizeof(source[0]));
+ } else {
+ int wrapped = size_ - pos;
+ memcpy(samples_ + (pos * num_channels_), source,
+ num_channels_ * wrapped * sizeof(source[0]));
+ int remaining = num_frames - wrapped;
+ memcpy(samples_, source + (wrapped * num_channels_),
+ num_channels_ * remaining * sizeof(source[0]));
+ }
+ }
+}
+
+} // namespace video_editing
diff --git a/variablespeed/jni/ring_buffer.h b/variablespeed/jni/ring_buffer.h
new file mode 100644
index 0000000..8afe436
--- /dev/null
+++ b/variablespeed/jni/ring_buffer.h
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_EX_VARIABLESPEED_JNI_RING_BUFFER_H_
+#define FRAMEWORKS_EX_VARIABLESPEED_JNI_RING_BUFFER_H_
+
+#include <vector>
+
+#include "integral_types.h"
+#include "macros.h"
+
+// Circular buffer of multichannel audio, maintaining the position of the
+// writing head and several reading head, and providing a method for
+// accessing a contiguous block (through direct reference or the copy in a
+// temporary read buffer in case the requested block crosses the buffer
+// boundaries).
+//
+// This code is not thread-safe.
+
+namespace video_editing {
+
+class RingBuffer {
+ public:
+ RingBuffer();
+ virtual ~RingBuffer();
+
+ // Initializes a RingBuffer.
+ // @param size: size of the buffer in frames.
+ // @param num_channels: number of channels of the original audio.
+ // @param num_readers: number of reading heads.
+ void Init(int size, int num_channels, int num_readers);
+
+ // Gets the position of a reading head.
+ // @param reader reading head index.
+ // @returns position pointed to by the #reader reading head.
+ int64 Tell(int reader) const;
+
+ // Moves a reading head.
+ // @param reader reading head index.
+ // @param position target position.
+ void Seek(int reader, int64 position);
+
+ // Reads samples for a reading head.
+ // @param reader reading head index.
+ // @param num_frames number of frames to read.
+ // @param destination float buffer to which the samples will be written.
+ void Copy(int reader, float* destination, int num_frames) const;
+
+ // Writes samples.
+ // @param samples float buffer containing the samples.
+ // @param num_frames number of frames to write.
+ void Write(const float* samples, int num_frames);
+
+ // Flushes the content of the buffer and reset the position of the heads.
+ void Reset();
+
+ // Returns the number of frames we can still write.
+ int overhead() const;
+
+ // Returns the number of frames we can read for a given reader.
+ // @param reader reading head index.
+ int available(int reader) const;
+
+ // Returns a pointer to num_frames x num_channels contiguous samples for
+ // a given reader. In most cases this directly returns a pointer to the
+ // data in the ring buffer. However, if the required block wraps around the
+ // boundaries of the ring buffer, the data is copied to a temporary buffer
+ // owned by the RingBuffer object.
+ // @param reader reading head index.
+ // @param num_frames number of frames to read.
+ // @returns pointer to a continuous buffer containing num_frames.
+ float* GetPointer(int reader, int num_frames);
+
+ // Merges updated data back into the ring buffer, if it was updated in
+ // the temporary buffer. This operation follows a GetPointer() that
+ // was used to obtain data to rewrite. The buffer address supplied
+ // here must match the one returned by GetPointer().
+ // @param reader reading head index.
+ // @param source pointer to a continuous buffer containing num_frames.
+ // @param num_frames number of frames to copy back to the ring buffer.
+ void MergeBack(int reader, const float* source, int num_frames);
+
+ private:
+ // Returns the position of the laziest reader.
+ int64 GetTail() const;
+
+ bool initialized_;
+ float* samples_;
+ std::vector<int64> readers_;
+ int size_;
+ int num_channels_;
+ int num_readers_;
+ int64 head_logical_;
+ int head_;
+
+ float* temp_read_buffer_;
+ int temp_read_buffer_size_;
+
+ DISALLOW_COPY_AND_ASSIGN(RingBuffer);
+};
+
+} // namespace video_editing
+
+#endif // FRAMEWORKS_EX_VARIABLESPEED_JNI_RING_BUFFER_H_
diff --git a/variablespeed/jni/sola_time_scaler.cc b/variablespeed/jni/sola_time_scaler.cc
new file mode 100644
index 0000000..1ff1236
--- /dev/null
+++ b/variablespeed/jni/sola_time_scaler.cc
@@ -0,0 +1,366 @@
+/*
+ * 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.
+ */
+
+#include "sola_time_scaler.h"
+
+#include <math.h>
+#include <hlogging.h>
+#include <algorithm>
+
+#include "ring_buffer.h"
+
+#define FLAGS_sola_ring_buffer 2.0
+#define FLAGS_sola_enable_correlation true
+
+
+namespace video_editing {
+
+// Returns a cross-correlation score for the specified buffers.
+int SolaAnalyzer::Correlate(const float* buffer1, const float* buffer2,
+ int num_frames) {
+ CHECK(initialized_);
+
+ int score = 0;
+ num_frames *= num_channels_;
+ while (num_frames-- > 0) {
+ // Increment the score if the sign bits match.
+ score += ((bit_cast<int32>(*buffer1++) ^ bit_cast<int32>(*buffer2++)) >= 0)
+ ? 1 : 0;
+ }
+ return score;
+}
+
+// Trivial SolaAnalyzer class to bypass correlation.
+class SolaBypassAnalyzer : public SolaAnalyzer {
+ public:
+ SolaBypassAnalyzer() { }
+ virtual int Correlate(const float*, const float*, int num_frames) {
+ return num_frames * num_channels_;
+ }
+};
+
+
+// Default constructor.
+SolaTimeScaler::SolaTimeScaler()
+ : input_buffer_(NULL), output_buffer_(NULL), analyzer_(NULL) {
+ sample_rate_ = 0;
+ num_channels_ = 0;
+
+ draining_ = false;
+ initialized_ = false;
+}
+
+SolaTimeScaler::~SolaTimeScaler() {
+ delete input_buffer_;
+ delete output_buffer_;
+ delete analyzer_;
+}
+
+// Injects a SolaAnalyzer instance for analyzing signal frames.
+void SolaTimeScaler::set_analyzer(SolaAnalyzer* analyzer) {
+ MutexLock lock(&mutex_); // lock out processing while updating
+ delete analyzer_;
+ analyzer_ = analyzer;
+}
+
+// Initializes a SOLA timescaler.
+void SolaTimeScaler::Init(double sample_rate,
+ int num_channels,
+ double initial_speed,
+ double window_duration,
+ double overlap_duration) {
+ MutexLock lock(&mutex_); // lock out processing while updating
+
+ sample_rate_ = sample_rate;
+ num_channels_ = num_channels;
+ speed_ = initial_speed;
+ window_duration_ = window_duration;
+ overlap_duration_ = overlap_duration;
+
+ initialized_ = true;
+ GenerateParameters();
+ Reset();
+}
+
+// Adjusts the rate scaling factor.
+void SolaTimeScaler::set_speed(double speed) {
+ MutexLock lock(&mutex_); // lock out processing while updating
+
+ speed_ = speed;
+ GenerateParameters();
+}
+
+// Generates processing parameters from the current settings.
+void SolaTimeScaler::GenerateParameters() {
+ if (speed_ < 0.1) {
+ LOGE("Requested speed %fx limited to 0.1x", speed_);
+ speed_ = 0.1;
+ } else if (speed_ > 8.0) {
+ LOGE("Requested speed %fx limited to 8.0x", speed_);
+ speed_ = 8.0;
+ }
+
+ ratio_ = 1.0 / speed_;
+
+ num_window_frames_ = nearbyint(sample_rate_ * window_duration_);
+
+ // Limit the overlap to half the window size, and round up to an odd number.
+ // Half of overlap window (rounded down) is also a useful number.
+ overlap_duration_ = min(overlap_duration_, window_duration_ / 2.0);
+ num_overlap_frames_ = nearbyint(sample_rate_ * overlap_duration_);
+ num_overlap_frames_ |= 1;
+ half_overlap_frames_ = num_overlap_frames_ >> 1;
+
+ if (speed_ >= 1.) {
+ // For compression (speed up), adjacent input windows overlap in the output.
+ input_window_offset_ = num_window_frames_;
+ target_merge_offset_ = nearbyint(num_window_frames_ * ratio_);
+ } else {
+ // For expansion (slow down), each input window start point overlaps the
+ // previous, and they are placed adjacently in the output
+ // (+/- half the overlap size).
+ input_window_offset_ = nearbyint(num_window_frames_ * speed_);
+ target_merge_offset_ = num_window_frames_;
+ }
+
+ // Make sure we copy enough extra data to be able to perform a
+ // frame correlation over the range of target merge point +/- half overlap,
+ // even when the previous merge point was adjusted backwards a half overlap.
+ max_frames_to_merge_ = max(num_window_frames_,
+ target_merge_offset_ + (2 * num_overlap_frames_));
+ min_output_to_hold_=
+ max_frames_to_merge_ + num_overlap_frames_ - target_merge_offset_;
+}
+
+// The input buffer has one writer and reader.
+// The output buffer has one reader/updater, and one reader/consumer.
+static const int kInputReader = 0;
+static const int kOutputAnalysis = 0;
+static const int kOutputConsumer = 1;
+
+void SolaTimeScaler::Reset() {
+ CHECK(initialized_);
+ double duration = max(FLAGS_sola_ring_buffer, 20. * window_duration_);
+ draining_ = false;
+
+ delete input_buffer_;
+ input_buffer_ = new RingBuffer();
+ input_buffer_->Init(static_cast<int>
+ (sample_rate_ * duration), num_channels_, 1);
+
+ delete output_buffer_;
+ output_buffer_ = new RingBuffer();
+ output_buffer_->Init(static_cast<int>
+ (sample_rate_ * ratio_ * duration), num_channels_, 2);
+
+ if (analyzer_ == NULL) {
+ if (FLAGS_sola_enable_correlation) {
+ analyzer_ = new SolaAnalyzer();
+ } else {
+ analyzer_ = new SolaBypassAnalyzer();
+ }
+ }
+ analyzer_->Init(sample_rate_, num_channels_);
+}
+
+// Returns the number of frames that the input buffer can accept.
+int SolaTimeScaler::input_limit() const {
+ CHECK(initialized_);
+ return input_buffer_->overhead();
+}
+
+// Returns the number of available output frames.
+int SolaTimeScaler::available() {
+ CHECK(initialized_);
+
+ int available = output_buffer_->available(kOutputConsumer);
+ if (available > min_output_to_hold_) {
+ available -= min_output_to_hold_;
+ } else if (draining_) {
+ Process();
+ available = output_buffer_->available(kOutputConsumer);
+ if (available > min_output_to_hold_) {
+ available -= min_output_to_hold_;
+ }
+ } else {
+ available = 0;
+ }
+ return available;
+}
+
+void SolaTimeScaler::Drain() {
+ CHECK(initialized_);
+
+ draining_ = true;
+}
+
+
+// Feeds audio to the timescaler, and processes as much data as possible.
+int SolaTimeScaler::InjectSamples(float* buffer, int num_frames) {
+ CHECK(initialized_);
+
+ // Do not write more frames than the buffer can accept.
+ num_frames = min(input_limit(), num_frames);
+ if (!num_frames) {
+ return 0;
+ }
+
+ // Copy samples to the input buffer and then process whatever can be consumed.
+ input_buffer_->Write(buffer, num_frames);
+ Process();
+ return num_frames;
+}
+
+// Retrieves audio data from the timescaler.
+int SolaTimeScaler::RetrieveSamples(float* buffer, int num_frames) {
+ CHECK(initialized_);
+
+ // Do not read more frames than available.
+ num_frames = min(available(), num_frames);
+ if (!num_frames) {
+ return 0;
+ }
+
+ output_buffer_->Copy(kOutputConsumer, buffer, num_frames);
+ output_buffer_->Seek(kOutputConsumer,
+ output_buffer_->Tell(kOutputConsumer) + num_frames);
+
+ return num_frames;
+}
+
+// Munges input samples to produce output.
+bool SolaTimeScaler::Process() {
+ CHECK(initialized_);
+ bool generated_data = false;
+
+ // We can only process data if there is sufficient input available
+ // (or we are draining the latency), and there is sufficient room
+ // for output to be merged.
+ while (((input_buffer_->available(kInputReader) > max_frames_to_merge_) ||
+ draining_) && (output_buffer_->overhead() >= max_frames_to_merge_)) {
+ MutexLock lock(&mutex_); // lock out updates while processing each window
+
+ // Determine the number of samples to merge into the output.
+ int input_count =
+ min(input_buffer_->available(kInputReader), max_frames_to_merge_);
+ if (input_count == 0) {
+ break;
+ }
+ // The input reader always points to the next window to process.
+ float* input_pointer = input_buffer_->GetPointer(kInputReader, input_count);
+
+ // The analysis reader always points to the ideal target merge point,
+ // minus half an overlap window (ie, the starting point for correlation).
+ // That means the available data from that point equals the number
+ // of samples that must be cross-faded.
+ int output_merge_cnt = output_buffer_->available(kOutputAnalysis);
+ float* output_pointer =
+ output_buffer_->GetPointer(kOutputAnalysis, output_merge_cnt);
+
+ // If there is not enough data to do a proper correlation,
+ // just merge at the ideal target point. Otherwise,
+ // find the best correlation score, working from the center out.
+ int merge_offset = min(output_merge_cnt, half_overlap_frames_);
+
+ if ((output_merge_cnt >= (2 * num_overlap_frames_)) &&
+ (input_count >= num_overlap_frames_)) {
+ int best_offset = merge_offset;
+ int best_score = 0;
+ int score;
+ for (int i = 0; i <= half_overlap_frames_; ++i) {
+ score = analyzer_->Correlate(input_pointer,
+ output_pointer + ((merge_offset + i) * num_channels_),
+ num_overlap_frames_);
+ if (score > best_score) {
+ best_score = score;
+ best_offset = merge_offset + i;
+ if (score == (num_overlap_frames_ * num_channels_)) {
+ break; // It doesn't get better than perfect.
+ }
+ }
+ if (i > 0) {
+ score = analyzer_->Correlate(input_pointer,
+ output_pointer + ((merge_offset - i) * num_channels_),
+ num_overlap_frames_);
+ if (score > best_score) {
+ best_score = score;
+ best_offset = merge_offset - i;
+ if (score == (num_overlap_frames_ * num_channels_)) {
+ break; // It doesn't get better than perfect.
+ }
+ }
+ }
+ }
+ merge_offset = best_offset;
+ } else if ((output_merge_cnt > 0) && !draining_) {
+ LOGE("no correlation performed");
+ }
+
+ // Crossfade the overlap between input and output, and then
+ // copy in the remaining input.
+ int crossfade_count = max(0, (output_merge_cnt - merge_offset));
+ crossfade_count = min(crossfade_count, input_count);
+ int remaining_count = input_count - crossfade_count;
+
+ float* merge_pointer = output_pointer + (merge_offset * num_channels_);
+ float flt_count = static_cast<float>(crossfade_count);
+ for (int i = 0; i < crossfade_count; ++i) {
+ // Linear cross-fade, for now.
+ float input_scale = static_cast<float>(i) / flt_count;
+ float output_scale = 1. - input_scale;
+ for (int j = 0; j < num_channels_; ++j) {
+ *merge_pointer = (*merge_pointer * output_scale) +
+ (*input_pointer++ * input_scale);
+ ++merge_pointer;
+ }
+ }
+ // Copy the merged buffer back into the output, if necessary, and
+ // append the rest of the window.
+ output_buffer_->MergeBack(kOutputAnalysis,
+ output_pointer, output_merge_cnt);
+ output_buffer_->Write(input_pointer, remaining_count);
+
+ // Advance the output analysis pointer to the next target merge point,
+ // minus half an overlap window. The target merge point is always
+ // calculated as a delta from the previous ideal target, not the actual
+ // target, to avoid drift.
+ int output_advance = target_merge_offset_;
+ if (output_merge_cnt < half_overlap_frames_) {
+ // On the first window, back up the pointer for the next correlation.
+ // Thereafter, that compensation is preserved.
+ output_advance -= half_overlap_frames_;
+ }
+
+ // Don't advance beyond the available data, when finishing up.
+ if (draining_) {
+ output_advance =
+ min(output_advance, output_buffer_->available(kOutputAnalysis));
+ }
+ output_buffer_->Seek(kOutputAnalysis,
+ output_buffer_->Tell(kOutputAnalysis) + output_advance);
+
+ // Advance the input pointer beyond the frames that are no longer needed.
+ input_buffer_->Seek(kInputReader, input_buffer_->Tell(kInputReader) +
+ min(input_count, input_window_offset_));
+
+ if ((crossfade_count + remaining_count) > 0) {
+ generated_data = true;
+ }
+ } // while (more to process)
+ return generated_data;
+}
+
+} // namespace video_editing
diff --git a/variablespeed/jni/sola_time_scaler.h b/variablespeed/jni/sola_time_scaler.h
new file mode 100644
index 0000000..c903f61
--- /dev/null
+++ b/variablespeed/jni/sola_time_scaler.h
@@ -0,0 +1,165 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_EX_VARIABLESPEED_JNI_SOLA_TIME_SCALER_H_
+#define FRAMEWORKS_EX_VARIABLESPEED_JNI_SOLA_TIME_SCALER_H_
+
+#include <android/log.h>
+
+#include <no_synchronization.h>
+
+#include <list>
+#include <vector>
+
+#include "macros.h"
+
+// Time-domain audio playback rate scaler using phase-aligned Synchronized
+// OverLap Add (SOLA).
+
+namespace video_editing {
+
+class RingBuffer;
+
+// The default SolaAnalyzer implements a sign-bit cross-correlation
+// function for determining the best fit between two signals.
+class SolaAnalyzer {
+ public:
+ SolaAnalyzer() : initialized_(false) { }
+ virtual ~SolaAnalyzer() { }
+
+ // Initializes a SolaAnalyzer.
+ // @param sample_rate sample rate of the audio signal.
+ // @param num_channels number of interleaved channels in the signal.
+ void Init(int sample_rate, int num_channels) {
+ sample_rate_ = sample_rate;
+ num_channels_ = num_channels;
+ initialized_ = true;
+ }
+
+ // Returns a cross-correlation score for the specified buffers.
+ // The correlation is performed over all channels of a multi-channel signal.
+ // @param buffer1 pointer to interleaved input samples
+ // @param buffer2 pointer to interleaved input samples
+ // @param num_frames number of input frames (that is to say, number of
+ // samples / number of channels)
+ // @param returns a correlation score in the range zero to num_frames
+ virtual int Correlate(const float* buffer1, const float* buffer2,
+ int num_frames);
+
+ protected:
+ bool initialized_;
+ int sample_rate_;
+ int num_channels_;
+
+ DISALLOW_COPY_AND_ASSIGN(SolaAnalyzer);
+};
+
+
+class SolaTimeScaler {
+ public:
+ // Default constructor.
+ SolaTimeScaler();
+ virtual ~SolaTimeScaler();
+
+ // Injects a SolaAnalyzer instance for analyzing signal frames.
+ // The scaler takes ownership of this instance.
+ // This is normally called once, before Init().
+ // @param analyzer SolaAnalyzer instance
+ void set_analyzer(SolaAnalyzer* analyzer);
+
+ // Initializes a SOLA timescaler.
+ // @param sample_rate sample rate of the signal to process
+ // @param num_channels number of channels of the signal to process
+ // @param initial_speed starting rate scaling factor
+ // @param window_duration processing window size, in seconds
+ // @param overlap_duration correlation overlap size, in seconds
+ void Init(double sample_rate, int num_channels, double initial_speed,
+ double window_duration, double overlap_duration);
+
+ // Adjusts the rate scaling factor.
+ // This may be called concurrently with processing, and will
+ // take effect on the next processing window.
+ // @param speed rate scaling factor
+ void set_speed(double speed);
+
+ // Indicates that we are done with the input and won't call Process anymore
+ // This processes all the data reamining in the analysis buffer.
+ void Drain();
+
+ // Flushes the buffers associated with the scaler.
+ void Reset();
+
+ // Feeds audio to the timescaler, and processes as much data as possible.
+ // @param buffer pointer to interleaved float input samples
+ // @param num_frames number of frames (num_samples / num_channels)
+ // @returns number of frames actually accepted
+ int InjectSamples(float* buffer, int num_frames);
+
+ // Retrieves audio data from the timescaler.
+ // @param buffer pointer to buffer to receive interleaved float output
+ // @param num_frames maximum desired number of frames
+ // @returns number of frames actually returned
+ int RetrieveSamples(float* buffer, int num_frames);
+
+ // Returns the number of frames that the input buffer can accept.
+ // @returns number of frames for the next Process() call
+ int input_limit() const;
+
+ // Returns the number of available output frames.
+ // @returns number of frames that can be retrieved
+ int available();
+
+ int num_channels() const { return num_channels_; }
+
+ private:
+ mutable Mutex mutex_; // allows concurrent produce/consume/param change
+ bool initialized_; // set true when input parameters have been set
+ bool draining_; // set true to drain latency
+
+ // Input parameters.
+ int num_channels_; // channel valence of audio stream
+ double sample_rate_; // sample rate of audio stream
+ double window_duration_; // the nominal time quantum for processing
+ double overlap_duration_; // the maximum slip for correlating windows
+ double speed_; // varispeed rate
+
+ // Derived parameters.
+ double ratio_; // inverse of speed
+ int num_window_frames_; // window_duration_ expressed as frame count
+ int num_overlap_frames_; // overlap_duration_ expressed as frame count
+ int half_overlap_frames_; // half of the overlap
+ int input_window_offset_; // frame delta between input windows
+ int target_merge_offset_; // ideal frame delta between output windows
+ int max_frames_to_merge_; // ideal frame count to merge to output
+ int min_output_to_hold_; // number of output frames needed for next merge
+
+ RingBuffer* input_buffer_;
+ RingBuffer* output_buffer_;
+ SolaAnalyzer* analyzer_;
+
+ // Generates processing parameters from the current settings.
+ void GenerateParameters();
+
+ // Munges input samples to produce output.
+ // @returns true if any output samples were generated
+ bool Process();
+
+ DISALLOW_COPY_AND_ASSIGN(SolaTimeScaler);
+};
+
+} // namespace video_editing
+
+#endif // FRAMEWORKS_EX_VARIABLESPEED_JNI_SOLA_TIME_SCALER_H_
diff --git a/variablespeed/jni/variablespeed.cc b/variablespeed/jni/variablespeed.cc
new file mode 100644
index 0000000..49eddbe
--- /dev/null
+++ b/variablespeed/jni/variablespeed.cc
@@ -0,0 +1,800 @@
+/*
+ * 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.
+ */
+
+#include <variablespeed.h>
+
+#include <unistd.h>
+#include <stdlib.h>
+
+#include <sola_time_scaler.h>
+#include <ring_buffer.h>
+
+#include <hlogging.h>
+
+#include <vector>
+
+// ****************************************************************************
+// Constants, utility methods, structures and other miscellany used throughout
+// this file.
+
+namespace {
+
+// These variables are used to determine the size of the buffer queue used by
+// the decoder.
+// This is not the same as the large buffer used to hold the uncompressed data
+// - for that see the member variable decodeBuffer_.
+// The choice of 1152 corresponds to the number of samples per mp3 frame, so is
+// a good choice of size for a decoding buffer in the absence of other
+// information (we don't know exactly what formats we will be working with).
+const size_t kNumberOfBuffersInQueue = 4;
+const size_t kNumberOfSamplesPerBuffer = 1152;
+const size_t kBufferSizeInBytes = 2 * kNumberOfSamplesPerBuffer;
+const size_t kSampleSizeInBytes = 4;
+
+// When calculating play buffer size before pushing to audio player.
+const size_t kNumberOfBytesPerInt16 = 2;
+
+// How long to sleep during the main play loop and the decoding callback loop.
+// In due course this should be replaced with the better signal and wait on
+// condition rather than busy-looping.
+const int kSleepTimeMicros = 1000;
+
+// Used in detecting errors with the OpenSL ES framework.
+const SLuint32 kPrefetchErrorCandidate =
+ SL_PREFETCHEVENT_STATUSCHANGE | SL_PREFETCHEVENT_FILLLEVELCHANGE;
+
+// Structure used when we perform a decoding callback.
+typedef struct CallbackContext_ {
+ SLPlayItf decoderPlay;
+ // Pointer to local storage buffers for decoded audio data.
+ int8_t* pDataBase;
+ // Pointer to the current buffer within local storage.
+ int8_t* pData;
+} CallbackContext;
+
+// Local storage for decoded audio data.
+int8_t pcmData[kNumberOfBuffersInQueue * kBufferSizeInBytes];
+
+#define CheckSLResult(message, result) \
+ CheckSLResult_Real(message, result, __LINE__)
+
+// Helper function for debugging - checks the OpenSL result for success.
+void CheckSLResult_Real(const char* message, SLresult result, int line) {
+ // This can be helpful when debugging.
+ // LOGD("sl result %d for %s", result, message);
+ if (SL_RESULT_SUCCESS != result) {
+ LOGE("slresult was %d at %s file variablespeed line %d",
+ static_cast<int>(result), message, line);
+ }
+ CHECK(SL_RESULT_SUCCESS == result);
+}
+
+} // namespace
+
+// ****************************************************************************
+// Static instance of audio engine, and methods for getting, setting and
+// deleting it.
+
+// The single global audio engine instance.
+AudioEngine* AudioEngine::audioEngine_ = NULL;
+android::Mutex publishEngineLock_;
+
+AudioEngine* AudioEngine::GetEngine() {
+ android::Mutex::Autolock autoLock(publishEngineLock_);
+ if (audioEngine_ == NULL) {
+ LOGE("you haven't initialized the audio engine");
+ CHECK(false);
+ return NULL;
+ }
+ return audioEngine_;
+}
+
+void AudioEngine::SetEngine(AudioEngine* engine) {
+ if (audioEngine_ != NULL) {
+ LOGE("you have already set the audio engine");
+ CHECK(false);
+ return;
+ }
+ audioEngine_ = engine;
+}
+
+void AudioEngine::DeleteEngine() {
+ if (audioEngine_ == NULL) {
+ LOGE("you haven't initialized the audio engine");
+ CHECK(false);
+ return;
+ }
+ delete audioEngine_;
+ audioEngine_ = NULL;
+}
+
+// ****************************************************************************
+// The callbacks from the engine require static callback functions.
+// Here are the static functions - they just delegate to instance methods on
+// the engine.
+
+static void PlayingBufferQueueCb(SLAndroidSimpleBufferQueueItf, void*) {
+ AudioEngine::GetEngine()->PlayingBufferQueueCallback();
+}
+
+static void PrefetchEventCb(SLPrefetchStatusItf caller, void*, SLuint32 event) {
+ AudioEngine::GetEngine()->PrefetchEventCallback(caller, event);
+}
+
+static void DecodingBufferQueueCb(SLAndroidSimpleBufferQueueItf queueItf,
+ void *context) {
+ AudioEngine::GetEngine()->DecodingBufferQueueCallback(queueItf, context);
+}
+
+static void DecodingEventCb(SLPlayItf caller, void*, SLuint32 event) {
+ AudioEngine::GetEngine()->DecodingEventCallback(caller, event);
+}
+
+// ****************************************************************************
+// Static utility methods.
+
+static void PausePlaying(SLPlayItf playItf) {
+ SLresult result = (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PAUSED);
+ CheckSLResult("pause playing", result);
+}
+
+static void StartPlaying(SLPlayItf playItf) {
+ SLresult result = (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);
+ CheckSLResult("start playing", result);
+}
+
+static void StopPlaying(SLPlayItf playItf) {
+ SLresult result = (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_STOPPED);
+ CheckSLResult("stop playing", result);
+}
+
+static void ExtractMetadataFromDecoder(SLObjectItf decoder) {
+ SLMetadataExtractionItf decoderMetadata;
+ SLresult result = (*decoder)->GetInterface(decoder,
+ SL_IID_METADATAEXTRACTION, &decoderMetadata);
+ CheckSLResult("getting metadata interface", result);
+ SLuint32 itemCount;
+ result = (*decoderMetadata)->GetItemCount(decoderMetadata, &itemCount);
+ CheckSLResult("getting item count", result);
+ SLuint32 i, keySize, valueSize;
+ SLMetadataInfo *keyInfo, *value;
+ for (i = 0; i < itemCount ; ++i) {
+ keyInfo = NULL;
+ keySize = 0;
+ value = NULL;
+ valueSize = 0;
+ result = (*decoderMetadata)->GetKeySize(decoderMetadata, i, &keySize);
+ CheckSLResult("get key size", result);
+ keyInfo = static_cast<SLMetadataInfo*>(malloc(keySize));
+ if (keyInfo) {
+ result = (*decoderMetadata)->GetKey(
+ decoderMetadata, i, keySize, keyInfo);
+ CheckSLResult("get key", result);
+ if (keyInfo->encoding == SL_CHARACTERENCODING_ASCII
+ || keyInfo->encoding == SL_CHARACTERENCODING_UTF8) {
+ result = (*decoderMetadata)->GetValueSize(
+ decoderMetadata, i, &valueSize);
+ CheckSLResult("get value size", result);
+ value = static_cast<SLMetadataInfo*>(malloc(valueSize));
+ if (value) {
+ result = (*decoderMetadata)->GetValue(
+ decoderMetadata, i, valueSize, value);
+ CheckSLResult("get value", result);
+ if (value->encoding == SL_CHARACTERENCODING_BINARY) {
+ LOGD("key[%d] size=%d, name=%s value size=%d value=%d",
+ i, keyInfo->size, keyInfo->data, value->size,
+ *(reinterpret_cast<SLuint32*>(value->data)));
+ }
+ free(value);
+ }
+ }
+ free(keyInfo);
+ }
+ }
+}
+
+static void SeekToPosition(SLSeekItf seekItf, size_t startPositionMillis) {
+ SLresult result = (*seekItf)->SetPosition(
+ seekItf, startPositionMillis, SL_SEEKMODE_ACCURATE);
+ CheckSLResult("seek to position", result);
+}
+
+static void RegisterCallbackContextAndAddEnqueueBuffersToDecoder(
+ SLAndroidSimpleBufferQueueItf decoderQueue, SLPlayItf player,
+ android::Mutex &callbackLock) {
+ android::Mutex::Autolock autoLock(callbackLock);
+ // Initialize the callback structure, used during the decoding.
+ // Then register a callback on the decoder queue, so that we will be called
+ // throughout the decoding process (and can then extract the decoded audio
+ // for the next bit of the pipeline).
+ CallbackContext cntxt;
+ cntxt.decoderPlay = player;
+ cntxt.pDataBase = pcmData;
+ cntxt.pData = pcmData;
+ {
+ SLresult result = (*decoderQueue)->RegisterCallback(
+ decoderQueue, DecodingBufferQueueCb, &cntxt);
+ CheckSLResult("decode callback", result);
+ }
+
+ // Enqueue buffers to map the region of memory allocated to store the
+ // decoded data.
+ for (size_t i = 0; i < kNumberOfBuffersInQueue; i++) {
+ SLresult result = (*decoderQueue)->Enqueue(
+ decoderQueue, cntxt.pData, kBufferSizeInBytes);
+ CheckSLResult("enqueue something", result);
+ cntxt.pData += kBufferSizeInBytes;
+ }
+ cntxt.pData = cntxt.pDataBase;
+}
+
+// ****************************************************************************
+// Constructor and Destructor.
+
+AudioEngine::AudioEngine(size_t channels, size_t sampleRate,
+ size_t targetFrames, float windowDuration, float windowOverlapDuration,
+ size_t maxPlayBufferCount, float initialRate, size_t decodeInitialSize,
+ size_t decodeMaxSize, size_t startPositionMillis)
+ : decodeBuffer_(decodeInitialSize, decodeMaxSize),
+ playingBuffers_(), freeBuffers_(), timeScaler_(NULL),
+ floatBuffer_(NULL), injectBuffer_(NULL),
+ channels_(channels), sampleRate_(sampleRate), slSampleRate_(0),
+ slOutputChannels_(0),
+ targetFrames_(targetFrames),
+ windowDuration_(windowDuration),
+ windowOverlapDuration_(windowOverlapDuration),
+ maxPlayBufferCount_(maxPlayBufferCount), initialRate_(initialRate),
+ startPositionMillis_(startPositionMillis),
+ totalDurationMs_(0), startRequested_(false),
+ stopRequested_(false), finishedDecoding_(false) {
+ if (sampleRate_ == 44100) {
+ slSampleRate_ = SL_SAMPLINGRATE_44_1;
+ } else if (sampleRate_ == 8000) {
+ slSampleRate_ = SL_SAMPLINGRATE_8;
+ } else if (sampleRate_ == 11025) {
+ slSampleRate_ = SL_SAMPLINGRATE_11_025;
+ } else {
+ LOGE("unknown sample rate, not changing");
+ CHECK(false);
+ }
+ if (channels_ == 2) {
+ slOutputChannels_ = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
+ } else if (channels_ == 1) {
+ slOutputChannels_ = SL_SPEAKER_FRONT_LEFT;
+ } else {
+ LOGE("unknown channels, not changing");
+ }
+}
+
+AudioEngine::~AudioEngine() {
+ // destroy the time scaler
+ if (timeScaler_ != NULL) {
+ delete timeScaler_;
+ timeScaler_ = NULL;
+ }
+
+ // delete all outstanding playing and free buffers
+ android::Mutex::Autolock autoLock(playBufferLock_);
+ while (playingBuffers_.size() > 0) {
+ delete[] playingBuffers_.front();
+ playingBuffers_.pop();
+ }
+ while (freeBuffers_.size() > 0) {
+ delete[] freeBuffers_.top();
+ freeBuffers_.pop();
+ }
+
+ delete[] floatBuffer_;
+ floatBuffer_ = NULL;
+ delete[] injectBuffer_;
+ injectBuffer_ = NULL;
+}
+
+// ****************************************************************************
+// Regular AudioEngine class methods.
+
+void AudioEngine::SetVariableSpeed(float speed) {
+ GetTimeScaler()->set_speed(speed);
+}
+
+void AudioEngine::RequestStart() {
+ android::Mutex::Autolock autoLock(lock_);
+ startRequested_ = true;
+}
+
+void AudioEngine::ClearRequestStart() {
+ android::Mutex::Autolock autoLock(lock_);
+ startRequested_ = false;
+}
+
+bool AudioEngine::GetWasStartRequested() {
+ android::Mutex::Autolock autoLock(lock_);
+ return startRequested_;
+}
+
+void AudioEngine::RequestStop() {
+ android::Mutex::Autolock autoLock(lock_);
+ stopRequested_ = true;
+}
+
+int AudioEngine::GetCurrentPosition() {
+ android::Mutex::Autolock autoLock(decodeBufferLock_);
+ double result = decodeBuffer_.GetTotalAdvancedCount();
+ return static_cast<int>(
+ (result * 1000) / sampleRate_ / channels_ + startPositionMillis_);
+}
+
+int AudioEngine::GetTotalDuration() {
+ android::Mutex::Autolock autoLock(lock_);
+ return static_cast<int>(totalDurationMs_);
+}
+
+video_editing::SolaTimeScaler* AudioEngine::GetTimeScaler() {
+ if (timeScaler_ == NULL) {
+ timeScaler_ = new video_editing::SolaTimeScaler();
+ timeScaler_->Init(sampleRate_, channels_, initialRate_, windowDuration_,
+ windowOverlapDuration_);
+ }
+ return timeScaler_;
+}
+
+void AudioEngine::PrefetchDurationSampleRateAndChannels(
+ SLPlayItf playItf, SLPrefetchStatusItf prefetchItf) {
+ // Set play state to pause, to begin the prefetching.
+ PausePlaying(playItf);
+
+ // Wait until the data has been prefetched.
+ // TODO(hugohudson): 0. Not dealing with error just yet.
+ {
+ SLuint32 prefetchStatus = SL_PREFETCHSTATUS_UNDERFLOW;
+ android::Mutex::Autolock autoLock(prefetchLock_);
+ while (prefetchStatus != SL_PREFETCHSTATUS_SUFFICIENTDATA) {
+ LOGI("waiting for condition");
+ // prefetchCondition_.waitRelative(prefetchLock, 1000 * 1000 * 10);
+ usleep(10 * 1000);
+ LOGI("getting the value");
+ (*prefetchItf)->GetPrefetchStatus(prefetchItf, &prefetchStatus);
+ }
+ LOGI("done with wait");
+ }
+
+ SLmillisecond durationInMsec = SL_TIME_UNKNOWN;
+ SLresult result = (*playItf)->GetDuration(playItf, &durationInMsec);
+ CheckSLResult("getting duration", result);
+ CHECK(durationInMsec != SL_TIME_UNKNOWN);
+ LOGD("duration: %d", static_cast<int>(durationInMsec));
+ android::Mutex::Autolock autoLock(lock_);
+ totalDurationMs_ = durationInMsec;
+}
+
+bool AudioEngine::EnqueueNextBufferOfAudio(
+ SLAndroidSimpleBufferQueueItf audioPlayerQueue) {
+ size_t frameSizeInBytes = kSampleSizeInBytes * channels_;
+ size_t frameCount = 0;
+ while (frameCount < targetFrames_) {
+ size_t framesLeft = targetFrames_ - frameCount;
+ // If there is data already in the time scaler, retrieve it.
+ if (GetTimeScaler()->available() > 0) {
+ size_t retrieveCount = min(GetTimeScaler()->available(), framesLeft);
+ int count = GetTimeScaler()->RetrieveSamples(
+ floatBuffer_ + frameCount * channels_, retrieveCount);
+ if (count <= 0) {
+ LOGD("ERROR: Count was %d", count);
+ break;
+ }
+ frameCount += count;
+ continue;
+ }
+ // If there is no data in the time scaler, then feed some into it.
+ android::Mutex::Autolock autoLock(decodeBufferLock_);
+ size_t framesInDecodeBuffer =
+ decodeBuffer_.GetSizeInBytes() / frameSizeInBytes;
+ size_t framesScalerCanHandle = GetTimeScaler()->input_limit();
+ size_t framesToInject = min(framesInDecodeBuffer,
+ min(targetFrames_, framesScalerCanHandle));
+ if (framesToInject <= 0) {
+ // No more frames left to inject.
+ break;
+ }
+ for (size_t i = 0; i < framesToInject * channels_ ; ++i) {
+ injectBuffer_[i] = decodeBuffer_.GetAtIndex(i);
+ }
+ int count = GetTimeScaler()->InjectSamples(injectBuffer_, framesToInject);
+ if (count <= 0) {
+ LOGD("ERROR: Count was %d", count);
+ break;
+ }
+ decodeBuffer_.AdvanceHeadPointerShorts(count * channels_);
+ }
+ if (frameCount <= 0) {
+ // We must have finished playback.
+ if (GetEndOfDecoderReached()) {
+ // If we've finished decoding, clear the buffer - so we will terminate.
+ ClearDecodeBuffer();
+ }
+ return false;
+ }
+
+ // Get a free playing buffer.
+ int16* playBuffer;
+ {
+ android::Mutex::Autolock autoLock(playBufferLock_);
+ if (freeBuffers_.size() > 0) {
+ // If we have a free buffer, recycle it.
+ playBuffer = freeBuffers_.top();
+ freeBuffers_.pop();
+ } else {
+ // Otherwise allocate a new one.
+ playBuffer = new int16[targetFrames_ * channels_];
+ }
+ }
+
+ // Try to play the buffer.
+ for (size_t i = 0; i < frameCount * channels_ ; ++i) {
+ playBuffer[i] = floatBuffer_[i];
+ }
+ size_t sizeOfPlayBufferInBytes =
+ frameCount * channels_ * kNumberOfBytesPerInt16;
+ SLresult result = (*audioPlayerQueue)->Enqueue(audioPlayerQueue, playBuffer,
+ sizeOfPlayBufferInBytes);
+ CheckSLResult("enqueue prebuilt audio", result);
+ if (result == SL_RESULT_SUCCESS) {
+ android::Mutex::Autolock autoLock(playBufferLock_);
+ playingBuffers_.push(playBuffer);
+ } else {
+ LOGE("could not enqueue audio buffer");
+ delete[] playBuffer;
+ }
+
+ return (result == SL_RESULT_SUCCESS);
+}
+
+bool AudioEngine::GetEndOfDecoderReached() {
+ android::Mutex::Autolock autoLock(lock_);
+ return finishedDecoding_;
+}
+
+void AudioEngine::SetEndOfDecoderReached() {
+ android::Mutex::Autolock autoLock(lock_);
+ finishedDecoding_ = true;
+}
+
+bool AudioEngine::PlayFileDescriptor(int fd, int64 offset, int64 length) {
+ SLDataLocator_AndroidFD loc_fd = {
+ SL_DATALOCATOR_ANDROIDFD, fd, offset, length };
+ SLDataFormat_MIME format_mime = {
+ SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED };
+ SLDataSource audioSrc = { &loc_fd, &format_mime };
+ return PlayFromThisSource(audioSrc);
+}
+
+bool AudioEngine::PlayUri(const char* uri) {
+ // Source of audio data for the decoding
+ SLDataLocator_URI decUri = { SL_DATALOCATOR_URI,
+ const_cast<SLchar*>(reinterpret_cast<const SLchar*>(uri)) };
+ SLDataFormat_MIME decMime = {
+ SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED };
+ SLDataSource decSource = { &decUri, &decMime };
+ return PlayFromThisSource(decSource);
+}
+
+bool AudioEngine::IsDecodeBufferEmpty() {
+ android::Mutex::Autolock autoLock(decodeBufferLock_);
+ return decodeBuffer_.GetSizeInBytes() <= 0;
+}
+
+void AudioEngine::ClearDecodeBuffer() {
+ android::Mutex::Autolock autoLock(decodeBufferLock_);
+ decodeBuffer_.Clear();
+}
+
+bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) {
+ ClearDecodeBuffer();
+ floatBuffer_ = new float[targetFrames_ * channels_];
+ injectBuffer_ = new float[targetFrames_ * channels_];
+
+ // Create the engine.
+ SLEngineOption EngineOption[] = { {
+ SL_ENGINEOPTION_THREADSAFE, SL_BOOLEAN_TRUE } };
+ SLObjectItf engine;
+ SLresult result = slCreateEngine(&engine, 1, EngineOption, 0, NULL, NULL);
+ CheckSLResult("create engine", result);
+ result = (*engine)->Realize(engine, SL_BOOLEAN_FALSE);
+ CheckSLResult("realise engine", result);
+ SLEngineItf engineInterface;
+ result = (*engine)->GetInterface(engine, SL_IID_ENGINE, &engineInterface);
+ CheckSLResult("get interface", result);
+
+ // Create the output mix for playing.
+ SLObjectItf outputMix;
+ result = (*engineInterface)->CreateOutputMix(
+ engineInterface, &outputMix, 0, NULL, NULL);
+ CheckSLResult("create output mix", result);
+ result = (*outputMix)->Realize(outputMix, SL_BOOLEAN_FALSE);
+ CheckSLResult("realize", result);
+
+ // Define the source and sink for the audio player: comes from a buffer queue
+ // and goes to the output mix.
+ SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {
+ SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2 };
+ SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, channels_, slSampleRate_,
+ SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
+ slOutputChannels_, SL_BYTEORDER_LITTLEENDIAN};
+ SLDataSource playingSrc = {&loc_bufq, &format_pcm};
+ SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMix};
+ SLDataSink audioSnk = {&loc_outmix, NULL};
+
+ // Create the audio player, which will play from the buffer queue and send to
+ // the output mix.
+ const size_t playerInterfaceCount = 1;
+ const SLInterfaceID iids[playerInterfaceCount] = {
+ SL_IID_ANDROIDSIMPLEBUFFERQUEUE };
+ const SLboolean reqs[playerInterfaceCount] = { SL_BOOLEAN_TRUE };
+ SLObjectItf audioPlayer;
+ result = (*engineInterface)->CreateAudioPlayer(engineInterface, &audioPlayer,
+ &playingSrc, &audioSnk, playerInterfaceCount, iids, reqs);
+ CheckSLResult("create audio player", result);
+ result = (*audioPlayer)->Realize(audioPlayer, SL_BOOLEAN_FALSE);
+ CheckSLResult("realize buffer queue", result);
+
+ // Get the play interface from the player, as well as the buffer queue
+ // interface from its source.
+ // Register for callbacks during play.
+ SLPlayItf audioPlayerPlay;
+ result = (*audioPlayer)->GetInterface(
+ audioPlayer, SL_IID_PLAY, &audioPlayerPlay);
+ CheckSLResult("get interface", result);
+ SLAndroidSimpleBufferQueueItf audioPlayerQueue;
+ result = (*audioPlayer)->GetInterface(audioPlayer,
+ SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &audioPlayerQueue);
+ CheckSLResult("get interface again", result);
+ result = (*audioPlayerQueue)->RegisterCallback(
+ audioPlayerQueue, PlayingBufferQueueCb, NULL);
+ CheckSLResult("register callback", result);
+
+ // Define the source and sink for the decoding player: comes from the source
+ // this method was called with, is sent to another buffer queue.
+ SLDataLocator_AndroidSimpleBufferQueue decBuffQueue;
+ decBuffQueue.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
+ decBuffQueue.numBuffers = kNumberOfBuffersInQueue;
+ // A valid value seems required here but is currently ignored.
+ SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM, 1, slSampleRate_,
+ SL_PCMSAMPLEFORMAT_FIXED_16, 16,
+ SL_SPEAKER_FRONT_LEFT, SL_BYTEORDER_LITTLEENDIAN};
+ SLDataSink decDest = { &decBuffQueue, &pcm };
+
+ // Create the decoder with the given source and sink.
+ const size_t decoderInterfaceCount = 4;
+ SLObjectItf decoder;
+ const SLInterfaceID decodePlayerInterfaces[decoderInterfaceCount] = {
+ SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_PREFETCHSTATUS, SL_IID_SEEK,
+ SL_IID_METADATAEXTRACTION };
+ const SLboolean decodePlayerRequired[decoderInterfaceCount] = {
+ SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE };
+ SLDataSource sourceCopy(audioSrc);
+ result = (*engineInterface)->CreateAudioPlayer(engineInterface, &decoder,
+ &sourceCopy, &decDest, decoderInterfaceCount, decodePlayerInterfaces,
+ decodePlayerRequired);
+ CheckSLResult("create audio player", result);
+ result = (*decoder)->Realize(decoder, SL_BOOLEAN_FALSE);
+ CheckSLResult("realize in sync mode", result);
+
+ // Get the play interface from the decoder, and register event callbacks.
+ // Get the buffer queue, prefetch and seek interfaces.
+ SLPlayItf decoderPlay;
+ result = (*decoder)->GetInterface(decoder, SL_IID_PLAY, &decoderPlay);
+ CheckSLResult("get play interface, implicit", result);
+ result = (*decoderPlay)->SetCallbackEventsMask(
+ decoderPlay, SL_PLAYEVENT_HEADATEND);
+ CheckSLResult("set the event mask", result);
+ result = (*decoderPlay)->RegisterCallback(
+ decoderPlay, DecodingEventCb, NULL);
+ CheckSLResult("register decoding event callback", result);
+ SLAndroidSimpleBufferQueueItf decoderQueue;
+ result = (*decoder)->GetInterface(
+ decoder, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &decoderQueue);
+ CheckSLResult("get decoder buffer queue", result);
+ SLPrefetchStatusItf decoderPrefetch;
+ result = (*decoder)->GetInterface(
+ decoder, SL_IID_PREFETCHSTATUS, &decoderPrefetch);
+ CheckSLResult("get prefetch status interface", result);
+ SLSeekItf decoderSeek;
+ result = (*decoder)->GetInterface(decoder, SL_IID_SEEK, &decoderSeek);
+ CheckSLResult("get seek interface", result);
+
+ RegisterCallbackContextAndAddEnqueueBuffersToDecoder(
+ decoderQueue, decoderPlay, callbackLock_);
+
+ // Initialize the callback for prefetch errors, if we can't open the
+ // resource to decode.
+ result = (*decoderPrefetch)->SetCallbackEventsMask(
+ decoderPrefetch, kPrefetchErrorCandidate);
+ CheckSLResult("set prefetch callback mask", result);
+ result = (*decoderPrefetch)->RegisterCallback(
+ decoderPrefetch, PrefetchEventCb, &decoderPrefetch);
+ CheckSLResult("set prefetch callback", result);
+
+ SeekToPosition(decoderSeek, startPositionMillis_);
+
+ PrefetchDurationSampleRateAndChannels(decoderPlay, decoderPrefetch);
+
+ ExtractMetadataFromDecoder(decoder);
+
+ StartPlaying(decoderPlay);
+
+ // The main loop - until we're told to stop: if there is audio data coming
+ // out of the decoder, feed it through the time scaler.
+ // As it comes out of the time scaler, feed it into the audio player.
+ while (!Finished()) {
+ if (GetWasStartRequested()) {
+ ClearRequestStart();
+ StartPlaying(audioPlayerPlay);
+ }
+ EnqueueMoreAudioIfNecessary(audioPlayerQueue);
+ usleep(kSleepTimeMicros);
+ }
+
+ StopPlaying(audioPlayerPlay);
+ StopPlaying(decoderPlay);
+
+ // Delete the audio player.
+ result = (*audioPlayerQueue)->Clear(audioPlayerQueue);
+ CheckSLResult("clear audio player queue", result);
+ result = (*audioPlayerQueue)->RegisterCallback(audioPlayerQueue, NULL, NULL);
+ CheckSLResult("clear callback", result);
+ (*audioPlayer)->AbortAsyncOperation(audioPlayer);
+ audioPlayerPlay = NULL;
+ audioPlayerQueue = NULL;
+ (*audioPlayer)->Destroy(audioPlayer);
+
+ // Delete the decoder.
+ result = (*decoderPrefetch)->RegisterCallback(decoderPrefetch, NULL, NULL);
+ CheckSLResult("clearing prefetch error callback", result);
+ // TODO(hugohudson): 0. This is returning slresult 13 if I do no playback.
+ // Repro is to comment out all before this line, and all after enqueueing
+ // my buffers.
+ // result = (*decoderQueue)->Clear(decoderQueue);
+ // CheckSLResult("clearing decode buffer queue", result);
+ result = (*decoderQueue)->RegisterCallback(decoderQueue, NULL, NULL);
+ CheckSLResult("clearing decode callback", result);
+ decoderSeek = NULL;
+ decoderPrefetch = NULL;
+ decoderQueue = NULL;
+ result = (*decoderPlay)->RegisterCallback(decoderPlay, NULL, NULL);
+ CheckSLResult("clear decoding event callback", result);
+ (*decoder)->AbortAsyncOperation(decoder);
+ decoderPlay = NULL;
+ (*decoder)->Destroy(decoder);
+
+ // Delete the output mix, then the engine.
+ (*outputMix)->Destroy(outputMix);
+ engineInterface = NULL;
+ (*engine)->Destroy(engine);
+
+ return true;
+}
+
+bool AudioEngine::Finished() {
+ if (GetWasStopRequested()) {
+ return true;
+ }
+ android::Mutex::Autolock autoLock(playBufferLock_);
+ return playingBuffers_.size() <= 0 &&
+ IsDecodeBufferEmpty() &&
+ GetEndOfDecoderReached();
+}
+
+bool AudioEngine::GetWasStopRequested() {
+ android::Mutex::Autolock autoLock(lock_);
+ return stopRequested_;
+}
+
+bool AudioEngine::GetHasReachedPlayingBuffersLimit() {
+ android::Mutex::Autolock autoLock(playBufferLock_);
+ return playingBuffers_.size() >= maxPlayBufferCount_;
+}
+
+void AudioEngine::EnqueueMoreAudioIfNecessary(
+ SLAndroidSimpleBufferQueueItf audioPlayerQueue) {
+ bool keepEnqueueing = true;
+ while (!GetWasStopRequested() &&
+ !IsDecodeBufferEmpty() &&
+ !GetHasReachedPlayingBuffersLimit() &&
+ keepEnqueueing) {
+ keepEnqueueing = EnqueueNextBufferOfAudio(audioPlayerQueue);
+ }
+}
+
+bool AudioEngine::DecodeBufferTooFull() {
+ android::Mutex::Autolock autoLock(decodeBufferLock_);
+ return decodeBuffer_.IsTooLarge();
+}
+
+// ****************************************************************************
+// Code for handling the static callbacks.
+
+void AudioEngine::PlayingBufferQueueCallback() {
+ // The head playing buffer is done, move it to the free list.
+ android::Mutex::Autolock autoLock(playBufferLock_);
+ if (playingBuffers_.size() > 0) {
+ freeBuffers_.push(playingBuffers_.front());
+ playingBuffers_.pop();
+ }
+}
+
+void AudioEngine::PrefetchEventCallback(
+ SLPrefetchStatusItf caller, SLuint32 event) {
+ // If there was a problem during decoding, then signal the end.
+ LOGI("in the prefetch callback");
+ SLpermille level = 0;
+ SLresult result = (*caller)->GetFillLevel(caller, &level);
+ CheckSLResult("get fill level", result);
+ SLuint32 status;
+ result = (*caller)->GetPrefetchStatus(caller, &status);
+ CheckSLResult("get prefetch status", result);
+ if ((kPrefetchErrorCandidate == (event & kPrefetchErrorCandidate)) &&
+ (level == 0) &&
+ (status == SL_PREFETCHSTATUS_UNDERFLOW)) {
+ LOGI("PrefetchEventCallback error while prefetching data");
+ SetEndOfDecoderReached();
+ }
+ if (SL_PREFETCHSTATUS_SUFFICIENTDATA == event) {
+ LOGI("looks like our event...");
+ // android::Mutex::Autolock autoLock(prefetchLock_);
+ // prefetchCondition_.broadcast();
+ LOGI("just sent a broadcast");
+ }
+}
+
+void AudioEngine::DecodingBufferQueueCallback(
+ SLAndroidSimpleBufferQueueItf queueItf, void *context) {
+ if (GetWasStopRequested()) {
+ return;
+ }
+
+ CallbackContext *pCntxt;
+ {
+ android::Mutex::Autolock autoLock(callbackLock_);
+ pCntxt = reinterpret_cast<CallbackContext*>(context);
+ }
+ {
+ android::Mutex::Autolock autoLock(decodeBufferLock_);
+ decodeBuffer_.AddData(pCntxt->pDataBase, kBufferSizeInBytes);
+ }
+
+ // Increase data pointer by buffer size
+ pCntxt->pData += kBufferSizeInBytes;
+ if (pCntxt->pData >= pCntxt->pDataBase +
+ (kNumberOfBuffersInQueue * kBufferSizeInBytes)) {
+ pCntxt->pData = pCntxt->pDataBase;
+ }
+
+ SLresult result = (*queueItf)->Enqueue(
+ queueItf, pCntxt->pDataBase, kBufferSizeInBytes);
+ CheckSLResult("enqueue something else", result);
+
+ // If we get too much data into the decoder,
+ // sleep until the playback catches up.
+ while (!GetWasStopRequested() && DecodeBufferTooFull()) {
+ usleep(kSleepTimeMicros);
+ }
+}
+
+void AudioEngine::DecodingEventCallback(SLPlayItf, SLuint32 event) {
+ if (SL_PLAYEVENT_HEADATEND & event) {
+ SetEndOfDecoderReached();
+ }
+}
diff --git a/variablespeed/jni/variablespeed.h b/variablespeed/jni/variablespeed.h
new file mode 100644
index 0000000..5c6b45d
--- /dev/null
+++ b/variablespeed/jni/variablespeed.h
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_EX_VARIABLESPEED_JNI_VARIABLESPEED_H_
+#define FRAMEWORKS_EX_VARIABLESPEED_JNI_VARIABLESPEED_H_
+
+#include <jni.h>
+
+#include <SLES/OpenSLES.h>
+#include <SLES/OpenSLES_Android.h>
+#include <SLES/OpenSLES_AndroidConfiguration.h>
+
+#include <integral_types.h>
+#include <utils/threads.h>
+
+#include <profile_timer.h>
+#include <decode_buffer.h>
+
+#include <queue>
+#include <stack>
+
+namespace video_editing {
+ class SolaTimeScaler;
+}
+
+// This is the audio engine class.
+// It forms the bulk of the variablespeed library.
+// It should not be used directly, but rather used indirectly from the java
+// native methods.
+class AudioEngine {
+ public:
+ AudioEngine(size_t channels, size_t sampleRate, size_t targetFrames,
+ float windowDuration, float windowOverlapDuration,
+ size_t maxPlayBufferCount, float initialRate, size_t decodeInitialSize,
+ size_t decodeMaxSize, size_t startPositionMillis);
+ virtual ~AudioEngine();
+
+ bool PlayUri(const char* uri);
+ bool PlayFileDescriptor(int fd, int64 offset, int64 length);
+ void SetVariableSpeed(float speed);
+ void RequestStart();
+ void RequestStop();
+ int GetCurrentPosition();
+ int GetTotalDuration();
+
+ void DecodingBufferQueueCallback(
+ SLAndroidSimpleBufferQueueItf queueItf, void *context);
+ void DecodingEventCallback(SLPlayItf caller, SLuint32 event);
+ void PrefetchEventCallback(SLPrefetchStatusItf caller, SLuint32 event);
+ void PlayingBufferQueueCallback();
+
+ static AudioEngine* GetEngine();
+ static void SetEngine(AudioEngine* engine);
+ static void DeleteEngine();
+
+ private:
+ bool PlayFromThisSource(const SLDataSource& audioSrc);
+ void EnqueueMoreAudioIfNecessary(SLAndroidSimpleBufferQueueItf bufferQueue);
+ bool EnqueueNextBufferOfAudio(SLAndroidSimpleBufferQueueItf bufferQueue);
+ void PrefetchDurationSampleRateAndChannels(
+ SLPlayItf playItf, SLPrefetchStatusItf prefetchItf);
+ video_editing::SolaTimeScaler* GetTimeScaler();
+ bool Finished();
+ bool GetWasStartRequested();
+ bool GetWasStopRequested();
+ void ClearRequestStart();
+ void SetEndOfDecoderReached();
+ bool GetEndOfDecoderReached();
+ bool DecodeBufferTooFull();
+ void ClearDecodeBuffer();
+ bool IsDecodeBufferEmpty();
+ bool GetHasReachedPlayingBuffersLimit();
+
+ // The single global audio engine instance.
+ static AudioEngine* audioEngine_;
+
+ // Protects access to the shared decode buffer.
+ android::Mutex decodeBufferLock_;
+ // Buffer into which we put the audio data as we decode.
+ // Protected by decodeBufferLock_.
+ DecodeBuffer decodeBuffer_;
+
+ // Protects access to the playingBuffers_ and freeBuffers_.
+ android::Mutex playBufferLock_;
+ // The buffers we're using for playback.
+ std::queue<int16*> playingBuffers_;
+ std::stack<int16*> freeBuffers_;
+
+ // The time scaler.
+ video_editing::SolaTimeScaler* timeScaler_;
+
+ // The frame buffer, used for converting between PCM data and float for
+ // time scaler.
+ float* floatBuffer_;
+ float* injectBuffer_;
+
+ size_t channels_;
+ size_t sampleRate_;
+ SLuint32 slSampleRate_;
+ SLuint32 slOutputChannels_;
+ size_t targetFrames_;
+ float windowDuration_;
+ float windowOverlapDuration_;
+ size_t maxPlayBufferCount_;
+ float initialRate_;
+ size_t startPositionMillis_;
+
+ // The prefetch callback signal, for letting the prefetch callback method
+ // indicate when it is done.
+ android::Mutex prefetchLock_;
+ android::Condition prefetchCondition_;
+
+ // Protects access to the CallbackContext object.
+ // I don't believe this to be necessary, I think that it's thread-confined,
+ // but it also won't do any harm.
+ android::Mutex callbackLock_;
+
+ // Protects access to the shared member variables below.
+ android::Mutex lock_;
+ // Protected by lock_.
+ // Stores the total duration of the track.
+ SLmillisecond totalDurationMs_;
+ // Protected by lock_.
+ // Set externally via RequestStart(), this determines when we begin to
+ // playback audio.
+ // Until this is set to true, our audio player will remain stopped.
+ bool startRequested_;
+ // Protected by lock_.
+ // Set externally via RequestStop(), this tells us top stop playing
+ // and therefore shut everything down.
+ bool stopRequested_;
+ // Protected by lock_.
+ // This is set to true once we reach the end of the decoder stream.
+ bool finishedDecoding_;
+
+ DISALLOW_COPY_AND_ASSIGN(AudioEngine);
+};
+
+#endif // FRAMEWORKS_EX_VARIABLESPEED_JNI_VARIABLESPEED_H_
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);
+}