diff options
author | Martin Fietz <Martin.Fietz@gmail.com> | 2015-09-19 00:54:02 +0200 |
---|---|---|
committer | Martin Fietz <Martin.Fietz@gmail.com> | 2015-09-19 14:13:07 +0200 |
commit | edb5d72705ad70f021553161b0381209578eef31 (patch) | |
tree | 7d0fa5fe9b21fde0632ff143c501348c8ace0605 | |
parent | 46ca00ef0f4d2206324ecb6177a684fc46785413 (diff) | |
download | AudioPlayer-edb5d72705ad70f021553161b0381209578eef31.tar.gz |
v1.0
28 files changed, 5148 insertions, 0 deletions
@@ -19,9 +19,14 @@ build/ # Local configuration file (sdk path, etc) local.properties +gradle.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log + +# Android Studio +.idea/ +*.iml diff --git a/README.md b/README.md new file mode 100644 index 0000000..0247b9d --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# AntennaPod-AudioPlayer + +This is the repository for library code separated from the main repository for licensing compliance. + +## License + +All code in this repository is licensed under the Apache License, Version 2.0. +You can find the license text in the LICENSE file. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..1b7886d --- /dev/null +++ b/build.gradle @@ -0,0 +1,19 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.0' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 0000000..8c0fb64 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..dbed527 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Sep 18 23:51:33 CEST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..acb5248 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 22 + buildToolsVersion '22.0.1' + defaultConfig { + minSdkVersion 3 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + productFlavors { + } +} diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..31fd763 --- /dev/null +++ b/library/proguard-rules.pro @@ -0,0 +1,22 @@ +-dontobfuscate +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* + +# disable logging +-assumenosideeffects class android.util.Log { + public static boolean isLoggable(java.lang.String, int); + public static *** v(...); + public static *** i(...); + public static *** w(...); + public static *** d(...); + public static *** e(...); +} + +#Keep the R +-keepclassmembers class **.R$* { + public static <fields>; +} diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..50c9b4a --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest package="org.antennapod.audio"> + +</manifest> diff --git a/library/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl b/library/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl new file mode 100644 index 0000000..6bdc768 --- /dev/null +++ b/library/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl @@ -0,0 +1,18 @@ +// Copyright 2011, Aocate, Inc. +// +// 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.aocate.presto.service; + +oneway interface IDeathCallback_0_8 { +}
\ No newline at end of file diff --git a/library/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl b/library/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl new file mode 100644 index 0000000..7357e40 --- /dev/null +++ b/library/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// 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.aocate.presto.service; + +interface IOnBufferingUpdateListenerCallback_0_8 { + void onBufferingUpdate(int percent); +}
\ No newline at end of file diff --git a/library/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl b/library/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl new file mode 100644 index 0000000..d5edea7 --- /dev/null +++ b/library/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// 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.aocate.presto.service; + +interface IOnCompletionListenerCallback_0_8 { + void onCompletion(); +}
\ No newline at end of file diff --git a/library/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl b/library/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl new file mode 100644 index 0000000..2c4f2df --- /dev/null +++ b/library/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// 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.aocate.presto.service; + +interface IOnErrorListenerCallback_0_8 { + boolean onError(int what, int extra); +} diff --git a/library/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl b/library/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl new file mode 100644 index 0000000..9dbd1d2 --- /dev/null +++ b/library/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// 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.aocate.presto.service; + +interface IOnInfoListenerCallback_0_8 { + boolean onInfo(int what, int extra); +}
\ No newline at end of file diff --git a/library/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl b/library/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl new file mode 100644 index 0000000..41223a9 --- /dev/null +++ b/library/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// 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.aocate.presto.service; + +interface IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 { + void onPitchAdjustmentAvailableChanged(boolean pitchAdjustmentAvailable); +}
\ No newline at end of file diff --git a/library/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl b/library/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl new file mode 100644 index 0000000..7be8f12 --- /dev/null +++ b/library/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// 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.aocate.presto.service; + +interface IOnPreparedListenerCallback_0_8 { + void onPrepared(); +}
\ No newline at end of file diff --git a/library/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl b/library/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl new file mode 100644 index 0000000..5bdda98 --- /dev/null +++ b/library/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// 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.aocate.presto.service; + +interface IOnSeekCompleteListenerCallback_0_8 { + void onSeekComplete(); +}
\ No newline at end of file diff --git a/library/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl b/library/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl new file mode 100644 index 0000000..a69c1cf --- /dev/null +++ b/library/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// Copyright 2011, Aocate, Inc. +// +// 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.aocate.presto.service; + +interface IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 { + void onSpeedAdjustmentAvailableChanged(boolean speedAdjustmentAvailable); +}
\ No newline at end of file diff --git a/library/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl b/library/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl new file mode 100644 index 0000000..12a6047 --- /dev/null +++ b/library/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl @@ -0,0 +1,75 @@ +// Copyright 2011, Aocate, Inc. +// +// 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.aocate.presto.service; + +import com.aocate.presto.service.IDeathCallback_0_8; +import com.aocate.presto.service.IOnBufferingUpdateListenerCallback_0_8; +import com.aocate.presto.service.IOnCompletionListenerCallback_0_8; +import com.aocate.presto.service.IOnErrorListenerCallback_0_8; +import com.aocate.presto.service.IOnPitchAdjustmentAvailableChangedListenerCallback_0_8; +import com.aocate.presto.service.IOnPreparedListenerCallback_0_8; +import com.aocate.presto.service.IOnSeekCompleteListenerCallback_0_8; +import com.aocate.presto.service.IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8; +import com.aocate.presto.service.IOnInfoListenerCallback_0_8; + +interface IPlayMedia_0_8 { + boolean canSetPitch(long sessionId); + boolean canSetSpeed(long sessionId); + float getCurrentPitchStepsAdjustment(long sessionId); + int getCurrentPosition(long sessionId); + float getCurrentSpeedMultiplier(long sessionId); + int getDuration(long sessionId); + float getMaxSpeedMultiplier(long sessionId); + float getMinSpeedMultiplier(long sessionId); + int getVersionCode(); + String getVersionName(); + boolean isLooping(long sessionId); + boolean isPlaying(long sessionId); + void pause(long sessionId); + void prepare(long sessionId); + void prepareAsync(long sessionId); + void registerOnBufferingUpdateCallback(long sessionId, IOnBufferingUpdateListenerCallback_0_8 cb); + void registerOnCompletionCallback(long sessionId, IOnCompletionListenerCallback_0_8 cb); + void registerOnErrorCallback(long sessionId, IOnErrorListenerCallback_0_8 cb); + void registerOnInfoCallback(long sessionId, IOnInfoListenerCallback_0_8 cb); + void registerOnPitchAdjustmentAvailableChangedCallback(long sessionId, IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 cb); + void registerOnPreparedCallback(long sessionId, IOnPreparedListenerCallback_0_8 cb); + void registerOnSeekCompleteCallback(long sessionId, IOnSeekCompleteListenerCallback_0_8 cb); + void registerOnSpeedAdjustmentAvailableChangedCallback(long sessionId, IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 cb); + void release(long sessionId); + void reset(long sessionId); + void seekTo(long sessionId, int msec); + void setAudioStreamType(long sessionId, int streamtype); + void setDataSourceString(long sessionId, String path); + void setDataSourceUri(long sessionId, in Uri uri); + void setEnableSpeedAdjustment(long sessionId, boolean enableSpeedAdjustment); + void setLooping(long sessionId, boolean looping); + void setPitchStepsAdjustment(long sessionId, float pitchSteps); + void setPlaybackPitch(long sessionId, float f); + void setPlaybackSpeed(long sessionId, float f); + void setSpeedAdjustmentAlgorithm(long sessionId, int algorithm); + void setVolume(long sessionId, float left, float right); + void start(long sessionId); + long startSession(IDeathCallback_0_8 cb); + void stop(long sessionId); + void unregisterOnBufferingUpdateCallback(long sessionId, IOnBufferingUpdateListenerCallback_0_8 cb); + void unregisterOnCompletionCallback(long sessionId, IOnCompletionListenerCallback_0_8 cb); + void unregisterOnErrorCallback(long sessionId, IOnErrorListenerCallback_0_8 cb); + void unregisterOnInfoCallback(long sessionId, IOnInfoListenerCallback_0_8 cb); + void unregisterOnPitchAdjustmentAvailableChangedCallback(long sessionId, IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 cb); + void unregisterOnPreparedCallback(long sessionId, IOnPreparedListenerCallback_0_8 cb); + void unregisterOnSeekCompleteCallback(long sessionId, IOnSeekCompleteListenerCallback_0_8 cb); + void unregisterOnSpeedAdjustmentAvailableChangedCallback(long sessionId, IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 cb); +}
\ No newline at end of file diff --git a/library/src/main/java/org/antennapod/audio/AbstractAudioPlayer.java b/library/src/main/java/org/antennapod/audio/AbstractAudioPlayer.java new file mode 100644 index 0000000..cbec776 --- /dev/null +++ b/library/src/main/java/org/antennapod/audio/AbstractAudioPlayer.java @@ -0,0 +1,117 @@ +// Copyright 2011, Aocate, Inc. +// +// 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 org.antennapod.audio; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.locks.ReentrantLock; + +public abstract class AbstractAudioPlayer { + + private static final String MPI_TAG = "AbstractMediaPlayer"; + protected final MediaPlayer owningMediaPlayer; + protected final Context mContext; + protected int muteOnPreparedCount = 0; + protected int muteOnSeekCount = 0; + + public AbstractAudioPlayer(MediaPlayer owningMediaPlayer, Context context) { + this.owningMediaPlayer = owningMediaPlayer; + + this.mContext = context; + } + + public abstract boolean canSetPitch(); + + public abstract boolean canSetSpeed(); + + public abstract float getCurrentPitchStepsAdjustment(); + + public abstract int getCurrentPosition(); + + public abstract float getCurrentSpeedMultiplier(); + + public abstract int getDuration(); + + public abstract float getMaxSpeedMultiplier(); + + public abstract float getMinSpeedMultiplier(); + + public abstract boolean isLooping(); + + public abstract boolean isPlaying(); + + public abstract void pause(); + + public abstract void prepare() throws IllegalStateException, IOException; + + public abstract void prepareAsync(); + + public abstract void release(); + + public abstract void reset(); + + public abstract void seekTo(int msec) throws IllegalStateException; + + public abstract void setAudioStreamType(int streamtype); + + public abstract void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException; + + public abstract void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException; + + public abstract void setEnableSpeedAdjustment(boolean enableSpeedAdjustment); + + public abstract void setLooping(boolean loop); + + public abstract void setPitchStepsAdjustment(float pitchSteps); + + public abstract void setPlaybackPitch(float f); + + public abstract void setPlaybackSpeed(float f); + + public abstract void setVolume(float leftVolume, float rightVolume); + + public abstract void setWakeMode(Context context, int mode); + + public abstract void start(); + + public abstract void stop(); + + protected ReentrantLock lockMuteOnPreparedCount = new ReentrantLock(); + public void muteNextOnPrepare() { + lockMuteOnPreparedCount.lock(); + Log.d(MPI_TAG, "muteNextOnPrepare()"); + try { + this.muteOnPreparedCount++; + } + finally { + lockMuteOnPreparedCount.unlock(); + } + } + + protected ReentrantLock lockMuteOnSeekCount = new ReentrantLock(); + public void muteNextSeek() { + lockMuteOnSeekCount.lock(); + Log.d(MPI_TAG, "muteNextOnSeek()"); + try { + this.muteOnSeekCount++; + } + finally { + lockMuteOnSeekCount.unlock(); + } + } +} diff --git a/library/src/main/java/org/antennapod/audio/AndroidAudioPlayer.java b/library/src/main/java/org/antennapod/audio/AndroidAudioPlayer.java new file mode 100644 index 0000000..c2a5c9a --- /dev/null +++ b/library/src/main/java/org/antennapod/audio/AndroidAudioPlayer.java @@ -0,0 +1,463 @@ +// Copyright 2011, Aocate, Inc. +// +// 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 org.antennapod.audio; + +import android.content.Context; +import android.media.MediaPlayer; +import android.net.Uri; +import android.util.Log; + +import java.io.IOException; + +public class AndroidAudioPlayer extends AbstractAudioPlayer { + + private final static String AMP_TAG = "AndroidMediaPlayer"; + + android.media.MediaPlayer mp = null; + + private android.media.MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { + public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { + if (owningMediaPlayer != null) { + owningMediaPlayer.lock.lock(); + try { + if ((owningMediaPlayer.onBufferingUpdateListener != null) + && (owningMediaPlayer.mpi == AndroidAudioPlayer.this)) { + owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + } + }; + + private android.media.MediaPlayer.OnCompletionListener onCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { + public void onCompletion(android.media.MediaPlayer mp) { + Log.d(AMP_TAG, "onCompletionListener being called"); + if (owningMediaPlayer != null) { + owningMediaPlayer.lock.lock(); + try { + if (owningMediaPlayer.onCompletionListener != null) { + owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + } + }; + + private android.media.MediaPlayer.OnErrorListener onErrorListener = new android.media.MediaPlayer.OnErrorListener() { + public boolean onError(android.media.MediaPlayer mp, int what, int extra) { + // Once we're in errored state, any received messages are going to be junked + if (owningMediaPlayer != null) { + owningMediaPlayer.lock.lock(); + try { + if (owningMediaPlayer.onErrorListener != null) { + return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + return false; + } + }; + + private android.media.MediaPlayer.OnInfoListener onInfoListener = new android.media.MediaPlayer.OnInfoListener() { + public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { + if (owningMediaPlayer != null) { + owningMediaPlayer.lock.lock(); + try { + if ((owningMediaPlayer.onInfoListener != null) + && (owningMediaPlayer.mpi == AndroidAudioPlayer.this)) { + return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + return false; + } + }; + + // We have to assign this.onPreparedListener because the + // onPreparedListener in owningMediaPlayer sets the state + // to PREPARED. Due to prepareAsync, that's the only + // reasonable place to do it + // The others it just didn't make sense to have a setOnXListener that didn't use the parameter + private android.media.MediaPlayer.OnPreparedListener onPreparedListener = new android.media.MediaPlayer.OnPreparedListener() { + public void onPrepared(android.media.MediaPlayer mp) { + Log.d(AMP_TAG, "Calling onPreparedListener.onPrepared()"); + if (AndroidAudioPlayer.this.owningMediaPlayer != null) { + AndroidAudioPlayer.this.lockMuteOnPreparedCount.lock(); + try { + if (AndroidAudioPlayer.this.muteOnPreparedCount > 0) { + AndroidAudioPlayer.this.muteOnPreparedCount--; + } + else { + AndroidAudioPlayer.this.muteOnPreparedCount = 0; + if (AndroidAudioPlayer.this.owningMediaPlayer.onPreparedListener != null) { + Log.d(AMP_TAG, "Invoking AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener.onPrepared"); + AndroidAudioPlayer.this.owningMediaPlayer.onPreparedListener.onPrepared(AndroidAudioPlayer.this.owningMediaPlayer); + } + } + } + finally { + AndroidAudioPlayer.this.lockMuteOnPreparedCount.unlock(); + } + if (owningMediaPlayer.mpi != AndroidAudioPlayer.this) { + Log.d(AMP_TAG, "owningMediaPlayer has changed implementation"); + } + } + } + }; + + private android.media.MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { + public void onSeekComplete(android.media.MediaPlayer mp) { + if (owningMediaPlayer != null) { + owningMediaPlayer.lock.lock(); + try { + lockMuteOnSeekCount.lock(); + try { + if (AndroidAudioPlayer.this.muteOnSeekCount > 0) { + AndroidAudioPlayer.this.muteOnSeekCount--; + } + else { + AndroidAudioPlayer.this.muteOnSeekCount = 0; + if (AndroidAudioPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) { + owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer); + } + } + } + finally { + lockMuteOnSeekCount.unlock(); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + } + }; + + public AndroidAudioPlayer(org.antennapod.audio.MediaPlayer owningMediaPlayer, Context context) { + super(owningMediaPlayer, context); + + mp = new MediaPlayer(); + +// final ReentrantLock lock = new ReentrantLock(); +// Handler handler = new Handler(Looper.getMainLooper()) { +// @Override +// public void handleMessage(Message msg) { +// Log.d(AMP_TAG, "Instantiating new AndroidMediaPlayer from Handler"); +// lock.lock(); +// if (mp == null) { +// mp = new MediaPlayer(); +// } +// lock.unlock(); +// } +// }; +// +// long endTime = System.currentTimeMillis() + TIMEOUT_DURATION_MS; +// +// while (true) { +// // Retry messages until mp isn't null or it's time to give up +// handler.sendMessage(handler.obtainMessage()); +// if ((mp != null) +// || (endTime < System.currentTimeMillis())) { +// break; +// } +// try { +// Thread.sleep(50); +// } catch (InterruptedException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// } + + if (mp == null) { + throw new IllegalStateException("Did not instantiate android.media.MediaPlayer successfully"); + } + + mp.setOnBufferingUpdateListener(this.onBufferingUpdateListener); + mp.setOnCompletionListener(this.onCompletionListener); + mp.setOnErrorListener(this.onErrorListener); + mp.setOnInfoListener(this.onInfoListener); + Log.d(AMP_TAG, " ++++++++++++++++++++++++++++++++ Setting prepared listener to this.onPreparedListener"); + mp.setOnPreparedListener(this.onPreparedListener); + mp.setOnSeekCompleteListener(this.onSeekCompleteListener); + } + + @Override + public boolean canSetPitch() { + return false; + } + + @Override + public boolean canSetSpeed() { + return false; + } + + @Override + public float getCurrentPitchStepsAdjustment() { + return 0; + } + + @Override + public int getCurrentPosition() { + owningMediaPlayer.lock.lock(); + try { + return mp.getCurrentPosition(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public float getCurrentSpeedMultiplier() { + return 1f; + } + + @Override + public int getDuration() { + owningMediaPlayer.lock.lock(); + try { + return mp.getDuration(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public float getMaxSpeedMultiplier() { + return 1f; + } + + @Override + public float getMinSpeedMultiplier() { + return 1f; + } + + @Override + public boolean isLooping() { + owningMediaPlayer.lock.lock(); + try { + return mp.isLooping(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public boolean isPlaying() { + owningMediaPlayer.lock.lock(); + try { + return mp.isPlaying(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void pause() { + owningMediaPlayer.lock.lock(); + try { + mp.pause(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void prepare() throws IllegalStateException, IOException { + owningMediaPlayer.lock.lock(); + Log.d(AMP_TAG, "prepare()"); + try { + mp.prepare(); + Log.d(AMP_TAG, "Finish prepare()"); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void prepareAsync() { + mp.prepareAsync(); + } + + @Override + public void release() { + owningMediaPlayer.lock.lock(); + try { + if (mp != null) { + Log.d(AMP_TAG, "mp.release()"); + mp.release(); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void reset() { + owningMediaPlayer.lock.lock(); + try { + mp.reset(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void seekTo(int msec) throws IllegalStateException { + owningMediaPlayer.lock.lock(); + try { + mp.setOnSeekCompleteListener(this.onSeekCompleteListener); + mp.seekTo(msec); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setAudioStreamType(int streamtype) { + owningMediaPlayer.lock.lock(); + try { + mp.setAudioStreamType(streamtype); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setDataSource(Context context, Uri uri) + throws IllegalArgumentException, IllegalStateException, IOException { + owningMediaPlayer.lock.lock(); + try { + Log.d(AMP_TAG, "setDataSource(context, " + uri.toString() + ")"); + mp.setDataSource(context, uri); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setDataSource(String path) throws IllegalArgumentException, + IllegalStateException, IOException { + owningMediaPlayer.lock.lock(); + try { + Log.d(AMP_TAG, "setDataSource(" + path + ")"); + mp.setDataSource(path); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { + // Can't! + } + + @Override + public void setLooping(boolean loop) { + owningMediaPlayer.lock.lock(); + try { + mp.setLooping(loop); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setPitchStepsAdjustment(float pitchSteps) { + // Can't! + } + + @Override + public void setPlaybackPitch(float f) { + // Can't! + } + + @Override + public void setPlaybackSpeed(float f) { + // Can't! + Log.d(AMP_TAG, "setPlaybackSpeed(" + f + ")"); + } + + @Override + public void setVolume(float leftVolume, float rightVolume) { + owningMediaPlayer.lock.lock(); + try { + mp.setVolume(leftVolume, rightVolume); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void setWakeMode(Context context, int mode) { + owningMediaPlayer.lock.lock(); + try { + if (mode != 0) { + mp.setWakeMode(context, mode); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void start() { + owningMediaPlayer.lock.lock(); + try { + mp.start(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + @Override + public void stop() { + owningMediaPlayer.lock.lock(); + try { + mp.stop(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } +} diff --git a/library/src/main/java/org/antennapod/audio/MediaPlayer.java b/library/src/main/java/org/antennapod/audio/MediaPlayer.java new file mode 100644 index 0000000..3bdfd12 --- /dev/null +++ b/library/src/main/java/org/antennapod/audio/MediaPlayer.java @@ -0,0 +1,1257 @@ +// Copyright 2011, Aocate, Inc. +// +// 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 org.antennapod.audio; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +public class MediaPlayer { + + public static final String TAG = "MediaPlayer"; + + public interface OnBufferingUpdateListener { + void onBufferingUpdate(MediaPlayer arg0, int percent); + } + + public interface OnCompletionListener { + void onCompletion(MediaPlayer arg0); + } + + public interface OnErrorListener { + boolean onError(MediaPlayer arg0, int what, int extra); + } + + public interface OnInfoListener { + boolean onInfo(MediaPlayer arg0, int what, int extra); + } + + public interface OnPitchAdjustmentAvailableChangedListener { + /** + * @param arg0 The owning media player + * @param pitchAdjustmentAvailable True if pitch adjustment is available, false if not + */ + public abstract void onPitchAdjustmentAvailableChanged( + MediaPlayer arg0, boolean pitchAdjustmentAvailable); + } + + public interface OnPreparedListener { + void onPrepared(MediaPlayer arg0); + } + + public interface OnSeekCompleteListener { + void onSeekComplete(MediaPlayer arg0); + } + + public interface OnSpeedAdjustmentAvailableChangedListener { + /** + * @param arg0 The owning media player + * @param speedAdjustmentAvailable True if speed adjustment is available, false if not + */ + void onSpeedAdjustmentAvailableChanged( + MediaPlayer arg0, boolean speedAdjustmentAvailable); + } + + public enum State { + IDLE, INITIALIZED, PREPARED, STARTED, PAUSED, STOPPED, PREPARING, PLAYBACK_COMPLETED, END, ERROR + } + + private static Uri SPEED_ADJUSTMENT_MARKET_URI = Uri.parse("market://details?id=com.aocate.presto"); + + private static Intent prestoMarketIntent = null; + + public static final int MEDIA_ERROR_SERVER_DIED = android.media.MediaPlayer.MEDIA_ERROR_SERVER_DIED; + public static final int MEDIA_ERROR_UNKNOWN = android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN; + public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = android.media.MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK; + + /** + * Indicates whether the specified action can be used as an intent. This + * method queries the package manager for installed packages that can + * respond to an intent with the specified action. If no suitable package is + * found, this method returns false. + * + * @param context The application's environment. + * @param action The Intent action to check for availability. + * @return True if an Intent with the specified action can be sent and + * responded to, false otherwise. + */ + public static boolean isIntentAvailable(Context context, String action) { + final PackageManager packageManager = context.getPackageManager(); + final Intent intent = new Intent(action); + List<ResolveInfo> list = packageManager.queryIntentServices(intent, + PackageManager.MATCH_DEFAULT_ONLY); + return list.size() > 0; + } + + /** + * Returns an explicit Intent for a service that accepts the given Intent + * or null if no such service was found. + * + * @param context The application's environment. + * @param action The Intent action to check for availability. + * @return The explicit service Intent or null if no service was found. + */ + public static Intent getPrestoServiceIntent(Context context, String action) { + final PackageManager packageManager = context.getPackageManager(); + final Intent actionIntent = new Intent(action); + List<ResolveInfo> list = packageManager.queryIntentServices(actionIntent, + PackageManager.MATCH_DEFAULT_ONLY); + if (list.size() > 0) { + ResolveInfo first = list.get(0); + if (first.serviceInfo != null) { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(first.serviceInfo.packageName, + first.serviceInfo.name)); + Log.i(TAG, "Returning intent:" + intent.toString()); + return intent; + } else { + Log.e(TAG, "Found service that accepts " + action + ", but serviceInfo was null"); + return null; + } + } else { + return null; + } + } + + /** + * Indicates whether the Presto library is installed + * + * @param context The context to use to query the package manager. + * @return True if the Presto library is installed, false if not. + */ + public static boolean isPrestoLibraryInstalled(Context context) { + return isIntentAvailable(context, ServiceBackedAudioPlayer.INTENT_NAME); + } + + /** + * Return an Intent that opens the Android Market page for the speed + * alteration library + * + * @return The Intent for the Presto library on the Android Market + */ + public static Intent getPrestoMarketIntent() { + if (prestoMarketIntent == null) { + prestoMarketIntent = new Intent(Intent.ACTION_VIEW, SPEED_ADJUSTMENT_MARKET_URI); + } + return prestoMarketIntent; + } + + /** + * Open the Android Market page for the Presto library + * + * @param context The context from which to open the Android Market page + */ + public static void openPrestoMarketIntent(Context context) { + context.startActivity(getPrestoMarketIntent()); + } + + private static final String MP_TAG = "ReplacementMediaPlayer"; + + private static final double PITCH_STEP_CONSTANT = 1.0594630943593; + + private AndroidAudioPlayer amp = null; + private ServiceBackedAudioPlayer sbmp = null; + private SonicAudioPlayer smp = null; + + // This is whether speed adjustment should be enabled (by the Service) + // To avoid the Service entirely, set useService to false + protected boolean enableSpeedAdjustment = true; + private int lastKnownPosition = 0; + // In some cases, we're going to have to replace the + // android.media.MediaPlayer on the fly, and we don't want to touch the + // wrong media player, so lock it way too much. + ReentrantLock lock = new ReentrantLock(); + private int mAudioStreamType = AudioManager.STREAM_MUSIC; + private Context mContext; + private boolean mIsLooping = false; + private float mLeftVolume = 1f; + private float mPitchStepsAdjustment = 0f; + private float mRightVolume = 1f; + private float mSpeedMultiplier = 1f; + private int mWakeMode = 0; + AbstractAudioPlayer mpi = null; + protected boolean pitchAdjustmentAvailable = false; + protected boolean speedAdjustmentAvailable = false; + + private Handler mServiceDisconnectedHandler = null; + + // Some parts of state cannot be found by calling MediaPlayerImpl functions, + // so store our own state. This also helps copy state when changing + // implementations + State state = State.INITIALIZED; + String stringDataSource = null; + Uri uriDataSource = null; + private boolean useService = false; + + // Naming Convention for Listeners + // Most listeners can both be set by clients and called by MediaPlayImpls + // There are a few that have to do things in this class as well as calling + // the function. In all cases, onX is what is called by MediaPlayerImpl + // If there is work to be done in this class, then the listener that is + // set by setX is X (with the first letter lowercase). + OnBufferingUpdateListener onBufferingUpdateListener = null; + OnCompletionListener onCompletionListener = null; + OnErrorListener onErrorListener = null; + OnInfoListener onInfoListener = null; + + // Special case. Pitch adjustment ceases to be available when we switch + // to the android.media.MediaPlayer (though it is not guaranteed to be + // available when using the ServiceBackedMediaPlayer) + OnPitchAdjustmentAvailableChangedListener onPitchAdjustmentAvailableChangedListener = new OnPitchAdjustmentAvailableChangedListener() { + public void onPitchAdjustmentAvailableChanged(MediaPlayer arg0, + boolean pitchAdjustmentAvailable) { + lock.lock(); + try { + Log.d(MP_TAG, "onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged being called"); + if (MediaPlayer.this.pitchAdjustmentAvailable != pitchAdjustmentAvailable) { + Log.d(MP_TAG, "Pitch adjustment state has changed from " + + MediaPlayer.this.pitchAdjustmentAvailable + + " to " + pitchAdjustmentAvailable); + MediaPlayer.this.pitchAdjustmentAvailable = pitchAdjustmentAvailable; + if (MediaPlayer.this.pitchAdjustmentAvailableChangedListener != null) { + MediaPlayer.this.pitchAdjustmentAvailableChangedListener + .onPitchAdjustmentAvailableChanged(arg0, pitchAdjustmentAvailable); + } + } + } finally { + lock.unlock(); + } + } + }; + OnPitchAdjustmentAvailableChangedListener pitchAdjustmentAvailableChangedListener = null; + + MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer arg0) { + Log.d(MP_TAG, "onPreparedListener 242 setting state to PREPARED"); + MediaPlayer.this.state = State.PREPARED; + if (MediaPlayer.this.preparedListener != null) { + Log.d(MP_TAG, "Calling preparedListener"); + MediaPlayer.this.preparedListener.onPrepared(arg0); + } + Log.d(MP_TAG, "Wrap up onPreparedListener"); + } + }; + + OnPreparedListener preparedListener = null; + OnSeekCompleteListener onSeekCompleteListener = null; + + // Special case. Speed adjustment ceases to be available when we switch + // to the android.media.MediaPlayer (though it is not guaranteed to be + // available when using the ServiceBackedMediaPlayer) + OnSpeedAdjustmentAvailableChangedListener onSpeedAdjustmentAvailableChangedListener = new OnSpeedAdjustmentAvailableChangedListener() { + public void onSpeedAdjustmentAvailableChanged(MediaPlayer arg0, + boolean speedAdjustmentAvailable) { + lock.lock(); + try { + Log.d(MP_TAG, "onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged being called"); + if (MediaPlayer.this.speedAdjustmentAvailable != speedAdjustmentAvailable) { + Log.d(MP_TAG, "Speed adjustment state has changed from " + + MediaPlayer.this.speedAdjustmentAvailable + + " to " + speedAdjustmentAvailable); + MediaPlayer.this.speedAdjustmentAvailable = speedAdjustmentAvailable; + if (MediaPlayer.this.speedAdjustmentAvailableChangedListener != null) { + MediaPlayer.this.speedAdjustmentAvailableChangedListener + .onSpeedAdjustmentAvailableChanged(arg0, speedAdjustmentAvailable); + } + } + } finally { + lock.unlock(); + } + } + }; + OnSpeedAdjustmentAvailableChangedListener speedAdjustmentAvailableChangedListener = null; + + public MediaPlayer(final Context context) { + this(context, true); + } + + public MediaPlayer(final Context context, boolean useService) { + this.mContext = context; + this.useService = useService; + + // So here's the major problem + // Sometimes the service won't exist or won't be connected, + // so start with an android.media.MediaPlayer, and when + // the service is connected, use that from then on + this.mpi = this.amp = new AndroidAudioPlayer(this, context); + if(Build.VERSION.SDK_INT >= 16) { + this.smp = new SonicAudioPlayer(this, context); + } + + // setupMpi will go get the Service, if it can, then bring that + // implementation into sync + Log.d(MP_TAG, "setupMpi"); + setupMpi(context); + } + + protected boolean useSonic() { + return false; + } + + private boolean invalidServiceConnectionConfiguration() { + if(smp != null) { + boolean usingSonic = this.mpi instanceof SonicAudioPlayer; + if((usingSonic && !useSonic()) || (!usingSonic && useSonic())) { + return true; + } + } + if (!(this.mpi instanceof ServiceBackedAudioPlayer)) { + if (this.useService && isPrestoLibraryInstalled()) { + // In this case, the Presto library has been installed + // or something while playing sound + // We could be using the service, but we're not + Log.d(MP_TAG, "We could be using the service, but we're not"); + return true; + } + // If useService is false, then we shouldn't be using the SBMP + // If the Presto library isn't installed, ditto + Log.d(MP_TAG, "this.mpi is not a ServiceBackedMediaPlayer, but we couldn't use it anyway"); + return false; + } else { + if (BuildConfig.DEBUG && !(this.mpi instanceof ServiceBackedAudioPlayer)) + throw new AssertionError(); + if (this.useService && isPrestoLibraryInstalled()) { + // We should be using the service, and we are. Great! + Log.d(MP_TAG, "We could be using a ServiceBackedMediaPlayer and we are"); + return false; + } + // We're trying to use the service when we shouldn't, + // that's an invalid configuration + Log.d(MP_TAG, "We're trying to use a ServiceBackedMediaPlayer but we shouldn't be"); + return true; + } + } + + private void setupMpi(final Context context) { + lock.lock(); + try { + Log.d(MP_TAG, "setupMpi"); + // Check if the client wants to use the service at all, + // then if we're already using the right kind of media player + if(useSonic()) { + if(mpi != null && mpi instanceof SonicAudioPlayer) { + Log.d(MP_TAG, "Already using SonicMediaPlayer"); + return; + } else { + Log.d(MP_TAG, "Switching to SonicMediaPlayer"); + switchMediaPlayerImpl(mpi, smp); + return; + } + } else if (this.useService && isPrestoLibraryInstalled()) { + if (mpi != null && mpi instanceof ServiceBackedAudioPlayer) { + Log.d(MP_TAG, "Already using ServiceBackedMediaPlayer"); + return; + } + if (this.sbmp == null) { + Log.d(MP_TAG, "Instantiating new ServiceBackedMediaPlayer"); + this.sbmp = new ServiceBackedAudioPlayer(this, context, + new ServiceConnection() { + public void onServiceConnected(ComponentName className, final IBinder service) { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + // This lock probably isn't granular + // enough + MediaPlayer.this.lock.lock(); + Log.d(MP_TAG, "onServiceConnected"); + try { + switchMediaPlayerImpl(mpi, sbmp); + Log.d(MP_TAG, "End onServiceConnected"); + } finally { + MediaPlayer.this.lock.unlock(); + } + } + }); + t.start(); + } + + public void onServiceDisconnected(ComponentName className) { + MediaPlayer.this.lock.lock(); + try { + // Can't get any more useful information + // out of sbmp + if (MediaPlayer.this.sbmp != null) { + MediaPlayer.this.sbmp.release(); + } + // Unlike most other cases, sbmp gets set + // to null since there's nothing useful + // backing it now + MediaPlayer.this.sbmp = null; + + if (mServiceDisconnectedHandler == null) { + mServiceDisconnectedHandler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + // switchMediaPlayerImpl won't try to + // clone anything from null + lock.lock(); + try { + if (MediaPlayer.this.amp == null) { + // This should never be in this state + MediaPlayer.this.amp = new AndroidAudioPlayer( + MediaPlayer.this, + MediaPlayer.this.mContext); + } + // Use sbmp instead of null in case by some miracle it's + // been restored in the meantime + switchMediaPlayerImpl(mpi, amp); + return true; + } finally { + lock.unlock(); + } + } + }); + } + + // This code needs to execute on the + // original thread to instantiate + // the new object in the right place + mServiceDisconnectedHandler.sendMessage( + mServiceDisconnectedHandler.obtainMessage()); + // Note that we do NOT want to set + // useService. useService is about + // what the user wants, not what they + // get + } finally { + MediaPlayer.this.lock.unlock(); + } + } + } + ); + } + Log.d(MP_TAG, "Switching to ServiceBackedMediaPlayer"); + switchMediaPlayerImpl(mpi, sbmp); + } else { + if (this.mpi != null && this.mpi instanceof AndroidAudioPlayer) { + Log.d(MP_TAG, "Already using AndroidMediaPlayer"); + return; + } + if (this.amp == null) { + Log.d(MP_TAG, "Instantiating new AndroidMediaPlayer (this should be impossible)"); + this.amp = new AndroidAudioPlayer(this, context); + } + switchMediaPlayerImpl(mpi, this.amp); + } + } finally { + lock.unlock(); + } + } + + private void switchMediaPlayerImpl(AbstractAudioPlayer from, AbstractAudioPlayer to) { + lock.lock(); + try { + Log.d(MP_TAG, "switchMediaPlayerImpl"); + if (from == to + // Same object, nothing to synchronize + || to == null + // Nothing to copy to (maybe this should throw an error?) + || (to instanceof ServiceBackedAudioPlayer && !((ServiceBackedAudioPlayer) to).isConnected()) + // ServiceBackedMediaPlayer hasn't yet connected, onServiceConnected will take care of the transition + || (MediaPlayer.this.state == State.END)) { + // State.END is after a release(), no further functions should + // be called on this class and from is likely to have problems + // retrieving state that won't be used anyway + return; + } + // Extract all that we can from the existing implementation + // and copy it to the new implementation + + Log.d(MP_TAG, "switchMediaPlayerImpl(), current state is " + this.state.toString()); + + to.reset(); + + // Do this first so we don't have to prepare the same + // data file twice + to.setEnableSpeedAdjustment(MediaPlayer.this.enableSpeedAdjustment); + + // This is a reasonable place to set all of these, + // none of them require prepare() or the like first + to.setAudioStreamType(this.mAudioStreamType); + to.setLooping(this.mIsLooping); + to.setPitchStepsAdjustment(this.mPitchStepsAdjustment); + Log.d(MP_TAG, "Setting playback speed to " + this.mSpeedMultiplier); + to.setPlaybackSpeed(this.mSpeedMultiplier); + to.setVolume(MediaPlayer.this.mLeftVolume, + MediaPlayer.this.mRightVolume); + to.setWakeMode(this.mContext, this.mWakeMode); + + Log.d(MP_TAG, "asserting at least one data source is null"); + assert ((MediaPlayer.this.stringDataSource == null) || (MediaPlayer.this.uriDataSource == null)); + + if (uriDataSource != null) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): uriDataSource != null"); + try { + to.setDataSource(this.mContext, uriDataSource); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (stringDataSource != null) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): stringDataSource != null"); + try { + to.setDataSource(stringDataSource); + } catch (Exception e) { + e.printStackTrace(); + } + } + if ((this.state == State.PREPARED) + || (this.state == State.PREPARING) + || (this.state == State.PAUSED) + || (this.state == State.STOPPED) + || (this.state == State.STARTED) + || (this.state == State.PLAYBACK_COMPLETED)) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): prepare and seek"); + // Use prepare here instead of prepareAsync so that + // we wait for it to be ready before we try to use it + try { + to.muteNextOnPrepare(); + to.prepare(); + } catch (Exception e) { + e.printStackTrace(); + } + + int seekPos = 0; + if (from != null) { + seekPos = from.getCurrentPosition(); + } else if (this.lastKnownPosition < to.getDuration()) { + // This can happen if the Service unexpectedly + // disconnected. Because it would result in too much + // information being passed around, we don't constantly + // poll for the lastKnownPosition, but we'll save it + // when getCurrentPosition is called + seekPos = this.lastKnownPosition; + } + to.muteNextSeek(); + to.seekTo(seekPos); + } + if ((from != null) + && from.isPlaying()) { + from.pause(); + } + if ((this.state == State.STARTED) + || (this.state == State.PAUSED) + || (this.state == State.STOPPED)) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): start"); + if (to != null) { + to.start(); + } + } + + if (this.state == State.PAUSED) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): paused"); + if (to != null) { + to.pause(); + } + } else if (this.state == State.STOPPED) { + Log.d(MP_TAG, "switchMediaPlayerImpl(): stopped"); + if (to != null) { + to.stop(); + } + } + + this.mpi = to; + Log.d(TAG, "Switched to " + to.getClass().toString()); + + // Cheating here by relying on the side effect in + // on(Pitch|Speed)AdjustmentAvailableChanged + if ((to.canSetPitch() != this.pitchAdjustmentAvailable) + && (this.onPitchAdjustmentAvailableChangedListener != null)) { + this.onPitchAdjustmentAvailableChangedListener + .onPitchAdjustmentAvailableChanged(this, to + .canSetPitch()); + } + if ((to.canSetSpeed() != this.speedAdjustmentAvailable) + && (this.onSpeedAdjustmentAvailableChangedListener != null)) { + this.onSpeedAdjustmentAvailableChangedListener + .onSpeedAdjustmentAvailableChanged(this, to + .canSetSpeed()); + } + Log.d(MP_TAG, "switchMediaPlayerImpl() " + this.state.toString()); + } finally { + lock.unlock(); + } + } + + /** + * Returns true if pitch can be changed at this moment + * + * @return True if pitch can be changed + */ + public boolean canSetPitch() { + lock.lock(); + try { + return this.mpi.canSetPitch(); + } finally { + lock.unlock(); + } + } + + /** + * Returns true if speed can be changed at this moment + * + * @return True if speed can be changed + */ + public boolean canSetSpeed() { + lock.lock(); + try { + return this.mpi.canSetSpeed(); + } finally { + lock.unlock(); + } + } + + protected void finalize() throws Throwable { + lock.lock(); + try { + Log.d(MP_TAG, "finalize()"); + this.release(); + } finally { + lock.unlock(); + } + } + + /** + * Returns the number of steps (in a musical scale) by which playback is + * currently shifted. When greater than zero, pitch is shifted up. When less + * than zero, pitch is shifted down. + * + * @return The number of steps pitch is currently shifted by + */ + public float getCurrentPitchStepsAdjustment() { + lock.lock(); + try { + return this.mpi.getCurrentPitchStepsAdjustment(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.getCurrentPosition() + * Accurate only to frame size of encoded data (26 ms for MP3s) + * + * @return Current position (in milliseconds) + */ + public int getCurrentPosition() { + lock.lock(); + try { + return (this.lastKnownPosition = this.mpi.getCurrentPosition()); + } finally { + lock.unlock(); + } + } + + /** + * Returns the current speed multiplier. Defaults to 1.0 (normal speed) + * + * @return The current speed multiplier + */ + public float getCurrentSpeedMultiplier() { + lock.lock(); + try { + return this.mpi.getCurrentSpeedMultiplier(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.getDuration() + * + * @return Length of the track (in milliseconds) + */ + public int getDuration() { + lock.lock(); + try { + return this.mpi.getDuration(); + } finally { + lock.unlock(); + } + } + + /** + * Get the maximum value that can be passed to setPlaybackSpeed + * + * @return The maximum speed multiplier + */ + public float getMaxSpeedMultiplier() { + lock.lock(); + try { + return this.mpi.getMaxSpeedMultiplier(); + } finally { + lock.unlock(); + } + } + + /** + * Get the minimum value that can be passed to setPlaybackSpeed + * + * @return The minimum speed multiplier + */ + public float getMinSpeedMultiplier() { + lock.lock(); + try { + return this.mpi.getMinSpeedMultiplier(); + } finally { + lock.unlock(); + } + } + + /** + * Gets the version code of the backing service + * + * @return -1 if ServiceBackedMediaPlayer is not used, 0 if the service is not + * connected, otherwise the version code retrieved from the service + */ + public int getServiceVersionCode() { + lock.lock(); + try { + if (this.mpi instanceof ServiceBackedAudioPlayer) { + return ((ServiceBackedAudioPlayer) this.mpi).getServiceVersionCode(); + } else { + return -1; + } + } finally { + lock.unlock(); + } + } + + /** + * Gets the version name of the backing service + * + * @return null if ServiceBackedMediaPlayer is not used, empty string if + * the service is not connected, otherwise the version name retrieved from + * the service + */ + public String getServiceVersionName() { + lock.lock(); + try { + if (this.mpi instanceof ServiceBackedAudioPlayer) { + return ((ServiceBackedAudioPlayer) this.mpi).getServiceVersionName(); + } else { + return null; + } + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.isLooping() + * + * @return True if the track is looping + */ + public boolean isLooping() { + lock.lock(); + try { + return this.mpi.isLooping(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.isPlaying() + * + * @return True if the track is playing + */ + public boolean isPlaying() { + lock.lock(); + try { + return this.mpi.isPlaying(); + } finally { + lock.unlock(); + } + } + + /** + * Returns true if this MediaPlayer has access to the Presto + * library + * + * @return True if the Presto library is installed + */ + public boolean isPrestoLibraryInstalled() { + if ((this.mpi == null) || (this.mpi.mContext == null)) { + return false; + } + return isPrestoLibraryInstalled(this.mpi.mContext); + } + + /** + * Open the Android Market page in the same context as this MediaPlayer + */ + public void openPrestoMarketIntent() { + if ((this.mpi != null) && (this.mpi.mContext != null)) { + openPrestoMarketIntent(this.mpi.mContext); + } + } + + /** + * Functions identically to android.media.MediaPlayer.pause() Pauses the + * track + */ + public void pause() { + lock.lock(); + try { + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.PAUSED; + this.mpi.pause(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.prepare() Prepares the + * track. This or prepareAsync must be called before start() + */ + public void prepare() throws IllegalStateException, IOException { + lock.lock(); + try { + Log.d(MP_TAG, "prepare() using " + ((this.mpi == null) ? "null (this shouldn't happen)" : this.mpi.getClass().toString()) + " state " + this.state.toString()); + Log.d(MP_TAG, "onPreparedListener is: " + ((this.onPreparedListener == null) ? "null" : "non-null")); + Log.d(MP_TAG, "preparedListener is: " + ((this.preparedListener == null) ? "null" : "non-null")); + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.mpi.prepare(); + this.state = State.PREPARED; + Log.d(MP_TAG, "prepare() finished"); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.prepareAsync() + * Prepares the track. This or prepare must be called before start() + */ + public void prepareAsync() { + lock.lock(); + try { + Log.d(MP_TAG, "prepareAsync()"); + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.PREPARING; + this.mpi.prepareAsync(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.release() Releases the + * underlying resources used by the media player. + */ + public void release() { + lock.lock(); + try { + Log.d(MP_TAG, "Releasing MediaPlayer"); + + this.state = State.END; + if (this.amp != null) { + this.amp.release(); + } + if (this.sbmp != null) { + this.sbmp.release(); + } + + this.onBufferingUpdateListener = null; + this.onCompletionListener = null; + this.onErrorListener = null; + this.onInfoListener = null; + this.preparedListener = null; + this.onPitchAdjustmentAvailableChangedListener = null; + this.pitchAdjustmentAvailableChangedListener = null; + Log.d(MP_TAG, "Setting onSeekCompleteListener to null 871"); + this.onSeekCompleteListener = null; + this.onSpeedAdjustmentAvailableChangedListener = null; + this.speedAdjustmentAvailableChangedListener = null; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.reset() Resets the + * track to idle state + */ + public void reset() { + lock.lock(); + try { + this.state = State.IDLE; + this.stringDataSource = null; + this.uriDataSource = null; + this.mpi.reset(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.seekTo(int msec) Seeks + * to msec in the track + */ + public void seekTo(int msec) throws IllegalStateException { + lock.lock(); + try { + this.mpi.seekTo(msec); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setAudioStreamType(int + * streamtype) Sets the audio stream type. + */ + public void setAudioStreamType(int streamtype) { + lock.lock(); + try { + this.mAudioStreamType = streamtype; + this.mpi.setAudioStreamType(streamtype); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setDataSource(Context + * context, Uri uri) Sets uri as data source in the context given + */ + public void setDataSource(Context context, Uri uri) + throws IllegalArgumentException, IllegalStateException, IOException { + lock.lock(); + try { + Log.d(MP_TAG, "In setDataSource(context, " + uri.toString() + "), using " + this.mpi.getClass().toString()); + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.INITIALIZED; + this.stringDataSource = null; + this.uriDataSource = uri; + this.mpi.setDataSource(context, uri); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setDataSource(String + * path) Sets the data source of the track to a file given. + */ + public void setDataSource(String path) throws IllegalArgumentException, + IllegalStateException, IOException { + lock.lock(); + try { + Log.d(MP_TAG, "In setDataSource(context, " + path + ")"); + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.INITIALIZED; + this.stringDataSource = path; + this.uriDataSource = null; + this.mpi.setDataSource(path); + } finally { + lock.unlock(); + } + } + + /** + * Sets whether to use speed adjustment or not. Speed adjustment on is more + * computation-intensive than with it off. + * + * @param enableSpeedAdjustment Whether speed adjustment should be supported. + */ + public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { + lock.lock(); + try { + this.enableSpeedAdjustment = enableSpeedAdjustment; + this.mpi.setEnableSpeedAdjustment(enableSpeedAdjustment); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setLooping(boolean + * loop) Sets the track to loop infinitely if loop is true, play once if + * loop is false + */ + public void setLooping(boolean loop) { + lock.lock(); + try { + this.mIsLooping = loop; + this.mpi.setLooping(loop); + } finally { + lock.unlock(); + } + } + + /** + * Sets the number of steps (in a musical scale) by which playback is + * currently shifted. When greater than zero, pitch is shifted up. When less + * than zero, pitch is shifted down. + * + * @param pitchSteps The number of steps by which to shift playback + */ + public void setPitchStepsAdjustment(float pitchSteps) { + lock.lock(); + try { + this.mPitchStepsAdjustment = pitchSteps; + this.mpi.setPitchStepsAdjustment(pitchSteps); + } finally { + lock.unlock(); + } + } + + private static float getPitchStepsAdjustment(float pitch) { + return (float) (Math.log(pitch) / (2 * Math.log(PITCH_STEP_CONSTANT))); + } + + /** + * Sets the percentage by which pitch is currently shifted. When greater + * than zero, pitch is shifted up. When less than zero, pitch is shifted + * down + * + * @param pitch The percentage to shift pitch + */ + public void setPlaybackPitch(float pitch) { + lock.lock(); + try { + this.mPitchStepsAdjustment = getPitchStepsAdjustment(pitch); + this.mpi.setPlaybackPitch(pitch); + } finally { + lock.unlock(); + } + } + + /** + * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so on. + * Speed should never be set to 0 or below. + * + * @param f The speed multiplier to use for further playback + */ + public void setPlaybackSpeed(float f) { + lock.lock(); + try { + this.mSpeedMultiplier = f; + this.mpi.setPlaybackSpeed(f); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setVolume(float + * leftVolume, float rightVolume) Sets the stereo volume + */ + public void setVolume(float leftVolume, float rightVolume) { + lock.lock(); + try { + this.mLeftVolume = leftVolume; + this.mRightVolume = rightVolume; + this.mpi.setVolume(leftVolume, rightVolume); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setWakeMode(Context + * context, int mode) Acquires a wake lock in the context given. You must + * request the appropriate permissions in your AndroidManifest.xml file. + */ + public void setWakeMode(Context context, int mode) { + lock.lock(); + try { + this.mWakeMode = mode; + this.mpi.setWakeMode(context, mode); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener + * listener) Sets a listener to be used when a track completes playing. + */ + public void setOnBufferingUpdateListener(OnBufferingUpdateListener listener) { + lock.lock(); + try { + this.onBufferingUpdateListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener + * listener) Sets a listener to be used when a track completes playing. + */ + public void setOnCompletionListener(OnCompletionListener listener) { + lock.lock(); + try { + this.onCompletionListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnErrorListener(OnErrorListener listener) + * Sets a listener to be used when a track encounters an error. + */ + public void setOnErrorListener(OnErrorListener listener) { + lock.lock(); + try { + this.onErrorListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnInfoListener(OnInfoListener listener) Sets + * a listener to be used when a track has info. + */ + public void setOnInfoListener(OnInfoListener listener) { + lock.lock(); + try { + this.onInfoListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Sets a listener that will fire when pitch adjustment becomes available or + * stops being available + */ + public void setOnPitchAdjustmentAvailableChangedListener( + OnPitchAdjustmentAvailableChangedListener listener) { + lock.lock(); + try { + this.pitchAdjustmentAvailableChangedListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnPreparedListener(OnPreparedListener + * listener) Sets a listener to be used when a track finishes preparing. + */ + public void setOnPreparedListener(OnPreparedListener listener) { + lock.lock(); + Log.d(MP_TAG, " ++++++++++++++++++++++++++++++++++++++++++++ setOnPreparedListener"); + try { + this.preparedListener = listener; + // For this one, we do not explicitly set the MediaPlayer or the + // Service listener. This is because in addition to calling the + // listener provided by the client, it's necessary to change + // state to PREPARED. See prepareAsync for implementation details + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to + * android.media.MediaPlayer.setOnSeekCompleteListener + * (OnSeekCompleteListener listener) Sets a listener to be used when a track + * finishes seeking. + */ + public void setOnSeekCompleteListener(OnSeekCompleteListener listener) { + lock.lock(); + try { + this.onSeekCompleteListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Sets a listener that will fire when speed adjustment becomes available or + * stops being available + */ + public void setOnSpeedAdjustmentAvailableChangedListener( + OnSpeedAdjustmentAvailableChangedListener listener) { + lock.lock(); + try { + this.speedAdjustmentAvailableChangedListener = listener; + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.start() Starts a track + * playing + */ + public void start() { + lock.lock(); + try { + Log.d(MP_TAG, "start()"); + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.STARTED; + this.mpi.start(); + } finally { + lock.unlock(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.stop() Stops a track + * playing and resets its position to the start. + */ + public void stop() { + lock.lock(); + try { + if (invalidServiceConnectionConfiguration()) { + setupMpi(this.mpi.mContext); + } + this.state = State.STOPPED; + this.mpi.stop(); + } finally { + lock.unlock(); + } + } + +} diff --git a/library/src/main/java/org/antennapod/audio/ServiceBackedAudioPlayer.java b/library/src/main/java/org/antennapod/audio/ServiceBackedAudioPlayer.java new file mode 100644 index 0000000..238de3a --- /dev/null +++ b/library/src/main/java/org/antennapod/audio/ServiceBackedAudioPlayer.java @@ -0,0 +1,1184 @@ +// Copyright 2011, Aocate, Inc. +// +// 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. +// +// ----------------------------------------------------------------------- +// Compared to the original version, this class been slightly modified so +// that any acquired WakeLocks are only held while the MediaPlayer is +// playing (see the stayAwake method for more details). + + +package org.antennapod.audio; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.AudioManager; +import android.net.Uri; +import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.RemoteException; +import android.util.Log; + +import com.aocate.presto.service.IDeathCallback_0_8; +import com.aocate.presto.service.IOnBufferingUpdateListenerCallback_0_8; +import com.aocate.presto.service.IOnCompletionListenerCallback_0_8; +import com.aocate.presto.service.IOnErrorListenerCallback_0_8; +import com.aocate.presto.service.IOnInfoListenerCallback_0_8; +import com.aocate.presto.service.IOnPitchAdjustmentAvailableChangedListenerCallback_0_8; +import com.aocate.presto.service.IOnPreparedListenerCallback_0_8; +import com.aocate.presto.service.IOnSeekCompleteListenerCallback_0_8; +import com.aocate.presto.service.IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8; +import com.aocate.presto.service.IPlayMedia_0_8; + +import java.io.IOException; + +/** + * Class for connecting to remote speed-altering, media playing Service + * Note that there is unusually high coupling between MediaPlayer and this + * class. This is an unfortunate compromise, since the alternative was to + * track state in two different places in this code (plus the internal state + * of the remote media player). + * @author aocate + * + */ +public class ServiceBackedAudioPlayer extends AbstractAudioPlayer { + + static final String INTENT_NAME = "com.aocate.intent.PLAY_AUDIO_ADJUST_SPEED_0_8"; + + private static final String SBMP_TAG = "ServiceBackedMediaPlaye"; + + private ServiceConnection mPlayMediaServiceConnection = null; + protected IPlayMedia_0_8 pmInterface = null; + private Intent playMediaServiceIntent = null; + // In some cases, we're going to have to replace the + // android.media.MediaPlayer on the fly, and we don't want to touch the + // wrong media player. + + private long sessionId = 0; + private boolean isErroring = false; + private int mAudioStreamType = AudioManager.STREAM_MUSIC; + + private WakeLock mWakeLock = null; + + // So here's the major problem + // Sometimes the service won't exist or won't be connected, + // so start with an android.media.MediaPlayer, and when + // the service is connected, use that from then on + public ServiceBackedAudioPlayer(MediaPlayer owningMediaPlayer, final Context context, final ServiceConnection serviceConnection) { + super(owningMediaPlayer, context); + Log.d(SBMP_TAG, "Instantiating ServiceBackedMediaPlayer 87"); + this.playMediaServiceIntent = + MediaPlayer.getPrestoServiceIntent(context, INTENT_NAME); + this.mPlayMediaServiceConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName name, IBinder service) { + IPlayMedia_0_8 tmpPlayMediaInterface = IPlayMedia_0_8.Stub.asInterface((IBinder) service); + + Log.d(SBMP_TAG, "Setting up pmInterface 94"); + if (ServiceBackedAudioPlayer.this.sessionId == 0) { + try { + // The IDeathCallback isn't a conventional callback. + // It exists so that if the client ceases to exist, + // the Service becomes aware of that and can shut + // down whatever it needs to shut down + ServiceBackedAudioPlayer.this.sessionId = tmpPlayMediaInterface.startSession(new IDeathCallback_0_8.Stub() { + }); + // This is really bad if this fails + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + Log.d(SBMP_TAG, "Assigning pmInterface"); + + ServiceBackedAudioPlayer.this.setOnBufferingUpdateCallback(tmpPlayMediaInterface); + ServiceBackedAudioPlayer.this.setOnCompletionCallback(tmpPlayMediaInterface); + ServiceBackedAudioPlayer.this.setOnErrorCallback(tmpPlayMediaInterface); + ServiceBackedAudioPlayer.this.setOnInfoCallback(tmpPlayMediaInterface); + ServiceBackedAudioPlayer.this.setOnPitchAdjustmentAvailableChangedListener(tmpPlayMediaInterface); + ServiceBackedAudioPlayer.this.setOnPreparedCallback(tmpPlayMediaInterface); + ServiceBackedAudioPlayer.this.setOnSeekCompleteCallback(tmpPlayMediaInterface); + ServiceBackedAudioPlayer.this.setOnSpeedAdjustmentAvailableChangedCallback(tmpPlayMediaInterface); + + // In order to avoid race conditions from the sessionId or listener not being assigned + pmInterface = tmpPlayMediaInterface; + + Log.d(SBMP_TAG, "Invoking onServiceConnected"); + serviceConnection.onServiceConnected(name, service); + } + + public void onServiceDisconnected(ComponentName name) { + Log.d(SBMP_TAG, "onServiceDisconnected 114"); + + pmInterface = null; + + sessionId = 0; + + serviceConnection.onServiceDisconnected(name); + } + }; + + Log.d(SBMP_TAG, "Connecting PlayMediaService 124"); + if (!ConnectPlayMediaService()) { + Log.e(SBMP_TAG, "bindService failed"); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private boolean ConnectPlayMediaService() { + Log.d(SBMP_TAG, "ConnectPlayMediaService()"); + + if (MediaPlayer.isIntentAvailable(mContext, INTENT_NAME)) { + Log.d(SBMP_TAG, INTENT_NAME + " is available"); + if (pmInterface == null) { + try { + Log.d(SBMP_TAG, "Binding service"); + return mContext.bindService(playMediaServiceIntent, mPlayMediaServiceConnection, Context.BIND_AUTO_CREATE); + } catch (Exception e) { + Log.e(SBMP_TAG, "Could not bind with service", e); + return false; + } + } else { + Log.d(SBMP_TAG, "Service already bound"); + return true; + } + } + else { + Log.d(SBMP_TAG, INTENT_NAME + " is not available"); + return false; + } + } + + /** + * Returns true if pitch can be changed at this moment + * @return True if pitch can be changed + */ + @Override + public boolean canSetPitch() { + Log.d(SBMP_TAG, "canSetPitch() 155"); + + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set pitch if the service isn't connected + try { + return pmInterface.canSetPitch(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return false; + } + + /** + * Returns true if speed can be changed at this moment + * @return True if speed can be changed + */ + @Override + public boolean canSetSpeed() { + Log.d(SBMP_TAG, "canSetSpeed() 180"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the service isn't connected + try { + return pmInterface.canSetSpeed(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return false; + } + + void error(int what, int extra) { + owningMediaPlayer.lock.lock(); + Log.e(SBMP_TAG, "error(" + what + ", " + extra + ")"); + stayAwake(false); + try { + if (!this.isErroring) { + this.isErroring = true; + owningMediaPlayer.state = MediaPlayer.State.ERROR; + if (owningMediaPlayer.onErrorListener != null) { + if (owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra)) { + return; + } + } + if (owningMediaPlayer.onCompletionListener != null) { + owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); + } + } + } + finally { + this.isErroring = false; + owningMediaPlayer.lock.unlock(); + } + } + + protected void finalize() throws Throwable { + owningMediaPlayer.lock.lock(); + try { + Log.d(SBMP_TAG, "finalize() 224"); + this.release(); + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + /** + * Returns the number of steps (in a musical scale) by which playback is + * currently shifted. When greater than zero, pitch is shifted up. + * When less than zero, pitch is shifted down. + * @return The number of steps pitch is currently shifted by + */ + @Override + public float getCurrentPitchStepsAdjustment() { + Log.d(SBMP_TAG, "getCurrentPitchStepsAdjustment() 240"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set pitch if the service isn't connected + try { + return pmInterface.getCurrentPitchStepsAdjustment( + ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return 0f; + } + + /** + * Functions identically to android.media.MediaPlayer.getCurrentPosition() + * @return Current position (in milliseconds) + */ + @Override + public int getCurrentPosition() { + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + return pmInterface.getCurrentPosition( + ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + return 0; + } + + /** + * Returns the current speed multiplier. Defaults to 1.0 (normal speed) + * @return The current speed multiplier + */ + @Override + public float getCurrentSpeedMultiplier() { + Log.d(SBMP_TAG, "getCurrentSpeedMultiplier() 286"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the service isn't connected + try { + return pmInterface.getCurrentSpeedMultiplier( + ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return 1; + } + + /** + * Functions identically to android.media.MediaPlayer.getDuration() + * @return Length of the track (in milliseconds) + */ + @Override + public int getDuration() { + Log.d(SBMP_TAG, "getDuration() 311"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + return pmInterface.getDuration(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + return 0; + } + + /** + * Get the maximum value that can be passed to setPlaybackSpeed + * @return The maximum speed multiplier + */ + @Override + public float getMaxSpeedMultiplier() { + Log.d(SBMP_TAG, "getMaxSpeedMultiplier() 332"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + return pmInterface.getMaxSpeedMultiplier( + ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return 1f; + } + + /** + * Get the minimum value that can be passed to setPlaybackSpeed + * @return The minimum speed multiplier + */ + @Override + public float getMinSpeedMultiplier() { + Log.d(SBMP_TAG, "getMinSpeedMultiplier() 357"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + return pmInterface.getMinSpeedMultiplier( + ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return 1f; + } + + public int getServiceVersionCode() { + Log.d(SBMP_TAG, "getVersionCode"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + return pmInterface.getVersionCode(); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + return 0; + } + + public String getServiceVersionName() { + Log.d(SBMP_TAG, "getVersionName"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + return pmInterface.getVersionName(); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + return ""; + } + + public boolean isConnected() { + return (pmInterface != null); + } + + /** + * Functions identically to android.media.MediaPlayer.isLooping() + * @return True if the track is looping + */ + @Override + public boolean isLooping() { + Log.d(SBMP_TAG, "isLooping() 382"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + return pmInterface.isLooping(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + return false; + } + + /** + * Functions identically to android.media.MediaPlayer.isPlaying() + * @return True if the track is playing + */ + @Override + public boolean isPlaying() { + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + try { + return pmInterface.isPlaying(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + return false; + } + + /** + * Functions identically to android.media.MediaPlayer.pause() + * Pauses the track + */ + @Override + public void pause() { + Log.d(SBMP_TAG, "pause() 424"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.pause(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + stayAwake(false); + } + + /** + * Functions identically to android.media.MediaPlayer.prepare() + * Prepares the track. This or prepareAsync must be called before start() + */ + @Override + public void prepare() throws IllegalStateException, IOException { + Log.d(SBMP_TAG, "prepare() 444"); + Log.d(SBMP_TAG, "onPreparedCallback is: " + ((this.mOnPreparedCallback == null) ? "null" : "non-null")); + if (pmInterface == null) { + Log.d(SBMP_TAG, "prepare: pmInterface is null"); + if (!ConnectPlayMediaService()) { + Log.d(SBMP_TAG, "prepare: Failed to connect play media service"); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + Log.d(SBMP_TAG, "prepare: pmInterface isn't null"); + try { + Log.d(SBMP_TAG, "prepare: Remote invoke pmInterface.prepare(" + ServiceBackedAudioPlayer.this.sessionId + ")"); + pmInterface.prepare(ServiceBackedAudioPlayer.this.sessionId); + Log.d(SBMP_TAG, "prepare: prepared"); + } catch (RemoteException e) { + Log.d(SBMP_TAG, "prepare: RemoteException"); + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + Log.d(SBMP_TAG, "Done with prepare()"); + } + + /** + * Functions identically to android.media.MediaPlayer.prepareAsync() + * Prepares the track. This or prepare must be called before start() + */ + @Override + public void prepareAsync() { + Log.d(SBMP_TAG, "prepareAsync() 469"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.prepareAsync(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.release() + * Releases the underlying resources used by the media player. + */ + @Override + public void release() { + Log.d(SBMP_TAG, "release() 492"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + Log.d(SBMP_TAG, "release() 500"); + try { + pmInterface.release(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + mContext.unbindService(this.mPlayMediaServiceConnection); + // Don't try to keep awake (if we were) + this.setWakeMode(mContext, 0); + pmInterface = null; + this.sessionId = 0; + } + + if ((this.mWakeLock != null) && this.mWakeLock.isHeld()) { + Log.d(SBMP_TAG, "Releasing wakelock"); + this.mWakeLock.release(); + } + } + + /** + * Functions identically to android.media.MediaPlayer.reset() + * Resets the track to idle state + */ + @Override + public void reset() { + Log.d(SBMP_TAG, "reset() 523"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.reset(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + stayAwake(false); + } + + /** + * Functions identically to android.media.MediaPlayer.seekTo(int msec) + * Seeks to msec in the track + */ + @Override + public void seekTo(int msec) throws IllegalStateException { + Log.d(SBMP_TAG, "seekTo(" + msec + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.seekTo(ServiceBackedAudioPlayer.this.sessionId, msec); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setAudioStreamType(int streamtype) + * Sets the audio stream type. + */ + @Override + public void setAudioStreamType(int streamtype) { + Log.d(SBMP_TAG, "setAudioStreamType(" + streamtype + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.setAudioStreamType( + ServiceBackedAudioPlayer.this.sessionId, + this.mAudioStreamType); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + + /** + * Functions identically to android.media.MediaPlayer.setDataSource(Context context, Uri uri) + * Sets uri as data source in the context given + */ + @Override + public void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException { + Log.d(SBMP_TAG, "setDataSource(context, uri)"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.setDataSourceUri( + ServiceBackedAudioPlayer.this.sessionId, + uri); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setDataSource(String path) + * Sets the data source of the track to a file given. + */ + @Override + public void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException { + Log.d(SBMP_TAG, "setDataSource(path)"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface == null) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + else { + try { + pmInterface.setDataSourceString( + ServiceBackedAudioPlayer.this.sessionId, + path); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + } + + /** + * Sets whether to use speed adjustment or not. Speed adjustment on is + * more computation-intensive than with it off. + * @param enableSpeedAdjustment Whether speed adjustment should be supported. + */ + @Override + public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { + // TODO: This has no business being here, I think + owningMediaPlayer.lock.lock(); + Log.d(SBMP_TAG, "setEnableSpeedAdjustment(enableSpeedAdjustment)"); + try { + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + pmInterface.setEnableSpeedAdjustment( + ServiceBackedAudioPlayer.this.sessionId, + enableSpeedAdjustment); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + + + /** + * Functions identically to android.media.MediaPlayer.setLooping(boolean loop) + * Sets the track to loop infinitely if loop is true, play once if loop is false + */ + @Override + public void setLooping(boolean loop) { + Log.d(SBMP_TAG, "setLooping(" + loop + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.setLooping(ServiceBackedAudioPlayer.this.sessionId, loop); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Sets the number of steps (in a musical scale) by which playback is + * currently shifted. When greater than zero, pitch is shifted up. + * When less than zero, pitch is shifted down. + * + * @param pitchSteps The number of steps by which to shift playback + */ + @Override + public void setPitchStepsAdjustment(float pitchSteps) { + Log.d(SBMP_TAG, "setPitchStepsAdjustment(" + pitchSteps + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + pmInterface.setPitchStepsAdjustment( + ServiceBackedAudioPlayer.this.sessionId, + pitchSteps); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + } + + /** + * Sets the percentage by which pitch is currently shifted. When + * greater than zero, pitch is shifted up. When less than zero, pitch + * is shifted down + * @param f The percentage to shift pitch + */ + @Override + public void setPlaybackPitch(float f) { + Log.d(SBMP_TAG, "setPlaybackPitch(" + f + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + pmInterface.setPlaybackPitch( + ServiceBackedAudioPlayer.this.sessionId, + f); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + } + + /** + * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so + * on. Speed should never be set to 0 or below. + * @param f The speed multiplier to use for further playback + */ + @Override + public void setPlaybackSpeed(float f) { + Log.d(SBMP_TAG, "setPlaybackSpeed(" + f + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + if (pmInterface != null) { + // Can't set speed if the Service isn't connected + try { + pmInterface.setPlaybackSpeed( + ServiceBackedAudioPlayer.this.sessionId, + f); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + } + + /** + * Functions identically to android.media.MediaPlayer.setVolume(float leftVolume, float rightVolume) + * Sets the stereo volume + */ + @Override + public void setVolume(float leftVolume, float rightVolume) { + Log.d(SBMP_TAG, "setVolume(" + leftVolume + ", " + rightVolume + ")"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.setVolume( + ServiceBackedAudioPlayer.this.sessionId, + leftVolume, + rightVolume); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.setWakeMode(Context context, int mode) + * Acquires a wake lock in the context given. You must request the appropriate permissions + * in your AndroidManifest.xml file. + */ + @Override + // This does not just call .setWakeMode() in the Service because doing so + // would add a permission requirement to the Service. Do it here, and it's + // the client app's responsibility to request that permission + public void setWakeMode(Context context, int mode) { + Log.d(SBMP_TAG, "setWakeMode(context, " + mode + ")"); + if ((this.mWakeLock != null) + && (this.mWakeLock.isHeld())) { + this.mWakeLock.release(); + } + if (mode != 0) { + if (this.mWakeLock == null) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + // Since mode can't be changed on the fly, we have to allocate a new one + this.mWakeLock = pm.newWakeLock(mode, this.getClass().getName()); + this.mWakeLock.setReferenceCounted(false); + } + + this.mWakeLock.acquire(); + } + } + + /** + * Changes the state of the WakeLock if it has been acquired. + * If no WakeLock has been acquired with setWakeMode, this method does nothing. + * */ + private void stayAwake(boolean awake) { + if (BuildConfig.DEBUG) Log.d(SBMP_TAG, "stayAwake(" + awake + ")"); + if (mWakeLock != null) { + if (awake && !mWakeLock.isHeld()) { + mWakeLock.acquire(); + } else if (!awake && mWakeLock.isHeld()) { + mWakeLock.release(); + } + } + } + + private IOnBufferingUpdateListenerCallback_0_8.Stub mOnBufferingUpdateCallback = null; + private void setOnBufferingUpdateCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnBufferingUpdateCallback == null) { + mOnBufferingUpdateCallback = new IOnBufferingUpdateListenerCallback_0_8.Stub() { + public void onBufferingUpdate(int percent) + throws RemoteException { + owningMediaPlayer.lock.lock(); + try { + if ((owningMediaPlayer.onBufferingUpdateListener != null) + && (owningMediaPlayer.mpi == ServiceBackedAudioPlayer.this)) { + owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnBufferingUpdateCallback( + ServiceBackedAudioPlayer.this.sessionId, + mOnBufferingUpdateCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnCompletionListenerCallback_0_8.Stub mOnCompletionCallback = null; + private void setOnCompletionCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnCompletionCallback == null) { + this.mOnCompletionCallback = new IOnCompletionListenerCallback_0_8.Stub() { + public void onCompletion() throws RemoteException { + owningMediaPlayer.lock.lock(); + Log.d(SBMP_TAG, "onCompletionListener being called"); + stayAwake(false); + try { + if (owningMediaPlayer.onCompletionListener != null) { + owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnCompletionCallback( + ServiceBackedAudioPlayer.this.sessionId, + this.mOnCompletionCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnErrorListenerCallback_0_8.Stub mOnErrorCallback = null; + private void setOnErrorCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnErrorCallback == null) { + this.mOnErrorCallback = new IOnErrorListenerCallback_0_8.Stub() { + public boolean onError(int what, int extra) throws RemoteException { + owningMediaPlayer.lock.lock(); + stayAwake(false); + try { + if (owningMediaPlayer.onErrorListener != null) { + return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra); + } + return false; + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnErrorCallback( + ServiceBackedAudioPlayer.this.sessionId, + this.mOnErrorCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnInfoListenerCallback_0_8.Stub mOnInfoCallback = null; + private void setOnInfoCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnInfoCallback == null) { + this.mOnInfoCallback = new IOnInfoListenerCallback_0_8.Stub() { + public boolean onInfo(int what, int extra) throws RemoteException { + owningMediaPlayer.lock.lock(); + try { + if ((owningMediaPlayer.onInfoListener != null) + && (owningMediaPlayer.mpi == ServiceBackedAudioPlayer.this)) { + return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + return false; + } + }; + } + iface.registerOnInfoCallback( + ServiceBackedAudioPlayer.this.sessionId, + this.mOnInfoCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnPitchAdjustmentAvailableChangedCallback = null; + private void setOnPitchAdjustmentAvailableChangedListener(IPlayMedia_0_8 iface) { + try { + if (this.mOnPitchAdjustmentAvailableChangedCallback == null) { + this.mOnPitchAdjustmentAvailableChangedCallback = new IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub() { + public void onPitchAdjustmentAvailableChanged( + boolean pitchAdjustmentAvailable) + throws RemoteException { + owningMediaPlayer.lock.lock(); + try { + if (owningMediaPlayer.onPitchAdjustmentAvailableChangedListener != null) { + owningMediaPlayer.onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged(owningMediaPlayer, pitchAdjustmentAvailable); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnPitchAdjustmentAvailableChangedCallback( + ServiceBackedAudioPlayer.this.sessionId, + this.mOnPitchAdjustmentAvailableChangedCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnPreparedListenerCallback_0_8.Stub mOnPreparedCallback = null; + private void setOnPreparedCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnPreparedCallback == null) { + this.mOnPreparedCallback = new IOnPreparedListenerCallback_0_8.Stub() { + public void onPrepared() throws RemoteException { + owningMediaPlayer.lock.lock(); + Log.d(SBMP_TAG, "setOnPreparedCallback.mOnPreparedCallback.onPrepared 1050"); + try { + Log.d(SBMP_TAG, "owningMediaPlayer.onPreparedListener is " + ((owningMediaPlayer.onPreparedListener == null) ? "null" : "non-null")); + Log.d(SBMP_TAG, "owningMediaPlayer.mpi is " + ((owningMediaPlayer.mpi == ServiceBackedAudioPlayer.this) ? "this" : "not this")); + ServiceBackedAudioPlayer.this.lockMuteOnPreparedCount.lock(); + try { + if (ServiceBackedAudioPlayer.this.muteOnPreparedCount > 0) { + ServiceBackedAudioPlayer.this.muteOnPreparedCount--; + } + else { + ServiceBackedAudioPlayer.this.muteOnPreparedCount = 0; + if (ServiceBackedAudioPlayer.this.owningMediaPlayer.onPreparedListener != null) { + owningMediaPlayer.onPreparedListener.onPrepared(owningMediaPlayer); + } + } + } + finally { + ServiceBackedAudioPlayer.this.lockMuteOnPreparedCount.unlock(); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnPreparedCallback( + ServiceBackedAudioPlayer.this.sessionId, + this.mOnPreparedCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnSeekCompleteListenerCallback_0_8.Stub mOnSeekCompleteCallback = null; + private void setOnSeekCompleteCallback(IPlayMedia_0_8 iface) { + try { + if (this.mOnSeekCompleteCallback == null) { + this.mOnSeekCompleteCallback = new IOnSeekCompleteListenerCallback_0_8.Stub() { + public void onSeekComplete() throws RemoteException { + Log.d(SBMP_TAG, "onSeekComplete() 941"); + owningMediaPlayer.lock.lock(); + try { + if (ServiceBackedAudioPlayer.this.muteOnSeekCount > 0) { + Log.d(SBMP_TAG, "The next " + ServiceBackedAudioPlayer.this.muteOnSeekCount + " seek events are muted (counting this one)"); + ServiceBackedAudioPlayer.this.muteOnSeekCount--; + } + else { + ServiceBackedAudioPlayer.this.muteOnSeekCount = 0; + Log.d(SBMP_TAG, "Attempting to invoke next seek event"); + if (ServiceBackedAudioPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) { + Log.d(SBMP_TAG, "Invoking onSeekComplete"); + owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer); + } + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnSeekCompleteCallback( + ServiceBackedAudioPlayer.this.sessionId, + this.mOnSeekCompleteCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + private IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnSpeedAdjustmentAvailableChangedCallback = null; + private void setOnSpeedAdjustmentAvailableChangedCallback(IPlayMedia_0_8 iface) { + try { + Log.d(SBMP_TAG, "Setting the service of on speed adjustment available changed"); + if (this.mOnSpeedAdjustmentAvailableChangedCallback == null) { + this.mOnSpeedAdjustmentAvailableChangedCallback = new IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub() { + public void onSpeedAdjustmentAvailableChanged( + boolean speedAdjustmentAvailable) + throws RemoteException { + owningMediaPlayer.lock.lock(); + try { + if (owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener != null) { + owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged(owningMediaPlayer, speedAdjustmentAvailable); + } + } + finally { + owningMediaPlayer.lock.unlock(); + } + } + }; + } + iface.registerOnSpeedAdjustmentAvailableChangedCallback( + ServiceBackedAudioPlayer.this.sessionId, + this.mOnSpeedAdjustmentAvailableChangedCallback); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + + /** + * Functions identically to android.media.MediaPlayer.start() + * Starts a track playing + */ + @Override + public void start() { + Log.d(SBMP_TAG, "start()"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.start(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + stayAwake(true); + } + + /** + * Functions identically to android.media.MediaPlayer.stop() + * Stops a track playing and resets its position to the start. + */ + @Override + public void stop() { + Log.d(SBMP_TAG, "stop()"); + if (pmInterface == null) { + if (!ConnectPlayMediaService()) { + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + } + try { + pmInterface.stop(ServiceBackedAudioPlayer.this.sessionId); + } catch (RemoteException e) { + e.printStackTrace(); + ServiceBackedAudioPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); + } + stayAwake(false); + } +} diff --git a/library/src/main/java/org/antennapod/audio/SonicAudioPlayer.java b/library/src/main/java/org/antennapod/audio/SonicAudioPlayer.java new file mode 100644 index 0000000..44b2ae6 --- /dev/null +++ b/library/src/main/java/org/antennapod/audio/SonicAudioPlayer.java @@ -0,0 +1,646 @@ +//Copyright 2012 James Falcon +// +//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. + +/* Minor modifications by Martin Fietz <Martin.Fietz@gmail.com> + * The original source can be found here: + * https://github.com/TheRealFalcon/Prestissimo/blob/master/src/com/falconware/prestissimo/Track.java + */ + +package org.antennapod.audio; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioTrack; +import android.media.MediaCodec; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.net.Uri; +import android.os.Build; +import android.os.PowerManager; +import android.util.Log; + +import org.vinuxproject.sonic.Sonic; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.locks.ReentrantLock; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +public class SonicAudioPlayer extends AbstractAudioPlayer { + + private static final String TAG = SonicAudioPlayer.class.getSimpleName(); + + protected final MediaPlayer mMediaPlayer; + private AudioTrack mTrack; + private Sonic mSonic; + private MediaExtractor mExtractor; + private MediaCodec mCodec; + private Thread mDecoderThread; + private String mPath; + private Uri mUri; + private final ReentrantLock mLock; + private final Object mDecoderLock; + private boolean mContinue; + private boolean mIsDecoding; + private long mDuration; + private float mCurrentSpeed; + private float mCurrentPitch; + private int mCurrentState; + private final Context mContext; + private PowerManager.WakeLock mWakeLock = null; + + private final static int TRACK_NUM = 0; + private final static String TAG_TRACK = "PrestissimoTrack"; + + private final static int STATE_IDLE = 0; + private final static int STATE_INITIALIZED = 1; + private final static int STATE_PREPARING = 2; + private final static int STATE_PREPARED = 3; + private final static int STATE_STARTED = 4; + private final static int STATE_PAUSED = 5; + private final static int STATE_STOPPED = 6; + private final static int STATE_PLAYBACK_COMPLETED = 7; + private final static int STATE_END = 8; + private final static int STATE_ERROR = 9; + + public SonicAudioPlayer(MediaPlayer owningMediaPlayer, Context context) { + super(owningMediaPlayer, context); + mMediaPlayer = owningMediaPlayer; + mCurrentState = STATE_IDLE; + mCurrentSpeed = 1.0f; + mCurrentPitch = 1.0f; + mContinue = false; + mIsDecoding = false; + mContext = context; + mPath = null; + mUri = null; + mLock = new ReentrantLock(); + mDecoderLock = new Object(); + } + + @Override + public boolean canSetPitch() { + return true; + } + + @Override + public boolean canSetSpeed() { + return true; + } + + @Override + public float getCurrentPitchStepsAdjustment() { + return mCurrentPitch; + } + + public int getCurrentPosition() { + switch (mCurrentState) { + case STATE_ERROR: + error(); + break; + default: + return (int) (mExtractor.getSampleTime() / 1000); + } + return 0; + } + + @Override + public float getCurrentSpeedMultiplier() { + return mCurrentSpeed; + } + + public int getDuration() { + switch (mCurrentState) { + case STATE_INITIALIZED: + case STATE_IDLE: + case STATE_ERROR: + error(); + break; + default: + return (int) (mDuration / 1000); + } + return 0; + } + + @Override + public float getMaxSpeedMultiplier() { + return 4.0f; + } + + @Override + public float getMinSpeedMultiplier() { + return 0.5f; + } + + @Override + public boolean isLooping() { + return false; + } + + public boolean isPlaying() { + switch (mCurrentState) { + case STATE_ERROR: + error(); + break; + default: + return mCurrentState == STATE_STARTED; + } + return false; + } + + public void pause() { + Log.d(TAG, "pause(), current state: " + mCurrentState); + switch (mCurrentState) { + case STATE_STARTED: + case STATE_PAUSED: + mTrack.pause(); + mCurrentState = STATE_PAUSED; + Log.d(TAG_TRACK, "State changed to STATE_PAUSED"); + break; + default: + error(); + } + } + + public void prepare() { + Log.d(TAG, "prepare(), current state: " + mCurrentState); + switch (mCurrentState) { + case STATE_INITIALIZED: + case STATE_STOPPED: + try { + initStream(); + } catch (IOException e) { + Log.e(TAG_TRACK, "Failed setting data source!", e); + error(); + return; + } + mCurrentState = STATE_PREPARED; + Log.d(TAG_TRACK, "State changed to STATE_PREPARED"); + if(owningMediaPlayer.onPreparedListener != null) { + owningMediaPlayer.onPreparedListener.onPrepared(owningMediaPlayer); + } + break; + default: + error(); + } + } + + public void prepareAsync() { + switch (mCurrentState) { + case STATE_INITIALIZED: + case STATE_STOPPED: + mCurrentState = STATE_PREPARING; + Log.d(TAG_TRACK, "State changed to STATE_PREPARING"); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + initStream(); + } catch (IOException e) { + Log.e(TAG_TRACK, "Failed setting data source!", e); + error(); + return; + } + if (mCurrentState != STATE_ERROR) { + mCurrentState = STATE_PREPARED; + Log.d(TAG_TRACK, "State changed to STATE_PREPARED"); + } + if(owningMediaPlayer.onPreparedListener != null) { + owningMediaPlayer.onPreparedListener.onPrepared(owningMediaPlayer); + } + } + }); + t.setDaemon(true); + t.start(); + break; + default: + error(); + } + } + + public void stop() { + switch (mCurrentState) { + case STATE_PREPARED: + case STATE_STARTED: + case STATE_STOPPED: + case STATE_PAUSED: + case STATE_PLAYBACK_COMPLETED: + mCurrentState = STATE_STOPPED; + Log.d(TAG_TRACK, "State changed to STATE_STOPPED"); + mContinue = false; + mTrack.pause(); + mTrack.flush(); + break; + default: + error(); + } + } + + public void start() { + switch (mCurrentState) { + case STATE_PREPARED: + case STATE_PLAYBACK_COMPLETED: + mCurrentState = STATE_STARTED; + Log.d(TAG, "State changed to STATE_STARTED"); + mContinue = true; + mTrack.play(); + decode(); + case STATE_STARTED: + break; + case STATE_PAUSED: + mCurrentState = STATE_STARTED; + Log.d(TAG, "State changed to STATE_STARTED"); + synchronized (mDecoderLock) { + mDecoderLock.notify(); + } + mTrack.play(); + break; + default: + mCurrentState = STATE_ERROR; + Log.d(TAG, "State changed to STATE_ERROR in start"); + if (mTrack != null) { + error(); + } else { + Log.d("start", + "Attempting to start while in idle after construction. Not allowed by no callbacks called"); + } + } + } + + public void release() { + reset(); + mCurrentState = STATE_END; + } + + public void reset() { + mLock.lock(); + mContinue = false; + try { + if (mDecoderThread != null + && mCurrentState != STATE_PLAYBACK_COMPLETED) { + while (mIsDecoding) { + synchronized (mDecoderLock) { + mDecoderLock.notify(); + mDecoderLock.wait(); + } + } + } + } catch (InterruptedException e) { + Log.e(TAG_TRACK, + "Interrupted in reset while waiting for decoder thread to stop.", + e); + } + if (mCodec != null) { + mCodec.release(); + mCodec = null; + } + if (mExtractor != null) { + mExtractor.release(); + mExtractor = null; + } + if (mTrack != null) { + mTrack.release(); + mTrack = null; + } + mCurrentState = STATE_IDLE; + Log.d(TAG_TRACK, "State changed to STATE_IDLE"); + mLock.unlock(); + } + + public void seekTo(final int msec) { + switch (mCurrentState) { + case STATE_PREPARED: + case STATE_STARTED: + case STATE_PAUSED: + case STATE_PLAYBACK_COMPLETED: + Thread t = new Thread(new Runnable() { + @Override + public void run() { + mLock.lock(); + if (mTrack == null) { + return; + } + mTrack.flush(); + mExtractor.seekTo(((long) msec * 1000), MediaExtractor.SEEK_TO_CLOSEST_SYNC); + Log.d(TAG, "seek completed"); + if(owningMediaPlayer.onSeekCompleteListener != null) { + owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer); + } + mLock.unlock(); + } + }); + t.setDaemon(true); + t.start(); + break; + default: + error(); + } + } + + @Override + public void setAudioStreamType(int streamtype) { + return; + } + + @Override + public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { + return; + } + + @Override + public void setLooping(boolean loop) { + return; + } + + @Override + public void setPitchStepsAdjustment(float pitchSteps) { + mCurrentPitch += pitchSteps; + } + + @Override + public void setPlaybackPitch(float f) { + mCurrentSpeed = f; + } + + @Override + public void setPlaybackSpeed(float f) { + mCurrentSpeed = f; + } + + @Override + public void setDataSource(String path) { + switch (mCurrentState) { + case STATE_IDLE: + mPath = path; + mCurrentState = STATE_INITIALIZED; + Log.d(TAG_TRACK, "Moving state to STATE_INITIALIZED"); + break; + default: + error(); + } + } + + @Override + public void setDataSource(Context context, Uri uri) { + switch (mCurrentState) { + case STATE_IDLE: + mUri = uri; + mCurrentState = STATE_INITIALIZED; + Log.d(TAG_TRACK, "Moving state to STATE_INITIALIZED"); + break; + default: + error(); + } + } + + @SuppressWarnings("deprecation") + @Override + public void setVolume(float leftVolume, float rightVolume) { + // Pass call directly to AudioTrack if available. + if (null != mTrack) { + mTrack.setStereoVolume(leftVolume, rightVolume); + } + } + + @Override + public void setWakeMode(Context context, int mode) { + boolean wasHeld = false; + if (mWakeLock != null) { + if (mWakeLock.isHeld()) { + wasHeld = true; + mWakeLock.release(); + } + mWakeLock = null; + } + + if(mode > 0) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(mode, this.getClass().getName()); + mWakeLock.setReferenceCounted(false); + if (wasHeld) { + mWakeLock.acquire(); + } + } + } + + public void error() { + error(0); + } + + public void error(int extra) { + Log.e(TAG_TRACK, "Moved to error state!"); + mCurrentState = STATE_ERROR; + if(owningMediaPlayer.onErrorListener != null) { + boolean handled = owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, 0, extra); + if (!handled && owningMediaPlayer.onCompletionListener != null) { + owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); + } + } + } + + private int findFormatFromChannels(int numChannels) { + switch (numChannels) { + case 1: + return AudioFormat.CHANNEL_OUT_MONO; + case 2: + return AudioFormat.CHANNEL_OUT_STEREO; + default: + return -1; // Error + } + } + + public void initStream() throws IOException { + mLock.lock(); + mExtractor = new MediaExtractor(); + if (mPath != null) { + mExtractor.setDataSource(mPath); + } else if (mUri != null) { + mExtractor.setDataSource(mContext, mUri, null); + } else { + throw new IOException(); + } + + final MediaFormat oFormat = mExtractor.getTrackFormat(TRACK_NUM); + int sampleRate = oFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); + int channelCount = oFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); + final String mime = oFormat.getString(MediaFormat.KEY_MIME); + mDuration = oFormat.getLong(MediaFormat.KEY_DURATION); + + Log.v(TAG_TRACK, "Sample rate: " + sampleRate); + Log.v(TAG_TRACK, "Mime type: " + mime); + + initDevice(sampleRate, channelCount); + mExtractor.selectTrack(TRACK_NUM); + mCodec = MediaCodec.createDecoderByType(mime); + mCodec.configure(oFormat, null, null, 0); + mLock.unlock(); + } + + private void initDevice(int sampleRate, int numChannels) { + mLock.lock(); + final int format = findFormatFromChannels(numChannels); + final int minSize = AudioTrack.getMinBufferSize(sampleRate, format, + AudioFormat.ENCODING_PCM_16BIT); + mTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, format, + AudioFormat.ENCODING_PCM_16BIT, minSize * 4, + AudioTrack.MODE_STREAM); + mSonic = new Sonic(sampleRate, numChannels); + mLock.unlock(); + } + + @SuppressWarnings("deprecation") + public void decode() { + mDecoderThread = new Thread(new Runnable() { + @Override + public void run() { + + mIsDecoding = true; + mCodec.start(); + + ByteBuffer[] inputBuffers = mCodec.getInputBuffers(); + ByteBuffer[] outputBuffers = mCodec.getOutputBuffers(); + + boolean sawInputEOS = false; + boolean sawOutputEOS = false; + + while (!sawInputEOS && !sawOutputEOS && mContinue) { + if (mCurrentState == STATE_PAUSED) { + System.out.println("Decoder changed to PAUSED"); + try { + synchronized (mDecoderLock) { + mDecoderLock.wait(); + System.out.println("Done with wait"); + } + } catch (InterruptedException e) { + // Purposely not doing anything here + } + continue; + } + + if (null != mSonic) { + mSonic.setSpeed(mCurrentSpeed); + mSonic.setPitch(mCurrentPitch); + } + + int inputBufIndex = mCodec.dequeueInputBuffer(200); + if (inputBufIndex >= 0) { + ByteBuffer dstBuf = inputBuffers[inputBufIndex]; + int sampleSize = mExtractor.readSampleData(dstBuf, 0); + long presentationTimeUs = 0; + if (sampleSize < 0) { + sawInputEOS = true; + sampleSize = 0; + } else { + presentationTimeUs = mExtractor.getSampleTime(); + } + mCodec.queueInputBuffer( + inputBufIndex, + 0, + sampleSize, + presentationTimeUs, + sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0); + if (!sawInputEOS) { + mExtractor.advance(); + } + } + + final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); + byte[] modifiedSamples = new byte[info.size]; + + int res; + do { + res = mCodec.dequeueOutputBuffer(info, 200); + if (res >= 0) { + int outputBufIndex = res; + final byte[] chunk = new byte[info.size]; + outputBuffers[res].get(chunk); + outputBuffers[res].clear(); + + if (chunk.length > 0) { + mSonic.writeBytesToStream(chunk, chunk.length); + } else { + mSonic.flushStream(); + } + int available = mSonic.samplesAvailable(); + if (available > 0) { + if (modifiedSamples.length < available) { + modifiedSamples = new byte[available]; + } + mSonic.readBytesFromStream(modifiedSamples, available); + mTrack.write(modifiedSamples, 0, available); + } + + mCodec.releaseOutputBuffer(outputBufIndex, false); + + if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + sawOutputEOS = true; + } + } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + outputBuffers = mCodec.getOutputBuffers(); + Log.d("PCM", "Output buffers changed"); + } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + mTrack.stop(); + mLock.lock(); + mTrack.release(); + final MediaFormat oformat = mCodec.getOutputFormat(); + Log.d("PCM", "Output format has changed to" + oformat); + initDevice( + oformat.getInteger(MediaFormat.KEY_SAMPLE_RATE), + oformat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)); + outputBuffers = mCodec.getOutputBuffers(); + mTrack.play(); + mLock.unlock(); + } + } while (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED + || res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + } + Log.d(TAG_TRACK, "Decoding loop exited. Stopping codec and track"); + Log.d(TAG_TRACK, "Duration: " + (int) (mDuration / 1000)); + Log.d(TAG_TRACK, "Current position: " + (int) (mExtractor.getSampleTime() / 1000)); + mCodec.stop(); + mTrack.stop(); + Log.d(TAG_TRACK, "Stopped codec and track"); + Log.d(TAG_TRACK, "Current position: " + (int) (mExtractor.getSampleTime() / 1000)); + mIsDecoding = false; + if (mContinue && (sawInputEOS || sawOutputEOS)) { + mCurrentState = STATE_PLAYBACK_COMPLETED; + if (owningMediaPlayer.onCompletionListener != null) { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); + + } + }); + t.setDaemon(true); + t.start(); + } + } else { + Log.d(TAG_TRACK, "Loop ended before saw input eos or output eos"); + Log.d(TAG_TRACK, "sawInputEOS: " + sawInputEOS); + Log.d(TAG_TRACK, "sawOutputEOS: " + sawOutputEOS); + } + synchronized (mDecoderLock) { + mDecoderLock.notifyAll(); + } + } + }); + mDecoderThread.setDaemon(true); + mDecoderThread.start(); + } + +} diff --git a/library/src/main/java/org/vinuxproject/sonic/Sonic.java b/library/src/main/java/org/vinuxproject/sonic/Sonic.java new file mode 100644 index 0000000..975085e --- /dev/null +++ b/library/src/main/java/org/vinuxproject/sonic/Sonic.java @@ -0,0 +1,896 @@ +/* Sonic library + Copyright 2010, 2011 + Bill Cox + This file is part of the Sonic Library. + + This file is licensed under the Apache 2.0 license. +*/ + +/* Minor modifications by Martin Fietz <Martin.Fietz@gmail.com> + * The original source can be found here: + * https://github.com/waywardgeek/sonic/blob/master/Sonic.java + */ + + + +package org.vinuxproject.sonic; + +public class Sonic { + + private static final int SONIC_MIN_PITCH = 65; + private static final int SONIC_MAX_PITCH = 400; + /* This is used to down-sample some inputs to improve speed */ + private static final int SONIC_AMDF_FREQ = 4000; + + private short inputBuffer[]; + private short outputBuffer[]; + private short pitchBuffer[]; + private short downSampleBuffer[]; + private float speed; + private float volume; + private float pitch; + private float rate; + private int oldRatePosition; + private int newRatePosition; + private boolean useChordPitch; + private int quality; + private int numChannels; + private int inputBufferSize; + private int pitchBufferSize; + private int outputBufferSize; + private int numInputSamples; + private int numOutputSamples; + private int numPitchSamples; + private int minPeriod; + private int maxPeriod; + private int maxRequired; + private int remainingInputToCopy; + private int sampleRate; + private int prevPeriod; + private int prevMinDiff; + + // Resize the array. + private short[] resize(short[] oldArray, + int newLength) { + newLength *= numChannels; + short[] newArray = new short[newLength]; + int length = oldArray.length <= newLength ? oldArray.length : newLength; + + System.arraycopy(oldArray, 0, newArray, 0, length); + + return newArray; + } + + // Move samples from one array to another. May move samples down within an array, but not up. + private void move(short dest[], int destPos, + short source[], + int sourcePos, + int numSamples) { + for (int xSample = 0; xSample < numSamples * numChannels; xSample++) { + dest[destPos * numChannels + xSample] = source[sourcePos * numChannels + xSample]; + } + } + + // Scale the samples by the factor. + private void scaleSamples(short samples[], + int position, + int numSamples, + float volume) { + int fixedPointVolume = (int) (volume * 4096.0f); + int start = position * numChannels; + int stop = start + numSamples * numChannels; + + for (int xSample = start; xSample < stop; xSample++) { + int value = (samples[xSample] * fixedPointVolume) >> 12; + if (value > 32767) { + value = 32767; + } else if (value < -32767) { + value = -32767; + } + samples[xSample] = (short) value; + } + } + + // Get the speed of the stream. + public float getSpeed() { + return speed; + } + + // Set the speed of the stream. + public void setSpeed(float speed) { + this.speed = speed; + } + + // Get the pitch of the stream. + public float getPitch() { + return pitch; + } + + // Set the pitch of the stream. + public void setPitch(float pitch) { + this.pitch = pitch; + } + + // Get the rate of the stream. + public float getRate() { + return rate; + } + + // Set the playback rate of the stream. This scales pitch and speed at the same time. + public void setRate(float rate) { + this.rate = rate; + this.oldRatePosition = 0; + this.newRatePosition = 0; + } + + // Get the vocal chord pitch setting. + public boolean getChordPitch() { + return useChordPitch; + } + + // Set the vocal chord mode for pitch computation. Default is off. + public void setChordPitch( + boolean useChordPitch) { + this.useChordPitch = useChordPitch; + } + + // Get the quality setting. + public int getQuality() { + return quality; + } + + // Set the "quality". Default 0 is virtually as good as 1, but very much faster. + public void setQuality(int quality) { + this.quality = quality; + } + + // Get the scaling factor of the stream. + public float getVolume() { + return volume; + } + + // Set the scaling factor of the stream. + public void setVolume(float volume) { + this.volume = volume; + } + + // Allocate stream buffers. + private void allocateStreamBuffers(int sampleRate, + int numChannels) { + minPeriod = sampleRate / SONIC_MAX_PITCH; + maxPeriod = sampleRate / SONIC_MIN_PITCH; + maxRequired = 2 * maxPeriod; + inputBufferSize = maxRequired; + inputBuffer = new short[maxRequired * numChannels]; + outputBufferSize = maxRequired; + outputBuffer = new short[maxRequired * numChannels]; + pitchBufferSize = maxRequired; + pitchBuffer = new short[maxRequired * numChannels]; + downSampleBuffer = new short[maxRequired]; + this.sampleRate = sampleRate; + this.numChannels = numChannels; + oldRatePosition = 0; + newRatePosition = 0; + prevPeriod = 0; + } + + // Create a sonic stream. + public Sonic(int sampleRate, + int numChannels) { + allocateStreamBuffers(sampleRate, numChannels); + speed = 1.0f; + pitch = 1.0f; + volume = 1.0f; + rate = 1.0f; + oldRatePosition = 0; + newRatePosition = 0; + useChordPitch = false; + quality = 0; + } + + // Get the sample rate of the stream. + public int getSampleRate() { + return sampleRate; + } + + // Set the sample rate of the stream. This will cause samples buffered in the stream to be lost. + public void setSampleRate(int sampleRate) { + allocateStreamBuffers(sampleRate, numChannels); + } + + // Get the number of channels. + public int getNumChannels() { + return numChannels; + } + + // Set the num channels of the stream. This will cause samples buffered in the stream to be lost. + public void setNumChannels(int numChannels) { + allocateStreamBuffers(sampleRate, numChannels); + } + + // Enlarge the output buffer if needed. + private void enlargeOutputBufferIfNeeded(int numSamples) { + if (numOutputSamples + numSamples > outputBufferSize) { + outputBufferSize += (outputBufferSize >> 1) + numSamples; + outputBuffer = resize(outputBuffer, outputBufferSize); + } + } + + // Enlarge the input buffer if needed. + private void enlargeInputBufferIfNeeded(int numSamples) { + if (numInputSamples + numSamples > inputBufferSize) { + inputBufferSize += (inputBufferSize >> 1) + numSamples; + inputBuffer = resize(inputBuffer, inputBufferSize); + } + } + + // Add the input samples to the input buffer. + private void addFloatSamplesToInputBuffer(float samples[], + int numSamples) { + if (numSamples == 0) { + return; + } + enlargeInputBufferIfNeeded(numSamples); + int xBuffer = numInputSamples * numChannels; + for (int xSample = 0; xSample < numSamples * numChannels; xSample++) { + inputBuffer[xBuffer++] = (short) (samples[xSample] * 32767.0f); + } + numInputSamples += numSamples; + } + + // Add the input samples to the input buffer. + private void addShortSamplesToInputBuffer(short samples[], + int numSamples) { + if (numSamples == 0) { + return; + } + enlargeInputBufferIfNeeded(numSamples); + move(inputBuffer, numInputSamples, samples, 0, numSamples); + numInputSamples += numSamples; + } + + // Add the input samples to the input buffer. + private void addUnsignedByteSamplesToInputBuffer(byte samples[], + int numSamples) { + short sample; + + enlargeInputBufferIfNeeded(numSamples); + int xBuffer = numInputSamples * numChannels; + for (int xSample = 0; xSample < numSamples * numChannels; xSample++) { + sample = (short) ((samples[xSample] & 0xff) - 128); // Convert from unsigned to signed + inputBuffer[xBuffer++] = (short) (sample << 8); + } + numInputSamples += numSamples; + } + + // Add the input samples to the input buffer. They must be 16-bit little-endian encoded in a byte array. + private void addBytesToInputBuffer(byte inBuffer[], + int numBytes) { + int numSamples = numBytes / (2 * numChannels); + short sample; + + enlargeInputBufferIfNeeded(numSamples); + int xBuffer = numInputSamples * numChannels; + for (int xByte = 0; xByte + 1 < numBytes; xByte += 2) { + sample = (short) ((inBuffer[xByte] & 0xff) | (inBuffer[xByte + 1] << 8)); + inputBuffer[xBuffer++] = sample; + } + numInputSamples += numSamples; + } + + // Remove input samples that we have already processed. + private void removeInputSamples(int position) { + int remainingSamples = numInputSamples - position; + + move(inputBuffer, 0, inputBuffer, position, remainingSamples); + numInputSamples = remainingSamples; + } + + // Just copy from the array to the output buffer + private void copyToOutput(short samples[], + int position, + int numSamples) { + enlargeOutputBufferIfNeeded(numSamples); + move(outputBuffer, numOutputSamples, samples, position, numSamples); + numOutputSamples += numSamples; + } + + // Just copy from the input buffer to the output buffer. Return num samples copied. + private int copyInputToOutput(int position) { + int numSamples = remainingInputToCopy; + + if (numSamples > maxRequired) { + numSamples = maxRequired; + } + copyToOutput(inputBuffer, position, numSamples); + remainingInputToCopy -= numSamples; + return numSamples; + } + + // Read data out of the stream. Sometimes no data will be available, and zero + // is returned, which is not an error condition. + public int readFloatFromStream(float samples[], + int maxSamples) { + int numSamples = numOutputSamples; + int remainingSamples = 0; + + if (numSamples == 0) { + return 0; + } + if (numSamples > maxSamples) { + remainingSamples = numSamples - maxSamples; + numSamples = maxSamples; + } + for (int xSample = 0; xSample < numSamples * numChannels; xSample++) { + samples[xSample++] = (outputBuffer[xSample]) / 32767.0f; + } + move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples); + numOutputSamples = remainingSamples; + return numSamples; + } + + // Read short data out of the stream. Sometimes no data will be available, and zero + // is returned, which is not an error condition. + public int readShortFromStream(short samples[], + int maxSamples) { + int numSamples = numOutputSamples; + int remainingSamples = 0; + + if (numSamples == 0) { + return 0; + } + if (numSamples > maxSamples) { + remainingSamples = numSamples - maxSamples; + numSamples = maxSamples; + } + move(samples, 0, outputBuffer, 0, numSamples); + move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples); + numOutputSamples = remainingSamples; + return numSamples; + } + + // Read unsigned byte data out of the stream. Sometimes no data will be available, and zero + // is returned, which is not an error condition. + public int readUnsignedByteFromStream(byte samples[], + int maxSamples) { + int numSamples = numOutputSamples; + int remainingSamples = 0; + + if (numSamples == 0) { + return 0; + } + if (numSamples > maxSamples) { + remainingSamples = numSamples - maxSamples; + numSamples = maxSamples; + } + for (int xSample = 0; xSample < numSamples * numChannels; xSample++) { + samples[xSample] = (byte) ((outputBuffer[xSample] >> 8) + 128); + } + move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples); + numOutputSamples = remainingSamples; + return numSamples; + } + + // Read unsigned byte data out of the stream. Sometimes no data will be available, and zero + // is returned, which is not an error condition. + public int readBytesFromStream(byte outBuffer[], + int maxBytes) { + int maxSamples = maxBytes / (2 * numChannels); + int numSamples = numOutputSamples; + int remainingSamples = 0; + + if (numSamples == 0 || maxSamples == 0) { + return 0; + } + if (numSamples > maxSamples) { + remainingSamples = numSamples - maxSamples; + numSamples = maxSamples; + } + for (int xSample = 0; xSample < numSamples * numChannels; xSample++) { + short sample = outputBuffer[xSample]; + outBuffer[xSample << 1] = (byte) (sample & 0xff); + outBuffer[(xSample << 1) + 1] = (byte) (sample >> 8); + } + move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples); + numOutputSamples = remainingSamples; + return 2 * numSamples * numChannels; + } + + // Force the sonic stream to generate output using whatever data it currently + // has. No extra delay will be added to the output, but flushing in the middle of + // words could introduce distortion. + public void flushStream() { + int remainingSamples = numInputSamples; + float s = speed / pitch; + float r = rate * pitch; + int expectedOutputSamples = numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / r + 0.5f); + + // Add enough silence to flush both input and pitch buffers. + enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired); + for (int xSample = 0; xSample < 2 * maxRequired * numChannels; xSample++) { + inputBuffer[remainingSamples * numChannels + xSample] = 0; + } + numInputSamples += 2 * maxRequired; + writeShortToStream(null, 0); + // Throw away any extra samples we generated due to the silence we added. + if (numOutputSamples > expectedOutputSamples) { + numOutputSamples = expectedOutputSamples; + } + // Empty input and pitch buffers. + numInputSamples = 0; + remainingInputToCopy = 0; + numPitchSamples = 0; + } + + // Return the number of samples in the output buffer + public int samplesAvailable() { + return numOutputSamples; + } + + // If skip is greater than one, average skip samples together and write them to + // the down-sample buffer. If numChannels is greater than one, mix the channels + // together as we down sample. + private void downSampleInput(short samples[], + int position, + int skip) { + int numSamples = maxRequired / skip; + int samplesPerValue = numChannels * skip; + int value; + + position *= numChannels; + for (int i = 0; i < numSamples; i++) { + value = 0; + for (int j = 0; j < samplesPerValue; j++) { + value += samples[position + i * samplesPerValue + j]; + } + value /= samplesPerValue; + downSampleBuffer[i] = (short) value; + } + } + + // Find the best frequency match in the range, and given a sample skip multiple. + // For now, just find the pitch of the first channel. Note that retMinDiff and + // retMaxDiff are Int objects, which the caller will need to create with new. + private int findPitchPeriodInRange(short samples[], + int position, + int minPeriod, + int maxPeriod, + Integer retMinDiff, + Integer retMaxDiff) { + int bestPeriod = 0, worstPeriod = 255; + int minDiff = 1, maxDiff = 0; + + position *= numChannels; + for (int period = minPeriod; period <= maxPeriod; period++) { + int diff = 0; + for (int i = 0; i < period; i++) { + short sVal = samples[position + i]; + short pVal = samples[position + period + i]; + diff += sVal >= pVal ? sVal - pVal : pVal - sVal; + } + /* Note that the highest number of samples we add into diff will be less + than 256, since we skip samples. Thus, diff is a 24 bit number, and + we can safely multiply by numSamples without overflow */ + if (diff * bestPeriod < minDiff * period) { + minDiff = diff; + bestPeriod = period; + } + if (diff * worstPeriod > maxDiff * period) { + maxDiff = diff; + worstPeriod = period; + } + } + retMinDiff = minDiff / bestPeriod; + retMaxDiff = maxDiff / worstPeriod; + return bestPeriod; + } + + // At abrupt ends of voiced words, we can have pitch periods that are better + // approximated by the previous pitch period estimate. Try to detect this case. + private boolean prevPeriodBetter(int period, + int minDiff, + int maxDiff, + boolean preferNewPeriod) { + if (minDiff == 0 || prevPeriod == 0) { + return false; + } + if (preferNewPeriod) { + if (maxDiff > minDiff * 3) { + // Got a reasonable match this period + return false; + } + if (minDiff * 2 <= prevMinDiff * 3) { + // Mismatch is not that much greater this period + return false; + } + } else { + if (minDiff <= prevMinDiff) { + return false; + } + } + return true; + } + + // Find the pitch period. This is a critical step, and we may have to try + // multiple ways to get a good answer. This version uses AMDF. To improve + // speed, we down sample by an integer factor get in the 11KHz range, and then + // do it again with a narrower frequency range without down sampling + private int findPitchPeriod(short samples[], + int position, + boolean preferNewPeriod) { + Integer minDiff = new Integer(0); + Integer maxDiff = new Integer(0); + int period, retPeriod; + int skip = 1; + + if (sampleRate > SONIC_AMDF_FREQ && quality == 0) { + skip = sampleRate / SONIC_AMDF_FREQ; + } + if (numChannels == 1 && skip == 1) { + period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod, minDiff, maxDiff); + } else { + downSampleInput(samples, position, skip); + period = findPitchPeriodInRange(downSampleBuffer, 0, minPeriod / skip, + maxPeriod / skip, minDiff, maxDiff); + if (skip != 1) { + period *= skip; + int minP = period - (skip << 2); + int maxP = period + (skip << 2); + if (minP < minPeriod) { + minP = minPeriod; + } + if (maxP > maxPeriod) { + maxP = maxPeriod; + } + if (numChannels == 1) { + period = findPitchPeriodInRange(samples, position, minP, maxP, minDiff, maxDiff); + } else { + downSampleInput(samples, position, 1); + period = findPitchPeriodInRange(downSampleBuffer, 0, minP, maxP, minDiff, maxDiff); + } + } + } + if (prevPeriodBetter(period, minDiff, maxDiff, preferNewPeriod)) { + retPeriod = prevPeriod; + } else { + retPeriod = period; + } + prevMinDiff = minDiff; + prevPeriod = period; + return retPeriod; + } + + // Overlap two sound segments, ramp the volume of one down, while ramping the + // other one from zero up, and add them, storing the result at the output. + private void overlapAdd(int numSamples, + int numChannels, + short out[], + int outPos, + short rampDown[], + int rampDownPos, + short rampUp[], + int rampUpPos) { + for (int i = 0; i < numChannels; i++) { + int o = outPos * numChannels + i; + int u = rampUpPos * numChannels + i; + int d = rampDownPos * numChannels + i; + for (int t = 0; t < numSamples; t++) { + out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * t) / numSamples); + o += numChannels; + d += numChannels; + u += numChannels; + } + } + } + + // Overlap two sound segments, ramp the volume of one down, while ramping the + // other one from zero up, and add them, storing the result at the output. + private void overlapAddWithSeparation(int numSamples, + int numChannels, + int separation, + short out[], + int outPos, + short rampDown[], + int rampDownPos, + short rampUp[], + int rampUpPos) { + for (int i = 0; i < numChannels; i++) { + int o = outPos * numChannels + i; + int u = rampUpPos * numChannels + i; + int d = rampDownPos * numChannels + i; + for (int t = 0; t < numSamples + separation; t++) { + if (t < separation) { + out[o] = (short) (rampDown[d] * (numSamples - t) / numSamples); + d += numChannels; + } else if (t < numSamples) { + out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * (t - separation)) / numSamples); + d += numChannels; + u += numChannels; + } else { + out[o] = (short) (rampUp[u] * (t - separation) / numSamples); + u += numChannels; + } + o += numChannels; + } + } + } + + // Just move the new samples in the output buffer to the pitch buffer + private void moveNewSamplesToPitchBuffer(int originalNumOutputSamples) { + int numSamples = numOutputSamples - originalNumOutputSamples; + + if (numPitchSamples + numSamples > pitchBufferSize) { + pitchBufferSize += (pitchBufferSize >> 1) + numSamples; + pitchBuffer = resize(pitchBuffer, pitchBufferSize); + } + move(pitchBuffer, numPitchSamples, outputBuffer, originalNumOutputSamples, numSamples); + numOutputSamples = originalNumOutputSamples; + numPitchSamples += numSamples; + } + + // Remove processed samples from the pitch buffer. + private void removePitchSamples(int numSamples) { + if (numSamples == 0) { + return; + } + move(pitchBuffer, 0, pitchBuffer, numSamples, numPitchSamples - numSamples); + numPitchSamples -= numSamples; + } + + // Change the pitch. The latency this introduces could be reduced by looking at + // past samples to determine pitch, rather than future. + private void adjustPitch(int originalNumOutputSamples) { + int period, newPeriod, separation; + int position = 0; + + if (numOutputSamples == originalNumOutputSamples) { + return; + } + moveNewSamplesToPitchBuffer(originalNumOutputSamples); + while (numPitchSamples - position >= maxRequired) { + period = findPitchPeriod(pitchBuffer, position, false); + newPeriod = (int) (period / pitch); + enlargeOutputBufferIfNeeded(newPeriod); + if (pitch >= 1.0f) { + overlapAdd(newPeriod, numChannels, outputBuffer, numOutputSamples, pitchBuffer, + position, pitchBuffer, position + period - newPeriod); + } else { + separation = newPeriod - period; + overlapAddWithSeparation(period, numChannels, separation, outputBuffer, numOutputSamples, + pitchBuffer, position, pitchBuffer, position); + } + numOutputSamples += newPeriod; + position += period; + } + removePitchSamples(position); + } + + // Interpolate the new output sample. + private short interpolate(short in[], + int inPos, + int oldSampleRate, + int newSampleRate) { + short left = in[inPos * numChannels]; + short right = in[inPos * numChannels + numChannels]; + int position = newRatePosition * oldSampleRate; + int leftPosition = oldRatePosition * newSampleRate; + int rightPosition = (oldRatePosition + 1) * newSampleRate; + int ratio = rightPosition - position; + int width = rightPosition - leftPosition; + + return (short) ((ratio * left + (width - ratio) * right) / width); + } + + // Change the rate. + private void adjustRate(float rate, + int originalNumOutputSamples) { + int newSampleRate = (int) (sampleRate / rate); + int oldSampleRate = sampleRate; + int position; + + // Set these values to help with the integer math + while (newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) { + newSampleRate >>= 1; + oldSampleRate >>= 1; + } + if (numOutputSamples == originalNumOutputSamples) { + return; + } + moveNewSamplesToPitchBuffer(originalNumOutputSamples); + // Leave at least one pitch sample in the buffer + for (position = 0; position < numPitchSamples - 1; position++) { + while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) { + enlargeOutputBufferIfNeeded(1); + for (int i = 0; i < numChannels; i++) { + outputBuffer[numOutputSamples * numChannels + i] = interpolate(pitchBuffer, position + i, + oldSampleRate, newSampleRate); + } + newRatePosition++; + numOutputSamples++; + } + oldRatePosition++; + if (oldRatePosition == oldSampleRate) { + oldRatePosition = 0; + if (newRatePosition != newSampleRate) { + System.out.printf("Assertion failed: newRatePosition != newSampleRate\n"); + assert false; + } + newRatePosition = 0; + } + } + removePitchSamples(position); + } + + + // Skip over a pitch period, and copy period/speed samples to the output + private int skipPitchPeriod(short samples[], + int position, + float speed, + int period) { + int newSamples; + + if (speed >= 2.0f) { + newSamples = (int) (period / (speed - 1.0f)); + } else { + newSamples = period; + remainingInputToCopy = (int) (period * (2.0f - speed) / (speed - 1.0f)); + } + enlargeOutputBufferIfNeeded(newSamples); + overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples, samples, position, + samples, position + period); + numOutputSamples += newSamples; + return newSamples; + } + + // Insert a pitch period, and determine how much input to copy directly. + private int insertPitchPeriod(short samples[], + int position, + float speed, + int period) { + int newSamples; + + if (speed < 0.5f) { + newSamples = (int) (period * speed / (1.0f - speed)); + } else { + newSamples = period; + remainingInputToCopy = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed)); + } + enlargeOutputBufferIfNeeded(period + newSamples); + move(outputBuffer, numOutputSamples, samples, position, period); + overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples, + position + period, samples, position); + numOutputSamples += period + newSamples; + return newSamples; + } + + // Resample as many pitch periods as we have buffered on the input. Return 0 if + // we fail to resize an input or output buffer. Also scale the output by the volume. + private void changeSpeed(float speed) { + int numSamples = numInputSamples; + int position = 0, period, newSamples; + + if (numInputSamples < maxRequired) { + return; + } + do { + if (remainingInputToCopy > 0) { + newSamples = copyInputToOutput(position); + position += newSamples; + } else { + period = findPitchPeriod(inputBuffer, position, true); + if (speed > 1.0) { + newSamples = skipPitchPeriod(inputBuffer, position, speed, period); + position += period + newSamples; + } else { + newSamples = insertPitchPeriod(inputBuffer, position, speed, period); + position += newSamples; + } + } + } while (position + maxRequired <= numSamples); + removeInputSamples(position); + } + + // Resample as many pitch periods as we have buffered on the input. Scale the output by the volume. + private void processStreamInput() { + int originalNumOutputSamples = numOutputSamples; + float s = speed / pitch; + float r = rate; + + if (!useChordPitch) { + r *= pitch; + } + if (s > 1.00001 || s < 0.99999) { + changeSpeed(s); + } else { + copyToOutput(inputBuffer, 0, numInputSamples); + numInputSamples = 0; + } + if (useChordPitch && pitch != 1.0f) { + adjustPitch(originalNumOutputSamples); + } else if (r != 1.0f) { + adjustRate(r, originalNumOutputSamples); + } + if (volume != 1.0f) { + // Adjust output volume. + scaleSamples(outputBuffer, originalNumOutputSamples, numOutputSamples - originalNumOutputSamples, + volume); + } + } + + // Write floating point data to the input buffer and process it. + public void writeFloatToStream(float samples[], + int numSamples) { + addFloatSamplesToInputBuffer(samples, numSamples); + processStreamInput(); + } + + // Write the data to the input stream, and process it. + public void writeShortToStream(short samples[], + int numSamples) { + addShortSamplesToInputBuffer(samples, numSamples); + processStreamInput(); + } + + // Simple wrapper around sonicWriteFloatToStream that does the unsigned byte to short + // conversion for you. + public void writeUnsignedByteToStream(byte samples[], + int numSamples) { + addUnsignedByteSamplesToInputBuffer(samples, numSamples); + processStreamInput(); + } + + // Simple wrapper around sonicWriteBytesToStream that does the byte to 16-bit LE conversion. + public void writeBytesToStream(byte inBuffer[], + int numBytes) { + addBytesToInputBuffer(inBuffer, numBytes); + processStreamInput(); + } + + // This is a non-stream oriented interface to just change the speed of a sound sample + public static int changeFloatSpeed(float samples[], + int numSamples, + float speed, + float pitch, + float rate, + float volume, + boolean useChordPitch, + int sampleRate, + int numChannels) { + Sonic stream = new Sonic(sampleRate, numChannels); + + stream.setSpeed(speed); + stream.setPitch(pitch); + stream.setRate(rate); + stream.setVolume(volume); + stream.setChordPitch(useChordPitch); + stream.writeFloatToStream(samples, numSamples); + stream.flushStream(); + numSamples = stream.samplesAvailable(); + stream.readFloatFromStream(samples, numSamples); + return numSamples; + } + + /* This is a non-stream oriented interface to just change the speed of a sound sample */ + public int sonicChangeShortSpeed(short samples[], + int numSamples, + float speed, + float pitch, + float rate, + float volume, + boolean useChordPitch, + int sampleRate, + int numChannels) { + Sonic stream = new Sonic(sampleRate, numChannels); + + stream.setSpeed(speed); + stream.setPitch(pitch); + stream.setRate(rate); + stream.setVolume(volume); + stream.setChordPitch(useChordPitch); + stream.writeShortToStream(samples, numSamples); + stream.flushStream(); + numSamples = stream.samplesAvailable(); + stream.readShortFromStream(samples, numSamples); + return numSamples; + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..d8f14a1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':library' |