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