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.annotation.TargetApi; 14 import android.media.audiofx.AcousticEchoCanceler; 15 import android.media.audiofx.AudioEffect; 16 import android.media.audiofx.AudioEffect.Descriptor; 17 import android.media.audiofx.AutomaticGainControl; 18 import android.media.audiofx.NoiseSuppressor; 19 import android.os.Build; 20 21 import org.webrtc.Logging; 22 23 import java.util.List; 24 25 import java.util.UUID; 26 27 // This class wraps control of three different platform effects. Supported 28 // effects are: AcousticEchoCanceler (AEC), AutomaticGainControl (AGC) and 29 // NoiseSuppressor (NS). Calling enable() will active all effects that are 30 // supported by the device if the corresponding |shouldEnableXXX| member is set. 31 class WebRtcAudioEffects { 32 private static final boolean DEBUG = false; 33 34 private static final String TAG = "WebRtcAudioEffects"; 35 36 // UUIDs for Software Audio Effects that we want to avoid using. 37 // The implementor field will be set to "The Android Open Source Project". 38 private static final UUID AOSP_ACOUSTIC_ECHO_CANCELER = 39 UUID.fromString("bb392ec0-8d4d-11e0-a896-0002a5d5c51b"); 40 private static final UUID AOSP_AUTOMATIC_GAIN_CONTROL = 41 UUID.fromString("aa8130e0-66fc-11e0-bad0-0002a5d5c51b"); 42 private static final UUID AOSP_NOISE_SUPPRESSOR = 43 UUID.fromString("c06c8400-8e06-11e0-9cb6-0002a5d5c51b"); 44 45 // Static Boolean objects used to avoid expensive queries more than once. 46 // The first result is cached in these members and then reused if needed. 47 // Each member is null until it has been evaluated/set for the first time. 48 private static Boolean canUseAcousticEchoCanceler = null; 49 private static Boolean canUseAutomaticGainControl = null; 50 private static Boolean canUseNoiseSuppressor = null; 51 52 // Contains the audio effect objects. Created in enable() and destroyed 53 // in release(). 54 private AcousticEchoCanceler aec = null; 55 private AutomaticGainControl agc = null; 56 private NoiseSuppressor ns = null; 57 58 // Affects the final state given to the setEnabled() method on each effect. 59 // The default state is set to "disabled" but each effect can also be enabled 60 // by calling setAEC(), setAGC() and setNS(). 61 // To enable an effect, both the shouldEnableXXX member and the static 62 // canUseXXX() must be true. 63 private boolean shouldEnableAec = false; 64 private boolean shouldEnableAgc = false; 65 private boolean shouldEnableNs = false; 66 67 // Checks if the device implements Acoustic Echo Cancellation (AEC). 68 // Returns true if the device implements AEC, false otherwise. isAcousticEchoCancelerSupported()69 public static boolean isAcousticEchoCancelerSupported() { 70 return WebRtcAudioUtils.runningOnJellyBeanOrHigher() 71 && AcousticEchoCanceler.isAvailable(); 72 } 73 74 // Checks if the device implements Automatic Gain Control (AGC). 75 // Returns true if the device implements AGC, false otherwise. isAutomaticGainControlSupported()76 public static boolean isAutomaticGainControlSupported() { 77 return WebRtcAudioUtils.runningOnJellyBeanOrHigher() 78 && AutomaticGainControl.isAvailable(); 79 } 80 81 // Checks if the device implements Noise Suppression (NS). 82 // Returns true if the device implements NS, false otherwise. isNoiseSuppressorSupported()83 public static boolean isNoiseSuppressorSupported() { 84 return WebRtcAudioUtils.runningOnJellyBeanOrHigher() 85 && NoiseSuppressor.isAvailable(); 86 } 87 88 // Returns true if the device is blacklisted for HW AEC usage. isAcousticEchoCancelerBlacklisted()89 public static boolean isAcousticEchoCancelerBlacklisted() { 90 List<String> blackListedModels = 91 WebRtcAudioUtils.getBlackListedModelsForAecUsage(); 92 boolean isBlacklisted = blackListedModels.contains(Build.MODEL); 93 if (isBlacklisted) { 94 Logging.w(TAG, Build.MODEL + " is blacklisted for HW AEC usage!"); 95 } 96 return isBlacklisted; 97 } 98 99 // Returns true if the device is blacklisted for HW AGC usage. isAutomaticGainControlBlacklisted()100 public static boolean isAutomaticGainControlBlacklisted() { 101 List<String> blackListedModels = 102 WebRtcAudioUtils.getBlackListedModelsForAgcUsage(); 103 boolean isBlacklisted = blackListedModels.contains(Build.MODEL); 104 if (isBlacklisted) { 105 Logging.w(TAG, Build.MODEL + " is blacklisted for HW AGC usage!"); 106 } 107 return isBlacklisted; 108 } 109 110 // Returns true if the device is blacklisted for HW NS usage. isNoiseSuppressorBlacklisted()111 public static boolean isNoiseSuppressorBlacklisted() { 112 List<String> blackListedModels = 113 WebRtcAudioUtils.getBlackListedModelsForNsUsage(); 114 boolean isBlacklisted = blackListedModels.contains(Build.MODEL); 115 if (isBlacklisted) { 116 Logging.w(TAG, Build.MODEL + " is blacklisted for HW NS usage!"); 117 } 118 return isBlacklisted; 119 } 120 121 // Returns true if the platform AEC should be excluded based on its UUID. 122 // AudioEffect.queryEffects() can throw IllegalStateException. 123 @TargetApi(18) isAcousticEchoCancelerExcludedByUUID()124 private static boolean isAcousticEchoCancelerExcludedByUUID() { 125 for (Descriptor d : AudioEffect.queryEffects()) { 126 if (d.type.equals(AudioEffect.EFFECT_TYPE_AEC) && 127 d.uuid.equals(AOSP_ACOUSTIC_ECHO_CANCELER)) { 128 return true; 129 } 130 } 131 return false; 132 } 133 134 // Returns true if the platform AGC should be excluded based on its UUID. 135 // AudioEffect.queryEffects() can throw IllegalStateException. 136 @TargetApi(18) isAutomaticGainControlExcludedByUUID()137 private static boolean isAutomaticGainControlExcludedByUUID() { 138 for (Descriptor d : AudioEffect.queryEffects()) { 139 if (d.type.equals(AudioEffect.EFFECT_TYPE_AGC) && 140 d.uuid.equals(AOSP_AUTOMATIC_GAIN_CONTROL)) { 141 return true; 142 } 143 } 144 return false; 145 } 146 147 // Returns true if the platform NS should be excluded based on its UUID. 148 // AudioEffect.queryEffects() can throw IllegalStateException. 149 @TargetApi(18) isNoiseSuppressorExcludedByUUID()150 private static boolean isNoiseSuppressorExcludedByUUID() { 151 for (Descriptor d : AudioEffect.queryEffects()) { 152 if (d.type.equals(AudioEffect.EFFECT_TYPE_NS) && 153 d.uuid.equals(AOSP_NOISE_SUPPRESSOR)) { 154 return true; 155 } 156 } 157 return false; 158 } 159 160 // Returns true if all conditions for supporting the HW AEC are fulfilled. 161 // It will not be possible to enable the HW AEC if this method returns false. canUseAcousticEchoCanceler()162 public static boolean canUseAcousticEchoCanceler() { 163 if (canUseAcousticEchoCanceler == null) { 164 canUseAcousticEchoCanceler = new Boolean( 165 isAcousticEchoCancelerSupported() 166 && !WebRtcAudioUtils.useWebRtcBasedAcousticEchoCanceler() 167 && !isAcousticEchoCancelerBlacklisted() 168 && !isAcousticEchoCancelerExcludedByUUID()); 169 Logging.d(TAG, "canUseAcousticEchoCanceler: " 170 + canUseAcousticEchoCanceler); 171 } 172 return canUseAcousticEchoCanceler; 173 } 174 175 // Returns true if all conditions for supporting the HW AGC are fulfilled. 176 // It will not be possible to enable the HW AGC if this method returns false. canUseAutomaticGainControl()177 public static boolean canUseAutomaticGainControl() { 178 if (canUseAutomaticGainControl == null) { 179 canUseAutomaticGainControl = new Boolean( 180 isAutomaticGainControlSupported() 181 && !WebRtcAudioUtils.useWebRtcBasedAutomaticGainControl() 182 && !isAutomaticGainControlBlacklisted() 183 && !isAutomaticGainControlExcludedByUUID()); 184 Logging.d(TAG, "canUseAutomaticGainControl: " 185 + canUseAutomaticGainControl); 186 } 187 return canUseAutomaticGainControl; 188 } 189 190 // Returns true if all conditions for supporting the HW NS are fulfilled. 191 // It will not be possible to enable the HW NS if this method returns false. canUseNoiseSuppressor()192 public static boolean canUseNoiseSuppressor() { 193 if (canUseNoiseSuppressor == null) { 194 canUseNoiseSuppressor = new Boolean( 195 isNoiseSuppressorSupported() 196 && !WebRtcAudioUtils.useWebRtcBasedNoiseSuppressor() 197 && !isNoiseSuppressorBlacklisted() 198 && !isNoiseSuppressorExcludedByUUID()); 199 Logging.d(TAG, "canUseNoiseSuppressor: " + canUseNoiseSuppressor); 200 } 201 return canUseNoiseSuppressor; 202 } 203 create()204 static WebRtcAudioEffects create() { 205 // Return null if VoIP effects (AEC, AGC and NS) are not supported. 206 if (!WebRtcAudioUtils.runningOnJellyBeanOrHigher()) { 207 Logging.w(TAG, "API level 16 or higher is required!"); 208 return null; 209 } 210 return new WebRtcAudioEffects(); 211 } 212 WebRtcAudioEffects()213 private WebRtcAudioEffects() { 214 Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo()); 215 } 216 217 // Call this method to enable or disable the platform AEC. It modifies 218 // |shouldEnableAec| which is used in enable() where the actual state 219 // of the AEC effect is modified. Returns true if HW AEC is supported and 220 // false otherwise. setAEC(boolean enable)221 public boolean setAEC(boolean enable) { 222 Logging.d(TAG, "setAEC(" + enable + ")"); 223 if (!canUseAcousticEchoCanceler()) { 224 Logging.w(TAG, "Platform AEC is not supported"); 225 shouldEnableAec = false; 226 return false; 227 } 228 if (aec != null && (enable != shouldEnableAec)) { 229 Logging.e(TAG, "Platform AEC state can't be modified while recording"); 230 return false; 231 } 232 shouldEnableAec = enable; 233 return true; 234 } 235 236 // Call this method to enable or disable the platform AGC. It modifies 237 // |shouldEnableAgc| which is used in enable() where the actual state 238 // of the AGC effect is modified. Returns true if HW AGC is supported and 239 // false otherwise. setAGC(boolean enable)240 public boolean setAGC(boolean enable) { 241 Logging.d(TAG, "setAGC(" + enable + ")"); 242 if (!canUseAutomaticGainControl()) { 243 Logging.w(TAG, "Platform AGC is not supported"); 244 shouldEnableAgc = false; 245 return false; 246 } 247 if (agc != null && (enable != shouldEnableAgc)) { 248 Logging.e(TAG, "Platform AGC state can't be modified while recording"); 249 return false; 250 } 251 shouldEnableAgc = enable; 252 return true; 253 } 254 255 // Call this method to enable or disable the platform NS. It modifies 256 // |shouldEnableNs| which is used in enable() where the actual state 257 // of the NS effect is modified. Returns true if HW NS is supported and 258 // false otherwise. setNS(boolean enable)259 public boolean setNS(boolean enable) { 260 Logging.d(TAG, "setNS(" + enable + ")"); 261 if (!canUseNoiseSuppressor()) { 262 Logging.w(TAG, "Platform NS is not supported"); 263 shouldEnableNs = false; 264 return false; 265 } 266 if (ns != null && (enable != shouldEnableNs)) { 267 Logging.e(TAG, "Platform NS state can't be modified while recording"); 268 return false; 269 } 270 shouldEnableNs = enable; 271 return true; 272 } 273 enable(int audioSession)274 public void enable(int audioSession) { 275 Logging.d(TAG, "enable(audioSession=" + audioSession + ")"); 276 assertTrue(aec == null); 277 assertTrue(agc == null); 278 assertTrue(ns == null); 279 280 // Add logging of supported effects but filter out "VoIP effects", i.e., 281 // AEC, AEC and NS. 282 for (Descriptor d : AudioEffect.queryEffects()) { 283 if (effectTypeIsVoIP(d.type) || DEBUG) { 284 Logging.d(TAG, "name: " + d.name + ", " 285 + "mode: " + d.connectMode + ", " 286 + "implementor: " + d.implementor + ", " 287 + "UUID: " + d.uuid); 288 } 289 } 290 291 if (isAcousticEchoCancelerSupported()) { 292 // Create an AcousticEchoCanceler and attach it to the AudioRecord on 293 // the specified audio session. 294 aec = AcousticEchoCanceler.create(audioSession); 295 if (aec != null) { 296 boolean enabled = aec.getEnabled(); 297 boolean enable = shouldEnableAec && canUseAcousticEchoCanceler(); 298 if (aec.setEnabled(enable) != AudioEffect.SUCCESS) { 299 Logging.e(TAG, "Failed to set the AcousticEchoCanceler state"); 300 } 301 Logging.d(TAG, "AcousticEchoCanceler: was " 302 + (enabled ? "enabled" : "disabled") 303 + ", enable: " + enable + ", is now: " 304 + (aec.getEnabled() ? "enabled" : "disabled")); 305 } else { 306 Logging.e(TAG, "Failed to create the AcousticEchoCanceler instance"); 307 } 308 } 309 310 if (isAutomaticGainControlSupported()) { 311 // Create an AutomaticGainControl and attach it to the AudioRecord on 312 // the specified audio session. 313 agc = AutomaticGainControl.create(audioSession); 314 if (agc != null) { 315 boolean enabled = agc.getEnabled(); 316 boolean enable = shouldEnableAgc && canUseAutomaticGainControl(); 317 if (agc.setEnabled(enable) != AudioEffect.SUCCESS) { 318 Logging.e(TAG, "Failed to set the AutomaticGainControl state"); 319 } 320 Logging.d(TAG, "AutomaticGainControl: was " 321 + (enabled ? "enabled" : "disabled") 322 + ", enable: " + enable + ", is now: " 323 + (agc.getEnabled() ? "enabled" : "disabled")); 324 } else { 325 Logging.e(TAG, "Failed to create the AutomaticGainControl instance"); 326 } 327 } 328 329 if (isNoiseSuppressorSupported()) { 330 // Create an NoiseSuppressor and attach it to the AudioRecord on the 331 // specified audio session. 332 ns = NoiseSuppressor.create(audioSession); 333 if (ns != null) { 334 boolean enabled = ns.getEnabled(); 335 boolean enable = shouldEnableNs && canUseNoiseSuppressor(); 336 if (ns.setEnabled(enable) != AudioEffect.SUCCESS) { 337 Logging.e(TAG, "Failed to set the NoiseSuppressor state"); 338 } 339 Logging.d(TAG, "NoiseSuppressor: was " 340 + (enabled ? "enabled" : "disabled") 341 + ", enable: " + enable + ", is now: " 342 + (ns.getEnabled() ? "enabled" : "disabled")); 343 } else { 344 Logging.e(TAG, "Failed to create the NoiseSuppressor instance"); 345 } 346 } 347 } 348 349 // Releases all native audio effect resources. It is a good practice to 350 // release the effect engine when not in use as control can be returned 351 // to other applications or the native resources released. release()352 public void release() { 353 Logging.d(TAG, "release"); 354 if (aec != null) { 355 aec.release(); 356 aec = null; 357 } 358 if (agc != null) { 359 agc.release(); 360 agc = null; 361 } 362 if (ns != null) { 363 ns.release(); 364 ns = null; 365 } 366 } 367 368 // Returns true for effect types in |type| that are of "VoIP" types: 369 // Acoustic Echo Canceler (AEC) or Automatic Gain Control (AGC) or 370 // Noise Suppressor (NS). Note that, an extra check for support is needed 371 // in each comparison since some devices includes effects in the 372 // AudioEffect.Descriptor array that are actually not available on the device. 373 // As an example: Samsung Galaxy S6 includes an AGC in the descriptor but 374 // AutomaticGainControl.isAvailable() returns false. 375 @TargetApi(18) effectTypeIsVoIP(UUID type)376 private boolean effectTypeIsVoIP(UUID type) { 377 if (!WebRtcAudioUtils.runningOnJellyBeanMR2OrHigher()) 378 return false; 379 380 return (AudioEffect.EFFECT_TYPE_AEC.equals(type) 381 && isAcousticEchoCancelerSupported()) 382 || (AudioEffect.EFFECT_TYPE_AGC.equals(type) 383 && isAutomaticGainControlSupported()) 384 || (AudioEffect.EFFECT_TYPE_NS.equals(type) 385 && isNoiseSuppressorSupported()); 386 } 387 388 // Helper method which throws an exception when an assertion has failed. assertTrue(boolean condition)389 private static void assertTrue(boolean condition) { 390 if (!condition) { 391 throw new AssertionError("Expected condition to be true"); 392 } 393 } 394 } 395