• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.media.AudioFormat;
14 import android.media.AudioRecord;
15 import android.media.MediaRecorder.AudioSource;
16 import android.os.Build;
17 import android.os.Process;
18 import android.support.annotation.Nullable;
19 import java.lang.System;
20 import java.nio.ByteBuffer;
21 import java.util.Arrays;
22 import java.util.concurrent.TimeUnit;
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 @Nullable WebRtcAudioEffects effects;
56 
57   private ByteBuffer byteBuffer;
58 
59   private @Nullable AudioRecord audioRecord;
60   private @Nullable AudioRecordThread audioThread;
61 
62   private static volatile boolean microphoneMute;
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 @Nullable WebRtcAudioRecordErrorCallback errorCallback;
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    * Contains audio sample information. Object is passed using {@link
86    * WebRtcAudioRecord.WebRtcAudioRecordSamplesReadyCallback}
87    */
88   public static class AudioSamples {
89     /** See {@link AudioRecord#getAudioFormat()} */
90     private final int audioFormat;
91     /** See {@link AudioRecord#getChannelCount()} */
92     private final int channelCount;
93     /** See {@link AudioRecord#getSampleRate()} */
94     private final int sampleRate;
95 
96     private final byte[] data;
97 
AudioSamples(AudioRecord audioRecord, byte[] data)98     private AudioSamples(AudioRecord audioRecord, byte[] data) {
99       this.audioFormat = audioRecord.getAudioFormat();
100       this.channelCount = audioRecord.getChannelCount();
101       this.sampleRate = audioRecord.getSampleRate();
102       this.data = data;
103     }
104 
getAudioFormat()105     public int getAudioFormat() {
106       return audioFormat;
107     }
108 
getChannelCount()109     public int getChannelCount() {
110       return channelCount;
111     }
112 
getSampleRate()113     public int getSampleRate() {
114       return sampleRate;
115     }
116 
getData()117     public byte[] getData() {
118       return data;
119     }
120   }
121 
122   /** Called when new audio samples are ready. This should only be set for debug purposes */
123   public static interface WebRtcAudioRecordSamplesReadyCallback {
onWebRtcAudioRecordSamplesReady(AudioSamples samples)124     void onWebRtcAudioRecordSamplesReady(AudioSamples samples);
125   }
126 
127   private static @Nullable WebRtcAudioRecordSamplesReadyCallback audioSamplesReadyCallback;
128 
setOnAudioSamplesReady(WebRtcAudioRecordSamplesReadyCallback callback)129   public static void setOnAudioSamplesReady(WebRtcAudioRecordSamplesReadyCallback callback) {
130     audioSamplesReadyCallback = callback;
131   }
132 
133   /**
134    * Audio thread which keeps calling ByteBuffer.read() waiting for audio
135    * to be recorded. Feeds recorded data to the native counterpart as a
136    * periodic sequence of callbacks using DataIsRecorded().
137    * This thread uses a Process.THREAD_PRIORITY_URGENT_AUDIO priority.
138    */
139   private class AudioRecordThread extends Thread {
140     private volatile boolean keepAlive = true;
141 
AudioRecordThread(String name)142     public AudioRecordThread(String name) {
143       super(name);
144     }
145 
146     // TODO(titovartem) make correct fix during webrtc:9175
147     @SuppressWarnings("ByteBufferBackingArray")
148     @Override
run()149     public void run() {
150       Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
151       Logging.d(TAG, "AudioRecordThread" + WebRtcAudioUtils.getThreadInfo());
152       assertTrue(audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING);
153 
154       long lastTime = System.nanoTime();
155       while (keepAlive) {
156         int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity());
157         if (bytesRead == byteBuffer.capacity()) {
158           if (microphoneMute) {
159             byteBuffer.clear();
160             byteBuffer.put(emptyBytes);
161           }
162           // It's possible we've been shut down during the read, and stopRecording() tried and
163           // failed to join this thread. To be a bit safer, try to avoid calling any native methods
164           // in case they've been unregistered after stopRecording() returned.
165           if (keepAlive) {
166             nativeDataIsRecorded(bytesRead, nativeAudioRecord);
167           }
168           if (audioSamplesReadyCallback != null) {
169             // Copy the entire byte buffer array.  Assume that the start of the byteBuffer is
170             // at index 0.
171             byte[] data = Arrays.copyOf(byteBuffer.array(), byteBuffer.capacity());
172             audioSamplesReadyCallback.onWebRtcAudioRecordSamplesReady(
173                 new AudioSamples(audioRecord, data));
174           }
175         } else {
176           String errorMessage = "AudioRecord.read failed: " + bytesRead;
177           Logging.e(TAG, errorMessage);
178           if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
179             keepAlive = false;
180             reportWebRtcAudioRecordError(errorMessage);
181           }
182         }
183         if (DEBUG) {
184           long nowTime = System.nanoTime();
185           long durationInMs = TimeUnit.NANOSECONDS.toMillis((nowTime - lastTime));
186           lastTime = nowTime;
187           Logging.d(TAG, "bytesRead[" + durationInMs + "] " + bytesRead);
188         }
189       }
190 
191       try {
192         if (audioRecord != null) {
193           audioRecord.stop();
194         }
195       } catch (IllegalStateException e) {
196         Logging.e(TAG, "AudioRecord.stop failed: " + e.getMessage());
197       }
198     }
199 
200     // Stops the inner thread loop and also calls AudioRecord.stop().
201     // Does not block the calling thread.
stopThread()202     public void stopThread() {
203       Logging.d(TAG, "stopThread");
204       keepAlive = false;
205     }
206   }
207 
WebRtcAudioRecord(long nativeAudioRecord)208   WebRtcAudioRecord(long nativeAudioRecord) {
209     Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo());
210     this.nativeAudioRecord = nativeAudioRecord;
211     if (DEBUG) {
212       WebRtcAudioUtils.logDeviceInfo(TAG);
213     }
214     effects = WebRtcAudioEffects.create();
215   }
216 
enableBuiltInAEC(boolean enable)217   private boolean enableBuiltInAEC(boolean enable) {
218     Logging.d(TAG, "enableBuiltInAEC(" + enable + ')');
219     if (effects == null) {
220       Logging.e(TAG, "Built-in AEC is not supported on this platform");
221       return false;
222     }
223     return effects.setAEC(enable);
224   }
225 
enableBuiltInNS(boolean enable)226   private boolean enableBuiltInNS(boolean enable) {
227     Logging.d(TAG, "enableBuiltInNS(" + enable + ')');
228     if (effects == null) {
229       Logging.e(TAG, "Built-in NS is not supported on this platform");
230       return false;
231     }
232     return effects.setNS(enable);
233   }
234 
initRecording(int sampleRate, int channels)235   private int initRecording(int sampleRate, int channels) {
236     Logging.d(TAG, "initRecording(sampleRate=" + sampleRate + ", channels=" + channels + ")");
237     if (audioRecord != null) {
238       reportWebRtcAudioRecordInitError("InitRecording called twice without StopRecording.");
239       return -1;
240     }
241     final int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8);
242     final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND;
243     byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);
244     Logging.d(TAG, "byteBuffer.capacity: " + byteBuffer.capacity());
245     emptyBytes = new byte[byteBuffer.capacity()];
246     // Rather than passing the ByteBuffer with every callback (requiring
247     // the potentially expensive GetDirectBufferAddress) we simply have the
248     // the native class cache the address to the memory once.
249     nativeCacheDirectBufferAddress(byteBuffer, nativeAudioRecord);
250 
251     // Get the minimum buffer size required for the successful creation of
252     // an AudioRecord object, in byte units.
253     // Note that this size doesn't guarantee a smooth recording under load.
254     final int channelConfig = channelCountToConfiguration(channels);
255     int minBufferSize =
256         AudioRecord.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
257     if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
258       reportWebRtcAudioRecordInitError("AudioRecord.getMinBufferSize failed: " + minBufferSize);
259       return -1;
260     }
261     Logging.d(TAG, "AudioRecord.getMinBufferSize: " + minBufferSize);
262 
263     // Use a larger buffer size than the minimum required when creating the
264     // AudioRecord instance to ensure smooth recording under load. It has been
265     // verified that it does not increase the actual recording latency.
266     int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity());
267     Logging.d(TAG, "bufferSizeInBytes: " + bufferSizeInBytes);
268     try {
269       audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig,
270           AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes);
271     } catch (IllegalArgumentException e) {
272       reportWebRtcAudioRecordInitError("AudioRecord ctor error: " + e.getMessage());
273       releaseAudioResources();
274       return -1;
275     }
276     if (audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
277       reportWebRtcAudioRecordInitError("Failed to create a new AudioRecord instance");
278       releaseAudioResources();
279       return -1;
280     }
281     if (effects != null) {
282       effects.enable(audioRecord.getAudioSessionId());
283     }
284     logMainParameters();
285     logMainParametersExtended();
286     return framesPerBuffer;
287   }
288 
startRecording()289   private boolean startRecording() {
290     Logging.d(TAG, "startRecording");
291     assertTrue(audioRecord != null);
292     assertTrue(audioThread == null);
293     try {
294       audioRecord.startRecording();
295     } catch (IllegalStateException e) {
296       reportWebRtcAudioRecordStartError(AudioRecordStartErrorCode.AUDIO_RECORD_START_EXCEPTION,
297           "AudioRecord.startRecording failed: " + e.getMessage());
298       return false;
299     }
300     if (audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
301       reportWebRtcAudioRecordStartError(
302           AudioRecordStartErrorCode.AUDIO_RECORD_START_STATE_MISMATCH,
303           "AudioRecord.startRecording failed - incorrect state :"
304           + audioRecord.getRecordingState());
305       return false;
306     }
307     audioThread = new AudioRecordThread("AudioRecordJavaThread");
308     audioThread.start();
309     return true;
310   }
311 
stopRecording()312   private boolean stopRecording() {
313     Logging.d(TAG, "stopRecording");
314     assertTrue(audioThread != null);
315     audioThread.stopThread();
316     if (!ThreadUtils.joinUninterruptibly(audioThread, AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS)) {
317       Logging.e(TAG, "Join of AudioRecordJavaThread timed out");
318       WebRtcAudioUtils.logAudioState(TAG);
319     }
320     audioThread = null;
321     if (effects != null) {
322       effects.release();
323     }
324     releaseAudioResources();
325     return true;
326   }
327 
logMainParameters()328   private void logMainParameters() {
329     Logging.d(TAG, "AudioRecord: "
330             + "session ID: " + audioRecord.getAudioSessionId() + ", "
331             + "channels: " + audioRecord.getChannelCount() + ", "
332             + "sample rate: " + audioRecord.getSampleRate());
333   }
334 
logMainParametersExtended()335   private void logMainParametersExtended() {
336     if (Build.VERSION.SDK_INT >= 23) {
337       Logging.d(TAG, "AudioRecord: "
338               // The frame count of the native AudioRecord buffer.
339               + "buffer size in frames: " + audioRecord.getBufferSizeInFrames());
340     }
341   }
342 
343   // Helper method which throws an exception  when an assertion has failed.
assertTrue(boolean condition)344   private static void assertTrue(boolean condition) {
345     if (!condition) {
346       throw new AssertionError("Expected condition to be true");
347     }
348   }
349 
channelCountToConfiguration(int channels)350   private int channelCountToConfiguration(int channels) {
351     return (channels == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO);
352   }
353 
nativeCacheDirectBufferAddress(ByteBuffer byteBuffer, long nativeAudioRecord)354   private native void nativeCacheDirectBufferAddress(ByteBuffer byteBuffer, long nativeAudioRecord);
355 
nativeDataIsRecorded(int bytes, long nativeAudioRecord)356   private native void nativeDataIsRecorded(int bytes, long nativeAudioRecord);
357 
358   @SuppressWarnings("NoSynchronizedMethodCheck")
setAudioSource(int source)359   public static synchronized void setAudioSource(int source) {
360     Logging.w(TAG, "Audio source is changed from: " + audioSource
361             + " to " + source);
362     audioSource = source;
363   }
364 
getDefaultAudioSource()365   private static int getDefaultAudioSource() {
366     return AudioSource.VOICE_COMMUNICATION;
367   }
368 
369   // Sets all recorded samples to zero if |mute| is true, i.e., ensures that
370   // the microphone is muted.
setMicrophoneMute(boolean mute)371   public static void setMicrophoneMute(boolean mute) {
372     Logging.w(TAG, "setMicrophoneMute(" + mute + ")");
373     microphoneMute = mute;
374   }
375 
376   // Releases the native AudioRecord resources.
releaseAudioResources()377   private void releaseAudioResources() {
378     Logging.d(TAG, "releaseAudioResources");
379     if (audioRecord != null) {
380       audioRecord.release();
381       audioRecord = null;
382     }
383   }
384 
reportWebRtcAudioRecordInitError(String errorMessage)385   private void reportWebRtcAudioRecordInitError(String errorMessage) {
386     Logging.e(TAG, "Init recording error: " + errorMessage);
387     WebRtcAudioUtils.logAudioState(TAG);
388     if (errorCallback != null) {
389       errorCallback.onWebRtcAudioRecordInitError(errorMessage);
390     }
391   }
392 
reportWebRtcAudioRecordStartError( AudioRecordStartErrorCode errorCode, String errorMessage)393   private void reportWebRtcAudioRecordStartError(
394       AudioRecordStartErrorCode errorCode, String errorMessage) {
395     Logging.e(TAG, "Start recording error: " + errorCode + ". " + errorMessage);
396     WebRtcAudioUtils.logAudioState(TAG);
397     if (errorCallback != null) {
398       errorCallback.onWebRtcAudioRecordStartError(errorCode, errorMessage);
399     }
400   }
401 
reportWebRtcAudioRecordError(String errorMessage)402   private void reportWebRtcAudioRecordError(String errorMessage) {
403     Logging.e(TAG, "Run-time recording error: " + errorMessage);
404     WebRtcAudioUtils.logAudioState(TAG);
405     if (errorCallback != null) {
406       errorCallback.onWebRtcAudioRecordError(errorMessage);
407     }
408   }
409 }
410