Tutorial.
Tutorial. Procedural audio synthesis: play a melody by changing the sine wave's frequency once per second.
This is the audio counterpart to start.c. It introduces the callback-driven PCM model, the audio device's threading rules, and the built-in bj_play_audio_note synthesiser. By the end you can plug your own callback in and generate any waveform you like.
What you'll learn
- Initialise the audio subsystem with
bj_begin(BJ_AUDIO_SYSTEM).
- Open a device with bj_open_audio_device, choosing format, sample rate, and channel count.
- Use the built-in bj_play_audio_note callback with bj_audio_play_note_data to drive sine / square / triangle / sawtooth waveforms.
- Start playback with bj_play_audio_device.
- Modify the waveform parameters from the main thread while the audio thread reads them: the simplest form of audio-side state.
Prerequisites
None beyond a working Banjo install with an enabled audio backend (ALSA on Linux, MME on Windows, CoreAudio on macOS, Emscripten on the web; see the Audio topic for the backend selection rules).
Walkthrough
1. The audio model: callback-driven
Banjo's audio system does not buffer audio for you. You give it a callback (bj_audio_callback_fn) and it calls that callback from a dedicated audio thread, asking you to fill an output buffer with the next batch of samples. This pattern is standard for low-latency audio (it's how ALSA, CoreAudio, ASIO, and JACK all expose themselves).
The downside is that your callback runs on a real-time thread: don't allocate, don't take locks, don't call into slow code. The upside is that latency is bounded by the buffer size, not by your scheduling.
2. Properties: what format do you want?
bj_audio_properties specifies the format you'd like; the device may return something different if your request isn't supported. The tutorial uses BJ_AUDIO_FORMAT_F32 (32-bit float, nominally -1.0..1.0), 44100 Hz, stereo. amplitude is only consulted by the INT16 path, so it's harmless here.
3. The built-in synthesiser
You don't have to write your own callback for simple waveforms. bj_play_audio_note is a ready-made bj_audio_callback_fn that reads bj_audio_play_note_data through its user_data pointer and generates one of the four supported waveforms at the requested frequency. The tutorial just changes data.frequency from the main thread, and the audio thread picks up the new value on the next sample.
Mutating data.frequency from the main thread while the audio thread reads it is technically a data race for the standard's purposes, but it's harmless here: the field is a double, the write is single-instruction on every platform Banjo targets, and a torn read would just produce a one-sample glitch. For anything more elaborate, gate updates through atomics.
4. Driving the melody from the main thread
The step callback reads bj_run_time, uses that to index into a 9-note melody, and updates data.frequency. The audio callback keeps running on the audio thread, smoothly switching pitch on the next sample. When the index exceeds the melody length we call bj_quit_app; the run loop unwinds through teardown.
5. Cleanup
bj_close_audio_device joins the audio thread before returning, then bj_end shuts the subsystem down. Always close devices before bj_end.
What's next
shaders.c: the same callback-style architecture, but for per-pixel rendering instead of per-sample audio.
- The Audio topic for the full audio API and the backend selection model.
static void*
setup(
struct bj_app* app,
void* init_data) {
(void)init_data;
return 0;
}
data.function = BJ_AUDIO_PLAY_SINE;
.amplitude = 16000,
.sample_rate = 44100,
.channels = 2,
return 0;
}
return 0;
}
static void step(
struct bj_app* app,
struct bj_tick_info tick,
void* user_data) {
(void)tick;
(void)user_data;
static const double melody[] = {
261.63, 293.66, 329.63,
349.23, 392.00, 349.23,
329.63, 293.66, 261.63
};
enum { MELODY_LEN = 9 };
if (note >= MELODY_LEN) {
return;
}
data.frequency = melody[note];
}
static void teardown(
struct bj_app* app,
void* user_data) {
(void)user_data;
}
int main(
int argc,
char* argv[]) {
(void)argc; (void)argv;
}
General-purpose definitions for Banjo API.
Application lifecycle: callback-driven setup, step, and teardown.
Basic audio library interface.
int main(int argc, char *argv[])
static void step(struct bj_app *app, struct bj_tick_info tick, void *user_data)
bj_audio_device * p_device
static void teardown(struct bj_app *app, void *user_data)
bj_audio_play_note_data data
static void * setup(struct bj_app *app, void *init_data)
int bj_run_app(bj_app_setup_fn setup, bj_app_step_fn step, bj_app_fixed_step_fn fixed_step, bj_app_teardown_fn teardown, void *init_data)
Drive the application lifecycle.
void bj_quit_app(struct bj_app *app, int exit_code)
Signal the given application to exit on the next iteration.
Timing snapshot handed to the step and fixed-step callbacks.
void bj_play_audio_note(void *buffer, unsigned frames, const struct bj_audio_properties *audio, void *user_data, uint64_t base_sample_index)
Generate a basic waveform tone using a built-in callback.
void bj_close_audio_device(struct bj_audio_device *device)
Close an audio device and release all associated resources.
struct bj_audio_device * bj_open_audio_device(const struct bj_audio_properties *properties, bj_audio_callback_fn callback, void *callback_user_data, struct bj_error **error)
Open the default audio device for playback.
void bj_play_audio_device(struct bj_audio_device *device)
Resume audio playback.
@ BJ_AUDIO_FORMAT_F32
32-bit IEEE-754 float PCM.
Describe properties of an audio device.
struct bj_audio_device bj_audio_device
bj_bool bj_begin(int systems, struct bj_error **error)
Initialises the system.
void bj_end(void)
De-initialises the system.
double bj_run_time(void)
Gets the current time in seconds since Banjo initialisation.
Logging utility functions.
Portable main() replacement with platform-aware entry shim.
Define parameters for generating simple waveforms.
Header file for system interactions.
Header file for time manipulation utilities.