Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Reference / UI

sound

Sound.Play(Sounds.Flap) · Sound.Play(Sounds.Flap, volume: 0.6) · Sound.Loop(Sounds.Valley) · Sound.Stop(Sounds.Valley) · Sound.StopAll()

Drop `.mp3`, `.wav`, `.ogg` or `.m4a` files into `model/sounds/` and play them with `Sound.Play(Sounds.Flap)`. The name is a compile-checked member of your app's `Sounds` vocabulary, so a typo is an error rather than silence. `Sound.Loop` starts background audio and `Sound.Stop` ends it.

preview1 example compiled by CIuiassetsaudiomedia

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

blip.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 haveput it inreach it with
a single-colour glyph that should follow your textmodel/icons/Icon(Icons.Search)
a vector illustration, logo or backgroundmodel/art/Svg(Art.Hexgrid)
a bitmap — a wall texture, a sprite sheet, a photographmodel/textures/Draw.Image(wall, …)
audio — an effect, a jingle, a music loopmodel/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 loop

Every 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 looping

Sound.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 .mp4 holding 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 .png renamed to .wav is refused, and so is any other image. Put a picture in model/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.

See also#

  • textures — the raster half of your app's assets, on the same file-is-the-declaration model
  • icons — single-colour glyphs
  • Canvas — the drawing surface a game pairs sound with
  • component — actions, on frame, and where these verbs are called from

Related

textures

Drop `.png`, `.jpg` or `.webp` files into `model/textures/` and blit them onto a canvas with `Draw.Image(wall, …)`. The…

icons

Drop `.svg` files into `model/icons/` and render them with `Icon(Icons.Search)`. The name is checked at compile time…

Canvas

A drawing surface, and the verbs that paint on it. Put a `Canvas` in a render block, call `Draw.*` from an `on frame`…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…