Banjo API 1.0.0-rc.2
Low-level C99 game development API
Loading...
Searching...
No Matches
audio_pcm.c

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.
#include <banjo/api.h>
#include <banjo/app.h>
#include <banjo/main.h>
#include <banjo/audio.h>
#include <banjo/log.h>
#include <banjo/system.h>
#include <banjo/time.h>
// Data passed to the audio callback. The callback reads this to know what
// frequency to generate. We modify it in real-time to change the pitch.
static void* setup(struct bj_app* app, void* init_data) {
(void)init_data;
// Initialize the audio subsystem.
bj_quit_app(app, 1);
return 0;
}
// Choose which waveform to generate. BJ_AUDIO_PLAY_SINE creates smooth
// sine wave tones. Other options include square, triangle, and sawtooth.
data.function = BJ_AUDIO_PLAY_SINE;
// Open an audio device with specific properties:
// - format: BJ_AUDIO_FORMAT_F32 uses 32-bit float samples (-1.0 to 1.0)
// - sample_rate: 44100 Hz (CD quality)
// - channels: 2 (stereo output)
// The callback bj_play_audio_note will be called repeatedly to fill buffers.
// We pass &data so the callback knows what frequency to generate.
.format = BJ_AUDIO_FORMAT_F32, /* float buffer */
.amplitude = 16000, /* used only for INT16 path */
.sample_rate = 44100,
.channels = 2,
if (!p_device) {
bj_quit_app(app, 1);
return 0;
}
// Start audio playback. The callback will now run in a separate thread,
// continuously filling audio buffers.
return 0;
}
static void step(struct bj_app* app, struct bj_tick_info tick, void* user_data) {
(void)tick;
(void)user_data;
// Simple melody: C-D-E-F-G-F-E-D-C (frequencies in Hz).
// Middle C (C4) = 261.63 Hz, D4 = 293.66 Hz, etc.
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 };
// Use runtime as note index - changes once per second.
int note = (int)bj_run_time();
if (note >= MELODY_LEN) {
bj_quit_app(app, 0);
return;
}
// Update the frequency. The audio callback runs in a separate thread and
// will immediately pick up this change, smoothly transitioning to the new note.
data.frequency = melody[note];
}
static void teardown(struct bj_app* app, void* user_data) {
(void)user_data;
// Stop audio playback and close the device.
bj_end();
}
int main(int argc, char* argv[]) {
(void)argc; (void)argv;
return bj_run_app(setup, step, 0, teardown, 0);
}
General-purpose definitions for Banjo API.
Application lifecycle: callback-driven setup, step, and teardown.
Basic audio library interface.
int main(int argc, char *argv[])
Definition audio_pcm.c:177
static void step(struct bj_app *app, struct bj_tick_info tick, void *user_data)
Definition audio_pcm.c:144
bj_audio_device * p_device
Definition audio_pcm.c:105
static void teardown(struct bj_app *app, void *user_data)
Definition audio_pcm.c:170
bj_audio_play_note_data data
Definition audio_pcm.c:104
static void * setup(struct bj_app *app, void *init_data)
Definition audio_pcm.c:107
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.
Definition app.h:106
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.
Definition audio.h:132
Describe properties of an audio device.
Definition audio.h:186
struct bj_audio_device bj_audio_device
Definition api.h:325
bj_bool bj_begin(int systems, struct bj_error **error)
Initialises the system.
void bj_end(void)
De-initialises the system.
@ BJ_AUDIO_SYSTEM
Definition system.h:80
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.
Definition audio.h:332
Header file for system interactions.
Header file for time manipulation utilities.