Audio

Short sound effects, streamed music, and procedural tones.

Load or generate resources in init! and keep the opaque values in your 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,
    |_startup| 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! take a path used as the app gives it, resolved against the process working directory. There is no Assets.Store here: an audio path is not resolved through a store handle and is not confined to one, so a packaged app should build the path from something it controls rather than from the directory it happened to be launched in.

A Sound is decoded into memory and played from there, so it suits short effects. A Music is streamed, so it suits a long track; the platform reads the file once, keeps the encoded bytes for the stream's lifetime, and advances every active stream once per frame, with nothing for the app to pump.

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 carries. gen_sound! and gen_tone! build a Sound with no file behind it, so they stay legal in init!, update!, and tasks.

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.

load_sound! : Str => Try(Sound, [SoundLoadFailed, ResourceLimit, ..])

Load a short sound effect from disk.

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. SoundLoadFailed covers both a path with nothing behind it and bytes raylib would not decode; the format is taken from the extension, and .wav, .ogg, .mp3, .qoa and .flac are the ones it reads.

load_music! : Str => Try(Music, [MusicLoadFailed, ResourceLimit, ..])

Load a streamed music file. 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 covers both a path with nothing behind it and bytes raylib would not decode; the format is taken from the extension, and .wav, .ogg, .mp3, .qoa, .flac, .xm and .mod are the ones it reads.

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

Generate a reusable procedural sound. Generation can fail if the fixed host resource heap is exhausted, 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.

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!.

Sound

Audio.Sound :: # (opaque)

Host-owned short sound effect. Use receiver 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 reaches treats it as an invalid one: playing, stopping, pausing, and resuming it are all no-ops, and is_playing! answers Bool.False. Put it in a model to reach 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 reaches treats it as an invalid one: transport and mutation calls are no-ops, is_playing! answers Bool.False, and length! and time_played! answer zero. Put it in a model to reach 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.