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