|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionI really like C#. It takes me about half as long to get the compiler to understand what I mean in C# as in C++. But because Managed DirectX is pretty new, it's not easy to find good examples of using it from C# and .NET managed code. In this second revision of my article, I pass on some of the things I've learned about playing sounds in C# code, both from my own experiments and helpful input from the good folks who read the first one and commented. Thanks to all of you! In particular, I've found out how to avoid an annoying bug that infests DirectSound's interaction with some sound-card drivers, that appears when streaming long sounds through a realistically-sized buffer, allocated with default properties. (I'll explain that somewhat-awkward clause later!) Here's my motivation: I'm working in C# on some new tools for transforming signals, especially sounds. As a part of that work, I needed to be able to read in lengthy sound files, modify them, write them out, and listen to them in the .NET environment. It seemed that would be easy, so I searched for some example code and documentation about working with WAVE files and playing them with the Managed DirectX ("MDX") DirectSound classes. I found that the vast majority of DirectX examples are written in unmanaged C++ (or even in C, working at the Win32 API level!). I found C# examples of reading WAVE files, but they sometimes broke on legitimate files, and didn't really support writing out your own. I found articles on playing short sounds from static sound buffers (more explanation below, and in the DirectX documentation). I found an interesting article, "Building a Drum Machine with DirectSound" by Ianier Munoz, that shows one approach to streaming sampled sounds in MDX. The DirectX Sample Browser that comes with the December 2005 SDK has one sample (helpfully named "CaptureSound Managed") that shows how to get sound into a streaming CaptureBuffer, which is a similar, but not identical, problem. Both these samples are useful, but I thought their approaches were unnecessarily complex for what I wanted to do -- stream long sound files through a MDX DirectSound buffer of reasonable size. So I wrote my own classes, staying within the managed boundaries of .NET. I think my approach is both reasonably efficient and reasonably easy to understand and use. Included in this project:
Background:WAVE sound filesThe generally-encountered standard for storing media files (including, in the Windows world, AVI and WAV) is the "Resource Interchange File Format" (RIFF). There are several more-or-less clear articles about RIFF available here at The Code Project, and on the web more generally. Two I found most useful are this Wikipedia article and this MSDN article. Although RIFF encompasses dozens of different kinds of resource files, the only
one MDX DirectSound understands is WAVE (.WAV or .wav). Furthermore,
MDX DirectSound only works with linear PCM (pulse code modulation) files, and, according
to the DirectX 9.0 SDK documentation for Just a few bits of jargon: In the real world, "PCM" means the (sound) signal was sampled at regular intervals in time, and that each sample is represented entirely. It does not specify how these instantaneous values are approximated ("quantized") and stored. In this project I use 16-bit integers, and the standard stereo arrangement. That means that every time sample has two 16-bit numbers: 4 bytes. The set of two channels' values in a single time sample is called a "frame". Two-channel, 16-bit linear PCM, sampled at 44100 Hz, is "standard" CD quality.
It's very common and perfectly adequate for this example. In real life, however,
floating-point samples are much easier to do math with. That's why I have my own
version of I first developed this project using Visual Studio 2003 and .NET 1.1, and it still relies on the December 2005 MDX DirectSound documentation. Please refer to that for background and clarification of the many things I don't cover. We may find better documentation (and more-complete MDX classes) in later updates -- I hope so! The value proposition for MDX is much faster development, with most of the performance of the unmanaged flavor. The downsides:
Oh, and there's no surplus of good sample code. In fact, I found that it's necessary to consult the documentation for unmanaged C++ programming to get answers to even intermediate-level questions about MDX programming. Despite all that, I'm glad to be working with MDX in C#. Really. The DirectSound Device classTo play sounds, the DirectSound classes need a Secondary BuffersAs a .NET programmer, you probably won't deal with feeding the The specification and operation of When you create a It wasn't so very long ago that mixing sound in real-time could significantly
burden the CPU. But these days, CPUs are so fast that letting them do the
arithmetic to mix a dozen extra channels into the main sound output is no problem
at all. And, as it turns out, merely specifying Unfortunately, if you let the framework decide, and your sound card supports
hardware buffers, it may well choose And as it turns out, that's still not quite the whole story. Even if I use a software secondary buffer, I still get one lone spurious notification, within the first segment of the buffer, on each of my systems. The simplest way around that is just to load only 3 of the 4 buffer sectors at any one time. My Notifications during playbackThe Specifically, the MSDN section on "Using Streaming Buffers" directs us as follows:
"When all data has been written to the buffer, set a notification position at the
last valid byte, or poll for this position. When the cursor reaches the end of the
data, call Never fear: there's a workaround for that, too. There is always some pipeline
delay between the executing code feeding the data to the As all mathematicians and programmers know, boundary conditions bite. So it is
with our project. In the CaptureSound sample, after the capture stops, the samples
remaining in the buffer get handled "manually" instead of with an event. That's
easy, because one knows where the end is, and can ignore the rest of the (stale)
data in the Windows is many things, but it's not a hard-real-time OS. So, in our players,
even though we can set the end-of-data event notification, we can't really guarantee
that we can call ErrorsYes, even perfect code (yeah, right) can experience errors. Some can be dealt
with gracefully, some not. I mentioned above that you can get an I hereby recommend and acknowledge using his technique in my code. Free! Console with every Windows AppThere's another bit of background you that may already know, but I find it so
useful in development that I want to point it out here. If you build a Windows Forms
app as a Console Application (set the output type property by right-clicking the
project in Solution Explorer), you'll see a background console as in my screenshot.
I write to the console in Debug mode using my A Tour of the CodeOK, enough background (maybe too much?). Here's how I approached these issues. Namespace StreamComputers.RiffFirst, in the namespace The /// <param name="fileName">name of RIFF WAVE file to work with.</param>
public RiffWaveReader(string fileName)
{
try
{
m_Stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
m_Reader = new BinaryReader(m_Stream);
ParseWaveInfo();
}
catch (RiffParserException)
{
Cleanup();
throw;
}
}
If the file is to its liking (even if the music isn't), the The (No, MP3 is not a PCM format. Please don't ask me about making or playing MP3s -- I still have ears. If you want more from me on audio quality, please consider my article, "It Ain't Just Rocket Science" in Positive Feedback online.) The Namespace StreamComputers.MdxDirectSoundThe
With the Free Bonus Console in the background, you can see fascinating facts about the sound device, WAV files, the buffers, and the progress of the streaming data transfer, if you choose that option. As with any GUI, the hard part is sequencing the user through operations that make sense, and defending against nonsensical inputs. And as with any GUI, this one isn't perfect -- but it's usable. When it first loads, the Surveyor finds the default sound device and dumps its Caps structure to the console. Note the number of free buffers and free hardware memory bytes before you create the secondary buffer. My Creative SB Audigy2 has 62 free "buffers" (mono mixing channels) and 0 free bytes when I'm not running another sound-enabled application. That's what I'd expect: Windows occupies 2 of the 64 maximum "buffers" (channels) for stereo system sounds, and there's no memory on-board. Now, select a sound file, and see if Next, select whether you want your sound mixed into the output by the sound-card hardware ("Locate (Mix) in Hardware"), or the CPU ("Locate (Mix) in Software"). Now you can select either a static or streaming buffer to play it. When you hit Create Buffer, the label control displays the size of the buffer (unless it won't fit, in which case a message box tells you to try something else). I chose a small enough buffer size for the streaming case that almost any system should have enough free memory to use it. If you haven't got 256K, God bless you. The the hardware device Caps should now reflect a smaller number of available "buffers" (channels) if you chose "Locate (Mix) in Hardware." Now take a look at the code. The abstract class The Back at the GUI, if you chose the static buffer radio button, the buffer length is the size of the entire data payload of the WAVE file. When you hit Play, you should hear it from your system's default sound device. If you don't, check your system's sound settings -- it's not my fault. You can pause, (un)pause, and let it play to the end, or hit Stop. All very normal. And Now the Fun BeginsIf you select a streaming buffer, and hit Create Buffer, you'll see a much smaller
buffer size shown. As I explain in the comments in the The constructor uses a public StreamingSoundBuffer(string fileName, Device dev, bool inHardware)
{
m_inHardware = inHardware;
// UI button should be disabled if no file
try
{
// Use a RiffWaveReader to access the WAVE file
m_RiffReader = new RiffWaveReader(fileName);
}
catch (RiffParserException)
{
if (m_RiffReader != null)
{
m_RiffReader.Dispose();
}
throw;
}
WaveFormat format = m_RiffReader.GetMDXWaveFormat();
DebugConsole.WriteLine(MdxInfo.WaveFormatAsString(format));
DebugConsole.WriteLine("WaveDataLength: {0,12} bytes",
m_RiffReader.DataLength);
// describe a SecondaryBuffer suitable for streaming,
//and very selfish focus
BufferDescription bdesc = new BufferDescription(format);
bdesc.BufferBytes = m_StreamBufferSize;
bdesc.ControlPositionNotify = true;
bdesc.CanGetCurrentPosition = true;
bdesc.ControlVolume = true;
bdesc.LocateInHardware = m_inHardware;
bdesc.LocateInSoftware = !m_inHardware;
bdesc.GlobalFocus = true;
bdesc.StickyFocus = true;
try
{
m_SecondaryBuffer = new SecondaryBuffer(bdesc, dev);
m_SecondaryBuffer.SetCurrentPosition(0);
m_secondaryBufferWritePosition = 0;
// ie not attenuated
m_SecondaryBuffer.Volume = 0;
DebugConsole.WriteLine(MdxInfo.BufferCapsAsString(
m_SecondaryBuffer.Caps));
// Create a notification Event object, to fire
//at each notify position
m_NotificationEvent = new AutoResetEvent(false);
// Preset as much of the EndNotificationPosition array as possible
//to avoid doing it in real-time.
m_EndNotificationPosition[0].EventNotifyHandle =
m_NotificationEvent.Handle;
m_predictedEndIndex = (int) (m_RiffReader.DataLength
% m_StreamBufferSize); //
To load the buffer with data, we need some more mechanisms. As I mentioned in
the background discussion, DirectSound offers a way to get the buffer to send us
events when it has played some of its data, and we can safely fill in new data over
the old. I use a single private BufferPositionNotify[] m_NotificationPositionArray
= new BufferPositionNotify[m_numberOfSectorsInBuffer];
private AutoResetEvent m_NotificationEvent;
private BufferPositionNotify[] m_EndNotificationPosition
= new BufferPositionNotify[1];
//...
private void SetFillNotifications(int numberOfSectors)
{
// Set up the fill-notification positions at last byte of each sector.
// All use the same event, in contrast to recipe in DX9.0 SDK Aug 2005
// titled "DirectSound Buffers | Using Streaming Buffers"
for (int i = 0; i < numberOfSectors; i++)
{
m_NotificationPositionArray[i].Offset = (i + 1) * m_SectorSize - 1;
DebugConsole.WriteLine("Fill Notification set at {0,12}",
m_NotificationPositionArray[i].Offset);
m_NotificationPositionArray[i].EventNotifyHandle =
m_NotificationEvent.Handle;
}
m_Notifier = new Notify(m_SecondaryBuffer);
// set the buffer to fire events at the notification positions
m_Notifier.SetNotificationPositions(m_NotificationPositionArray,
numberOfSectors);
}
But the GUI won't be happy being bugged every 370 ms. So private void CreateDataTransferThread()
{
// No thread should exist yet.
Debug.Assert(m_DataTransferThread == null,
"CreateDataTransferThread() saw thread non-null.");
m_AbortDataTransfer = false;
m_MoreWaveDataAvailable = (m_RiffReader.DataLength > m_dataBytesSoFar);
m_numSpuriousNotifies = 0;
m_numberOfDataSectorsTransferred = 0;
// Create a thread to monitor the notify events.
m_DataTransferThread = new Thread(new ThreadStart(DataTransferActivity));
m_DataTransferThread.Name = "DataTransferThread";
m_DataTransferThread.Priority = ThreadPriority.Highest;
m_DataTransferThread.Start();
// thread will wait for notification events
}
The thread will wait for an event, then dutifully execute its work function,
the cleverly named At this point, I'd like to shift your attention to the (The only reason to keep
First, it checks to see whether there's any more wave data available to transfer. If so, it checks to see whether it's been aborted (the user hit the Stop button or its SoundBuffer is being replaced by another). In that case, it returns immediately and the thread terminates. Otherwise, it waits for a notification event. When that happens, it has room to transfer another block to the buffer. private void DataTransferActivity()
{
int endWaveSector = 0;
while (m_MoreWaveDataAvailable)
{
if (m_AbortDataTransfer)
{
return;
}
//wait here for a notification event
m_NotificationEvent.WaitOne(Timeout.Infinite, true);
endWaveSector = m_secondaryBufferWritePosition / m_SectorSize;
m_MoreWaveDataAvailable = TransferBlockToSecondaryBuffer();
}
// Fill one more sector with silence, to avoid playing old data during
// the time between end-event-notification and SecondaryBuffer.Stop().
Array.Clear(m_transferBuffer, 0, m_transferBuffer.Length);
m_NotificationEvent.WaitOne(Timeout.Infinite, true);
int silentSector;
silentSector = m_secondaryBufferWritePosition / m_SectorSize;
WriteBlockToSecondaryBuffer();
// No more blocks to write: Remove fill-notify points,
//and mark end of data.
int dataEndInBuffer = m_dataBytesSoFar % m_StreamBufferSize;
SetEndNotification(dataEndInBuffer);
Debug.Assert(dataEndInBuffer == m_predictedEndIndex,
"Wave Data Stream end is not at predicted position.");
// end of data or the silent sector
bool notificationWithinEndSectors = false;
// Wait for play to reach the end
while (!notificationWithinEndSectors)
{
m_NotificationEvent.WaitOne(Timeout.Infinite, true);
int currentPlayPos, unused;
m_SecondaryBuffer.GetCurrentPosition(out currentPlayPos,out unused);
int currentPlaySector = currentPlayPos / m_SectorSize;
notificationWithinEndSectors = currentPlaySector == endWaveSector
| currentPlaySector == silentSector;
}
m_SecondaryBuffer.Stop();
m_State = BufferPlayState.Idle;
}
The We're almost done now. We could just set the end-notification event at
at the end of the sound data, wait for it, call The thread waits for play to reach the end, That pretty much covers the operation of the Its older brother, It took me a day or so to figure out what was going on when I started experimenting
with streaming buffers. There seems to be a bug in the way hardware-capable sound
card drivers interact with DirectSound. When the streaming buffer is "located in
hardware", that is, the sound card is in charge of accessing sound data from system
memory and mixing it to the output stream, the notification event occasionally fires
for no apparent reason. This happens unpredictably, but it seems to be correlated
with other activities on my computer -- opening an IE window, moving files around,
etc. Sometimes it seems to fire off just for fun. I can make it go crazy
with extra events just by streaming some MP3 sound in Windows Media Player at the
same time our application is running. If you examine the screenshot, from
(I think this is related to the fact that the sound card has only one interrupt to get the Windows OS's attention. Perhaps there's no reliable way to tell just which buffer (that is, channel or sound data stream) is in need of service. But I'd think some kind of vectoring or cause-identification would be implemented. If you know the details of sound-card device drivers and hardware, please let me know what's really going on!) Fortunately, the confusion doesn't happen when the CPU does the data access and
mixing. So the problem can be avoided by always selecting "LocateInSoftware"
in the (One more side note: because static buffers, used for short sounds, don't use
notification events, they don't have this problem. So, for instance, if you're
coding up a game with lots of short sound effects or loops, you can let the DirectX
framework decide for you to use the hardware to mix these buffers into the output
stream, saving some CPU cycles while still avoiding the spurious event problem.
That's what I do in the Well, the tour's over. Hope you liked it. On the other tentacle, if you think this is way too many words, just read the code. Points of Interest
History
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||