I started writing a gba emulator a few weeks ago, and at this point I decided that would be nice to add sound support to it. The emulator is being written is C with the SDL library, so I used SDL audio to play the sound stream. Atm I calculate the amount of samples I need to generate given the gba cpu frequency (16MHz) and my sampling rate, which is 32khz, which gives me 512 cycles per sample ((2 ^ 24) / 32768), so I basically generate a sample after each 512 cycles and write it to the ring buffer, at the position of my write cursor, and then increment the cursor. When SDL calls my callback function, I copy the data starting on the playback cursor position, the length of the data copied being the size of the SDL stream.
So the process is basically this:
- Write samples to the ring buffer periodically (put in on ring buffer, increment write cursor)
- When SDL calls the callback (which means that it needs more samples to play):
-- Copy *length* samples from ring buffer to the SDL stream, playback cursor += *length*
This method kind of works. Actually, it would work perfectly, if the amount of data being written and the amount of data being requested was the same, but unfortunately it isn't. What happens it writes a bit more of data than whats needed. Even through the difference is small, this makes the write cursor get more and more ahead of the playback cursor. After some time (usually a few seconds, depends on the size of the ring buffer) it overwrites data that wasn't even played yet, and that's when the desync occurs. At this point it starts making some weird echoing sounds (or just some weird noise if the buffer is very small) until it gets in "sync" again.
To aliviate the problem, I had a idea to automatically correct the playback cursor over time. The code to do this was really simple, after each copy (ring buffer -> sdl stream), I calculated the difference between the write cursor and the playback cursor (ideally, the playback cursor should be at the position of the write cursor at this point), and divided it by some amount (which was basically an acceptable margin of error), and added the result to the playback cursor. With this code in place, it could automatically correct itself over time, skipping a sample or two, and the loss wasn't even noticeable.
It worked pretty well, for some time, but the problem would still randomly show up. Doing things that could mess up the sync, like moving the window around or even minimizing stills makes the problem go back easily too. So this was no real solution.
So, the question is, how this is usually done? I already played in a bunch of emulators and never saw problems of this kind, so I'm probably doing something wrong here. Thanks in advance. Also sorry if this is the wrong place (I saw the emulation board, but it seems to be about NES emulation only, through this applies to any kind of emulator, hmm...)
So the process is basically this:
- Write samples to the ring buffer periodically (put in on ring buffer, increment write cursor)
- When SDL calls the callback (which means that it needs more samples to play):
-- Copy *length* samples from ring buffer to the SDL stream, playback cursor += *length*
This method kind of works. Actually, it would work perfectly, if the amount of data being written and the amount of data being requested was the same, but unfortunately it isn't. What happens it writes a bit more of data than whats needed. Even through the difference is small, this makes the write cursor get more and more ahead of the playback cursor. After some time (usually a few seconds, depends on the size of the ring buffer) it overwrites data that wasn't even played yet, and that's when the desync occurs. At this point it starts making some weird echoing sounds (or just some weird noise if the buffer is very small) until it gets in "sync" again.
To aliviate the problem, I had a idea to automatically correct the playback cursor over time. The code to do this was really simple, after each copy (ring buffer -> sdl stream), I calculated the difference between the write cursor and the playback cursor (ideally, the playback cursor should be at the position of the write cursor at this point), and divided it by some amount (which was basically an acceptable margin of error), and added the result to the playback cursor. With this code in place, it could automatically correct itself over time, skipping a sample or two, and the loss wasn't even noticeable.
It worked pretty well, for some time, but the problem would still randomly show up. Doing things that could mess up the sync, like moving the window around or even minimizing stills makes the problem go back easily too. So this was no real solution.
So, the question is, how this is usually done? I already played in a bunch of emulators and never saw problems of this kind, so I'm probably doing something wrong here. Thanks in advance. Also sorry if this is the wrong place (I saw the emulation board, but it seems to be about NES emulation only, through this applies to any kind of emulator, hmm...)