summaryrefslogtreecommitdiffstats
path: root/media
diff options
context:
space:
mode:
authorhenrika@chromium.org <henrika@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-01-11 10:02:55 +0000
committerhenrika@chromium.org <henrika@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-01-11 10:02:55 +0000
commit6e13b90a2485ecb04b4abf275ebfd9f0e1203572 (patch)
tree4c9c97b64fc37b3f89f0f359e865c98797971c2a /media
parentdf30b4664ab109e8259ed9e7a584245d8d54e38c (diff)
downloadchromium_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.cc244
-rw-r--r--media/audio/win/audio_low_latency_output_win.h106
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);
};