PModeLib contains a lot of sound-related functions in the SB16 (see Section A.13) and DMA (see Section A.14) modules. These functions provide all the pieces to make a program that plays sounds in the foreground or in the background. However, as one might expect, it takes a bit of work to actually get sounds playing.
Fortunately, there's some free sample code available in the testsb16.asm file in the examples directory in V:\ece291\pmodelib that plays a short sound in the foreground.
Depending on the needs of the program, and the size of the DMA buffer, chances are the program will want to play or record a sound longer than the length of the DMA buffer. This section will explain the necessary steps to do so.
What information do we have when playing a sound?
Note: Recording a sound is just like playing a sound, but reversed in both action and timing.
All the various settings of the DSP.
The location of the DMA buffer.
The exact location of the read point (via DMA_Todo()).
Looking at that, it should be enough. But DMA_Todo() is a (relatively) long operation, so using it is not really an option. What else do we have?
An Interrupt (or at least a callback), generated as often as we want.
Insight: we can use the ISR (or callback) to set variables so we know exactly where we are, without the overhead of DMA_Todo()!
The most obvious way to use this would be to generate the IRQ every time we finish loading the buffer's current contents. If we were to use a 4k buffer to play a 11k sound, and program the DMA and the DSP with a length of 4k, auto initialized, we should get something that looks like Figure 19-1.
Looks good, right? Whenever an interrupt is generated at the end of the buffer, we can just refill the buffer with more sound data. The DMA will then feed the new data to the DSP.
But there's a problem with this. The DMA and DSP just keep going, and don't need even as much as an "I heard you,"[1] so just because the program just ran the ISR and set a flag, and just noticed the flag you set back in the main loop, it doesn't mean the DMA hasn't read the first bit of the buffer all over again.
If it does read old information, chances are the sound output is going to get a pop, blip, or worst case a noticeable repeat, followed by a sudden switch to the new sound. It's somewhat like a record player, but instead of a long spiral, there's a series of concentric circles. If the needle isn't pushed into the next circle, the sound it plays will be incorrect. If it's pushed too late, the listener will recognize something old. If it's early, the listener will hear something new too soon. If it's not exactly perfect, the listener will hear a pop.
So let's give our DMA a "spiral groove."
Let's divide our DMA buffer in half. The DMA will still read the entire thing before it loops back to the beginning, but instead of generating an Interrupt at only the rightmost end, let's generate one in the middle too.
Our theoretical invocations would now still be auto initialized, but now we'll tell the DMA to use a length of 4k, and the DSP to Interrupt every 2k. For our same 11k sound, that should get us something that looks like Figure 19-2.
Looks good, right? Whenever an interrupt is generated, we can refill half of the buffer with more sound data. The DMA will continue feeding data from the other half, and then feed the new data to the DSP. When it reaches the new data, we'll get another interrupt and can refill the other half.
What's the downside? Only that we have to keep track of which half of the buffer we're in, and which is safe to fill. But sound won't be interrupted, as we've just managed to move the groove of our record over to align with the next section of sound; the needle won't need to jump.
Just be careful. Figure out how the end case works. When you tell the DSP to step down to single cycle from auto init, it will wait until the next time it generates an interrupt to do so. (This is a good thing!)
And as always, keep your ISR or callback simple. Increment a counter, set a flag, whatever. Don't mix four samples together for an entire half-buffer's worth. Don't read from a file.[2] All the standard rules.
| [1] |
This isn't quite true. If the program does not acknowledge the DSP, chances are it will stop accepting data. However this is likely to cause a much worse audio artifact than we are addressing here. |
| [2] |
Of the several "Do"s and "Don't"s here, this last one is the most important. In general DOS interrupts (which is what ReadFile() uses, for example) are not reentrant, which just means if they get interrupted in the middle (for example, by an interrupt) and called again (for example, by the ISR), they will provide inconsistent results, and generally cause your program to crash. The first three suggestions are really all the same as each other: keep your ISRs short. |