1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.media;
6
7import android.annotation.SuppressLint;
8import android.media.AudioFormat;
9import android.media.AudioRecord;
10import android.media.MediaRecorder.AudioSource;
11import android.media.audiofx.AcousticEchoCanceler;
12import android.media.audiofx.AudioEffect;
13import android.media.audiofx.AudioEffect.Descriptor;
14import android.os.Process;
15import android.util.Log;
16
17import org.chromium.base.CalledByNative;
18import org.chromium.base.JNINamespace;
19
20import java.nio.ByteBuffer;
21
22// Owned by its native counterpart declared in audio_record_input.h. Refer to
23// that class for general comments.
24@JNINamespace("media")
25class AudioRecordInput {
26    private static final String TAG = "AudioRecordInput";
27    // Set to true to enable debug logs. Always check in as false.
28    private static final boolean DEBUG = false;
29    // We are unable to obtain a precise measurement of the hardware delay on
30    // Android. This is a conservative lower-bound based on measurments. It
31    // could surely be tightened with further testing.
32    private static final int HARDWARE_DELAY_MS = 100;
33
34    private final long mNativeAudioRecordInputStream;
35    private final int mSampleRate;
36    private final int mChannels;
37    private final int mBitsPerSample;
38    private final int mHardwareDelayBytes;
39    private final boolean mUsePlatformAEC;
40    private ByteBuffer mBuffer;
41    private AudioRecord mAudioRecord;
42    private AudioRecordThread mAudioRecordThread;
43    private AcousticEchoCanceler mAEC;
44
45    private class AudioRecordThread extends Thread {
46        // The "volatile" synchronization technique is discussed here:
47        // http://stackoverflow.com/a/106787/299268
48        // and more generally in this article:
49        // https://www.ibm.com/developerworks/java/library/j-jtp06197/
50        private volatile boolean mKeepAlive = true;
51
52        @Override
53        public void run() {
54            Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
55            try {
56                mAudioRecord.startRecording();
57            } catch (IllegalStateException e) {
58                Log.e(TAG, "startRecording failed", e);
59                return;
60            }
61
62            while (mKeepAlive) {
63                int bytesRead = mAudioRecord.read(mBuffer, mBuffer.capacity());
64                if (bytesRead > 0) {
65                    nativeOnData(mNativeAudioRecordInputStream, bytesRead,
66                                 mHardwareDelayBytes);
67                } else {
68                    Log.e(TAG, "read failed: " + bytesRead);
69                    if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
70                        // This can happen if there is already an active
71                        // AudioRecord (e.g. in another tab).
72                        mKeepAlive = false;
73                    }
74                }
75            }
76
77            try {
78                mAudioRecord.stop();
79            } catch (IllegalStateException e) {
80                Log.e(TAG, "stop failed", e);
81            }
82        }
83
84        public void joinRecordThread() {
85            mKeepAlive = false;
86            while (isAlive()) {
87                try {
88                    join();
89                } catch (InterruptedException e) {
90                    // Ignore.
91                }
92            }
93        }
94    }
95
96    @CalledByNative
97    private static AudioRecordInput createAudioRecordInput(long nativeAudioRecordInputStream,
98            int sampleRate, int channels, int bitsPerSample, int bytesPerBuffer,
99            boolean usePlatformAEC) {
100        return new AudioRecordInput(nativeAudioRecordInputStream, sampleRate, channels,
101                                    bitsPerSample, bytesPerBuffer, usePlatformAEC);
102    }
103
104    private AudioRecordInput(long nativeAudioRecordInputStream, int sampleRate, int channels,
105                             int bitsPerSample, int bytesPerBuffer, boolean usePlatformAEC) {
106        mNativeAudioRecordInputStream = nativeAudioRecordInputStream;
107        mSampleRate = sampleRate;
108        mChannels = channels;
109        mBitsPerSample = bitsPerSample;
110        mHardwareDelayBytes = HARDWARE_DELAY_MS * sampleRate / 1000 * bitsPerSample / 8;
111        mUsePlatformAEC = usePlatformAEC;
112
113        // We use a direct buffer so that the native class can have access to
114        // the underlying memory address. This avoids the need to copy from a
115        // jbyteArray to native memory. More discussion of this here:
116        // http://developer.android.com/training/articles/perf-jni.html
117        mBuffer = ByteBuffer.allocateDirect(bytesPerBuffer);
118        // Rather than passing the ByteBuffer with every OnData call (requiring
119        // the potentially expensive GetDirectBufferAddress) we simply have the
120        // the native class cache the address to the memory once.
121        //
122        // Unfortunately, profiling with traceview was unable to either confirm
123        // or deny the advantage of this approach, as the values for
124        // nativeOnData() were not stable across runs.
125        nativeCacheDirectBufferAddress(mNativeAudioRecordInputStream, mBuffer);
126    }
127
128    @SuppressLint("NewApi")
129    @CalledByNative
130    private boolean open() {
131        if (mAudioRecord != null) {
132            Log.e(TAG, "open() called twice without a close()");
133            return false;
134        }
135        int channelConfig;
136        if (mChannels == 1) {
137            channelConfig = AudioFormat.CHANNEL_IN_MONO;
138        } else if (mChannels == 2) {
139            channelConfig = AudioFormat.CHANNEL_IN_STEREO;
140        } else {
141            Log.e(TAG, "Unsupported number of channels: " + mChannels);
142            return false;
143        }
144
145        int audioFormat;
146        if (mBitsPerSample == 8) {
147            audioFormat = AudioFormat.ENCODING_PCM_8BIT;
148        } else if (mBitsPerSample == 16) {
149            audioFormat = AudioFormat.ENCODING_PCM_16BIT;
150        } else {
151            Log.e(TAG, "Unsupported bits per sample: " + mBitsPerSample);
152            return false;
153        }
154
155        // TODO(ajm): Do we need to make this larger to avoid underruns? The
156        // Android documentation notes "this size doesn't guarantee a smooth
157        // recording under load".
158        int minBufferSize = AudioRecord.getMinBufferSize(mSampleRate, channelConfig, audioFormat);
159        if (minBufferSize < 0) {
160            Log.e(TAG, "getMinBufferSize error: " + minBufferSize);
161            return false;
162        }
163
164        // We will request mBuffer.capacity() with every read call. The
165        // underlying AudioRecord buffer should be at least this large.
166        int audioRecordBufferSizeInBytes = Math.max(mBuffer.capacity(), minBufferSize);
167        try {
168            // TODO(ajm): Allow other AudioSource types to be requested?
169            mAudioRecord = new AudioRecord(AudioSource.VOICE_COMMUNICATION,
170                                           mSampleRate,
171                                           channelConfig,
172                                           audioFormat,
173                                           audioRecordBufferSizeInBytes);
174        } catch (IllegalArgumentException e) {
175            Log.e(TAG, "AudioRecord failed", e);
176            return false;
177        }
178
179        if (AcousticEchoCanceler.isAvailable()) {
180            mAEC = AcousticEchoCanceler.create(mAudioRecord.getAudioSessionId());
181            if (mAEC == null) {
182                Log.e(TAG, "AcousticEchoCanceler.create failed");
183                return false;
184            }
185            int ret = mAEC.setEnabled(mUsePlatformAEC);
186            if (ret != AudioEffect.SUCCESS) {
187                Log.e(TAG, "setEnabled error: " + ret);
188                return false;
189            }
190
191            if (DEBUG) {
192                Descriptor descriptor = mAEC.getDescriptor();
193                Log.d(TAG, "AcousticEchoCanceler " +
194                        "name: " + descriptor.name + ", " +
195                        "implementor: " + descriptor.implementor + ", " +
196                        "uuid: " + descriptor.uuid);
197            }
198        }
199        return true;
200    }
201
202    @CalledByNative
203    private void start() {
204        if (mAudioRecord == null) {
205            Log.e(TAG, "start() called before open().");
206            return;
207        }
208        if (mAudioRecordThread != null) {
209            // start() was already called.
210            return;
211        }
212        mAudioRecordThread = new AudioRecordThread();
213        mAudioRecordThread.start();
214    }
215
216    @CalledByNative
217    private void stop() {
218        if (mAudioRecordThread == null) {
219            // start() was never called, or stop() was already called.
220            return;
221        }
222        mAudioRecordThread.joinRecordThread();
223        mAudioRecordThread = null;
224    }
225
226    @SuppressLint("NewApi")
227    @CalledByNative
228    private void close() {
229        if (mAudioRecordThread != null) {
230            Log.e(TAG, "close() called before stop().");
231            return;
232        }
233        if (mAudioRecord == null) {
234            // open() was not called.
235            return;
236        }
237
238        if (mAEC != null) {
239            mAEC.release();
240            mAEC = null;
241        }
242        mAudioRecord.release();
243        mAudioRecord = null;
244    }
245
246    private native void nativeCacheDirectBufferAddress(long nativeAudioRecordInputStream,
247                                                       ByteBuffer buffer);
248    private native void nativeOnData(long nativeAudioRecordInputStream, int size,
249                                     int hardwareDelayBytes);
250}
251