diff options
author | henrika@chromium.org <henrika@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-01-11 10:02:55 +0000 |
---|---|---|
committer | henrika@chromium.org <henrika@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-01-11 10:02:55 +0000 |
commit | 6e13b90a2485ecb04b4abf275ebfd9f0e1203572 (patch) | |
tree | 4c9c97b64fc37b3f89f0f359e865c98797971c2a /media | |
parent | df30b4664ab109e8259ed9e7a584245d8d54e38c (diff) | |
download | chromium_src-6e13b90a2485ecb04b4abf275ebfd9f0e1203572.zip chromium_src-6e13b90a2485ecb04b4abf275ebfd9f0e1203572.tar.gz chromium_src-6e13b90a2485ecb04b4abf275ebfd9f0e1203572.tar.bz2 |
Adds device notification to WASAPI rendering client.
Also adds support for stream switching where audio rendering will automatically switch from one default device to a new default device when the first on is disabled or removed.
See the header-file comments for more details comments.
BUG=none
TEST=media_unittests
Review URL: http://codereview.chromium.org/9008041
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@117195 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media')
-rw-r--r-- | media/audio/win/audio_low_latency_output_win.cc | 244 | ||||
-rw-r--r-- | media/audio/win/audio_low_latency_output_win.h | 106 |
2 files changed, 334 insertions, 16 deletions
diff --git a/media/audio/win/audio_low_latency_output_win.cc b/media/audio/win/audio_low_latency_output_win.cc index b036b94..994cbb6 100644 --- a/media/audio/win/audio_low_latency_output_win.cc +++ b/media/audio/win/audio_low_latency_output_win.cc @@ -4,6 +4,8 @@ #include "media/audio/win/audio_low_latency_output_win.h" +#include <Functiondiscoverykeys_devpkey.h> + #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/utf_string_conversions.h" @@ -23,6 +25,7 @@ WASAPIAudioOutputStream::WASAPIAudioOutputStream(AudioManagerWin* manager, render_thread_(NULL), opened_(false), started_(false), + restart_rendering_mode_(false), volume_(1.0), endpoint_buffer_size_frames_(0), device_role_(device_role), @@ -66,6 +69,10 @@ WASAPIAudioOutputStream::WASAPIAudioOutputStream(AudioManagerWin* manager, // Create the event which will be set in Stop() when capturing shall stop. stop_render_event_.Set(CreateEvent(NULL, FALSE, FALSE, NULL)); DCHECK(stop_render_event_.IsValid()); + + // Create the event which will be set when a stream switch shall take place. + stream_switch_event_.Set(CreateEvent(NULL, FALSE, FALSE, NULL)); + DCHECK(stream_switch_event_.IsValid()); } WASAPIAudioOutputStream::~WASAPIAudioOutputStream() {} @@ -75,8 +82,9 @@ bool WASAPIAudioOutputStream::Open() { if (opened_) return true; - // Obtain a reference to the IMMDevice interface of the default rendering - // device with the specified role. + // Create an IMMDeviceEnumerator interface and obtain a reference to + // the IMMDevice interface of the default rendering device with the + // specified role. HRESULT hr = SetRenderDevice(device_role_); if (FAILED(hr)) { return false; @@ -109,9 +117,13 @@ bool WASAPIAudioOutputStream::Open() { return false; } - opened_ = true; + // Register this client as an IMMNotificationClient implementation. + // Only OnDefaultDeviceChanged() and OnDeviceStateChanged() and are + // non-trivial. + hr = device_enumerator_->RegisterEndpointNotificationCallback(this); - return true; + opened_ = true; + return SUCCEEDED(hr); } void WASAPIAudioOutputStream::Start(AudioSourceCallback* callback) { @@ -124,6 +136,18 @@ void WASAPIAudioOutputStream::Start(AudioSourceCallback* callback) { if (started_) return; + if (restart_rendering_mode_) { + // The selected audio device has been removed or disabled and a new + // default device has been enabled instead. The current implementation + // does not to support this sequence of events. Given that Open() + // and Start() are usually called in one sequence; it should be a very + // rare event. + // TODO(henrika): it is possible to extend the functionality here. + LOG(ERROR) << "Unable to start since the selected default device has " + "changed since Open() was called."; + return; + } + source_ = callback; // Avoid start-up glitches by filling up the endpoint buffer with "silence" @@ -181,8 +205,10 @@ void WASAPIAudioOutputStream::Stop() { // Stop output audio streaming. HRESULT hr = audio_client_->Stop(); - DLOG_IF(ERROR, FAILED(hr)) << "Failed to stop output streaming: " - << std::hex << hr; + if (FAILED(hr)) { + DLOG_IF(ERROR, hr != AUDCLNT_E_NOT_INITIALIZED) + << "Failed to stop output streaming: " << std::hex << hr; + } // Wait until the thread completes and perform cleanup. if (render_thread_) { @@ -201,6 +227,14 @@ void WASAPIAudioOutputStream::Close() { // It is also valid to call Close() after Start() has been called. Stop(); + if (opened_ && device_enumerator_) { + // De-register the IMMNotificationClient callback interface. + HRESULT hr = device_enumerator_->UnregisterEndpointNotificationCallback( + this); + DLOG_IF(ERROR, FAILED(hr)) << "Failed to disable device notifications: " + << std::hex << hr; + } + // Inform the audio manager that we have been closed. This will cause our // destruction. manager_->ReleaseOutputStream(this); @@ -289,7 +323,9 @@ void WASAPIAudioOutputStream::Run() { bool playing = true; bool error = false; - HANDLE wait_array[2] = {stop_render_event_, audio_samples_render_event_}; + HANDLE wait_array[] = { stop_render_event_, + stream_switch_event_, + audio_samples_render_event_ }; UINT64 device_frequency = 0; // The IAudioClock interface enables us to monitor a stream's data @@ -307,10 +343,14 @@ void WASAPIAudioOutputStream::Run() { PLOG_IF(ERROR, error) << "Failed to acquire IAudioClock interface: " << std::hex << hr; - // Render audio until stop event or error. + // Keep rendering audio until the stop event or the stream-switch event + // is signaled. An error event can also break the main thread loop. while (playing && !error) { - // Wait for a close-down event or a new render event. - DWORD wait_result = WaitForMultipleObjects(2, wait_array, FALSE, INFINITE); + // Wait for a close-down event, stream-switch event or a new render event. + DWORD wait_result = WaitForMultipleObjects(arraysize(wait_array), + wait_array, + FALSE, + INFINITE); switch (wait_result) { case WAIT_OBJECT_0 + 0: @@ -318,6 +358,15 @@ void WASAPIAudioOutputStream::Run() { playing = false; break; case WAIT_OBJECT_0 + 1: + // |stream_switch_event_| has been set. Stop rendering and try to + // re-start the session using a new endpoint device. + if (!RestartRenderingUsingNewDefaultDevice()) { + // Abort the thread since stream switching failed. + playing = false; + error = true; + } + break; + case WAIT_OBJECT_0 + 2: { // |audio_samples_render_event_| has been set. UINT32 num_queued_frames = 0; @@ -441,19 +490,20 @@ void WASAPIAudioOutputStream::HandleError(HRESULT err) { } HRESULT WASAPIAudioOutputStream::SetRenderDevice(ERole device_role) { - ScopedComPtr<IMMDeviceEnumerator> enumerator; + // Create the IMMDeviceEnumerator interface. HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), - enumerator.ReceiveVoid()); + device_enumerator_.ReceiveVoid()); if (SUCCEEDED(hr)) { // Retrieve the default render audio endpoint for the specified role. // Note that, in Windows Vista, the MMDevice API supports device roles // but the system-supplied user interface programs do not. - hr = enumerator->GetDefaultAudioEndpoint(eRender, - device_role, - endpoint_device_.Receive()); + hr = device_enumerator_->GetDefaultAudioEndpoint( + eRender, device_role, endpoint_device_.Receive()); + if (FAILED(hr)) + return hr; // Verify that the audio endpoint device is active. That is, the audio // adapter that connects to the endpoint device is present and enabled. @@ -592,3 +642,167 @@ HRESULT WASAPIAudioOutputStream::InitializeAudioEngine() { audio_render_client_.ReceiveVoid()); return hr; } + +ULONG WASAPIAudioOutputStream::AddRef() { + NOTREACHED() << "IMMNotificationClient should not use this method."; + return 1; +} + +ULONG WASAPIAudioOutputStream::Release() { + NOTREACHED() << "IMMNotificationClient should not use this method."; + return 1; +} + +HRESULT WASAPIAudioOutputStream::QueryInterface(REFIID iid, void** object) { + NOTREACHED() << "IMMNotificationClient should not use this method."; + if (iid == IID_IUnknown || iid == __uuidof(IMMNotificationClient)) { + *object = static_cast < IMMNotificationClient*>(this); + } else { + return E_NOINTERFACE; + } + return S_OK; +} + +STDMETHODIMP WASAPIAudioOutputStream::OnDeviceStateChanged(LPCWSTR device_id, + DWORD new_state) { +#ifndef NDEBUG + std::string device_name = GetDeviceName(device_id); + std::string device_state; + + switch (new_state) { + case DEVICE_STATE_ACTIVE: + device_state = "ACTIVE"; + break; + case DEVICE_STATE_DISABLED: + device_state = "DISABLED"; + break; + case DEVICE_STATE_NOTPRESENT: + device_state = "NOTPRESENT"; + break; + case DEVICE_STATE_UNPLUGGED: + device_state = "UNPLUGGED"; + break; + default: + break; + } + + DVLOG(1) << "-> State changed to " << device_state + << " for device: " << device_name; +#endif + return S_OK; +} + +HRESULT WASAPIAudioOutputStream::OnDefaultDeviceChanged(EDataFlow flow, + ERole role, LPCWSTR new_default_device_id) { + if (new_default_device_id == NULL) { + // The user has removed or disabled the default device for our + // particular role, and no other device is available to take that role. + DLOG(ERROR) << "All devices are disabled."; + return E_FAIL; + } + + if (flow == eRender && role == device_role_) { + // Log the name of the new default device for our configured role. + std::string new_default_device = GetDeviceName(new_default_device_id); + DVLOG(1) << "-> New default device: " << new_default_device; + + // Initiate a stream switch if not already initiated by signaling the + // stream-switch event to inform the render thread that it is OK to + // re-initialize the active audio renderer. All the action takes place + // on the WASAPI render thread. + if (!restart_rendering_mode_) { + restart_rendering_mode_ = true; + SetEvent(stream_switch_event_.Get()); + } + } + + return S_OK; +} + +std::string WASAPIAudioOutputStream::GetDeviceName(LPCWSTR device_id) const { + std::string name; + ScopedComPtr<IMMDevice> audio_device; + + // Get the IMMDevice interface corresponding to the given endpoint ID string. + HRESULT hr = device_enumerator_->GetDevice(device_id, audio_device.Receive()); + if (SUCCEEDED(hr)) { + // Retrieve user-friendly name of endpoint device. + // Example: "Speakers (Realtek High Definition Audio)". + ScopedComPtr<IPropertyStore> properties; + hr = audio_device->OpenPropertyStore(STGM_READ, properties.Receive()); + if (SUCCEEDED(hr)) { + PROPVARIANT friendly_name; + PropVariantInit(&friendly_name); + hr = properties->GetValue(PKEY_Device_FriendlyName, &friendly_name); + if (SUCCEEDED(hr) && friendly_name.vt == VT_LPWSTR) { + if (friendly_name.pwszVal) + name = WideToUTF8(friendly_name.pwszVal); + } + PropVariantClear(&friendly_name); + } + } + return name; +} + +bool WASAPIAudioOutputStream::RestartRenderingUsingNewDefaultDevice() { + DCHECK(base::PlatformThread::CurrentId() == render_thread_->tid()); + DCHECK(restart_rendering_mode_); + + // The |restart_rendering_mode_| event has been signaled which means that + // we must stop the current renderer and start a new render session using + // the new default device with the configured role. + + // Stop the current rendering. + HRESULT hr = audio_client_->Stop(); + if (FAILED(hr)) { + restart_rendering_mode_ = false; + return false; + } + + // Release acquired interfaces (IAudioRenderClient, IAudioClient, IMMDevice). + audio_render_client_.Release(); + audio_client_.Release(); + endpoint_device_.Release(); + + // Retrieve the new default render audio endpoint (IMMDevice) for the + // specified role. + hr = device_enumerator_->GetDefaultAudioEndpoint( + eRender, device_role_, endpoint_device_.Receive()); + if (FAILED(hr)) { + restart_rendering_mode_ = false; + return false; + } + + // Re-create an IAudioClient interface. + hr = ActivateRenderDevice(); + if (FAILED(hr)) { + restart_rendering_mode_ = false; + return false; + } + + // Retrieve the new mix format and ensure that it is supported given + // the predefined format set at construction. + base::win::ScopedCoMem<WAVEFORMATEX> new_audio_engine_mix_format; + hr = audio_client_->GetMixFormat(&new_audio_engine_mix_format); + if (FAILED(hr) || !DesiredFormatIsSupported()) { + restart_rendering_mode_ = false; + return false; + } + + // Re-initialize the audio engine using the new audio endpoint. + // This method will create a new IAudioRenderClient interface. + hr = InitializeAudioEngine(); + if (FAILED(hr)) { + restart_rendering_mode_ = false; + return false; + } + + // All released interfaces (IAudioRenderClient, IAudioClient, IMMDevice) + // are now re-initiated and it is now possible to re-start audio rendering. + + // Start rendering again using the new default audio endpoint. + hr = audio_client_->Start(); + + restart_rendering_mode_ = false; + return SUCCEEDED(hr); +} diff --git a/media/audio/win/audio_low_latency_output_win.h b/media/audio/win/audio_low_latency_output_win.h index 72fb585..cc844a0 100644 --- a/media/audio/win/audio_low_latency_output_win.h +++ b/media/audio/win/audio_low_latency_output_win.h @@ -32,6 +32,29 @@ // o Endpoint buffer (~20 ms to ensure glitch-free rendering). // - Note that, if the user selects a packet size of e.g. 100 ms, the total // delay will be approximately 115 ms (10 + 5 + 100). +// - Supports device events using the IMMNotificationClient Interface. If +// streaming has started, a so-called stream switch will take place in the +// following situations: +// o The user enables or disables an audio endpoint device from Device +// Manager or from the Windows multimedia control panel, Mmsys.cpl. +// o The user adds an audio adapter to the system or removes an audio +// adapter from the system. +// o The user plugs an audio endpoint device into an audio jack with +// jack-presence detection, or removes an audio endpoint device from +// such a jack. +// o The user changes the device role that is assigned to a device. +// o The value of a property of a device changes. +// Practical/typical example: A user has two audio devices A and B where +// A is a built-in device configured as Default Communication and B is a +// USB device set as Default device. Audio rendering starts and audio is +// played through the device B since the eConsole role is used by the audio +// manager in Chrome today. If the user now removes the USB device (B), it +// will be detected and device A will instead be defined as the new default +// device. Rendering will automatically stop, all resources will be released +// and a new session will be initialized and started using device A instead. +// The net effect for the user is that audio will automatically switch from +// device B to device A. Same thing will happen if the user now re-inserts +// the USB device again. // // Implementation notes: // @@ -43,6 +66,12 @@ // input device and then use the same rate when creating this object. Use // WASAPIAudioOutputStream::HardwareSampleRate() to retrieve the sample rate. // - Calling Close() also leads to self destruction. +// - Stream switching is not supported if the user shifts the audio device +// after Open() is called but before Start() has been called. +// - Stream switching can fail if streaming starts on one device with a +// supported format (X) and the new default device - to which we would like +// to switch - uses another format (Y), which is not supported given the +// configured audio parameters. // // Core Audio API details: // @@ -66,13 +95,32 @@ // - Audio-rendering endpoint devices can have three roles: // Console (eConsole), Communications (eCommunications), and Multimedia // (eMultimedia). Search for "Device Roles" on MSDN for more details. +// - The actual stream-switch is executed on the audio-render thread but it +// is triggered by an internal MMDevice thread using callback methods +// in the IMMNotificationClient interface. +// +// Threading details: +// +// - It is assumed that this class is created on the audio thread owned +// by the AudioManager. +// - It is a requirement to call the following methods on the same audio +// thread: Open(), Start(), Stop(), and Close(). +// - Audio rendering is performed on the audio render thread, owned by this +// class, and the AudioSourceCallback::OnMoreData() method will be called +// from this thread. Stream switching also takes place on the audio-render +// thread. +// - All callback methods from the IMMNotificationClient interface will be +// called on a Windows-internal MMDevice thread. // #ifndef MEDIA_AUDIO_WIN_AUDIO_LOW_LATENCY_OUTPUT_WIN_H_ #define MEDIA_AUDIO_WIN_AUDIO_LOW_LATENCY_OUTPUT_WIN_H_ #include <Audioclient.h> +#include <audiopolicy.h> #include <MMDeviceAPI.h> +#include <string> + #include "base/compiler_specific.h" #include "base/threading/platform_thread.h" #include "base/threading/simple_thread.h" @@ -87,8 +135,11 @@ class AudioManagerWin; // AudioOutputStream implementation using Windows Core Audio APIs. +// The IMMNotificationClient interface enables device event notifications +// related to changes in the status of an audio endpoint device. class MEDIA_EXPORT WASAPIAudioOutputStream - : public AudioOutputStream, + : public IMMNotificationClient, + public AudioOutputStream, public base::DelegateSimpleThread::Delegate { public: // The ctor takes all the usual parameters, plus |manager| which is the @@ -115,6 +166,37 @@ class MEDIA_EXPORT WASAPIAudioOutputStream bool started() const { return started_; } private: + // Implementation of IUnknown (trivial in this case). See + // msdn.microsoft.com/en-us/library/windows/desktop/dd371403(v=vs.85).aspx + // for details regarding why proper implementations of AddRef(), Release() + // and QueryInterface() are not needed here. + STDMETHOD_(ULONG, AddRef)(); + STDMETHOD_(ULONG, Release)(); + STDMETHOD(QueryInterface)(REFIID iid, void** object); + + // Implementation of the abstract interface IMMNotificationClient. + // Provides notifications when an audio endpoint device is added or removed, + // when the state or properties of a device change, or when there is a + // change in the default role assigned to a device. See + // msdn.microsoft.com/en-us/library/windows/desktop/dd371417(v=vs.85).aspx + // for more details about the IMMNotificationClient interface. + + // The default audio endpoint device for a particular role has changed. + // This method is only used for diagnostic purposes. + STDMETHOD(OnDeviceStateChanged)(LPCWSTR device_id, DWORD new_state); + + // Indicates that the state of an audio endpoint device has changed. + STDMETHOD(OnDefaultDeviceChanged)(EDataFlow flow, ERole role, + LPCWSTR new_default_device_id); + + // These IMMNotificationClient methods are currently not utilized. + STDMETHOD(OnDeviceAdded)(LPCWSTR device_id) { return S_OK; } + STDMETHOD(OnDeviceRemoved)(LPCWSTR device_id) { return S_OK; } + STDMETHOD(OnPropertyValueChanged)(LPCWSTR device_id, + const PROPERTYKEY key) { + return S_OK; + } + // DelegateSimpleThread::Delegate implementation. virtual void Run() OVERRIDE; @@ -128,6 +210,16 @@ class MEDIA_EXPORT WASAPIAudioOutputStream bool DesiredFormatIsSupported(); HRESULT InitializeAudioEngine(); + // Converts unique endpoint ID to user-friendly device name. + std::string GetDeviceName(LPCWSTR device_id) const; + + // Called on the audio render thread when the current audio stream must + // be re-initialized because the default audio device has changed. This + // method: stops the current renderer, releases and re-creates all WASAPI + // interfaces, creates a new IMMDevice and re-starts rendering using the + // new default audio device. + bool RestartRenderingUsingNewDefaultDevice(); + // Initializes the COM library for use by the calling thread and sets the // thread's concurrency model to multi-threaded. base::win::ScopedCOMInitializer com_init_; @@ -153,6 +245,12 @@ class MEDIA_EXPORT WASAPIAudioOutputStream bool opened_; bool started_; + // Set to true as soon as a new default device is detected, and cleared when + // the streaming has switched from using the old device to the new device. + // All additional device detections during an active state are ignored to + // ensure that the ongoing switch can finalize without disruptions. + bool restart_rendering_mode_; + // Volume level from 0 to 1. float volume_; @@ -182,6 +280,9 @@ class MEDIA_EXPORT WASAPIAudioOutputStream // Pointer to the client that will deliver audio samples to be played out. AudioSourceCallback* source_; + // An IMMDeviceEnumerator interface which represents a device enumerator. + base::win::ScopedComPtr<IMMDeviceEnumerator> device_enumerator_; + // An IMMDevice interface which represents an audio endpoint device. base::win::ScopedComPtr<IMMDevice> endpoint_device_; @@ -200,6 +301,9 @@ class MEDIA_EXPORT WASAPIAudioOutputStream // This event will be signaled when rendering shall stop. base::win::ScopedHandle stop_render_event_; + // This event will be signaled when stream switching shall take place. + base::win::ScopedHandle stream_switch_event_; + DISALLOW_COPY_AND_ASSIGN(WASAPIAudioOutputStream); }; |