blob: dc8526b6257a7f2ed917792e638d21402dc9045e [file] [log] [blame]
/*
* 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(), &params, *sample_rate_)
: core_audio_utility::GetPreferredAudioParameters(
audio_client.Get(), &params);
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