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.audio; 12 13 import android.annotation.TargetApi; 14 import android.content.Context; 15 import android.media.AudioDeviceInfo; 16 import android.media.AudioFormat; 17 import android.media.AudioManager; 18 import android.media.AudioRecord; 19 import android.media.AudioRecordingConfiguration; 20 import android.media.AudioTimestamp; 21 import android.media.MediaRecorder.AudioSource; 22 import android.os.Build; 23 import android.os.Process; 24 import androidx.annotation.Nullable; 25 import androidx.annotation.RequiresApi; 26 import java.lang.System; 27 import java.nio.ByteBuffer; 28 import java.util.Arrays; 29 import java.util.Iterator; 30 import java.util.List; 31 import java.util.concurrent.Callable; 32 import java.util.concurrent.Executors; 33 import java.util.concurrent.ScheduledExecutorService; 34 import java.util.concurrent.ScheduledFuture; 35 import java.util.concurrent.ThreadFactory; 36 import java.util.concurrent.TimeUnit; 37 import java.util.concurrent.atomic.AtomicInteger; 38 import java.util.concurrent.atomic.AtomicReference; 39 import org.webrtc.CalledByNative; 40 import org.webrtc.Logging; 41 import org.webrtc.ThreadUtils; 42 import org.webrtc.audio.JavaAudioDeviceModule.AudioRecordErrorCallback; 43 import org.webrtc.audio.JavaAudioDeviceModule.AudioRecordStartErrorCode; 44 import org.webrtc.audio.JavaAudioDeviceModule.AudioRecordStateCallback; 45 import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback; 46 47 class WebRtcAudioRecord { 48 private static final String TAG = "WebRtcAudioRecordExternal"; 49 50 // Requested size of each recorded buffer provided to the client. 51 private static final int CALLBACK_BUFFER_SIZE_MS = 10; 52 53 // Average number of callbacks per second. 54 private static final int BUFFERS_PER_SECOND = 1000 / CALLBACK_BUFFER_SIZE_MS; 55 56 // We ask for a native buffer size of BUFFER_SIZE_FACTOR * (minimum required 57 // buffer size). The extra space is allocated to guard against glitches under 58 // high load. 59 private static final int BUFFER_SIZE_FACTOR = 2; 60 61 // The AudioRecordJavaThread is allowed to wait for successful call to join() 62 // but the wait times out afther this amount of time. 63 private static final long AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS = 2000; 64 65 public static final int DEFAULT_AUDIO_SOURCE = AudioSource.VOICE_COMMUNICATION; 66 67 // Default audio data format is PCM 16 bit per sample. 68 // Guaranteed to be supported by all devices. 69 public static final int DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; 70 71 // Indicates AudioRecord has started recording audio. 72 private static final int AUDIO_RECORD_START = 0; 73 74 // Indicates AudioRecord has stopped recording audio. 75 private static final int AUDIO_RECORD_STOP = 1; 76 77 // Time to wait before checking recording status after start has been called. Tests have 78 // shown that the result can sometimes be invalid (our own status might be missing) if we check 79 // directly after start. 80 private static final int CHECK_REC_STATUS_DELAY_MS = 100; 81 82 private final Context context; 83 private final AudioManager audioManager; 84 private final int audioSource; 85 private final int audioFormat; 86 87 private long nativeAudioRecord; 88 89 private final WebRtcAudioEffects effects = new WebRtcAudioEffects(); 90 91 private @Nullable ByteBuffer byteBuffer; 92 93 private @Nullable AudioRecord audioRecord; 94 private @Nullable AudioRecordThread audioThread; 95 private @Nullable AudioDeviceInfo preferredDevice; 96 97 private final ScheduledExecutorService executor; 98 private @Nullable ScheduledFuture<String> future; 99 100 private volatile boolean microphoneMute; 101 private final AtomicReference<Boolean> audioSourceMatchesRecordingSessionRef = 102 new AtomicReference<>(); 103 private byte[] emptyBytes; 104 105 private final @Nullable AudioRecordErrorCallback errorCallback; 106 private final @Nullable AudioRecordStateCallback stateCallback; 107 private final @Nullable SamplesReadyCallback audioSamplesReadyCallback; 108 private final boolean isAcousticEchoCancelerSupported; 109 private final boolean isNoiseSuppressorSupported; 110 111 /** 112 * Audio thread which keeps calling ByteBuffer.read() waiting for audio 113 * to be recorded. Feeds recorded data to the native counterpart as a 114 * periodic sequence of callbacks using DataIsRecorded(). 115 * This thread uses a Process.THREAD_PRIORITY_URGENT_AUDIO priority. 116 */ 117 private class AudioRecordThread extends Thread { 118 private volatile boolean keepAlive = true; 119 AudioRecordThread(String name)120 public AudioRecordThread(String name) { 121 super(name); 122 } 123 124 @Override run()125 public void run() { 126 Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); 127 Logging.d(TAG, "AudioRecordThread" + WebRtcAudioUtils.getThreadInfo()); 128 assertTrue(audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING); 129 130 // Audio recording has started and the client is informed about it. 131 doAudioRecordStateCallback(AUDIO_RECORD_START); 132 133 long lastTime = System.nanoTime(); 134 AudioTimestamp audioTimestamp = null; 135 if (Build.VERSION.SDK_INT >= 24) { 136 audioTimestamp = new AudioTimestamp(); 137 } 138 while (keepAlive) { 139 int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity()); 140 if (bytesRead == byteBuffer.capacity()) { 141 if (microphoneMute) { 142 byteBuffer.clear(); 143 byteBuffer.put(emptyBytes); 144 } 145 // It's possible we've been shut down during the read, and stopRecording() tried and 146 // failed to join this thread. To be a bit safer, try to avoid calling any native methods 147 // in case they've been unregistered after stopRecording() returned. 148 if (keepAlive) { 149 long captureTimeNs = 0; 150 if (Build.VERSION.SDK_INT >= 24) { 151 if (audioRecord.getTimestamp(audioTimestamp, AudioTimestamp.TIMEBASE_MONOTONIC) 152 == AudioRecord.SUCCESS) { 153 captureTimeNs = audioTimestamp.nanoTime; 154 } 155 } 156 nativeDataIsRecorded(nativeAudioRecord, bytesRead, captureTimeNs); 157 } 158 if (audioSamplesReadyCallback != null) { 159 // Copy the entire byte buffer array. The start of the byteBuffer is not necessarily 160 // at index 0. 161 byte[] data = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.arrayOffset(), 162 byteBuffer.capacity() + byteBuffer.arrayOffset()); 163 audioSamplesReadyCallback.onWebRtcAudioRecordSamplesReady( 164 new JavaAudioDeviceModule.AudioSamples(audioRecord.getAudioFormat(), 165 audioRecord.getChannelCount(), audioRecord.getSampleRate(), data)); 166 } 167 } else { 168 String errorMessage = "AudioRecord.read failed: " + bytesRead; 169 Logging.e(TAG, errorMessage); 170 if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) { 171 keepAlive = false; 172 reportWebRtcAudioRecordError(errorMessage); 173 } 174 } 175 } 176 177 try { 178 if (audioRecord != null) { 179 audioRecord.stop(); 180 doAudioRecordStateCallback(AUDIO_RECORD_STOP); 181 } 182 } catch (IllegalStateException e) { 183 Logging.e(TAG, "AudioRecord.stop failed: " + e.getMessage()); 184 } 185 } 186 187 // Stops the inner thread loop and also calls AudioRecord.stop(). 188 // Does not block the calling thread. stopThread()189 public void stopThread() { 190 Logging.d(TAG, "stopThread"); 191 keepAlive = false; 192 } 193 } 194 195 @CalledByNative WebRtcAudioRecord(Context context, AudioManager audioManager)196 WebRtcAudioRecord(Context context, AudioManager audioManager) { 197 this(context, newDefaultScheduler() /* scheduler */, audioManager, DEFAULT_AUDIO_SOURCE, 198 DEFAULT_AUDIO_FORMAT, null /* errorCallback */, null /* stateCallback */, 199 null /* audioSamplesReadyCallback */, WebRtcAudioEffects.isAcousticEchoCancelerSupported(), 200 WebRtcAudioEffects.isNoiseSuppressorSupported()); 201 } 202 WebRtcAudioRecord(Context context, ScheduledExecutorService scheduler, AudioManager audioManager, int audioSource, int audioFormat, @Nullable AudioRecordErrorCallback errorCallback, @Nullable AudioRecordStateCallback stateCallback, @Nullable SamplesReadyCallback audioSamplesReadyCallback, boolean isAcousticEchoCancelerSupported, boolean isNoiseSuppressorSupported)203 public WebRtcAudioRecord(Context context, ScheduledExecutorService scheduler, 204 AudioManager audioManager, int audioSource, int audioFormat, 205 @Nullable AudioRecordErrorCallback errorCallback, 206 @Nullable AudioRecordStateCallback stateCallback, 207 @Nullable SamplesReadyCallback audioSamplesReadyCallback, 208 boolean isAcousticEchoCancelerSupported, boolean isNoiseSuppressorSupported) { 209 if (isAcousticEchoCancelerSupported && !WebRtcAudioEffects.isAcousticEchoCancelerSupported()) { 210 throw new IllegalArgumentException("HW AEC not supported"); 211 } 212 if (isNoiseSuppressorSupported && !WebRtcAudioEffects.isNoiseSuppressorSupported()) { 213 throw new IllegalArgumentException("HW NS not supported"); 214 } 215 this.context = context; 216 this.executor = scheduler; 217 this.audioManager = audioManager; 218 this.audioSource = audioSource; 219 this.audioFormat = audioFormat; 220 this.errorCallback = errorCallback; 221 this.stateCallback = stateCallback; 222 this.audioSamplesReadyCallback = audioSamplesReadyCallback; 223 this.isAcousticEchoCancelerSupported = isAcousticEchoCancelerSupported; 224 this.isNoiseSuppressorSupported = isNoiseSuppressorSupported; 225 Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo()); 226 } 227 228 @CalledByNative setNativeAudioRecord(long nativeAudioRecord)229 public void setNativeAudioRecord(long nativeAudioRecord) { 230 this.nativeAudioRecord = nativeAudioRecord; 231 } 232 233 @CalledByNative isAcousticEchoCancelerSupported()234 boolean isAcousticEchoCancelerSupported() { 235 return isAcousticEchoCancelerSupported; 236 } 237 238 @CalledByNative isNoiseSuppressorSupported()239 boolean isNoiseSuppressorSupported() { 240 return isNoiseSuppressorSupported; 241 } 242 243 // Returns true if a valid call to verifyAudioConfig() has been done. Should always be 244 // checked before using the returned value of isAudioSourceMatchingRecordingSession(). 245 @CalledByNative isAudioConfigVerified()246 boolean isAudioConfigVerified() { 247 return audioSourceMatchesRecordingSessionRef.get() != null; 248 } 249 250 // Returns true if verifyAudioConfig() succeeds. This value is set after a specific delay when 251 // startRecording() has been called. Hence, should preferably be called in combination with 252 // stopRecording() to ensure that it has been set properly. `isAudioConfigVerified` is 253 // enabled in WebRtcAudioRecord to ensure that the returned value is valid. 254 @CalledByNative isAudioSourceMatchingRecordingSession()255 boolean isAudioSourceMatchingRecordingSession() { 256 Boolean audioSourceMatchesRecordingSession = audioSourceMatchesRecordingSessionRef.get(); 257 if (audioSourceMatchesRecordingSession == null) { 258 Logging.w(TAG, "Audio configuration has not yet been verified"); 259 return false; 260 } 261 return audioSourceMatchesRecordingSession; 262 } 263 264 @CalledByNative enableBuiltInAEC(boolean enable)265 private boolean enableBuiltInAEC(boolean enable) { 266 Logging.d(TAG, "enableBuiltInAEC(" + enable + ")"); 267 return effects.setAEC(enable); 268 } 269 270 @CalledByNative enableBuiltInNS(boolean enable)271 private boolean enableBuiltInNS(boolean enable) { 272 Logging.d(TAG, "enableBuiltInNS(" + enable + ")"); 273 return effects.setNS(enable); 274 } 275 276 @CalledByNative initRecording(int sampleRate, int channels)277 private int initRecording(int sampleRate, int channels) { 278 Logging.d(TAG, "initRecording(sampleRate=" + sampleRate + ", channels=" + channels + ")"); 279 if (audioRecord != null) { 280 reportWebRtcAudioRecordInitError("InitRecording called twice without StopRecording."); 281 return -1; 282 } 283 final int bytesPerFrame = channels * getBytesPerSample(audioFormat); 284 final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND; 285 byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer); 286 if (!(byteBuffer.hasArray())) { 287 reportWebRtcAudioRecordInitError("ByteBuffer does not have backing array."); 288 return -1; 289 } 290 Logging.d(TAG, "byteBuffer.capacity: " + byteBuffer.capacity()); 291 emptyBytes = new byte[byteBuffer.capacity()]; 292 // Rather than passing the ByteBuffer with every callback (requiring 293 // the potentially expensive GetDirectBufferAddress) we simply have the 294 // the native class cache the address to the memory once. 295 nativeCacheDirectBufferAddress(nativeAudioRecord, byteBuffer); 296 297 // Get the minimum buffer size required for the successful creation of 298 // an AudioRecord object, in byte units. 299 // Note that this size doesn't guarantee a smooth recording under load. 300 final int channelConfig = channelCountToConfiguration(channels); 301 int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); 302 if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) { 303 reportWebRtcAudioRecordInitError("AudioRecord.getMinBufferSize failed: " + minBufferSize); 304 return -1; 305 } 306 Logging.d(TAG, "AudioRecord.getMinBufferSize: " + minBufferSize); 307 308 // Use a larger buffer size than the minimum required when creating the 309 // AudioRecord instance to ensure smooth recording under load. It has been 310 // verified that it does not increase the actual recording latency. 311 int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity()); 312 Logging.d(TAG, "bufferSizeInBytes: " + bufferSizeInBytes); 313 try { 314 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 315 // Use the AudioRecord.Builder class on Android M (23) and above. 316 // Throws IllegalArgumentException. 317 audioRecord = createAudioRecordOnMOrHigher( 318 audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes); 319 audioSourceMatchesRecordingSessionRef.set(null); 320 if (preferredDevice != null) { 321 setPreferredDevice(preferredDevice); 322 } 323 } else { 324 // Use the old AudioRecord constructor for API levels below 23. 325 // Throws UnsupportedOperationException. 326 audioRecord = createAudioRecordOnLowerThanM( 327 audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes); 328 audioSourceMatchesRecordingSessionRef.set(null); 329 } 330 } catch (IllegalArgumentException | UnsupportedOperationException e) { 331 // Report of exception message is sufficient. Example: "Cannot create AudioRecord". 332 reportWebRtcAudioRecordInitError(e.getMessage()); 333 releaseAudioResources(); 334 return -1; 335 } 336 if (audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { 337 reportWebRtcAudioRecordInitError("Creation or initialization of audio recorder failed."); 338 releaseAudioResources(); 339 return -1; 340 } 341 effects.enable(audioRecord.getAudioSessionId()); 342 logMainParameters(); 343 logMainParametersExtended(); 344 // Check number of active recording sessions. Should be zero but we have seen conflict cases 345 // and adding a log for it can help us figure out details about conflicting sessions. 346 final int numActiveRecordingSessions = 347 logRecordingConfigurations(audioRecord, false /* verifyAudioConfig */); 348 if (numActiveRecordingSessions != 0) { 349 // Log the conflict as a warning since initialization did in fact succeed. Most likely, the 350 // upcoming call to startRecording() will fail under these conditions. 351 Logging.w( 352 TAG, "Potential microphone conflict. Active sessions: " + numActiveRecordingSessions); 353 } 354 return framesPerBuffer; 355 } 356 357 /** 358 * Prefer a specific {@link AudioDeviceInfo} device for recording. Calling after recording starts 359 * is valid but may cause a temporary interruption if the audio routing changes. 360 */ 361 @RequiresApi(Build.VERSION_CODES.M) 362 @TargetApi(Build.VERSION_CODES.M) setPreferredDevice(@ullable AudioDeviceInfo preferredDevice)363 void setPreferredDevice(@Nullable AudioDeviceInfo preferredDevice) { 364 Logging.d( 365 TAG, "setPreferredDevice " + (preferredDevice != null ? preferredDevice.getId() : null)); 366 this.preferredDevice = preferredDevice; 367 if (audioRecord != null) { 368 if (!audioRecord.setPreferredDevice(preferredDevice)) { 369 Logging.e(TAG, "setPreferredDevice failed"); 370 } 371 } 372 } 373 374 @CalledByNative startRecording()375 private boolean startRecording() { 376 Logging.d(TAG, "startRecording"); 377 assertTrue(audioRecord != null); 378 assertTrue(audioThread == null); 379 try { 380 audioRecord.startRecording(); 381 } catch (IllegalStateException e) { 382 reportWebRtcAudioRecordStartError(AudioRecordStartErrorCode.AUDIO_RECORD_START_EXCEPTION, 383 "AudioRecord.startRecording failed: " + e.getMessage()); 384 return false; 385 } 386 if (audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { 387 reportWebRtcAudioRecordStartError(AudioRecordStartErrorCode.AUDIO_RECORD_START_STATE_MISMATCH, 388 "AudioRecord.startRecording failed - incorrect state: " 389 + audioRecord.getRecordingState()); 390 return false; 391 } 392 audioThread = new AudioRecordThread("AudioRecordJavaThread"); 393 audioThread.start(); 394 scheduleLogRecordingConfigurationsTask(audioRecord); 395 return true; 396 } 397 398 @CalledByNative stopRecording()399 private boolean stopRecording() { 400 Logging.d(TAG, "stopRecording"); 401 assertTrue(audioThread != null); 402 if (future != null) { 403 if (!future.isDone()) { 404 // Might be needed if the client calls startRecording(), stopRecording() back-to-back. 405 future.cancel(true /* mayInterruptIfRunning */); 406 } 407 future = null; 408 } 409 audioThread.stopThread(); 410 if (!ThreadUtils.joinUninterruptibly(audioThread, AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS)) { 411 Logging.e(TAG, "Join of AudioRecordJavaThread timed out"); 412 WebRtcAudioUtils.logAudioState(TAG, context, audioManager); 413 } 414 audioThread = null; 415 effects.release(); 416 releaseAudioResources(); 417 return true; 418 } 419 420 @TargetApi(Build.VERSION_CODES.M) createAudioRecordOnMOrHigher( int audioSource, int sampleRate, int channelConfig, int audioFormat, int bufferSizeInBytes)421 private static AudioRecord createAudioRecordOnMOrHigher( 422 int audioSource, int sampleRate, int channelConfig, int audioFormat, int bufferSizeInBytes) { 423 Logging.d(TAG, "createAudioRecordOnMOrHigher"); 424 return new AudioRecord.Builder() 425 .setAudioSource(audioSource) 426 .setAudioFormat(new AudioFormat.Builder() 427 .setEncoding(audioFormat) 428 .setSampleRate(sampleRate) 429 .setChannelMask(channelConfig) 430 .build()) 431 .setBufferSizeInBytes(bufferSizeInBytes) 432 .build(); 433 } 434 createAudioRecordOnLowerThanM( int audioSource, int sampleRate, int channelConfig, int audioFormat, int bufferSizeInBytes)435 private static AudioRecord createAudioRecordOnLowerThanM( 436 int audioSource, int sampleRate, int channelConfig, int audioFormat, int bufferSizeInBytes) { 437 Logging.d(TAG, "createAudioRecordOnLowerThanM"); 438 return new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes); 439 } 440 logMainParameters()441 private void logMainParameters() { 442 Logging.d(TAG, 443 "AudioRecord: " 444 + "session ID: " + audioRecord.getAudioSessionId() + ", " 445 + "channels: " + audioRecord.getChannelCount() + ", " 446 + "sample rate: " + audioRecord.getSampleRate()); 447 } 448 449 @TargetApi(Build.VERSION_CODES.M) logMainParametersExtended()450 private void logMainParametersExtended() { 451 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 452 Logging.d(TAG, 453 "AudioRecord: " 454 // The frame count of the native AudioRecord buffer. 455 + "buffer size in frames: " + audioRecord.getBufferSizeInFrames()); 456 } 457 } 458 459 @TargetApi(Build.VERSION_CODES.N) 460 // Checks the number of active recording sessions and logs the states of all active sessions. 461 // Returns number of active sessions. Note that this could occur on arbituary thread. logRecordingConfigurations(AudioRecord audioRecord, boolean verifyAudioConfig)462 private int logRecordingConfigurations(AudioRecord audioRecord, boolean verifyAudioConfig) { 463 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { 464 Logging.w(TAG, "AudioManager#getActiveRecordingConfigurations() requires N or higher"); 465 return 0; 466 } 467 if (audioRecord == null) { 468 return 0; 469 } 470 471 // Get a list of the currently active audio recording configurations of the device (can be more 472 // than one). An empty list indicates there is no recording active when queried. 473 List<AudioRecordingConfiguration> configs = audioManager.getActiveRecordingConfigurations(); 474 final int numActiveRecordingSessions = configs.size(); 475 Logging.d(TAG, "Number of active recording sessions: " + numActiveRecordingSessions); 476 if (numActiveRecordingSessions > 0) { 477 logActiveRecordingConfigs(audioRecord.getAudioSessionId(), configs); 478 if (verifyAudioConfig) { 479 // Run an extra check to verify that the existing audio source doing the recording (tied 480 // to the AudioRecord instance) is matching what the audio recording configuration lists 481 // as its client parameters. If these do not match, recording might work but under invalid 482 // conditions. 483 audioSourceMatchesRecordingSessionRef.set( 484 verifyAudioConfig(audioRecord.getAudioSource(), audioRecord.getAudioSessionId(), 485 audioRecord.getFormat(), audioRecord.getRoutedDevice(), configs)); 486 } 487 } 488 return numActiveRecordingSessions; 489 } 490 491 // Helper method which throws an exception when an assertion has failed. assertTrue(boolean condition)492 private static void assertTrue(boolean condition) { 493 if (!condition) { 494 throw new AssertionError("Expected condition to be true"); 495 } 496 } 497 channelCountToConfiguration(int channels)498 private int channelCountToConfiguration(int channels) { 499 return (channels == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO); 500 } 501 nativeCacheDirectBufferAddress( long nativeAudioRecordJni, ByteBuffer byteBuffer)502 private native void nativeCacheDirectBufferAddress( 503 long nativeAudioRecordJni, ByteBuffer byteBuffer); nativeDataIsRecorded( long nativeAudioRecordJni, int bytes, long captureTimestampNs)504 private native void nativeDataIsRecorded( 505 long nativeAudioRecordJni, int bytes, long captureTimestampNs); 506 507 // Sets all recorded samples to zero if `mute` is true, i.e., ensures that 508 // the microphone is muted. setMicrophoneMute(boolean mute)509 public void setMicrophoneMute(boolean mute) { 510 Logging.w(TAG, "setMicrophoneMute(" + mute + ")"); 511 microphoneMute = mute; 512 } 513 514 // Releases the native AudioRecord resources. releaseAudioResources()515 private void releaseAudioResources() { 516 Logging.d(TAG, "releaseAudioResources"); 517 if (audioRecord != null) { 518 audioRecord.release(); 519 audioRecord = null; 520 } 521 audioSourceMatchesRecordingSessionRef.set(null); 522 } 523 reportWebRtcAudioRecordInitError(String errorMessage)524 private void reportWebRtcAudioRecordInitError(String errorMessage) { 525 Logging.e(TAG, "Init recording error: " + errorMessage); 526 WebRtcAudioUtils.logAudioState(TAG, context, audioManager); 527 logRecordingConfigurations(audioRecord, false /* verifyAudioConfig */); 528 if (errorCallback != null) { 529 errorCallback.onWebRtcAudioRecordInitError(errorMessage); 530 } 531 } 532 reportWebRtcAudioRecordStartError( AudioRecordStartErrorCode errorCode, String errorMessage)533 private void reportWebRtcAudioRecordStartError( 534 AudioRecordStartErrorCode errorCode, String errorMessage) { 535 Logging.e(TAG, "Start recording error: " + errorCode + ". " + errorMessage); 536 WebRtcAudioUtils.logAudioState(TAG, context, audioManager); 537 logRecordingConfigurations(audioRecord, false /* verifyAudioConfig */); 538 if (errorCallback != null) { 539 errorCallback.onWebRtcAudioRecordStartError(errorCode, errorMessage); 540 } 541 } 542 reportWebRtcAudioRecordError(String errorMessage)543 private void reportWebRtcAudioRecordError(String errorMessage) { 544 Logging.e(TAG, "Run-time recording error: " + errorMessage); 545 WebRtcAudioUtils.logAudioState(TAG, context, audioManager); 546 if (errorCallback != null) { 547 errorCallback.onWebRtcAudioRecordError(errorMessage); 548 } 549 } 550 doAudioRecordStateCallback(int audioState)551 private void doAudioRecordStateCallback(int audioState) { 552 Logging.d(TAG, "doAudioRecordStateCallback: " + audioStateToString(audioState)); 553 if (stateCallback != null) { 554 if (audioState == WebRtcAudioRecord.AUDIO_RECORD_START) { 555 stateCallback.onWebRtcAudioRecordStart(); 556 } else if (audioState == WebRtcAudioRecord.AUDIO_RECORD_STOP) { 557 stateCallback.onWebRtcAudioRecordStop(); 558 } else { 559 Logging.e(TAG, "Invalid audio state"); 560 } 561 } 562 } 563 564 // Reference from Android code, AudioFormat.getBytesPerSample. BitPerSample / 8 565 // Default audio data format is PCM 16 bits per sample. 566 // Guaranteed to be supported by all devices getBytesPerSample(int audioFormat)567 private static int getBytesPerSample(int audioFormat) { 568 switch (audioFormat) { 569 case AudioFormat.ENCODING_PCM_8BIT: 570 return 1; 571 case AudioFormat.ENCODING_PCM_16BIT: 572 case AudioFormat.ENCODING_IEC61937: 573 case AudioFormat.ENCODING_DEFAULT: 574 return 2; 575 case AudioFormat.ENCODING_PCM_FLOAT: 576 return 4; 577 case AudioFormat.ENCODING_INVALID: 578 default: 579 throw new IllegalArgumentException("Bad audio format " + audioFormat); 580 } 581 } 582 583 // Use an ExecutorService to schedule a task after a given delay where the task consists of 584 // checking (by logging) the current status of active recording sessions. scheduleLogRecordingConfigurationsTask(AudioRecord audioRecord)585 private void scheduleLogRecordingConfigurationsTask(AudioRecord audioRecord) { 586 Logging.d(TAG, "scheduleLogRecordingConfigurationsTask"); 587 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { 588 return; 589 } 590 591 Callable<String> callable = () -> { 592 if (this.audioRecord == audioRecord) { 593 logRecordingConfigurations(audioRecord, true /* verifyAudioConfig */); 594 } else { 595 Logging.d(TAG, "audio record has changed"); 596 } 597 return "Scheduled task is done"; 598 }; 599 600 if (future != null && !future.isDone()) { 601 future.cancel(true /* mayInterruptIfRunning */); 602 } 603 // Schedule call to logRecordingConfigurations() from executor thread after fixed delay. 604 future = executor.schedule(callable, CHECK_REC_STATUS_DELAY_MS, TimeUnit.MILLISECONDS); 605 }; 606 607 @TargetApi(Build.VERSION_CODES.N) logActiveRecordingConfigs( int session, List<AudioRecordingConfiguration> configs)608 private static boolean logActiveRecordingConfigs( 609 int session, List<AudioRecordingConfiguration> configs) { 610 assertTrue(!configs.isEmpty()); 611 final Iterator<AudioRecordingConfiguration> it = configs.iterator(); 612 Logging.d(TAG, "AudioRecordingConfigurations: "); 613 while (it.hasNext()) { 614 final AudioRecordingConfiguration config = it.next(); 615 StringBuilder conf = new StringBuilder(); 616 // The audio source selected by the client. 617 final int audioSource = config.getClientAudioSource(); 618 conf.append(" client audio source=") 619 .append(WebRtcAudioUtils.audioSourceToString(audioSource)) 620 .append(", client session id=") 621 .append(config.getClientAudioSessionId()) 622 // Compare with our own id (based on AudioRecord#getAudioSessionId()). 623 .append(" (") 624 .append(session) 625 .append(")") 626 .append("\n"); 627 // Audio format at which audio is recorded on this Android device. Note that it may differ 628 // from the client application recording format (see getClientFormat()). 629 AudioFormat format = config.getFormat(); 630 conf.append(" Device AudioFormat: ") 631 .append("channel count=") 632 .append(format.getChannelCount()) 633 .append(", channel index mask=") 634 .append(format.getChannelIndexMask()) 635 // Only AudioFormat#CHANNEL_IN_MONO is guaranteed to work on all devices. 636 .append(", channel mask=") 637 .append(WebRtcAudioUtils.channelMaskToString(format.getChannelMask())) 638 .append(", encoding=") 639 .append(WebRtcAudioUtils.audioEncodingToString(format.getEncoding())) 640 .append(", sample rate=") 641 .append(format.getSampleRate()) 642 .append("\n"); 643 // Audio format at which the client application is recording audio. 644 format = config.getClientFormat(); 645 conf.append(" Client AudioFormat: ") 646 .append("channel count=") 647 .append(format.getChannelCount()) 648 .append(", channel index mask=") 649 .append(format.getChannelIndexMask()) 650 // Only AudioFormat#CHANNEL_IN_MONO is guaranteed to work on all devices. 651 .append(", channel mask=") 652 .append(WebRtcAudioUtils.channelMaskToString(format.getChannelMask())) 653 .append(", encoding=") 654 .append(WebRtcAudioUtils.audioEncodingToString(format.getEncoding())) 655 .append(", sample rate=") 656 .append(format.getSampleRate()) 657 .append("\n"); 658 // Audio input device used for this recording session. 659 final AudioDeviceInfo device = config.getAudioDevice(); 660 if (device != null) { 661 assertTrue(device.isSource()); 662 conf.append(" AudioDevice: ") 663 .append("type=") 664 .append(WebRtcAudioUtils.deviceTypeToString(device.getType())) 665 .append(", id=") 666 .append(device.getId()); 667 } 668 Logging.d(TAG, conf.toString()); 669 } 670 return true; 671 } 672 673 // Verify that the client audio configuration (device and format) matches the requested 674 // configuration (same as AudioRecord's). 675 @TargetApi(Build.VERSION_CODES.N) verifyAudioConfig(int source, int session, AudioFormat format, AudioDeviceInfo device, List<AudioRecordingConfiguration> configs)676 private static boolean verifyAudioConfig(int source, int session, AudioFormat format, 677 AudioDeviceInfo device, List<AudioRecordingConfiguration> configs) { 678 assertTrue(!configs.isEmpty()); 679 final Iterator<AudioRecordingConfiguration> it = configs.iterator(); 680 while (it.hasNext()) { 681 final AudioRecordingConfiguration config = it.next(); 682 final AudioDeviceInfo configDevice = config.getAudioDevice(); 683 if (configDevice == null) { 684 continue; 685 } 686 if ((config.getClientAudioSource() == source) 687 && (config.getClientAudioSessionId() == session) 688 // Check the client format (should match the format of the AudioRecord instance). 689 && (config.getClientFormat().getEncoding() == format.getEncoding()) 690 && (config.getClientFormat().getSampleRate() == format.getSampleRate()) 691 && (config.getClientFormat().getChannelMask() == format.getChannelMask()) 692 && (config.getClientFormat().getChannelIndexMask() == format.getChannelIndexMask()) 693 // Ensure that the device format is properly configured. 694 && (config.getFormat().getEncoding() != AudioFormat.ENCODING_INVALID) 695 && (config.getFormat().getSampleRate() > 0) 696 // For the channel mask, either the position or index-based value must be valid. 697 && ((config.getFormat().getChannelMask() != AudioFormat.CHANNEL_INVALID) 698 || (config.getFormat().getChannelIndexMask() != AudioFormat.CHANNEL_INVALID)) 699 && checkDeviceMatch(configDevice, device)) { 700 Logging.d(TAG, "verifyAudioConfig: PASS"); 701 return true; 702 } 703 } 704 Logging.e(TAG, "verifyAudioConfig: FAILED"); 705 return false; 706 } 707 708 @TargetApi(Build.VERSION_CODES.N) 709 // Returns true if device A parameters matches those of device B. 710 // TODO(henrika): can be improved by adding AudioDeviceInfo#getAddress() but it requires API 29. checkDeviceMatch(AudioDeviceInfo devA, AudioDeviceInfo devB)711 private static boolean checkDeviceMatch(AudioDeviceInfo devA, AudioDeviceInfo devB) { 712 return ((devA.getId() == devB.getId() && (devA.getType() == devB.getType()))); 713 } 714 audioStateToString(int state)715 private static String audioStateToString(int state) { 716 switch (state) { 717 case WebRtcAudioRecord.AUDIO_RECORD_START: 718 return "START"; 719 case WebRtcAudioRecord.AUDIO_RECORD_STOP: 720 return "STOP"; 721 default: 722 return "INVALID"; 723 } 724 } 725 726 private static final AtomicInteger nextSchedulerId = new AtomicInteger(0); 727 newDefaultScheduler()728 static ScheduledExecutorService newDefaultScheduler() { 729 AtomicInteger nextThreadId = new AtomicInteger(0); 730 return Executors.newScheduledThreadPool(0, new ThreadFactory() { 731 /** 732 * Constructs a new {@code Thread} 733 */ 734 @Override 735 public Thread newThread(Runnable r) { 736 Thread thread = Executors.defaultThreadFactory().newThread(r); 737 thread.setName(String.format("WebRtcAudioRecordScheduler-%s-%s", 738 nextSchedulerId.getAndIncrement(), nextThreadId.getAndIncrement())); 739 return thread; 740 } 741 }); 742 } 743 } 744