So I’ve been writing an SDL backend for an emulator, it works, but in the process I ran into some issues.
First, SDL_OpenAudio has a quip in it that confuses me,
SDL_OpenAudio() is legacy and can only act on Device ID #1.
is there another preferred more modern way to open an audio sync given an AudioSpec, that can use multiple simultaneous devices? I don’t need it for this project, but I probably will if I want multiple out sinks in the future, and a cursory google search and forum search didn’t turn up anything. What is the story on this?
Second, in the signature of OpenAudio,
int SDL_OpenAudio(SDL_AudioSpec * desired, SDL_AudioSpec * obtained)
on certain backends, and this is documented, can mutate the spec given even if you pass a null obtained spec in terms of its sample count or buffer size. However, I had a nasty run debugging why even with an obtained spec SDL still mutates the desired spec - my impression is that it should probably be const, or at least in the presence of an obtained spec telling you the actual audio buffer you are getting, or have a note in the documentation. It was just inherently unintuitive that both specs get mutated if you proivde a desired and obtained one, and it makes me have to do some hacky coding whenever I reinit SDL audio to reset the sample count in my desired spec. Just seems unintuitive.
I’m also curious if there is a story behind why the callback for the AudioSpec:
(void *userdata, Uint8 * stream, int len);
Uses an int, where every other integral value in SDL uses either Sint32 or Uint32. I mean you can read the size of the buffer from the AudioSpec after opening it as a Uint32, so whats up with this? There is no documentation what a negative value would mean here, if it is intentional. Just seemed odd considering the pervasive use of Uint32 for the buffer.
While investigating why this was happening (and it always happens in pulseaudio) I was digging in PA and found this block:
/* Calculate the final parameters for this audio specification */
this->spec.samples /= 2; /* Mix in smaller chunck to avoid underruns */
/* Allocate mixing buffer */ h->mixlen = this->spec.size;
/* Reduced prebuffering compared to the defaults. */
/* 2x original requested bufsize /
paattr.tlength = h->mixlen * 4;
paattr.prebuf = -1;
paattr.maxlength = -1;
/ -1 can lead to pa_stream_writable_size() >= mixlen never being true /
paattr.minreq = h->mixlen;
flags = PA_STREAM_ADJUST_LATENCY;
paattr.tlength = h->mixlen2;
paattr.prebuf = h->mixlen2;
paattr.maxlength = h->mixlen2;
paattr.minreq = h->mixlen;
If you want to prevent underruns, you adjust minreq, I don’t get why this is always cutting sample count in half. I specify a sample poll rate I think is appropriate, and having to test on every sound backend to see which ones slice the sample count (in the documentation, SDL says 512 is a minimum, but since Pulse cuts that in half any time you run this on PA version > 1.0, it causes distortion and noise on some machines I’ve tested on which seems against the intent.
Additionally, these audio buffers are all bigger than the length from the spec anyway. I know from experience pulse can handle a 2 * 2 * 512 buffer size just fine (512 stereo 16 bit samples), polling at 48 khz. I just don’t get why you would intentionally mangle user provided values they want when you don’t have to. Rather than guessing an optimal buffer size, pulseaudio has a method given context and spec about what size to make spec->samples & size relative to the returned PA buffer(similar to how alsa does it - supply your desired values, calcuate real ones based off what the backend says):
size_t pa_context_get_tile_size(pa_context *c, const pa_sample_spec *ss)
The function has been available since 0.9.2, too.
That problem doesn’t happen in ALSA, which, while it does reset samples after setup, usually gets back the same sample count it puts into the backend. The way you would expect (ie, pass in num frames and size of a frame, get out a number of frames per pass, set samples to half that because stereo samples are 2 frames each). Though humorously:
/* !!! FIXME: Is this safe to do? */
this->spec.samples = bufsize / 2;
From alsa.c, only works with stereo. Because 2 channels. There is some surround support, I haven’t tested it, I don’t know if that will murder babies if you try running surround sound on alsa at the moment. So if you give it surround audio, you do mangle it. I just found this while skimming the alsa implementation.
Just some thoughts. It works, I just think the pulseaudio module could be improved to not wreck the user-requested sample rate. Should I try patching it, or am I missing some genius here?