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.voiceengine; 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.List; 20 import java.util.UUID; 21 import org.webrtc.Logging; 22 23 // This class wraps control of three different platform effects. Supported 24 // effects are: AcousticEchoCanceler (AEC) and NoiseSuppressor (NS). 25 // Calling enable() will active all effects that are 26 // supported by the device if the corresponding `shouldEnableXXX` member is set. 27 public class WebRtcAudioEffects { 28 private static final boolean DEBUG = false; 29 30 private static final String TAG = "WebRtcAudioEffects"; 31 32 // UUIDs for Software Audio Effects that we want to avoid using. 33 // The implementor field will be set to "The Android Open Source Project". 34 private static final UUID AOSP_ACOUSTIC_ECHO_CANCELER = 35 UUID.fromString("bb392ec0-8d4d-11e0-a896-0002a5d5c51b"); 36 private static final UUID AOSP_NOISE_SUPPRESSOR = 37 UUID.fromString("c06c8400-8e06-11e0-9cb6-0002a5d5c51b"); 38 39 // Contains the available effect descriptors returned from the 40 // AudioEffect.getEffects() call. This result is cached to avoid doing the 41 // slow OS call multiple times. 42 private static @Nullable Descriptor[] cachedEffects; 43 44 // Contains the audio effect objects. Created in enable() and destroyed 45 // in release(). 46 private @Nullable AcousticEchoCanceler aec; 47 private @Nullable NoiseSuppressor ns; 48 49 // Affects the final state given to the setEnabled() method on each effect. 50 // The default state is set to "disabled" but each effect can also be enabled 51 // by calling setAEC() and setNS(). 52 // To enable an effect, both the shouldEnableXXX member and the static 53 // canUseXXX() must be true. 54 private boolean shouldEnableAec; 55 private boolean shouldEnableNs; 56 57 // Checks if the device implements Acoustic Echo Cancellation (AEC). 58 // Returns true if the device implements AEC, false otherwise. isAcousticEchoCancelerSupported()59 public static boolean isAcousticEchoCancelerSupported() { 60 // Note: we're using isAcousticEchoCancelerEffectAvailable() instead of 61 // AcousticEchoCanceler.isAvailable() to avoid the expensive getEffects() 62 // OS API call. 63 return isAcousticEchoCancelerEffectAvailable(); 64 } 65 66 // Checks if the device implements Noise Suppression (NS). 67 // Returns true if the device implements NS, false otherwise. isNoiseSuppressorSupported()68 public static boolean isNoiseSuppressorSupported() { 69 // Note: we're using isNoiseSuppressorEffectAvailable() instead of 70 // NoiseSuppressor.isAvailable() to avoid the expensive getEffects() 71 // OS API call. 72 return isNoiseSuppressorEffectAvailable(); 73 } 74 75 // Returns true if the device is blacklisted for HW AEC usage. isAcousticEchoCancelerBlacklisted()76 public static boolean isAcousticEchoCancelerBlacklisted() { 77 List<String> blackListedModels = WebRtcAudioUtils.getBlackListedModelsForAecUsage(); 78 boolean isBlacklisted = blackListedModels.contains(Build.MODEL); 79 if (isBlacklisted) { 80 Logging.w(TAG, Build.MODEL + " is blacklisted for HW AEC usage!"); 81 } 82 return isBlacklisted; 83 } 84 85 // Returns true if the device is blacklisted for HW NS usage. isNoiseSuppressorBlacklisted()86 public static boolean isNoiseSuppressorBlacklisted() { 87 List<String> blackListedModels = WebRtcAudioUtils.getBlackListedModelsForNsUsage(); 88 boolean isBlacklisted = blackListedModels.contains(Build.MODEL); 89 if (isBlacklisted) { 90 Logging.w(TAG, Build.MODEL + " is blacklisted for HW NS usage!"); 91 } 92 return isBlacklisted; 93 } 94 95 // Returns true if the platform AEC should be excluded based on its UUID. 96 // AudioEffect.queryEffects() can throw IllegalStateException. isAcousticEchoCancelerExcludedByUUID()97 private static boolean isAcousticEchoCancelerExcludedByUUID() { 98 for (Descriptor d : getAvailableEffects()) { 99 if (d.type.equals(AudioEffect.EFFECT_TYPE_AEC) 100 && d.uuid.equals(AOSP_ACOUSTIC_ECHO_CANCELER)) { 101 return true; 102 } 103 } 104 return false; 105 } 106 107 // Returns true if the platform NS should be excluded based on its UUID. 108 // AudioEffect.queryEffects() can throw IllegalStateException. isNoiseSuppressorExcludedByUUID()109 private static boolean isNoiseSuppressorExcludedByUUID() { 110 for (Descriptor d : getAvailableEffects()) { 111 if (d.type.equals(AudioEffect.EFFECT_TYPE_NS) && d.uuid.equals(AOSP_NOISE_SUPPRESSOR)) { 112 return true; 113 } 114 } 115 return false; 116 } 117 118 // Returns true if the device supports Acoustic Echo Cancellation (AEC). isAcousticEchoCancelerEffectAvailable()119 private static boolean isAcousticEchoCancelerEffectAvailable() { 120 return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_AEC); 121 } 122 123 // Returns true if the device supports Noise Suppression (NS). isNoiseSuppressorEffectAvailable()124 private static boolean isNoiseSuppressorEffectAvailable() { 125 return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_NS); 126 } 127 128 // Returns true if all conditions for supporting the HW AEC are fulfilled. 129 // It will not be possible to enable the HW AEC if this method returns false. canUseAcousticEchoCanceler()130 public static boolean canUseAcousticEchoCanceler() { 131 boolean canUseAcousticEchoCanceler = isAcousticEchoCancelerSupported() 132 && !WebRtcAudioUtils.useWebRtcBasedAcousticEchoCanceler() 133 && !isAcousticEchoCancelerBlacklisted() && !isAcousticEchoCancelerExcludedByUUID(); 134 Logging.d(TAG, "canUseAcousticEchoCanceler: " + canUseAcousticEchoCanceler); 135 return canUseAcousticEchoCanceler; 136 } 137 138 // Returns true if all conditions for supporting the HW NS are fulfilled. 139 // It will not be possible to enable the HW NS if this method returns false. canUseNoiseSuppressor()140 public static boolean canUseNoiseSuppressor() { 141 boolean canUseNoiseSuppressor = isNoiseSuppressorSupported() 142 && !WebRtcAudioUtils.useWebRtcBasedNoiseSuppressor() && !isNoiseSuppressorBlacklisted() 143 && !isNoiseSuppressorExcludedByUUID(); 144 Logging.d(TAG, "canUseNoiseSuppressor: " + canUseNoiseSuppressor); 145 return canUseNoiseSuppressor; 146 } 147 create()148 public static WebRtcAudioEffects create() { 149 return new WebRtcAudioEffects(); 150 } 151 WebRtcAudioEffects()152 private WebRtcAudioEffects() { 153 Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo()); 154 } 155 156 // Call this method to enable or disable the platform AEC. It modifies 157 // `shouldEnableAec` which is used in enable() where the actual state 158 // of the AEC effect is modified. Returns true if HW AEC is supported and 159 // false otherwise. setAEC(boolean enable)160 public boolean setAEC(boolean enable) { 161 Logging.d(TAG, "setAEC(" + enable + ")"); 162 if (!canUseAcousticEchoCanceler()) { 163 Logging.w(TAG, "Platform AEC is not supported"); 164 shouldEnableAec = false; 165 return false; 166 } 167 if (aec != null && (enable != shouldEnableAec)) { 168 Logging.e(TAG, "Platform AEC state can't be modified while recording"); 169 return false; 170 } 171 shouldEnableAec = enable; 172 return true; 173 } 174 175 // Call this method to enable or disable the platform NS. It modifies 176 // `shouldEnableNs` which is used in enable() where the actual state 177 // of the NS effect is modified. Returns true if HW NS is supported and 178 // false otherwise. setNS(boolean enable)179 public boolean setNS(boolean enable) { 180 Logging.d(TAG, "setNS(" + enable + ")"); 181 if (!canUseNoiseSuppressor()) { 182 Logging.w(TAG, "Platform NS is not supported"); 183 shouldEnableNs = false; 184 return false; 185 } 186 if (ns != null && (enable != shouldEnableNs)) { 187 Logging.e(TAG, "Platform NS state can't be modified while recording"); 188 return false; 189 } 190 shouldEnableNs = enable; 191 return true; 192 } 193 enable(int audioSession)194 public void enable(int audioSession) { 195 Logging.d(TAG, "enable(audioSession=" + audioSession + ")"); 196 assertTrue(aec == null); 197 assertTrue(ns == null); 198 199 if (DEBUG) { 200 // Add logging of supported effects but filter out "VoIP effects", i.e., 201 // AEC, AEC and NS. Avoid calling AudioEffect.queryEffects() unless the 202 // DEBUG flag is set since we have seen crashes in this API. 203 for (Descriptor d : AudioEffect.queryEffects()) { 204 if (effectTypeIsVoIP(d.type)) { 205 Logging.d(TAG, "name: " + d.name + ", " 206 + "mode: " + d.connectMode + ", " 207 + "implementor: " + d.implementor + ", " 208 + "UUID: " + d.uuid); 209 } 210 } 211 } 212 213 if (isAcousticEchoCancelerSupported()) { 214 // Create an AcousticEchoCanceler and attach it to the AudioRecord on 215 // the specified audio session. 216 aec = AcousticEchoCanceler.create(audioSession); 217 if (aec != null) { 218 boolean enabled = aec.getEnabled(); 219 boolean enable = shouldEnableAec && canUseAcousticEchoCanceler(); 220 if (aec.setEnabled(enable) != AudioEffect.SUCCESS) { 221 Logging.e(TAG, "Failed to set the AcousticEchoCanceler state"); 222 } 223 Logging.d(TAG, "AcousticEchoCanceler: was " + (enabled ? "enabled" : "disabled") 224 + ", enable: " + enable + ", is now: " 225 + (aec.getEnabled() ? "enabled" : "disabled")); 226 } else { 227 Logging.e(TAG, "Failed to create the AcousticEchoCanceler instance"); 228 } 229 } 230 231 if (isNoiseSuppressorSupported()) { 232 // Create an NoiseSuppressor and attach it to the AudioRecord on the 233 // specified audio session. 234 ns = NoiseSuppressor.create(audioSession); 235 if (ns != null) { 236 boolean enabled = ns.getEnabled(); 237 boolean enable = shouldEnableNs && canUseNoiseSuppressor(); 238 if (ns.setEnabled(enable) != AudioEffect.SUCCESS) { 239 Logging.e(TAG, "Failed to set the NoiseSuppressor state"); 240 } 241 Logging.d(TAG, "NoiseSuppressor: was " + (enabled ? "enabled" : "disabled") + ", enable: " 242 + enable + ", is now: " + (ns.getEnabled() ? "enabled" : "disabled")); 243 } else { 244 Logging.e(TAG, "Failed to create the NoiseSuppressor instance"); 245 } 246 } 247 } 248 249 // Releases all native audio effect resources. It is a good practice to 250 // release the effect engine when not in use as control can be returned 251 // to other applications or the native resources released. release()252 public void release() { 253 Logging.d(TAG, "release"); 254 if (aec != null) { 255 aec.release(); 256 aec = null; 257 } 258 if (ns != null) { 259 ns.release(); 260 ns = null; 261 } 262 } 263 264 // Returns true for effect types in `type` that are of "VoIP" types: 265 // Acoustic Echo Canceler (AEC) or Automatic Gain Control (AGC) or 266 // Noise Suppressor (NS). Note that, an extra check for support is needed 267 // in each comparison since some devices includes effects in the 268 // AudioEffect.Descriptor array that are actually not available on the device. 269 // As an example: Samsung Galaxy S6 includes an AGC in the descriptor but 270 // AutomaticGainControl.isAvailable() returns false. effectTypeIsVoIP(UUID type)271 private boolean effectTypeIsVoIP(UUID type) { 272 return (AudioEffect.EFFECT_TYPE_AEC.equals(type) && isAcousticEchoCancelerSupported()) 273 || (AudioEffect.EFFECT_TYPE_NS.equals(type) && isNoiseSuppressorSupported()); 274 } 275 276 // Helper method which throws an exception when an assertion has failed. assertTrue(boolean condition)277 private static void assertTrue(boolean condition) { 278 if (!condition) { 279 throw new AssertionError("Expected condition to be true"); 280 } 281 } 282 283 // Returns the cached copy of the audio effects array, if available, or 284 // queries the operating system for the list of effects. getAvailableEffects()285 private static @Nullable Descriptor[] getAvailableEffects() { 286 if (cachedEffects != null) { 287 return cachedEffects; 288 } 289 // The caching is best effort only - if this method is called from several 290 // threads in parallel, they may end up doing the underlying OS call 291 // multiple times. It's normally only called on one thread so there's no 292 // real need to optimize for the multiple threads case. 293 cachedEffects = AudioEffect.queryEffects(); 294 return cachedEffects; 295 } 296 297 // Returns true if an effect of the specified type is available. Functionally 298 // equivalent to (NoiseSuppressor`AutomaticGainControl`...).isAvailable(), but 299 // faster as it avoids the expensive OS call to enumerate effects. isEffectTypeAvailable(UUID effectType)300 private static boolean isEffectTypeAvailable(UUID effectType) { 301 Descriptor[] effects = getAvailableEffects(); 302 if (effects == null) { 303 return false; 304 } 305 for (Descriptor d : effects) { 306 if (d.type.equals(effectType)) { 307 return true; 308 } 309 } 310 return false; 311 } 312 } 313