Streaming PCM Examples?


Established Member
I'm struggling to finish my latest project. I ported minimodem to the Saturn in an attempt to be able to retrieve save games over audio (to assist with fan translation projects). Here's a demo video video if you are interested. I have a few major issues:

1) I don't know how to stream PCM audio. I'm dynamically creating PCM audio, waiting until I have enough audio data, then playing it with slPCMOn(), waiting for it to finish playing, then repeating. While this works this completely kills performance (as seen in the demo video).
2) The transmission mechanism isn't reliable enough. I've tried lowering the bitrate, adding stop bits, etc but I can't improve the reliability. Sometimes I have corrupted bytes sent. Sometimes I have bytes missing and sometimes I have extraneous data. I'm hoping by streaming the data continuously it would be more reliable.
3) I've tried @Ponut 's 68k driver. It's not clear to me how to stream on that as well. There might be a way by using alt-looping sounds.

Any advice on how to make my project usable? I have been able to successfully retrieve ~300 byte save games from real hardware (connecting the Saturn's stereo audio -> 3.55 mm line in on my laptop). Larger files are too often corrupted. Thanks in advance.
Last edited:
Double-buffer the data in a looping voice, use the CA register or a SCSP timer to determine when it's time to refill the buffer. I believe playing back at a multiple of 44.1kHz (44.1, 22.05, etc.) would avoid interpolation, but that needs to be verified by measuring.
Thanks for the fast reply. I'm transmitting at 441.kHz @ 1200 bits.

What do you mean by double-buffering the data? Do you mean to transmit every byte twice? I'll look into the CA register.
It's the same double-buffering as when eg. drawing graphics. There's a "front buffer" that's being played, and a "back buffer" that's refilled. In this case they're just two halves of an audio loop.
I'm using Jo Engine which is calling slPCMOn(). I'll try to figure out what slPCMOn() does under the hood.
slPCMOn is double-buffered from work RAM to sound RAM.
This will always have delays in it because slPCMOn does not force the transfers (and thus, the bitrate) to be aligned with vblank. It does that so you can play any bitrate.

To set up a PCM stream with my driver, you have to manually schedule the transfers to the front buffer according to the playback time __precisely__ on a vblank period. There is no fixed "front buffer", in that case I am specifically referring to the sound RAM address that a PCM slot is working from.

Alternatively, using my driver, you place the entire sound data in sound RAM and schedule cascading playback over the entire chunk of data over multiple PCM slots.

e.g. you set up 7 PCM slots, each 64KB from each other, the second to play exactly after the first one is over, the third after the second, et cetera.
Again, has to be time __exactly__ over a vblank period (so your bytes per blank must be an even divisor of your buffer length -- 256, 512, 1024, 2048, 4096,). But this'll only work for 448KB of data.
Using sync bytes, start/stop bits, and base64 encoding my buffer I was able to transmit a 20k save (~ 28k b64 encoded). I had a single bit flip which I was able to detect and manually correct. My throughput was around 8 bytes a second.

I'm considering dropping the base64 encoding and replacing it with an error correcting code like Reed Solomon. Will have to do some more research.
Last edited:
Swapping out base64 for Reed Solomon encoding worked really well. In fact it's probably overkill. I'm still outputting at ~8 bytes a second but now in the rare cases I have bit flips RS can recover for me. Also RS is ~14% overhead versus 33% for base64 encoding.

At 8 bytes a second you can dump the internal backup RAM (32k) in about one hour. I feel this is a reasonable speed for a first release. I plan to cleanup the code and hopefully have a beta out this weekend.

In terms of throughput improvements I really haven't tried anything yet. At a minimum I need to buffer more output while waiting for the channel to be free again.
It may take some refactoring, but nothing should stop you from running two instances, producing two independent data streams. Send half the data over one channel, the other half over the other.