Adds stream-switch support in new Windows ADM.

Second round of the new Windows ADM is now ready for review. Main
changes are:

Supports internal (automatic) restart of audio streams when an active
audio stream disconnects (happens when a device is removed).

Adds support for IAudioClient3 and IAudioClient2 for platforms which
supports it (>Win8 and >Win10).

Modifies the threading model to support restart "from the inside" on
the native audio thread.

Adds two new test methods for the ADM to emulate restart events or
stream-switch events.

Adds two new test methods to support rate conversion to ensure that
audio can be tested in loopback even if devices runs at different
sample rates.

Added initial components for low-latency support. Verified that it works
but disabled it with a flag for now.

Bug: webrtc:9265
Change-Id: Ia8e577daabea6b433f2c2eabab4e46ce8added6a
Reviewed-on: https://webrtc-review.googlesource.com/86020
Reviewed-by: Oskar Sundbom <ossu@webrtc.org>
Commit-Queue: Henrik Andreassson <henrika@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#24578}
This commit is contained in:
henrika
2018-09-05 14:34:40 +02:00
committed by Commit Bot
parent e10f3b7403
commit 5b6afc0ce6
20 changed files with 1805 additions and 264 deletions

View File

@ -20,6 +20,7 @@
#include "rtc_base/checks.h"
#include "rtc_base/format_macros.h"
#include "rtc_base/logging.h"
#include "rtc_base/numerics/safe_conversions.h"
#include "rtc_base/timeutils.h"
#include "system_wrappers/include/metrics.h"

View File

@ -68,10 +68,16 @@
namespace webrtc {
// static
rtc::scoped_refptr<AudioDeviceModule> AudioDeviceModule::Create(
const AudioLayer audio_layer) {
RTC_LOG(INFO) << __FUNCTION__;
return AudioDeviceModule::CreateForTest(audio_layer);
}
// static
rtc::scoped_refptr<AudioDeviceModuleForTest> AudioDeviceModule::CreateForTest(
const AudioLayer audio_layer) {
RTC_LOG(INFO) << __FUNCTION__;
// The "AudioDeviceModule::kWindowsCoreAudio2" audio layer has its own
// dedicated factory method which should be used instead.

View File

@ -25,7 +25,7 @@ namespace webrtc {
class AudioDeviceGeneric;
class AudioManager;
class AudioDeviceModuleImpl : public AudioDeviceModule {
class AudioDeviceModuleImpl : public AudioDeviceModuleForTest {
public:
enum PlatformType {
kPlatformNotSupported = 0,
@ -148,6 +148,11 @@ class AudioDeviceModuleImpl : public AudioDeviceModule {
#endif
AudioDeviceBuffer* GetAudioDeviceBuffer() { return &audio_device_buffer_; }
int RestartPlayoutInternally() override { return -1; }
int RestartRecordingInternally() override { return -1; }
int SetPlayoutSampleRate(uint32_t sample_rate) override { return -1; }
int SetRecordingSampleRate(uint32_t sample_rate) override { return -1; }
private:
PlatformType Platform() const;
AudioLayer PlatformAudioLayer() const;

View File

@ -28,6 +28,7 @@
#include "rtc_base/thread_annotations.h"
#include "rtc_base/thread_checker.h"
#include "rtc_base/timeutils.h"
#include "system_wrappers/include/sleep.h"
#include "test/gmock.h"
#include "test/gtest.h"
#ifdef WEBRTC_WIN
@ -41,6 +42,7 @@ using ::testing::Ge;
using ::testing::Invoke;
using ::testing::NiceMock;
using ::testing::NotNull;
using ::testing::Mock;
namespace webrtc {
namespace {
@ -330,6 +332,14 @@ class MockAudioTransport : public test::MockAudioTransport {
}
}
// Special constructor used in manual tests where the user wants to run audio
// until e.g. a keyboard key is pressed. The event flag is set to nullptr by
// default since it is up to the user to stop the test. See e.g.
// DISABLED_RunPlayoutAndRecordingInFullDuplexAndWaitForEnterKey().
void HandleCallbacks(AudioStream* audio_stream) {
HandleCallbacks(nullptr, audio_stream, 0);
}
int32_t RealRecordedDataIsAvailable(const void* audio_buffer,
const size_t samples_per_channel,
const size_t bytes_per_frame,
@ -341,7 +351,6 @@ class MockAudioTransport : public test::MockAudioTransport {
const bool typing_status,
uint32_t& new_mic_level) {
EXPECT_TRUE(rec_mode()) << "No test is expecting these callbacks.";
RTC_LOG(INFO) << "+";
// Store audio parameters once in the first callback. For all other
// callbacks, verify that the provided audio parameters are maintained and
// that each callback corresponds to 10ms for any given sample rate.
@ -364,7 +373,7 @@ class MockAudioTransport : public test::MockAudioTransport {
samples_per_channel * channels));
}
// Signal the event after given amount of callbacks.
if (ReceivedEnoughCallbacks()) {
if (event_ && ReceivedEnoughCallbacks()) {
event_->Set();
}
return 0;
@ -379,7 +388,6 @@ class MockAudioTransport : public test::MockAudioTransport {
int64_t* elapsed_time_ms,
int64_t* ntp_time_ms) {
EXPECT_TRUE(play_mode()) << "No test is expecting these callbacks.";
RTC_LOG(INFO) << "-";
// Store audio parameters once in the first callback. For all other
// callbacks, verify that the provided audio parameters are maintained and
// that each callback corresponds to 10ms for any given sample rate.
@ -406,7 +414,7 @@ class MockAudioTransport : public test::MockAudioTransport {
std::memset(audio_buffer, 0, num_bytes);
}
// Signal the event after given amount of callbacks.
if (ReceivedEnoughCallbacks()) {
if (event_ && ReceivedEnoughCallbacks()) {
event_->Set();
}
return 0;
@ -438,6 +446,15 @@ class MockAudioTransport : public test::MockAudioTransport {
type_ == TransportType::kPlayAndRecord;
}
void ResetCallbackCounters() {
if (play_mode()) {
play_count_ = 0;
}
if (rec_mode()) {
rec_count_ = 0;
}
}
private:
TransportType type_ = TransportType::kInvalid;
rtc::Event* event_ = nullptr;
@ -506,18 +523,22 @@ class AudioDeviceTest
bool requirements_satisfied() const { return requirements_satisfied_; }
rtc::Event* event() { return &event_; }
AudioDeviceModule::AudioLayer audio_layer() const { return audio_layer_; }
const rtc::scoped_refptr<AudioDeviceModule>& audio_device() const {
// AudioDeviceModuleForTest extends the default ADM interface with some extra
// test methods. Intended for usage in tests only and requires a unique
// factory method. See CreateAudioDevice() for details.
const rtc::scoped_refptr<AudioDeviceModuleForTest>& audio_device() const {
return audio_device_;
}
rtc::scoped_refptr<AudioDeviceModule> CreateAudioDevice() {
rtc::scoped_refptr<AudioDeviceModuleForTest> CreateAudioDevice() {
// Use the default factory for kPlatformDefaultAudio and a special factory
// CreateWindowsCoreAudioAudioDeviceModule() for kWindowsCoreAudio2.
// CreateWindowsCoreAudioAudioDeviceModuleForTest() for kWindowsCoreAudio2.
// The value of |audio_layer_| is set at construction by GetParam() and two
// different layers are tested on Windows only.
if (audio_layer_ == AudioDeviceModule::kPlatformDefaultAudio) {
return AudioDeviceModule::Create(audio_layer_);
return AudioDeviceModule::CreateForTest(audio_layer_);
} else if (audio_layer_ == AudioDeviceModule::kWindowsCoreAudio2) {
#ifdef WEBRTC_WIN
// We must initialize the COM library on a thread before we calling any of
@ -528,7 +549,7 @@ class AudioDeviceTest
EXPECT_TRUE(com_initializer_->Succeeded());
EXPECT_TRUE(webrtc_win::core_audio_utility::IsSupported());
EXPECT_TRUE(webrtc_win::core_audio_utility::IsMMCSSSupported());
return CreateWindowsCoreAudioAudioDeviceModule();
return CreateWindowsCoreAudioAudioDeviceModuleForTest();
#else
return nullptr;
#endif
@ -587,7 +608,7 @@ class AudioDeviceTest
AudioDeviceModule::AudioLayer audio_layer_;
bool requirements_satisfied_ = true;
rtc::Event event_;
rtc::scoped_refptr<AudioDeviceModule> audio_device_;
rtc::scoped_refptr<AudioDeviceModuleForTest> audio_device_;
bool stereo_playout_ = false;
};
@ -777,6 +798,124 @@ TEST_P(AudioDeviceTest, InitStopInitPlayoutWhileRecording) {
StopRecording();
}
// TODO(henrika): restart without intermediate destruction is currently only
// supported on Windows.
#ifdef WEBRTC_WIN
// Tests Start/Stop playout followed by a second session (emulates a restart
// triggered by a user using public APIs).
TEST_P(AudioDeviceTest, StartStopPlayoutWithExternalRestart) {
SKIP_TEST_IF_NOT(requirements_satisfied());
StartPlayout();
StopPlayout();
// Restart playout without destroying the ADM in between. Ensures that we
// support: Init(), Start(), Stop(), Init(), Start(), Stop().
StartPlayout();
StopPlayout();
}
// Tests Start/Stop recording followed by a second session (emulates a restart
// triggered by a user using public APIs).
TEST_P(AudioDeviceTest, StartStopRecordingWithExternalRestart) {
SKIP_TEST_IF_NOT(requirements_satisfied());
StartRecording();
StopRecording();
// Restart recording without destroying the ADM in between. Ensures that we
// support: Init(), Start(), Stop(), Init(), Start(), Stop().
StartRecording();
StopRecording();
}
// Tests Start/Stop playout followed by a second session (emulates a restart
// triggered by an internal callback e.g. corresponding to a device switch).
// Note that, internal restart is only supported in combination with the latest
// Windows ADM.
TEST_P(AudioDeviceTest, StartStopPlayoutWithInternalRestart) {
SKIP_TEST_IF_NOT(requirements_satisfied());
if (audio_layer() != AudioDeviceModule::kWindowsCoreAudio2) {
return;
}
MockAudioTransport mock(TransportType::kPlay);
mock.HandleCallbacks(event(), nullptr, kNumCallbacks);
EXPECT_CALL(mock, NeedMorePlayData(_, _, _, _, NotNull(), _, _, _))
.Times(AtLeast(kNumCallbacks));
EXPECT_EQ(0, audio_device()->RegisterAudioCallback(&mock));
StartPlayout();
event()->Wait(kTestTimeOutInMilliseconds);
EXPECT_TRUE(audio_device()->Playing());
// Restart playout but without stopping the internal audio thread.
// This procedure uses a non-public test API and it emulates what happens
// inside the ADM when e.g. a device is removed.
EXPECT_EQ(0, audio_device()->RestartPlayoutInternally());
// Run basic tests of public APIs while a restart attempt is active.
// These calls should now be very thin and not trigger any new actions.
EXPECT_EQ(-1, audio_device()->StopPlayout());
EXPECT_TRUE(audio_device()->Playing());
EXPECT_TRUE(audio_device()->PlayoutIsInitialized());
EXPECT_EQ(0, audio_device()->InitPlayout());
EXPECT_EQ(0, audio_device()->StartPlayout());
// Wait until audio has restarted and a new sequence of audio callbacks
// becomes active.
// TODO(henrika): is it possible to verify that the internal state transition
// is Stop->Init->Start?
ASSERT_TRUE(Mock::VerifyAndClearExpectations(&mock));
mock.ResetCallbackCounters();
EXPECT_CALL(mock, NeedMorePlayData(_, _, _, _, NotNull(), _, _, _))
.Times(AtLeast(kNumCallbacks));
event()->Wait(kTestTimeOutInMilliseconds);
EXPECT_TRUE(audio_device()->Playing());
// Stop playout and the audio thread after successful internal restart.
StopPlayout();
}
// Tests Start/Stop recording followed by a second session (emulates a restart
// triggered by an internal callback e.g. corresponding to a device switch).
// Note that, internal restart is only supported in combination with the latest
// Windows ADM.
TEST_P(AudioDeviceTest, StartStopRecordingWithInternalRestart) {
SKIP_TEST_IF_NOT(requirements_satisfied());
if (audio_layer() != AudioDeviceModule::kWindowsCoreAudio2) {
return;
}
MockAudioTransport mock(TransportType::kRecord);
mock.HandleCallbacks(event(), nullptr, kNumCallbacks);
EXPECT_CALL(mock, RecordedDataIsAvailable(NotNull(), _, _, _, _, Ge(0u), 0, _,
false, _))
.Times(AtLeast(kNumCallbacks));
EXPECT_EQ(0, audio_device()->RegisterAudioCallback(&mock));
StartRecording();
event()->Wait(kTestTimeOutInMilliseconds);
EXPECT_TRUE(audio_device()->Recording());
// Restart recording but without stopping the internal audio thread.
// This procedure uses a non-public test API and it emulates what happens
// inside the ADM when e.g. a device is removed.
EXPECT_EQ(0, audio_device()->RestartRecordingInternally());
// Run basic tests of public APIs while a restart attempt is active.
// These calls should now be very thin and not trigger any new actions.
EXPECT_EQ(-1, audio_device()->StopRecording());
EXPECT_TRUE(audio_device()->Recording());
EXPECT_TRUE(audio_device()->RecordingIsInitialized());
EXPECT_EQ(0, audio_device()->InitRecording());
EXPECT_EQ(0, audio_device()->StartRecording());
// Wait until audio has restarted and a new sequence of audio callbacks
// becomes active.
// TODO(henrika): is it possible to verify that the internal state transition
// is Stop->Init->Start?
ASSERT_TRUE(Mock::VerifyAndClearExpectations(&mock));
mock.ResetCallbackCounters();
EXPECT_CALL(mock, RecordedDataIsAvailable(NotNull(), _, _, _, _, Ge(0u), 0, _,
false, _))
.Times(AtLeast(kNumCallbacks));
event()->Wait(kTestTimeOutInMilliseconds);
EXPECT_TRUE(audio_device()->Recording());
// Stop recording and the audio thread after successful internal restart.
StopRecording();
}
#endif // #ifdef WEBRTC_WIN
// Start playout and verify that the native audio layer starts asking for real
// audio samples to play out using the NeedMorePlayData() callback.
// Note that we can't add expectations on audio parameters in EXPECT_CALL
@ -866,6 +1005,35 @@ TEST_P(AudioDeviceTest, RunPlayoutAndRecordingInFullDuplex) {
PRINT("\n");
}
// Runs audio in full duplex until user hits Enter. Intended as a manual test
// to ensure that the audio quality is good and that real device switches works
// as intended.
TEST_P(AudioDeviceTest,
DISABLED_RunPlayoutAndRecordingInFullDuplexAndWaitForEnterKey) {
SKIP_TEST_IF_NOT(requirements_satisfied());
if (audio_layer() != AudioDeviceModule::kWindowsCoreAudio2) {
return;
}
NiceMock<MockAudioTransport> mock(TransportType::kPlayAndRecord);
FifoAudioStream audio_stream;
mock.HandleCallbacks(&audio_stream);
EXPECT_EQ(0, audio_device()->RegisterAudioCallback(&mock));
EXPECT_EQ(0, audio_device()->SetStereoPlayout(true));
EXPECT_EQ(0, audio_device()->SetStereoRecording(true));
// Ensure that the sample rate for both directions are identical so that we
// always can listen to our own voice. Will lead to rate conversion (and
// higher latency) if the native sample rate is not 48kHz.
EXPECT_EQ(0, audio_device()->SetPlayoutSampleRate(48000));
EXPECT_EQ(0, audio_device()->SetRecordingSampleRate(48000));
StartPlayout();
StartRecording();
do {
PRINT("Loopback audio is active at 48kHz. Press Enter to stop.\n");
} while (getchar() != '\n');
StopRecording();
StopPlayout();
}
// Measures loopback latency and reports the min, max and average values for
// a full duplex audio session.
// The latency is measured like so:

View File

@ -26,6 +26,7 @@ FineAudioBuffer::FineAudioBuffer(AudioDeviceBuffer* audio_device_buffer)
playout_channels_(audio_device_buffer->PlayoutChannels()),
record_channels_(audio_device_buffer->RecordingChannels()) {
RTC_DCHECK(audio_device_buffer_);
RTC_DLOG(INFO) << __FUNCTION__;
if (IsReadyForPlayout()) {
RTC_DLOG(INFO) << "playout_samples_per_channel_10ms: "
<< playout_samples_per_channel_10ms_;
@ -38,7 +39,9 @@ FineAudioBuffer::FineAudioBuffer(AudioDeviceBuffer* audio_device_buffer)
}
}
FineAudioBuffer::~FineAudioBuffer() {}
FineAudioBuffer::~FineAudioBuffer() {
RTC_DLOG(INFO) << __FUNCTION__;
}
void FineAudioBuffer::ResetPlayout() {
playout_buffer_.Clear();

View File

@ -17,6 +17,8 @@
namespace webrtc {
class AudioDeviceModuleForTest;
class AudioDeviceModule : public rtc::RefCountInterface {
public:
// Deprecated.
@ -46,9 +48,13 @@ class AudioDeviceModule : public rtc::RefCountInterface {
enum ChannelType { kChannelLeft = 0, kChannelRight = 1, kChannelBoth = 2 };
public:
// Creates an ADM.
// Creates a default ADM for usage in production code.
static rtc::scoped_refptr<AudioDeviceModule> Create(
const AudioLayer audio_layer);
// Creates an ADM with support for extra test methods. Don't use this factory
// in production code.
static rtc::scoped_refptr<AudioDeviceModuleForTest> CreateForTest(
const AudioLayer audio_layer);
// TODO(bugs.webrtc.org/7306): deprecated (to be removed).
static rtc::scoped_refptr<AudioDeviceModule> Create(
const int32_t id,
@ -158,6 +164,20 @@ class AudioDeviceModule : public rtc::RefCountInterface {
~AudioDeviceModule() override {}
};
// Extends the default ADM interface with some extra test methods.
// Intended for usage in tests only and requires a unique factory method.
class AudioDeviceModuleForTest : public AudioDeviceModule {
public:
// Triggers internal restart sequences of audio streaming. Can be used by
// tests to emulate events corresponding to e.g. removal of an active audio
// device or other actions which causes the stream to be disconnected.
virtual int RestartPlayoutInternally() = 0;
virtual int RestartRecordingInternally() = 0;
virtual int SetPlayoutSampleRate(uint32_t sample_rate) = 0;
virtual int SetRecordingSampleRate(uint32_t sample_rate) = 0;
};
} // namespace webrtc
#endif // MODULES_AUDIO_DEVICE_INCLUDE_AUDIO_DEVICE_H_

View File

@ -24,6 +24,12 @@ namespace webrtc {
rtc::scoped_refptr<AudioDeviceModule>
CreateWindowsCoreAudioAudioDeviceModule() {
RTC_DLOG(INFO) << __FUNCTION__;
return CreateWindowsCoreAudioAudioDeviceModuleForTest();
}
rtc::scoped_refptr<AudioDeviceModuleForTest>
CreateWindowsCoreAudioAudioDeviceModuleForTest() {
RTC_DLOG(INFO) << __FUNCTION__;
// Returns NULL if Core Audio is not supported or if COM has not been
// initialized correctly using webrtc_win::ScopedCOMInitializer.

View File

@ -35,6 +35,9 @@ namespace webrtc {
//
rtc::scoped_refptr<AudioDeviceModule> CreateWindowsCoreAudioAudioDeviceModule();
rtc::scoped_refptr<AudioDeviceModuleForTest>
CreateWindowsCoreAudioAudioDeviceModuleForTest();
} // namespace webrtc
#endif // MODULES_AUDIO_DEVICE_INCLUDE_AUDIO_DEVICE_FACTORY_H_

View File

@ -24,6 +24,20 @@ namespace webrtc {
namespace webrtc_win {
namespace {
#define RETURN_IF_OUTPUT_RESTARTS(...) \
do { \
if (output_->Restarting()) { \
return __VA_ARGS__; \
} \
} while (0)
#define RETURN_IF_INPUT_RESTARTS(...) \
do { \
if (input_->Restarting()) { \
return __VA_ARGS__; \
} \
} while (0)
// This class combines a generic instance of an AudioInput and a generic
// instance of an AudioOutput to create an AudioDeviceModule. This is mostly
// done by delegating to the audio input/output with some glue code. This class
@ -34,7 +48,7 @@ namespace {
// i.e., all public methods must also be called on the same thread. A thread
// checker will RTC_DCHECK if any method is called on an invalid thread.
// TODO(henrika): is thread checking needed in AudioInput and AudioOutput?
class WindowsAudioDeviceModule : public AudioDeviceModule {
class WindowsAudioDeviceModule : public AudioDeviceModuleForTest {
public:
enum class InitStatus {
OK = 0,
@ -81,6 +95,8 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
int32_t Init() override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(0);
RETURN_IF_INPUT_RESTARTS(0);
if (initialized_) {
return 0;
}
@ -106,6 +122,8 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
int32_t Terminate() override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(0);
RETURN_IF_INPUT_RESTARTS(0);
if (!initialized_)
return 0;
int32_t err = input_->Terminate();
@ -123,12 +141,14 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
int16_t PlayoutDevices() override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(0);
return output_->NumDevices();
}
int16_t RecordingDevices() override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_INPUT_RESTARTS(0);
return input_->NumDevices();
}
@ -137,6 +157,7 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
char guid[kAdmMaxGuidSize]) override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(0);
std::string name_str, guid_str;
int ret = -1;
if (guid != nullptr) {
@ -153,6 +174,7 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
char guid[kAdmMaxGuidSize]) override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_INPUT_RESTARTS(0);
std::string name_str, guid_str;
int ret = -1;
if (guid != nullptr) {
@ -168,6 +190,7 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
int32_t SetPlayoutDevice(uint16_t index) override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(0);
return output_->SetDevice(index);
}
@ -175,6 +198,7 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
AudioDeviceModule::WindowsDeviceType device) override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(0);
return output_->SetDevice(device);
}
int32_t SetRecordingDevice(uint16_t index) override {
@ -200,12 +224,14 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
int32_t InitPlayout() override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(0);
return output_->InitPlayout();
}
bool PlayoutIsInitialized() const override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(true);
return output_->PlayoutIsInitialized();
}
@ -219,47 +245,55 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
int32_t InitRecording() override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_INPUT_RESTARTS(0);
return input_->InitRecording();
}
bool RecordingIsInitialized() const override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_INPUT_RESTARTS(true);
return input_->RecordingIsInitialized();
}
int32_t StartPlayout() override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(0);
return output_->StartPlayout();
}
int32_t StopPlayout() override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(-1);
return output_->StopPlayout();
}
bool Playing() const override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(true);
return output_->Playing();
}
int32_t StartRecording() override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_INPUT_RESTARTS(0);
return input_->StartRecording();
}
int32_t StopRecording() override {
RTC_LOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_INPUT_RESTARTS(-1);
return input_->StopRecording();
}
bool Recording() const override {
RTC_LOG(INFO) << __FUNCTION__;
RETURN_IF_INPUT_RESTARTS(true);
return input_->Recording();
}
@ -388,6 +422,31 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
return 0;
}
int RestartPlayoutInternally() override {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RETURN_IF_OUTPUT_RESTARTS(0);
return output_->RestartPlayout();
}
int RestartRecordingInternally() override {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
return input_->RestartRecording();
}
int SetPlayoutSampleRate(uint32_t sample_rate) override {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
return output_->SetSampleRate(sample_rate);
}
int SetRecordingSampleRate(uint32_t sample_rate) override {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
return input_->SetSampleRate(sample_rate);
}
private:
// Ensures that the class is used on the same thread as it is constructed
// and destroyed on.
@ -410,7 +469,7 @@ class WindowsAudioDeviceModule : public AudioDeviceModule {
} // namespace
rtc::scoped_refptr<AudioDeviceModule>
rtc::scoped_refptr<AudioDeviceModuleForTest>
CreateWindowsCoreAudioAudioDeviceModuleFromInputAndOutput(
std::unique_ptr<AudioInput> audio_input,
std::unique_ptr<AudioOutput> audio_output) {

View File

@ -42,6 +42,9 @@ class AudioInput {
virtual int StopRecording() = 0;
virtual bool Recording() = 0;
virtual int VolumeIsAvailable(bool* available) = 0;
virtual int RestartRecording() = 0;
virtual bool Restarting() const = 0;
virtual int SetSampleRate(uint32_t sample_rate) = 0;
};
// This interface represents the main output-related parts of the complete
@ -63,11 +66,14 @@ class AudioOutput {
virtual int StopPlayout() = 0;
virtual bool Playing() = 0;
virtual int VolumeIsAvailable(bool* available) = 0;
virtual int RestartPlayout() = 0;
virtual bool Restarting() const = 0;
virtual int SetSampleRate(uint32_t sample_rate) = 0;
};
// Combines an AudioInput and an AudioOutput implementation to build an
// AudioDeviceModule. Hides most parts of the full ADM interface.
rtc::scoped_refptr<AudioDeviceModule>
rtc::scoped_refptr<AudioDeviceModuleForTest>
CreateWindowsCoreAudioAudioDeviceModuleFromInputAndOutput(
std::unique_ptr<AudioInput> audio_input,
std::unique_ptr<AudioOutput> audio_output);

View File

@ -9,6 +9,7 @@
*/
#include "modules/audio_device/win/core_audio_base_win.h"
#include "modules/audio_device/audio_device_buffer.h"
#include <string>
@ -17,6 +18,7 @@
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
#include "rtc_base/numerics/safe_conversions.h"
#include "rtc_base/timeutils.h"
#include "rtc_base/win/windows_version.h"
using Microsoft::WRL::ComPtr;
@ -25,10 +27,22 @@ 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 {
kDefault,
kDefaultCommunications,
kDefaultDeviceTypeMaxCount,
kUndefined = -1,
kDefault = 0,
kDefaultCommunications = 1,
kDefaultDeviceTypeMaxCount = kDefaultCommunications + 1,
};
const char* DirectionToString(CoreAudioBase::Direction direction) {
@ -42,35 +56,119 @@ const char* DirectionToString(CoreAudioBase::Direction direction) {
}
}
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";
}
}
void Run(void* obj) {
RTC_DCHECK(obj);
reinterpret_cast<CoreAudioBase*>(obj)->ThreadRun();
}
// 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(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(INFO) << "low_latency: " << low_latency;
*min_period_in_frames = low_latency ? min_period : 0;
return low_latency;
}
} // namespace
CoreAudioBase::CoreAudioBase(Direction direction, OnDataCallback callback)
: direction_(direction), on_data_callback_(callback), format_() {
CoreAudioBase::CoreAudioBase(Direction direction,
OnDataCallback data_callback,
OnErrorCallback error_callback)
: format_(),
direction_(direction),
on_data_callback_(data_callback),
on_error_callback_(error_callback),
device_index_(kUndefined),
is_restarting_(false) {
RTC_DLOG(INFO) << __FUNCTION__ << "[" << DirectionToString(direction) << "]";
RTC_DLOG(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 be set in Stop() when rendering/capturing shall stop.
// 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(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());
}
@ -80,6 +178,21 @@ int CoreAudioBase::NumberOfEnumeratedDevices() const {
return num_active > 0 ? num_active + kDefaultDeviceTypeMaxCount : 0;
}
void CoreAudioBase::ReleaseCOMObjects() {
RTC_DLOG(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;
}
@ -138,6 +251,21 @@ std::string CoreAudioBase::GetDeviceID(int index) const {
return device_id;
}
int CoreAudioBase::SetDevice(int index) {
RTC_DLOG(INFO) << __FUNCTION__ << "[" << DirectionToString(direction())
<< "]";
if (initialized_) {
return -1;
}
std::string device_id = GetDeviceID(index);
RTC_DLOG(INFO) << "index=" << 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 {
@ -170,11 +298,11 @@ bool CoreAudioBase::Init() {
<< "]";
RTC_DCHECK(!device_id_.empty());
RTC_DCHECK(audio_device_buffer_);
RTC_DCHECK(!audio_client_.Get());
RTC_DCHECK(!audio_client_);
RTC_DCHECK(!audio_session_control_.Get());
// Use an existing |device_id_| and set parameters which are required to
// create an audio client. It is up to the parent class to set |device_id_|.
// TODO(henrika): improve device notification.
std::string device_id = device_id_;
ERole role = eConsole;
if (IsDefaultDevice(device_id)) {
@ -183,20 +311,51 @@ bool CoreAudioBase::Init() {
} else if (IsDefaultCommunicationsDevice(device_id)) {
device_id = AudioDeviceName::kDefaultCommunicationsDeviceId;
role = eCommunications;
} else {
RTC_DLOG(LS_WARNING) << "Not using a default device";
}
// 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 =
ComPtr<IAudioClient> audio_client;
if (core_audio_utility::GetAudioClientVersion() == 3) {
RTC_DLOG(INFO) << "Using IAudioClient3";
audio_client =
core_audio_utility::CreateClient3(device_id, GetDataFlow(), role);
} else if (core_audio_utility::GetAudioClientVersion() == 2) {
RTC_DLOG(INFO) << "Using IAudioClient2";
audio_client =
core_audio_utility::CreateClient2(device_id, GetDataFlow(), role);
} else {
RTC_DLOG(INFO) << "Using IAudioClient";
audio_client =
core_audio_utility::CreateClient(device_id, GetDataFlow(), role);
if (!audio_client.Get()) {
}
if (!audio_client) {
return false;
}
// Retrieve preferred audio input or output parameters for the given client.
// 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;
if (FAILED(core_audio_utility::GetPreferredAudioParameters(audio_client.Get(),
&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;
}
@ -219,24 +378,66 @@ bool CoreAudioBase::Init() {
format_.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
RTC_DLOG(INFO) << core_audio_utility::WaveFormatExToString(&format_);
// Verify that the format is supported.
// 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.
// 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(henrik): sort out if a non-perfect match really is an issue.
// 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))) {
@ -256,8 +457,35 @@ bool CoreAudioBase::Init() {
<< preferred_frames_per_buffer;
}
// Store valid COM interfaces.
// 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(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 interface.
if (audio_client) {
audio_client_ = audio_client;
}
audio_session_control_ = audio_session_control;
return true;
}
@ -265,45 +493,65 @@ bool CoreAudioBase::Init() {
bool CoreAudioBase::Start() {
RTC_DLOG(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_);
}
// Start an audio thread but only if one does not already exist (which is the
// case during restart).
if (!audio_thread_) {
audio_thread_ = absl::make_unique<rtc::PlatformThread>(
Run, this, IsInput() ? "wasapi_capture_thread" : "wasapi_render_thread",
rtc::kRealtimePriority);
RTC_DCHECK(audio_thread_);
audio_thread_->Start();
if (!audio_thread_->IsRunning()) {
StopThread();
RTC_LOG(LS_ERROR) << "Failed to start audio thread";
return false;
}
RTC_DLOG(INFO) << "Started thread with name: " << audio_thread_->name();
RTC_DLOG(INFO) << "Started thread with name: " << audio_thread_->name()
<< " and id: " << audio_thread_->GetThreadRef();
}
// Start streaming data between the endpoint buffer and the audio engine.
_com_error error = audio_client_->Start();
if (error.Error() != S_OK) {
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(INFO) << __FUNCTION__ << "[" << DirectionToString(direction())
<< "]";
RTC_DLOG(INFO) << "total activity time: " << TimeSinceStart();
// Stop streaming and the internal audio thread.
// Stop audio streaming.
_com_error error = audio_client_->Stop();
if (error.Error() != S_OK) {
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 (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::Reset failed: "
<< core_audio_utility::ErrorToString(error);
}
@ -318,6 +566,30 @@ bool CoreAudioBase::Stop() {
RTC_DCHECK_EQ(0u, num_queued_frames);
}
// Delete the previous registration by the client to receive notifications
// about audio session events.
RTC_DLOG(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_.DetachFromThread();
IsOutput() ? audio_device_buffer_->NativeAudioPlayoutInterrupted()
: audio_device_buffer_->NativeAudioRecordingInterrupted();
}
// Release all allocated COM interfaces to allow for a restart without
// intermediate destruction.
ReleaseCOMObjects();
return true;
}
@ -327,7 +599,7 @@ bool CoreAudioBase::IsVolumeControlAvailable(bool* available) const {
// 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_.Get()) {
if (!audio_client_) {
return false;
}
@ -353,8 +625,21 @@ bool CoreAudioBase::IsVolumeControlAvailable(bool* available) const {
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(INFO) << __FUNCTION__ << "[" << DirectionToString(direction())
<< "]";
is_restarting_ = true;
SetEvent(restart_event_.Get());
return true;
}
void CoreAudioBase::StopThread() {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK(!IsRestarting());
if (audio_thread_) {
if (audio_thread_->IsRunning()) {
RTC_DLOG(INFO) << "Sets stop_event...";
@ -367,15 +652,164 @@ void CoreAudioBase::StopThread() {
// 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(INFO) << __FUNCTION__ << "[" << DirectionToString(direction())
<< "]";
RTC_DCHECK_RUN_ON(&thread_checker_audio_);
RTC_DCHECK(audio_thread_);
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(INFO) << __FUNCTION__ << "[" << DirectionToString(direction())
<< "]";
RTC_DCHECK_RUN_ON(&thread_checker_audio_);
RTC_DCHECK(IsRestarting());
RTC_DLOG(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(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(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(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(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.
HRESULT CoreAudioBase::OnSessionDisconnected(
AudioSessionDisconnectReason disconnect_reason) {
RTC_DLOG(INFO) << "___" << __FUNCTION__ << "["
<< DirectionToString(direction()) << "] reason: "
<< SessionDisconnectReasonToString(disconnect_reason);
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(INFO) << "ThreadRun starts...";
RTC_DLOG(INFO) << "[" << DirectionToString(direction())
<< "] ThreadRun starts...";
// TODO(henrika): difference between "Pro Audio" and "Audio"?
ScopedMMCSSRegistration mmcss_registration(L"Pro Audio");
ScopedCOMInitializer com_initializer(ScopedCOMInitializer::kMTA);
@ -386,17 +820,19 @@ void CoreAudioBase::ThreadRun() {
bool streaming = true;
bool error = false;
HANDLE wait_array[] = {stop_event_.Get(), audio_samples_event_.Get()};
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;
if (audio_clock_.Get()) {
_com_error result(S_FALSE);
if (audio_clock_) {
RTC_DCHECK(IsOutput());
_com_error result = audio_clock_->GetFrequency(&device_frequency);
if ((error = result.Error()) != S_OK) {
result = audio_clock_->GetFrequency(&device_frequency);
if (FAILED(result.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClock::GetFrequency failed: "
<< core_audio_utility::ErrorToString(error);
<< core_audio_utility::ErrorToString(result);
}
}
@ -412,6 +848,10 @@ void CoreAudioBase::ThreadRun() {
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;
@ -422,17 +862,23 @@ void CoreAudioBase::ThreadRun() {
}
if (streaming && error) {
RTC_LOG(LS_ERROR) << "WASAPI streaming failed.";
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.
audio_client_->Stop();
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(INFO) << "...ThreadRun stops";
RTC_DLOG(INFO) << "[" << DirectionToString(direction())
<< "] ...ThreadRun stops";
}
} // namespace webrtc_win

View File

@ -11,10 +11,12 @@
#ifndef MODULES_AUDIO_DEVICE_WIN_CORE_AUDIO_BASE_WIN_H_
#define MODULES_AUDIO_DEVICE_WIN_CORE_AUDIO_BASE_WIN_H_
#include <atomic>
#include <functional>
#include <memory>
#include <string>
#include "absl/types/optional.h"
#include "modules/audio_device/win/core_audio_utility_win.h"
#include "rtc_base/platform_thread.h"
#include "rtc_base/thread_checker.h"
@ -29,30 +31,44 @@ namespace webrtc_win {
// Serves as base class for CoreAudioInput and CoreAudioOutput and supports
// device handling and audio streaming where the direction (input or output)
// is set at constructions by the parent.
class CoreAudioBase {
// The IAudioSessionEvents interface provides notifications of session-related
// events such as changes in the volume level, display name, and session state.
// This class does not use the default ref-counting memory management method
// provided by IUnknown: calling CoreAudioBase::Release() will not delete the
// object. The client will receive notification from the session manager on
// a separate thread owned and controlled by the manager.
// TODO(henrika): investigate if CoreAudioBase should implement
// IMMNotificationClient as well (might improve support for device changes).
class CoreAudioBase : public IAudioSessionEvents {
public:
enum class Direction {
kInput,
kOutput,
};
// TODO(henrika): add more error types.
enum class ErrorType {
kStreamDisconnected,
};
template <typename T>
auto as_integer(T const value) -> typename std::underlying_type<T>::type {
return static_cast<typename std::underlying_type<T>::type>(value);
}
// Callback definition for notifications of new audio data. For input clients,
// it means that "new audio data has now been captured", and for output
// clients, "the output layer now needs new audio data".
typedef std::function<bool(uint64_t device_frequency)> OnDataCallback;
explicit CoreAudioBase(Direction direction, OnDataCallback callback);
~CoreAudioBase();
std::string GetDeviceID(int index) const;
int DeviceName(int index, std::string* name, std::string* guid) const;
bool Init();
bool Start();
bool Stop();
bool IsVolumeControlAvailable(bool* available) const;
Direction direction() const { return direction_; }
// Callback definition for notifications of run-time error messages. It can
// be called e.g. when an active audio device is removed and an audio stream
// is disconnected (|error| is then set to kStreamDisconnected). Both input
// and output clients implements OnErrorCallback() and will trigger an
// internal restart sequence for kStreamDisconnected.
// This method is currently always called on the audio thread.
// TODO(henrika): add support for more error types.
typedef std::function<bool(ErrorType error)> OnErrorCallback;
void ThreadRun();
@ -60,7 +76,33 @@ class CoreAudioBase {
CoreAudioBase& operator=(const CoreAudioBase&) = delete;
protected:
// Returns number of active devices given the specified |direction_|.
explicit CoreAudioBase(Direction direction,
OnDataCallback data_callback,
OnErrorCallback error_callback);
~CoreAudioBase();
std::string GetDeviceID(int index) const;
int SetDevice(int index);
int DeviceName(int index, std::string* name, std::string* guid) const;
// Checks if the current device ID is no longer in use (e.g. due to a
// disconnected stream), and if so, switches device to the default audio
// device. Called on the audio thread during restart attempts.
bool SwitchDeviceIfNeeded();
bool Init();
bool Start();
bool Stop();
bool IsVolumeControlAvailable(bool* available) const;
bool Restart();
Direction direction() const { return direction_; }
// Releases all allocated COM resources in the base class.
void ReleaseCOMObjects();
// Returns number of active devices given the specified |direction_| set
// by the parent (input or output).
int NumberOfActiveDevices() const;
// Returns total number of enumerated audio devices which is the sum of all
@ -76,24 +118,79 @@ class CoreAudioBase {
bool IsDefaultDevice(const std::string& device_id) const;
bool IsDefaultCommunicationsDevice(const std::string& device_id) const;
EDataFlow GetDataFlow() const;
bool IsRestarting() const;
int64_t TimeSinceStart() const;
// TODO(henrika): is the existing thread checker in WindowsAudioDeviceModule
// sufficient? As is, we have one top-level protection and then a second
// level here. In addition, calls to Init(), Start() and Stop() are not
// included to allow for support of internal restart (where these methods are
// called on the audio thread).
rtc::ThreadChecker thread_checker_;
rtc::ThreadChecker thread_checker_audio_;
const Direction direction_;
const OnDataCallback on_data_callback_;
AudioDeviceBuffer* audio_device_buffer_ = nullptr;
bool initialized_ = false;
std::string device_id_;
WAVEFORMATEXTENSIBLE format_ = {};
uint32_t endpoint_buffer_size_frames_ = 0;
Microsoft::WRL::ComPtr<IAudioClient> audio_client_;
Microsoft::WRL::ComPtr<IAudioClock> audio_clock_;
ScopedHandle audio_samples_event_;
ScopedHandle stop_event_;
std::unique_ptr<rtc::PlatformThread> audio_thread_;
Microsoft::WRL::ComPtr<IAudioClient> audio_client_;
bool is_active_ = false;
int64_t num_data_callbacks_ = 0;
int latency_ms_ = 0;
absl::optional<uint32_t> sample_rate_;
private:
const Direction direction_;
const OnDataCallback on_data_callback_;
const OnErrorCallback on_error_callback_;
ScopedHandle audio_samples_event_;
ScopedHandle stop_event_;
ScopedHandle restart_event_;
int64_t start_time_ = 0;
std::string device_id_;
int device_index_;
// Used by the IAudioSessionEvents implementations. Currently only utilized
// for debugging purposes.
LONG ref_count_ = 1;
// Set when restart process starts and cleared when restart stops
// successfully. Accessed atomically.
std::atomic<bool> is_restarting_;
std::unique_ptr<rtc::PlatformThread> audio_thread_;
Microsoft::WRL::ComPtr<IAudioSessionControl> audio_session_control_;
void StopThread();
AudioSessionState GetAudioSessionState() const;
// Called on the audio thread when a restart event has been set.
// It will then trigger calls to the installed error callbacks with error
// type set to kStreamDisconnected.
bool HandleRestartEvent();
// IUnknown (required by IAudioSessionEvents and IMMNotificationClient).
ULONG __stdcall AddRef() override;
ULONG __stdcall Release() override;
HRESULT __stdcall QueryInterface(REFIID iid, void** object) override;
// IAudioSessionEvents implementation.
// These methods are called on separate threads owned by the session manager.
// More than one thread can be involved depending on the type of callback
// and audio session.
HRESULT __stdcall OnStateChanged(AudioSessionState new_state) override;
HRESULT __stdcall OnSessionDisconnected(
AudioSessionDisconnectReason disconnect_reason) override;
HRESULT __stdcall OnDisplayNameChanged(LPCWSTR new_display_name,
LPCGUID event_context) override;
HRESULT __stdcall OnIconPathChanged(LPCWSTR new_icon_path,
LPCGUID event_context) override;
HRESULT __stdcall OnSimpleVolumeChanged(float new_simple_volume,
BOOL new_mute,
LPCGUID event_context) override;
HRESULT __stdcall OnChannelVolumeChanged(DWORD channel_count,
float new_channel_volumes[],
DWORD changed_channel,
LPCGUID event_context) override;
HRESULT __stdcall OnGroupingParamChanged(LPCGUID new_grouping_param,
LPCGUID event_context) override;
};
} // namespace webrtc_win

View File

@ -22,9 +22,14 @@ using Microsoft::WRL::ComPtr;
namespace webrtc {
namespace webrtc_win {
enum AudioDeviceMessageType : uint32_t {
kMessageInputStreamDisconnected,
};
CoreAudioInput::CoreAudioInput()
: CoreAudioBase(CoreAudioBase::Direction::kInput,
[this](uint64_t freq) { return OnDataCallback(freq); }) {
[this](uint64_t freq) { return OnDataCallback(freq); },
[this](ErrorType err) { return OnErrorCallback(err); }) {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
thread_checker_audio_.DetachFromThread();
@ -55,16 +60,7 @@ int CoreAudioInput::NumDevices() const {
int CoreAudioInput::SetDevice(int index) {
RTC_DLOG(INFO) << __FUNCTION__ << ": " << index;
RTC_DCHECK_RUN_ON(&thread_checker_);
if (initialized_) {
return -1;
}
std::string device_id = GetDeviceID(index);
RTC_DLOG(INFO) << "index=" << index << " => device_id: " << device_id;
device_id_ = device_id;
return device_id_.empty() ? -1 : 0;
return CoreAudioBase::SetDevice(index);
}
int CoreAudioInput::SetDevice(AudioDeviceModule::WindowsDeviceType device) {
@ -96,21 +92,20 @@ bool CoreAudioInput::RecordingIsInitialized() const {
int CoreAudioInput::InitRecording() {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RTC_DCHECK(!initialized_);
RTC_DCHECK(!Recording());
RTC_DCHECK(!audio_client_.Get());
RTC_DCHECK(!audio_capture_client_.Get());
RTC_DCHECK(!audio_capture_client_);
// Create an IAudioClient and store the valid interface pointer in
// |audio_client_|. The base class will use optimal input parameters and do
// Creates an IAudioClient instance and stores the valid interface pointer in
// |audio_client3_|, |audio_client2_|, or |audio_client_| depending on
// platform support. The base class will use optimal input parameters and do
// an event driven shared mode initialization. The utilized format will be
// stored in |format_| and can be used for configuration and allocation of
// audio buffers.
if (!CoreAudioBase::Init()) {
return -1;
}
RTC_DCHECK(audio_client_.Get());
RTC_DCHECK(audio_client_);
// Configure the recording side of the audio device buffer using |format_|
// after a trivial sanity check of the format structure.
@ -123,7 +118,7 @@ int CoreAudioInput::InitRecording() {
// Create a modified audio buffer class which allows us to supply any number
// of samples (and not only multiple of 10ms) to match the optimal buffer
// size per callback used by Core Audio.
// TODO(henrika): can we share one FineAudioBuffer?
// TODO(henrika): can we share one FineAudioBuffer with the output side?
fine_audio_buffer_ = absl::make_unique<FineAudioBuffer>(audio_device_buffer_);
// Create an IAudioCaptureClient for an initialized IAudioClient.
@ -131,7 +126,7 @@ int CoreAudioInput::InitRecording() {
// a capture endpoint buffer.
ComPtr<IAudioCaptureClient> audio_capture_client =
core_audio_utility::CreateCaptureClient(audio_client_.Get());
if (!audio_capture_client.Get()) {
if (!audio_capture_client) {
return -1;
}
@ -144,8 +139,7 @@ int CoreAudioInput::InitRecording() {
qpc_to_100ns_ = 10000000.0 / qpc_ticks_per_second;
}
// Store valid COM interfaces. Note that, |audio_client_| has already been
// set in CoreAudioBase::Init().
// Store valid COM interfaces.
audio_capture_client_ = audio_capture_client;
initialized_ = true;
@ -154,7 +148,6 @@ int CoreAudioInput::InitRecording() {
int CoreAudioInput::StartRecording() {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RTC_DCHECK(!Recording());
if (!initialized_) {
RTC_DLOG(LS_WARNING)
@ -169,12 +162,12 @@ int CoreAudioInput::StartRecording() {
return -1;
}
is_active_ = true;
return 0;
}
int CoreAudioInput::StopRecording() {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
if (!initialized_) {
return 0;
}
@ -183,8 +176,7 @@ int CoreAudioInput::StopRecording() {
// method is called without any active input audio.
if (!Recording()) {
RTC_DLOG(WARNING) << "No input stream is active";
audio_client_.Reset();
audio_capture_client_.Reset();
ReleaseCOMObjects();
initialized_ = false;
return 0;
}
@ -194,22 +186,19 @@ int CoreAudioInput::StopRecording() {
return -1;
}
// TODO(henrika): if we want to support Init(), Start(), Stop(), Init(),
// Start(), Stop() without close in between, these lines are needed.
// Not supported on mobile ADMs, hence we can probably live without it.
// audio_client_.Reset();
// audio_capture_client_.Reset();
// audio_device_buffer_->NativeAudioRecordingInterrupted();
thread_checker_audio_.DetachFromThread();
// Release all allocated resources to allow for a restart without
// intermediate destruction.
ReleaseCOMObjects();
qpc_to_100ns_.reset();
initialized_ = false;
is_active_ = false;
return 0;
}
bool CoreAudioInput::Recording() {
RTC_DLOG(INFO) << __FUNCTION__ << ": " << (audio_thread_ != nullptr);
RTC_DCHECK_RUN_ON(&thread_checker_);
return audio_thread_ != nullptr;
RTC_DLOG(INFO) << __FUNCTION__ << ": " << is_active_;
return is_active_;
}
// TODO(henrika): finalize support of audio session volume control. As is, we
@ -221,12 +210,60 @@ int CoreAudioInput::VolumeIsAvailable(bool* available) {
return IsVolumeControlAvailable(available) ? 0 : -1;
}
// Triggers the restart sequence. Only used for testing purposes to emulate
// a real event where e.g. an active input device is removed.
int CoreAudioInput::RestartRecording() {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
if (!Recording()) {
return 0;
}
if (!Restart()) {
RTC_LOG(LS_ERROR) << "RestartRecording failed";
return -1;
}
return 0;
}
bool CoreAudioInput::Restarting() const {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
return IsRestarting();
}
int CoreAudioInput::SetSampleRate(uint32_t sample_rate) {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
sample_rate_ = sample_rate;
return 0;
}
void CoreAudioInput::ReleaseCOMObjects() {
RTC_DLOG(INFO) << __FUNCTION__;
CoreAudioBase::ReleaseCOMObjects();
if (audio_capture_client_.Get()) {
audio_capture_client_.Reset();
}
}
bool CoreAudioInput::OnDataCallback(uint64_t device_frequency) {
RTC_DCHECK_RUN_ON(&thread_checker_audio_);
if (num_data_callbacks_ == 0) {
RTC_LOG(INFO) << "--- Input audio stream is alive ---";
}
UINT32 num_frames_in_next_packet = 0;
_com_error error =
audio_capture_client_->GetNextPacketSize(&num_frames_in_next_packet);
if (error.Error() != S_OK) {
if (error.Error() == AUDCLNT_E_DEVICE_INVALIDATED) {
// Avoid breaking the thread loop implicitly by returning false and return
// true instead for AUDCLNT_E_DEVICE_INVALIDATED even it is a valid error
// message. We will use notifications about device changes instead to stop
// data callbacks and attempt to restart streaming .
RTC_DLOG(LS_ERROR) << "AUDCLNT_E_DEVICE_INVALIDATED";
return true;
}
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioCaptureClient::GetNextPacketSize failed: "
<< core_audio_utility::ErrorToString(error);
return false;
@ -248,16 +285,28 @@ bool CoreAudioInput::OnDataCallback(uint64_t device_frequency) {
RTC_DCHECK_EQ(num_frames_to_read, 0u);
return true;
}
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioCaptureClient::GetBuffer failed: "
<< core_audio_utility::ErrorToString(error);
return false;
}
// TODO(henrika): only update the latency estimate N times per second to
// save resources.
// Update input delay estimate but only about once per second to save
// resources. The estimate is usually stable.
if (num_data_callbacks_ % 100 == 0) {
absl::optional<int> opt_record_delay_ms;
// TODO(henrika): note that FineAudioBuffer adds latency as well.
auto opt_record_delay_ms = EstimateLatencyMillis(capture_time_100ns);
opt_record_delay_ms = EstimateLatencyMillis(capture_time_100ns);
if (opt_record_delay_ms) {
latency_ms_ = *opt_record_delay_ms;
} else {
RTC_DLOG(LS_WARNING) << "Input latency is set to fixed value";
latency_ms_ = 20;
}
}
if (num_data_callbacks_ % 500 == 0) {
RTC_DLOG(INFO) << "latency: " << latency_ms_;
}
// The data in the packet is not correlated with the previous packet's
// device position; possibly due to a stream state transition or timing
@ -283,21 +332,15 @@ bool CoreAudioInput::OnDataCallback(uint64_t device_frequency) {
} else {
// Copy recorded audio in |audio_data| to the WebRTC sink using the
// FineAudioBuffer object.
// TODO(henrika): fix delay estimation.
int record_delay_ms = 0;
if (opt_record_delay_ms) {
record_delay_ms = *opt_record_delay_ms;
// RTC_DLOG(INFO) << "record_delay_ms: " << record_delay_ms;
}
fine_audio_buffer_->DeliverRecordedData(
rtc::MakeArrayView(reinterpret_cast<const int16_t*>(audio_data),
format_.Format.nChannels * num_frames_to_read),
record_delay_ms);
latency_ms_);
}
error = audio_capture_client_->ReleaseBuffer(num_frames_to_read);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioCaptureClient::ReleaseBuffer failed: "
<< core_audio_utility::ErrorToString(error);
return false;
@ -305,13 +348,24 @@ bool CoreAudioInput::OnDataCallback(uint64_t device_frequency) {
error =
audio_capture_client_->GetNextPacketSize(&num_frames_in_next_packet);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioCaptureClient::GetNextPacketSize failed: "
<< core_audio_utility::ErrorToString(error);
return false;
}
}
++num_data_callbacks_;
return true;
}
bool CoreAudioInput::OnErrorCallback(ErrorType error) {
RTC_DLOG(INFO) << __FUNCTION__ << ": " << as_integer(error);
RTC_DCHECK_RUN_ON(&thread_checker_audio_);
if (error == CoreAudioBase::ErrorType::kStreamDisconnected) {
HandleStreamDisconnected();
} else {
RTC_DLOG(WARNING) << "Unsupported error type";
}
return true;
}
@ -338,5 +392,38 @@ absl::optional<int> CoreAudioInput::EstimateLatencyMillis(
return delay_us.ms();
}
// Called from OnErrorCallback() when error type is kStreamDisconnected.
// Note that this method is called on the audio thread and the internal restart
// sequence is also executed on that same thread. The audio thread is therefore
// not stopped during restart. Such a scheme also makes the restart process less
// complex.
// Note that, none of the called methods are thread checked since they can also
// be called on the main thread. Thread checkers are instead added on one layer
// above (in audio_device_module.cc) which ensures that the public API is thread
// safe.
// TODO(henrika): add more details.
bool CoreAudioInput::HandleStreamDisconnected() {
RTC_DLOG(INFO) << "<<<--- " << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_audio_);
if (StopRecording() != 0) {
return false;
}
if (!SwitchDeviceIfNeeded()) {
return false;
}
if (InitRecording() != 0) {
return false;
}
if (StartRecording() != 0) {
return false;
}
RTC_DLOG(INFO) << __FUNCTION__ << " --->>>";
return true;
}
} // namespace webrtc_win
} // namespace webrtc

View File

@ -47,13 +47,19 @@ class CoreAudioInput final : public CoreAudioBase, public AudioInput {
int StopRecording() override;
bool Recording() override;
int VolumeIsAvailable(bool* available) override;
int RestartRecording() override;
bool Restarting() const override;
int SetSampleRate(uint32_t sample_rate) override;
CoreAudioInput(const CoreAudioInput&) = delete;
CoreAudioInput& operator=(const CoreAudioInput&) = delete;
private:
void ReleaseCOMObjects();
bool OnDataCallback(uint64_t device_frequency);
bool OnErrorCallback(ErrorType error);
absl::optional<int> EstimateLatencyMillis(uint64_t capture_time_100ns);
bool HandleStreamDisconnected();
std::unique_ptr<FineAudioBuffer> fine_audio_buffer_;
Microsoft::WRL::ComPtr<IAudioCaptureClient> audio_capture_client_;

View File

@ -24,7 +24,8 @@ namespace webrtc_win {
CoreAudioOutput::CoreAudioOutput()
: CoreAudioBase(CoreAudioBase::Direction::kOutput,
[this](uint64_t freq) { return OnDataCallback(freq); }) {
[this](uint64_t freq) { return OnDataCallback(freq); },
[this](ErrorType err) { return OnErrorCallback(err); }) {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
thread_checker_audio_.DetachFromThread();
@ -57,15 +58,7 @@ int CoreAudioOutput::NumDevices() const {
int CoreAudioOutput::SetDevice(int index) {
RTC_DLOG(INFO) << __FUNCTION__ << ": " << index;
RTC_DCHECK_RUN_ON(&thread_checker_);
if (initialized_) {
return -1;
}
std::string device_id = GetDeviceID(index);
RTC_DLOG(INFO) << "index=" << index << " => device_id: " << device_id;
device_id_ = device_id;
return device_id_.empty() ? -1 : 0;
return CoreAudioBase::SetDevice(index);
}
int CoreAudioOutput::SetDevice(AudioDeviceModule::WindowsDeviceType device) {
@ -96,22 +89,21 @@ bool CoreAudioOutput::PlayoutIsInitialized() const {
}
int CoreAudioOutput::InitPlayout() {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RTC_DLOG(INFO) << __FUNCTION__ << ": " << IsRestarting();
RTC_DCHECK(!initialized_);
RTC_DCHECK(!Playing());
RTC_DCHECK(!audio_client_.Get());
RTC_DCHECK(!audio_render_client_.Get());
RTC_DCHECK(!audio_render_client_);
// Create an IAudioClient client and store the valid interface pointer in
// |audio_client_|. The base class will use optimal output parameters and do
// Creates an IAudioClient instance and stores the valid interface pointer in
// |audio_client3_|, |audio_client2_|, or |audio_client_| depending on
// platform support. The base class will use optimal output parameters and do
// an event driven shared mode initialization. The utilized format will be
// stored in |format_| and can be used for configuration and allocation of
// audio buffers.
if (!CoreAudioBase::Init()) {
return -1;
}
RTC_DCHECK(audio_client_.Get());
RTC_DCHECK(audio_client_);
// Configure the playout side of the audio device buffer using |format_|
// after a trivial sanity check of the format structure.
@ -124,7 +116,7 @@ int CoreAudioOutput::InitPlayout() {
// Create a modified audio buffer class which allows us to ask for any number
// of samples (and not only multiple of 10ms) to match the optimal
// buffer size per callback used by Core Audio.
// TODO(henrika): can we use a shared buffer instead?
// TODO(henrika): can we share one FineAudioBuffer with the input side?
fine_audio_buffer_ = absl::make_unique<FineAudioBuffer>(audio_device_buffer_);
// Create an IAudioRenderClient for an initialized IAudioClient.
@ -132,16 +124,17 @@ int CoreAudioOutput::InitPlayout() {
// a rendering endpoint buffer.
ComPtr<IAudioRenderClient> audio_render_client =
core_audio_utility::CreateRenderClient(audio_client_.Get());
if (!audio_render_client.Get())
if (!audio_render_client.Get()) {
return -1;
}
ComPtr<IAudioClock> audio_clock =
core_audio_utility::CreateAudioClock(audio_client_.Get());
if (!audio_clock.Get())
if (!audio_clock.Get()) {
return -1;
}
// Store valid COM interfaces. Note that, |audio_client_| has already been
// set in CoreAudioBase::Init().
// Store valid COM interfaces.
audio_render_client_ = audio_render_client;
audio_clock_ = audio_clock;
@ -150,13 +143,11 @@ int CoreAudioOutput::InitPlayout() {
}
int CoreAudioOutput::StartPlayout() {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RTC_DLOG(INFO) << __FUNCTION__ << ": " << IsRestarting();
RTC_DCHECK(!Playing());
if (!initialized_) {
RTC_DLOG(LS_WARNING)
<< "Playout can not start since InitPlayout must succeed first";
return 0;
}
if (fine_audio_buffer_) {
fine_audio_buffer_->ResetPlayout();
@ -173,12 +164,12 @@ int CoreAudioOutput::StartPlayout() {
return -1;
}
is_active_ = true;
return 0;
}
int CoreAudioOutput::StopPlayout() {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
RTC_DLOG(INFO) << __FUNCTION__ << ": " << IsRestarting();
if (!initialized_) {
return 0;
}
@ -187,8 +178,7 @@ int CoreAudioOutput::StopPlayout() {
// method is called without any active output audio.
if (!Playing()) {
RTC_DLOG(WARNING) << "No output stream is active";
audio_client_.Reset();
audio_render_client_.Reset();
ReleaseCOMObjects();
initialized_ = false;
return 0;
}
@ -198,15 +188,18 @@ int CoreAudioOutput::StopPlayout() {
return -1;
}
thread_checker_audio_.DetachFromThread();
// Release all allocated resources to allow for a restart without
// intermediate destruction.
ReleaseCOMObjects();
initialized_ = false;
is_active_ = false;
return 0;
}
bool CoreAudioOutput::Playing() {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
return audio_thread_ != nullptr;
RTC_DLOG(INFO) << __FUNCTION__ << ": " << is_active_;
return is_active_;
}
// TODO(henrika): finalize support of audio session volume control. As is, we
@ -218,13 +211,75 @@ int CoreAudioOutput::VolumeIsAvailable(bool* available) {
return IsVolumeControlAvailable(available) ? 0 : -1;
}
// Triggers the restart sequence. Only used for testing purposes to emulate
// a real event where e.g. an active output device is removed.
int CoreAudioOutput::RestartPlayout() {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
if (!Playing()) {
return 0;
}
if (!Restart()) {
RTC_LOG(LS_ERROR) << "RestartPlayout failed";
return -1;
}
return 0;
}
bool CoreAudioOutput::Restarting() const {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
return IsRestarting();
}
int CoreAudioOutput::SetSampleRate(uint32_t sample_rate) {
RTC_DLOG(INFO) << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_);
sample_rate_ = sample_rate;
return 0;
}
void CoreAudioOutput::ReleaseCOMObjects() {
RTC_DLOG(INFO) << __FUNCTION__;
CoreAudioBase::ReleaseCOMObjects();
if (audio_render_client_.Get()) {
audio_render_client_.Reset();
}
}
bool CoreAudioOutput::OnErrorCallback(ErrorType error) {
RTC_DLOG(INFO) << __FUNCTION__ << ": " << as_integer(error);
RTC_DCHECK_RUN_ON(&thread_checker_audio_);
if (!initialized_ || !Playing()) {
return true;
}
if (error == CoreAudioBase::ErrorType::kStreamDisconnected) {
HandleStreamDisconnected();
} else {
RTC_DLOG(WARNING) << "Unsupported error type";
}
return true;
}
bool CoreAudioOutput::OnDataCallback(uint64_t device_frequency) {
RTC_DCHECK_RUN_ON(&thread_checker_audio_);
if (num_data_callbacks_ == 0) {
RTC_LOG(INFO) << "--- Output audio stream is alive ---";
}
// Get the padding value which indicates the amount of valid unread data that
// the endpoint buffer currently contains.
UINT32 num_unread_frames = 0;
_com_error error = audio_client_->GetCurrentPadding(&num_unread_frames);
if (error.Error() != S_OK) {
if (error.Error() == AUDCLNT_E_DEVICE_INVALIDATED) {
// Avoid breaking the thread loop implicitly by returning false and return
// true instead for AUDCLNT_E_DEVICE_INVALIDATED even it is a valid error
// message. We will use notifications about device changes instead to stop
// data callbacks and attempt to restart streaming .
RTC_DLOG(LS_ERROR) << "AUDCLNT_E_DEVICE_INVALIDATED";
return true;
}
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::GetCurrentPadding failed: "
<< core_audio_utility::ErrorToString(error);
return false;
@ -236,39 +291,49 @@ bool CoreAudioOutput::OnDataCallback(uint64_t device_frequency) {
// calling IAudioRenderClient::GetBuffer().
UINT32 num_requested_frames =
endpoint_buffer_size_frames_ - num_unread_frames;
if (num_requested_frames == 0) {
RTC_DLOG(LS_WARNING)
<< "Audio thread is signaled but no new audio samples are needed";
return true;
}
// Request all available space in the rendering endpoint buffer into which the
// client can later write an audio packet.
uint8_t* audio_data;
error = audio_render_client_->GetBuffer(num_requested_frames, &audio_data);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioRenderClient::GetBuffer failed: "
<< core_audio_utility::ErrorToString(error);
return false;
}
// TODO(henrika): only update the latency estimate N times per second to
// save resources.
// Update output delay estimate but only about once per second to save
// resources. The estimate is usually stable.
if (num_data_callbacks_ % 100 == 0) {
// TODO(henrika): note that FineAudioBuffer adds latency as well.
int playout_delay_ms = EstimateOutputLatencyMillis(device_frequency);
// RTC_DLOG(INFO) << "playout_delay_ms: " << playout_delay_ms;
latency_ms_ = EstimateOutputLatencyMillis(device_frequency);
if (num_data_callbacks_ % 500 == 0) {
RTC_DLOG(INFO) << "latency: " << latency_ms_;
}
}
// Get audio data from WebRTC and write it to the allocated buffer in
// |audio_data|.
// |audio_data|. The playout latency is not updated for each callback.
fine_audio_buffer_->GetPlayoutData(
rtc::MakeArrayView(reinterpret_cast<int16_t*>(audio_data),
num_requested_frames * format_.Format.nChannels),
playout_delay_ms);
latency_ms_);
// Release the buffer space acquired in IAudioRenderClient::GetBuffer.
error = audio_render_client_->ReleaseBuffer(num_requested_frames, 0);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioRenderClient::ReleaseBuffer failed: "
<< core_audio_utility::ErrorToString(error);
return false;
}
num_frames_written_ += num_requested_frames;
++num_data_callbacks_;
return true;
}
@ -301,6 +366,39 @@ int CoreAudioOutput::EstimateOutputLatencyMillis(uint64_t device_frequency) {
return delay_ms;
}
// Called from OnErrorCallback() when error type is kStreamDisconnected.
// Note that this method is called on the audio thread and the internal restart
// sequence is also executed on that same thread. The audio thread is therefore
// not stopped during restart. Such a scheme also makes the restart process less
// complex.
// Note that, none of the called methods are thread checked since they can also
// be called on the main thread. Thread checkers are instead added on one layer
// above (in audio_device_module.cc) which ensures that the public API is thread
// safe.
// TODO(henrika): add more details.
bool CoreAudioOutput::HandleStreamDisconnected() {
RTC_DLOG(INFO) << "<<<--- " << __FUNCTION__;
RTC_DCHECK_RUN_ON(&thread_checker_audio_);
if (StopPlayout() != 0) {
return false;
}
if (!SwitchDeviceIfNeeded()) {
return false;
}
if (InitPlayout() != 0) {
return false;
}
if (StartPlayout() != 0) {
return false;
}
RTC_DLOG(INFO) << __FUNCTION__ << " --->>>";
return true;
}
} // namespace webrtc_win
} // namespace webrtc

View File

@ -47,13 +47,19 @@ class CoreAudioOutput final : public CoreAudioBase, public AudioOutput {
int StopPlayout() override;
bool Playing() override;
int VolumeIsAvailable(bool* available) override;
int RestartPlayout() override;
bool Restarting() const override;
int SetSampleRate(uint32_t sample_rate) override;
CoreAudioOutput(const CoreAudioOutput&) = delete;
CoreAudioOutput& operator=(const CoreAudioOutput&) = delete;
private:
void ReleaseCOMObjects();
bool OnDataCallback(uint64_t device_frequency);
bool OnErrorCallback(ErrorType error);
int EstimateOutputLatencyMillis(uint64_t device_frequency);
bool HandleStreamDisconnected();
std::unique_ptr<FineAudioBuffer> fine_audio_buffer_;
Microsoft::WRL::ComPtr<IAudioRenderClient> audio_render_client_;

View File

@ -24,6 +24,7 @@
#include "rtc_base/platform_thread_types.h"
#include "rtc_base/strings/string_builder.h"
#include "rtc_base/stringutils.h"
#include "rtc_base/win/windows_version.h"
using ATL::CComHeapPtr;
using Microsoft::WRL::ComPtr;
@ -61,7 +62,7 @@ ComPtr<IMMDeviceEnumerator> CreateDeviceEnumeratorInternal(
_com_error error =
::CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
IID_PPV_ARGS(&device_enumerator));
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "CoCreateInstance failed: " << ErrorToString(error);
}
@ -72,10 +73,10 @@ ComPtr<IMMDeviceEnumerator> CreateDeviceEnumeratorInternal(
// modules. Calling CoInitializeEx() is an attempt to resolve the reported
// issues. See http://crbug.com/378465 for details.
error = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
error = ::CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
CLSCTX_ALL, IID_PPV_ARGS(&device_enumerator));
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "CoCreateInstance failed: "
<< ErrorToString(error);
}
@ -131,7 +132,7 @@ ComPtr<IMMDevice> CreateDeviceInternal(const std::string& device_id,
if (device_id == AudioDeviceName::kDefaultDeviceId) {
error = device_enum->GetDefaultAudioEndpoint(
data_flow, role, audio_endpoint_device.GetAddressOf());
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR)
<< "IMMDeviceEnumerator::GetDefaultAudioEndpoint failed: "
<< ErrorToString(error);
@ -139,7 +140,7 @@ ComPtr<IMMDevice> CreateDeviceInternal(const std::string& device_id,
} else {
error = device_enum->GetDevice(rtc::ToUtf16(device_id).c_str(),
audio_endpoint_device.GetAddressOf());
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IMMDeviceEnumerator::GetDevice failed: "
<< ErrorToString(error);
}
@ -199,7 +200,7 @@ ComPtr<IAudioSessionManager2> CreateSessionManager2Internal(
_com_error error =
audio_device->Activate(__uuidof(IAudioSessionManager2), CLSCTX_ALL,
nullptr, &audio_session_manager);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IMMDevice::Activate(IAudioSessionManager2) failed: "
<< ErrorToString(error);
}
@ -220,7 +221,7 @@ ComPtr<IAudioSessionEnumerator> CreateSessionEnumeratorInternal(
}
_com_error error =
audio_session_manager->GetSessionEnumerator(&audio_session_enumerator);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR)
<< "IAudioSessionEnumerator::IAudioSessionEnumerator failed: "
<< ErrorToString(error);
@ -238,7 +239,7 @@ ComPtr<IAudioClient> CreateClientInternal(IMMDevice* audio_device) {
ComPtr<IAudioClient> audio_client;
_com_error error = audio_device->Activate(__uuidof(IAudioClient), CLSCTX_ALL,
nullptr, &audio_client);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IMMDevice::Activate(IAudioClient) failed: "
<< ErrorToString(error);
}
@ -252,13 +253,27 @@ ComPtr<IAudioClient2> CreateClient2Internal(IMMDevice* audio_device) {
ComPtr<IAudioClient2> audio_client;
_com_error error = audio_device->Activate(__uuidof(IAudioClient2), CLSCTX_ALL,
nullptr, &audio_client);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IMMDevice::Activate(IAudioClient2) failed: "
<< ErrorToString(error);
}
return audio_client;
}
ComPtr<IAudioClient3> CreateClient3Internal(IMMDevice* audio_device) {
if (!audio_device)
return ComPtr<IAudioClient3>();
ComPtr<IAudioClient3> audio_client;
_com_error error = audio_device->Activate(__uuidof(IAudioClient3), CLSCTX_ALL,
nullptr, &audio_client);
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IMMDevice::Activate(IAudioClient3) failed: "
<< ErrorToString(error);
}
return audio_client;
}
ComPtr<IMMDeviceCollection> CreateCollectionInternal(EDataFlow data_flow) {
ComPtr<IMMDeviceEnumerator> device_enumerator(
CreateDeviceEnumeratorInternal(true));
@ -272,7 +287,7 @@ ComPtr<IMMDeviceCollection> CreateCollectionInternal(EDataFlow data_flow) {
ComPtr<IMMDeviceCollection> collection;
_com_error error = device_enumerator->EnumAudioEndpoints(
data_flow, DEVICE_STATE_ACTIVE, collection.GetAddressOf());
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IMMDeviceCollection::EnumAudioEndpoints failed: "
<< ErrorToString(error);
}
@ -338,7 +353,7 @@ bool GetDeviceNamesInternal(EDataFlow data_flow,
// Retrieve a pointer to the specified item in the device collection.
ComPtr<IMMDevice> audio_device;
_com_error error = collection->Item(i, audio_device.GetAddressOf());
if (error.Error() != S_OK)
if (FAILED(error.Error()))
continue;
// Retrieve the complete device name for the given audio device endpoint.
AudioDeviceName device_name(
@ -354,7 +369,8 @@ bool GetDeviceNamesInternal(EDataFlow data_flow,
}
HRESULT GetPreferredAudioParametersInternal(IAudioClient* client,
AudioParameters* params) {
AudioParameters* params,
int fixed_sample_rate) {
WAVEFORMATPCMEX mix_format;
HRESULT hr = core_audio_utility::GetSharedModeMixFormat(client, &mix_format);
if (FAILED(hr))
@ -366,7 +382,14 @@ HRESULT GetPreferredAudioParametersInternal(IAudioClient* client,
if (FAILED(hr))
return hr;
const int sample_rate = mix_format.Format.nSamplesPerSec;
int sample_rate = mix_format.Format.nSamplesPerSec;
// Override default sample rate if |fixed_sample_rate| is set and different
// from the default rate.
if (fixed_sample_rate > 0 && fixed_sample_rate != sample_rate) {
RTC_DLOG(INFO) << "Using fixed sample rate instead of the preferred: "
<< sample_rate << " is replaced by " << fixed_sample_rate;
sample_rate = fixed_sample_rate;
}
// TODO(henrika): utilize full mix_format.Format.wBitsPerSample.
// const size_t bits_per_sample = AudioParameters::kBitsPerSample;
// TODO(henrika): improve channel layout support.
@ -427,6 +450,16 @@ int NumberOfActiveDevices(EDataFlow data_flow) {
return static_cast<int>(number_of_active_devices);
}
uint32_t GetAudioClientVersion() {
uint32_t version = 1;
if (rtc::rtc_win::GetVersion() >= rtc::rtc_win::VERSION_WIN10) {
version = 3;
} else if (rtc::rtc_win::GetVersion() >= rtc::rtc_win::VERSION_WIN8) {
version = 2;
}
return version;
}
ComPtr<IMMDeviceEnumerator> CreateDeviceEnumerator() {
RTC_DLOG(INFO) << "CreateDeviceEnumerator";
return CreateDeviceEnumeratorInternal(true);
@ -494,7 +527,7 @@ EDataFlow GetDataFlow(IMMDevice* device) {
RTC_DCHECK(device);
ComPtr<IMMEndpoint> endpoint;
_com_error error = device->QueryInterface(endpoint.GetAddressOf());
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IMMDevice::QueryInterface failed: "
<< ErrorToString(error);
return eAll;
@ -502,7 +535,7 @@ EDataFlow GetDataFlow(IMMDevice* device) {
EDataFlow data_flow;
error = endpoint->GetDataFlow(&data_flow);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IMMEndpoint::GetDataFlow failed: "
<< ErrorToString(error);
return eAll;
@ -541,7 +574,7 @@ int NumberOfActiveSessions(IMMDevice* device) {
// Iterate over all audio sessions for the given device.
int session_count = 0;
_com_error error = session_enumerator->GetCount(&session_count);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioSessionEnumerator::GetCount failed: "
<< ErrorToString(error);
return 0;
@ -553,7 +586,7 @@ int NumberOfActiveSessions(IMMDevice* device) {
// Acquire the session control interface.
ComPtr<IAudioSessionControl> session_control;
error = session_enumerator->GetSession(session, &session_control);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioSessionEnumerator::GetSession failed: "
<< ErrorToString(error);
return 0;
@ -569,7 +602,7 @@ int NumberOfActiveSessions(IMMDevice* device) {
// Get the current state and check if the state is active or not.
AudioSessionState state;
error = session_control->GetState(&state);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioSessionControl::GetState failed: "
<< ErrorToString(error);
return 0;
@ -599,26 +632,87 @@ ComPtr<IAudioClient2> CreateClient2(const std::string& device_id,
return CreateClient2Internal(device.Get());
}
ComPtr<IAudioClient3> CreateClient3(const std::string& device_id,
EDataFlow data_flow,
ERole role) {
RTC_DLOG(INFO) << "CreateClient3";
ComPtr<IMMDevice> device(CreateDevice(device_id, data_flow, role));
return CreateClient3Internal(device.Get());
}
HRESULT SetClientProperties(IAudioClient2* client) {
RTC_DLOG(INFO) << "SetClientProperties";
RTC_DCHECK(client);
AudioClientProperties properties = {0};
properties.cbSize = sizeof(AudioClientProperties);
properties.bIsOffload = false;
if (GetAudioClientVersion() < 2) {
RTC_LOG(LS_WARNING) << "Requires IAudioClient2 or higher";
return AUDCLNT_E_UNSUPPORTED_FORMAT;
}
AudioClientProperties props = {0};
props.cbSize = sizeof(AudioClientProperties);
// Real-time VoIP communication.
// TODO(henrika): other categories?
properties.eCategory = AudioCategory_Communications;
// TODO(henrika): can AUDCLNT_STREAMOPTIONS_RAW be used?
properties.Options = AUDCLNT_STREAMOPTIONS_NONE;
_com_error error = client->SetClientProperties(&properties);
if (error.Error() != S_OK) {
props.eCategory = AudioCategory_Communications;
// Hardware-offloaded audio processing allows the main audio processing tasks
// to be performed outside the computer's main CPU. Check support and log the
// result but hard-code |bIsOffload| to FALSE for now.
// TODO(henrika): evaluate hardware-offloading. Might complicate usage of
// IAudioClient::GetMixFormat().
BOOL supports_offload = FALSE;
_com_error error =
client->IsOffloadCapable(props.eCategory, &supports_offload);
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient2::IsOffloadCapable failed: "
<< ErrorToString(error);
}
RTC_DLOG(INFO) << "supports_offload: " << supports_offload;
props.bIsOffload = false;
// TODO(henrika): pros and cons compared with AUDCLNT_STREAMOPTIONS_NONE?
props.Options |= AUDCLNT_STREAMOPTIONS_NONE;
// Requires System.Devices.AudioDevice.RawProcessingSupported.
// props.Options |= AUDCLNT_STREAMOPTIONS_RAW;
// If it is important to avoid resampling in the audio engine, set this flag.
// props.Options |= AUDCLNT_STREAMOPTIONS_MATCH_FORMAT;
RTC_DLOG(INFO) << "options: 0x" << rtc::ToHex(props.Options);
error = client->SetClientProperties(&props);
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient2::SetClientProperties failed: "
<< ErrorToString(error);
}
return error.Error();
}
HRESULT GetBufferSizeLimits(IAudioClient2* client,
const WAVEFORMATEXTENSIBLE* format,
REFERENCE_TIME* min_buffer_duration,
REFERENCE_TIME* max_buffer_duration) {
RTC_DLOG(INFO) << "GetBufferSizeLimits";
RTC_DCHECK(client);
if (GetAudioClientVersion() < 2) {
RTC_LOG(LS_WARNING) << "Requires IAudioClient2 or higher";
return AUDCLNT_E_UNSUPPORTED_FORMAT;
}
REFERENCE_TIME min_duration = 0;
REFERENCE_TIME max_duration = 0;
_com_error error =
client->GetBufferSizeLimits(reinterpret_cast<const WAVEFORMATEX*>(format),
TRUE, &min_duration, &max_duration);
if (error.Error() == AUDCLNT_E_OFFLOAD_MODE_ONLY) {
// This API seems to be supported in off-load mode only but it is not
// documented as a valid error code. Making a special note about it here.
RTC_LOG(LS_ERROR) << "IAudioClient2::GetBufferSizeLimits failed: "
<< "AUDCLNT_E_OFFLOAD_MODE_ONLY";
} else if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient2::GetBufferSizeLimits failed: "
<< ErrorToString(error);
} else {
*min_buffer_duration = min_duration;
*max_buffer_duration = max_duration;
RTC_DLOG(INFO) << "min_buffer_duration: " << min_buffer_duration;
RTC_DLOG(INFO) << "max_buffer_duration: " << max_buffer_duration;
}
return error.Error();
}
HRESULT GetSharedModeMixFormat(IAudioClient* client,
WAVEFORMATEXTENSIBLE* format) {
RTC_DLOG(INFO) << "GetSharedModeMixFormat";
@ -626,7 +720,7 @@ HRESULT GetSharedModeMixFormat(IAudioClient* client,
ScopedCoMem<WAVEFORMATEXTENSIBLE> format_ex;
_com_error error =
client->GetMixFormat(reinterpret_cast<WAVEFORMATEX**>(&format_ex));
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::GetMixFormat failed: "
<< ErrorToString(error);
return error.Error();
@ -689,7 +783,7 @@ HRESULT GetDevicePeriod(IAudioClient* client,
REFERENCE_TIME default_period = 0;
REFERENCE_TIME minimum_period = 0;
_com_error error = client->GetDevicePeriod(&default_period, &minimum_period);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::GetDevicePeriod failed: "
<< ErrorToString(error);
return error.Error();
@ -699,13 +793,55 @@ HRESULT GetDevicePeriod(IAudioClient* client,
: minimum_period;
RTC_LOG(INFO) << "device_period: "
<< ReferenceTimeToTimeDelta(*device_period).ms() << " [ms]";
RTC_LOG(INFO) << "minimum_period: "
<< ReferenceTimeToTimeDelta(minimum_period).ms() << " [ms]";
return error.Error();
}
HRESULT GetSharedModeEnginePeriod(IAudioClient3* client3,
const WAVEFORMATEXTENSIBLE* format,
uint32_t* default_period_in_frames,
uint32_t* fundamental_period_in_frames,
uint32_t* min_period_in_frames,
uint32_t* max_period_in_frames) {
RTC_DLOG(INFO) << "GetSharedModeEnginePeriod";
RTC_DCHECK(client3);
UINT32 default_period = 0;
UINT32 fundamental_period = 0;
UINT32 min_period = 0;
UINT32 max_period = 0;
_com_error error = client3->GetSharedModeEnginePeriod(
reinterpret_cast<const WAVEFORMATEX*>(format), &default_period,
&fundamental_period, &min_period, &max_period);
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient3::GetSharedModeEnginePeriod failed: "
<< ErrorToString(error);
return error.Error();
}
WAVEFORMATEX format_ex = format->Format;
const WORD sample_rate = format_ex.nSamplesPerSec;
RTC_LOG(INFO) << "default_period_in_frames: " << default_period << " ("
<< FramesToMilliseconds(default_period, sample_rate) << " ms)";
RTC_LOG(INFO) << "fundamental_period_in_frames: " << fundamental_period
<< " (" << FramesToMilliseconds(fundamental_period, sample_rate)
<< " ms)";
RTC_LOG(INFO) << "min_period_in_frames: " << min_period << " ("
<< FramesToMilliseconds(min_period, sample_rate) << " ms)";
RTC_LOG(INFO) << "max_period_in_frames: " << max_period << " ("
<< FramesToMilliseconds(max_period, sample_rate) << " ms)";
*default_period_in_frames = default_period;
*fundamental_period_in_frames = fundamental_period;
*min_period_in_frames = min_period;
*max_period_in_frames = max_period;
return error.Error();
}
HRESULT GetPreferredAudioParameters(const std::string& device_id,
bool is_output_device,
AudioParameters* params) {
RTC_DLOG(INFO) << "GetPreferredAudioParameters";
RTC_DLOG(INFO) << "GetPreferredAudioParameters: " << is_output_device;
EDataFlow data_flow = is_output_device ? eRender : eCapture;
ComPtr<IMMDevice> device;
if (device_id == AudioDeviceName::kDefaultCommunicationsDeviceId) {
@ -724,21 +860,40 @@ HRESULT GetPreferredAudioParameters(const std::string& device_id,
if (!client.Get())
return E_FAIL;
return GetPreferredAudioParametersInternal(client.Get(), params);
return GetPreferredAudioParametersInternal(client.Get(), params, -1);
}
HRESULT GetPreferredAudioParameters(IAudioClient* client,
AudioParameters* params) {
RTC_DLOG(INFO) << "GetPreferredAudioParameters";
RTC_DCHECK(client);
return GetPreferredAudioParametersInternal(client, params);
return GetPreferredAudioParametersInternal(client, params, -1);
}
HRESULT GetPreferredAudioParameters(IAudioClient* client,
webrtc::AudioParameters* params,
uint32_t sample_rate) {
RTC_DLOG(INFO) << "GetPreferredAudioParameters: " << sample_rate;
RTC_DCHECK(client);
return GetPreferredAudioParametersInternal(client, params, sample_rate);
}
HRESULT SharedModeInitialize(IAudioClient* client,
const WAVEFORMATEXTENSIBLE* format,
HANDLE event_handle,
REFERENCE_TIME buffer_duration,
bool auto_convert_pcm,
uint32_t* endpoint_buffer_size) {
RTC_DLOG(INFO) << "SharedModeInitialize";
RTC_DLOG(INFO) << "SharedModeInitialize: buffer_duration=" << buffer_duration
<< ", auto_convert_pcm=" << auto_convert_pcm;
RTC_DCHECK(client);
RTC_DCHECK_GE(buffer_duration, 0);
if (buffer_duration != 0) {
RTC_DLOG(LS_WARNING) << "Non-default buffer size is used";
}
if (auto_convert_pcm) {
RTC_DLOG(LS_WARNING) << "Sample rate converter can be utilized";
}
// The AUDCLNT_STREAMFLAGS_NOPERSIST flag disables persistence of the volume
// and mute settings for a session that contains rendering streams.
// By default, the volume level and muting state for a rendering session are
@ -757,13 +912,26 @@ HRESULT SharedModeInitialize(IAudioClient* client,
stream_flags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
RTC_DLOG(INFO) << "The stream is initialized to be event driven";
}
// Check if sample-rate conversion is requested.
if (auto_convert_pcm) {
// Add channel matrixer (not utilized here) and rate converter to convert
// from our (the client's) format to the audio engine mix format.
// Currently only supported for testing, i.e., not possible to enable using
// public APIs.
RTC_DLOG(INFO) << "The stream is initialized to support rate conversion";
stream_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM;
stream_flags |= AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
}
RTC_DLOG(INFO) << "stream_flags: 0x" << rtc::ToHex(stream_flags);
// Initialize the shared mode client for minimal delay.
// Initialize the shared mode client for minimal delay if |buffer_duration|
// is 0 or possibly a higher delay (more robust) if |buffer_duration| is
// larger than 0. The actual size is given by IAudioClient::GetBufferSize().
_com_error error = client->Initialize(
AUDCLNT_SHAREMODE_SHARED, stream_flags, 0, 0,
AUDCLNT_SHAREMODE_SHARED, stream_flags, buffer_duration, 0,
reinterpret_cast<const WAVEFORMATEX*>(format), nullptr);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::Initialize failed: "
<< ErrorToString(error);
return error.Error();
@ -774,7 +942,7 @@ HRESULT SharedModeInitialize(IAudioClient* client,
// IAudioClient::SetEventHandle.
if (use_event) {
error = client->SetEventHandle(event_handle);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::SetEventHandle failed: "
<< ErrorToString(error);
return error.Error();
@ -784,8 +952,13 @@ HRESULT SharedModeInitialize(IAudioClient* client,
UINT32 buffer_size_in_frames = 0;
// Retrieves the size (maximum capacity) of the endpoint buffer. The size is
// expressed as the number of audio frames the buffer can hold.
// For rendering clients, the buffer length determines the maximum amount of
// rendering data that the application can write to the endpoint buffer
// during a single processing pass. For capture clients, the buffer length
// determines the maximum amount of capture data that the audio engine can
// read from the endpoint buffer during a single processing pass.
error = client->GetBufferSize(&buffer_size_in_frames);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::GetBufferSize failed: "
<< ErrorToString(error);
return error.Error();
@ -794,6 +967,10 @@ HRESULT SharedModeInitialize(IAudioClient* client,
*endpoint_buffer_size = buffer_size_in_frames;
RTC_DLOG(INFO) << "endpoint buffer size: " << buffer_size_in_frames
<< " [audio frames]";
const double size_in_ms = static_cast<double>(buffer_size_in_frames) /
(format->Format.nSamplesPerSec / 1000.0);
RTC_DLOG(INFO) << "endpoint buffer size: "
<< static_cast<int>(size_in_ms + 0.5) << " [ms]";
RTC_DLOG(INFO) << "bytes per audio frame: " << format->Format.nBlockAlign;
RTC_DLOG(INFO) << "endpoint buffer size: "
<< buffer_size_in_frames * format->Format.nChannels *
@ -808,6 +985,92 @@ HRESULT SharedModeInitialize(IAudioClient* client,
return error.Error();
}
HRESULT SharedModeInitializeLowLatency(IAudioClient3* client,
const WAVEFORMATEXTENSIBLE* format,
HANDLE event_handle,
uint32_t period_in_frames,
bool auto_convert_pcm,
uint32_t* endpoint_buffer_size) {
RTC_DLOG(INFO) << "SharedModeInitializeLowLatency: period_in_frames="
<< period_in_frames
<< ", auto_convert_pcm=" << auto_convert_pcm;
RTC_DCHECK(client);
RTC_DCHECK_GT(period_in_frames, 0);
if (auto_convert_pcm) {
RTC_DLOG(LS_WARNING) << "Sample rate converter is enabled";
}
// Define stream flags.
DWORD stream_flags = 0;
bool use_event =
(event_handle != nullptr && event_handle != INVALID_HANDLE_VALUE);
if (use_event) {
stream_flags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
RTC_DLOG(INFO) << "The stream is initialized to be event driven";
}
if (auto_convert_pcm) {
stream_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM;
stream_flags |= AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
}
RTC_DLOG(INFO) << "stream_flags: 0x" << rtc::ToHex(stream_flags);
// Initialize the shared mode client for lowest possible latency.
// It is assumed that GetSharedModeEnginePeriod() has been used to query the
// smallest possible engine period and that it is given by |period_in_frames|.
_com_error error = client->InitializeSharedAudioStream(
stream_flags, period_in_frames,
reinterpret_cast<const WAVEFORMATEX*>(format), nullptr);
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient3::InitializeSharedAudioStream failed: "
<< ErrorToString(error);
return error.Error();
}
// Set the event handle.
if (use_event) {
error = client->SetEventHandle(event_handle);
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::SetEventHandle failed: "
<< ErrorToString(error);
return error.Error();
}
}
UINT32 buffer_size_in_frames = 0;
// Retrieve the size (maximum capacity) of the endpoint buffer.
error = client->GetBufferSize(&buffer_size_in_frames);
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::GetBufferSize failed: "
<< ErrorToString(error);
return error.Error();
}
*endpoint_buffer_size = buffer_size_in_frames;
RTC_DLOG(INFO) << "endpoint buffer size: " << buffer_size_in_frames
<< " [audio frames]";
const double size_in_ms = static_cast<double>(buffer_size_in_frames) /
(format->Format.nSamplesPerSec / 1000.0);
RTC_DLOG(INFO) << "endpoint buffer size: "
<< static_cast<int>(size_in_ms + 0.5) << " [ms]";
RTC_DLOG(INFO) << "bytes per audio frame: " << format->Format.nBlockAlign;
RTC_DLOG(INFO) << "endpoint buffer size: "
<< buffer_size_in_frames * format->Format.nChannels *
(format->Format.wBitsPerSample / 8)
<< " [bytes]";
// TODO(henrika): utilize when delay measurements are added.
REFERENCE_TIME latency = 0;
error = client->GetStreamLatency(&latency);
if (FAILED(error.Error())) {
RTC_LOG(LS_WARNING) << "IAudioClient::GetStreamLatency failed: "
<< ErrorToString(error);
} else {
RTC_DLOG(INFO) << "stream latency: "
<< ReferenceTimeToTimeDelta(latency).ms() << " [ms]";
}
return error.Error();
}
ComPtr<IAudioRenderClient> CreateRenderClient(IAudioClient* client) {
RTC_DLOG(INFO) << "CreateRenderClient";
RTC_DCHECK(client);
@ -815,7 +1078,7 @@ ComPtr<IAudioRenderClient> CreateRenderClient(IAudioClient* client) {
// enables us to write output data to a rendering endpoint buffer.
ComPtr<IAudioRenderClient> audio_render_client;
_com_error error = client->GetService(IID_PPV_ARGS(&audio_render_client));
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR)
<< "IAudioClient::GetService(IID_IAudioRenderClient) failed: "
<< ErrorToString(error);
@ -831,7 +1094,7 @@ ComPtr<IAudioCaptureClient> CreateCaptureClient(IAudioClient* client) {
// enables us to read input data from a capturing endpoint buffer.
ComPtr<IAudioCaptureClient> audio_capture_client;
_com_error error = client->GetService(IID_PPV_ARGS(&audio_capture_client));
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR)
<< "IAudioClient::GetService(IID_IAudioCaptureClient) failed: "
<< ErrorToString(error);
@ -847,7 +1110,7 @@ ComPtr<IAudioClock> CreateAudioClock(IAudioClient* client) {
// monitor a stream's data rate and the current position in the stream.
ComPtr<IAudioClock> audio_clock;
_com_error error = client->GetService(IID_PPV_ARGS(&audio_clock));
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::GetService(IID_IAudioClock) failed: "
<< ErrorToString(error);
return ComPtr<IAudioClock>();
@ -855,6 +1118,19 @@ ComPtr<IAudioClock> CreateAudioClock(IAudioClient* client) {
return audio_clock;
}
ComPtr<IAudioSessionControl> CreateAudioSessionControl(IAudioClient* client) {
RTC_DLOG(INFO) << "CreateAudioSessionControl";
RTC_DCHECK(client);
ComPtr<IAudioSessionControl> audio_session_control;
_com_error error = client->GetService(IID_PPV_ARGS(&audio_session_control));
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::GetService(IID_IAudioControl) failed: "
<< ErrorToString(error);
return ComPtr<IAudioSessionControl>();
}
return audio_session_control;
}
ComPtr<ISimpleAudioVolume> CreateSimpleAudioVolume(IAudioClient* client) {
RTC_DLOG(INFO) << "CreateSimpleAudioVolume";
RTC_DCHECK(client);
@ -862,7 +1138,7 @@ ComPtr<ISimpleAudioVolume> CreateSimpleAudioVolume(IAudioClient* client) {
// client to control the master volume level of an audio session.
ComPtr<ISimpleAudioVolume> simple_audio_volume;
_com_error error = client->GetService(IID_PPV_ARGS(&simple_audio_volume));
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR)
<< "IAudioClient::GetService(IID_ISimpleAudioVolume) failed: "
<< ErrorToString(error);
@ -878,7 +1154,7 @@ bool FillRenderEndpointBufferWithSilence(IAudioClient* client,
RTC_DCHECK(render_client);
UINT32 endpoint_buffer_size = 0;
_com_error error = client->GetBufferSize(&endpoint_buffer_size);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::GetBufferSize failed: "
<< ErrorToString(error);
return false;
@ -888,7 +1164,7 @@ bool FillRenderEndpointBufferWithSilence(IAudioClient* client,
// Get number of audio frames that are queued up to play in the endpoint
// buffer.
error = client->GetCurrentPadding(&num_queued_frames);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioClient::GetCurrentPadding failed: "
<< ErrorToString(error);
return false;
@ -899,7 +1175,7 @@ bool FillRenderEndpointBufferWithSilence(IAudioClient* client,
int num_frames_to_fill = endpoint_buffer_size - num_queued_frames;
RTC_DLOG(INFO) << "num_frames_to_fill: " << num_frames_to_fill;
error = render_client->GetBuffer(num_frames_to_fill, &data);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioRenderClient::GetBuffer failed: "
<< ErrorToString(error);
return false;
@ -909,7 +1185,7 @@ bool FillRenderEndpointBufferWithSilence(IAudioClient* client,
// explicitly write silence data to the rendering buffer.
error = render_client->ReleaseBuffer(num_frames_to_fill,
AUDCLNT_BUFFERFLAGS_SILENT);
if (error.Error() != S_OK) {
if (FAILED(error.Error())) {
RTC_LOG(LS_ERROR) << "IAudioRenderClient::ReleaseBuffer failed: "
<< ErrorToString(error);
return false;
@ -947,6 +1223,11 @@ webrtc::TimeDelta ReferenceTimeToTimeDelta(REFERENCE_TIME time) {
return webrtc::TimeDelta::us(0.1 * time + 0.5);
}
double FramesToMilliseconds(uint32_t num_frames, uint16_t sample_rate) {
// Convert the current period in frames into milliseconds.
return static_cast<double>(num_frames) / (sample_rate / 1000.0);
}
std::string ErrorToString(const _com_error& error) {
char ss_buf[1024];
rtc::SimpleStringBuilder ss(ss_buf);

View File

@ -33,6 +33,7 @@ namespace webrtc {
namespace webrtc_win {
static const int64_t kNumMicrosecsPerSec = webrtc::TimeDelta::seconds(1).us();
static const int64_t kNumMillisecsPerSec = webrtc::TimeDelta::seconds(1).ms();
// Utility class which registers a thread with MMCSS in the constructor and
// deregisters MMCSS in the destructor. The task name is given by |task_name|.
@ -42,6 +43,47 @@ static const int64_t kNumMicrosecsPerSec = webrtc::TimeDelta::seconds(1).us();
// lower-priority applications.
class ScopedMMCSSRegistration {
public:
const char* PriorityClassToString(DWORD priority_class) {
switch (priority_class) {
case ABOVE_NORMAL_PRIORITY_CLASS:
return "ABOVE_NORMAL";
case BELOW_NORMAL_PRIORITY_CLASS:
return "BELOW_NORMAL";
case HIGH_PRIORITY_CLASS:
return "HIGH";
case IDLE_PRIORITY_CLASS:
return "IDLE";
case NORMAL_PRIORITY_CLASS:
return "NORMAL";
case REALTIME_PRIORITY_CLASS:
return "REALTIME";
default:
return "INVALID";
}
}
const char* PriorityToString(int priority) {
switch (priority) {
case THREAD_PRIORITY_ABOVE_NORMAL:
return "ABOVE_NORMAL";
case THREAD_PRIORITY_BELOW_NORMAL:
return "BELOW_NORMAL";
case THREAD_PRIORITY_HIGHEST:
return "HIGHEST";
case THREAD_PRIORITY_IDLE:
return "IDLE";
case THREAD_PRIORITY_LOWEST:
return "LOWEST";
case THREAD_PRIORITY_NORMAL:
return "NORMAL";
case THREAD_PRIORITY_TIME_CRITICAL:
return "TIME_CRITICAL";
default:
// Can happen in combination with REALTIME_PRIORITY_CLASS.
return "INVALID";
}
}
explicit ScopedMMCSSRegistration(const TCHAR* task_name) {
RTC_DLOG(INFO) << "ScopedMMCSSRegistration: " << rtc::ToUtf8(task_name);
// Register the calling thread with MMCSS for the supplied |task_name|.
@ -50,6 +92,14 @@ class ScopedMMCSSRegistration {
if (mmcss_handle_ == nullptr) {
RTC_LOG(LS_ERROR) << "Failed to enable MMCSS on this thread: "
<< GetLastError();
} else {
const DWORD priority_class = GetPriorityClass(GetCurrentProcess());
const int priority = GetThreadPriority(GetCurrentThread());
RTC_DLOG(INFO) << "priority class: "
<< PriorityClassToString(priority_class) << "("
<< priority_class << ")";
RTC_DLOG(INFO) << "priority: " << PriorityToString(priority) << "("
<< priority << ")";
}
}
@ -299,6 +349,11 @@ bool IsMMCSSSupported();
// devices.
int NumberOfActiveDevices(EDataFlow data_flow);
// Returns 1, 2, or 3 depending on what version of IAudioClient the platform
// supports.
// Example: IAudioClient2 is supported on Windows 8 and higher => 2 is returned.
uint32_t GetAudioClientVersion();
// Creates an IMMDeviceEnumerator interface which provides methods for
// enumerating audio endpoint devices.
// TODO(henrika): IMMDeviceEnumerator::RegisterEndpointNotificationCallback.
@ -367,16 +422,29 @@ int NumberOfActiveSessions(IMMDevice* device);
Microsoft::WRL::ComPtr<IAudioClient> CreateClient(const std::string& device_id,
EDataFlow data_flow,
ERole role);
Microsoft::WRL::ComPtr<IAudioClient2>
CreateClient2(const std::string& device_id, EDataFlow data_flow, ERole role);
Microsoft::WRL::ComPtr<IAudioClient3>
CreateClient3(const std::string& device_id, EDataFlow data_flow, ERole role);
// Sets the AudioCategory_Communications category. Should be called before
// GetSharedModeMixFormat() and IsFormatSupported().
// Minimum supported client: Windows 8.
// GetSharedModeMixFormat() and IsFormatSupported(). The |client| argument must
// be an IAudioClient2 or IAudioClient3 interface pointer, hence only supported
// on Windows 8 and above.
// TODO(henrika): evaluate effect (if any).
HRESULT SetClientProperties(IAudioClient2* client);
// Returns the buffer size limits of the hardware audio engine in
// 100-nanosecond units given a specified |format|. Does not require prior
// audio stream initialization. The |client| argument must be an IAudioClient2
// or IAudioClient3 interface pointer, hence only supported on Windows 8 and
// above.
// TODO(henrika): always fails with AUDCLNT_E_OFFLOAD_MODE_ONLY.
HRESULT GetBufferSizeLimits(IAudioClient2* client,
const WAVEFORMATEXTENSIBLE* format,
REFERENCE_TIME* min_buffer_duration,
REFERENCE_TIME* max_buffer_duration);
// Get the mix format that the audio engine uses internally for processing
// of shared-mode streams. The client can call this method before calling
// IAudioClient::Initialize. When creating a shared-mode stream for an audio
@ -403,15 +471,35 @@ HRESULT GetDevicePeriod(IAudioClient* client,
AUDCLNT_SHAREMODE share_mode,
REFERENCE_TIME* device_period);
// Get the preferred audio parameters for the given |device_id|. The acquired
// values should only be utilized for shared mode streamed since there are no
// preferred settings for an exclusive mode stream.
// Returns the range of periodicities supported by the engine for the specified
// stream |format|. The periodicity of the engine is the rate at which the
// engine wakes an event-driven audio client to transfer audio data to or from
// the engine. Can be used for low-latency support on some devices.
// The |client| argument must be an IAudioClient3 interface pointer, hence only
// supported on Windows 10 and above.
HRESULT GetSharedModeEnginePeriod(IAudioClient3* client3,
const WAVEFORMATEXTENSIBLE* format,
uint32_t* default_period_in_frames,
uint32_t* fundamental_period_in_frames,
uint32_t* min_period_in_frames,
uint32_t* max_period_in_frames);
// Get the preferred audio parameters for the given |device_id| or |client|
// corresponding to the stream format that the audio engine uses for its
// internal processing of shared-mode streams. The acquired values should only
// be utilized for shared mode streamed since there are no preferred settings
// for an exclusive mode stream.
HRESULT GetPreferredAudioParameters(const std::string& device_id,
bool is_output_device,
webrtc::AudioParameters* params);
HRESULT GetPreferredAudioParameters(IAudioClient* client,
webrtc::AudioParameters* params);
// As above but override the preferred sample rate and use |sample_rate|
// instead. Intended mainly for testing purposes and in combination with rate
// conversion.
HRESULT GetPreferredAudioParameters(IAudioClient* client,
webrtc::AudioParameters* params,
uint32_t sample_rate);
// After activating an IAudioClient interface on an audio endpoint device,
// the client must initialize it once, and only once, to initialize the audio
@ -419,17 +507,39 @@ HRESULT GetPreferredAudioParameters(IAudioClient* client,
// connects indirectly through the audio engine which does the mixing.
// If a valid event is provided in |event_handle|, the client will be
// initialized for event-driven buffer handling. If |event_handle| is set to
// nullptr, event-driven buffer handling is not utilized.
// nullptr, event-driven buffer handling is not utilized. To achieve the
// minimum stream latency between the client application and audio endpoint
// device, set |buffer_duration| to 0. A client has the option of requesting a
// buffer size that is larger than what is strictly necessary to make timing
// glitches rare or nonexistent. Increasing the buffer size does not necessarily
// increase the stream latency. Each unit of reference time is 100 nanoseconds.
// The |auto_convert_pcm| parameter can be used for testing purposes to ensure
// that the sample rate of the client side does not have to match the audio
// engine mix format. If |auto_convert_pcm| is set to true, a rate converter
// will be inserted to convert between the sample rate in |format| and the
// preferred rate given by GetPreferredAudioParameters().
// The output parameter |endpoint_buffer_size| contains the size of the
// endpoint buffer and it is expressed as the number of audio frames the
// buffer can hold.
// TODO(henrika):
// - use IAudioClient2::SetClientProperties before calling this method
// - IAudioClient::Initialize(your_format, AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
// AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY)
HRESULT SharedModeInitialize(IAudioClient* client,
const WAVEFORMATEXTENSIBLE* format,
HANDLE event_handle,
REFERENCE_TIME buffer_duration,
bool auto_convert_pcm,
uint32_t* endpoint_buffer_size);
// Works as SharedModeInitialize() but adds support for using smaller engine
// periods than the default period.
// The |client| argument must be an IAudioClient3 interface pointer, hence only
// supported on Windows 10 and above.
// TODO(henrika): can probably be merged into SharedModeInitialize() to avoid
// duplicating code. Keeping as separate method for now until decided if we
// need low-latency support.
HRESULT SharedModeInitializeLowLatency(IAudioClient3* client,
const WAVEFORMATEXTENSIBLE* format,
HANDLE event_handle,
uint32_t period_in_frames,
bool auto_convert_pcm,
uint32_t* endpoint_buffer_size);
// Creates an IAudioRenderClient client for an existing IAudioClient given by
@ -451,6 +561,12 @@ Microsoft::WRL::ComPtr<IAudioCaptureClient> CreateCaptureClient(
// data rate and the current position in the stream.
Microsoft::WRL::ComPtr<IAudioClock> CreateAudioClock(IAudioClient* client);
// Creates an AudioSessionControl interface for an existing IAudioClient given
// by |client|. The IAudioControl interface enables a client to configure the
// control parameters for an audio session and to monitor events in the session.
Microsoft::WRL::ComPtr<IAudioSessionControl> CreateAudioSessionControl(
IAudioClient* client);
// Creates an ISimpleAudioVolume interface for an existing IAudioClient given by
// |client|. This interface enables a client to control the master volume level
// of an active audio session.
@ -469,6 +585,10 @@ std::string WaveFormatExToString(const WAVEFORMATEXTENSIBLE* format);
// generic webrtc::TimeDelta which then can be converted to any time unit.
webrtc::TimeDelta ReferenceTimeToTimeDelta(REFERENCE_TIME time);
// Converts size expressed in number of audio frames, |num_frames|, into
// milliseconds given a specified |sample_rate|.
double FramesToMilliseconds(uint32_t num_frames, uint16_t sample_rate);
// Converts a COM error into a human-readable string.
std::string ErrorToString(const _com_error& error);

View File

@ -91,6 +91,12 @@ TEST_F(CoreAudioUtilityWinTest, NumberOfActiveDevices) {
EXPECT_EQ(total_devices, render_devices + capture_devices);
}
TEST_F(CoreAudioUtilityWinTest, GetAudioClientVersion) {
uint32_t client_version = core_audio_utility::GetAudioClientVersion();
EXPECT_GE(client_version, 1u);
EXPECT_LE(client_version, 3u);
}
TEST_F(CoreAudioUtilityWinTest, CreateDeviceEnumerator) {
ABORT_TEST_IF_NOT(DevicesAvailable());
ComPtr<IMMDeviceEnumerator> enumerator =
@ -338,27 +344,90 @@ TEST_F(CoreAudioUtilityWinTest, CreateClient) {
TEST_F(CoreAudioUtilityWinTest, CreateClient2) {
ABORT_TEST_IF_NOT(DevicesAvailable() &&
rtc::rtc_win::GetVersion() >= rtc::rtc_win::VERSION_WIN10);
core_audio_utility::GetAudioClientVersion() >= 2);
EDataFlow data_flow[] = {eRender, eCapture};
// Obtain reference to an IAudioClient2 interface for a default audio endpoint
// device specified by two different data flows and the |eConsole| role.
for (size_t i = 0; i < arraysize(data_flow); ++i) {
ComPtr<IAudioClient2> client = core_audio_utility::CreateClient2(
ComPtr<IAudioClient2> client2 = core_audio_utility::CreateClient2(
AudioDeviceName::kDefaultDeviceId, data_flow[i], eConsole);
EXPECT_TRUE(client.Get());
EXPECT_TRUE(client2.Get());
}
}
TEST_F(CoreAudioUtilityWinTest, CreateClient3) {
ABORT_TEST_IF_NOT(DevicesAvailable() &&
core_audio_utility::GetAudioClientVersion() >= 3);
EDataFlow data_flow[] = {eRender, eCapture};
// Obtain reference to an IAudioClient3 interface for a default audio endpoint
// device specified by two different data flows and the |eConsole| role.
for (size_t i = 0; i < arraysize(data_flow); ++i) {
ComPtr<IAudioClient3> client3 = core_audio_utility::CreateClient3(
AudioDeviceName::kDefaultDeviceId, data_flow[i], eConsole);
EXPECT_TRUE(client3.Get());
}
}
TEST_F(CoreAudioUtilityWinTest, SetClientProperties) {
ABORT_TEST_IF_NOT(DevicesAvailable());
ABORT_TEST_IF_NOT(DevicesAvailable() &&
core_audio_utility::GetAudioClientVersion() >= 2);
ComPtr<IAudioClient2> client = core_audio_utility::CreateClient2(
ComPtr<IAudioClient2> client2 = core_audio_utility::CreateClient2(
AudioDeviceName::kDefaultDeviceId, eRender, eConsole);
EXPECT_TRUE(client.Get());
EXPECT_TRUE(client2.Get());
EXPECT_TRUE(
SUCCEEDED(core_audio_utility::SetClientProperties(client2.Get())));
EXPECT_TRUE(SUCCEEDED(core_audio_utility::SetClientProperties(client.Get())));
ComPtr<IAudioClient3> client3 = core_audio_utility::CreateClient3(
AudioDeviceName::kDefaultDeviceId, eRender, eConsole);
EXPECT_TRUE(client3.Get());
EXPECT_TRUE(
SUCCEEDED(core_audio_utility::SetClientProperties(client3.Get())));
}
TEST_F(CoreAudioUtilityWinTest, GetSharedModeEnginePeriod) {
ABORT_TEST_IF_NOT(DevicesAvailable() &&
core_audio_utility::GetAudioClientVersion() >= 3);
ComPtr<IAudioClient3> client3 = core_audio_utility::CreateClient3(
AudioDeviceName::kDefaultDeviceId, eRender, eConsole);
EXPECT_TRUE(client3.Get());
WAVEFORMATPCMEX format;
EXPECT_TRUE(SUCCEEDED(
core_audio_utility::GetSharedModeMixFormat(client3.Get(), &format)));
uint32_t default_period = 0;
uint32_t fundamental_period = 0;
uint32_t min_period = 0;
uint32_t max_period = 0;
EXPECT_TRUE(SUCCEEDED(core_audio_utility::GetSharedModeEnginePeriod(
client3.Get(), &format, &default_period, &fundamental_period, &min_period,
&max_period)));
}
// TODO(henrika): figure out why usage of this API always reports
// AUDCLNT_E_OFFLOAD_MODE_ONLY.
TEST_F(CoreAudioUtilityWinTest, DISABLED_GetBufferSizeLimits) {
ABORT_TEST_IF_NOT(DevicesAvailable() &&
core_audio_utility::GetAudioClientVersion() >= 2);
ComPtr<IAudioClient2> client2 = core_audio_utility::CreateClient2(
AudioDeviceName::kDefaultDeviceId, eRender, eConsole);
EXPECT_TRUE(client2.Get());
WAVEFORMATPCMEX format;
EXPECT_TRUE(SUCCEEDED(
core_audio_utility::GetSharedModeMixFormat(client2.Get(), &format)));
REFERENCE_TIME min_buffer_duration = 0;
REFERENCE_TIME max_buffer_duration = 0;
EXPECT_TRUE(SUCCEEDED(core_audio_utility::GetBufferSizeLimits(
client2.Get(), &format, &min_buffer_duration, &max_buffer_duration)));
}
TEST_F(CoreAudioUtilityWinTest, GetSharedModeMixFormat) {
@ -468,13 +537,13 @@ TEST_F(CoreAudioUtilityWinTest, SharedModeInitialize) {
// Perform a shared-mode initialization without event-driven buffer handling.
uint32_t endpoint_buffer_size = 0;
HRESULT hr = core_audio_utility::SharedModeInitialize(
client.Get(), &format, nullptr, &endpoint_buffer_size);
client.Get(), &format, nullptr, 0, false, &endpoint_buffer_size);
EXPECT_TRUE(SUCCEEDED(hr));
EXPECT_GT(endpoint_buffer_size, 0u);
// It is only possible to create a client once.
hr = core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr,
&endpoint_buffer_size);
hr = core_audio_utility::SharedModeInitialize(
client.Get(), &format, nullptr, 0, false, &endpoint_buffer_size);
EXPECT_FALSE(SUCCEEDED(hr));
EXPECT_EQ(hr, AUDCLNT_E_ALREADY_INITIALIZED);
@ -483,8 +552,8 @@ TEST_F(CoreAudioUtilityWinTest, SharedModeInitialize) {
client = core_audio_utility::CreateClient(AudioDeviceName::kDefaultDeviceId,
eRender, eConsole);
EXPECT_TRUE(client.Get());
hr = core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr,
&endpoint_buffer_size);
hr = core_audio_utility::SharedModeInitialize(
client.Get(), &format, nullptr, 0, false, &endpoint_buffer_size);
EXPECT_TRUE(SUCCEEDED(hr));
EXPECT_GT(endpoint_buffer_size, 0u);
@ -497,8 +566,8 @@ TEST_F(CoreAudioUtilityWinTest, SharedModeInitialize) {
format.Format.nSamplesPerSec = format.Format.nSamplesPerSec + 1;
EXPECT_FALSE(core_audio_utility::IsFormatSupported(
client.Get(), AUDCLNT_SHAREMODE_SHARED, &format));
hr = core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr,
&endpoint_buffer_size);
hr = core_audio_utility::SharedModeInitialize(
client.Get(), &format, nullptr, 0, false, &endpoint_buffer_size);
EXPECT_TRUE(FAILED(hr));
EXPECT_EQ(hr, E_INVALIDARG);
@ -515,9 +584,12 @@ TEST_F(CoreAudioUtilityWinTest, SharedModeInitialize) {
EXPECT_TRUE(core_audio_utility::IsFormatSupported(
client.Get(), AUDCLNT_SHAREMODE_SHARED, &format));
hr = core_audio_utility::SharedModeInitialize(
client.Get(), &format, event_handle, &endpoint_buffer_size);
client.Get(), &format, event_handle, 0, false, &endpoint_buffer_size);
EXPECT_TRUE(SUCCEEDED(hr));
EXPECT_GT(endpoint_buffer_size, 0u);
// TODO(henrika): possibly add test for signature which overrides the default
// sample rate.
}
TEST_F(CoreAudioUtilityWinTest, CreateRenderAndCaptureClients) {
@ -547,7 +619,7 @@ TEST_F(CoreAudioUtilityWinTest, CreateRenderAndCaptureClients) {
// Do a proper initialization and verify that it works this time.
core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr,
&endpoint_buffer_size);
0, false, &endpoint_buffer_size);
render_client = core_audio_utility::CreateRenderClient(client.Get());
EXPECT_TRUE(render_client.Get());
EXPECT_GT(endpoint_buffer_size, 0u);
@ -559,7 +631,7 @@ TEST_F(CoreAudioUtilityWinTest, CreateRenderAndCaptureClients) {
// Do a proper initialization and verify that it works this time.
core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr,
&endpoint_buffer_size);
0, false, &endpoint_buffer_size);
capture_client = core_audio_utility::CreateCaptureClient(client.Get());
EXPECT_TRUE(capture_client.Get());
EXPECT_GT(endpoint_buffer_size, 0u);
@ -592,8 +664,8 @@ TEST_F(CoreAudioUtilityWinTest, CreateAudioClock) {
EXPECT_FALSE(audio_clock.Get());
// Do a proper initialization and verify that it works this time.
core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr,
&endpoint_buffer_size);
core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr, 0,
false, &endpoint_buffer_size);
audio_clock = core_audio_utility::CreateAudioClock(client.Get());
EXPECT_TRUE(audio_clock.Get());
EXPECT_GT(endpoint_buffer_size, 0u);
@ -605,6 +677,51 @@ TEST_F(CoreAudioUtilityWinTest, CreateAudioClock) {
}
}
TEST_F(CoreAudioUtilityWinTest, CreateAudioSessionControl) {
ABORT_TEST_IF_NOT(DevicesAvailable());
EDataFlow data_flow[] = {eRender, eCapture};
WAVEFORMATPCMEX format;
uint32_t endpoint_buffer_size = 0;
for (size_t i = 0; i < arraysize(data_flow); ++i) {
ComPtr<IAudioClient> client;
ComPtr<IAudioSessionControl> audio_session_control;
// Create a default client for the given data-flow direction.
client = core_audio_utility::CreateClient(AudioDeviceName::kDefaultDeviceId,
data_flow[i], eConsole);
EXPECT_TRUE(client.Get());
EXPECT_TRUE(SUCCEEDED(
core_audio_utility::GetSharedModeMixFormat(client.Get(), &format)));
// It is not possible to create an audio session control using an
// unitialized client interface.
audio_session_control =
core_audio_utility::CreateAudioSessionControl(client.Get());
EXPECT_FALSE(audio_session_control.Get());
// Do a proper initialization and verify that it works this time.
core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr, 0,
false, &endpoint_buffer_size);
audio_session_control =
core_audio_utility::CreateAudioSessionControl(client.Get());
EXPECT_TRUE(audio_session_control.Get());
EXPECT_GT(endpoint_buffer_size, 0u);
// Use the audio session control and verify that the session state can be
// queried. When a client opens a session by assigning the first stream to
// the session (by calling the IAudioClient::Initialize method), the initial
// session state is inactive. The session state changes from inactive to
// active when a stream in the session begins running (because the client
// has called the IAudioClient::Start method).
AudioSessionState state;
EXPECT_TRUE(SUCCEEDED(audio_session_control->GetState(&state)));
EXPECT_EQ(state, AudioSessionStateInactive);
}
}
TEST_F(CoreAudioUtilityWinTest, CreateSimpleAudioVolume) {
ABORT_TEST_IF_NOT(DevicesAvailable());
@ -631,8 +748,8 @@ TEST_F(CoreAudioUtilityWinTest, CreateSimpleAudioVolume) {
EXPECT_FALSE(simple_audio_volume.Get());
// Do a proper initialization and verify that it works this time.
core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr,
&endpoint_buffer_size);
core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr, 0,
false, &endpoint_buffer_size);
simple_audio_volume =
core_audio_utility::CreateSimpleAudioVolume(client.Get());
EXPECT_TRUE(simple_audio_volume.Get());
@ -666,8 +783,8 @@ TEST_F(CoreAudioUtilityWinTest, FillRenderEndpointBufferWithSilence) {
uint32_t endpoint_buffer_size = 0;
EXPECT_TRUE(SUCCEEDED(
core_audio_utility::GetSharedModeMixFormat(client.Get(), &format)));
core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr,
&endpoint_buffer_size);
core_audio_utility::SharedModeInitialize(client.Get(), &format, nullptr, 0,
false, &endpoint_buffer_size);
EXPECT_GT(endpoint_buffer_size, 0u);
ComPtr<IAudioRenderClient> render_client(

View File

@ -261,6 +261,12 @@ OSInfo::OSInfo()
architecture_(OTHER_ARCHITECTURE),
wow64_status_(GetWOW64StatusForProcess(GetCurrentProcess())) {
OSVERSIONINFOEX version_info = {sizeof version_info};
// Applications not manifested for Windows 8.1 or Windows 10 will return the
// Windows 8 OS version value (6.2). Once an application is manifested for a
// given operating system version, GetVersionEx() will always return the
// version that the application is manifested for in future releases.
// https://docs.microsoft.com/en-us/windows/desktop/SysInfo/targeting-your-application-at-windows-8-1.
// https://www.codeproject.com/Articles/678606/Part-Overcoming-Windows-s-deprecation-of-GetVe.
::GetVersionEx(reinterpret_cast<OSVERSIONINFO*>(&version_info));
version_number_.major = version_info.dwMajorVersion;
version_number_.minor = version_info.dwMinorVersion;