| 1 |
/** |
| 2 |
* |
| 3 |
* For information on usage and redistribution, and for a DISCLAIMER OF ALL |
| 4 |
* WARRANTIES, see the file, "LICENSE.txt," in this distribution. |
| 5 |
* |
| 6 |
*/ |
| 7 |
|
| 8 |
package org.puredata.android.io; |
| 9 |
|
| 10 |
import java.io.IOException; |
| 11 |
|
| 12 |
import org.puredata.android.service.R; |
| 13 |
|
| 14 |
import android.content.Context; |
| 15 |
import android.media.AudioFormat; |
| 16 |
import android.media.AudioManager; |
| 17 |
import android.media.AudioRecord; |
| 18 |
import android.media.AudioTrack; |
| 19 |
import android.media.MediaPlayer; |
| 20 |
import android.os.Process; |
| 21 |
import android.util.Log; |
| 22 |
|
| 23 |
/** |
| 24 |
* |
| 25 |
* AudioWrapper wraps {@link AudioTrack} and {@link AudioRecord} objects and manages the main audio rendering |
| 26 |
* thread. It hides the complexity of working with raw PCM audio; client code only needs to implement a JACK-style |
| 27 |
* audio processing callback (jackaudio.org). |
| 28 |
* |
| 29 |
* @author Peter Brinkmann (peter.brinkmann@gmail.com) |
| 30 |
* |
| 31 |
*/ |
| 32 |
public abstract class AudioWrapper { |
| 33 |
|
| 34 |
private static final String AUDIO_WRAPPER = "AudioWrapper"; |
| 35 |
private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; |
| 36 |
private final AudioRecordWrapper rec; |
| 37 |
private final AudioTrack track; |
| 38 |
final short outBuf[]; |
| 39 |
final int inputSizeShorts; |
| 40 |
final int bufSizeShorts; |
| 41 |
private Thread audioThread = null; |
| 42 |
|
| 43 |
/** |
| 44 |
* Constructor; initializes {@link AudioTrack} and {@link AudioRecord} objects |
| 45 |
* |
| 46 |
* @param sampleRate |
| 47 |
* @param inChannels number of input channels |
| 48 |
* @param outChannels number of output channels |
| 49 |
* @param bufferSizePerChannel number of samples per buffer per channel |
| 50 |
* @throws IOException if the audio parameters are not supported by the device |
| 51 |
*/ |
| 52 |
public AudioWrapper(int sampleRate, int inChannels, int outChannels, int bufferSizePerChannel) throws IOException { |
| 53 |
int channelConfig = VersionedAudioFormat.getOutFormat(outChannels); |
| 54 |
rec = (inChannels == 0) ? null : new AudioRecordWrapper(sampleRate, inChannels, bufferSizePerChannel); |
| 55 |
inputSizeShorts = inChannels * bufferSizePerChannel; |
| 56 |
bufSizeShorts = outChannels * bufferSizePerChannel; |
| 57 |
outBuf = new short[bufSizeShorts]; |
| 58 |
int bufSizeBytes = 2 * bufSizeShorts; |
| 59 |
int trackSizeBytes = 2 * bufSizeBytes; |
| 60 |
int minTrackSizeBytes = AudioTrack.getMinBufferSize(sampleRate, channelConfig, ENCODING); |
| 61 |
if (minTrackSizeBytes <= 0) { |
| 62 |
throw new IOException("bad AudioTrack parameters; sr: " + sampleRate +", ch: " + outChannels + ", bufSize: " + trackSizeBytes); |
| 63 |
} |
| 64 |
while (trackSizeBytes < minTrackSizeBytes) trackSizeBytes += bufSizeBytes; |
| 65 |
track = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, ENCODING, trackSizeBytes, AudioTrack.MODE_STREAM); |
| 66 |
if (track.getState() != AudioTrack.STATE_INITIALIZED) { |
| 67 |
track.release(); |
| 68 |
throw new IOException("unable to initialize AudioTrack instance for sr: " + sampleRate +", ch: " + outChannels + ", bufSize: " + trackSizeBytes); |
| 69 |
} |
| 70 |
} |
| 71 |
|
| 72 |
/** |
| 73 |
* Main audio rendering callback, reads input samples and writes output samples; inspired by the process callback of JACK |
| 74 |
* |
| 75 |
* Channels are striped across buffers, i.e., if there are two output channels, then outBuffer[0] will be the first sample |
| 76 |
* for the left channel, outBuffer[1] will be the first sample for the right channel, outBuffer[2] will be the second sample |
| 77 |
* for the left channel, etc. |
| 78 |
* |
| 79 |
* @param inBuffer array of input samples to be processed, e.g., from the microphone |
| 80 |
* @param outBuffer array of output samples, e.g., to be sent to the speakers |
| 81 |
* @return |
| 82 |
*/ |
| 83 |
protected abstract int process(short inBuffer[], short outBuffer[]); |
| 84 |
|
| 85 |
/** |
| 86 |
* Start the audio rendering thread as well as {@link AudioTrack} and {@link AudioRecord} objects |
| 87 |
* |
| 88 |
* @param context |
| 89 |
*/ |
| 90 |
public synchronized void start(Context context) { |
| 91 |
avoidClickHack(context); |
| 92 |
audioThread = new Thread() { |
| 93 |
@Override |
| 94 |
public void run() { |
| 95 |
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); |
| 96 |
if (rec != null) rec.start(); |
| 97 |
track.play(); |
| 98 |
short inBuf[]; |
| 99 |
try { |
| 100 |
inBuf = (rec != null) ? rec.take() : new short[inputSizeShorts]; |
| 101 |
} catch (InterruptedException e) { |
| 102 |
return; |
| 103 |
} |
| 104 |
while (!Thread.interrupted()) { |
| 105 |
if (process(inBuf, outBuf) != 0) break; |
| 106 |
track.write(outBuf, 0, bufSizeShorts); |
| 107 |
if (rec != null) { |
| 108 |
short newBuf[] = rec.poll(); |
| 109 |
if (newBuf != null) { |
| 110 |
inBuf = newBuf; |
| 111 |
} else { |
| 112 |
Log.w(AUDIO_WRAPPER, "no input buffer available"); |
| 113 |
} |
| 114 |
} |
| 115 |
} |
| 116 |
if (rec != null) rec.stop(); |
| 117 |
track.stop(); |
| 118 |
} |
| 119 |
}; |
| 120 |
audioThread.start(); |
| 121 |
} |
| 122 |
|
| 123 |
/** |
| 124 |
* Stop the audio thread as well as {@link AudioTrack} and {@link AudioRecord} objects |
| 125 |
*/ |
| 126 |
public synchronized void stop() { |
| 127 |
if (audioThread == null) return; |
| 128 |
audioThread.interrupt(); |
| 129 |
try { |
| 130 |
audioThread.join(); |
| 131 |
} catch (InterruptedException e) { |
| 132 |
// do nothing |
| 133 |
} |
| 134 |
audioThread = null; |
| 135 |
} |
| 136 |
|
| 137 |
/** |
| 138 |
* Release resources held by {@link AudioTrack} and {@link AudioRecord} objects; |
| 139 |
* stops the audio thread if it is still running |
| 140 |
*/ |
| 141 |
public synchronized void release() { |
| 142 |
stop(); |
| 143 |
track.release(); |
| 144 |
if (rec != null) rec.release(); |
| 145 |
} |
| 146 |
|
| 147 |
/** |
| 148 |
* @return true if and only if the audio thread is currently running |
| 149 |
*/ |
| 150 |
public synchronized boolean isRunning() { |
| 151 |
return audioThread != null && audioThread.getState() != Thread.State.TERMINATED; |
| 152 |
} |
| 153 |
|
| 154 |
// weird little hack; eliminates the nasty click when AudioTrack (dis)engages by playing |
| 155 |
// a few milliseconds of silence before starting AudioTrack |
| 156 |
private void avoidClickHack(Context context) { |
| 157 |
try { |
| 158 |
MediaPlayer mp = MediaPlayer.create(context, R.raw.silence); |
| 159 |
mp.start(); |
| 160 |
Thread.sleep(10); |
| 161 |
mp.stop(); |
| 162 |
mp.release(); |
| 163 |
} catch (Exception e) { |
| 164 |
Log.e(AUDIO_WRAPPER, e.toString()); |
| 165 |
} |
| 166 |
} |
| 167 |
} |