/* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ package org.webrtc.audio; import android.annotation.TargetApi; import android.content.Context; import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; import android.media.AudioRecordingConfiguration; import android.media.AudioTimestamp; import android.media.MediaRecorder.AudioSource; import android.os.Build; import android.os.Process; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.util.Log; import java.lang.System; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.webrtc.CalledByNative; import org.webrtc.Logging; import org.webrtc.ThreadUtils; import org.webrtc.audio.JavaAudioDeviceModule.AudioRecordErrorCallback; import org.webrtc.audio.JavaAudioDeviceModule.AudioRecordStartErrorCode; import org.webrtc.audio.JavaAudioDeviceModule.AudioRecordStateCallback; import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback; class WebRtcAudioRecord { private static final String TAG = "WebRtcAudioRecordExternal"; // Requested size of each recorded buffer provided to the client. private static final int CALLBACK_BUFFER_SIZE_MS = 10; // Average number of callbacks per second. private static final int BUFFERS_PER_SECOND = 1000 / CALLBACK_BUFFER_SIZE_MS; // We ask for a native buffer size of BUFFER_SIZE_FACTOR * (minimum required // buffer size). The extra space is allocated to guard against glitches under // high load. private static final int BUFFER_SIZE_FACTOR = 2; // The AudioRecordJavaThread is allowed to wait for successful call to join() // but the wait times out afther this amount of time. private static final long AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS = 2000; public static final int DEFAULT_AUDIO_SOURCE = AudioSource.VOICE_COMMUNICATION; // Default audio data format is PCM 16 bit per sample. // Guaranteed to be supported by all devices. public static final int DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; // Indicates AudioRecord has started recording audio. private static final int AUDIO_RECORD_START = 0; // Indicates AudioRecord has stopped recording audio. private static final int AUDIO_RECORD_STOP = 1; // Time to wait before checking recording status after start has been called. Tests have // shown that the result can sometimes be invalid (our own status might be missing) if we check // directly after start. private static final int CHECK_REC_STATUS_DELAY_MS = 100; private final Context context; private final AudioManager audioManager; private final int audioSource; private final int audioFormat; private long nativeAudioRecord; private final WebRtcAudioEffects effects = new WebRtcAudioEffects(); private @Nullable ByteBuffer byteBuffer; private @Nullable AudioRecord audioRecord; private @Nullable AudioRecordThread audioThread; private @Nullable AudioDeviceInfo preferredDevice; private final ScheduledExecutorService executor; private @Nullable ScheduledFuture future; private volatile boolean microphoneMute; private final AtomicReference audioSourceMatchesRecordingSessionRef = new AtomicReference<>(); private byte[] emptyBytes; private final @Nullable AudioRecordErrorCallback errorCallback; private final @Nullable AudioRecordStateCallback stateCallback; private final @Nullable SamplesReadyCallback audioSamplesReadyCallback; private final boolean isAcousticEchoCancelerSupported; private final boolean isNoiseSuppressorSupported; /** * Audio thread which keeps calling ByteBuffer.read() waiting for audio * to be recorded. Feeds recorded data to the native counterpart as a * periodic sequence of callbacks using DataIsRecorded(). * This thread uses a Process.THREAD_PRIORITY_URGENT_AUDIO priority. */ private class AudioRecordThread extends Thread { private volatile boolean keepAlive = true; public AudioRecordThread(String name) { super(name); } @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); Logging.d(TAG, "AudioRecordThread" + WebRtcAudioUtils.getThreadInfo()); assertTrue(audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING); // Audio recording has started and the client is informed about it. doAudioRecordStateCallback(AUDIO_RECORD_START); long lastTime = System.nanoTime(); while (keepAlive) { int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity()); if (bytesRead == byteBuffer.capacity()) { if (microphoneMute) { byteBuffer.clear(); byteBuffer.put(emptyBytes); } // It's possible we've been shut down during the read, and stopRecording() tried and // failed to join this thread. To be a bit safer, try to avoid calling any native methods // in case they've been unregistered after stopRecording() returned. if (keepAlive) { nativeDataIsRecorded(nativeAudioRecord, bytesRead); } if (audioSamplesReadyCallback != null) { // Copy the entire byte buffer array. The start of the byteBuffer is not necessarily // at index 0. byte[] data = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.capacity() + byteBuffer.arrayOffset()); audioSamplesReadyCallback.onWebRtcAudioRecordSamplesReady( new JavaAudioDeviceModule.AudioSamples(audioRecord.getAudioFormat(), audioRecord.getChannelCount(), audioRecord.getSampleRate(), data)); } } else { String errorMessage = "AudioRecord.read failed: " + bytesRead; Logging.e(TAG, errorMessage); if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) { keepAlive = false; reportWebRtcAudioRecordError(errorMessage); } } } try { if (audioRecord != null) { audioRecord.stop(); doAudioRecordStateCallback(AUDIO_RECORD_STOP); } } catch (IllegalStateException e) { Logging.e(TAG, "AudioRecord.stop failed: " + e.getMessage()); } } // Stops the inner thread loop and also calls AudioRecord.stop(). // Does not block the calling thread. public void stopThread() { Logging.d(TAG, "stopThread"); keepAlive = false; } } @CalledByNative WebRtcAudioRecord(Context context, AudioManager audioManager) { this(context, newDefaultScheduler() /* scheduler */, audioManager, DEFAULT_AUDIO_SOURCE, DEFAULT_AUDIO_FORMAT, null /* errorCallback */, null /* stateCallback */, null /* audioSamplesReadyCallback */, WebRtcAudioEffects.isAcousticEchoCancelerSupported(), WebRtcAudioEffects.isNoiseSuppressorSupported()); } public WebRtcAudioRecord(Context context, ScheduledExecutorService scheduler, AudioManager audioManager, int audioSource, int audioFormat, @Nullable AudioRecordErrorCallback errorCallback, @Nullable AudioRecordStateCallback stateCallback, @Nullable SamplesReadyCallback audioSamplesReadyCallback, boolean isAcousticEchoCancelerSupported, boolean isNoiseSuppressorSupported) { if (isAcousticEchoCancelerSupported && !WebRtcAudioEffects.isAcousticEchoCancelerSupported()) { throw new IllegalArgumentException("HW AEC not supported"); } if (isNoiseSuppressorSupported && !WebRtcAudioEffects.isNoiseSuppressorSupported()) { throw new IllegalArgumentException("HW NS not supported"); } this.context = context; this.executor = scheduler; this.audioManager = audioManager; this.audioSource = audioSource; this.audioFormat = audioFormat; this.errorCallback = errorCallback; this.stateCallback = stateCallback; this.audioSamplesReadyCallback = audioSamplesReadyCallback; this.isAcousticEchoCancelerSupported = isAcousticEchoCancelerSupported; this.isNoiseSuppressorSupported = isNoiseSuppressorSupported; Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo()); } @CalledByNative public void setNativeAudioRecord(long nativeAudioRecord) { this.nativeAudioRecord = nativeAudioRecord; } @CalledByNative boolean isAcousticEchoCancelerSupported() { return isAcousticEchoCancelerSupported; } @CalledByNative boolean isNoiseSuppressorSupported() { return isNoiseSuppressorSupported; } // Returns true if a valid call to verifyAudioConfig() has been done. Should always be // checked before using the returned value of isAudioSourceMatchingRecordingSession(). @CalledByNative boolean isAudioConfigVerified() { return audioSourceMatchesRecordingSessionRef.get() != null; } // Returns true if verifyAudioConfig() succeeds. This value is set after a specific delay when // startRecording() has been called. Hence, should preferably be called in combination with // stopRecording() to ensure that it has been set properly. |isAudioConfigVerified| is // enabled in WebRtcAudioRecord to ensure that the returned value is valid. @CalledByNative boolean isAudioSourceMatchingRecordingSession() { Boolean audioSourceMatchesRecordingSession = audioSourceMatchesRecordingSessionRef.get(); if (audioSourceMatchesRecordingSession == null) { Logging.w(TAG, "Audio configuration has not yet been verified"); return false; } return audioSourceMatchesRecordingSession; } @CalledByNative private boolean enableBuiltInAEC(boolean enable) { Logging.d(TAG, "enableBuiltInAEC(" + enable + ")"); return effects.setAEC(enable); } @CalledByNative private boolean enableBuiltInNS(boolean enable) { Logging.d(TAG, "enableBuiltInNS(" + enable + ")"); return effects.setNS(enable); } @CalledByNative private int initRecording(int sampleRate, int channels) { Logging.d(TAG, "initRecording(sampleRate=" + sampleRate + ", channels=" + channels + ")"); if (audioRecord != null) { reportWebRtcAudioRecordInitError("InitRecording called twice without StopRecording."); return -1; } final int bytesPerFrame = channels * getBytesPerSample(audioFormat); final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND; byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer); if (!(byteBuffer.hasArray())) { reportWebRtcAudioRecordInitError("ByteBuffer does not have backing array."); return -1; } Logging.d(TAG, "byteBuffer.capacity: " + byteBuffer.capacity()); emptyBytes = new byte[byteBuffer.capacity()]; // Rather than passing the ByteBuffer with every callback (requiring // the potentially expensive GetDirectBufferAddress) we simply have the // the native class cache the address to the memory once. nativeCacheDirectBufferAddress(nativeAudioRecord, byteBuffer); // Get the minimum buffer size required for the successful creation of // an AudioRecord object, in byte units. // Note that this size doesn't guarantee a smooth recording under load. final int channelConfig = channelCountToConfiguration(channels); int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) { reportWebRtcAudioRecordInitError("AudioRecord.getMinBufferSize failed: " + minBufferSize); return -1; } Logging.d(TAG, "AudioRecord.getMinBufferSize: " + minBufferSize); // Use a larger buffer size than the minimum required when creating the // AudioRecord instance to ensure smooth recording under load. It has been // verified that it does not increase the actual recording latency. int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity()); Logging.d(TAG, "bufferSizeInBytes: " + bufferSizeInBytes); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Use the AudioRecord.Builder class on Android M (23) and above. // Throws IllegalArgumentException. audioRecord = createAudioRecordOnMOrHigher( audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes); audioSourceMatchesRecordingSessionRef.set(null); if (preferredDevice != null) { setPreferredDevice(preferredDevice); } } else { // Use the old AudioRecord constructor for API levels below 23. // Throws UnsupportedOperationException. audioRecord = createAudioRecordOnLowerThanM( audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes); audioSourceMatchesRecordingSessionRef.set(null); } } catch (IllegalArgumentException | UnsupportedOperationException e) { // Report of exception message is sufficient. Example: "Cannot create AudioRecord". reportWebRtcAudioRecordInitError(e.getMessage()); releaseAudioResources(); return -1; } if (audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { reportWebRtcAudioRecordInitError("Creation or initialization of audio recorder failed."); releaseAudioResources(); return -1; } effects.enable(audioRecord.getAudioSessionId()); logMainParameters(); logMainParametersExtended(); // Check number of active recording sessions. Should be zero but we have seen conflict cases // and adding a log for it can help us figure out details about conflicting sessions. final int numActiveRecordingSessions = logRecordingConfigurations(audioRecord, false /* verifyAudioConfig */); if (numActiveRecordingSessions != 0) { // Log the conflict as a warning since initialization did in fact succeed. Most likely, the // upcoming call to startRecording() will fail under these conditions. Logging.w( TAG, "Potential microphone conflict. Active sessions: " + numActiveRecordingSessions); } return framesPerBuffer; } /** * Prefer a specific {@link AudioDeviceInfo} device for recording. Calling after recording starts * is valid but may cause a temporary interruption if the audio routing changes. */ @RequiresApi(Build.VERSION_CODES.M) @TargetApi(Build.VERSION_CODES.M) void setPreferredDevice(@Nullable AudioDeviceInfo preferredDevice) { Logging.d( TAG, "setPreferredDevice " + (preferredDevice != null ? preferredDevice.getId() : null)); this.preferredDevice = preferredDevice; if (audioRecord != null) { if (!audioRecord.setPreferredDevice(preferredDevice)) { Logging.e(TAG, "setPreferredDevice failed"); } } } @CalledByNative private boolean startRecording() { Logging.d(TAG, "startRecording"); assertTrue(audioRecord != null); assertTrue(audioThread == null); try { audioRecord.startRecording(); } catch (IllegalStateException e) { reportWebRtcAudioRecordStartError(AudioRecordStartErrorCode.AUDIO_RECORD_START_EXCEPTION, "AudioRecord.startRecording failed: " + e.getMessage()); return false; } if (audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { reportWebRtcAudioRecordStartError(AudioRecordStartErrorCode.AUDIO_RECORD_START_STATE_MISMATCH, "AudioRecord.startRecording failed - incorrect state: " + audioRecord.getRecordingState()); return false; } audioThread = new AudioRecordThread("AudioRecordJavaThread"); audioThread.start(); scheduleLogRecordingConfigurationsTask(audioRecord); return true; } @CalledByNative private boolean stopRecording() { Logging.d(TAG, "stopRecording"); assertTrue(audioThread != null); if (future != null) { if (!future.isDone()) { // Might be needed if the client calls startRecording(), stopRecording() back-to-back. future.cancel(true /* mayInterruptIfRunning */); } future = null; } audioThread.stopThread(); if (!ThreadUtils.joinUninterruptibly(audioThread, AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS)) { Logging.e(TAG, "Join of AudioRecordJavaThread timed out"); WebRtcAudioUtils.logAudioState(TAG, context, audioManager); } audioThread = null; effects.release(); releaseAudioResources(); return true; } @TargetApi(Build.VERSION_CODES.M) private static AudioRecord createAudioRecordOnMOrHigher( int audioSource, int sampleRate, int channelConfig, int audioFormat, int bufferSizeInBytes) { Logging.d(TAG, "createAudioRecordOnMOrHigher"); return new AudioRecord.Builder() .setAudioSource(audioSource) .setAudioFormat(new AudioFormat.Builder() .setEncoding(audioFormat) .setSampleRate(sampleRate) .setChannelMask(channelConfig) .build()) .setBufferSizeInBytes(bufferSizeInBytes) .build(); } private static AudioRecord createAudioRecordOnLowerThanM( int audioSource, int sampleRate, int channelConfig, int audioFormat, int bufferSizeInBytes) { Logging.d(TAG, "createAudioRecordOnLowerThanM"); return new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes); } private void logMainParameters() { Logging.d(TAG, "AudioRecord: " + "session ID: " + audioRecord.getAudioSessionId() + ", " + "channels: " + audioRecord.getChannelCount() + ", " + "sample rate: " + audioRecord.getSampleRate()); } @TargetApi(Build.VERSION_CODES.M) private void logMainParametersExtended() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Logging.d(TAG, "AudioRecord: " // The frame count of the native AudioRecord buffer. + "buffer size in frames: " + audioRecord.getBufferSizeInFrames()); } } @TargetApi(Build.VERSION_CODES.N) // Checks the number of active recording sessions and logs the states of all active sessions. // Returns number of active sessions. Note that this could occur on arbituary thread. private int logRecordingConfigurations(AudioRecord audioRecord, boolean verifyAudioConfig) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { Logging.w(TAG, "AudioManager#getActiveRecordingConfigurations() requires N or higher"); return 0; } if (audioRecord == null) { return 0; } // Get a list of the currently active audio recording configurations of the device (can be more // than one). An empty list indicates there is no recording active when queried. List configs = audioManager.getActiveRecordingConfigurations(); final int numActiveRecordingSessions = configs.size(); Logging.d(TAG, "Number of active recording sessions: " + numActiveRecordingSessions); if (numActiveRecordingSessions > 0) { logActiveRecordingConfigs(audioRecord.getAudioSessionId(), configs); if (verifyAudioConfig) { // Run an extra check to verify that the existing audio source doing the recording (tied // to the AudioRecord instance) is matching what the audio recording configuration lists // as its client parameters. If these do not match, recording might work but under invalid // conditions. audioSourceMatchesRecordingSessionRef.set( verifyAudioConfig(audioRecord.getAudioSource(), audioRecord.getAudioSessionId(), audioRecord.getFormat(), audioRecord.getRoutedDevice(), configs)); } } return numActiveRecordingSessions; } // Helper method which throws an exception when an assertion has failed. private static void assertTrue(boolean condition) { if (!condition) { throw new AssertionError("Expected condition to be true"); } } private int channelCountToConfiguration(int channels) { return (channels == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO); } private native void nativeCacheDirectBufferAddress( long nativeAudioRecordJni, ByteBuffer byteBuffer); private native void nativeDataIsRecorded(long nativeAudioRecordJni, int bytes); // Sets all recorded samples to zero if |mute| is true, i.e., ensures that // the microphone is muted. public void setMicrophoneMute(boolean mute) { Logging.w(TAG, "setMicrophoneMute(" + mute + ")"); microphoneMute = mute; } // Releases the native AudioRecord resources. private void releaseAudioResources() { Logging.d(TAG, "releaseAudioResources"); if (audioRecord != null) { audioRecord.release(); audioRecord = null; } audioSourceMatchesRecordingSessionRef.set(null); } private void reportWebRtcAudioRecordInitError(String errorMessage) { Logging.e(TAG, "Init recording error: " + errorMessage); WebRtcAudioUtils.logAudioState(TAG, context, audioManager); logRecordingConfigurations(audioRecord, false /* verifyAudioConfig */); if (errorCallback != null) { errorCallback.onWebRtcAudioRecordInitError(errorMessage); } } private void reportWebRtcAudioRecordStartError( AudioRecordStartErrorCode errorCode, String errorMessage) { Logging.e(TAG, "Start recording error: " + errorCode + ". " + errorMessage); WebRtcAudioUtils.logAudioState(TAG, context, audioManager); logRecordingConfigurations(audioRecord, false /* verifyAudioConfig */); if (errorCallback != null) { errorCallback.onWebRtcAudioRecordStartError(errorCode, errorMessage); } } private void reportWebRtcAudioRecordError(String errorMessage) { Logging.e(TAG, "Run-time recording error: " + errorMessage); WebRtcAudioUtils.logAudioState(TAG, context, audioManager); if (errorCallback != null) { errorCallback.onWebRtcAudioRecordError(errorMessage); } } private void doAudioRecordStateCallback(int audioState) { Logging.d(TAG, "doAudioRecordStateCallback: " + audioStateToString(audioState)); if (stateCallback != null) { if (audioState == WebRtcAudioRecord.AUDIO_RECORD_START) { stateCallback.onWebRtcAudioRecordStart(); } else if (audioState == WebRtcAudioRecord.AUDIO_RECORD_STOP) { stateCallback.onWebRtcAudioRecordStop(); } else { Logging.e(TAG, "Invalid audio state"); } } } // Reference from Android code, AudioFormat.getBytesPerSample. BitPerSample / 8 // Default audio data format is PCM 16 bits per sample. // Guaranteed to be supported by all devices private static int getBytesPerSample(int audioFormat) { switch (audioFormat) { case AudioFormat.ENCODING_PCM_8BIT: return 1; case AudioFormat.ENCODING_PCM_16BIT: case AudioFormat.ENCODING_IEC61937: case AudioFormat.ENCODING_DEFAULT: return 2; case AudioFormat.ENCODING_PCM_FLOAT: return 4; case AudioFormat.ENCODING_INVALID: default: throw new IllegalArgumentException("Bad audio format " + audioFormat); } } // Use an ExecutorService to schedule a task after a given delay where the task consists of // checking (by logging) the current status of active recording sessions. private void scheduleLogRecordingConfigurationsTask(AudioRecord audioRecord) { Logging.d(TAG, "scheduleLogRecordingConfigurationsTask"); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { return; } Callable callable = () -> { if (this.audioRecord == audioRecord) { logRecordingConfigurations(audioRecord, true /* verifyAudioConfig */); } else { Logging.d(TAG, "audio record has changed"); } return "Scheduled task is done"; }; if (future != null && !future.isDone()) { future.cancel(true /* mayInterruptIfRunning */); } // Schedule call to logRecordingConfigurations() from executor thread after fixed delay. future = executor.schedule(callable, CHECK_REC_STATUS_DELAY_MS, TimeUnit.MILLISECONDS); }; @TargetApi(Build.VERSION_CODES.N) private static boolean logActiveRecordingConfigs( int session, List configs) { assertTrue(!configs.isEmpty()); final Iterator it = configs.iterator(); Logging.d(TAG, "AudioRecordingConfigurations: "); while (it.hasNext()) { final AudioRecordingConfiguration config = it.next(); StringBuilder conf = new StringBuilder(); // The audio source selected by the client. final int audioSource = config.getClientAudioSource(); conf.append(" client audio source=") .append(WebRtcAudioUtils.audioSourceToString(audioSource)) .append(", client session id=") .append(config.getClientAudioSessionId()) // Compare with our own id (based on AudioRecord#getAudioSessionId()). .append(" (") .append(session) .append(")") .append("\n"); // Audio format at which audio is recorded on this Android device. Note that it may differ // from the client application recording format (see getClientFormat()). AudioFormat format = config.getFormat(); conf.append(" Device AudioFormat: ") .append("channel count=") .append(format.getChannelCount()) .append(", channel index mask=") .append(format.getChannelIndexMask()) // Only AudioFormat#CHANNEL_IN_MONO is guaranteed to work on all devices. .append(", channel mask=") .append(WebRtcAudioUtils.channelMaskToString(format.getChannelMask())) .append(", encoding=") .append(WebRtcAudioUtils.audioEncodingToString(format.getEncoding())) .append(", sample rate=") .append(format.getSampleRate()) .append("\n"); // Audio format at which the client application is recording audio. format = config.getClientFormat(); conf.append(" Client AudioFormat: ") .append("channel count=") .append(format.getChannelCount()) .append(", channel index mask=") .append(format.getChannelIndexMask()) // Only AudioFormat#CHANNEL_IN_MONO is guaranteed to work on all devices. .append(", channel mask=") .append(WebRtcAudioUtils.channelMaskToString(format.getChannelMask())) .append(", encoding=") .append(WebRtcAudioUtils.audioEncodingToString(format.getEncoding())) .append(", sample rate=") .append(format.getSampleRate()) .append("\n"); // Audio input device used for this recording session. final AudioDeviceInfo device = config.getAudioDevice(); if (device != null) { assertTrue(device.isSource()); conf.append(" AudioDevice: ") .append("type=") .append(WebRtcAudioUtils.deviceTypeToString(device.getType())) .append(", id=") .append(device.getId()); } Logging.d(TAG, conf.toString()); } return true; } // Verify that the client audio configuration (device and format) matches the requested // configuration (same as AudioRecord's). @TargetApi(Build.VERSION_CODES.N) private static boolean verifyAudioConfig(int source, int session, AudioFormat format, AudioDeviceInfo device, List configs) { assertTrue(!configs.isEmpty()); final Iterator it = configs.iterator(); while (it.hasNext()) { final AudioRecordingConfiguration config = it.next(); final AudioDeviceInfo configDevice = config.getAudioDevice(); if (configDevice == null) { continue; } if ((config.getClientAudioSource() == source) && (config.getClientAudioSessionId() == session) // Check the client format (should match the format of the AudioRecord instance). && (config.getClientFormat().getEncoding() == format.getEncoding()) && (config.getClientFormat().getSampleRate() == format.getSampleRate()) && (config.getClientFormat().getChannelMask() == format.getChannelMask()) && (config.getClientFormat().getChannelIndexMask() == format.getChannelIndexMask()) // Ensure that the device format is properly configured. && (config.getFormat().getEncoding() != AudioFormat.ENCODING_INVALID) && (config.getFormat().getSampleRate() > 0) // For the channel mask, either the position or index-based value must be valid. && ((config.getFormat().getChannelMask() != AudioFormat.CHANNEL_INVALID) || (config.getFormat().getChannelIndexMask() != AudioFormat.CHANNEL_INVALID)) && checkDeviceMatch(configDevice, device)) { Logging.d(TAG, "verifyAudioConfig: PASS"); return true; } } Logging.e(TAG, "verifyAudioConfig: FAILED"); return false; } @TargetApi(Build.VERSION_CODES.N) // Returns true if device A parameters matches those of device B. // TODO(henrika): can be improved by adding AudioDeviceInfo#getAddress() but it requires API 29. private static boolean checkDeviceMatch(AudioDeviceInfo devA, AudioDeviceInfo devB) { return ((devA.getId() == devB.getId() && (devA.getType() == devB.getType()))); } private static String audioStateToString(int state) { switch (state) { case WebRtcAudioRecord.AUDIO_RECORD_START: return "START"; case WebRtcAudioRecord.AUDIO_RECORD_STOP: return "STOP"; default: return "INVALID"; } } private static final AtomicInteger nextSchedulerId = new AtomicInteger(0); static ScheduledExecutorService newDefaultScheduler() { AtomicInteger nextThreadId = new AtomicInteger(0); return Executors.newScheduledThreadPool(0, new ThreadFactory() { /** * Constructs a new {@code Thread} */ @Override public Thread newThread(Runnable r) { Thread thread = Executors.defaultThreadFactory().newThread(r); thread.setName(String.format("WebRtcAudioRecordScheduler-%s-%s", nextSchedulerId.getAndIncrement(), nextThreadId.getAndIncrement())); return thread; } }); } }