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
}