| /* |
| * 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.voiceengine; |
| |
| import android.media.audiofx.AcousticEchoCanceler; |
| import android.media.audiofx.AudioEffect; |
| import android.media.audiofx.AudioEffect.Descriptor; |
| import android.media.audiofx.NoiseSuppressor; |
| import android.os.Build; |
| import android.support.annotation.Nullable; |
| import java.util.List; |
| import java.util.UUID; |
| import org.webrtc.Logging; |
| |
| // This class wraps control of three different platform effects. Supported |
| // effects are: AcousticEchoCanceler (AEC) and NoiseSuppressor (NS). |
| // Calling enable() will active all effects that are |
| // supported by the device if the corresponding |shouldEnableXXX| member is set. |
| public class WebRtcAudioEffects { |
| private static final boolean DEBUG = false; |
| |
| private static final String TAG = "WebRtcAudioEffects"; |
| |
| // UUIDs for Software Audio Effects that we want to avoid using. |
| // The implementor field will be set to "The Android Open Source Project". |
| private static final UUID AOSP_ACOUSTIC_ECHO_CANCELER = |
| UUID.fromString("bb392ec0-8d4d-11e0-a896-0002a5d5c51b"); |
| private static final UUID AOSP_NOISE_SUPPRESSOR = |
| UUID.fromString("c06c8400-8e06-11e0-9cb6-0002a5d5c51b"); |
| |
| // Contains the available effect descriptors returned from the |
| // AudioEffect.getEffects() call. This result is cached to avoid doing the |
| // slow OS call multiple times. |
| private static @Nullable Descriptor[] cachedEffects; |
| |
| // Contains the audio effect objects. Created in enable() and destroyed |
| // in release(). |
| private @Nullable AcousticEchoCanceler aec; |
| private @Nullable NoiseSuppressor ns; |
| |
| // Affects the final state given to the setEnabled() method on each effect. |
| // The default state is set to "disabled" but each effect can also be enabled |
| // by calling setAEC() and setNS(). |
| // To enable an effect, both the shouldEnableXXX member and the static |
| // canUseXXX() must be true. |
| private boolean shouldEnableAec; |
| private boolean shouldEnableNs; |
| |
| // Checks if the device implements Acoustic Echo Cancellation (AEC). |
| // Returns true if the device implements AEC, false otherwise. |
| public static boolean isAcousticEchoCancelerSupported() { |
| // Note: we're using isAcousticEchoCancelerEffectAvailable() instead of |
| // AcousticEchoCanceler.isAvailable() to avoid the expensive getEffects() |
| // OS API call. |
| return isAcousticEchoCancelerEffectAvailable(); |
| } |
| |
| // Checks if the device implements Noise Suppression (NS). |
| // Returns true if the device implements NS, false otherwise. |
| public static boolean isNoiseSuppressorSupported() { |
| // Note: we're using isNoiseSuppressorEffectAvailable() instead of |
| // NoiseSuppressor.isAvailable() to avoid the expensive getEffects() |
| // OS API call. |
| return isNoiseSuppressorEffectAvailable(); |
| } |
| |
| // Returns true if the device is blacklisted for HW AEC usage. |
| public static boolean isAcousticEchoCancelerBlacklisted() { |
| List<String> blackListedModels = WebRtcAudioUtils.getBlackListedModelsForAecUsage(); |
| boolean isBlacklisted = blackListedModels.contains(Build.MODEL); |
| if (isBlacklisted) { |
| Logging.w(TAG, Build.MODEL + " is blacklisted for HW AEC usage!"); |
| } |
| return isBlacklisted; |
| } |
| |
| // Returns true if the device is blacklisted for HW NS usage. |
| public static boolean isNoiseSuppressorBlacklisted() { |
| List<String> blackListedModels = WebRtcAudioUtils.getBlackListedModelsForNsUsage(); |
| boolean isBlacklisted = blackListedModels.contains(Build.MODEL); |
| if (isBlacklisted) { |
| Logging.w(TAG, Build.MODEL + " is blacklisted for HW NS usage!"); |
| } |
| return isBlacklisted; |
| } |
| |
| // Returns true if the platform AEC should be excluded based on its UUID. |
| // AudioEffect.queryEffects() can throw IllegalStateException. |
| private static boolean isAcousticEchoCancelerExcludedByUUID() { |
| if (Build.VERSION.SDK_INT < 18) |
| return false; |
| for (Descriptor d : getAvailableEffects()) { |
| if (d.type.equals(AudioEffect.EFFECT_TYPE_AEC) |
| && d.uuid.equals(AOSP_ACOUSTIC_ECHO_CANCELER)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Returns true if the platform NS should be excluded based on its UUID. |
| // AudioEffect.queryEffects() can throw IllegalStateException. |
| private static boolean isNoiseSuppressorExcludedByUUID() { |
| if (Build.VERSION.SDK_INT < 18) |
| return false; |
| for (Descriptor d : getAvailableEffects()) { |
| if (d.type.equals(AudioEffect.EFFECT_TYPE_NS) && d.uuid.equals(AOSP_NOISE_SUPPRESSOR)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Returns true if the device supports Acoustic Echo Cancellation (AEC). |
| private static boolean isAcousticEchoCancelerEffectAvailable() { |
| if (Build.VERSION.SDK_INT < 18) |
| return false; |
| return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_AEC); |
| } |
| |
| // Returns true if the device supports Noise Suppression (NS). |
| private static boolean isNoiseSuppressorEffectAvailable() { |
| if (Build.VERSION.SDK_INT < 18) |
| return false; |
| return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_NS); |
| } |
| |
| // Returns true if all conditions for supporting the HW AEC are fulfilled. |
| // It will not be possible to enable the HW AEC if this method returns false. |
| public static boolean canUseAcousticEchoCanceler() { |
| boolean canUseAcousticEchoCanceler = isAcousticEchoCancelerSupported() |
| && !WebRtcAudioUtils.useWebRtcBasedAcousticEchoCanceler() |
| && !isAcousticEchoCancelerBlacklisted() && !isAcousticEchoCancelerExcludedByUUID(); |
| Logging.d(TAG, "canUseAcousticEchoCanceler: " + canUseAcousticEchoCanceler); |
| return canUseAcousticEchoCanceler; |
| } |
| |
| // Returns true if all conditions for supporting the HW NS are fulfilled. |
| // It will not be possible to enable the HW NS if this method returns false. |
| public static boolean canUseNoiseSuppressor() { |
| boolean canUseNoiseSuppressor = isNoiseSuppressorSupported() |
| && !WebRtcAudioUtils.useWebRtcBasedNoiseSuppressor() && !isNoiseSuppressorBlacklisted() |
| && !isNoiseSuppressorExcludedByUUID(); |
| Logging.d(TAG, "canUseNoiseSuppressor: " + canUseNoiseSuppressor); |
| return canUseNoiseSuppressor; |
| } |
| |
| public static WebRtcAudioEffects create() { |
| return new WebRtcAudioEffects(); |
| } |
| |
| private WebRtcAudioEffects() { |
| Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo()); |
| } |
| |
| // Call this method to enable or disable the platform AEC. It modifies |
| // |shouldEnableAec| which is used in enable() where the actual state |
| // of the AEC effect is modified. Returns true if HW AEC is supported and |
| // false otherwise. |
| public boolean setAEC(boolean enable) { |
| Logging.d(TAG, "setAEC(" + enable + ")"); |
| if (!canUseAcousticEchoCanceler()) { |
| Logging.w(TAG, "Platform AEC is not supported"); |
| shouldEnableAec = false; |
| return false; |
| } |
| if (aec != null && (enable != shouldEnableAec)) { |
| Logging.e(TAG, "Platform AEC state can't be modified while recording"); |
| return false; |
| } |
| shouldEnableAec = enable; |
| return true; |
| } |
| |
| // Call this method to enable or disable the platform NS. It modifies |
| // |shouldEnableNs| which is used in enable() where the actual state |
| // of the NS effect is modified. Returns true if HW NS is supported and |
| // false otherwise. |
| public boolean setNS(boolean enable) { |
| Logging.d(TAG, "setNS(" + enable + ")"); |
| if (!canUseNoiseSuppressor()) { |
| Logging.w(TAG, "Platform NS is not supported"); |
| shouldEnableNs = false; |
| return false; |
| } |
| if (ns != null && (enable != shouldEnableNs)) { |
| Logging.e(TAG, "Platform NS state can't be modified while recording"); |
| return false; |
| } |
| shouldEnableNs = enable; |
| return true; |
| } |
| |
| public void enable(int audioSession) { |
| Logging.d(TAG, "enable(audioSession=" + audioSession + ")"); |
| assertTrue(aec == null); |
| assertTrue(ns == null); |
| |
| if (DEBUG) { |
| // Add logging of supported effects but filter out "VoIP effects", i.e., |
| // AEC, AEC and NS. Avoid calling AudioEffect.queryEffects() unless the |
| // DEBUG flag is set since we have seen crashes in this API. |
| for (Descriptor d : AudioEffect.queryEffects()) { |
| if (effectTypeIsVoIP(d.type)) { |
| Logging.d(TAG, "name: " + d.name + ", " |
| + "mode: " + d.connectMode + ", " |
| + "implementor: " + d.implementor + ", " |
| + "UUID: " + d.uuid); |
| } |
| } |
| } |
| |
| if (isAcousticEchoCancelerSupported()) { |
| // Create an AcousticEchoCanceler and attach it to the AudioRecord on |
| // the specified audio session. |
| aec = AcousticEchoCanceler.create(audioSession); |
| if (aec != null) { |
| boolean enabled = aec.getEnabled(); |
| boolean enable = shouldEnableAec && canUseAcousticEchoCanceler(); |
| if (aec.setEnabled(enable) != AudioEffect.SUCCESS) { |
| Logging.e(TAG, "Failed to set the AcousticEchoCanceler state"); |
| } |
| Logging.d(TAG, "AcousticEchoCanceler: was " + (enabled ? "enabled" : "disabled") |
| + ", enable: " + enable + ", is now: " |
| + (aec.getEnabled() ? "enabled" : "disabled")); |
| } else { |
| Logging.e(TAG, "Failed to create the AcousticEchoCanceler instance"); |
| } |
| } |
| |
| if (isNoiseSuppressorSupported()) { |
| // Create an NoiseSuppressor and attach it to the AudioRecord on the |
| // specified audio session. |
| ns = NoiseSuppressor.create(audioSession); |
| if (ns != null) { |
| boolean enabled = ns.getEnabled(); |
| boolean enable = shouldEnableNs && canUseNoiseSuppressor(); |
| if (ns.setEnabled(enable) != AudioEffect.SUCCESS) { |
| Logging.e(TAG, "Failed to set the NoiseSuppressor state"); |
| } |
| Logging.d(TAG, "NoiseSuppressor: was " + (enabled ? "enabled" : "disabled") + ", enable: " |
| + enable + ", is now: " + (ns.getEnabled() ? "enabled" : "disabled")); |
| } else { |
| Logging.e(TAG, "Failed to create the NoiseSuppressor instance"); |
| } |
| } |
| } |
| |
| // Releases all native audio effect resources. It is a good practice to |
| // release the effect engine when not in use as control can be returned |
| // to other applications or the native resources released. |
| public void release() { |
| Logging.d(TAG, "release"); |
| if (aec != null) { |
| aec.release(); |
| aec = null; |
| } |
| if (ns != null) { |
| ns.release(); |
| ns = null; |
| } |
| } |
| |
| // Returns true for effect types in |type| that are of "VoIP" types: |
| // Acoustic Echo Canceler (AEC) or Automatic Gain Control (AGC) or |
| // Noise Suppressor (NS). Note that, an extra check for support is needed |
| // in each comparison since some devices includes effects in the |
| // AudioEffect.Descriptor array that are actually not available on the device. |
| // As an example: Samsung Galaxy S6 includes an AGC in the descriptor but |
| // AutomaticGainControl.isAvailable() returns false. |
| private boolean effectTypeIsVoIP(UUID type) { |
| if (Build.VERSION.SDK_INT < 18) |
| return false; |
| |
| return (AudioEffect.EFFECT_TYPE_AEC.equals(type) && isAcousticEchoCancelerSupported()) |
| || (AudioEffect.EFFECT_TYPE_NS.equals(type) && isNoiseSuppressorSupported()); |
| } |
| |
| // 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"); |
| } |
| } |
| |
| // Returns the cached copy of the audio effects array, if available, or |
| // queries the operating system for the list of effects. |
| private static @Nullable Descriptor[] getAvailableEffects() { |
| if (cachedEffects != null) { |
| return cachedEffects; |
| } |
| // The caching is best effort only - if this method is called from several |
| // threads in parallel, they may end up doing the underlying OS call |
| // multiple times. It's normally only called on one thread so there's no |
| // real need to optimize for the multiple threads case. |
| cachedEffects = AudioEffect.queryEffects(); |
| return cachedEffects; |
| } |
| |
| // Returns true if an effect of the specified type is available. Functionally |
| // equivalent to (NoiseSuppressor|AutomaticGainControl|...).isAvailable(), but |
| // faster as it avoids the expensive OS call to enumerate effects. |
| private static boolean isEffectTypeAvailable(UUID effectType) { |
| Descriptor[] effects = getAvailableEffects(); |
| if (effects == null) { |
| return false; |
| } |
| for (Descriptor d : effects) { |
| if (d.type.equals(effectType)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |