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 android.support.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 if (Build.VERSION.SDK_INT < 18) 99 return false; 100 for (Descriptor d : getAvailableEffects()) { 101 if (d.type.equals(AudioEffect.EFFECT_TYPE_AEC) 102 && d.uuid.equals(AOSP_ACOUSTIC_ECHO_CANCELER)) { 103 return true; 104 } 105 } 106 return false; 107 } 108 109 // Returns true if the platform NS should be excluded based on its UUID. 110 // AudioEffect.queryEffects() can throw IllegalStateException. isNoiseSuppressorExcludedByUUID()111 private static boolean isNoiseSuppressorExcludedByUUID() { 112 if (Build.VERSION.SDK_INT < 18) 113 return false; 114 for (Descriptor d : getAvailableEffects()) { 115 if (d.type.equals(AudioEffect.EFFECT_TYPE_NS) && d.uuid.equals(AOSP_NOISE_SUPPRESSOR)) { 116 return true; 117 } 118 } 119 return false; 120 } 121 122 // Returns true if the device supports Acoustic Echo Cancellation (AEC). isAcousticEchoCancelerEffectAvailable()123 private static boolean isAcousticEchoCancelerEffectAvailable() { 124 if (Build.VERSION.SDK_INT < 18) 125 return false; 126 return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_AEC); 127 } 128 129 // Returns true if the device supports Noise Suppression (NS). isNoiseSuppressorEffectAvailable()130 private static boolean isNoiseSuppressorEffectAvailable() { 131 if (Build.VERSION.SDK_INT < 18) 132 return false; 133 return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_NS); 134 } 135 136 // Returns true if all conditions for supporting the HW AEC are fulfilled. 137 // It will not be possible to enable the HW AEC if this method returns false. canUseAcousticEchoCanceler()138 public static boolean canUseAcousticEchoCanceler() { 139 boolean canUseAcousticEchoCanceler = isAcousticEchoCancelerSupported() 140 && !WebRtcAudioUtils.useWebRtcBasedAcousticEchoCanceler() 141 && !isAcousticEchoCancelerBlacklisted() && !isAcousticEchoCancelerExcludedByUUID(); 142 Logging.d(TAG, "canUseAcousticEchoCanceler: " + canUseAcousticEchoCanceler); 143 return canUseAcousticEchoCanceler; 144 } 145 146 // Returns true if all conditions for supporting the HW NS are fulfilled. 147 // It will not be possible to enable the HW NS if this method returns false. canUseNoiseSuppressor()148 public static boolean canUseNoiseSuppressor() { 149 boolean canUseNoiseSuppressor = isNoiseSuppressorSupported() 150 && !WebRtcAudioUtils.useWebRtcBasedNoiseSuppressor() && !isNoiseSuppressorBlacklisted() 151 && !isNoiseSuppressorExcludedByUUID(); 152 Logging.d(TAG, "canUseNoiseSuppressor: " + canUseNoiseSuppressor); 153 return canUseNoiseSuppressor; 154 } 155 create()156 public static WebRtcAudioEffects create() { 157 return new WebRtcAudioEffects(); 158 } 159 WebRtcAudioEffects()160 private WebRtcAudioEffects() { 161 Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo()); 162 } 163 164 // Call this method to enable or disable the platform AEC. It modifies 165 // |shouldEnableAec| which is used in enable() where the actual state 166 // of the AEC effect is modified. Returns true if HW AEC is supported and 167 // false otherwise. setAEC(boolean enable)168 public boolean setAEC(boolean enable) { 169 Logging.d(TAG, "setAEC(" + enable + ")"); 170 if (!canUseAcousticEchoCanceler()) { 171 Logging.w(TAG, "Platform AEC is not supported"); 172 shouldEnableAec = false; 173 return false; 174 } 175 if (aec != null && (enable != shouldEnableAec)) { 176 Logging.e(TAG, "Platform AEC state can't be modified while recording"); 177 return false; 178 } 179 shouldEnableAec = enable; 180 return true; 181 } 182 183 // Call this method to enable or disable the platform NS. It modifies 184 // |shouldEnableNs| which is used in enable() where the actual state 185 // of the NS effect is modified. Returns true if HW NS is supported and 186 // false otherwise. setNS(boolean enable)187 public boolean setNS(boolean enable) { 188 Logging.d(TAG, "setNS(" + enable + ")"); 189 if (!canUseNoiseSuppressor()) { 190 Logging.w(TAG, "Platform NS is not supported"); 191 shouldEnableNs = false; 192 return false; 193 } 194 if (ns != null && (enable != shouldEnableNs)) { 195 Logging.e(TAG, "Platform NS state can't be modified while recording"); 196 return false; 197 } 198 shouldEnableNs = enable; 199 return true; 200 } 201 enable(int audioSession)202 public void enable(int audioSession) { 203 Logging.d(TAG, "enable(audioSession=" + audioSession + ")"); 204 assertTrue(aec == null); 205 assertTrue(ns == null); 206 207 if (DEBUG) { 208 // Add logging of supported effects but filter out "VoIP effects", i.e., 209 // AEC, AEC and NS. Avoid calling AudioEffect.queryEffects() unless the 210 // DEBUG flag is set since we have seen crashes in this API. 211 for (Descriptor d : AudioEffect.queryEffects()) { 212 if (effectTypeIsVoIP(d.type)) { 213 Logging.d(TAG, "name: " + d.name + ", " 214 + "mode: " + d.connectMode + ", " 215 + "implementor: " + d.implementor + ", " 216 + "UUID: " + d.uuid); 217 } 218 } 219 } 220 221 if (isAcousticEchoCancelerSupported()) { 222 // Create an AcousticEchoCanceler and attach it to the AudioRecord on 223 // the specified audio session. 224 aec = AcousticEchoCanceler.create(audioSession); 225 if (aec != null) { 226 boolean enabled = aec.getEnabled(); 227 boolean enable = shouldEnableAec && canUseAcousticEchoCanceler(); 228 if (aec.setEnabled(enable) != AudioEffect.SUCCESS) { 229 Logging.e(TAG, "Failed to set the AcousticEchoCanceler state"); 230 } 231 Logging.d(TAG, "AcousticEchoCanceler: was " + (enabled ? "enabled" : "disabled") 232 + ", enable: " + enable + ", is now: " 233 + (aec.getEnabled() ? "enabled" : "disabled")); 234 } else { 235 Logging.e(TAG, "Failed to create the AcousticEchoCanceler instance"); 236 } 237 } 238 239 if (isNoiseSuppressorSupported()) { 240 // Create an NoiseSuppressor and attach it to the AudioRecord on the 241 // specified audio session. 242 ns = NoiseSuppressor.create(audioSession); 243 if (ns != null) { 244 boolean enabled = ns.getEnabled(); 245 boolean enable = shouldEnableNs && canUseNoiseSuppressor(); 246 if (ns.setEnabled(enable) != AudioEffect.SUCCESS) { 247 Logging.e(TAG, "Failed to set the NoiseSuppressor state"); 248 } 249 Logging.d(TAG, "NoiseSuppressor: was " + (enabled ? "enabled" : "disabled") + ", enable: " 250 + enable + ", is now: " + (ns.getEnabled() ? "enabled" : "disabled")); 251 } else { 252 Logging.e(TAG, "Failed to create the NoiseSuppressor instance"); 253 } 254 } 255 } 256 257 // Releases all native audio effect resources. It is a good practice to 258 // release the effect engine when not in use as control can be returned 259 // to other applications or the native resources released. release()260 public void release() { 261 Logging.d(TAG, "release"); 262 if (aec != null) { 263 aec.release(); 264 aec = null; 265 } 266 if (ns != null) { 267 ns.release(); 268 ns = null; 269 } 270 } 271 272 // Returns true for effect types in |type| that are of "VoIP" types: 273 // Acoustic Echo Canceler (AEC) or Automatic Gain Control (AGC) or 274 // Noise Suppressor (NS). Note that, an extra check for support is needed 275 // in each comparison since some devices includes effects in the 276 // AudioEffect.Descriptor array that are actually not available on the device. 277 // As an example: Samsung Galaxy S6 includes an AGC in the descriptor but 278 // AutomaticGainControl.isAvailable() returns false. effectTypeIsVoIP(UUID type)279 private boolean effectTypeIsVoIP(UUID type) { 280 if (Build.VERSION.SDK_INT < 18) 281 return false; 282 283 return (AudioEffect.EFFECT_TYPE_AEC.equals(type) && isAcousticEchoCancelerSupported()) 284 || (AudioEffect.EFFECT_TYPE_NS.equals(type) && isNoiseSuppressorSupported()); 285 } 286 287 // Helper method which throws an exception when an assertion has failed. assertTrue(boolean condition)288 private static void assertTrue(boolean condition) { 289 if (!condition) { 290 throw new AssertionError("Expected condition to be true"); 291 } 292 } 293 294 // Returns the cached copy of the audio effects array, if available, or 295 // queries the operating system for the list of effects. getAvailableEffects()296 private static @Nullable Descriptor[] getAvailableEffects() { 297 if (cachedEffects != null) { 298 return cachedEffects; 299 } 300 // The caching is best effort only - if this method is called from several 301 // threads in parallel, they may end up doing the underlying OS call 302 // multiple times. It's normally only called on one thread so there's no 303 // real need to optimize for the multiple threads case. 304 cachedEffects = AudioEffect.queryEffects(); 305 return cachedEffects; 306 } 307 308 // Returns true if an effect of the specified type is available. Functionally 309 // equivalent to (NoiseSuppressor|AutomaticGainControl|...).isAvailable(), but 310 // faster as it avoids the expensive OS call to enumerate effects. isEffectTypeAvailable(UUID effectType)311 private static boolean isEffectTypeAvailable(UUID effectType) { 312 Descriptor[] effects = getAvailableEffects(); 313 if (effects == null) { 314 return false; 315 } 316 for (Descriptor d : effects) { 317 if (d.type.equals(effectType)) { 318 return true; 319 } 320 } 321 return false; 322 } 323 } 324