#97884 - TheChuckster - Sat Aug 12, 2006 3:28 am
Many people here are writing emulators and other apps that require the DS to seamlessly stream audio from the ARM9 to the ARM7. StellaDS and the newly written 303 code for my DS Drum Machine both still need seamless streaming audio code. The DS isn't really designed to do that because there is no interrupt for requesting additional audio data, so I have to use a ring buffer along with some clever FIFO and shared memory communications. I feel that if I figure this out and release it publicly, a lot of people would benefit from being able to add it to their emulators as well, because it is a standing problem in DS development. After all, not even libnds has streaming audio capabilities.
I have written code based off of DekuTree's psuedocode at http://forum.gbadev.org/viewtopic.php?p=97432#97432. My code right now is a simple sine wave oscillator going through the ring buffer code. You can ideally substitute the mixing function with any SDL callback from your ported projects or any function in general that generates a given number of audio samples from the ARM9. Highly useful stuff. The code is in a ZIP archive on my server at http://thechuckster.homelinux.com/~chuck/ringbuffer.zip -- it should compile nicely and be easy to read and understand.
I would like to say that it works, but that's not the case. I'm having problems because I do not quite understand NDS timers. Thus, I'm getting a bunch of clicks and pops with my ring buffer, the very thing that the ring buffer is designed to eliminate. You can see just by looking at the debug output that it isn't working. I (along with any other emulator and game port authors -- especially GPF) would appreciate if someone who has done streaming audio such as Gladius or Deku Tree could glance over the code and see what is wrong or even suggest an alternative method to the ring buffer that would be easier to implement.
#97889 - DekuTree64 - Sat Aug 12, 2006 4:34 am
Code: |
void DoRingBuffer (void)
{
int timerValue = TIMER0_DATA;
... |
That should be TIMER1_DATA. Each time timer0 overflows, that means the sound channel has played one sample. So timer1 contains the number of samples that have been played, and therefore need to be replaced.
Everything looks good otherwise. I think this bit of code has been needed for quite some time :)
_________________
___________
The best optimization is to do nothing at all.
Therefore a fully optimized program doesn't exist.
-Deku
#97891 - gladius - Sat Aug 12, 2006 4:37 am
Using a repeating buffer is a bit tricky because you can't tell if you ever get out of sync, there is no hardware sound cursor. That's why I preferred to start the sound on a different channel every time it was going to be played.
Doing the mixing on vblank is also a bit questionable as you then need a much larger mix buffer, and hence more latency. But that's okay.
If you set: TIMER1_DATA = 0x10000 - (MIXBUFSIZE / 2), then you will get an interrupt on timer 1 every MIXBUFSIZE / 2 samples. Then you can update the part of the buffer that just finished playing.
Your TimerHandler code is almost right, just change the soundCursor to be (MIXBUFSIZE / 2) - SPC_IPC->soundCursor, and use that instead of the vblank method and you should be in business. If you still get crackling, try starting a new sample for every chunk you play back.
Btw, I like the names, SPC for the win :).
#97895 - DekuTree64 - Sat Aug 12, 2006 6:15 am
If you make the buffer size a factor of timer1's period (normally just a power of two), then it's impossible for it to go out of sync. Setting timer1's period to MIXBUFSIZE / 2 has the same effect though. But I prefer having the exact sample count when doing the mixing on VBlank, because then there's only a few samples difference between any two frames.
The reason I prefer to do mixing on VBlank instead of a timer interrupt is to keep the per-frame timing consistent, rather than having the mixer come along at its own interval and block things up for a long time. That, and if you don't have an interrupt handler that can do nested interrupts, it will destroy HBlank effects (although the top of the screen will still be messed up if your mixing runs past VBlank, but that's a bit less disruptive than being in a different place every frame).
Anyway, I noticed a couple more problems i the code. First, this:
Code: |
TIMER1_DATA = 0x10000 - 2;
TIMER1_CR = TIMER_CASCADE | TIMER_IRQ_REQ | TIMER_ENABLE; |
TIMER1_DATA should start at 0. The way it is now, it will just be toggling between 0xfffe and 0xffff. Also, no need to set the interrupt bit, since all you're doing with it is reading out the data periodically.
Second, you should clear bit0 of the value you write to TIMER0_DATA, because sound channel timers run at half the frequency of CPU timers, so if that bottom bit is set on the CPU timer, it will cause a veeeery gradual drift out of sync from the channel.
_________________
___________
The best optimization is to do nothing at all.
Therefore a fully optimized program doesn't exist.
-Deku
#97961 - TheChuckster - Sat Aug 12, 2006 4:00 pm
I tried implementing Deku's method, but it's still clicking and popping badly. I think the timer is catching up on the DS audio output. It's impossible to tell without an audio interrupt. Then I tried Gladius's and I got the same results. I uploaded both attempts at http://thechuckster.homelinux.com/~chuck/ringbuffer_deku.zip and http://thechuckster.homelinux.com/~chuck/ringbuffer_gladius.zip -- if we can get one working, that would be great.
#97969 - agentq - Sat Aug 12, 2006 4:53 pm
It looks like gbatek validated something I suspected when I wrote the streaming code in ScummVM. Starting the sound and the timer at the same time won't work, as the sound seeems to start late.
Quote: |
Sound delayed Start/Restart
A sound will be started/restarted when changing its start bit from 0 to 1, however, the sound won't start immediately: PSG/Noise starts after 1 sample, PCM starts after 3 samples, and ADPCM starts after 11 samples (3 dummy samples as for PCM, plus 8 dummy samples for the ADPCM header).
|
This would cause popping if you're restarting the sound when the timer triggers, as the sound wouldn't have been completely played. You can take a look at the sound code in ScummVM, which can stream audio from disk without any popping, but it's a complete mess as I nearly ripped out all my remaining hair in frustration writing it.
#97977 - TheChuckster - Sat Aug 12, 2006 5:27 pm
The thing is... I don't restart the sound. I just loop one sample. I added a 100 ms delay anyways but it still pops.
#98018 - gladius - Sat Aug 12, 2006 7:55 pm
Well, a huge bug in the one I can see is "SPC_IPC->soundCursor = MIXBUFSIZE - SPC_IPC->soundCursor;". That should be "SPC_IPC->soundCursor = (MIXBUFSIZE / 2) - SPC_IPC->soundCursor;". Otherwise you are mixing sound data past the end of the buffer.
Also, you should start soundCursor at MIXBUFSIZE / 2 instead of 0. When the first interrupt happens, you have already played the first half of the mix buffer, so you want to mix into that part of it, not the second half as you will currently.
#98212 - TheChuckster - Mon Aug 14, 2006 3:16 am
Yeah, I tried those changes. No matter what I do, it still crackles.. I started off with the PocketSPC 0.1 source, as you can probably tell, but it crackled even with that.
#98225 - gladius - Mon Aug 14, 2006 6:57 am
Yes, things did look familar :). So you hear any popping in pocketspc when you compile it? Have you tried listening to the built version?
I'd try simplifying things by sending the mix command on the timer irq and have the arm9 mix MIXBUFSIZE / 2 samples, instead of some variable amount. Then go back to the vblank method if you get that working.