/**
*
* For information on usage and redistribution, and for a DISCLAIMER OF ALL
* WARRANTIES, see the file, "LICENSE.txt," in this distribution.
*
*/
package org.puredata.android.io;
import java.io.IOException;
import org.puredata.android.service.R;
import org.puredata.android.utils.Properties;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaPlayer;
import android.os.Process;
import android.util.Log;
/**
*
* AudioWrapper wraps {@link AudioTrack} and {@link AudioRecord} objects and manages the main audio rendering
* thread. It hides the complexity of working with raw PCM audio; client code only needs to implement a JACK-style
* audio processing callback (jackaudio.org).
*
* @author Peter Brinkmann (peter.brinkmann@gmail.com)
*
*/
public abstract class AudioWrapper {
private static final String AUDIO_WRAPPER = "AudioWrapper";
private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
private final AudioRecordWrapper rec;
private final AudioTrack track;
final short outBuf[];
final int inputSizeShorts;
final int bufSizeShorts;
private Thread audioThread = null;
/**
* Constructor; initializes {@link AudioTrack} and {@link AudioRecord} objects
*
* @param sampleRate
* @param inChannels number of input channels
* @param outChannels number of output channels
* @param bufferSizePerChannel number of samples per buffer per channel
* @throws IOException if the audio parameters are not supported by the device
*/
public AudioWrapper(int sampleRate, int inChannels, int outChannels, int bufferSizePerChannel) throws IOException {
int channelConfig = VersionedAudioFormat.getOutFormat(outChannels);
rec = (inChannels == 0) ? null : new AudioRecordWrapper(sampleRate, inChannels, bufferSizePerChannel);
inputSizeShorts = inChannels * bufferSizePerChannel;
bufSizeShorts = outChannels * bufferSizePerChannel;
outBuf = new short[bufSizeShorts];
int bufSizeBytes = 2 * bufSizeShorts;
int trackSizeBytes = 2 * bufSizeBytes;
int minTrackSizeBytes = AudioTrack.getMinBufferSize(sampleRate, channelConfig, ENCODING);
if (minTrackSizeBytes <= 0) {
throw new IOException("bad AudioTrack parameters; sr: " + sampleRate +", ch: " + outChannels + ", bufSize: " + trackSizeBytes);
}
while (trackSizeBytes < minTrackSizeBytes) trackSizeBytes += bufSizeBytes;
track = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, ENCODING, trackSizeBytes, AudioTrack.MODE_STREAM);
if (track.getState() != AudioTrack.STATE_INITIALIZED) {
track.release();
throw new IOException("unable to initialize AudioTrack instance for sr: " + sampleRate +", ch: " + outChannels + ", bufSize: " + trackSizeBytes);
}
}
/**
* Main audio rendering callback, reads input samples and writes output samples; inspired by the process callback of JACK
*
* Channels are striped across buffers, i.e., if there are two output channels, then outBuffer[0] will be the first sample
* for the left channel, outBuffer[1] will be the first sample for the right channel, outBuffer[2] will be the second sample
* for the left channel, etc.
*
* @param inBuffer array of input samples to be processed, e.g., from the microphone
* @param outBuffer array of output samples, e.g., to be sent to the speakers
* @return
*/
protected abstract int process(short inBuffer[], short outBuffer[]);
/**
* Start the audio rendering thread as well as {@link AudioTrack} and {@link AudioRecord} objects
*
* @param context
*/
public synchronized void start(Context context) {
avoidClickHack(context);
audioThread = new Thread() {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
if (rec != null) rec.start();
track.play();
short inBuf[];
try {
inBuf = (rec != null) ? rec.take() : new short[inputSizeShorts];
} catch (InterruptedException e) {
return;
}
while (!Thread.interrupted()) {
if (process(inBuf, outBuf) != 0) break;
track.write(outBuf, 0, bufSizeShorts);
if (rec != null) {
short newBuf[] = rec.poll();
if (newBuf != null) {
inBuf = newBuf;
} else {
Log.w(AUDIO_WRAPPER, "no input buffer available");
}
}
}
if (rec != null) rec.stop();
track.stop();
}
};
audioThread.start();
}
/**
* Stop the audio thread as well as {@link AudioTrack} and {@link AudioRecord} objects
*/
public synchronized void stop() {
if (audioThread == null) return;
audioThread.interrupt();
try {
audioThread.join();
} catch (InterruptedException e) {
// do nothing
}
audioThread = null;
}
/**
* Release resources held by {@link AudioTrack} and {@link AudioRecord} objects;
* stops the audio thread if it is still running
*/
public synchronized void release() {
stop();
track.release();
if (rec != null) rec.release();
}
/**
* @return true if and only if the audio thread is currently running
*/
public synchronized boolean isRunning() {
return audioThread != null && audioThread.getState() != Thread.State.TERMINATED;
}
/**
* @return the audio session ID, for Gingerbread and later; will throw an exception on older versions
*/
public synchronized int getAudioSessionId() {
int version = Properties.version;
if (version >= 9) {
return AudioSessionHandler.getAudioSessionId(track); // Lazy class loading trick.
} else {
throw new UnsupportedOperationException("audio sessions not supported in Android " + version);
}
}
private static class AudioSessionHandler {
private static int getAudioSessionId(AudioTrack track) {
return track.getAudioSessionId();
}
}
// weird little hack; eliminates the nasty click when AudioTrack (dis)engages by playing
// a few milliseconds of silence before starting AudioTrack
private void avoidClickHack(Context context) {
try {
MediaPlayer mp = MediaPlayer.create(context, R.raw.silence);
mp.start();
Thread.sleep(10);
mp.stop();
mp.release();
} catch (Exception e) {
Log.e(AUDIO_WRAPPER, e.toString());
}
}
}