251 lines
9.4 KiB
Java
Executable File
251 lines
9.4 KiB
Java
Executable File
// 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);
|
|
}
|