/**
* org.hermit.android.io: Android utilities for accessing peripherals.
*
* These classes provide some basic utilities for accessing the builtin
* interface, at present.
*
* <br>Copyright 2009 Ian Cameron Smith
*
* <p>This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2
* as published by the Free Software Foundation (see COPYING).
*
* <p>This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
package pl.llp.aircasting.sensor.builtin;
import pl.llp.aircasting.util.Constants;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;
import java.util.Arrays;
/**
* A class which reads builtin input from the mic in a background thread and
* passes it to the caller when ready.
* <p/>
* <p>To use this class, your application must have permission RECORD_AUDIO.
*/
public class AudioReader {
// ******************************************************************** //
// Public Classes.
// ******************************************************************** //
/**
* Listener for builtin reads.
*/
public static abstract class Listener {
/**
* Audio read error code: the builtin reader failed to initialise.
*/
public static final int ERR_INIT_FAILED = 1;
/**
* Audio read error code: an builtin read failed.
*/
public static final int ERR_READ_FAILED = 2;
/**
* An builtin read has completed.
*
* @param buffer Buffer containing the data.
*/
public abstract void onReadComplete(short[] buffer);
/**
* An error has occurred. The reader has been terminated.
*
* @param error ERR_XXX code describing the error.
*/
public abstract void onReadError(int error);
}
// ******************************************************************** //
// Constructor.
// ******************************************************************** //
/**
* Create an AudioReader instance.
*/
public AudioReader() {
// audioManager = (AudioManager) app.getSystemService(Context.AUDIO_SERVICE);
}
// ******************************************************************** //
// Run Control.
// ******************************************************************** //
/**
* Start this reader.
*
* @param rate The builtin sampling rate, in samples / sec.
* @param block Number of samples of input to read at a time.
* This is different from the system builtin
* buffer size.
* @param listener Listener to be notified on each completed read.
*/
public void startReader(int rate, int block, Listener listener) {
Log.i(TAG, "Reader: Start Thread");
synchronized (this) {
// Calculate the required I/O buffer size.
int audioBuf = AudioRecord.getMinBufferSize(rate,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT);
Log.d(TAG, "Will use buffer size: " + audioBuf);
// Set up the builtin input.
audioInput = new AudioRecord(MediaRecorder.AudioSource.MIC,
rate,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT,
audioBuf);
inputBlockSize = block;
sleepTime = (long) (1000f / ((float) rate / (float) block));
// We double inputBlockSize because of Android 5.0 bug,
// AudioRecord.read(short[] audioData, int offsetInShorts, int sizeInShorts)
// writes twice as much data to a buffer than it should.
// We cut it later: Arrays.copyOfRange(buffer, 0, inputBlockSize)
inputBuffer = new short[2][inputBlockSize * 2];
inputBufferWhich = 0;
inputBufferIndex = 0;
inputListener = listener;
running = true;
readerThread = new Thread(new Runnable() {
@Override
public void run() {
readerRun();
}
}, "Audio Reader");
readerThread.start();
}
}
/**
* Stop this reader.
*/
public void stopReader() {
Log.i(TAG, "Reader: Signal Stop");
synchronized (this) {
running = false;
}
try {
if (readerThread != null)
readerThread.join();
} catch (InterruptedException e) {
//Ignore - just stop
}
readerThread = null;
// Kill the builtin input.
synchronized (this) {
if (audioInput != null) {
audioInput.release();
audioInput = null;
}
}
Log.i(TAG, "Reader: Thread Stopped");
}
// ******************************************************************** //
// Main Loop.
// ******************************************************************** //
/**
* Main loop of the builtin reader. This runs in our own thread.
*/
private void readerRun() {
short[] buffer;
int index, readSize;
int timeout = 5000;
try {
while (timeout > 0 && audioInput.getState() != AudioRecord.STATE_INITIALIZED) {
Thread.sleep(50);
timeout -= 50;
}
} catch (InterruptedException e) {
Log.e(TAG, "Audio reader thread interrupted", e);
}
Log.d(TAG, "Audio reader state: " + audioInput.getState());
if (audioInput.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "Audio reader failed to initialize");
readError(Listener.ERR_INIT_FAILED);
running = false;
return;
}
try {
Log.i(TAG, "Reader: Start Recording");
audioInput.startRecording();
long stime = 0;
while (running) {
if (inputBufferIndex == 0) {
stime = System.currentTimeMillis();
}
if (!running)
break;
readSize = inputBlockSize;
int space = inputBlockSize - inputBufferIndex;
if (readSize > space)
readSize = space;
buffer = inputBuffer[inputBufferWhich];
index = inputBufferIndex;
synchronized (buffer) {
int nread = audioInput.read(buffer, index, readSize);
boolean done = false;
if (!running)
break;
if (nread < 0) {
Log.e(TAG, "Audio read failed: error " + nread);
readError(Listener.ERR_READ_FAILED);
running = false;
break;
}
int end = inputBufferIndex + nread;
if (end >= inputBlockSize) {
inputBufferWhich = (inputBufferWhich + 1) % 2;
inputBufferIndex = 0;
done = true;
} else
inputBufferIndex = end;
if (done) {
readDone(Arrays.copyOfRange(buffer, 0, inputBlockSize));
// Because our block size is way smaller than the builtin
// buffer, we get blocks in bursts, which messes up
// the builtin analyzer. We don't want to be forced to
// wait until the analysis is done, because if
// the analysis is slow, lag will build up. Instead
// wait, but with a timeout which lets us keep the
// input serviced.
long etime = System.currentTimeMillis();
long sleep = sleepTime - (etime - stime);
if (sleep < 5)
sleep = 5;
try {
buffer.wait(sleep);
} catch (InterruptedException e) {
}
}
}
}
} finally {
Log.i(TAG, "Reader: Stop Recording");
if (audioInput.getState() == AudioRecord.RECORDSTATE_RECORDING)
audioInput.stop();
}
}
/**
* Notify the client that a read has completed.
*
* @param buffer Buffer containing the data.
*/
private void readDone(short[] buffer) {
inputListener.onReadComplete(buffer);
}
/**
* Notify the client that an error has occurred. The reader has been
* terminated.
*
* @param code ERR_XXX code describing the error.
*/
private void readError(int code) {
inputListener.onReadError(code);
}
// ******************************************************************** //
// Class Data.
// ******************************************************************** //
// Debugging tag.
private static final String TAG = Constants.TAG + "/AudioReader";
// ******************************************************************** //
// Private Data.
// ******************************************************************** //
// Our builtin input device.
private AudioRecord audioInput;
// Our builtin input buffer, and the index of the next item to go in.
private short[][] inputBuffer = null;
private int inputBufferWhich = 0;
private int inputBufferIndex = 0;
// Size of the block to read each time.
private int inputBlockSize = 0;
// Time in ms to sleep between blocks, to meter the supply rate.
private long sleepTime = 0;
// Listener for input.
private Listener inputListener = null;
// Flag whether the thread should be running.
private volatile boolean running = false;
// The thread, if any, which is currently reading. Null if not running.
private Thread readerThread = null;
}