Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sound

#![allow(unused)]
fn main() {
let hit = ctx.load_sound("hit.ogg");   // OGG Vorbis, WAV, MP3 or FLAC from assets/
ctx.play_sound(&hit);

let music = ctx.load_sound("theme.ogg");
let song = ctx.play_sound_ex(&music, SoundParams { volume: 0.5, looping: true, ..Default::default() });
ctx.stop_sound(song);
}
    const crabby::sound hit = ctx.load_sound("hit.ogg");   // OGG Vorbis, WAV, MP3 or FLAC from assets/
    ctx.play_sound(hit);

    const crabby::sound music = ctx.load_sound("theme.ogg");
    auto params = crabby::sound_defaults();
    params.volume = 0.5f;
    params.looping = true;
    const crabby::playing song = ctx.play_sound(music, params);
    ctx.stop_sound(song);
  • SoundParams { volume, speed, looping }: speed also changes the pitch, and a little random variation keeps repeated effects from sounding mechanical.
  • ctx.create_sound(sample_rate, &samples) makes a sound from generated mono samples (-1..1). The ui example’s click is made this way:
#![allow(unused)]
fn main() {
let samples: Vec<f32> = (0..4410) // 0.1 s at 44.1 kHz
    .map(|i| 0.3 * (i as f32 * 880.0 * std::f32::consts::TAU / 44100.0).sin())
    .collect();
let click = ctx.create_sound(44100, &samples);
}
    std::vector<float> samples(4410);   // 0.1 s at 44.1 kHz
    for (std::size_t i = 0; i < samples.size(); ++i)
        samples[i] = 0.3f * std::sin(float(i) * 880.0f * 6.2831853f / 44100.0f);
    const crabby::sound click = ctx.create_sound(44100, samples.data(), samples.size());
- `Sound` is a cheap `Copy` handle, like `Texture` (C++: `crabby::sound`, with `play_sound(s, params)` for `play_sound_ex`).

The audio device opens the first time a sound plays. Headless runs, and machines without audio, are silent but run the same code. For servers you can drop the audio library entirely:

ponko = { version = "0.0.1", default-features = false }

The sound API stays; it just makes no noise. The C library has the same switch: build ponko_ffi with --no-default-features. Linux builds with audio need the ALSA headers (libasound2-dev on Debian and Ubuntu).

Sound in 3D

With the 3d feature, pair AudioSource3D with Transform3D, or play a one-off sound at a point with ctx.play_sound_at. Every frame, after your systems, the engine moves each sound to its entity and sets its volume, panning and doppler from where the listener is. The listener follows ctx.camera3d; to hear from somewhere else, such as the player in a third-person game, call ctx.set_audio_listener(Some(..)). Positions use world units; doppler assumes metres and takes velocities from frame to frame. Set doppler: 0.0 to turn it off, for example for something that teleports.

fn setup(ctx: &mut Ctx) {
    let samples: Vec<f32> = (0..44100)
        .map(|i| (i as f32 * 220.0 * std::f32::consts::TAU / 44100.0).sin() * 0.1)
        .collect();
    let hum = ctx.create_sound(44100, &samples);
    let settings = AudioSettings3D {
        sound: SoundParams {
            volume: 0.4,
            looping: true,
            ..Default::default()
        },
        min_distance: 2.0,
        max_distance: 60.0,
        rolloff: AudioRolloff::Linear,
        ..Default::default()
    };
    ctx.world
        .spawn((Transform3D::at(vec3(4.0, 0.0, 0.0)), AudioSource3D::new(hum, settings)));
    ctx.play_sound_at(&hum, vec3(-4.0, 0.0, 0.0), AudioSettings3D::default());
}

fn main() {
    // The sounds follow their entities and the listener (ctx.camera3d) by themselves.
    Game::new("Spatial audio", 800, 600).run(setup);
}
static void sound3d(crabby::ctx& ctx) {
    float samples[4410];
    for (int i = 0; i < 4410; i++) samples[i] = 0.1f * std::sin(i * 220.0f * 6.2831853f / 44100.0f);
    auto hum = ctx.create_sound(44100, samples, 4410);
    auto settings = crabby::audio_settings3d_defaults();
    settings.sound.volume = 0.4f;
    settings.sound.looping = true;
    settings.min_distance = 2;
    settings.max_distance = 60;
    settings.rolloff = CRABBY_ROLLOFF_LINEAR;
    auto e = ctx.world().spawn(crabby::transform3d_at({4, 0, 0}));
    ctx.set_audio_source3d(e, hum, settings);
    ctx.play_sound_at(hum, {-4, 0, 0});
}

Build this example with cargo build --example audio3d --features 3d. Only running it opens a window and plays audio. Procedural clips already use create_sound; file-backed clips use load_sound.

AudioSettings3D controls volume, pitch (sound.speed), looping, minimum and maximum distance, and logarithmic, linear or keyed rolloff. Keyed curves use sorted (normalized_distance, gain) pairs in a static slice. All curves are silent at the maximum distance and full volume inside the minimum distance. Stereo panning follows the listener’s right axis. Kira applies gain, panning and playback-rate changes with smoothing; this is stereo positional audio, not an HRTF simulation. Prefer mono source clips for localized effects.

ctx.play_sound_at starts a voice at a fixed world position. Save its handle to stop it; removing an entity source stops its voice on the next audio update. Headless execution never opens an audio device, and disabling audio keeps the same APIs silent. No allocation occurs during updates unless a new voice starts. load_sound decodes clips into memory; load_stream keeps long files streamed.

Buses, low-pass and reverb

Ordinary playback uses the sfx bus. Create other named buses once with audio_bus, save their handles, and pass one to play_sound_on or to AudioSettings3D::bus. Handles belong to the context that created them. Volume is a linear gain; muting preserves the configured gain. Bus changes apply to active voices as well as future playback.

#![allow(unused)]
fn main() {
#[allow(dead_code)]
fn buses_and_effects(ctx: &mut Ctx, sound: Sound) {
    let music = ctx.audio_bus("music"); // Save this handle; names are looked up once.
    ctx.set_audio_bus_volume(music, 0.6);
    let voice = ctx.play_sound_on(
        &sound,
        SoundParams {
            looping: true,
            ..Default::default()
        },
        music,
        AudioEffects {
            low_pass_hz: 4_000.0,
            reverb: 0.2,
        },
    );
    ctx.set_sound_effects(
        voice,
        AudioEffects {
            low_pass_hz: 800.0,
            reverb: 0.5,
        },
    );
    ctx.mute_audio_bus(music, true);
    ctx.mute_audio_bus(music, false); // Restores the configured gain.
    let settings = AudioSettings3D {
        bus: music,
        effects: AudioEffects {
            low_pass_hz: 2_000.0,
            reverb: 0.3,
        },
        ..Default::default()
    };
    ctx.play_sound_at(&sound, vec3(3.0, 0.0, 0.0), settings);
}
}
static void buses_and_effects(crabby::ctx& ctx, crabby::sound sound) {
    auto music = ctx.audio_bus("music"); // Save this handle; names are looked up once.
    ctx.set_audio_bus_volume(music, 0.6f);
    auto params = crabby::sound_defaults();
    params.looping = true;
    auto voice = ctx.play_sound_on(sound, music, params, {4000, 0.2f});
    ctx.set_sound_effects(voice, {800, 0.5f});
    ctx.mute_audio_bus(music, true);
    ctx.mute_audio_bus(music, false); // Restores the configured gain.
    auto settings = crabby::audio_settings3d_defaults();
    settings.bus = music;
    settings.effects = {2000, 0.3f};
    ctx.play_sound_at(sound, {3, 0, 0}, settings);
}

Each voice uses a Kira low-pass filter on its own child track. The cutoff is clamped to 20–20,000 Hz. set_sound_effects changes cutoff and reverb send while a voice is playing, including positional voices. Reverb is a single shared, fully wet Kira send track; the send amount is linear 0–1. The send follows voice attenuation, filtering and bus gain. Muting a bus stops its new reverb input; existing shared reverb tails decay naturally. Nonfinite cutoff/send values use defaults. No custom DSP is used. Starting a voice allocates its track and decoder; changing parameters and updating existing spatial voices does not allocate engine-side buffers each frame.

Streaming music and ambience

#![allow(unused)]
fn main() {
#[allow(dead_code)]
fn streaming_music(ctx: &mut Ctx) {
    let music = ctx.audio_bus("music");
    let song = ctx.load_stream("music.ogg");
    let voice = ctx.play_sound_on(
        &song,
        SoundParams {
            looping: true,
            ..Default::default()
        },
        music,
        AudioEffects::default(),
    );
    ctx.stop_sound(voice);
}
}
[[maybe_unused]] static void streaming_music(crabby::ctx& ctx) {
    auto music = ctx.audio_bus("music");
    auto song = ctx.load_stream("music.ogg");
    auto params = crabby::sound_defaults();
    params.looping = true;
    ctx.play_sound_on(song, music, params);
}

load_stream reads the header and keeps the native filesystem path rather than decoding the entire clip. Each playback opens a separate decoder; Kira performs buffered decoding on its worker thread. The returned Sound works with 2D playback, positional playback, buses, effects and looping. The file must remain available until playback ends. Streaming files are not hot reloaded and currently require native filesystem assets (web builds should use load_sound for prefetched assets). With audio disabled, stream handles are silent and report zero duration. Headless playback never creates tracks, decoder workers or an audio device. These additions currently have Rust APIs only; C and C++ bindings are deferred.