I've been Googling/StackOverflowing for days and can't figure this out.
I have a WPF project where I want to play videos on multiple screens simultaneously with separate audio systems. For playing video, we're using WPFMediaKit, which relies on DirectShow Lib .NET. For other systems, we'd like to use something a little more modern, such as the Core Audio API via NAudio. We control the environment and only have to support Windows 7 (may migrate to 8 in the future).
WPFMediaKit's MediaUriElement
allows you to set its AudioRenderer
but it exhibits issues because of the 31 character limit imposed by... DirectSound? I'm not entirely sure where the limitation comes from but it's a problem because all of our audio devices have very similar names (which most likely won't be unique within the space of 31 characters). So, the plan is to modify the source and allow it to take in a GUID representing a DirectSound device and change its behavior to look it up by that. The code that'll present the audio device endpoint list to the user will rely on NAudio's MMDeviceEnumerator
from the Core Audio API.
I've gotten this far:
private static void DemonstrateTheIssue() {
SpeechSynthesizer speech = new SpeechSynthesizer();
MemoryStream stream = new MemoryStream();
SpeechAudioFormatInfo synthFormat = new SpeechAudioFormatInfo(EncodingFormat.Pcm, 88200, 16, 1, 16000, 2, null);
speech.SetOutputToAudioStream(stream, synthFormat);
speech.Speak("This is a test. This is only a test.");
stream.Position = 0;
RawSourceWaveStream reader = new RawSourceWaveStream(stream, new WaveFormat());
MMDeviceEnumerator coreAudioDeviceEnumerator = new MMDeviceEnumerator();
MMDeviceCollection coreAudioAudioEndPoints = coreAudioDeviceEnumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active);
MMDevice coreAudioSpdif = coreAudioAudioEndPoints.First(ep => ep.FriendlyName.Contains("S/PDIF")); // just an example. In the real application, would be selected by user and the GUID would be stored instead of the FriendlyName
string spdifGuidValue = coreAudioSpdif.Properties[PropertyKeys.PKEY_AudioEndpoint_GUID].Value as string;
Guid spdifGuid = Guid.Parse(spdifGuidValue);
DirectSoundOut directSoundSpdif = new DirectSoundOut(spdifGuid);
directSoundSpdif.Init(reader);
directSoundSpdif.Play();
}
This code works. When I inspect NAudio's source of DirectSoundOut
, I can see it calls DirectSoundOut.DirectSoundCreate
from dsound.dll
, which initializes an IDirectSound
instance. I'm not entirely sure what I can do with that instance. So, going back to the other side of this puzzle...
Let's look at what WPFMediaKit does when I give it an AudioRenderer
of "Digital Audio (S/PDIF) (High De"
. Digging through the source, I eventually came across AddFilterByName(IGraphBuilder graphBuilder, Guid deviceCategory, string friendlyName)
which eventually calls AddFilterByDevice(IGraphBuilder graphBuilder, DsDevice device)
. Okay, so this uses its own wrapper class, DsDevice
... which wraps IMoniker
and a few other things, including some properties from an IPropertyBag
attached to the IMoniker
instance. The last line of C# code I see before my trail vanishes into COM calls this: filterGraph.AddSourceFilterForMoniker(device.Mon, null, device.Name, out filter)
. filterGraph
is an IFilterGraph2
instance and device.Name
returns "FriendlyName"
from the moniker's property bag.
I feel like I'm close but I just can't connect the dots. How can I take an IDirectSound
instance and tell the filterGraph
to play audio on it? Do I need to get a moniker for the IDirectSound
? If so, how? Is there another method I'm unaware of? Or... can I create/compose/get a reference to my own filter and tell the filterGraph
about it? If so, how would I go about doing that?
Please answer this question like I'm a child -- this stuff is really over my head.
DirectSound and DirectShow are two different APIs. There is no direct adding of DirectSound objects into DirectShow filter graphs and hence your inability to do impossible.
There are two ways to address the problem: you either insist that you already hold an
IDirectSound
interface pointer and you want to use it within DirectShow. In this case you have to implement a custom audio renderer filter, which will be backed by this DirectSound interface and your renderer will forward all audio to the device of your interest. You don't really want to go this way. In particular, you won't be able to implement this in C# only.And the second way is to find an existing DirectShow audio renderer, which is using the device of your interest. Then use it and it will play through the same DirectSound device, just not exactly the pointer you had on your hands in first place.
More details on the second approach. DirectShow has a special category where all audio renderers are listed:
CLSID_AudioRendererCategory
. Enumerating filters there, you enumerate instances ofDirectSound Renderer Filter
created for every existing device.You can find the right device there, and you then you can use this filter as a regular audio renderer (that is, you add it to your graph, you connect it there). The filter does not however expose
IDirectSound
pointer it is using internally.