//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 <[email protected]> * 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.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import static org.antennapod.audio.SonicAudioPlayerState.END; import static org.antennapod.audio.SonicAudioPlayerState.ERROR; import static org.antennapod.audio.SonicAudioPlayerState.IDLE; import static org.antennapod.audio.SonicAudioPlayerState.INITIALIZED; import static org.antennapod.audio.SonicAudioPlayerState.PAUSED; import static org.antennapod.audio.SonicAudioPlayerState.PLAYBACK_COMPLETED; import static org.antennapod.audio.SonicAudioPlayerState.PREPARED; import static org.antennapod.audio.SonicAudioPlayerState.PREPARING; import static org.antennapod.audio.SonicAudioPlayerState.STARTED; import static org.antennapod.audio.SonicAudioPlayerState.STOPPED; @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public class SonicAudioPlayer extends AbstractAudioPlayer { private static final String TAG = SonicAudioPlayer.class.getSimpleName(); private final static String TAG_TRACK = "SonicTrack"; private AudioTrack mTrack; private int mBufferSize; 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 AtomicInteger mInitiatingCount = new AtomicInteger(0); private AtomicInteger mSeekingCount = new AtomicInteger(0); private boolean mIsDecoding; private long mDuration; private float mCurrentSpeed; private float mCurrentPitch; private final SonicAudioPlayerState state = new SonicAudioPlayerState(); private final Context mContext; private PowerManager.WakeLock mWakeLock = null; private boolean mDownMix; SonicAudioPlayer(MediaPlayer owningMediaPlayer, Context context) { super(owningMediaPlayer, context); mCurrentSpeed = 1.0f; mCurrentPitch = 1.0f; mContinue = false; mIsDecoding = false; mContext = context; mPath = null; mUri = null; mLock = new ReentrantLock(); mDecoderLock = new Object(); mDownMix = false; } @Override public int getAudioSessionId() { if (mTrack == null) { return 0; } return mTrack.getAudioSessionId(); } @Override public boolean canSetPitch() { return true; } @Override public boolean canSetSpeed() { return true; } @Override public float getCurrentPitchStepsAdjustment() { return mCurrentPitch; } public int getCurrentPosition() { if (state.is(INITIALIZED) || state.is(IDLE) || state.is(ERROR)) { return 0; } return (int) (mExtractor.getSampleTime() / 1000); } @Override public float getCurrentSpeedMultiplier() { return mCurrentSpeed; } @Override public boolean canDownmix() { return true; } @Override public void setDownmix(boolean enable) { mDownMix = enable; } public int getDuration() { if (state.is(INITIALIZED) || state.is(IDLE) || state.is(ERROR)) { error(); return 0; } return (int) (mDuration / 1000); } @Override public float getMaxSpeedMultiplier() { return 4.0f; } @Override public float getMinSpeedMultiplier() { return 0.5f; } @Override public boolean isLooping() { return false; } public boolean isPlaying() { if (state.is(ERROR)) { error(); return false; } return state.is(STARTED); } public void pause() { Log.d(TAG, "pause(), current state: " + state); if (state.is(PREPARED)) { Log.d(TAG_TRACK, "PREPARED, ignore pause()"); return; } if (!state.is(STARTED) && !state.is(PAUSED)) { error(); return; } mTrack.pause(); state.changeTo(PAUSED); } public void prepare() { Log.d(TAG, "prepare(), current state: " + state); if (!state.is(INITIALIZED) && !state.is(STOPPED)) { error(); return; } doPrepare(); } public void prepareAsync() { Log.d(TAG, "prepareAsync(), current state: " + state); if (!state.is(INITIALIZED) && !state.is(STOPPED)) { error(); return; } Thread t = new Thread(new Runnable() { @Override public void run() { doPrepare(); } }); t.setDaemon(true); t.start(); } private void doPrepare() { boolean streamInitialized; String lastPath = currentPath(); state.changeTo(PREPARING); try { streamInitialized = initStream(); } catch (IOException e) { String currentPath = currentPath(); if (currentPath == null || currentPath.equals(lastPath)) { Log.e(TAG_TRACK, "Failed setting data source!", e); error(); } return; } if (streamInitialized) { if (!state.is(ERROR)) { state.changeTo(PREPARED); } owningMediaPlayer.onPreparedListener.onPrepared(owningMediaPlayer); } } public void stop() { if (!state.stoppingAllowed()) { error(); Log.d(TAG_TRACK, "Stopping in current state " + state + " not allowed"); return; } state.changeTo(STOPPED); mContinue = false; mTrack.pause(); mTrack.flush(); } public void start() { if (state.is(STARTED)) { return; } if (state.is(PLAYBACK_COMPLETED) || state.is(PREPARED)) { if(state.is(PLAYBACK_COMPLETED)) { try { initStream(); } catch (IOException e) { Log.e(TAG, "initStream() failed"); error(); return; } } state.changeTo(STARTED); mContinue = true; mTrack.play(); decode(); } else if (state.is(PAUSED)) { state.changeTo(STARTED); synchronized (mDecoderLock) { mDecoderLock.notify(); } mTrack.play(); } else { state.changeTo(ERROR); 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(); state.changeTo(END); } public void reset() { mLock.lock(); mContinue = false; try { if (mDecoderThread != null && !state.is(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; } mPath = null; mUri = null; mBufferSize = 0; state.changeTo(IDLE); mLock.unlock(); } public void seekTo(final int msec) { boolean playing = false; if (!state.seekingAllowed()) { error(); Log.d(TAG_TRACK, "Seeking in current state " + state + " is not seekable"); return; } if (state.is(STARTED)) { playing = true; pause(); } if (mTrack == null) { return; } mTrack.flush(); final boolean wasPlaying = playing; Runnable seekRunnable = new Runnable() { @Override public void run() { String lastPath = currentPath(); mSeekingCount.incrementAndGet(); try { mExtractor.seekTo(((long) msec * 1000), MediaExtractor.SEEK_TO_PREVIOUS_SYNC); } catch (Exception e) { error(); return; } finally { mSeekingCount.decrementAndGet(); } // make sure that the current episode didn't change while seeking if (mExtractor != null && lastPath != null && lastPath.equals(currentPath()) && !state.is(ERROR)) { Log.d(TAG, "seek completed, position: " + getCurrentPosition()); if (owningMediaPlayer.onSeekCompleteListener != null) { owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer); } if (wasPlaying) { start(); } } } }; // when streaming, the seeking is started in another thread to prevent UI locking if (mUri != null) { Thread t = new Thread(seekRunnable); t.setDaemon(true); t.start(); } else { seekRunnable.run(); } } @Override public void setAudioStreamType(int streamtype) {} @Override public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) {} @Override public void setLooping(boolean loop) {} @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) { if (!state.settingDataSourceAllowed()) { error(); return; } mPath = path; state.changeTo(INITIALIZED); } @Override public void setDataSource(Context context, Uri uri) { if (!state.settingDataSourceAllowed()) { error(); return; } mUri = uri; state.changeTo(INITIALIZED); } void setDownMix(boolean downmix) { mDownMix = downmix; } @SuppressWarnings("deprecation") @Override public void setVolume(float leftVolume, float rightVolume) { // Pass call directly to AudioTrack if available. if (mTrack == null) { return; } 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(); } } } private void error() { error(0); } private void error(int extra) { if (state.is(ERROR)) { return; } state.changeTo(ERROR); if (owningMediaPlayer.onErrorListener != null) { boolean handled = owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, 0, extra); if (!handled && owningMediaPlayer.onCompletionListener != null) { owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); } } } private String currentPath() { if (mPath != null) { return mPath; } else if (mUri != null) { return mUri.toString(); } return null; } private boolean initStream() throws IOException { // Since this method could be running in another thread, when "setDataSource" returns // we need to check if the media path has changed String lastPath = currentPath(); mInitiatingCount.incrementAndGet(); try { mExtractor = new MediaExtractor(); if (mPath != null) { mExtractor.setDataSource(mPath); } else if (mUri != null) { mExtractor.setDataSource(mContext, mUri, null); } else { throw new IOException("Neither path nor uri set"); } } finally { mInitiatingCount.decrementAndGet(); } String currentPath = currentPath(); if (currentPath == null || !currentPath.equals(lastPath) || state.is(ERROR)) { return false; } mLock.lock(); if (mExtractor == null) { mLock.unlock(); throw new IOException("Extractor is null"); } int trackNum = -1; for (int i = 0; i < mExtractor.getTrackCount(); i++) { final MediaFormat oFormat = mExtractor.getTrackFormat(i); String mime = oFormat.getString(MediaFormat.KEY_MIME); if (trackNum < 0 && mime.startsWith("audio/")) { trackNum = i; } else { mExtractor.unselectTrack(i); } } if (trackNum < 0) { mLock.unlock(); throw new IOException("No audio track found"); } final MediaFormat oFormat = mExtractor.getTrackFormat(trackNum); try { 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, "Channel count: " + channelCount); Log.v(TAG_TRACK, "Mime type: " + mime); Log.v(TAG_TRACK, "Duration: " + mDuration); initDevice(sampleRate, channelCount); mExtractor.selectTrack(trackNum); mCodec = MediaCodec.createDecoderByType(mime); mCodec.configure(oFormat, null, null, 0); } catch (Throwable th) { Log.e(TAG, Log.getStackTraceString(th)); error(); } mLock.unlock(); return true; } private void initDevice(int sampleRate, int numChannels) { mLock.lock(); final int format = findFormatFromChannels(numChannels); int oldBufferSize = mBufferSize; mBufferSize = AudioTrack.getMinBufferSize(sampleRate, format, AudioFormat.ENCODING_PCM_16BIT); if (mBufferSize != oldBufferSize) { if (mTrack != null) { mTrack.release(); } mTrack = createAudioTrack(sampleRate, format, mBufferSize); } mSonic = new Sonic(sampleRate, numChannels); mLock.unlock(); } private static int findFormatFromChannels(int numChannels) { switch (numChannels) { case 1: return AudioFormat.CHANNEL_OUT_MONO; case 2: return AudioFormat.CHANNEL_OUT_STEREO; case 3: return AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; case 4: return AudioFormat.CHANNEL_OUT_QUAD; case 5: return AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; case 6: return AudioFormat.CHANNEL_OUT_5POINT1; case 7: return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; case 8: if (Build.VERSION.SDK_INT >= 23) { return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; } else { return -1; } default: return -1; // Error } } private AudioTrack createAudioTrack(int sampleRate, int channelConfig, int minBufferSize) { for (int i = 4; i >= 1; i--) { int bufferSize = minBufferSize * i; AudioTrack audioTrack = null; try { audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM); if (audioTrack.getState() == AudioTrack.STATE_INITIALIZED) { mBufferSize = bufferSize; return audioTrack; } else { audioTrack.release(); } } catch (IllegalArgumentException e) { if (audioTrack != null) { audioTrack.release(); } } } throw new IllegalStateException("Could not create buffer for AudioTrack"); } @SuppressWarnings("deprecation") private void decode() { mDecoderThread = new Thread(new Runnable() { private int currHeadPos; @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) { currHeadPos = mTrack.getPlaybackHeadPosition(); if (state.is(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]; } if (mDownMix && mSonic.getNumChannels() == 2) { int maxBytes = (available / 4) * 4; mSonic.readBytesFromStream(modifiedSamples, maxBytes); DownMixer.downMix(modifiedSamples); mTrack.write(modifiedSamples, 0, maxBytes); } else { 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) { final MediaFormat oFormat = mCodec.getOutputFormat(); Log.d("PCM", "Output format has changed to " + oFormat); int sampleRate = oFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); int channelCount = oFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); if (sampleRate != mSonic.getSampleRate() || channelCount != mSonic.getNumChannels()) { mTrack.stop(); mLock.lock(); mTrack.release(); initDevice(sampleRate, channelCount); 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)); if (!((mInitiatingCount.get() > 0) || (mSeekingCount.get() > 0))) { Log.d(TAG_TRACK, "Current position: " + getCurrentPosition()); } mCodec.stop(); // wait for track to finish playing int lastHeadPos; do { lastHeadPos = currHeadPos; try { Thread.sleep(100); currHeadPos = mTrack.getPlaybackHeadPosition(); } catch (InterruptedException e) { /* ignore */ } } while (currHeadPos != lastHeadPos); mTrack.stop(); Log.d(TAG_TRACK, "Stopped codec and track"); if (!((mInitiatingCount.get() > 0) || (mSeekingCount.get() > 0))) { Log.d(TAG_TRACK, "Current position: " + getCurrentPosition()); } mIsDecoding = false; if (mContinue && (sawInputEOS || sawOutputEOS)) { state.changeTo(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.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable ex) { Log.e(TAG_TRACK, Log.getStackTraceString(ex)); error(); } }); mDecoderThread.setDaemon(true); mDecoderThread.start(); } }