CubebUtils.cpp (36375B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "CubebUtils.h" 8 9 #include "audio_thread_priority.h" 10 #include "mozilla/AbstractThread.h" 11 #include "mozilla/Components.h" 12 #include "mozilla/Logging.h" 13 #include "mozilla/Preferences.h" 14 #include "mozilla/SharedThreadPool.h" 15 #include "mozilla/Sprintf.h" 16 #include "mozilla/StaticMutex.h" 17 #include "mozilla/StaticPtr.h" 18 #include "mozilla/UnderrunHandler.h" 19 #include "mozilla/dom/ContentChild.h" 20 #include "mozilla/glean/DomMediaMetrics.h" 21 #include "mozilla/ipc/FileDescriptor.h" 22 #if defined(MOZ_SANDBOX) 23 # include "mozilla/SandboxSettings.h" 24 #endif 25 #include <stdint.h> 26 27 #include <algorithm> 28 29 #include "nsContentUtils.h" 30 #include "nsDebug.h" 31 #include "nsIStringBundle.h" 32 #include "nsString.h" 33 #include "nsThreadUtils.h" 34 #include "prdtoa.h" 35 #ifdef MOZ_WIDGET_ANDROID 36 # include "mozilla/java/GeckoAppShellWrappers.h" 37 #endif 38 #ifdef XP_WIN 39 # include "mozilla/mscom/EnsureMTA.h" 40 #endif 41 #include <cmath> 42 #include <thread> 43 44 #include "CallbackThreadRegistry.h" 45 #include "CubebDeviceEnumerator.h" 46 #include "audioipc2_client_ffi_generated.h" 47 #include "audioipc2_server_ffi_generated.h" 48 #include "mozilla/StaticPrefs_media.h" 49 50 #if defined(ENABLE_TESTS) || defined(FUZZING) 51 # define ENABLE_MOCK_CUBEB 1 52 # include "MockCubeb.h" 53 #endif 54 55 #define AUDIOIPC_STACK_SIZE_DEFAULT (64 * 4096) 56 57 #define PREF_VOLUME_SCALE "media.volume_scale" 58 #define PREF_CUBEB_BACKEND "media.cubeb.backend" 59 #define PREF_CUBEB_OUTPUT_DEVICE "media.cubeb.output_device" 60 #define PREF_CUBEB_LATENCY_PLAYBACK "media.cubeb_latency_playback_ms" 61 #define PREF_CUBEB_LATENCY_MTG "media.cubeb_latency_mtg_frames" 62 // Allows to get something non-default for the preferred sample-rate, to allow 63 // troubleshooting in the field and testing. 64 #define PREF_CUBEB_FORCE_SAMPLE_RATE "media.cubeb.force_sample_rate" 65 #define PREF_CUBEB_LOGGING_LEVEL "logging.cubeb" 66 // Hidden pref used by tests to force failure to obtain cubeb context 67 #define PREF_CUBEB_FORCE_NULL_CONTEXT "media.cubeb.force_null_context" 68 #define PREF_CUBEB_FORCE_MOCK_CONTEXT "media.cubeb.force_mock_context" 69 #define PREF_CUBEB_OUTPUT_VOICE_ROUTING "media.cubeb.output_voice_routing" 70 #define PREF_CUBEB_SANDBOX "media.cubeb.sandbox" 71 #define PREF_AUDIOIPC_STACK_SIZE "media.audioipc.stack_size" 72 #define PREF_AUDIOIPC_SHM_AREA_SIZE "media.audioipc.shm_area_size" 73 74 #if defined(XP_LINUX) || defined(XP_MACOSX) || defined(XP_WIN) 75 # define MOZ_CUBEB_REMOTING 76 #endif 77 78 namespace mozilla { 79 80 namespace { 81 82 using glean::media_audio::BackendLabel; 83 84 LazyLogModule gCubebLog("cubeb"); 85 86 void CubebLogCallback(const char* aFmt, ...) { 87 char buffer[1024]; 88 89 va_list arglist; 90 va_start(arglist, aFmt); 91 VsprintfLiteral(buffer, aFmt, arglist); 92 MOZ_LOG(gCubebLog, LogLevel::Error, ("%s", buffer)); 93 va_end(arglist); 94 } 95 96 // This mutex protects the variables below. 97 StaticMutex sMutex; 98 enum class CubebState { 99 Uninitialized = 0, 100 Initialized, 101 Shutdown 102 } sCubebState = CubebState::Uninitialized; 103 StaticRefPtr<CubebUtils::CubebHandle> sCubebHandle; 104 double sVolumeScale = 1.0; 105 uint32_t sCubebPlaybackLatencyInMilliseconds = 100; 106 uint32_t sCubebMTGLatencyInFrames = 512; 107 // If sCubebForcedSampleRate is zero, PreferredSampleRate will return the 108 // preferred sample-rate for the audio backend in use. Otherwise, it will be 109 // used as the preferred sample-rate. 110 Atomic<uint32_t> sCubebForcedSampleRate{0}; 111 bool sCubebPlaybackLatencyPrefSet = false; 112 bool sCubebMTGLatencyPrefSet = false; 113 bool sAudioStreamInitEverSucceeded = false; 114 bool sCubebForceNullContext = false; 115 bool sRouteOutputAsVoice = false; 116 #ifdef MOZ_CUBEB_REMOTING 117 bool sCubebSandbox = false; 118 size_t sAudioIPCStackSize; 119 size_t sAudioIPCShmAreaSize; 120 #endif 121 StaticAutoPtr<char> sBrandName; 122 StaticAutoPtr<char> sCubebBackendName; 123 StaticAutoPtr<char> sCubebOutputDeviceName; 124 #ifdef MOZ_WIDGET_ANDROID 125 // Counts the number of time a request for switching to global "communication 126 // mode" has been received. If this is > 0, global communication mode is to be 127 // enabled. If it is 0, the global communication mode is to be disabled. 128 // This allows to correctly track the global behaviour to adopt accross 129 // asynchronous GraphDriver changes, on Android. 130 int sInCommunicationCount = 0; 131 #endif 132 133 const char kBrandBundleURL[] = "chrome://branding/locale/brand.properties"; 134 135 MOZ_RUNINIT std::unordered_map<std::string, BackendLabel> 136 kTelemetryBackendLabel = { 137 {"audiounit", BackendLabel::eAudiounit}, 138 {"audiounit-rust", BackendLabel::eAudiounitRust}, 139 {"aaudio", BackendLabel::eAaudio}, 140 {"opensl", BackendLabel::eOpensl}, 141 {"wasapi", BackendLabel::eWasapi}, 142 {"winmm", BackendLabel::eWinmm}, 143 {"alsa", BackendLabel::eAlsa}, 144 {"jack", BackendLabel::eJack}, 145 {"oss", BackendLabel::eOss}, 146 {"pulse", BackendLabel::ePulse}, 147 {"pulse-rust", BackendLabel::ePulseRust}, 148 {"sndio", BackendLabel::eSndio}, 149 {"sun", BackendLabel::eSunaudio}, 150 }; 151 152 // Prefered samplerate, in Hz (characteristic of the hardware, mixer, platform, 153 // and API used). 154 // 155 // sMutex protects *initialization* of this, which must be performed from each 156 // thread before fetching, after which it is safe to fetch without holding the 157 // mutex because it is only written once per process execution (by the first 158 // initialization to complete). Since the init must have been called on a 159 // given thread before fetching the value, it's guaranteed (via the mutex) that 160 // sufficient memory barriers have occurred to ensure the correct value is 161 // visible on the querying thread/CPU. 162 static Atomic<uint32_t> sPreferredSampleRate{0}; 163 164 #ifdef MOZ_CUBEB_REMOTING 165 // AudioIPC server handle 166 void* sServerHandle = nullptr; 167 168 // Initialized during early startup, protected by sMutex. 169 StaticAutoPtr<ipc::FileDescriptor> sIPCConnection; 170 171 static bool StartAudioIPCServer() { 172 if (sCubebSandbox) { 173 audioipc2::AudioIpcServerInitParams initParams{}; 174 initParams.mThreadCreateCallback = [](const char* aName) { 175 PROFILER_REGISTER_THREAD(aName); 176 }; 177 initParams.mThreadDestroyCallback = []() { PROFILER_UNREGISTER_THREAD(); }; 178 179 sServerHandle = audioipc2::audioipc2_server_start( 180 sBrandName, sCubebBackendName, &initParams); 181 } 182 return sServerHandle != nullptr; 183 } 184 185 static void ShutdownAudioIPCServer() { 186 if (!sServerHandle) { 187 return; 188 } 189 190 audioipc2::audioipc2_server_stop(sServerHandle); 191 sServerHandle = nullptr; 192 } 193 #endif // MOZ_CUBEB_REMOTING 194 } // namespace 195 196 static const uint32_t CUBEB_NORMAL_LATENCY_MS = 100; 197 // Consevative default that can work on all platforms. 198 static const uint32_t CUBEB_NORMAL_LATENCY_FRAMES = 1024; 199 200 namespace CubebUtils { 201 nsCString ProcessingParamsToString(cubeb_input_processing_params aParams) { 202 if (aParams == CUBEB_INPUT_PROCESSING_PARAM_NONE) { 203 return "NONE"_ns; 204 } 205 nsCString str; 206 for (auto p : {CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION, 207 CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL, 208 CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION, 209 CUBEB_INPUT_PROCESSING_PARAM_VOICE_ISOLATION}) { 210 if (!(aParams & p)) { 211 continue; 212 } 213 if (!str.IsEmpty()) { 214 str.Append(" | "); 215 } 216 str.Append([&p] { 217 switch (p) { 218 case CUBEB_INPUT_PROCESSING_PARAM_NONE: 219 // Handled above. 220 MOZ_CRASH( 221 "NONE is the absence of a param, thus not for logging here."); 222 case CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION: 223 return "AEC"; 224 case CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL: 225 return "AGC"; 226 case CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION: 227 return "NS"; 228 case CUBEB_INPUT_PROCESSING_PARAM_VOICE_ISOLATION: 229 return "VOICE"; 230 } 231 MOZ_ASSERT_UNREACHABLE("Unexpected input processing param"); 232 return "<Unknown input processing param>"; 233 }()); 234 } 235 return str; 236 } 237 238 RefPtr<CubebHandle> GetCubebUnlocked(); 239 240 void GetPrefAndSetString(const char* aPref, StaticAutoPtr<char>& aStorage) { 241 nsAutoCString value; 242 Preferences::GetCString(aPref, value); 243 if (value.IsEmpty()) { 244 aStorage = nullptr; 245 } else { 246 aStorage = new char[value.Length() + 1]; 247 PodCopy(aStorage.get(), value.get(), value.Length()); 248 aStorage[value.Length()] = 0; 249 } 250 } 251 252 void PrefChanged(const char* aPref, void* aClosure) { 253 if (strcmp(aPref, PREF_VOLUME_SCALE) == 0) { 254 nsAutoCString value; 255 Preferences::GetCString(aPref, value); 256 StaticMutexAutoLock lock(sMutex); 257 if (value.IsEmpty()) { 258 sVolumeScale = 1.0; 259 } else { 260 sVolumeScale = std::max<double>(0, PR_strtod(value.get(), nullptr)); 261 } 262 } else if (strcmp(aPref, PREF_CUBEB_LATENCY_PLAYBACK) == 0) { 263 StaticMutexAutoLock lock(sMutex); 264 // Arbitrary default stream latency of 100ms. The higher this 265 // value, the longer stream volume changes will take to become 266 // audible. 267 sCubebPlaybackLatencyPrefSet = Preferences::HasUserValue(aPref); 268 uint32_t value = Preferences::GetUint(aPref, CUBEB_NORMAL_LATENCY_MS); 269 sCubebPlaybackLatencyInMilliseconds = 270 std::min<uint32_t>(std::max<uint32_t>(value, 1), 1000); 271 } else if (strcmp(aPref, PREF_CUBEB_LATENCY_MTG) == 0) { 272 StaticMutexAutoLock lock(sMutex); 273 sCubebMTGLatencyPrefSet = Preferences::HasUserValue(aPref); 274 uint32_t value = Preferences::GetUint(aPref, CUBEB_NORMAL_LATENCY_FRAMES); 275 // 128 is the block size for the Web Audio API, which limits how low the 276 // latency can be here. 277 // We don't want to limit the upper limit too much, so that people can 278 // experiment. 279 sCubebMTGLatencyInFrames = 280 std::min<uint32_t>(std::max<uint32_t>(value, 128), 1e6); 281 } else if (strcmp(aPref, PREF_CUBEB_FORCE_SAMPLE_RATE) == 0) { 282 StaticMutexAutoLock lock(sMutex); 283 sCubebForcedSampleRate = Preferences::GetUint(aPref); 284 } else if (strcmp(aPref, PREF_CUBEB_LOGGING_LEVEL) == 0) { 285 LogLevel value = 286 ToLogLevel(Preferences::GetInt(aPref, 0 /* LogLevel::Disabled */)); 287 if (value == LogLevel::Verbose) { 288 cubeb_set_log_callback(CUBEB_LOG_VERBOSE, CubebLogCallback); 289 } else if (value == LogLevel::Debug) { 290 cubeb_set_log_callback(CUBEB_LOG_NORMAL, CubebLogCallback); 291 } else if (value == LogLevel::Disabled) { 292 cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr); 293 } 294 } else if (strcmp(aPref, PREF_CUBEB_BACKEND) == 0) { 295 StaticMutexAutoLock lock(sMutex); 296 GetPrefAndSetString(aPref, sCubebBackendName); 297 } else if (strcmp(aPref, PREF_CUBEB_OUTPUT_DEVICE) == 0) { 298 StaticMutexAutoLock lock(sMutex); 299 GetPrefAndSetString(aPref, sCubebOutputDeviceName); 300 } else if (strcmp(aPref, PREF_CUBEB_FORCE_NULL_CONTEXT) == 0) { 301 StaticMutexAutoLock lock(sMutex); 302 sCubebForceNullContext = Preferences::GetBool(aPref, false); 303 MOZ_LOG(gCubebLog, LogLevel::Verbose, 304 ("%s: %s", PREF_CUBEB_FORCE_NULL_CONTEXT, 305 sCubebForceNullContext ? "true" : "false")); 306 } 307 #ifdef ENABLE_MOCK_CUBEB 308 else if (strcmp(aPref, PREF_CUBEB_FORCE_MOCK_CONTEXT) == 0) { 309 if (Preferences::GetBool(aPref, false)) { 310 MockCubeb* mock = new MockCubeb(); 311 constexpr const char* kGroupIds[] = {"group_id_1", "group_id_2", 312 "group_id_3"}; 313 { 314 constexpr size_t kNumDevices = 3; 315 constexpr const char* kDeviceIds[] = {"mock_input_1", "mock_input_2", 316 "mock_input_3"}; 317 constexpr const char* kDeviceNames[] = { 318 "Fake Audio Input 1", "Fake Audio Input 2 (PREFERRED)", 319 "Fake Audio Input 3"}; 320 for (size_t i = 0; i < kNumDevices; ++i) { 321 cubeb_device_info devinfo{ 322 .devid = (cubeb_devid)(i + 1), 323 .device_id = kDeviceIds[i], 324 .friendly_name = kDeviceNames[i], 325 .group_id = kGroupIds[i], 326 .vendor_name = "Mozilla", 327 .type = CUBEB_DEVICE_TYPE_INPUT, 328 .state = CUBEB_DEVICE_STATE_ENABLED, 329 .preferred = 330 i == 1 ? CUBEB_DEVICE_PREF_ALL : CUBEB_DEVICE_PREF_NONE, 331 .format = CUBEB_DEVICE_FMT_F32NE, 332 .default_format = CUBEB_DEVICE_FMT_F32NE, 333 .max_channels = 2, 334 .default_rate = 44100, 335 .max_rate = 44100, 336 .min_rate = 16000, 337 .latency_lo = 256, 338 .latency_hi = 1024, 339 }; 340 mock->AddDevice(devinfo); 341 } 342 } 343 344 { 345 constexpr size_t kNumDevices = 2; 346 constexpr const char* kDeviceIds[] = {"mock_output_0", "mock_output_1"}; 347 constexpr const char* kDeviceNames[] = { 348 "Fake Audio Output 1 (PREFERRED)", "Fake Audio Output 2"}; 349 for (size_t i = 0; i < kNumDevices; ++i) { 350 cubeb_device_info devinfo{ 351 .devid = (cubeb_devid)(i + 1), 352 .device_id = kDeviceIds[i], 353 .friendly_name = kDeviceNames[i], 354 .group_id = kGroupIds[i], 355 .vendor_name = "Mozilla", 356 .type = CUBEB_DEVICE_TYPE_OUTPUT, 357 .state = CUBEB_DEVICE_STATE_ENABLED, 358 .preferred = 359 i == 0 ? CUBEB_DEVICE_PREF_ALL : CUBEB_DEVICE_PREF_NONE, 360 .format = CUBEB_DEVICE_FMT_F32NE, 361 .default_format = CUBEB_DEVICE_FMT_F32NE, 362 .max_channels = 2, 363 .default_rate = 44100, 364 .max_rate = 44100, 365 .min_rate = 16000, 366 .latency_lo = 256, 367 .latency_hi = 1024, 368 }; 369 mock->AddDevice(devinfo); 370 } 371 } 372 ForceSetCubebContext(mock->AsCubebContext()); 373 } else { 374 ForceUnsetCubebContext(); 375 } 376 } 377 #endif 378 #ifdef MOZ_CUBEB_REMOTING 379 else if (strcmp(aPref, PREF_CUBEB_SANDBOX) == 0) { 380 StaticMutexAutoLock lock(sMutex); 381 sCubebSandbox = Preferences::GetBool(aPref); 382 MOZ_LOG(gCubebLog, LogLevel::Verbose, 383 ("%s: %s", PREF_CUBEB_SANDBOX, sCubebSandbox ? "true" : "false")); 384 # if defined(MOZ_SANDBOX) 385 if (!sCubebSandbox && IsContentSandboxEnabled()) { 386 sCubebSandbox = true; 387 MOZ_LOG(gCubebLog, LogLevel::Error, 388 ("%s: false, but content sandbox enabled - forcing true", 389 PREF_CUBEB_SANDBOX)); 390 } 391 # endif 392 } else if (strcmp(aPref, PREF_AUDIOIPC_STACK_SIZE) == 0) { 393 StaticMutexAutoLock lock(sMutex); 394 sAudioIPCStackSize = Preferences::GetUint(PREF_AUDIOIPC_STACK_SIZE, 395 AUDIOIPC_STACK_SIZE_DEFAULT); 396 } else if (strcmp(aPref, PREF_AUDIOIPC_SHM_AREA_SIZE) == 0) { 397 StaticMutexAutoLock lock(sMutex); 398 sAudioIPCShmAreaSize = Preferences::GetUint(PREF_AUDIOIPC_SHM_AREA_SIZE); 399 } 400 #endif 401 else if (strcmp(aPref, PREF_CUBEB_OUTPUT_VOICE_ROUTING) == 0) { 402 StaticMutexAutoLock lock(sMutex); 403 sRouteOutputAsVoice = Preferences::GetBool(aPref); 404 MOZ_LOG(gCubebLog, LogLevel::Verbose, 405 ("%s: %s", PREF_CUBEB_OUTPUT_VOICE_ROUTING, 406 sRouteOutputAsVoice ? "true" : "false")); 407 } 408 } 409 410 bool GetFirstStream() { 411 static bool sFirstStream = true; 412 413 StaticMutexAutoLock lock(sMutex); 414 bool result = sFirstStream; 415 sFirstStream = false; 416 return result; 417 } 418 419 double GetVolumeScale() { 420 StaticMutexAutoLock lock(sMutex); 421 return sVolumeScale; 422 } 423 424 RefPtr<CubebHandle> GetCubeb() { 425 StaticMutexAutoLock lock(sMutex); 426 return GetCubebUnlocked(); 427 } 428 429 #ifdef ENABLE_MOCK_CUBEB 430 // This is only exported when running tests. 431 void ForceSetCubebContext(cubeb* aCubebContext) { 432 RefPtr<CubebHandle> oldHandle; // For release without sMutex 433 { 434 StaticMutexAutoLock lock(sMutex); 435 oldHandle = sCubebHandle.forget(); 436 sCubebHandle = aCubebContext ? new CubebHandle(aCubebContext) : nullptr; 437 sCubebState = CubebState::Initialized; 438 } 439 CubebDeviceEnumerator::Shutdown(); 440 } 441 442 void ForceUnsetCubebContext() { 443 RefPtr<CubebHandle> oldHandle; // For release without sMutex 444 StaticMutexAutoLock lock(sMutex); 445 oldHandle = sCubebHandle.forget(); 446 sCubebState = CubebState::Uninitialized; 447 } 448 #endif 449 450 void SetInCommunication(bool aInCommunication) { 451 #ifdef MOZ_WIDGET_ANDROID 452 StaticMutexAutoLock lock(sMutex); 453 if (aInCommunication) { 454 sInCommunicationCount++; 455 } else { 456 MOZ_ASSERT(sInCommunicationCount > 0); 457 sInCommunicationCount--; 458 } 459 460 if (sInCommunicationCount == 1) { 461 java::GeckoAppShell::SetCommunicationAudioModeOn(true); 462 } else if (sInCommunicationCount == 0) { 463 java::GeckoAppShell::SetCommunicationAudioModeOn(false); 464 } 465 #endif 466 } 467 468 bool InitPreferredSampleRate() MOZ_REQUIRES(sMutex) { 469 sMutex.AssertCurrentThreadOwns(); 470 if (sPreferredSampleRate != 0) { 471 return true; 472 } 473 #ifdef MOZ_WIDGET_ANDROID 474 int rate = AndroidGetAudioOutputSampleRate(); 475 if (rate > 0) { 476 sPreferredSampleRate = rate; 477 return true; 478 } else { 479 return false; 480 } 481 #else 482 RefPtr<CubebHandle> handle = GetCubebUnlocked(); 483 if (!handle) { 484 return false; 485 } 486 uint32_t rate; 487 { 488 StaticMutexAutoUnlock unlock(sMutex); 489 if (cubeb_get_preferred_sample_rate(handle->Context(), &rate) != CUBEB_OK) { 490 return false; 491 } 492 } 493 sPreferredSampleRate = rate; 494 #endif 495 MOZ_ASSERT(sPreferredSampleRate); 496 return true; 497 } 498 499 uint32_t PreferredSampleRate(bool aShouldResistFingerprinting) { 500 StaticMutexAutoLock lock(sMutex); 501 if (sCubebForcedSampleRate) { 502 return sCubebForcedSampleRate; 503 } 504 if (aShouldResistFingerprinting) { 505 return 44100; 506 } 507 if (!InitPreferredSampleRate()) { 508 return 44100; 509 } 510 MOZ_ASSERT(sPreferredSampleRate); 511 return sPreferredSampleRate; 512 } 513 514 int CubebStreamInit(cubeb* context, cubeb_stream** stream, 515 char const* stream_name, cubeb_devid input_device, 516 cubeb_stream_params* input_stream_params, 517 cubeb_devid output_device, 518 cubeb_stream_params* output_stream_params, 519 uint32_t latency_frames, cubeb_data_callback data_callback, 520 cubeb_state_callback state_callback, void* user_ptr) { 521 uint32_t ms = StaticPrefs::media_cubeb_slow_stream_init_ms(); 522 if (ms) { 523 std::this_thread::sleep_for(std::chrono::milliseconds(ms)); 524 } 525 cubeb_stream_params inputParamData; 526 cubeb_stream_params outputParamData; 527 cubeb_stream_params* inputParamPtr = input_stream_params; 528 cubeb_stream_params* outputParamPtr = output_stream_params; 529 if (input_stream_params && !output_stream_params) { 530 inputParamData = *input_stream_params; 531 inputParamData.rate = llround( 532 static_cast<double>(StaticPrefs::media_cubeb_input_drift_factor()) * 533 inputParamData.rate); 534 MOZ_LOG( 535 gCubebLog, LogLevel::Info, 536 ("CubebStreamInit input stream rate %" PRIu32, inputParamData.rate)); 537 inputParamPtr = &inputParamData; 538 } else if (output_stream_params && !input_stream_params) { 539 outputParamData = *output_stream_params; 540 outputParamData.rate = llround( 541 static_cast<double>(StaticPrefs::media_cubeb_output_drift_factor()) * 542 outputParamData.rate); 543 MOZ_LOG( 544 gCubebLog, LogLevel::Info, 545 ("CubebStreamInit output stream rate %" PRIu32, outputParamData.rate)); 546 outputParamPtr = &outputParamData; 547 } 548 549 return cubeb_stream_init( 550 context, stream, stream_name, input_device, inputParamPtr, output_device, 551 outputParamPtr, latency_frames, data_callback, state_callback, user_ptr); 552 } 553 554 void InitBrandName() { 555 if (sBrandName) { 556 return; 557 } 558 nsAutoString brandName; 559 nsCOMPtr<nsIStringBundleService> stringBundleService = 560 mozilla::components::StringBundle::Service(); 561 if (stringBundleService) { 562 nsCOMPtr<nsIStringBundle> brandBundle; 563 nsresult rv = stringBundleService->CreateBundle( 564 kBrandBundleURL, getter_AddRefs(brandBundle)); 565 if (NS_SUCCEEDED(rv)) { 566 rv = brandBundle->GetStringFromName("brandShortName", brandName); 567 NS_WARNING_ASSERTION( 568 NS_SUCCEEDED(rv), 569 "Could not get the program name for a cubeb stream."); 570 } 571 } 572 NS_LossyConvertUTF16toASCII ascii(brandName); 573 sBrandName = new char[ascii.Length() + 1]; 574 PodCopy(sBrandName.get(), ascii.get(), ascii.Length()); 575 sBrandName[ascii.Length()] = 0; 576 } 577 578 #ifdef MOZ_CUBEB_REMOTING 579 void InitAudioIPCConnection() { 580 MOZ_ASSERT(NS_IsMainThread()); 581 auto contentChild = dom::ContentChild::GetSingleton(); 582 auto promise = contentChild->SendCreateAudioIPCConnection(); 583 promise->Then( 584 AbstractThread::MainThread(), __func__, 585 [](dom::FileDescOrError&& aFD) { 586 StaticMutexAutoLock lock(sMutex); 587 MOZ_ASSERT(!sIPCConnection); 588 if (aFD.type() == dom::FileDescOrError::Type::TFileDescriptor) { 589 sIPCConnection = new ipc::FileDescriptor(std::move(aFD)); 590 } else { 591 MOZ_LOG(gCubebLog, LogLevel::Error, 592 ("SendCreateAudioIPCConnection failed: invalid FD")); 593 } 594 }, 595 [](mozilla::ipc::ResponseRejectReason&& aReason) { 596 MOZ_LOG(gCubebLog, LogLevel::Error, 597 ("SendCreateAudioIPCConnection rejected: %d", int(aReason))); 598 }); 599 } 600 #endif 601 602 #ifdef MOZ_CUBEB_REMOTING 603 ipc::FileDescriptor CreateAudioIPCConnectionUnlocked() { 604 MOZ_ASSERT(sCubebSandbox && XRE_IsParentProcess()); 605 if (!sServerHandle) { 606 MOZ_LOG(gCubebLog, LogLevel::Debug, ("Starting cubeb server...")); 607 if (!StartAudioIPCServer()) { 608 MOZ_LOG(gCubebLog, LogLevel::Error, ("audioipc_server_start failed")); 609 return ipc::FileDescriptor(); 610 } 611 } 612 MOZ_LOG(gCubebLog, LogLevel::Debug, 613 ("%s: %d", PREF_AUDIOIPC_SHM_AREA_SIZE, (int)sAudioIPCShmAreaSize)); 614 MOZ_ASSERT(sServerHandle); 615 ipc::FileDescriptor::PlatformHandleType rawFD; 616 rawFD = audioipc2::audioipc2_server_new_client(sServerHandle, 617 sAudioIPCShmAreaSize); 618 ipc::FileDescriptor fd(rawFD); 619 if (!fd.IsValid()) { 620 MOZ_LOG(gCubebLog, LogLevel::Error, ("audioipc_server_new_client failed")); 621 return ipc::FileDescriptor(); 622 } 623 // Close rawFD since FileDescriptor's ctor cloned it. 624 // TODO: Find cleaner cross-platform way to close rawFD. 625 # ifdef XP_WIN 626 CloseHandle(rawFD); 627 # else 628 close(rawFD); 629 # endif 630 return fd; 631 } 632 #endif 633 634 ipc::FileDescriptor CreateAudioIPCConnection() { 635 #ifdef MOZ_CUBEB_REMOTING 636 StaticMutexAutoLock lock(sMutex); 637 return CreateAudioIPCConnectionUnlocked(); 638 #else 639 return ipc::FileDescriptor(); 640 #endif 641 } 642 643 RefPtr<CubebHandle> GetCubebUnlocked() { 644 sMutex.AssertCurrentThreadOwns(); 645 if (sCubebForceNullContext) { 646 // Pref set such that we should return a null context 647 MOZ_LOG(gCubebLog, LogLevel::Debug, 648 ("%s: returning null context due to %s!", __func__, 649 PREF_CUBEB_FORCE_NULL_CONTEXT)); 650 return nullptr; 651 } 652 if (sCubebState != CubebState::Uninitialized) { 653 // If we have already passed the initialization point (below), just return 654 // the current context, which may be null (e.g., after error or shutdown.) 655 return sCubebHandle; 656 } 657 658 if (!sBrandName && NS_IsMainThread()) { 659 InitBrandName(); 660 } else { 661 NS_WARNING_ASSERTION( 662 sBrandName, 663 "Did not initialize sbrandName, and not on the main thread?"); 664 } 665 666 int rv = CUBEB_ERROR; 667 #ifdef MOZ_CUBEB_REMOTING 668 MOZ_LOG(gCubebLog, LogLevel::Info, 669 ("%s: %s", PREF_CUBEB_SANDBOX, sCubebSandbox ? "true" : "false")); 670 671 if (sCubebSandbox) { 672 if (XRE_IsParentProcess() && !sIPCConnection) { 673 // TODO: Don't use audio IPC when within the same process. 674 auto fd = CreateAudioIPCConnectionUnlocked(); 675 if (fd.IsValid()) { 676 sIPCConnection = new ipc::FileDescriptor(fd); 677 } 678 } 679 if (NS_WARN_IF(!sIPCConnection)) { 680 // Either the IPC connection failed to init or we're still waiting for 681 // InitAudioIPCConnection to complete (bug 1454782). 682 return nullptr; 683 } 684 685 MOZ_LOG(gCubebLog, LogLevel::Debug, 686 ("%s: %d", PREF_AUDIOIPC_STACK_SIZE, (int)sAudioIPCStackSize)); 687 688 audioipc2::AudioIpcInitParams initParams{}; 689 initParams.mStackSize = sAudioIPCStackSize; 690 initParams.mServerConnection = 691 sIPCConnection->ClonePlatformHandle().release(); 692 initParams.mThreadCreateCallback = [](const char* aName) { 693 PROFILER_REGISTER_THREAD(aName); 694 }; 695 initParams.mThreadDestroyCallback = []() { PROFILER_UNREGISTER_THREAD(); }; 696 697 cubeb* temp = nullptr; 698 rv = audioipc2::audioipc2_client_init(&temp, sBrandName, &initParams); 699 if (temp) { 700 sCubebHandle = new CubebHandle(temp); 701 } 702 } else { 703 #endif // MOZ_CUBEB_REMOTING 704 #ifdef XP_WIN 705 mozilla::mscom::EnsureMTA([&]() -> void { 706 #endif 707 cubeb* temp = nullptr; 708 rv = cubeb_init(&temp, sBrandName, sCubebBackendName); 709 if (temp) { 710 sCubebHandle = new CubebHandle(temp); 711 } 712 #ifdef XP_WIN 713 }); 714 #endif 715 #ifdef MOZ_CUBEB_REMOTING 716 } 717 sIPCConnection = nullptr; 718 #endif // MOZ_CUBEB_REMOTING 719 NS_WARNING_ASSERTION(rv == CUBEB_OK, "Could not get a cubeb context."); 720 sCubebState = 721 (rv == CUBEB_OK) ? CubebState::Initialized : CubebState::Uninitialized; 722 723 return sCubebHandle; 724 } 725 726 void ReportCubebBackendUsed() { 727 RefPtr<CubebHandle> handle; 728 { 729 StaticMutexAutoLock lock(sMutex); 730 sAudioStreamInitEverSucceeded = true; 731 handle = sCubebHandle; 732 } 733 734 MOZ_RELEASE_ASSERT(handle.get()); 735 736 BackendLabel label = BackendLabel::eUnknown; 737 auto backend = 738 kTelemetryBackendLabel.find(cubeb_get_backend_id(handle->Context())); 739 if (backend != kTelemetryBackendLabel.end()) { 740 label = backend->second; 741 } 742 743 mozilla::glean::media_audio::backend.EnumGet(label).Add(); 744 } 745 746 void ReportCubebStreamInitFailure(bool aIsFirst) { 747 StaticMutexAutoLock lock(sMutex); 748 if (!aIsFirst && !sAudioStreamInitEverSucceeded) { 749 // This machine has no audio hardware, or it's in really bad shape, don't 750 // send this info, since we want CUBEB_BACKEND_INIT_FAILURE_OTHER to detect 751 // failures to open multiple streams in a process over time. 752 return; 753 } 754 mozilla::glean::media_audio::init_failure 755 .EnumGet(aIsFirst ? mozilla::glean::media_audio::InitFailureLabel::eFirst 756 : mozilla::glean::media_audio::InitFailureLabel::eOther) 757 .Add(); 758 } 759 760 uint32_t GetCubebPlaybackLatencyInMilliseconds() { 761 StaticMutexAutoLock lock(sMutex); 762 return sCubebPlaybackLatencyInMilliseconds; 763 } 764 765 bool CubebPlaybackLatencyPrefSet() { 766 StaticMutexAutoLock lock(sMutex); 767 return sCubebPlaybackLatencyPrefSet; 768 } 769 770 bool CubebMTGLatencyPrefSet() { 771 StaticMutexAutoLock lock(sMutex); 772 return sCubebMTGLatencyPrefSet; 773 } 774 775 uint32_t GetCubebMTGLatencyInFrames(cubeb_stream_params* params) { 776 StaticMutexAutoLock lock(sMutex); 777 if (sCubebMTGLatencyPrefSet) { 778 MOZ_ASSERT(sCubebMTGLatencyInFrames > 0); 779 return sCubebMTGLatencyInFrames; 780 } 781 782 #ifdef MOZ_WIDGET_ANDROID 783 int32_t frames = AndroidGetAudioOutputFramesPerBuffer(); 784 // Allow extra time until audioipc threads are scheduled with higher 785 // priority (bug 1931080). 768 was not sufficient on a Samsung SM-A528B 786 // when switching to the home screen. 787 return std::max(1024, frames); 788 #else 789 RefPtr<CubebHandle> handle = GetCubebUnlocked(); 790 if (!handle) { 791 return sCubebMTGLatencyInFrames; // default 512 792 } 793 uint32_t latency_frames = 0; 794 int cubeb_result = CUBEB_OK; 795 796 { 797 StaticMutexAutoUnlock unlock(sMutex); 798 cubeb_result = 799 cubeb_get_min_latency(handle->Context(), params, &latency_frames); 800 } 801 802 if (cubeb_result != CUBEB_OK) { 803 NS_WARNING("Could not get minimal latency from cubeb."); 804 return sCubebMTGLatencyInFrames; // default 512 805 } 806 return latency_frames; 807 #endif 808 } 809 810 static const char* gInitCallbackPrefs[] = { 811 PREF_VOLUME_SCALE, 812 PREF_CUBEB_OUTPUT_DEVICE, 813 PREF_CUBEB_LATENCY_PLAYBACK, 814 PREF_CUBEB_LATENCY_MTG, 815 PREF_CUBEB_BACKEND, 816 PREF_CUBEB_FORCE_SAMPLE_RATE, 817 PREF_CUBEB_FORCE_NULL_CONTEXT, 818 PREF_CUBEB_FORCE_MOCK_CONTEXT, 819 PREF_CUBEB_SANDBOX, 820 PREF_AUDIOIPC_STACK_SIZE, 821 PREF_AUDIOIPC_SHM_AREA_SIZE, 822 nullptr, 823 }; 824 825 static const char* gCallbackPrefs[] = { 826 // We don't want to call the callback on startup, because the pref is the 827 // empty string by default ("", which means "logging disabled"). Because the 828 // logging can be enabled via environment variables (MOZ_LOG="module:5"), 829 // calling this callback on init would immediately re-disable the logging. 830 PREF_CUBEB_LOGGING_LEVEL, 831 nullptr, 832 }; 833 834 void InitLibrary() { 835 Preferences::RegisterCallbacksAndCall(PrefChanged, gInitCallbackPrefs); 836 Preferences::RegisterCallbacks(PrefChanged, gCallbackPrefs); 837 838 if (MOZ_LOG_TEST(gCubebLog, LogLevel::Verbose)) { 839 cubeb_set_log_callback(CUBEB_LOG_VERBOSE, CubebLogCallback); 840 } else if (MOZ_LOG_TEST(gCubebLog, LogLevel::Error)) { 841 cubeb_set_log_callback(CUBEB_LOG_NORMAL, CubebLogCallback); 842 } 843 844 #ifndef MOZ_WIDGET_ANDROID 845 NS_DispatchToMainThread( 846 NS_NewRunnableFunction("CubebUtils::InitLibrary", &InitBrandName)); 847 #endif 848 #ifdef MOZ_CUBEB_REMOTING 849 if (sCubebSandbox && XRE_IsContentProcess()) { 850 # if defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID) 851 if (atp_set_real_time_limit(0, 48000)) { 852 MOZ_LOG(gCubebLog, LogLevel::Warning, 853 ("could not set real-time limit in CubebUtils::InitLibrary")); 854 } 855 InstallSoftRealTimeLimitHandler(); 856 # endif 857 InitAudioIPCConnection(); 858 } 859 #endif 860 861 // Ensure the CallbackThreadRegistry is not created in an audio callback by 862 // creating it now. 863 (void)CallbackThreadRegistry::Get(); 864 } 865 866 void ShutdownLibrary() { 867 Preferences::UnregisterCallbacks(PrefChanged, gInitCallbackPrefs); 868 Preferences::UnregisterCallbacks(PrefChanged, gCallbackPrefs); 869 870 cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr); 871 RefPtr<CubebHandle> trash; 872 StaticMutexAutoLock lock(sMutex); 873 trash = sCubebHandle.forget(); 874 sBrandName = nullptr; 875 sCubebBackendName = nullptr; 876 // This will ensure we don't try to re-create a context. 877 sCubebState = CubebState::Shutdown; 878 879 if (trash) { 880 StaticMutexAutoUnlock unlock(sMutex); 881 nsrefcnt count = trash.forget().take()->Release(); 882 MOZ_RELEASE_ASSERT(!count, 883 "ShutdownLibrary should be releasing the last reference " 884 "to the cubeb ctx!"); 885 } 886 887 #ifdef MOZ_CUBEB_REMOTING 888 sIPCConnection = nullptr; 889 ShutdownAudioIPCServer(); 890 #endif 891 } 892 893 bool SandboxEnabled() { 894 #ifdef MOZ_CUBEB_REMOTING 895 StaticMutexAutoLock lock(sMutex); 896 return !!sCubebSandbox; 897 #else 898 return false; 899 #endif 900 } 901 902 already_AddRefed<SharedThreadPool> GetCubebOperationThread() { 903 RefPtr<SharedThreadPool> pool = SharedThreadPool::Get("CubebOperation"_ns, 1); 904 const uint32_t kIdleThreadTimeoutMs = 2000; 905 pool->SetIdleThreadMaximumTimeout( 906 PR_MillisecondsToInterval(kIdleThreadTimeoutMs)); 907 return pool.forget(); 908 } 909 910 uint32_t MaxNumberOfChannels() { 911 RefPtr<CubebHandle> handle = GetCubeb(); 912 uint32_t maxNumberOfChannels; 913 if (handle && cubeb_get_max_channel_count(handle->Context(), 914 &maxNumberOfChannels) == CUBEB_OK) { 915 return maxNumberOfChannels; 916 } 917 918 return 0; 919 } 920 921 void GetCurrentBackend(nsAString& aBackend) { 922 RefPtr<CubebHandle> handle = GetCubeb(); 923 if (handle) { 924 const char* backend = cubeb_get_backend_id(handle->Context()); 925 if (backend) { 926 aBackend.AssignASCII(backend); 927 return; 928 } 929 } 930 aBackend.AssignLiteral("unknown"); 931 } 932 933 char* GetForcedOutputDevice() { 934 StaticMutexAutoLock lock(sMutex); 935 return sCubebOutputDeviceName; 936 } 937 938 cubeb_stream_prefs GetDefaultStreamPrefs(cubeb_device_type aType) { 939 cubeb_stream_prefs prefs = CUBEB_STREAM_PREF_NONE; 940 #ifdef XP_WIN 941 if (StaticPrefs::media_cubeb_wasapi_raw() & static_cast<uint32_t>(aType)) { 942 prefs |= CUBEB_STREAM_PREF_RAW; 943 } 944 #endif 945 return prefs; 946 } 947 948 bool RouteOutputAsVoice() { return sRouteOutputAsVoice; } 949 950 long datacb(cubeb_stream*, void*, const void*, void* out_buffer, long nframes) { 951 PodZero(static_cast<float*>(out_buffer), nframes * 2); 952 return nframes; 953 } 954 955 void statecb(cubeb_stream*, void*, cubeb_state) {} 956 957 bool EstimatedLatencyDefaultDevices(double* aMean, double* aStdDev, 958 Side aSide) { 959 RefPtr<CubebHandle> handle = GetCubeb(); 960 if (!handle) { 961 MOZ_LOG(gCubebLog, LogLevel::Error, ("No cubeb context, bailing.")); 962 return false; 963 } 964 bool includeInput = aSide & Side::Input; 965 bool includeOutput = aSide & Side::Output; 966 nsTArray<double> latencies; 967 // Create a cubeb stream with the correct latency and default input/output 968 // devices (mono/stereo channels). Wait for two seconds, get the latency a few 969 // times. 970 int rv; 971 uint32_t rate; 972 uint32_t latencyFrames; 973 rv = cubeb_get_preferred_sample_rate(handle->Context(), &rate); 974 if (rv != CUBEB_OK) { 975 MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not get preferred rate")); 976 return false; 977 } 978 979 cubeb_stream_params output_params; 980 output_params.format = CUBEB_SAMPLE_FLOAT32NE; 981 output_params.rate = rate; 982 output_params.channels = 2; 983 output_params.layout = CUBEB_LAYOUT_UNDEFINED; 984 output_params.prefs = GetDefaultStreamPrefs(CUBEB_DEVICE_TYPE_OUTPUT); 985 output_params.input_params = CUBEB_INPUT_PROCESSING_PARAM_NONE; 986 987 latencyFrames = GetCubebMTGLatencyInFrames(&output_params); 988 989 cubeb_stream_params input_params; 990 input_params.format = CUBEB_SAMPLE_FLOAT32NE; 991 input_params.rate = rate; 992 input_params.channels = 1; 993 input_params.layout = CUBEB_LAYOUT_UNDEFINED; 994 input_params.prefs = GetDefaultStreamPrefs(CUBEB_DEVICE_TYPE_INPUT); 995 input_params.input_params = CUBEB_INPUT_PROCESSING_PARAM_NONE; 996 997 cubeb_stream* stm; 998 rv = cubeb_stream_init(handle->Context(), &stm, 999 "about:support latency estimation", NULL, 1000 &input_params, NULL, &output_params, latencyFrames, 1001 datacb, statecb, NULL); 1002 if (rv != CUBEB_OK) { 1003 MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not get init stream")); 1004 return false; 1005 } 1006 1007 rv = cubeb_stream_start(stm); 1008 if (rv != CUBEB_OK) { 1009 MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not start stream")); 1010 return false; 1011 } 1012 // +-2s 1013 for (uint32_t i = 0; i < 40; i++) { 1014 std::this_thread::sleep_for(std::chrono::milliseconds(50)); 1015 uint32_t inputLatency, outputLatency, rvIn, rvOut; 1016 rvOut = cubeb_stream_get_latency(stm, &outputLatency); 1017 if (rvOut) { 1018 MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not get output latency")); 1019 } 1020 rvIn = cubeb_stream_get_input_latency(stm, &inputLatency); 1021 if (rvIn) { 1022 MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not get input latency")); 1023 } 1024 if (rvIn != CUBEB_OK || rvOut != CUBEB_OK) { 1025 continue; 1026 } 1027 1028 double latency = static_cast<double>((includeInput ? inputLatency : 0) + 1029 (includeOutput ? outputLatency : 0)) / 1030 rate; 1031 latencies.AppendElement(latency); 1032 } 1033 rv = cubeb_stream_stop(stm); 1034 if (rv != CUBEB_OK) { 1035 MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not stop the stream")); 1036 } 1037 1038 *aMean = 0.0; 1039 *aStdDev = 0.0; 1040 double variance = 0.0; 1041 for (uint32_t i = 0; i < latencies.Length(); i++) { 1042 *aMean += latencies[i]; 1043 } 1044 1045 *aMean /= latencies.Length(); 1046 1047 for (uint32_t i = 0; i < latencies.Length(); i++) { 1048 variance += pow(latencies[i] - *aMean, 2.); 1049 } 1050 variance /= latencies.Length(); 1051 1052 *aStdDev = sqrt(variance); 1053 1054 MOZ_LOG(gCubebLog, LogLevel::Debug, 1055 ("Default devices latency in seconds %lf (stddev: %lf)", *aMean, 1056 *aStdDev)); 1057 1058 cubeb_stream_destroy(stm); 1059 1060 return true; 1061 } 1062 1063 #ifdef MOZ_WIDGET_ANDROID 1064 int32_t AndroidGetAudioOutputSampleRate() { 1065 if (java::GeckoAppShell::IsIsolatedProcess()) { 1066 return 44100; // TODO: Remote value; will be handled in following patch. 1067 } 1068 1069 int32_t sample_rate = java::GeckoAppShell::GetAudioOutputSampleRate(); 1070 return sample_rate; 1071 } 1072 int32_t AndroidGetAudioOutputFramesPerBuffer() { 1073 if (java::GeckoAppShell::IsIsolatedProcess()) { 1074 return 512; // TODO: Remote value; will be handled in following patch. 1075 } 1076 int32_t frames = java::GeckoAppShell::GetAudioOutputFramesPerBuffer(); 1077 return frames; 1078 } 1079 #endif 1080 1081 } // namespace CubebUtils 1082 } // namespace mozilla 1083 1084 #undef ENABLE_MOCK_CUBEB