1 /*
2  *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3  *
4  *  Use of this source code is governed by a BSD-style license
5  *  that can be found in the LICENSE file in the root of the source
6  *  tree. An additional intellectual property rights grant can be found
7  *  in the file PATENTS.  All contributing project authors may
8  *  be found in the AUTHORS file in the root of the source tree.
9  */
10 
11 package org.webrtc.voiceengine;
12 
13 import android.annotation.TargetApi;
14 import android.content.Context;
15 import android.media.AudioFormat;
16 import android.media.AudioRecord;
17 import android.media.MediaRecorder.AudioSource;
18 import android.os.Process;
19 import java.lang.System;
20 import java.nio.ByteBuffer;
21 import java.util.concurrent.TimeUnit;
22 import org.webrtc.ContextUtils;
23 import org.webrtc.Logging;
24 import org.webrtc.ThreadUtils;
25 
26 public class WebRtcAudioRecord {
27   private static final boolean DEBUG = false;
28 
29   private static final String TAG = "WebRtcAudioRecord";
30 
31   // Default audio data format is PCM 16 bit per sample.
32   // Guaranteed to be supported by all devices.
33   private static final int BITS_PER_SAMPLE = 16;
34 
35   // Requested size of each recorded buffer provided to the client.
36   private static final int CALLBACK_BUFFER_SIZE_MS = 10;
37 
38   // Average number of callbacks per second.
39   private static final int BUFFERS_PER_SECOND = 1000 / CALLBACK_BUFFER_SIZE_MS;
40 
41   // We ask for a native buffer size of BUFFER_SIZE_FACTOR * (minimum required
42   // buffer size). The extra space is allocated to guard against glitches under
43   // high load.
44   private static final int BUFFER_SIZE_FACTOR = 2;
45 
46   // The AudioRecordJavaThread is allowed to wait for successful call to join()
47   // but the wait times out afther this amount of time.
48   private static final long AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS = 2000;
49 
50   private static final int DEFAULT_AUDIO_SOURCE = getDefaultAudioSource();
51   private static int audioSource = DEFAULT_AUDIO_SOURCE;
52 
53   private final long nativeAudioRecord;
54 
55   private WebRtcAudioEffects effects = null;
56 
57   private ByteBuffer byteBuffer;
58 
59   private AudioRecord audioRecord = null;
60   private AudioRecordThread audioThread = null;
61 
62   private static volatile boolean microphoneMute = false;
63   private byte[] emptyBytes;
64 
65   // Audio recording error handler functions.
66   public enum AudioRecordStartErrorCode {
67     AUDIO_RECORD_START_EXCEPTION,
68     AUDIO_RECORD_START_STATE_MISMATCH,
69   }
70 
71   public static interface WebRtcAudioRecordErrorCallback {
onWebRtcAudioRecordInitError(String errorMessage)72     void onWebRtcAudioRecordInitError(String errorMessage);
onWebRtcAudioRecordStartError(AudioRecordStartErrorCode errorCode, String errorMessage)73     void onWebRtcAudioRecordStartError(AudioRecordStartErrorCode errorCode, String errorMessage);
onWebRtcAudioRecordError(String errorMessage)74     void onWebRtcAudioRecordError(String errorMessage);
75   }
76 
77   private static WebRtcAudioRecordErrorCallback errorCallback = null;
78 
setErrorCallback(WebRtcAudioRecordErrorCallback errorCallback)79   public static void setErrorCallback(WebRtcAudioRecordErrorCallback errorCallback) {
80     Logging.d(TAG, "Set error callback");
81     WebRtcAudioRecord.errorCallback = errorCallback;
82   }
83 
84   /**
85    * Audio thread which keeps calling ByteBuffer.read() waiting for audio
86    * to be recorded. Feeds recorded data to the native counterpart as a
87    * periodic sequence of callbacks using DataIsRecorded().
88    * This thread uses a Process.THREAD_PRIORITY_URGENT_AUDIO priority.
89    */
90   private class AudioRecordThread extends Thread {
91     private volatile boolean keepAlive = true;
92 
AudioRecordThread(String name)93     public AudioRecordThread(String name) {
94       super(name);
95     }
96 
97     @Override
run()98     public void run() {
99       Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
100       Logging.d(TAG, "AudioRecordThread" + WebRtcAudioUtils.getThreadInfo());
101       assertTrue(audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING);
102 
103       long lastTime = System.nanoTime();
104       while (keepAlive) {
105         int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity());
106         if (bytesRead == byteBuffer.capacity()) {
107           if (microphoneMute) {
108             byteBuffer.clear();
109             byteBuffer.put(emptyBytes);
110           }
111           nativeDataIsRecorded(bytesRead, nativeAudioRecord);
112         } else {
113           String errorMessage = "AudioRecord.read failed: " + bytesRead;
114           Logging.e(TAG, errorMessage);
115           if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
116             keepAlive = false;
117             reportWebRtcAudioRecordError(errorMessage);
118           }
119         }
120         if (DEBUG) {
121           long nowTime = System.nanoTime();
122           long durationInMs = TimeUnit.NANOSECONDS.toMillis((nowTime - lastTime));
123           lastTime = nowTime;
124           Logging.d(TAG, "bytesRead[" + durationInMs + "] " + bytesRead);
125         }
126       }
127 
128       try {
129         if (audioRecord != null) {
130           audioRecord.stop();
131         }
132       } catch (IllegalStateException e) {
133         Logging.e(TAG, "AudioRecord.stop failed: " + e.getMessage());
134       }
135     }
136 
137     // Stops the inner thread loop and also calls AudioRecord.stop().
138     // Does not block the calling thread.
stopThread()139     public void stopThread() {
140       Logging.d(TAG, "stopThread");
141       keepAlive = false;
142     }
143   }
144 
WebRtcAudioRecord(long nativeAudioRecord)145   WebRtcAudioRecord(long nativeAudioRecord) {
146     Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo());
147     this.nativeAudioRecord = nativeAudioRecord;
148     if (DEBUG) {
149       WebRtcAudioUtils.logDeviceInfo(TAG);
150     }
151     effects = WebRtcAudioEffects.create();
152   }
153 
enableBuiltInAEC(boolean enable)154   private boolean enableBuiltInAEC(boolean enable) {
155     Logging.d(TAG, "enableBuiltInAEC(" + enable + ')');
156     if (effects == null) {
157       Logging.e(TAG, "Built-in AEC is not supported on this platform");
158       return false;
159     }
160     return effects.setAEC(enable);
161   }
162 
enableBuiltInNS(boolean enable)163   private boolean enableBuiltInNS(boolean enable) {
164     Logging.d(TAG, "enableBuiltInNS(" + enable + ')');
165     if (effects == null) {
166       Logging.e(TAG, "Built-in NS is not supported on this platform");
167       return false;
168     }
169     return effects.setNS(enable);
170   }
171 
initRecording(int sampleRate, int channels)172   private int initRecording(int sampleRate, int channels) {
173     Logging.d(TAG, "initRecording(sampleRate=" + sampleRate + ", channels=" + channels + ")");
174     if (audioRecord != null) {
175       reportWebRtcAudioRecordInitError("InitRecording called twice without StopRecording.");
176       return -1;
177     }
178     final int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8);
179     final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND;
180     byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);
181     Logging.d(TAG, "byteBuffer.capacity: " + byteBuffer.capacity());
182     emptyBytes = new byte[byteBuffer.capacity()];
183     // Rather than passing the ByteBuffer with every callback (requiring
184     // the potentially expensive GetDirectBufferAddress) we simply have the
185     // the native class cache the address to the memory once.
186     nativeCacheDirectBufferAddress(byteBuffer, nativeAudioRecord);
187 
188     // Get the minimum buffer size required for the successful creation of
189     // an AudioRecord object, in byte units.
190     // Note that this size doesn't guarantee a smooth recording under load.
191     final int channelConfig = channelCountToConfiguration(channels);
192     int minBufferSize =
193         AudioRecord.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
194     if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
195       reportWebRtcAudioRecordInitError("AudioRecord.getMinBufferSize failed: " + minBufferSize);
196       return -1;
197     }
198     Logging.d(TAG, "AudioRecord.getMinBufferSize: " + minBufferSize);
199 
200     // Use a larger buffer size than the minimum required when creating the
201     // AudioRecord instance to ensure smooth recording under load. It has been
202     // verified that it does not increase the actual recording latency.
203     int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity());
204     Logging.d(TAG, "bufferSizeInBytes: " + bufferSizeInBytes);
205     try {
206       audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig,
207           AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes);
208     } catch (IllegalArgumentException e) {
209       reportWebRtcAudioRecordInitError("AudioRecord ctor error: " + e.getMessage());
210       releaseAudioResources();
211       return -1;
212     }
213     if (audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
214       reportWebRtcAudioRecordInitError("Failed to create a new AudioRecord instance");
215       releaseAudioResources();
216       return -1;
217     }
218     if (effects != null) {
219       effects.enable(audioRecord.getAudioSessionId());
220     }
221     logMainParameters();
222     logMainParametersExtended();
223     return framesPerBuffer;
224   }
225 
startRecording()226   private boolean startRecording() {
227     Logging.d(TAG, "startRecording");
228     assertTrue(audioRecord != null);
229     assertTrue(audioThread == null);
230     try {
231       audioRecord.startRecording();
232     } catch (IllegalStateException e) {
233       reportWebRtcAudioRecordStartError(AudioRecordStartErrorCode.AUDIO_RECORD_START_EXCEPTION,
234           "AudioRecord.startRecording failed: " + e.getMessage());
235       return false;
236     }
237     if (audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
238       reportWebRtcAudioRecordStartError(
239           AudioRecordStartErrorCode.AUDIO_RECORD_START_STATE_MISMATCH,
240           "AudioRecord.startRecording failed - incorrect state :"
241           + audioRecord.getRecordingState());
242       return false;
243     }
244     audioThread = new AudioRecordThread("AudioRecordJavaThread");
245     audioThread.start();
246     return true;
247   }
248 
stopRecording()249   private boolean stopRecording() {
250     Logging.d(TAG, "stopRecording");
251     assertTrue(audioThread != null);
252     audioThread.stopThread();
253     if (!ThreadUtils.joinUninterruptibly(audioThread, AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS)) {
254       Logging.e(TAG, "Join of AudioRecordJavaThread timed out");
255     }
256     audioThread = null;
257     if (effects != null) {
258       effects.release();
259     }
260     releaseAudioResources();
261     return true;
262   }
263 
logMainParameters()264   private void logMainParameters() {
265     Logging.d(TAG, "AudioRecord: "
266             + "session ID: " + audioRecord.getAudioSessionId() + ", "
267             + "channels: " + audioRecord.getChannelCount() + ", "
268             + "sample rate: " + audioRecord.getSampleRate());
269   }
270 
271   @TargetApi(23)
logMainParametersExtended()272   private void logMainParametersExtended() {
273     if (WebRtcAudioUtils.runningOnMarshmallowOrHigher()) {
274       Logging.d(TAG, "AudioRecord: "
275               // The frame count of the native AudioRecord buffer.
276               + "buffer size in frames: " + audioRecord.getBufferSizeInFrames());
277     }
278   }
279 
280   // Helper method which throws an exception  when an assertion has failed.
assertTrue(boolean condition)281   private static void assertTrue(boolean condition) {
282     if (!condition) {
283       throw new AssertionError("Expected condition to be true");
284     }
285   }
286 
channelCountToConfiguration(int channels)287   private int channelCountToConfiguration(int channels) {
288     return (channels == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO);
289   }
290 
nativeCacheDirectBufferAddress(ByteBuffer byteBuffer, long nativeAudioRecord)291   private native void nativeCacheDirectBufferAddress(ByteBuffer byteBuffer, long nativeAudioRecord);
292 
nativeDataIsRecorded(int bytes, long nativeAudioRecord)293   private native void nativeDataIsRecorded(int bytes, long nativeAudioRecord);
294 
295   @SuppressWarnings("NoSynchronizedMethodCheck")
setAudioSource(int source)296   public static synchronized void setAudioSource(int source) {
297     Logging.w(TAG, "Audio source is changed from: " + audioSource
298             + " to " + source);
299     audioSource = source;
300   }
301 
getDefaultAudioSource()302   private static int getDefaultAudioSource() {
303     return AudioSource.VOICE_COMMUNICATION;
304   }
305 
306   // Sets all recorded samples to zero if |mute| is true, i.e., ensures that
307   // the microphone is muted.
setMicrophoneMute(boolean mute)308   public static void setMicrophoneMute(boolean mute) {
309     Logging.w(TAG, "setMicrophoneMute(" + mute + ")");
310     microphoneMute = mute;
311   }
312 
313   // Releases the native AudioRecord resources.
releaseAudioResources()314   private void releaseAudioResources() {
315     if (audioRecord != null) {
316       audioRecord.release();
317       audioRecord = null;
318     }
319   }
320 
reportWebRtcAudioRecordInitError(String errorMessage)321   private void reportWebRtcAudioRecordInitError(String errorMessage) {
322     Logging.e(TAG, "Init recording error: " + errorMessage);
323     if (errorCallback != null) {
324       errorCallback.onWebRtcAudioRecordInitError(errorMessage);
325     }
326   }
327 
reportWebRtcAudioRecordStartError( AudioRecordStartErrorCode errorCode, String errorMessage)328   private void reportWebRtcAudioRecordStartError(
329       AudioRecordStartErrorCode errorCode, String errorMessage) {
330     Logging.e(TAG, "Start recording error: " + errorCode + ". " + errorMessage);
331     if (errorCallback != null) {
332       errorCallback.onWebRtcAudioRecordStartError(errorCode, errorMessage);
333     }
334   }
335 
reportWebRtcAudioRecordError(String errorMessage)336   private void reportWebRtcAudioRecordError(String errorMessage) {
337     Logging.e(TAG, "Run-time recording error: " + errorMessage);
338     if (errorCallback != null) {
339       errorCallback.onWebRtcAudioRecordError(errorMessage);
340     }
341   }
342 }
343