xref: /aosp_15_r20/external/webrtc/sdk/android/src/java/org/webrtc/audio/WebRtcAudioRecord.java (revision d9f758449e529ab9291ac668be2861e7a55c2422)
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