/* * Copyright 2018 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.content.Context; import android.media.AudioAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.os.Build; import androidx.annotation.RequiresApi; import java.util.concurrent.ScheduledExecutorService; import org.webrtc.JniCommon; import org.webrtc.Logging; /** * AudioDeviceModule implemented using android.media.AudioRecord as input and * android.media.AudioTrack as output. */ public class JavaAudioDeviceModule implements AudioDeviceModule { private static final String TAG = "JavaAudioDeviceModule"; public static Builder builder(Context context) { return new Builder(context); } public static class Builder { private final Context context; private ScheduledExecutorService scheduler; private final AudioManager audioManager; private int inputSampleRate; private int outputSampleRate; private int audioSource = WebRtcAudioRecord.DEFAULT_AUDIO_SOURCE; private int audioFormat = WebRtcAudioRecord.DEFAULT_AUDIO_FORMAT; private AudioTrackErrorCallback audioTrackErrorCallback; private AudioRecordErrorCallback audioRecordErrorCallback; private SamplesReadyCallback samplesReadyCallback; private AudioTrackStateCallback audioTrackStateCallback; private AudioRecordStateCallback audioRecordStateCallback; private boolean useHardwareAcousticEchoCanceler = isBuiltInAcousticEchoCancelerSupported(); private boolean useHardwareNoiseSuppressor = isBuiltInNoiseSuppressorSupported(); private boolean useStereoInput; private boolean useStereoOutput; private AudioAttributes audioAttributes; private boolean useLowLatency; private boolean enableVolumeLogger; private Builder(Context context) { this.context = context; this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); this.inputSampleRate = WebRtcAudioManager.getSampleRate(audioManager); this.outputSampleRate = WebRtcAudioManager.getSampleRate(audioManager); this.useLowLatency = false; this.enableVolumeLogger = true; } public Builder setScheduler(ScheduledExecutorService scheduler) { this.scheduler = scheduler; return this; } /** * Call this method if the default handling of querying the native sample rate shall be * overridden. Can be useful on some devices where the available Android APIs are known to * return invalid results. */ public Builder setSampleRate(int sampleRate) { Logging.d(TAG, "Input/Output sample rate overridden to: " + sampleRate); this.inputSampleRate = sampleRate; this.outputSampleRate = sampleRate; return this; } /** * Call this method to specifically override input sample rate. */ public Builder setInputSampleRate(int inputSampleRate) { Logging.d(TAG, "Input sample rate overridden to: " + inputSampleRate); this.inputSampleRate = inputSampleRate; return this; } /** * Call this method to specifically override output sample rate. */ public Builder setOutputSampleRate(int outputSampleRate) { Logging.d(TAG, "Output sample rate overridden to: " + outputSampleRate); this.outputSampleRate = outputSampleRate; return this; } /** * Call this to change the audio source. The argument should be one of the values from * android.media.MediaRecorder.AudioSource. The default is AudioSource.VOICE_COMMUNICATION. */ public Builder setAudioSource(int audioSource) { this.audioSource = audioSource; return this; } /** * Call this to change the audio format. The argument should be one of the values from * android.media.AudioFormat ENCODING_PCM_8BIT, ENCODING_PCM_16BIT or ENCODING_PCM_FLOAT. * Default audio data format is PCM 16 bit per sample. * Guaranteed to be supported by all devices. */ public Builder setAudioFormat(int audioFormat) { this.audioFormat = audioFormat; return this; } /** * Set a callback to retrieve errors from the AudioTrack. */ public Builder setAudioTrackErrorCallback(AudioTrackErrorCallback audioTrackErrorCallback) { this.audioTrackErrorCallback = audioTrackErrorCallback; return this; } /** * Set a callback to retrieve errors from the AudioRecord. */ public Builder setAudioRecordErrorCallback(AudioRecordErrorCallback audioRecordErrorCallback) { this.audioRecordErrorCallback = audioRecordErrorCallback; return this; } /** * Set a callback to listen to the raw audio input from the AudioRecord. */ public Builder setSamplesReadyCallback(SamplesReadyCallback samplesReadyCallback) { this.samplesReadyCallback = samplesReadyCallback; return this; } /** * Set a callback to retrieve information from the AudioTrack on when audio starts and stop. */ public Builder setAudioTrackStateCallback(AudioTrackStateCallback audioTrackStateCallback) { this.audioTrackStateCallback = audioTrackStateCallback; return this; } /** * Set a callback to retrieve information from the AudioRecord on when audio starts and stops. */ public Builder setAudioRecordStateCallback(AudioRecordStateCallback audioRecordStateCallback) { this.audioRecordStateCallback = audioRecordStateCallback; return this; } /** * Control if the built-in HW noise suppressor should be used or not. The default is on if it is * supported. It is possible to query support by calling isBuiltInNoiseSuppressorSupported(). */ public Builder setUseHardwareNoiseSuppressor(boolean useHardwareNoiseSuppressor) { if (useHardwareNoiseSuppressor && !isBuiltInNoiseSuppressorSupported()) { Logging.e(TAG, "HW NS not supported"); useHardwareNoiseSuppressor = false; } this.useHardwareNoiseSuppressor = useHardwareNoiseSuppressor; return this; } /** * Control if the built-in HW acoustic echo canceler should be used or not. The default is on if * it is supported. It is possible to query support by calling * isBuiltInAcousticEchoCancelerSupported(). */ public Builder setUseHardwareAcousticEchoCanceler(boolean useHardwareAcousticEchoCanceler) { if (useHardwareAcousticEchoCanceler && !isBuiltInAcousticEchoCancelerSupported()) { Logging.e(TAG, "HW AEC not supported"); useHardwareAcousticEchoCanceler = false; } this.useHardwareAcousticEchoCanceler = useHardwareAcousticEchoCanceler; return this; } /** * Control if stereo input should be used or not. The default is mono. */ public Builder setUseStereoInput(boolean useStereoInput) { this.useStereoInput = useStereoInput; return this; } /** * Control if stereo output should be used or not. The default is mono. */ public Builder setUseStereoOutput(boolean useStereoOutput) { this.useStereoOutput = useStereoOutput; return this; } /** * Control if the low-latency mode should be used. The default is disabled. */ public Builder setUseLowLatency(boolean useLowLatency) { this.useLowLatency = useLowLatency; return this; } /** * Set custom {@link AudioAttributes} to use. */ public Builder setAudioAttributes(AudioAttributes audioAttributes) { this.audioAttributes = audioAttributes; return this; } /** Disables the volume logger on the audio output track. */ public Builder setEnableVolumeLogger(boolean enableVolumeLogger) { this.enableVolumeLogger = enableVolumeLogger; return this; } /** * Construct an AudioDeviceModule based on the supplied arguments. The caller takes ownership * and is responsible for calling release(). */ public JavaAudioDeviceModule createAudioDeviceModule() { Logging.d(TAG, "createAudioDeviceModule"); if (useHardwareNoiseSuppressor) { Logging.d(TAG, "HW NS will be used."); } else { if (isBuiltInNoiseSuppressorSupported()) { Logging.d(TAG, "Overriding default behavior; now using WebRTC NS!"); } Logging.d(TAG, "HW NS will not be used."); } if (useHardwareAcousticEchoCanceler) { Logging.d(TAG, "HW AEC will be used."); } else { if (isBuiltInAcousticEchoCancelerSupported()) { Logging.d(TAG, "Overriding default behavior; now using WebRTC AEC!"); } Logging.d(TAG, "HW AEC will not be used."); } // Low-latency mode was introduced in API version 26, see // https://developer.android.com/reference/android/media/AudioTrack#PERFORMANCE_MODE_LOW_LATENCY final int MIN_LOW_LATENCY_SDK_VERSION = 26; if (useLowLatency && Build.VERSION.SDK_INT >= MIN_LOW_LATENCY_SDK_VERSION) { Logging.d(TAG, "Low latency mode will be used."); } ScheduledExecutorService executor = this.scheduler; if (executor == null) { executor = WebRtcAudioRecord.newDefaultScheduler(); } final WebRtcAudioRecord audioInput = new WebRtcAudioRecord(context, executor, audioManager, audioSource, audioFormat, audioRecordErrorCallback, audioRecordStateCallback, samplesReadyCallback, useHardwareAcousticEchoCanceler, useHardwareNoiseSuppressor); final WebRtcAudioTrack audioOutput = new WebRtcAudioTrack(context, audioManager, audioAttributes, audioTrackErrorCallback, audioTrackStateCallback, useLowLatency, enableVolumeLogger); return new JavaAudioDeviceModule(context, audioManager, audioInput, audioOutput, inputSampleRate, outputSampleRate, useStereoInput, useStereoOutput); } } /* AudioRecord */ // Audio recording error handler functions. public enum AudioRecordStartErrorCode { AUDIO_RECORD_START_EXCEPTION, AUDIO_RECORD_START_STATE_MISMATCH, } public static interface AudioRecordErrorCallback { void onWebRtcAudioRecordInitError(String errorMessage); void onWebRtcAudioRecordStartError(AudioRecordStartErrorCode errorCode, String errorMessage); void onWebRtcAudioRecordError(String errorMessage); } /** Called when audio recording starts and stops. */ public static interface AudioRecordStateCallback { void onWebRtcAudioRecordStart(); void onWebRtcAudioRecordStop(); } /** * Contains audio sample information. */ public static class AudioSamples { /** See {@link AudioRecord#getAudioFormat()} */ private final int audioFormat; /** See {@link AudioRecord#getChannelCount()} */ private final int channelCount; /** See {@link AudioRecord#getSampleRate()} */ private final int sampleRate; private final byte[] data; public AudioSamples(int audioFormat, int channelCount, int sampleRate, byte[] data) { this.audioFormat = audioFormat; this.channelCount = channelCount; this.sampleRate = sampleRate; this.data = data; } public int getAudioFormat() { return audioFormat; } public int getChannelCount() { return channelCount; } public int getSampleRate() { return sampleRate; } public byte[] getData() { return data; } } /** Called when new audio samples are ready. This should only be set for debug purposes */ public static interface SamplesReadyCallback { void onWebRtcAudioRecordSamplesReady(AudioSamples samples); } /* AudioTrack */ // Audio playout/track error handler functions. public enum AudioTrackStartErrorCode { AUDIO_TRACK_START_EXCEPTION, AUDIO_TRACK_START_STATE_MISMATCH, } public static interface AudioTrackErrorCallback { void onWebRtcAudioTrackInitError(String errorMessage); void onWebRtcAudioTrackStartError(AudioTrackStartErrorCode errorCode, String errorMessage); void onWebRtcAudioTrackError(String errorMessage); } /** Called when audio playout starts and stops. */ public static interface AudioTrackStateCallback { void onWebRtcAudioTrackStart(); void onWebRtcAudioTrackStop(); } /** * Returns true if the device supports built-in HW AEC, and the UUID is approved (some UUIDs can * be excluded). */ public static boolean isBuiltInAcousticEchoCancelerSupported() { return WebRtcAudioEffects.isAcousticEchoCancelerSupported(); } /** * Returns true if the device supports built-in HW NS, and the UUID is approved (some UUIDs can be * excluded). */ public static boolean isBuiltInNoiseSuppressorSupported() { return WebRtcAudioEffects.isNoiseSuppressorSupported(); } private final Context context; private final AudioManager audioManager; private final WebRtcAudioRecord audioInput; private final WebRtcAudioTrack audioOutput; private final int inputSampleRate; private final int outputSampleRate; private final boolean useStereoInput; private final boolean useStereoOutput; private final Object nativeLock = new Object(); private long nativeAudioDeviceModule; private JavaAudioDeviceModule(Context context, AudioManager audioManager, WebRtcAudioRecord audioInput, WebRtcAudioTrack audioOutput, int inputSampleRate, int outputSampleRate, boolean useStereoInput, boolean useStereoOutput) { this.context = context; this.audioManager = audioManager; this.audioInput = audioInput; this.audioOutput = audioOutput; this.inputSampleRate = inputSampleRate; this.outputSampleRate = outputSampleRate; this.useStereoInput = useStereoInput; this.useStereoOutput = useStereoOutput; } @Override public long getNativeAudioDeviceModulePointer() { synchronized (nativeLock) { if (nativeAudioDeviceModule == 0) { nativeAudioDeviceModule = nativeCreateAudioDeviceModule(context, audioManager, audioInput, audioOutput, inputSampleRate, outputSampleRate, useStereoInput, useStereoOutput); } return nativeAudioDeviceModule; } } @Override public void release() { synchronized (nativeLock) { if (nativeAudioDeviceModule != 0) { JniCommon.nativeReleaseRef(nativeAudioDeviceModule); nativeAudioDeviceModule = 0; } } } @Override public void setSpeakerMute(boolean mute) { Logging.d(TAG, "setSpeakerMute: " + mute); audioOutput.setSpeakerMute(mute); } @Override public void setMicrophoneMute(boolean mute) { Logging.d(TAG, "setMicrophoneMute: " + mute); audioInput.setMicrophoneMute(mute); } @Override public boolean setNoiseSuppressorEnabled(boolean enabled) { Logging.d(TAG, "setNoiseSuppressorEnabled: " + enabled); return audioInput.setNoiseSuppressorEnabled(enabled); } /** * Start to prefer a specific {@link AudioDeviceInfo} device for recording. Typically this should * only be used if a client gives an explicit option for choosing a physical device to record * from. Otherwise the best-matching device for other parameters will be used. Calling after * recording is started may cause a temporary interruption if the audio routing changes. */ @RequiresApi(Build.VERSION_CODES.M) public void setPreferredInputDevice(AudioDeviceInfo preferredInputDevice) { Logging.d(TAG, "setPreferredInputDevice: " + preferredInputDevice); audioInput.setPreferredDevice(preferredInputDevice); } private static native long nativeCreateAudioDeviceModule(Context context, AudioManager audioManager, WebRtcAudioRecord audioInput, WebRtcAudioTrack audioOutput, int inputSampleRate, int outputSampleRate, boolean useStereoInput, boolean useStereoOutput); }