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.media.audiofx.AcousticEchoCanceler; 14 import android.media.audiofx.AudioEffect; 15 import android.media.audiofx.AudioEffect.Descriptor; 16 import android.media.audiofx.NoiseSuppressor; 17 import android.os.Build; 18 import androidx.annotation.Nullable; 19 import java.util.UUID; 20 import org.webrtc.Logging; 21 22 // This class wraps control of three different platform effects. Supported 23 // effects are: AcousticEchoCanceler (AEC) and NoiseSuppressor (NS). 24 // Calling enable() will active all effects that are 25 // supported by the device if the corresponding `shouldEnableXXX` member is set. 26 class WebRtcAudioEffects { 27 private static final boolean DEBUG = false; 28 29 private static final String TAG = "WebRtcAudioEffectsExternal"; 30 31 // UUIDs for Software Audio Effects that we want to avoid using. 32 // The implementor field will be set to "The Android Open Source Project". 33 private static final UUID AOSP_ACOUSTIC_ECHO_CANCELER = 34 UUID.fromString("bb392ec0-8d4d-11e0-a896-0002a5d5c51b"); 35 private static final UUID AOSP_NOISE_SUPPRESSOR = 36 UUID.fromString("c06c8400-8e06-11e0-9cb6-0002a5d5c51b"); 37 38 // Contains the available effect descriptors returned from the 39 // AudioEffect.getEffects() call. This result is cached to avoid doing the 40 // slow OS call multiple times. 41 private static @Nullable Descriptor[] cachedEffects; 42 43 // Contains the audio effect objects. Created in enable() and destroyed 44 // in release(). 45 private @Nullable AcousticEchoCanceler aec; 46 private @Nullable NoiseSuppressor ns; 47 48 // Affects the final state given to the setEnabled() method on each effect. 49 // The default state is set to "disabled" but each effect can also be enabled 50 // by calling setAEC() and setNS(). 51 private boolean shouldEnableAec; 52 private boolean shouldEnableNs; 53 54 // Returns true if all conditions for supporting HW Acoustic Echo Cancellation (AEC) are 55 // fulfilled. isAcousticEchoCancelerSupported()56 public static boolean isAcousticEchoCancelerSupported() { 57 return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_AEC, AOSP_ACOUSTIC_ECHO_CANCELER); 58 } 59 60 // Returns true if all conditions for supporting HW Noise Suppression (NS) are fulfilled. isNoiseSuppressorSupported()61 public static boolean isNoiseSuppressorSupported() { 62 return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_NS, AOSP_NOISE_SUPPRESSOR); 63 } 64 WebRtcAudioEffects()65 public WebRtcAudioEffects() { 66 Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo()); 67 } 68 69 // Call this method to enable or disable the platform AEC. It modifies 70 // `shouldEnableAec` which is used in enable() where the actual state 71 // of the AEC effect is modified. Returns true if HW AEC is supported and 72 // false otherwise. setAEC(boolean enable)73 public boolean setAEC(boolean enable) { 74 Logging.d(TAG, "setAEC(" + enable + ")"); 75 if (!isAcousticEchoCancelerSupported()) { 76 Logging.w(TAG, "Platform AEC is not supported"); 77 shouldEnableAec = false; 78 return false; 79 } 80 if (aec != null && (enable != shouldEnableAec)) { 81 Logging.e(TAG, "Platform AEC state can't be modified while recording"); 82 return false; 83 } 84 shouldEnableAec = enable; 85 return true; 86 } 87 88 // Call this method to enable or disable the platform NS. It modifies 89 // `shouldEnableNs` which is used in enable() where the actual state 90 // of the NS effect is modified. Returns true if HW NS is supported and 91 // false otherwise. setNS(boolean enable)92 public boolean setNS(boolean enable) { 93 Logging.d(TAG, "setNS(" + enable + ")"); 94 if (!isNoiseSuppressorSupported()) { 95 Logging.w(TAG, "Platform NS is not supported"); 96 shouldEnableNs = false; 97 return false; 98 } 99 if (ns != null && (enable != shouldEnableNs)) { 100 Logging.e(TAG, "Platform NS state can't be modified while recording"); 101 return false; 102 } 103 shouldEnableNs = enable; 104 return true; 105 } 106 enable(int audioSession)107 public void enable(int audioSession) { 108 Logging.d(TAG, "enable(audioSession=" + audioSession + ")"); 109 assertTrue(aec == null); 110 assertTrue(ns == null); 111 112 if (DEBUG) { 113 // Add logging of supported effects but filter out "VoIP effects", i.e., 114 // AEC, AEC and NS. Avoid calling AudioEffect.queryEffects() unless the 115 // DEBUG flag is set since we have seen crashes in this API. 116 for (Descriptor d : AudioEffect.queryEffects()) { 117 if (effectTypeIsVoIP(d.type)) { 118 Logging.d(TAG, 119 "name: " + d.name + ", " 120 + "mode: " + d.connectMode + ", " 121 + "implementor: " + d.implementor + ", " 122 + "UUID: " + d.uuid); 123 } 124 } 125 } 126 127 if (isAcousticEchoCancelerSupported()) { 128 // Create an AcousticEchoCanceler and attach it to the AudioRecord on 129 // the specified audio session. 130 aec = AcousticEchoCanceler.create(audioSession); 131 if (aec != null) { 132 boolean enabled = aec.getEnabled(); 133 boolean enable = shouldEnableAec && isAcousticEchoCancelerSupported(); 134 if (aec.setEnabled(enable) != AudioEffect.SUCCESS) { 135 Logging.e(TAG, "Failed to set the AcousticEchoCanceler state"); 136 } 137 Logging.d(TAG, 138 "AcousticEchoCanceler: was " + (enabled ? "enabled" : "disabled") + ", enable: " 139 + enable + ", is now: " + (aec.getEnabled() ? "enabled" : "disabled")); 140 } else { 141 Logging.e(TAG, "Failed to create the AcousticEchoCanceler instance"); 142 } 143 } 144 145 if (isNoiseSuppressorSupported()) { 146 // Create an NoiseSuppressor and attach it to the AudioRecord on the 147 // specified audio session. 148 ns = NoiseSuppressor.create(audioSession); 149 if (ns != null) { 150 boolean enabled = ns.getEnabled(); 151 boolean enable = shouldEnableNs && isNoiseSuppressorSupported(); 152 if (ns.setEnabled(enable) != AudioEffect.SUCCESS) { 153 Logging.e(TAG, "Failed to set the NoiseSuppressor state"); 154 } 155 Logging.d(TAG, 156 "NoiseSuppressor: was " + (enabled ? "enabled" : "disabled") + ", enable: " + enable 157 + ", is now: " + (ns.getEnabled() ? "enabled" : "disabled")); 158 } else { 159 Logging.e(TAG, "Failed to create the NoiseSuppressor instance"); 160 } 161 } 162 } 163 164 // Releases all native audio effect resources. It is a good practice to 165 // release the effect engine when not in use as control can be returned 166 // to other applications or the native resources released. release()167 public void release() { 168 Logging.d(TAG, "release"); 169 if (aec != null) { 170 aec.release(); 171 aec = null; 172 } 173 if (ns != null) { 174 ns.release(); 175 ns = null; 176 } 177 } 178 179 // Returns true for effect types in `type` that are of "VoIP" types: 180 // Acoustic Echo Canceler (AEC) or Automatic Gain Control (AGC) or 181 // Noise Suppressor (NS). Note that, an extra check for support is needed 182 // in each comparison since some devices includes effects in the 183 // AudioEffect.Descriptor array that are actually not available on the device. 184 // As an example: Samsung Galaxy S6 includes an AGC in the descriptor but 185 // AutomaticGainControl.isAvailable() returns false. effectTypeIsVoIP(UUID type)186 private boolean effectTypeIsVoIP(UUID type) { 187 return (AudioEffect.EFFECT_TYPE_AEC.equals(type) && isAcousticEchoCancelerSupported()) 188 || (AudioEffect.EFFECT_TYPE_NS.equals(type) && isNoiseSuppressorSupported()); 189 } 190 191 // Helper method which throws an exception when an assertion has failed. assertTrue(boolean condition)192 private static void assertTrue(boolean condition) { 193 if (!condition) { 194 throw new AssertionError("Expected condition to be true"); 195 } 196 } 197 198 // Returns the cached copy of the audio effects array, if available, or 199 // queries the operating system for the list of effects. getAvailableEffects()200 private static @Nullable Descriptor[] getAvailableEffects() { 201 if (cachedEffects != null) { 202 return cachedEffects; 203 } 204 // The caching is best effort only - if this method is called from several 205 // threads in parallel, they may end up doing the underlying OS call 206 // multiple times. It's normally only called on one thread so there's no 207 // real need to optimize for the multiple threads case. 208 cachedEffects = AudioEffect.queryEffects(); 209 return cachedEffects; 210 } 211 212 // Returns true if an effect of the specified type is available. Functionally 213 // equivalent to (NoiseSuppressor`AutomaticGainControl`...).isAvailable(), but 214 // faster as it avoids the expensive OS call to enumerate effects. isEffectTypeAvailable(UUID effectType, UUID blockListedUuid)215 private static boolean isEffectTypeAvailable(UUID effectType, UUID blockListedUuid) { 216 Descriptor[] effects = getAvailableEffects(); 217 if (effects == null) { 218 return false; 219 } 220 for (Descriptor d : effects) { 221 if (d.type.equals(effectType)) { 222 return !d.uuid.equals(blockListedUuid); 223 } 224 } 225 return false; 226 } 227 } 228