| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.media; |
| |
| import android.annotation.SuppressLint; |
| import android.media.AudioFormat; |
| import android.media.AudioRecord; |
| import android.media.MediaRecorder.AudioSource; |
| import android.media.audiofx.AcousticEchoCanceler; |
| import android.media.audiofx.AudioEffect; |
| import android.media.audiofx.AudioEffect.Descriptor; |
| import android.os.Process; |
| import android.util.Log; |
| |
| import org.chromium.base.CalledByNative; |
| import org.chromium.base.JNINamespace; |
| |
| import java.nio.ByteBuffer; |
| |
| // Owned by its native counterpart declared in audio_record_input.h. Refer to |
| // that class for general comments. |
| @JNINamespace("media") |
| class AudioRecordInput { |
| private static final String TAG = "AudioRecordInput"; |
| // Set to true to enable debug logs. Always check in as false. |
| private static final boolean DEBUG = false; |
| // We are unable to obtain a precise measurement of the hardware delay on |
| // Android. This is a conservative lower-bound based on measurments. It |
| // could surely be tightened with further testing. |
| private static final int HARDWARE_DELAY_MS = 100; |
| |
| private final long mNativeAudioRecordInputStream; |
| private final int mSampleRate; |
| private final int mChannels; |
| private final int mBitsPerSample; |
| private final int mHardwareDelayBytes; |
| private final boolean mUsePlatformAEC; |
| private ByteBuffer mBuffer; |
| private AudioRecord mAudioRecord; |
| private AudioRecordThread mAudioRecordThread; |
| private AcousticEchoCanceler mAEC; |
| |
| private class AudioRecordThread extends Thread { |
| // The "volatile" synchronization technique is discussed here: |
| // http://stackoverflow.com/a/106787/299268 |
| // and more generally in this article: |
| // https://www.ibm.com/developerworks/java/library/j-jtp06197/ |
| private volatile boolean mKeepAlive = true; |
| |
| @Override |
| public void run() { |
| Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); |
| try { |
| mAudioRecord.startRecording(); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "startRecording failed", e); |
| return; |
| } |
| |
| while (mKeepAlive) { |
| int bytesRead = mAudioRecord.read(mBuffer, mBuffer.capacity()); |
| if (bytesRead > 0) { |
| nativeOnData(mNativeAudioRecordInputStream, bytesRead, |
| mHardwareDelayBytes); |
| } else { |
| Log.e(TAG, "read failed: " + bytesRead); |
| if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) { |
| // This can happen if there is already an active |
| // AudioRecord (e.g. in another tab). |
| mKeepAlive = false; |
| } |
| } |
| } |
| |
| try { |
| mAudioRecord.stop(); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "stop failed", e); |
| } |
| } |
| |
| public void joinRecordThread() { |
| mKeepAlive = false; |
| while (isAlive()) { |
| try { |
| join(); |
| } catch (InterruptedException e) { |
| // Ignore. |
| } |
| } |
| } |
| } |
| |
| @CalledByNative |
| private static AudioRecordInput createAudioRecordInput(long nativeAudioRecordInputStream, |
| int sampleRate, int channels, int bitsPerSample, int bytesPerBuffer, |
| boolean usePlatformAEC) { |
| return new AudioRecordInput(nativeAudioRecordInputStream, sampleRate, channels, |
| bitsPerSample, bytesPerBuffer, usePlatformAEC); |
| } |
| |
| private AudioRecordInput(long nativeAudioRecordInputStream, int sampleRate, int channels, |
| int bitsPerSample, int bytesPerBuffer, boolean usePlatformAEC) { |
| mNativeAudioRecordInputStream = nativeAudioRecordInputStream; |
| mSampleRate = sampleRate; |
| mChannels = channels; |
| mBitsPerSample = bitsPerSample; |
| mHardwareDelayBytes = HARDWARE_DELAY_MS * sampleRate / 1000 * bitsPerSample / 8; |
| mUsePlatformAEC = usePlatformAEC; |
| |
| // We use a direct buffer so that the native class can have access to |
| // the underlying memory address. This avoids the need to copy from a |
| // jbyteArray to native memory. More discussion of this here: |
| // http://developer.android.com/training/articles/perf-jni.html |
| mBuffer = ByteBuffer.allocateDirect(bytesPerBuffer); |
| // Rather than passing the ByteBuffer with every OnData call (requiring |
| // the potentially expensive GetDirectBufferAddress) we simply have the |
| // the native class cache the address to the memory once. |
| // |
| // Unfortunately, profiling with traceview was unable to either confirm |
| // or deny the advantage of this approach, as the values for |
| // nativeOnData() were not stable across runs. |
| nativeCacheDirectBufferAddress(mNativeAudioRecordInputStream, mBuffer); |
| } |
| |
| @SuppressLint("NewApi") |
| @CalledByNative |
| private boolean open() { |
| if (mAudioRecord != null) { |
| Log.e(TAG, "open() called twice without a close()"); |
| return false; |
| } |
| int channelConfig; |
| if (mChannels == 1) { |
| channelConfig = AudioFormat.CHANNEL_IN_MONO; |
| } else if (mChannels == 2) { |
| channelConfig = AudioFormat.CHANNEL_IN_STEREO; |
| } else { |
| Log.e(TAG, "Unsupported number of channels: " + mChannels); |
| return false; |
| } |
| |
| int audioFormat; |
| if (mBitsPerSample == 8) { |
| audioFormat = AudioFormat.ENCODING_PCM_8BIT; |
| } else if (mBitsPerSample == 16) { |
| audioFormat = AudioFormat.ENCODING_PCM_16BIT; |
| } else { |
| Log.e(TAG, "Unsupported bits per sample: " + mBitsPerSample); |
| return false; |
| } |
| |
| // TODO(ajm): Do we need to make this larger to avoid underruns? The |
| // Android documentation notes "this size doesn't guarantee a smooth |
| // recording under load". |
| int minBufferSize = AudioRecord.getMinBufferSize(mSampleRate, channelConfig, audioFormat); |
| if (minBufferSize < 0) { |
| Log.e(TAG, "getMinBufferSize error: " + minBufferSize); |
| return false; |
| } |
| |
| // We will request mBuffer.capacity() with every read call. The |
| // underlying AudioRecord buffer should be at least this large. |
| int audioRecordBufferSizeInBytes = Math.max(mBuffer.capacity(), minBufferSize); |
| try { |
| // TODO(ajm): Allow other AudioSource types to be requested? |
| mAudioRecord = new AudioRecord(AudioSource.VOICE_COMMUNICATION, |
| mSampleRate, |
| channelConfig, |
| audioFormat, |
| audioRecordBufferSizeInBytes); |
| } catch (IllegalArgumentException e) { |
| Log.e(TAG, "AudioRecord failed", e); |
| return false; |
| } |
| |
| if (AcousticEchoCanceler.isAvailable()) { |
| mAEC = AcousticEchoCanceler.create(mAudioRecord.getAudioSessionId()); |
| if (mAEC == null) { |
| Log.e(TAG, "AcousticEchoCanceler.create failed"); |
| return false; |
| } |
| int ret = mAEC.setEnabled(mUsePlatformAEC); |
| if (ret != AudioEffect.SUCCESS) { |
| Log.e(TAG, "setEnabled error: " + ret); |
| return false; |
| } |
| |
| if (DEBUG) { |
| Descriptor descriptor = mAEC.getDescriptor(); |
| Log.d(TAG, "AcousticEchoCanceler " + |
| "name: " + descriptor.name + ", " + |
| "implementor: " + descriptor.implementor + ", " + |
| "uuid: " + descriptor.uuid); |
| } |
| } |
| return true; |
| } |
| |
| @CalledByNative |
| private void start() { |
| if (mAudioRecord == null) { |
| Log.e(TAG, "start() called before open()."); |
| return; |
| } |
| if (mAudioRecordThread != null) { |
| // start() was already called. |
| return; |
| } |
| mAudioRecordThread = new AudioRecordThread(); |
| mAudioRecordThread.start(); |
| } |
| |
| @CalledByNative |
| private void stop() { |
| if (mAudioRecordThread == null) { |
| // start() was never called, or stop() was already called. |
| return; |
| } |
| mAudioRecordThread.joinRecordThread(); |
| mAudioRecordThread = null; |
| } |
| |
| @SuppressLint("NewApi") |
| @CalledByNative |
| private void close() { |
| if (mAudioRecordThread != null) { |
| Log.e(TAG, "close() called before stop()."); |
| return; |
| } |
| if (mAudioRecord == null) { |
| // open() was not called. |
| return; |
| } |
| |
| if (mAEC != null) { |
| mAEC.release(); |
| mAEC = null; |
| } |
| mAudioRecord.release(); |
| mAudioRecord = null; |
| } |
| |
| private native void nativeCacheDirectBufferAddress(long nativeAudioRecordInputStream, |
| ByteBuffer buffer); |
| private native void nativeOnData(long nativeAudioRecordInputStream, int size, |
| int hardwareDelayBytes); |
| } |