Skip to content
Moku Labs
@moku-labs/game pages

audio

Opt-in. Music and sound effects on three buses; volumes read from the committed player; sound as an effect a node awaits.

On this page

What it does

audio plays music and sound effects on three buses: master, music and sfx. It handles the effect kinds sfx, stopSfx and music, plays the scene's music, and reads the volumes from the committed player.

SetTierBuilds onEmits
opt-in, plugins: [...screen, audioPlugin]Standardlifecycle, model, flow, assets, scenesnothing

game.screen() of defineGameApp composes it for the game. An app made with createApp adds it itself. Without audio every sfx and music effect resolves undefined at once, so the same node runs in a game with no sound.

EffectWhat it does
sfx(key, { bus?, id?, fadeMs? })Resolves when the sound started. With an id it plays through its own gain, and the next sound with that id fades it out.
stopSfx(id, { fadeMs? })Fades out and ends a named sound. fadeMs: 0 cuts it.
music(key | null, { fadeMs? })Cross-fades to a track. null fades out.

Volumes. The player chooses volumes in the game, and a settings node commits them to player. Config volumes reads them from the committed player and applies them on every model:committed. The volumes of defineGame types the reader by the game's player.

Unlock. The context is asked to run at start. Where the shell lets audio play with no gesture, unlocked() is true at once. A browser that wants a gesture unlocks on the first pointerdown or touchend.

Config

pluginConfigs.audio.

OptionTypeDefaultDescription
buses{ master; music; sfx }{ master: 1, music: 0.6, sfx: 1 }Start gain of each bus, 0..1.
musicFadeMsnumber600Cross-fade of a music switch, in real milliseconds.
sfxFadeMsnumber120Fade of a named sound when nothing names fadeMs.
volumes(player) => volumes | undefinedundefinedReads the player's choice from the committed player. Absent: the buses stay at buses.
context() => AudioContextLikeundefinedThe context factory, a test seam. Absent: new AudioContext() where it exists.
journalnumber0How many started sounds journal() keeps. 0 turns it off.
music"decode" | "stream""decode""decode": a gapless buffer. "stream": an <audio> element, less memory, a gap at every loop. MP3 or AAC only.
session"ambient" | "playback" | "auto""ambient"What the page asks iOS for. "ambient" mixes with other apps and obeys the silent switch.

In a game made with defineGameApp, context belongs to the shell.

API

Mounted at app.audio, reachable with ctx.require(audioPlugin).

MethodSignatureDescription
setVolume(bus: "master" | "music" | "sfx", value: number) => voidSets a bus at once. Not persisted: the next commit applies volumes again.
volume(bus) => numberThe stored volume, never the live gain.
mute, muted(bus, on: boolean) => void, (bus) => booleanA mute flag per bus, independent of the pause.
unlocked() => booleanWhether the audio context runs.
journal() => readonly SoundEntry[]The last started sounds, oldest first. Empty while journal is 0.
stop(id: string, options?: { fadeMs?: number }) => voidThe same stop as the stopSfx effect. Headless it does nothing.

Events

Emits

None.

Listens to

model:committed (volumes), scenes:changed (the scene's music), lifecycle:changed (the pause) and assets:replaced (a swapped sound).

Example

sound.ts
import { audioPlugin, createApp, defineGame, music, screen, sfx, stopSfx, type } from "@moku-labs/game";

type Player = { settings: { audio: { master?: number; music?: number; sfx?: number } } };

const { defineNode, volumes } = defineGame<{
  player: Player;
  session: { rolls: number };
  assets: string;
  strings: Record<string, unknown>;
}>();

// Sound is an effect a node awaits, or starts and goes on.
export const win = defineNode({
  outcomes: { done: type() },
  run: async ({ fx, out }) => {
    await fx(music("board.theme"));
    fx.start(sfx("board.fanfare", { id: "fanfare" }));
    fx.start(stopSfx("fanfare", { fadeMs: 300 }));
    return out.done();
  }
});

const app = createApp({
  plugins: [...screen, audioPlugin],
  pluginConfigs: {
    model: { initialPlayer: { settings: { audio: { music: 0.5 } } } },
    audio: { volumes: volumes(player => player.settings.audio), journal: 20 }
  }
});
app.audio.mute("music", true);
app.audio.volume("music");
  • A failed cosmetic effect never fails the node. sfx, music, play and haptic that fail are a warning, and resolve undefined.
  • Store the choice in player. setVolume is overwritten by the next commit when volumes is set.
  • "stream" needs MP3 or AAC. WebM Opus is silent through a media element in WebKit.
  • scenes: music per scene.
  • assets: sounds are asset keys, .mp3 or .m4a.
  • anim: sfx as a timeline step.