| /* |
| * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. |
| * |
| * Use of this source code is governed by a BSD-style license |
| * that can be found in the LICENSE file in the root of the source |
| * tree. An additional intellectual property rights grant can be found |
| * in the file PATENTS. All contributing project authors may |
| * be found in the AUTHORS file in the root of the source tree. |
| */ |
| |
| #ifndef MODULES_AUDIO_DEVICE_WIN_CORE_AUDIO_UTILITY_WIN_H_ |
| #define MODULES_AUDIO_DEVICE_WIN_CORE_AUDIO_UTILITY_WIN_H_ |
| |
| #include <audioclient.h> |
| #include <audiopolicy.h> |
| #include <avrt.h> |
| #include <comdef.h> |
| #include <mmdeviceapi.h> |
| #include <objbase.h> |
| #include <propidl.h> |
| #include <wrl/client.h> |
| |
| #include <string> |
| |
| #include "absl/strings/string_view.h" |
| #include "api/units/time_delta.h" |
| #include "modules/audio_device/audio_device_name.h" |
| #include "modules/audio_device/include/audio_device_defines.h" |
| #include "rtc_base/logging.h" |
| #include "rtc_base/string_utils.h" |
| |
| #pragma comment(lib, "Avrt.lib") |
| |
| namespace webrtc { |
| namespace webrtc_win { |
| |
| // 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`. |
| // The Multimedia Class Scheduler service (MMCSS) enables multimedia |
| // applications to ensure that their time-sensitive processing receives |
| // prioritized access to CPU resources without denying CPU resources to |
| // 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 wchar_t* task_name) { |
| RTC_DLOG(LS_INFO) << "ScopedMMCSSRegistration: " << rtc::ToUtf8(task_name); |
| // Register the calling thread with MMCSS for the supplied `task_name`. |
| DWORD mmcss_task_index = 0; |
| mmcss_handle_ = AvSetMmThreadCharacteristicsW(task_name, &mmcss_task_index); |
| 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(LS_INFO) << "priority class: " |
| << PriorityClassToString(priority_class) << "(" |
| << priority_class << ")"; |
| RTC_DLOG(LS_INFO) << "priority: " << PriorityToString(priority) << "(" |
| << priority << ")"; |
| } |
| } |
| |
| ~ScopedMMCSSRegistration() { |
| if (Succeeded()) { |
| // Deregister with MMCSS. |
| RTC_DLOG(LS_INFO) << "~ScopedMMCSSRegistration"; |
| AvRevertMmThreadCharacteristics(mmcss_handle_); |
| } |
| } |
| |
| ScopedMMCSSRegistration(const ScopedMMCSSRegistration&) = delete; |
| ScopedMMCSSRegistration& operator=(const ScopedMMCSSRegistration&) = delete; |
| |
| bool Succeeded() const { return mmcss_handle_ != nullptr; } |
| |
| private: |
| HANDLE mmcss_handle_ = nullptr; |
| }; |
| |
| // A PROPVARIANT that is automatically initialized and cleared upon respective |
| // construction and destruction of this class. |
| class ScopedPropVariant { |
| public: |
| ScopedPropVariant() { PropVariantInit(&pv_); } |
| |
| ~ScopedPropVariant() { Reset(); } |
| |
| ScopedPropVariant(const ScopedPropVariant&) = delete; |
| ScopedPropVariant& operator=(const ScopedPropVariant&) = delete; |
| bool operator==(const ScopedPropVariant&) const = delete; |
| bool operator!=(const ScopedPropVariant&) const = delete; |
| |
| // Returns a pointer to the underlying PROPVARIANT for use as an out param in |
| // a function call. |
| PROPVARIANT* Receive() { |
| RTC_DCHECK_EQ(pv_.vt, VT_EMPTY); |
| return &pv_; |
| } |
| |
| // Clears the instance to prepare it for re-use (e.g., via Receive). |
| void Reset() { |
| if (pv_.vt != VT_EMPTY) { |
| HRESULT result = PropVariantClear(&pv_); |
| RTC_DCHECK_EQ(result, S_OK); |
| } |
| } |
| |
| const PROPVARIANT& get() const { return pv_; } |
| const PROPVARIANT* ptr() const { return &pv_; } |
| |
| private: |
| PROPVARIANT pv_; |
| }; |
| |
| // Simple scoped memory releaser class for COM allocated memory. |
| template <typename T> |
| class ScopedCoMem { |
| public: |
| ScopedCoMem() : mem_ptr_(nullptr) {} |
| |
| ~ScopedCoMem() { Reset(nullptr); } |
| |
| ScopedCoMem(const ScopedCoMem&) = delete; |
| ScopedCoMem& operator=(const ScopedCoMem&) = delete; |
| |
| T** operator&() { // NOLINT |
| RTC_DCHECK(mem_ptr_ == nullptr); // To catch memory leaks. |
| return &mem_ptr_; |
| } |
| |
| operator T*() { return mem_ptr_; } |
| |
| T* operator->() { |
| RTC_DCHECK(mem_ptr_ != nullptr); |
| return mem_ptr_; |
| } |
| |
| const T* operator->() const { |
| RTC_DCHECK(mem_ptr_ != nullptr); |
| return mem_ptr_; |
| } |
| |
| explicit operator bool() const { return mem_ptr_; } |
| |
| friend bool operator==(const ScopedCoMem& lhs, std::nullptr_t) { |
| return lhs.Get() == nullptr; |
| } |
| |
| friend bool operator==(std::nullptr_t, const ScopedCoMem& rhs) { |
| return rhs.Get() == nullptr; |
| } |
| |
| friend bool operator!=(const ScopedCoMem& lhs, std::nullptr_t) { |
| return lhs.Get() != nullptr; |
| } |
| |
| friend bool operator!=(std::nullptr_t, const ScopedCoMem& rhs) { |
| return rhs.Get() != nullptr; |
| } |
| |
| void Reset(T* ptr) { |
| if (mem_ptr_) |
| CoTaskMemFree(mem_ptr_); |
| mem_ptr_ = ptr; |
| } |
| |
| T* Get() const { return mem_ptr_; } |
| |
| private: |
| T* mem_ptr_; |
| }; |
| |
| // A HANDLE that is automatically initialized and closed upon respective |
| // construction and destruction of this class. |
| class ScopedHandle { |
| public: |
| ScopedHandle() : handle_(nullptr) {} |
| explicit ScopedHandle(HANDLE h) : handle_(nullptr) { Set(h); } |
| |
| ~ScopedHandle() { Close(); } |
| |
| ScopedHandle& operator=(const ScopedHandle&) = delete; |
| bool operator==(const ScopedHandle&) const = delete; |
| bool operator!=(const ScopedHandle&) const = delete; |
| |
| // Use this instead of comparing to INVALID_HANDLE_VALUE. |
| bool IsValid() const { return handle_ != nullptr; } |
| |
| void Set(HANDLE new_handle) { |
| Close(); |
| // Windows is inconsistent about invalid handles. |
| // See https://blogs.msdn.microsoft.com/oldnewthing/20040302-00/?p=40443 |
| // for details. |
| if (new_handle != INVALID_HANDLE_VALUE) { |
| handle_ = new_handle; |
| } |
| } |
| |
| HANDLE Get() const { return handle_; } |
| |
| operator HANDLE() const { return handle_; } |
| |
| void Close() { |
| if (handle_) { |
| if (!::CloseHandle(handle_)) { |
| RTC_DCHECK_NOTREACHED(); |
| } |
| handle_ = nullptr; |
| } |
| } |
| |
| private: |
| HANDLE handle_; |
| }; |
| |
| // Utility methods for the Core Audio API on Windows. |
| // Always ensure that Core Audio is supported before using these methods. |
| // Use webrtc_win::core_audio_utility::IsSupported() for this purpose. |
| // Also, all methods must be called on a valid COM thread. This can be done |
| // by using the ScopedCOMInitializer helper class. |
| // These methods are based on media::CoreAudioUtil in Chrome. |
| namespace core_audio_utility { |
| |
| // Helper class which automates casting between WAVEFORMATEX and |
| // WAVEFORMATEXTENSIBLE raw pointers using implicit constructors and |
| // operator overloading. Note that, no memory is allocated by this utility |
| // structure. It only serves as a handle (or a wrapper) of the structure |
| // provided to it at construction. |
| class WaveFormatWrapper { |
| public: |
| WaveFormatWrapper(WAVEFORMATEXTENSIBLE* p) |
| : ptr_(reinterpret_cast<WAVEFORMATEX*>(p)) {} |
| WaveFormatWrapper(WAVEFORMATEX* p) : ptr_(p) {} |
| ~WaveFormatWrapper() = default; |
| |
| operator WAVEFORMATEX*() const { return ptr_; } |
| WAVEFORMATEX* operator->() const { return ptr_; } |
| WAVEFORMATEX* get() const { return ptr_; } |
| WAVEFORMATEXTENSIBLE* GetExtensible() const; |
| |
| bool IsExtensible() const; |
| bool IsPcm() const; |
| bool IsFloat() const; |
| size_t size() const; |
| |
| private: |
| WAVEFORMATEX* ptr_; |
| }; |
| |
| // Returns true if Windows Core Audio is supported. |
| // Always verify that this method returns true before using any of the |
| // other methods in this class. |
| bool IsSupported(); |
| |
| // Returns true if Multimedia Class Scheduler service (MMCSS) is supported. |
| // The MMCSS enables multimedia applications to ensure that their time-sensitive |
| // processing receives prioritized access to CPU resources without denying CPU |
| // resources to lower-priority applications. |
| bool IsMMCSSSupported(); |
| |
| // The MMDevice API lets clients discover the audio endpoint devices in the |
| // system and determine which devices are suitable for the application to use. |
| // Header file Mmdeviceapi.h defines the interfaces in the MMDevice API. |
| |
| // Number of active audio devices in the specified data flow direction. |
| // Set `data_flow` to eAll to retrieve the total number of active audio |
| // 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. |
| Microsoft::WRL::ComPtr<IMMDeviceEnumerator> CreateDeviceEnumerator(); |
| |
| // These functions return the unique device id of the default or |
| // communications input/output device, or an empty string if no such device |
| // exists or if the device has been disabled. |
| std::string GetDefaultInputDeviceID(); |
| std::string GetDefaultOutputDeviceID(); |
| std::string GetCommunicationsInputDeviceID(); |
| std::string GetCommunicationsOutputDeviceID(); |
| |
| // Creates an IMMDevice interface corresponding to the unique device id in |
| // `device_id`, or by data-flow direction and role if `device_id` is set to |
| // AudioDeviceName::kDefaultDeviceId. |
| Microsoft::WRL::ComPtr<IMMDevice> CreateDevice(absl::string_view device_id, |
| EDataFlow data_flow, |
| ERole role); |
| |
| // Returns the unique ID and user-friendly name of a given endpoint device. |
| // Example: "{0.0.1.00000000}.{8db6020f-18e3-4f25-b6f5-7726c9122574}", and |
| // "Microphone (Realtek High Definition Audio)". |
| webrtc::AudioDeviceName GetDeviceName(IMMDevice* device); |
| |
| // Gets the user-friendly name of the endpoint device which is represented |
| // by a unique id in `device_id`, or by data-flow direction and role if |
| // `device_id` is set to AudioDeviceName::kDefaultDeviceId. |
| std::string GetFriendlyName(absl::string_view device_id, |
| EDataFlow data_flow, |
| ERole role); |
| |
| // Query if the audio device is a rendering device or a capture device. |
| EDataFlow GetDataFlow(IMMDevice* device); |
| |
| // Enumerates all input devices and adds the names (friendly name and unique |
| // device id) to the list in `device_names`. |
| bool GetInputDeviceNames(webrtc::AudioDeviceNames* device_names); |
| |
| // Enumerates all output devices and adds the names (friendly name and unique |
| // device id) to the list in `device_names`. |
| bool GetOutputDeviceNames(webrtc::AudioDeviceNames* device_names); |
| |
| // The Windows Audio Session API (WASAPI) enables client applications to |
| // manage the flow of audio data between the application and an audio endpoint |
| // device. Header files Audioclient.h and Audiopolicy.h define the WASAPI |
| // interfaces. |
| |
| // Creates an IAudioSessionManager2 interface for the specified `device`. |
| // This interface provides access to e.g. the IAudioSessionEnumerator |
| Microsoft::WRL::ComPtr<IAudioSessionManager2> CreateSessionManager2( |
| IMMDevice* device); |
| |
| // Creates an IAudioSessionEnumerator interface for the specified `device`. |
| // The client can use the interface to enumerate audio sessions on the audio |
| // device |
| Microsoft::WRL::ComPtr<IAudioSessionEnumerator> CreateSessionEnumerator( |
| IMMDevice* device); |
| |
| // Number of active audio sessions for the given `device`. Expired or inactive |
| // sessions are not included. |
| int NumberOfActiveSessions(IMMDevice* device); |
| |
| // Creates an IAudioClient instance for a specific device or the default |
| // device specified by data-flow direction and role. |
| Microsoft::WRL::ComPtr<IAudioClient> CreateClient(absl::string_view device_id, |
| EDataFlow data_flow, |
| ERole role); |
| Microsoft::WRL::ComPtr<IAudioClient2> CreateClient2(absl::string_view device_id, |
| EDataFlow data_flow, |
| ERole role); |
| Microsoft::WRL::ComPtr<IAudioClient3> CreateClient3(absl::string_view device_id, |
| EDataFlow data_flow, |
| ERole role); |
| |
| // Sets the AudioCategory_Communications category. Should be called before |
| // 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 |
| // endpoint device, the Initialize method always accepts the stream format |
| // obtained by this method. |
| HRESULT GetSharedModeMixFormat(IAudioClient* client, |
| WAVEFORMATEXTENSIBLE* format); |
| |
| // Returns true if the specified `client` supports the format in `format` |
| // for the given `share_mode` (shared or exclusive). The client can call this |
| // method before calling IAudioClient::Initialize. |
| bool IsFormatSupported(IAudioClient* client, |
| AUDCLNT_SHAREMODE share_mode, |
| const WAVEFORMATEXTENSIBLE* format); |
| |
| // For a shared-mode stream, the audio engine periodically processes the |
| // data in the endpoint buffer at the period obtained in `device_period`. |
| // For an exclusive mode stream, `device_period` corresponds to the minimum |
| // time interval between successive processing by the endpoint device. |
| // This period plus the stream latency between the buffer and endpoint device |
| // represents the minimum possible latency that an audio application can |
| // achieve. The time in `device_period` is expressed in 100-nanosecond units. |
| HRESULT GetDevicePeriod(IAudioClient* client, |
| AUDCLNT_SHAREMODE share_mode, |
| REFERENCE_TIME* device_period); |
| |
| // 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 `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(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 |
| // stream between the client and the device. In shared mode, the 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. 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. |
| 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 |
| // manage the movement of data packets that contain audio-rendering data. |
| Microsoft::WRL::ComPtr<IAudioRenderClient> CreateRenderClient( |
| IAudioClient* client); |
| |
| // Creates an IAudioCaptureClient client for an existing IAudioClient given by |
| // `client`. The IAudioCaptureClient interface enables a client to read |
| // input data from a capture endpoint buffer. The methods in this interface |
| // manage the movement of data packets that contain capture data. |
| Microsoft::WRL::ComPtr<IAudioCaptureClient> CreateCaptureClient( |
| IAudioClient* client); |
| |
| // Creates an IAudioClock interface for an existing IAudioClient given by |
| // `client`. The IAudioClock interface enables a client to monitor a stream's |
| // data rate and the current position in the stream. |
| Microsoft::WRL::ComPtr<IAudioClock> CreateAudioClock(IAudioClient* client); |
| |
| // Creates an AudioSessionControl interface for an existing IAudioClient given |
| // by `client`. The IAudioControl interface enables a client to configure the |
| // control parameters for an audio session and to monitor events in the session. |
| Microsoft::WRL::ComPtr<IAudioSessionControl> CreateAudioSessionControl( |
| IAudioClient* client); |
| |
| // Creates an ISimpleAudioVolume interface for an existing IAudioClient given by |
| // `client`. This interface enables a client to control the master volume level |
| // of an active audio session. |
| Microsoft::WRL::ComPtr<ISimpleAudioVolume> CreateSimpleAudioVolume( |
| IAudioClient* client); |
| |
| // Fills up the endpoint rendering buffer with silence for an existing |
| // IAudioClient given by `client` and a corresponding IAudioRenderClient |
| // given by `render_client`. |
| bool FillRenderEndpointBufferWithSilence(IAudioClient* client, |
| IAudioRenderClient* render_client); |
| |
| // Prints/logs all fields of the format structure in `format`. |
| // Also supports extended versions (WAVEFORMATEXTENSIBLE). |
| std::string WaveFormatToString(WaveFormatWrapper format); |
| |
| // Converts Windows internal REFERENCE_TIME (100 nanosecond units) into |
| // 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); |
| |
| } // namespace core_audio_utility |
| } // namespace webrtc_win |
| } // namespace webrtc |
| |
| #endif // MODULES_AUDIO_DEVICE_WIN_CORE_AUDIO_UTILITY_WIN_H_ |