Audio

Short sound effects, streamed music, and procedural tones.

Load or generate resources in init! and keep the opaque values in the model. Sound and music are distinct types, so they cannot be mixed by accident. Their final Roc reference automatically unloads the host resource.

init! = App.init(
    App.default,
    |_io| Ok({ blip: Audio.gen_tone!({ freq: 440, ms: 120 })? }),
)
if input.devices.key_pressed(KeySpace) {
    model.blip.playback().with_volume(0.4).play!()
}

load_sound! and load_music! read from an Assets.Store, exactly as Assets.load_texture! does.

Sound is decoded into memory for short effects. Music streams encoded bytes for long tracks and is advanced automatically each frame.

The two loaders read the disk, so they wait: load_sound! and load_music! are legal in init!, where they block startup, and in tasks, where they park the task, and are refused in update! and render!. To start a track or a new effect after startup, call the loader inside Task.spawn! and keep the resource the task's message contains. gen_sound! and gen_tone! build a Sound with no file behind it, so they stay legal in init!, update!, and tasks. A generated sound lasts from 1 ms to max_generated_ms; asking for any other length stops the app, naming the fix, rather than making a different sound.

Every other effect here changes what the mixer is doing and is legal in init!, update!, and tasks, and refused in render!. The four queries that only read a scalar the device already has -- Sound.is_playing!, Music.is_playing!, Music.length!, and Music.time_played! -- are legal in any callback, render! included.

max_generated_ms : I32

The longest sound gen_sound! and gen_tone! make: five seconds, in milliseconds. For anything longer, load a file with load_music!.

gen_sound! : GenSound => Try(Sound, [SoundGenerationFailed, ResourceLimit])

Generate a reusable procedural sound.

ms must be from 1 to max_generated_ms. Any other length is a programming mistake, not a runtime condition, so it stops the app with a message naming the fix rather than making a shorter sound; for anything longer, load a file. ResourceLimit is the host's fixed sound table being full, and SoundGenerationFailed is the audio device refusing the samples, so initialization should propagate the returned error.

Legal in init!, update!, and tasks; refused in render!.

gen_tone! : { freq : F32, ms : I32 } => Try(Sound, [SoundGenerationFailed, ResourceLimit])

Generate a reusable sine tone. freq is Hz and ms is milliseconds, from 1 to max_generated_ms, as for gen_sound!.

Legal in init!, update!, and tasks; refused in render!.

set_master_volume! : F32 => {}

Set global output volume for all sounds and music, clamped to 0 through 1.

Legal in init!, update!, and tasks; refused in render!.

load_sound! : Store, Str => Try(Sound, [PathInvalid, NotFound, ReadFailed, SoundLoadFailed, ResourceLimit])

Load a short sound effect from an asset store.

Legal in init!, where it blocks startup, and in tasks, where it parks the task; refused in update! and render!. The file is read off the frame thread and decoded onto the audio device when the bytes are back. PathInvalid, NotFound and ReadFailed mean what they mean for Assets.load_texture!; SoundLoadFailed is bytes raylib would not decode. The format is taken from the extension, and .wav, .ogg, .mp3 and .qoa are the ones it reads; any other extension is SoundLoadFailed.

load_music! : Store, Str => Try(Music, [PathInvalid, NotFound, ReadFailed, MusicLoadFailed, ResourceLimit])

Load a streamed music file from an asset store. Keep the returned value in the app model.

Legal in init!, where it blocks startup, and in tasks, where it parks the task; refused in update! and render!. The file is read off the frame thread and the host keeps those bytes for as long as the stream exists, releasing them with the final reference to the Music. MusicLoadFailed is bytes raylib would not decode; the format is taken from the extension, and .wav, .ogg, .mp3, .qoa, .xm and .mod are the ones it reads; any other extension is MusicLoadFailed.

Sound

Audio.Sound :: # (opaque)

Host-owned short sound effect. Use methods such as sound.play!().

A sound has no volume, pitch, or pan of its own that outlives a play. raylib's are sticky per resource, and every play sets all three, so there is nothing to set once and inherit -- see Playback.

play! : Sound => {}

Play this sound at its default volume, pitch, and pan.

Equivalent to sound.playback().play!(), and stated the same way: the three playback parameters are always set before the sound starts.

Legal in init!, update!, and tasks; refused in render!.

stop! : Sound => {}

Stop playback and rewind to the beginning.

Legal in init!, update!, and tasks; refused in render!.

pause! : Sound => {}

Pause playback at the current position.

Legal in init!, update!, and tasks; refused in render!.

resume! : Sound => {}

Resume a paused sound.

Legal in init!, update!, and tasks; refused in render!.

is_playing! : Sound => Bool

Whether this sound is currently playing.

Legal in any callback, render! included.

playback : Sound -> Playback

The settings this sound plays at by default, so a play can adjust one of them without spelling out the rest: sound.playback().with_pitch(0.8).play!().

stub : Sound

Resource-free sound value for pure tests.

The handle never resolves to a host resource, so every host path it is passed to treats it as an invalid one: playing, stopping, pausing, and resuming it are all no-ops, and is_playing! returns Bool.False. Put it in a model to test the app's pure update logic from an expect. Do not use it to test playback or resource lifetime.

Playback

:= { sound : Sound, volume : F32, pitch : F32, pan : F32 }

A sound together with the settings it should be played at.

Volume, pitch, and pan are applied in that order before playback starts. Every play states all three, so a play cannot inherit what some earlier play left behind on the same host resource. That is why Sound has no set_volume!: raylib's setters are sticky per sound, and a PlaySound overwriting them silently was a trap rather than a feature.

with_volume : Playback, F32 -> Playback

Volume for this play, clamped by the host to 0 through 1.

with_pitch : Playback, F32 -> Playback

Pitch multiplier for this play. Non-positive values are clamped.

with_pan : Playback, F32 -> Playback

Stereo pan for this play, clamped by the host to -1 through 1.

play! : Playback => {}

Apply the settings and start playback.

Legal in init!, update!, and tasks; refused in render!.

Volume, pitch, and pan are all set before the sound starts, so this play cannot inherit what an earlier play left on the same sound.

Music

Audio.Music :: # (opaque)

Host-owned streamed music. The platform updates active streams each frame.

play! : Music => {}

Start or restart playback.

Legal in init!, update!, and tasks; refused in render!.

stop! : Music => {}

Stop playback and rewind.

Legal in init!, update!, and tasks; refused in render!.

pause! : Music => {}

Pause at the current position.

Legal in init!, update!, and tasks; refused in render!.

resume! : Music => {}

Resume paused playback.

Legal in init!, update!, and tasks; refused in render!.

set_volume! : Music, F32 => {}

Set stream volume, clamped to 0 through 1.

Legal in init!, update!, and tasks; refused in render!.

set_pitch! : Music, F32 => {}

Set stream pitch multiplier.

Legal in init!, update!, and tasks; refused in render!.

set_pan! : Music, F32 => {}

Set stereo pan, clamped to -1 through 1.

Legal in init!, update!, and tasks; refused in render!.

set_looping! : Music, Bool => {}

Enable or disable automatic looping.

Legal in init!, update!, and tasks; refused in render!.

is_playing! : Music => Bool

Whether this stream is currently playing.

Legal in any callback, render! included.

seek! : Music, F32 => {}

Seek to seconds from the start. Negative values are clamped to zero.

Legal in init!, update!, and tasks; refused in render!.

length! : Music => F32

Total stream length in seconds, or zero for an invalid resource.

Legal in any callback, render! included.

time_played! : Music => F32

Current playback position in seconds.

Legal in any callback, render! included.

stub : Music

Resource-free music value for pure tests.

The handle never resolves to a host resource, so every host path it is passed to treats it as an invalid one: transport and mutation calls are no-ops, is_playing! returns Bool.False, and length! and time_played! return zero. Put it in a model to test the app's pure update logic from an expect. Do not use it to test playback or resource lifetime.

Waveform

:= [Sine, Square, Triangle, Saw, Noise]

Procedural waveform used by gen_sound!.

GenSound : {
    waveform : Waveform,
    freq_start : F32,
    freq_end : F32,
    ms : I32,
    attack_ms : I32,
    decay_ms : I32,
    sustain : F32,
    release_ms : I32,
    volume : F32,
}

Envelope and pitch configuration for a generated sound.