Audio can be used without hooking into the Lua runtime via playdate->sound->addSource()
. There is also a version of this function that operates on channel objects, but I won't be covering it at this time. Refer to the Channels section of Inside Playdate with C for more information if you're curious. When not used in the context of a channel, the new source is associated with the "default channel", which is a resource managed by the audio runtime.
addSource()
has the following arguments:
SoundSource* playdate->sound->addSource(
AudioSourceFunction *callback,
void *context,
int stereo
);
callback
is a pointer to a handler function that will be described in just a moment.
context
is any pointer defined by the programmer. This same pointer will be used in calls to the handler callback.
stereo
is a boolean value for the number of channels in the source: 0=mono, 1=stereo.
- The return value is a pointer to the new
SoundSource
object.
AudioSourceFunction
has the following arguments:
int AudioSourceFunction(
void *context,
int16_t *left,
int16_t *right,
int len
);
context
is the same pointer that was used in the call to addSource()
. Every time the handler is called, context
will contain this pointer.
left
and right
are both pointers to a sample buffers to receive the output audio. They can be accessed like arrays, such as left[0]
, left[1]
and so-on.
len
is the number of audio frames to render. This many samples should be stored to left
and (if stereo) right
every time the handler is called.
- The return value indicates whether there is any meaningful audio data: 0=all audio is silent, 1=data in
left
and right
is meaningful.
Question: Is it true that mono sources only need to store to left
? I feel like that's the case but I can't say with absolute certainty.
Once addSource()
has been called, the runtime will automatically invoke the callback whenever it needs more samples, until playdate->sound->removeSource()
has been called for that source object. This happens in a manner similar to the program's update callback, but in a distinct context. The pointer to the PlaydateAPI
object is not passed to the audio handler, so if any runtime features (such as access to removeSource()
) is needed inside the handler, the program will need to make use of the context
argument.
Question: Is it safe to use removeSource()
from inside the audio callback? I've been doing this without issue so far, but I don't know if it's guaranteed to continue working in future SDK versions.
The way I recommend to associate state with the context
argument is to define a simple structure type. Consider the following:
// Context state for audio callback
typedef struct {
PlaydateAPI *pd;
SoundSource *source;
} AudioState;
// Create a new audio source with a state context
AudioState *state = pd->system.realloc(NULL, sizeof (AudioContext));
state->pd = pd;
state->source = pd->sound->addSource(&AudioHandler, state, 1);
// Audio callback routine
int AudioHandler(void *context, int16_t *left, int16_t *right, int len) {
// Resolve state fields as needed
AudioState *state = (AudioState *) context;
// Do not use the audio callback for system tasks:
// spend as little time here as possible
// Process all requested samples
for (int x = 0; x < len; x++) {
left [x] = rand(); // White noise
right[x] = rand(); // Narrowing conversion is automatic, so don't @ me
}
// Audio data is meaningful, so return 1
return 1;
}
Here, a structure type AudioState
is defined that points to both the PlaydateAPI
object and a SoundSource
object. This can be used to store any number of fields applicable to the program. For instance, if a sound effect is to be played and its length in samples is known, the state structure can store the number of remaining samples, allowing removeSource()
to be called after the sound effect has finished playing.
It's important to remember that the audio callback is not intended for anything except delivering audio samples to the runtime. Don't use it for any other purpose: use the program's update callback for everything else.