diff --git a/modules/audio_device/audio_device_buffer.cc b/modules/audio_device/audio_device_buffer.cc index 8da8c1c903..e6c2fa2a6b 100644 --- a/modules/audio_device/audio_device_buffer.cc +++ b/modules/audio_device/audio_device_buffer.cc @@ -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" diff --git a/modules/audio_device/audio_device_impl.cc b/modules/audio_device/audio_device_impl.cc index 834f4aa917..91d6208fe1 100644 --- a/modules/audio_device/audio_device_impl.cc +++ b/modules/audio_device/audio_device_impl.cc @@ -68,10 +68,16 @@ namespace webrtc { -// static rtc::scoped_refptr AudioDeviceModule::Create( const AudioLayer audio_layer) { RTC_LOG(INFO) << __FUNCTION__; + return AudioDeviceModule::CreateForTest(audio_layer); +} + +// static +rtc::scoped_refptr 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. diff --git a/modules/audio_device/audio_device_impl.h b/modules/audio_device/audio_device_impl.h index 9ccb4154a3..481cdf32e0 100644 --- a/modules/audio_device/audio_device_impl.h +++ b/modules/audio_device/audio_device_impl.h @@ -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; diff --git a/modules/audio_device/audio_device_unittest.cc b/modules/audio_device/audio_device_unittest.cc index 16aef4d8b6..3f2a3f3ddc 100644 --- a/modules/audio_device/audio_device_unittest.cc +++ b/modules/audio_device/audio_device_unittest.cc @@ -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& 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& audio_device() const { return audio_device_; } - rtc::scoped_refptr CreateAudioDevice() { + rtc::scoped_refptr 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 audio_device_; + rtc::scoped_refptr 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 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: diff --git a/modules/audio_device/fine_audio_buffer.cc b/modules/audio_device/fine_audio_buffer.cc index bd18359009..4af344aef3 100644 --- a/modules/audio_device/fine_audio_buffer.cc +++ b/modules/audio_device/fine_audio_buffer.cc @@ -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(); diff --git a/modules/audio_device/include/audio_device.h b/modules/audio_device/include/audio_device.h index a861319374..d73c1e66ea 100644 --- a/modules/audio_device/include/audio_device.h +++ b/modules/audio_device/include/audio_device.h @@ -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 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 CreateForTest( + const AudioLayer audio_layer); // TODO(bugs.webrtc.org/7306): deprecated (to be removed). static rtc::scoped_refptr 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_ diff --git a/modules/audio_device/include/audio_device_factory.cc b/modules/audio_device/include/audio_device_factory.cc index abcaa212e2..4e0a759c4b 100644 --- a/modules/audio_device/include/audio_device_factory.cc +++ b/modules/audio_device/include/audio_device_factory.cc @@ -24,6 +24,12 @@ namespace webrtc { rtc::scoped_refptr CreateWindowsCoreAudioAudioDeviceModule() { + RTC_DLOG(INFO) << __FUNCTION__; + return CreateWindowsCoreAudioAudioDeviceModuleForTest(); +} + +rtc::scoped_refptr +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. diff --git a/modules/audio_device/include/audio_device_factory.h b/modules/audio_device/include/audio_device_factory.h index 4297988a13..c6a694afac 100644 --- a/modules/audio_device/include/audio_device_factory.h +++ b/modules/audio_device/include/audio_device_factory.h @@ -35,6 +35,9 @@ namespace webrtc { // rtc::scoped_refptr CreateWindowsCoreAudioAudioDeviceModule(); +rtc::scoped_refptr +CreateWindowsCoreAudioAudioDeviceModuleForTest(); + } // namespace webrtc #endif // MODULES_AUDIO_DEVICE_INCLUDE_AUDIO_DEVICE_FACTORY_H_ diff --git a/modules/audio_device/win/audio_device_module_win.cc b/modules/audio_device/win/audio_device_module_win.cc index b919334bc7..dc10adaa7f 100644 --- a/modules/audio_device/win/audio_device_module_win.cc +++ b/modules/audio_device/win/audio_device_module_win.cc @@ -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 +rtc::scoped_refptr CreateWindowsCoreAudioAudioDeviceModuleFromInputAndOutput( std::unique_ptr audio_input, std::unique_ptr audio_output) { diff --git a/modules/audio_device/win/audio_device_module_win.h b/modules/audio_device/win/audio_device_module_win.h index 7484ef7f8d..e4c5e56b1c 100644 --- a/modules/audio_device/win/audio_device_module_win.h +++ b/modules/audio_device/win/audio_device_module_win.h @@ -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 +rtc::scoped_refptr CreateWindowsCoreAudioAudioDeviceModuleFromInputAndOutput( std::unique_ptr audio_input, std::unique_ptr audio_output); diff --git a/modules/audio_device/win/core_audio_base_win.cc b/modules/audio_device/win/core_audio_base_win.cc index 2f37e4bcb6..a0164e27cb 100644 --- a/modules/audio_device/win/core_audio_base_win.cc +++ b/modules/audio_device/win/core_audio_base_win.cc @@ -9,6 +9,7 @@ */ #include "modules/audio_device/win/core_audio_base_win.h" +#include "modules/audio_device/audio_device_buffer.h" #include @@ -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(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 audio_client = - core_audio_utility::CreateClient(device_id, GetDataFlow(), role); - if (!audio_client.Get()) { + ComPtr 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) { 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(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(), - ¶ms))) { + HRESULT res = sample_rate_ ? core_audio_utility::GetPreferredAudioParameters( + audio_client.Get(), ¶ms, *sample_rate_) + : core_audio_utility::GetPreferredAudioParameters( + audio_client.Get(), ¶ms); + if (FAILED(res)) { return false; } @@ -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. - if (!core_audio_utility::IsFormatSupported( - audio_client.Get(), AUDCLNT_SHAREMODE_SHARED, &format_)) { - return false; + // 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; + } } - // Initialize the audio stream between the client and the device in shared - // mode using event-driven buffer handling. - if (FAILED(core_audio_utility::SharedModeInitialize( - audio_client.Get(), &format_, audio_samples_event_, - &endpoint_buffer_size_frames_))) { - return false; + // Check if low-latency is supported and use special initialization if it is. + // Low-latency initialization requires these things: + // - IAudioClient3 (>= Win10) + // - HDAudio driver + // - kEnableLowLatencyIfSupported changed from false (default) to true. + // TODO(henrika): IsLowLatencySupported() returns AUDCLNT_E_UNSUPPORTED_FORMAT + // when |sample_rate_.has_value()| returns true if rate conversion is + // actually required (i.e., client asks for other than the default rate). + bool low_latency_support = false; + uint32_t min_period_in_frames = 0; + if (kEnableLowLatencyIfSupported && + core_audio_utility::GetAudioClientVersion() >= 3) { + low_latency_support = + IsLowLatencySupported(static_cast(audio_client.Get()), + &format_, &min_period_in_frames); + } + + if (low_latency_support) { + RTC_DCHECK_GE(core_audio_utility::GetAudioClientVersion(), 3); + // Use IAudioClient3::InitializeSharedAudioStream() API to initialize a + // low-latency event-driven client. Request the smallest possible + // periodicity. + // TODO(henrika): evaluate this scheme in terms of CPU etc. + if (FAILED(core_audio_utility::SharedModeInitializeLowLatency( + static_cast(audio_client.Get()), &format_, + audio_samples_event_, min_period_in_frames, + sample_rate_.has_value(), &endpoint_buffer_size_frames_))) { + return false; + } + } else { + // Initialize the audio stream between the client and the device in shared + // mode using event-driven buffer handling. Also, using 0 as requested + // buffer size results in a default (minimum) endpoint buffer size. + // TODO(henrika): possibly increase |requested_buffer_size| to add + // robustness. + const REFERENCE_TIME requested_buffer_size = 0; + if (FAILED(core_audio_utility::SharedModeInitialize( + audio_client.Get(), &format_, audio_samples_event_, + requested_buffer_size, sample_rate_.has_value(), + &endpoint_buffer_size_frames_))) { + return false; + } } // Check device period and the preferred buffer size and log a warning if // WebRTC's buffer size is not an even divisor of the preferred buffer size // in Core Audio. - // TODO(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. - audio_client_ = audio_client; + // Create an AudioSessionControl interface given the initialized client. + // The IAudioControl interface enables a client to configure the control + // parameters for an audio session and to monitor events in the session. + ComPtr audio_session_control = + core_audio_utility::CreateAudioSessionControl(audio_client.Get()); + if (!audio_session_control.Get()) { + return false; + } + + // The Sndvol program displays volume and mute controls for sessions that + // are in the active and inactive states. + AudioSessionState state; + if (FAILED(audio_session_control->GetState(&state))) { + return false; + } + RTC_DLOG(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()) << "]"; - - audio_thread_ = absl::make_unique( - Run, this, IsInput() ? "wasapi_capture_thread" : "wasapi_render_thread", - rtc::kRealtimePriority); - audio_thread_->Start(); - if (!audio_thread_->IsRunning()) { - StopThread(); - RTC_LOG(LS_ERROR) << "Failed to start audio thread"; - return false; + 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( + 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() + << " and id: " << audio_thread_->GetThreadRef(); } - RTC_DLOG(INFO) << "Started thread with name: " << audio_thread_->name(); // 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); } - StopThread(); + // 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(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 diff --git a/modules/audio_device/win/core_audio_base_win.h b/modules/audio_device/win/core_audio_base_win.h index b0886f99d3..56efc56b27 100644 --- a/modules/audio_device/win/core_audio_base_win.h +++ b/modules/audio_device/win/core_audio_base_win.h @@ -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 #include #include #include +#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 + auto as_integer(T const value) -> typename std::underlying_type::type { + return static_cast::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 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 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 audio_client_; Microsoft::WRL::ComPtr audio_clock_; - ScopedHandle audio_samples_event_; - ScopedHandle stop_event_; - std::unique_ptr audio_thread_; + Microsoft::WRL::ComPtr audio_client_; + bool is_active_ = false; + int64_t num_data_callbacks_ = 0; + int latency_ms_ = 0; + absl::optional 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 is_restarting_; + std::unique_ptr audio_thread_; + Microsoft::WRL::ComPtr 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 diff --git a/modules/audio_device/win/core_audio_input_win.cc b/modules/audio_device/win/core_audio_input_win.cc index a389393f04..e5512c1fad 100644 --- a/modules/audio_device/win/core_audio_input_win.cc +++ b/modules/audio_device/win/core_audio_input_win.cc @@ -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(audio_device_buffer_); // Create an IAudioCaptureClient for an initialized IAudioClient. @@ -131,7 +126,7 @@ int CoreAudioInput::InitRecording() { // a capture endpoint buffer. ComPtr 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. - // TODO(henrika): note that FineAudioBuffer adds latency as well. - auto opt_record_delay_ms = EstimateLatencyMillis(capture_time_100ns); + // 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 opt_record_delay_ms; + // TODO(henrika): note that FineAudioBuffer adds latency as well. + 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(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 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 diff --git a/modules/audio_device/win/core_audio_input_win.h b/modules/audio_device/win/core_audio_input_win.h index 0dd2e3730a..709dced9f3 100644 --- a/modules/audio_device/win/core_audio_input_win.h +++ b/modules/audio_device/win/core_audio_input_win.h @@ -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 EstimateLatencyMillis(uint64_t capture_time_100ns); + bool HandleStreamDisconnected(); std::unique_ptr fine_audio_buffer_; Microsoft::WRL::ComPtr audio_capture_client_; diff --git a/modules/audio_device/win/core_audio_output_win.cc b/modules/audio_device/win/core_audio_output_win.cc index a391109dba..e6e84fe03e 100644 --- a/modules/audio_device/win/core_audio_output_win.cc +++ b/modules/audio_device/win/core_audio_output_win.cc @@ -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(audio_device_buffer_); // Create an IAudioRenderClient for an initialized IAudioClient. @@ -132,16 +124,17 @@ int CoreAudioOutput::InitPlayout() { // a rendering endpoint buffer. ComPtr audio_render_client = core_audio_utility::CreateRenderClient(audio_client_.Get()); - if (!audio_render_client.Get()) + if (!audio_render_client.Get()) { return -1; + } ComPtr 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. - // 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; + // 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. + 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(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 diff --git a/modules/audio_device/win/core_audio_output_win.h b/modules/audio_device/win/core_audio_output_win.h index c947f3dd81..f0f619756b 100644 --- a/modules/audio_device/win/core_audio_output_win.h +++ b/modules/audio_device/win/core_audio_output_win.h @@ -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 fine_audio_buffer_; Microsoft::WRL::ComPtr audio_render_client_; diff --git a/modules/audio_device/win/core_audio_utility_win.cc b/modules/audio_device/win/core_audio_utility_win.cc index 5dc45a8a98..040672a3ce 100644 --- a/modules/audio_device/win/core_audio_utility_win.cc +++ b/modules/audio_device/win/core_audio_utility_win.cc @@ -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 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 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 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 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 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 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 CreateClientInternal(IMMDevice* audio_device) { ComPtr 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 CreateClient2Internal(IMMDevice* audio_device) { ComPtr 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 CreateClient3Internal(IMMDevice* audio_device) { + if (!audio_device) + return ComPtr(); + + ComPtr 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 CreateCollectionInternal(EDataFlow data_flow) { ComPtr device_enumerator( CreateDeviceEnumeratorInternal(true)); @@ -272,7 +287,7 @@ ComPtr CreateCollectionInternal(EDataFlow data_flow) { ComPtr 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 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(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 CreateDeviceEnumerator() { RTC_DLOG(INFO) << "CreateDeviceEnumerator"; return CreateDeviceEnumeratorInternal(true); @@ -494,7 +527,7 @@ EDataFlow GetDataFlow(IMMDevice* device) { RTC_DCHECK(device); ComPtr 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 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 CreateClient2(const std::string& device_id, return CreateClient2Internal(device.Get()); } +ComPtr CreateClient3(const std::string& device_id, + EDataFlow data_flow, + ERole role) { + RTC_DLOG(INFO) << "CreateClient3"; + ComPtr 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(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 format_ex; _com_error error = client->GetMixFormat(reinterpret_cast(&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(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 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(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(buffer_size_in_frames) / + (format->Format.nSamplesPerSec / 1000.0); + RTC_DLOG(INFO) << "endpoint buffer size: " + << static_cast(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(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(buffer_size_in_frames) / + (format->Format.nSamplesPerSec / 1000.0); + RTC_DLOG(INFO) << "endpoint buffer size: " + << static_cast(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 CreateRenderClient(IAudioClient* client) { RTC_DLOG(INFO) << "CreateRenderClient"; RTC_DCHECK(client); @@ -815,7 +1078,7 @@ ComPtr CreateRenderClient(IAudioClient* client) { // enables us to write output data to a rendering endpoint buffer. ComPtr 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 CreateCaptureClient(IAudioClient* client) { // enables us to read input data from a capturing endpoint buffer. ComPtr 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 CreateAudioClock(IAudioClient* client) { // monitor a stream's data rate and the current position in the stream. ComPtr 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(); @@ -855,6 +1118,19 @@ ComPtr CreateAudioClock(IAudioClient* client) { return audio_clock; } +ComPtr CreateAudioSessionControl(IAudioClient* client) { + RTC_DLOG(INFO) << "CreateAudioSessionControl"; + RTC_DCHECK(client); + ComPtr 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(); + } + return audio_session_control; +} + ComPtr CreateSimpleAudioVolume(IAudioClient* client) { RTC_DLOG(INFO) << "CreateSimpleAudioVolume"; RTC_DCHECK(client); @@ -862,7 +1138,7 @@ ComPtr CreateSimpleAudioVolume(IAudioClient* client) { // client to control the master volume level of an audio session. ComPtr 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(num_frames) / (sample_rate / 1000.0); +} + std::string ErrorToString(const _com_error& error) { char ss_buf[1024]; rtc::SimpleStringBuilder ss(ss_buf); diff --git a/modules/audio_device/win/core_audio_utility_win.h b/modules/audio_device/win/core_audio_utility_win.h index 87e49606ca..9cec9ebe6f 100644 --- a/modules/audio_device/win/core_audio_utility_win.h +++ b/modules/audio_device/win/core_audio_utility_win.h @@ -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 CreateClient(const std::string& device_id, EDataFlow data_flow, ERole role); - Microsoft::WRL::ComPtr CreateClient2(const std::string& device_id, EDataFlow data_flow, ERole role); +Microsoft::WRL::ComPtr +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,19 +507,41 @@ 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 // |client|. The IAudioRenderClient interface enables a client to write // output data to a rendering endpoint buffer. The methods in this interface @@ -451,6 +561,12 @@ Microsoft::WRL::ComPtr CreateCaptureClient( // data rate and the current position in the stream. Microsoft::WRL::ComPtr 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 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); diff --git a/modules/audio_device/win/core_audio_utility_win_unittest.cc b/modules/audio_device/win/core_audio_utility_win_unittest.cc index 3944cb002d..2d423feeb0 100644 --- a/modules/audio_device/win/core_audio_utility_win_unittest.cc +++ b/modules/audio_device/win/core_audio_utility_win_unittest.cc @@ -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 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 client = core_audio_utility::CreateClient2( + ComPtr 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 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 client = core_audio_utility::CreateClient2( + ComPtr 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 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 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 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 client; + ComPtr 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 render_client( diff --git a/rtc_base/win/windows_version.cc b/rtc_base/win/windows_version.cc index 9ccde4c510..f10e42c202 100644 --- a/rtc_base/win/windows_version.cc +++ b/rtc_base/win/windows_version.cc @@ -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(&version_info)); version_number_.major = version_info.dwMajorVersion; version_number_.minor = version_info.dwMinorVersion;