/* * Copyright (C) 2014 Andrew Comminos * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package se.lublin.humla.audio; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.audiofx.AcousticEchoCanceler; import android.media.audiofx.AutomaticGainControl; import android.media.audiofx.NoiseSuppressor; import android.os.Build; import android.util.Log; import se.lublin.humla.Constants; import se.lublin.humla.exception.AudioInitializationException; import se.lublin.humla.exception.NativeAudioException; import se.lublin.humla.protocol.AudioHandler; /** * Created by andrew on 23/08/13. */ public class AudioInput implements Runnable { public static final int[] SAMPLE_RATES = { 48000, 44100, 16000, 8000 }; // AudioRecord state private AudioInputListener mListener; private AudioRecord mAudioRecord; private final int mFrameSize; private AcousticEchoCanceler mAEC = null; private Thread mRecordThread; private boolean mRecording; public AudioInput(AudioInputListener listener, int audioSource, int targetSampleRate) throws NativeAudioException, AudioInitializationException { mListener = listener; // Attempt to construct an AudioRecord with the target sample rate first. // If it fails, keep producing AudioRecord instances until we find one that initializes // correctly. Maybe one day Android will let us probe for supported sample rates, as we // aren't even guaranteed that 44100hz will work across all devices. for (int i = 0; i < SAMPLE_RATES.length + 1; i++) { int sampleRate = i == 0 ? targetSampleRate : SAMPLE_RATES[i - 1]; try { mAudioRecord = setupAudioRecord(sampleRate, audioSource); break; } catch (AudioInitializationException e) { // Continue iteration, probing for a supported sample rate. } } if (mAudioRecord == null) { throw new AudioInitializationException("Unable to initialize AudioInput."); } setupAEC(); int sampleRate = getSampleRate(); // FIXME: does not work properly if 10ms frames cannot be represented as integers mFrameSize = (sampleRate * AudioHandler.FRAME_SIZE) / AudioHandler.SAMPLE_RATE; } private static AudioRecord setupAudioRecord(int sampleRate, int audioSource) throws AudioInitializationException { int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); if (minBufferSize <= 0) throw new AudioInitializationException("Invalid buffer size returned (unsupported sample rate)."); AudioRecord audioRecord; try { audioRecord = new AudioRecord(audioSource, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize); } catch (IllegalArgumentException e) { throw new AudioInitializationException(e); } if(audioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) { audioRecord.release(); throw new AudioInitializationException("AudioRecord failed to initialize!"); } return audioRecord; } private void setupAEC() { if (mAudioRecord == null) { return; } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { return; } // TODO "On some devices, an AEC can be inserted by default in the // capture path by the platform according to the // MediaRecorder.AudioSource used. The application should call // AcousticEchoCanceler.getEnable() after creating the AEC to // check the default AEC activation state on a particular // AudioRecord session." // https://developer.android.com/reference/android/media/audiofx/AcousticEchoCanceler.html // vv same for AGC and NS // Read https://source.android.com/devices/audio/implement-pre-processing int audioSessionId = mAudioRecord.getAudioSessionId(); if (AcousticEchoCanceler.isAvailable()) { mAEC = AcousticEchoCanceler.create(audioSessionId); } mAEC.setEnabled(true); // TODO? // if (aec != null) { //  int ret = aec.setEnabled(enable); //  if (ret != AudioEffect.SUCCESS) { //  return false; //  } //  } return; // if(AutomaticGainControl.isAvailable()) { // AutomaticGainControl.create(audioSessionId); // } // if(NoiseSuppressor.isAvailable()) { // NoiseSuppressor.create(audioSessionId); // } } /** * Starts the recording thread. * Not thread-safe. */ public void startRecording() { mRecording = true; mRecordThread = new Thread(this); mRecordThread.start(); } /** * Stops the record loop after the current iteration, joining it. * Not thread-safe. */ public void stopRecording() { if(!mRecording) return; mRecording = false; try { mRecordThread.interrupt(); mRecordThread.join(); mRecordThread = null; } catch (InterruptedException e) { e.printStackTrace(); } } /** * Stops the record loop and waits on it to finish. * Releases native audio resources. * NOTE: It is not safe to call startRecording after. */ public void shutdown() { stopRecording(); if(mAudioRecord != null) { mAudioRecord.release(); mAudioRecord = null; if (mAEC != null) { mAEC.release(); mAEC = null; } } } public boolean isRecording() { return mRecording; } /** * @return the sample rate used by the AudioRecord instance. */ public int getSampleRate() { return mAudioRecord.getSampleRate(); } /** * @return the frame size used, varying depending on the sample rate selected. */ public int getFrameSize() { return mFrameSize; } @Override public void run() { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); Log.i(Constants.TAG, "AudioInput: started"); mAudioRecord.startRecording(); if(mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) return; final short[] mAudioBuffer = new short[mFrameSize]; // We loop when the 'recording' instance var is true instead of checking audio record state because we want to always cleanly shutdown. while(mRecording) { int shortsRead = mAudioRecord.read(mAudioBuffer, 0, mFrameSize); if(shortsRead > 0) { mListener.onAudioInputReceived(mAudioBuffer, mFrameSize); } else { Log.e(Constants.TAG, "Error fetching audio! AudioRecord error " + shortsRead); } } mAudioRecord.stop(); Log.i(Constants.TAG, "AudioInput: stopped"); } public interface AudioInputListener { void onAudioInputReceived(short[] frame, int frameSize); } }