/* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ #include "modules/audio_device/win/core_audio_base_win.h" #include #include #include "absl/strings/string_view.h" #include "modules/audio_device/audio_device_buffer.h" #include "rtc_base/arraysize.h" #include "rtc_base/checks.h" #include "rtc_base/logging.h" #include "rtc_base/numerics/safe_conversions.h" #include "rtc_base/platform_thread.h" #include "rtc_base/time_utils.h" #include "rtc_base/win/scoped_com_initializer.h" #include "rtc_base/win/windows_version.h" using Microsoft::WRL::ComPtr; namespace webrtc { namespace webrtc_win { namespace { // Even if the device supports low latency and even if IAudioClient3 can be // used (requires Win10 or higher), we currently disable any attempts to // initialize the client for low-latency. // TODO(henrika): more research is needed before we can enable low-latency. const bool kEnableLowLatencyIfSupported = false; // Each unit of reference time is 100 nanoseconds, hence `kReftimesPerSec` // corresponds to one second. // TODO(henrika): possibly add usage in Init(). // const REFERENCE_TIME kReferenceTimesPerSecond = 10000000; enum DefaultDeviceType { kUndefined = -1, kDefault = 0, kDefaultCommunications = 1, kDefaultDeviceTypeMaxCount = kDefaultCommunications + 1, }; const char* DirectionToString(CoreAudioBase::Direction direction) { switch (direction) { case CoreAudioBase::Direction::kOutput: return "Output"; case CoreAudioBase::Direction::kInput: return "Input"; default: return "Unkown"; } } const char* RoleToString(const ERole role) { switch (role) { case eConsole: return "Console"; case eMultimedia: return "Multimedia"; case eCommunications: return "Communications"; default: return "Unsupported"; } } std::string IndexToString(int index) { std::string ss = std::to_string(index); switch (index) { case kDefault: ss += " (Default)"; break; case kDefaultCommunications: ss += " (Communications)"; break; default: break; } return ss; } const char* SessionStateToString(AudioSessionState state) { switch (state) { case AudioSessionStateActive: return "Active"; case AudioSessionStateInactive: return "Inactive"; case AudioSessionStateExpired: return "Expired"; default: return "Invalid"; } } const char* SessionDisconnectReasonToString( AudioSessionDisconnectReason reason) { switch (reason) { case DisconnectReasonDeviceRemoval: return "DeviceRemoval"; case DisconnectReasonServerShutdown: return "ServerShutdown"; case DisconnectReasonFormatChanged: return "FormatChanged"; case DisconnectReasonSessionLogoff: return "SessionLogoff"; case DisconnectReasonSessionDisconnected: return "Disconnected"; case DisconnectReasonExclusiveModeOverride: return "ExclusiveModeOverride"; default: return "Invalid"; } } // Returns true if the selected audio device supports low latency, i.e, if it // is possible to initialize the engine using periods less than the default // period (10ms). bool IsLowLatencySupported(IAudioClient3* client3, const WAVEFORMATEXTENSIBLE* format, uint32_t* min_period_in_frames) { RTC_DLOG(LS_INFO) << __FUNCTION__; // Get the range of periodicities supported by the engine for the specified // stream format. uint32_t default_period = 0; uint32_t fundamental_period = 0; uint32_t min_period = 0; uint32_t max_period = 0; if (FAILED(core_audio_utility::GetSharedModeEnginePeriod( client3, format, &default_period, &fundamental_period, &min_period, &max_period))) { return false; } // Low latency is supported if the shortest allowed period is less than the // default engine period. // TODO(henrika): verify that this assumption is correct. const bool low_latency = min_period < default_period; RTC_LOG(LS_INFO) << "low_latency: " << low_latency; *min_period_in_frames = low_latency ? min_period : 0; return low_latency; } } // namespace CoreAudioBase::CoreAudioBase(Direction direction, bool automatic_restart, OnDataCallback data_callback, OnErrorCallback error_callback) : format_(), direction_(direction), automatic_restart_(automatic_restart), on_data_callback_(data_callback), on_error_callback_(error_callback), device_index_(kUndefined), is_restarting_(false) { RTC_DLOG(LS_INFO) << __FUNCTION__ << "[" << DirectionToString(direction) << "]"; RTC_DLOG(LS_INFO) << "Automatic restart: " << automatic_restart; RTC_DLOG(LS_INFO) << "Windows version: " << rtc::rtc_win::GetVersion(); // Create the event which the audio engine will signal each time a buffer // becomes ready to be processed by the client. audio_samples_event_.Set(CreateEvent(nullptr, false, false, nullptr)); RTC_DCHECK(audio_samples_event_.IsValid()); // Event to be set in Stop() when rendering/capturing shall stop. stop_event_.Set(CreateEvent(nullptr, false, false, nullptr)); RTC_DCHECK(stop_event_.IsValid()); // Event to be set when it has been detected that an active device has been // invalidated or the stream format has changed. restart_event_.Set(CreateEvent(nullptr, false, false, nullptr)); RTC_DCHECK(restart_event_.IsValid()); } CoreAudioBase::~CoreAudioBase() { RTC_DLOG(LS_INFO) << __FUNCTION__; RTC_DCHECK_EQ(ref_count_, 1); } EDataFlow CoreAudioBase::GetDataFlow() const { return direction_ == CoreAudioBase::Direction::kOutput ? eRender : eCapture; } bool CoreAudioBase::IsRestarting() const { return is_restarting_; } int64_t CoreAudioBase::TimeSinceStart() const { return rtc::TimeSince(start_time_); } int CoreAudioBase::NumberOfActiveDevices() const { return core_audio_utility::NumberOfActiveDevices(GetDataFlow()); } int CoreAudioBase::NumberOfEnumeratedDevices() const { const int num_active = NumberOfActiveDevices(); return num_active > 0 ? num_active + kDefaultDeviceTypeMaxCount : 0; } void CoreAudioBase::ReleaseCOMObjects() { RTC_DLOG(LS_INFO) << __FUNCTION__; // ComPtr::Reset() sets the ComPtr to nullptr releasing any previous // reference. if (audio_client_) { audio_client_.Reset(); } if (audio_clock_.Get()) { audio_clock_.Reset(); } if (audio_session_control_.Get()) { audio_session_control_.Reset(); } } bool CoreAudioBase::IsDefaultDevice(int index) const { return index == kDefault; } bool CoreAudioBase::IsDefaultCommunicationsDevice(int index) const { return index == kDefaultCommunications; } bool CoreAudioBase::IsDefaultDeviceId(absl::string_view device_id) const { // Returns true if `device_id` corresponds to the id of the default // device. Note that, if only one device is available (or if the user has not // explicitly set a default device), `device_id` will also math // IsDefaultCommunicationsDeviceId(). return (IsInput() && (device_id == core_audio_utility::GetDefaultInputDeviceID())) || (IsOutput() && (device_id == core_audio_utility::GetDefaultOutputDeviceID())); } bool CoreAudioBase::IsDefaultCommunicationsDeviceId( absl::string_view device_id) const { // Returns true if `device_id` corresponds to the id of the default // communication device. Note that, if only one device is available (or if // the user has not explicitly set a communication device), `device_id` will // also math IsDefaultDeviceId(). return (IsInput() && (device_id == core_audio_utility::GetCommunicationsInputDeviceID())) || (IsOutput() && (device_id == core_audio_utility::GetCommunicationsOutputDeviceID())); } bool CoreAudioBase::IsInput() const { return direction_ == CoreAudioBase::Direction::kInput; } bool CoreAudioBase::IsOutput() const { return direction_ == CoreAudioBase::Direction::kOutput; } std::string CoreAudioBase::GetDeviceID(int index) const { if (index >= NumberOfEnumeratedDevices()) { RTC_LOG(LS_ERROR) << "Invalid device index"; return std::string(); } std::string device_id; if (IsDefaultDevice(index)) { device_id = IsInput() ? core_audio_utility::GetDefaultInputDeviceID() : core_audio_utility::GetDefaultOutputDeviceID(); } else if (IsDefaultCommunicationsDevice(index)) { device_id = IsInput() ? core_audio_utility::GetCommunicationsInputDeviceID() : core_audio_utility::GetCommunicationsOutputDeviceID(); } else { AudioDeviceNames device_names; bool ok = IsInput() ? core_audio_utility::GetInputDeviceNames(&device_names) : core_audio_utility::GetOutputDeviceNames(&device_names); if (ok) { device_id = device_names[index].unique_id; } } return device_id; } int CoreAudioBase::SetDevice(int index) { RTC_DLOG(LS_INFO) << __FUNCTION__ << "[" << DirectionToString(direction()) << "]: index=" << IndexToString(index); if (initialized_) { return -1; } std::string device_id = GetDeviceID(index); RTC_DLOG(LS_INFO) << "index=" << IndexToString(index) << " => device_id: " << device_id; device_index_ = index; device_id_ = device_id; return device_id_.empty() ? -1 : 0; } int CoreAudioBase::DeviceName(int index, std::string* name, std::string* guid) const { RTC_DLOG(LS_INFO) << __FUNCTION__ << "[" << DirectionToString(direction()) << "]: index=" << IndexToString(index); if (index > NumberOfEnumeratedDevices() - 1) { RTC_LOG(LS_ERROR) << "Invalid device index"; return -1; } AudioDeviceNames device_names; bool ok = IsInput() ? core_audio_utility::GetInputDeviceNames(&device_names) : core_audio_utility::GetOutputDeviceNames(&device_names); // Validate the index one extra time in-case the size of the generated list // did not match NumberOfEnumeratedDevices(). if (!ok || static_cast(device_names.size()) <= index) { RTC_LOG(LS_ERROR) << "Failed to get the device name"; return -1; } *name = device_names[index].device_name; RTC_DLOG(LS_INFO) << "name: " << *name; if (guid != nullptr) { *guid = device_names[index].unique_id; RTC_DLOG(LS_INFO) << "guid: " << *guid; } return 0; } bool CoreAudioBase::Init() { RTC_DLOG(LS_INFO) << __FUNCTION__ << "[" << DirectionToString(direction()) << "]"; RTC_DCHECK_GE(device_index_, 0); RTC_DCHECK(!device_id_.empty()); RTC_DCHECK(audio_device_buffer_); RTC_DCHECK(!audio_client_); RTC_DCHECK(!audio_session_control_.Get()); // Use an existing combination of `device_index_` and `device_id_` to set // parameters which are required to create an audio client. It is up to the // parent class to set `device_index_` and `device_id_`. std::string device_id = AudioDeviceName::kDefaultDeviceId; ERole role = ERole(); if (IsDefaultDevice(device_index_)) { role = eConsole; } else if (IsDefaultCommunicationsDevice(device_index_)) { role = eCommunications; } else { device_id = device_id_; } RTC_LOG(LS_INFO) << "Unique device identifier: device_id=" << device_id << ", role=" << RoleToString(role); // Create an IAudioClient interface which enables us to create and initialize // an audio stream between an audio application and the audio engine. ComPtr audio_client; if (core_audio_utility::GetAudioClientVersion() == 3) { RTC_DLOG(LS_INFO) << "Using IAudioClient3"; audio_client = core_audio_utility::CreateClient3(device_id, GetDataFlow(), role); } else if (core_audio_utility::GetAudioClientVersion() == 2) { RTC_DLOG(LS_INFO) << "Using IAudioClient2"; audio_client = core_audio_utility::CreateClient2(device_id, GetDataFlow(), role); } else { RTC_DLOG(LS_INFO) << "Using IAudioClient"; audio_client = core_audio_utility::CreateClient(device_id, GetDataFlow(), role); } if (!audio_client) { return false; } // Set extra client properties before initialization if the audio client // supports it. // TODO(henrika): evaluate effect(s) of making these changes. Also, perhaps // these types of settings belongs to the client and not the utility parts. if (core_audio_utility::GetAudioClientVersion() >= 2) { if (FAILED(core_audio_utility::SetClientProperties( static_cast(audio_client.Get())))) { return false; } } // Retrieve preferred audio input or output parameters for the given client // and the specified client properties. Override the preferred rate if sample // rate has been defined by the user. Rate conversion will be performed by // the audio engine to match the client if needed. AudioParameters params; HRESULT res = sample_rate_ ? core_audio_utility::GetPreferredAudioParameters( audio_client.Get(), ¶ms, *sample_rate_) : core_audio_utility::GetPreferredAudioParameters( audio_client.Get(), ¶ms); if (FAILED(res)) { return false; } // Define the output WAVEFORMATEXTENSIBLE format in `format_`. WAVEFORMATEX* format = &format_.Format; format->wFormatTag = WAVE_FORMAT_EXTENSIBLE; // Check the preferred channel configuration and request implicit channel // upmixing (audio engine extends from 2 to N channels internally) if the // preferred number of channels is larger than two; i.e., initialize the // stream in stereo even if the preferred configuration is multi-channel. if (params.channels() <= 2) { format->nChannels = rtc::dchecked_cast(params.channels()); } else { // TODO(henrika): ensure that this approach works on different multi-channel // devices. Verified on: // - Corsair VOID PRO Surround USB Adapter (supports 7.1) RTC_LOG(LS_WARNING) << "Using channel upmixing in WASAPI audio engine (2 => " << params.channels() << ")"; format->nChannels = 2; } format->nSamplesPerSec = params.sample_rate(); format->wBitsPerSample = rtc::dchecked_cast(params.bits_per_sample()); format->nBlockAlign = (format->wBitsPerSample / 8) * format->nChannels; format->nAvgBytesPerSec = format->nSamplesPerSec * format->nBlockAlign; format->cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); // Add the parts which are unique for the WAVE_FORMAT_EXTENSIBLE structure. format_.Samples.wValidBitsPerSample = rtc::dchecked_cast(params.bits_per_sample()); format_.dwChannelMask = format->nChannels == 1 ? KSAUDIO_SPEAKER_MONO : KSAUDIO_SPEAKER_STEREO; format_.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; RTC_DLOG(LS_INFO) << core_audio_utility::WaveFormatToString(&format_); // Verify that the format is supported but exclude the test if the default // sample rate has been overridden. If so, the WASAPI audio engine will do // any necessary conversions between the client format we have given it and // the playback mix format or recording split format. if (!sample_rate_) { if (!core_audio_utility::IsFormatSupported( audio_client.Get(), AUDCLNT_SHAREMODE_SHARED, &format_)) { return false; } } // Check if low-latency is supported and use special initialization if it is. // Low-latency initialization requires these things: // - IAudioClient3 (>= Win10) // - HDAudio driver // - kEnableLowLatencyIfSupported changed from false (default) to true. // TODO(henrika): IsLowLatencySupported() returns AUDCLNT_E_UNSUPPORTED_FORMAT // when `sample_rate_.has_value()` returns true if rate conversion is // actually required (i.e., client asks for other than the default rate). bool low_latency_support = false; uint32_t min_period_in_frames = 0; if (kEnableLowLatencyIfSupported && core_audio_utility::GetAudioClientVersion() >= 3) { low_latency_support = IsLowLatencySupported(static_cast(audio_client.Get()), &format_, &min_period_in_frames); } if (low_latency_support) { RTC_DCHECK_GE(core_audio_utility::GetAudioClientVersion(), 3); // Use IAudioClient3::InitializeSharedAudioStream() API to initialize a // low-latency event-driven client. Request the smallest possible // periodicity. // TODO(henrika): evaluate this scheme in terms of CPU etc. if (FAILED(core_audio_utility::SharedModeInitializeLowLatency( static_cast(audio_client.Get()), &format_, audio_samples_event_, min_period_in_frames, sample_rate_.has_value(), &endpoint_buffer_size_frames_))) { return false; } } else { // Initialize the audio stream between the client and the device in shared // mode using event-driven buffer handling. Also, using 0 as requested // buffer size results in a default (minimum) endpoint buffer size. // TODO(henrika): possibly increase `requested_buffer_size` to add // robustness. const REFERENCE_TIME requested_buffer_size = 0; if (FAILED(core_audio_utility::SharedModeInitialize( audio_client.Get(), &format_, audio_samples_event_, requested_buffer_size, sample_rate_.has_value(), &endpoint_buffer_size_frames_))) { return false; } } // Check device period and the preferred buffer size and log a warning if // WebRTC's buffer size is not an even divisor of the preferred buffer size // in Core Audio. // TODO(henrika): sort out if a non-perfect match really is an issue. // TODO(henrika): compare with IAudioClient3::GetSharedModeEnginePeriod(). REFERENCE_TIME device_period; if (FAILED(core_audio_utility::GetDevicePeriod( audio_client.Get(), AUDCLNT_SHAREMODE_SHARED, &device_period))) { return false; } const double device_period_in_seconds = static_cast( core_audio_utility::ReferenceTimeToTimeDelta(device_period).ms()) / 1000.0L; const int preferred_frames_per_buffer = static_cast(params.sample_rate() * device_period_in_seconds + 0.5); RTC_DLOG(LS_INFO) << "preferred_frames_per_buffer: " << preferred_frames_per_buffer; if (preferred_frames_per_buffer % params.frames_per_buffer()) { RTC_LOG(LS_WARNING) << "Buffer size of " << params.frames_per_buffer() << " is not an even divisor of " << preferred_frames_per_buffer; } // Create an AudioSessionControl interface given the initialized client. // The IAudioControl interface enables a client to configure the control // parameters for an audio session and to monitor events in the session. ComPtr audio_session_control = core_audio_utility::CreateAudioSessionControl(audio_client.Get()); if (!audio_session_control.Get()) { return false; } // The Sndvol program displays volume and mute controls for sessions that // are in the active and inactive states. AudioSessionState state; if (FAILED(audio_session_control->GetState(&state))) { return false; } RTC_DLOG(LS_INFO) << "audio session state: " << SessionStateToString(state); RTC_DCHECK_EQ(state, AudioSessionStateInactive); // Register the client to receive notifications of session events, including // changes in the stream state. if (FAILED(audio_session_control->RegisterAudioSessionNotification(this))) { return false; } // Store valid COM interfaces. audio_client_ = audio_client; audio_session_control_ = audio_session_control; return true; } bool CoreAudioBase::Start() { RTC_DLOG(LS_INFO) << __FUNCTION__ << "[" << DirectionToString(direction()) << "]"; if (IsRestarting()) { // Audio thread should be alive during internal restart since the restart // callback is triggered on that thread and it also makes the restart // sequence less complex. RTC_DCHECK(!audio_thread_.empty()); } // Start an audio thread but only if one does not already exist (which is the // case during restart). if (audio_thread_.empty()) { const absl::string_view name = IsInput() ? "wasapi_capture_thread" : "wasapi_render_thread"; audio_thread_ = rtc::PlatformThread::SpawnJoinable( [this] { ThreadRun(); }, name, rtc::ThreadAttributes().SetPriority(rtc::ThreadPriority::kRealtime)); RTC_DLOG(LS_INFO) << "Started thread with name: " << name << " and handle: " << *audio_thread_.GetHandle(); } // Start streaming data between the endpoint buffer and the audio engine. _com_error error = audio_client_->Start(); if (FAILED(error.Error())) { StopThread(); RTC_LOG(LS_ERROR) << "IAudioClient::Start failed: " << core_audio_utility::ErrorToString(error); return false; } start_time_ = rtc::TimeMillis(); num_data_callbacks_ = 0; return true; } bool CoreAudioBase::Stop() { RTC_DLOG(LS_INFO) << __FUNCTION__ << "[" << DirectionToString(direction()) << "]"; RTC_DLOG(LS_INFO) << "total activity time: " << TimeSinceStart(); // Stop audio streaming. _com_error error = audio_client_->Stop(); if (FAILED(error.Error())) { RTC_LOG(LS_ERROR) << "IAudioClient::Stop failed: " << core_audio_utility::ErrorToString(error); } // Stop and destroy the audio thread but only when a restart attempt is not // ongoing. if (!IsRestarting()) { StopThread(); } // Flush all pending data and reset the audio clock stream position to 0. error = audio_client_->Reset(); if (FAILED(error.Error())) { RTC_LOG(LS_ERROR) << "IAudioClient::Reset failed: " << core_audio_utility::ErrorToString(error); } if (IsOutput()) { // Extra safety check to ensure that the buffers are cleared. // If the buffers are not cleared correctly, the next call to Start() // would fail with AUDCLNT_E_BUFFER_ERROR at // IAudioRenderClient::GetBuffer(). UINT32 num_queued_frames = 0; audio_client_->GetCurrentPadding(&num_queued_frames); RTC_DCHECK_EQ(0u, num_queued_frames); } // Delete the previous registration by the client to receive notifications // about audio session events. RTC_DLOG(LS_INFO) << "audio session state: " << SessionStateToString(GetAudioSessionState()); error = audio_session_control_->UnregisterAudioSessionNotification(this); if (FAILED(error.Error())) { RTC_LOG(LS_ERROR) << "IAudioSessionControl::UnregisterAudioSessionNotification failed: " << core_audio_utility::ErrorToString(error); } // To ensure that the restart process is as simple as possible, the audio // thread is not destroyed during restart attempts triggered by internal // error callbacks. if (!IsRestarting()) { thread_checker_audio_.Detach(); } // Release all allocated COM interfaces to allow for a restart without // intermediate destruction. ReleaseCOMObjects(); return true; } bool CoreAudioBase::IsVolumeControlAvailable(bool* available) const { // A valid IAudioClient is required to access the ISimpleAudioVolume interface // properly. It is possible to use IAudioSessionManager::GetSimpleAudioVolume // as well but we use the audio client here to ensure that the initialized // audio session is visible under group box labeled "Applications" in // Sndvol.exe. if (!audio_client_) { return false; } // Try to create an ISimpleAudioVolume instance. ComPtr audio_volume = core_audio_utility::CreateSimpleAudioVolume(audio_client_.Get()); if (!audio_volume.Get()) { RTC_DLOG(LS_ERROR) << "Volume control is not supported"; return false; } // Try to use the valid volume control. float volume = 0.0; _com_error error = audio_volume->GetMasterVolume(&volume); if (error.Error() != S_OK) { RTC_LOG(LS_ERROR) << "ISimpleAudioVolume::GetMasterVolume failed: " << core_audio_utility::ErrorToString(error); *available = false; } RTC_DLOG(LS_INFO) << "master volume for output audio session: " << volume; *available = true; return false; } // Internal test method which can be used in tests to emulate a restart signal. // It simply sets the same event which is normally triggered by session and // device notifications. Hence, the emulated restart sequence covers most parts // of a real sequence expect the actual device switch. bool CoreAudioBase::Restart() { RTC_DLOG(LS_INFO) << __FUNCTION__ << "[" << DirectionToString(direction()) << "]"; if (!automatic_restart()) { return false; } is_restarting_ = true; SetEvent(restart_event_.Get()); return true; } void CoreAudioBase::StopThread() { RTC_DLOG(LS_INFO) << __FUNCTION__; RTC_DCHECK(!IsRestarting()); if (!audio_thread_.empty()) { RTC_DLOG(LS_INFO) << "Sets stop_event..."; SetEvent(stop_event_.Get()); RTC_DLOG(LS_INFO) << "PlatformThread::Finalize..."; audio_thread_.Finalize(); // Ensure that we don't quit the main thread loop immediately next // time Start() is called. ResetEvent(stop_event_.Get()); ResetEvent(restart_event_.Get()); } } bool CoreAudioBase::HandleRestartEvent() { RTC_DLOG(LS_INFO) << __FUNCTION__ << "[" << DirectionToString(direction()) << "]"; RTC_DCHECK_RUN_ON(&thread_checker_audio_); RTC_DCHECK(!audio_thread_.empty()); RTC_DCHECK(IsRestarting()); // Let each client (input and/or output) take care of its own restart // sequence since each side might need unique actions. // TODO(henrika): revisit and investigate if one common base implementation // is possible bool restart_ok = on_error_callback_(ErrorType::kStreamDisconnected); is_restarting_ = false; return restart_ok; } bool CoreAudioBase::SwitchDeviceIfNeeded() { RTC_DLOG(LS_INFO) << __FUNCTION__ << "[" << DirectionToString(direction()) << "]"; RTC_DCHECK_RUN_ON(&thread_checker_audio_); RTC_DCHECK(IsRestarting()); RTC_DLOG(LS_INFO) << "device_index=" << device_index_ << " => device_id: " << device_id_; // Ensure that at least one device exists and can be utilized. The most // probable cause for ending up here is that a device has been removed. if (core_audio_utility::NumberOfActiveDevices(IsInput() ? eCapture : eRender) < 1) { RTC_DLOG(LS_ERROR) << "All devices are disabled or removed"; return false; } // Get the unique device ID for the index which is currently used. It seems // safe to assume that if the ID is the same as the existing device ID, then // the device configuration is the same as before. std::string device_id = GetDeviceID(device_index_); if (device_id != device_id_) { RTC_LOG(LS_WARNING) << "Device configuration has changed => changing device selection..."; // TODO(henrika): depending on the current state and how we got here, we // must select a new device here. if (SetDevice(kDefault) == -1) { RTC_LOG(LS_WARNING) << "Failed to set new audio device"; return false; } } else { RTC_LOG(LS_INFO) << "Device configuration has not changed => keeping selected device"; } return true; } AudioSessionState CoreAudioBase::GetAudioSessionState() const { AudioSessionState state = AudioSessionStateInactive; RTC_DCHECK(audio_session_control_.Get()); _com_error error = audio_session_control_->GetState(&state); if (FAILED(error.Error())) { RTC_DLOG(LS_ERROR) << "IAudioSessionControl::GetState failed: " << core_audio_utility::ErrorToString(error); } return state; } // TODO(henrika): only used for debugging purposes currently. ULONG CoreAudioBase::AddRef() { ULONG new_ref = InterlockedIncrement(&ref_count_); // RTC_DLOG(LS_INFO) << "__AddRef => " << new_ref; return new_ref; } // TODO(henrika): does not call delete this. ULONG CoreAudioBase::Release() { ULONG new_ref = InterlockedDecrement(&ref_count_); // RTC_DLOG(LS_INFO) << "__Release => " << new_ref; return new_ref; } // TODO(henrika): can probably be replaced by "return S_OK" only. HRESULT CoreAudioBase::QueryInterface(REFIID iid, void** object) { if (object == nullptr) { return E_POINTER; } if (iid == IID_IUnknown || iid == __uuidof(IAudioSessionEvents)) { *object = static_cast(this); return S_OK; } *object = nullptr; return E_NOINTERFACE; } // IAudioSessionEvents::OnStateChanged. HRESULT CoreAudioBase::OnStateChanged(AudioSessionState new_state) { RTC_DLOG(LS_INFO) << "___" << __FUNCTION__ << "[" << DirectionToString(direction()) << "] new_state: " << SessionStateToString(new_state); return S_OK; } // When a session is disconnected because of a device removal or format change // event, we want to inform the audio thread about the lost audio session and // trigger an attempt to restart audio using a new (default) device. // This method is called on separate threads owned by the session manager and // it can happen that the same type of callback is called more than once for the // same event. HRESULT CoreAudioBase::OnSessionDisconnected( AudioSessionDisconnectReason disconnect_reason) { RTC_DLOG(LS_INFO) << "___" << __FUNCTION__ << "[" << DirectionToString(direction()) << "] reason: " << SessionDisconnectReasonToString(disconnect_reason); // Ignore changes in the audio session (don't try to restart) if the user // has explicitly asked for this type of ADM during construction. if (!automatic_restart()) { RTC_DLOG(LS_WARNING) << "___Automatic restart is disabled"; return S_OK; } if (IsRestarting()) { RTC_DLOG(LS_WARNING) << "___Ignoring since restart is already active"; return S_OK; } // By default, automatic restart is enabled and the restart event will be set // below if the device was removed or the format was changed. if (disconnect_reason == DisconnectReasonDeviceRemoval || disconnect_reason == DisconnectReasonFormatChanged) { is_restarting_ = true; SetEvent(restart_event_.Get()); } return S_OK; } // IAudioSessionEvents::OnDisplayNameChanged HRESULT CoreAudioBase::OnDisplayNameChanged(LPCWSTR new_display_name, LPCGUID event_context) { return S_OK; } // IAudioSessionEvents::OnIconPathChanged HRESULT CoreAudioBase::OnIconPathChanged(LPCWSTR new_icon_path, LPCGUID event_context) { return S_OK; } // IAudioSessionEvents::OnSimpleVolumeChanged HRESULT CoreAudioBase::OnSimpleVolumeChanged(float new_simple_volume, BOOL new_mute, LPCGUID event_context) { return S_OK; } // IAudioSessionEvents::OnChannelVolumeChanged HRESULT CoreAudioBase::OnChannelVolumeChanged(DWORD channel_count, float new_channel_volumes[], DWORD changed_channel, LPCGUID event_context) { return S_OK; } // IAudioSessionEvents::OnGroupingParamChanged HRESULT CoreAudioBase::OnGroupingParamChanged(LPCGUID new_grouping_param, LPCGUID event_context) { return S_OK; } void CoreAudioBase::ThreadRun() { if (!core_audio_utility::IsMMCSSSupported()) { RTC_LOG(LS_ERROR) << "MMCSS is not supported"; return; } RTC_DLOG(LS_INFO) << "[" << DirectionToString(direction()) << "] ThreadRun starts..."; // TODO(henrika): difference between "Pro Audio" and "Audio"? ScopedMMCSSRegistration mmcss_registration(L"Pro Audio"); ScopedCOMInitializer com_initializer(ScopedCOMInitializer::kMTA); RTC_DCHECK(mmcss_registration.Succeeded()); RTC_DCHECK(com_initializer.Succeeded()); RTC_DCHECK(stop_event_.IsValid()); RTC_DCHECK(audio_samples_event_.IsValid()); bool streaming = true; bool error = false; HANDLE wait_array[] = {stop_event_.Get(), restart_event_.Get(), audio_samples_event_.Get()}; // The device frequency is the frequency generated by the hardware clock in // the audio device. The GetFrequency() method reports a constant frequency. UINT64 device_frequency = 0; _com_error result(S_FALSE); if (audio_clock_) { RTC_DCHECK(IsOutput()); result = audio_clock_->GetFrequency(&device_frequency); if (FAILED(result.Error())) { RTC_LOG(LS_ERROR) << "IAudioClock::GetFrequency failed: " << core_audio_utility::ErrorToString(result); } } // Keep streaming audio until the stop event or the stream-switch event // is signaled. An error event can also break the main thread loop. while (streaming && !error) { // Wait for a close-down event, stream-switch event or a new render event. DWORD wait_result = WaitForMultipleObjects(arraysize(wait_array), wait_array, false, INFINITE); switch (wait_result) { case WAIT_OBJECT_0 + 0: // `stop_event_` has been set. streaming = false; break; case WAIT_OBJECT_0 + 1: // `restart_event_` has been set. error = !HandleRestartEvent(); break; case WAIT_OBJECT_0 + 2: // `audio_samples_event_` has been set. error = !on_data_callback_(device_frequency); break; default: error = true; break; } } if (streaming && error) { RTC_LOG(LS_ERROR) << "[" << DirectionToString(direction()) << "] WASAPI streaming failed."; // Stop audio streaming since something has gone wrong in our main thread // loop. Note that, we are still in a "started" state, hence a Stop() call // is required to join the thread properly. result = audio_client_->Stop(); if (FAILED(result.Error())) { RTC_LOG(LS_ERROR) << "IAudioClient::Stop failed: " << core_audio_utility::ErrorToString(result); } // TODO(henrika): notify clients that something has gone wrong and that // this stream should be destroyed instead of reused in the future. } RTC_DLOG(LS_INFO) << "[" << DirectionToString(direction()) << "] ...ThreadRun stops"; } } // namespace webrtc_win } // namespace webrtc