blob: e055513ec28e502f8dcd9c0612f85b3b31063167 [file] [log] [blame]
//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 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 boolean mIsDecoding;
private long mDuration;
private float mCurrentSpeed;
private float mCurrentPitch;
private int mCurrentState;
private final Context mContext;
private PowerManager.WakeLock mWakeLock = null;
private boolean mDownMix;
private final static String TAG_TRACK = "SonicTrack";
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();
mDownMix = false;
}
@Override
public int getAudioSessionId() {
if(mTrack == null) {
return 0;
} else {
return mTrack.getAudioSessionId();
}
}
@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:
case STATE_IDLE:
case STATE_INITIALIZED:
return 0;
default:
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() {
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_PREPARED:
Log.d(TAG_TRACK, "Prepared, ignore pause()");
break;
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_PLAYBACK_COMPLETED:
try {
initStream();
} catch (IOException e) {
Log.e(TAG, "initStream() failed");
error();
return;
}
// deliberate fallthrough
case STATE_PREPARED:
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) {
boolean playing = false;
switch (mCurrentState) {
case STATE_STARTED:
playing = true;
pause();
case STATE_PREPARED:
case STATE_PAUSED:
case STATE_PLAYBACK_COMPLETED:
mLock.lock();
if (mTrack == null) {
return;
}
mTrack.flush();
mExtractor.seekTo(((long) msec * 1000), MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
Log.d(TAG, "seek completed, position: " + (mExtractor.getSampleTime() / 1000L));
if (owningMediaPlayer.onSeekCompleteListener != null) {
owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer);
}
mLock.unlock();
if (playing) {
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();
}
}
public void setDownMix(boolean downmix) {
mDownMix = downmix;
}
@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) {
if(mCurrentState == STATE_ERROR) {
return;
}
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;
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
}
}
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();
}
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) {
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();
}
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 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) {
Log.e(TAG, Log.getStackTraceString(e));
if (audioTrack != null) {
audioTrack.release();
}
}
}
throw new IllegalStateException("Could not create buffer for AudioTrack");
}
@SuppressWarnings("deprecation")
public 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 (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];
}
if (mDownMix && mSonic.getNumChannels() == 2) {
int maxBytes = (available / 4) * 4;
mSonic.readBytesFromStream(modifiedSamples, maxBytes);
for (int i = 0; (i + 3) < modifiedSamples.length; i += 4) {
short left = (short) ((modifiedSamples[i] & 0xff) | (modifiedSamples[i + 1] << 8));
short right = (short) ((modifiedSamples[i + 2] & 0xff) | (modifiedSamples[i + 3] << 8));
short value = (short) (0.5 * left + 0.5 * right);
modifiedSamples[i] = (byte) (value & 0xff);
modifiedSamples[i + 1] = (byte) (value >> 8);
modifiedSamples[i + 2] = (byte) (value & 0xff);
modifiedSamples[i + 3] = (byte) (value >> 8);
}
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) {
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();
// 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");
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();
}
}