Summary#
A sound is a file in your app, not data. Put a .wav in model/sounds/ and it becomes part of your app's
vocabulary:
model/
sounds/
blip.wav
game_over.mp3
valley.oggblip.wav is now playable as Sound.Play(Sounds.Blip). There is nothing to register, nothing to import, and no
path to spell.
Sounds are the fourth kind of asset an app ships, and they divide by what the file is:
| you have | put it in | reach it with |
|---|---|---|
| a single-colour glyph that should follow your text | model/icons/ | Icon(Icons.Search) |
| a vector illustration, logo or background | model/art/ | Svg(Art.Hexgrid) |
| a bitmap — a wall texture, a sprite sheet, a photograph | model/textures/ | Draw.Image(wall, …) |
| audio — an effect, a jingle, a music loop | model/sounds/ | Sound.Play(Sounds.Blip) |
Signature#
Sound.Play(Sounds.Blip) // one shot; overlaps with itself and with anything else
Sound.Play(Sounds.Blip, volume: 0.6) // volume is 0..1; absent means full
Sound.Loop(Sounds.Valley) // start looping — background music, an engine hum, rain
Sound.Loop(Sounds.Valley, volume: 0.2)
Sound.Stop(Sounds.Valley) // stop that loop
Sound.StopAll() // stop every loopEvery verb returns nothing. Playback is fire-and-forget from your app's point of view.
Description#
The verbs run on the client, because that is where the speakers are. You can call them from an ordinary action,
from an on change block, or from on frame in a game — there is no server round trip and nothing to await.
The name is a value, not a string#
Sounds is a real type, synthesized from the files you ship. So a sound goes wherever a value goes — into a local,
across a component boundary as a parameter, through a switch:
[Composable]
component Blipper(Sounds note, string label) {
action Poke() { Sound.Play(note, volume: 0.5); }
render { Pressable(label, onClick: Poke); }
}
[Page("/blip")] [AllowAnonymous]
component BlipPage() {
render {
Stack(gap: 2) {
Blipper(note: Sounds.Blip, label: "blip");
Blipper(note: Sounds.Blip, label: "again, quietly");
}
}
}That is why the argument is Sounds.Blip and never a bare blip or a "blip.wav". Both of those are refused, each
with the rewrite that fixes it:
Sound.Play(blip) → `blip` is a declared sound, but a sound is read through its own
vocabulary — write `Sounds.Blip`.
Sound.Play("blip.wav") → `Sound.Play` takes a declared sound, not a file name — write
`Sounds.Blip`. (The app's sounds are compiled in and content-addressed,
so there is no path to spell.)Nothing plays until the visitor has interacted#
Every browser refuses to start audio until the person has clicked, tapped or typed something on the page, and it refuses silently — no error, no warning, just nothing. This is not something you work around; it is a rule about consent that every site is subject to.
The platform handles the mechanics: the first gesture the page sees unlocks audio, and everything after that plays normally. What you have to handle is the design consequence — a sound that is meant to play the instant the page opens will not. Put the first sound behind something the visitor does.
// ✗ nothing will be heard: the page has had no interaction yet
component TitleScreen() {
on mount { Sound.Loop(Sounds.Valley); }
}
// ✓ the music starts when they start
component TitleScreen() {
action Begin() { Sound.Loop(Sounds.Valley); Navigation.Go("/game"); }
render { Pressable("Play", onClick: Begin); }
}A loop replaces itself#
Sound.Loop(Sounds.Valley) while Valley is already looping stops the first one. That is what you want: an app
that calls it from a render pass or on every state change would otherwise stack a fresh copy of the track on top of
the last one, over and over, with no way back short of reloading the page.
One consequence worth knowing: two independent loops of the same file are not expressible. Two loops of different files are ordinary — each name is its own loop.
Sound.Loop(Sounds.Engine); // both play
Sound.Loop(Sounds.Wind);
Sound.Stop(Sounds.Engine); // the wind keeps going
Sound.StopAll(); // now nothing is loopingSound.StopAll() stops loops only. A one-shot already in flight is milliseconds long and is left to finish — when
you reach for StopAll you mean "stop the music", not "cut the click that is half-played".
Which formats, and why the file name does not decide#
.mp3, .wav, .ogg and .m4a. The type a sound is served under is read from its bytes at compile time, never
from its extension — so a file whose name disagrees with its contents is a compile error naming both, rather than a
file served as something it is not.
Two rejections are worth knowing about because they look like they should work:
- An
.mp4holding video is refused. A browser cannot decode it as audio, so accepting it would turn a wrong-file mistake into a sound that never plays and never says why. - A
.pngrenamed to.wavis refused, and so is any other image. Put a picture inmodel/textures/.
One sound per name, and where the files live#
A sound name resolves to exactly one file. Two files with the same stem — blip.wav and blip.mp3 — is an error
naming both, rather than a result that depends on which one the file system happened to list first.
The default glob is **/sounds/* at any depth, so model/sounds/, model/pages/sounds/ and a kit's vendored
ui/lib/sounds/ are all picked up. To put them somewhere else, declare the role in app.osy:
app Arcade {
model "model/**/*.osy";
sounds "audio/*.wav"; // instead of the default
}Size, and what is loaded when#
A single sound may be up to 16 MB, and one app's sounds up to 200 MB in total. Both are generous for their purpose: an effect is a few kilobytes and a music track is a few megabytes.
Sounds under 1 MB are decoded when the page loads, so the first effect you play is instant — a jump noise that arrives 300 ms after the jump is worse than no jump noise. Larger files are fetched the first time you play them, which is right for music: it starts once, and a few hundred milliseconds before it begins is inaudible.
When nothing is heard#
Silence is also what a correct app with no sound produces, so this surface says out loud what went wrong. Open the browser console: every failure reports once, naming the sound and the reason — a name the app does not ship, a file that would not fetch, a codec the browser would not decode, or audio the visitor has not unlocked yet.