| /* |
| * 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 <memory> |
| #include <string> |
| |
| #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<int>(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<IAudioClient> 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<IAudioClient2*>(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<WORD>(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<WORD>(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<WORD>(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<IAudioClient3*>(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<IAudioClient3*>(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<double>( |
| core_audio_utility::ReferenceTimeToTimeDelta(device_period).ms()) / |
| 1000.0L; |
| const int preferred_frames_per_buffer = |
| static_cast<int>(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<IAudioSessionControl> 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<ISimpleAudioVolume> 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<IAudioSessionEvents*>(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 |