Audio
Two setup rules
import { audio, loader } from "melonjs";
audio.init("mp3,ogg"); // 1. BEFORE preloading any audio
await loader.preload([
{ name: "cling", type: "audio", src: "data/sfx/" }, // 2. a PREFIX, not a file
]);
audio.init(format)must run before any audio asset is preloaded, and lists the formats you actually ship. Preloading first throwstarget audio extension(s) should be set through me.audio.init() before calling the preloader.It returnsfalsewhen no audio backend is available, so it is worth checking. The format list defaults to"mp3".srcis a prefix, not a file. The loader appends the assetnameplus each configured extension and uses the first that loads — sodata/sfx/+cling+mp3→data/sfx/cling.mp3. Passing a full filename gives you a 404. Adata:audio/...URL is the one exception: it is used verbatim, prefix and extension skipped.
Order matters — melonJS tries the listed formats left to right, so put the
preferred one first. Two formats ("webm,mp3" or "mp3,ogg") is the usual
belt-and-braces for codec coverage. Accepted tokens: mp3, mpeg, opus,
ogg, oga, wav, aac, caf, m4a, m4b, mp4, weba, webm, dolby,
flac.
Effects versus music
audio.play("cling"); // fire and forget
audio.play("cling", false, null, 0.5); // loop, onend, volume
const id = audio.play("engine", true); // looping; keep the id to stop it
audio.playTrack("theme"); // background music
audio.stopTrack();
Use playTrack for music, not play. playTrack(name, volume?) always
loops, and it records the name as the current track — which is what
app.pauseOnBlur / resumeOnFocus / stopOnBlur act on, via
state.pause(true) → pauseTrack(). stopTrack(), pauseTrack(),
resumeTrack() and getCurrentTrack() all read that same single slot, so there
is only ever one current track. Stop it in onDestroyEvent.
Every per-clip function throws audio clip <name> does not exist if the clip was
never loaded, and play additionally throws when a named sprite region is
missing.
Audio sprites
One file carrying many effects, with named regions in milliseconds:
await loader.preload([{
name: "sfx", type: "audio", src: "data/sfx/",
sprite: { jump: [0, 450], hit: [2000, 250], music: [4000, 12000, true] },
}]);
audio.play("sfx", { sprite: "jump" });
The optional third element marks a region as looping. play()'s second argument
takes either the original loop boolean or a PlayOptions object
(sprite, loop, onend, volume) — both forms work. Object fields win over
the positional onend / volume arguments when both are supplied.
Clip state
audio.duration("theme"); // seconds — 0 while still loading
audio.duration("sfx", id); // duration of the region THAT instance plays
audio.playing("theme"); // is anything playing?
audio.playing("theme", id); // is that instance playing?
audio.state("theme"); // "unloaded" | "loading" | "loaded"
stop(), pause() and resume() take an optional instance id too; omit it
and the whole group is affected. audio.stop() with no arguments at all stops
every sound. audio.seek(name) reads the position, audio.seek(name, s, id?)
writes it; audio.rate and audio.fade(name, from, to, ms, id?) round it out.
Volume and muting
audio.setVolume(0.5); // global, 0..1
audio.getVolume(); // read it back
audio.mute("cling"); // one clip — mute(name, id?, shouldMute = true)
audio.unmute("cling");
audio.muteAll(); // everything (audio.disable() is an alias)
audio.unmuteAll(); // (audio.enable() is an alias)
audio.muted(); // global mute state
audio.unload(name) / audio.unloadAll() free the decoded buffers; unload
also clears the current-track pointer if it named that clip.
Spatial audio
audio.position("engine", x, y, z);
audio.orientation("engine", x, y, z);
audio.stereo("engine", -1); // -1 left … 1 right
audio.panner("engine", { coneInnerAngle: 90, /* … */ });
position(name, x, y = 0, z = -0.5, id?) and
orientation(name, x, y, z, id?) both default id to "the whole group".
Called with just the clip name, each of the four returns the current value —
stereo gives 0 and position gives [0, 0, 0] before they have ever been
set.
Spatial audio is WebAudio-only. On a clip loaded with stream: true (or
html5: true) stereo / position / orientation return early and do
nothing — which is exactly the case for the long music tracks people reach for
first.
Procedural sound
For beeps, blips and noise without shipping files. Both take one options
object, and duration (in seconds) is required:
audio.tone({ freq: 440, duration: 0.2, gain: 0.3, pan: -0.5 });
audio.tone({ freq: [440, 660, 880], duration: 0.4 }); // layered partials
audio.noise({ duration: 0.5, filter: { type: "lowpass", frequency: 800 } });
tone also takes wave (default "sine"), attack (0.005) and pitchSlide
(1). noise takes type ("white" | "pink" | "brown", default
"white"), attack, pan, filter and filterSweep. gain defaults to 0.1
on both.
Sequencing: delay, not setTimeout
Both take delay — seconds before the sound starts, scheduled on the audio
clock. That is what builds a multi-part sound: a stinger's second note, an
explosion's double-tap.
audio.tone({ freq: 392, duration: 0.12 });
audio.tone({ freq: 587, duration: 0.18, delay: 0.11 });
audio.tone({ freq: [784, 1176], duration: 0.75, delay: 0.23 });
Do not reach for setTimeout here. A timer fires on the main thread, so a busy
frame slips the note late and the sequence audibly loses time — the fault is
easy to blame on the sound design rather than the scheduling. delay is
sample-accurate because the whole envelope is scheduled up front.
Both are silent no-ops when there is no WebAudio context — they bail on
getAudioContext() === null rather than throwing. Neither needs the clip to be
loaded; neither goes through the file-playback path at all.
audio.getAudioContext() and audio.getMasterGain() are the escape hatches for
custom Web Audio work; both return null when audio is unavailable, so guard.
Connect a custom graph to the master gain rather than ctx.destination if you
want it to respect setVolume / muteAll.
Autoplay: sound is silent until the first gesture
Browsers create the audio context in the suspended state and keep it there
until a user interaction. Nothing errors — audio simply does not play, which
reads as a bug. melonJS installs one-shot touchstart / touchend / click /
keydown unlock listeners on document, so the first real gesture anywhere
unlocks it and fires the clip's on.unlock callback. Design for it: start music
on the first click or key press, not on load. tone / noise additionally call
ctx.resume() themselves, best-effort, on every invocation.
Playback also starts asynchronously: play() returns an id while the
instance is still starting, so playing() can legitimately be false on the
very next line.
Load-time options
{ name: "sfx", type: "audio", src: "data/sfx/",
sprite: { jump: [0, 450] },
pool: 5, // finished instances kept for reuse — NOT a concurrency cap
rate: 1.0, // initial playback rate, 0.5..4.0
mute: false,
loop: false,
autoplay: false,
preload: true, // or "metadata"
format: "mp3", // explicit hint when the URL has no usable extension
stream: true, // long music: stream rather than decode into memory
on: { play() {}, end() {}, stop() {}, unlock() {} },
}
pool (default 5) is a reuse cap on finished voices, not a limit on
simultaneous playback: when the number of ended voices exceeds it, the surplus
are disconnected and dropped. Raise it for a clip triggered in rapid bursts, not
to allow more overlap.
Use stream: true for long music tracks — it plays through an HTML5 <audio>
element instead of decoding the whole file into memory. (html5: true is the
same switch under its backend name.) on accepts the full lifecycle set:
play, pause, stop, end, fade, seek, rate, volume, mute,
unlock.
Re-preloading a manifest that already contains a loaded clip is a no-op —
audio.unload(name) first if you genuinely want to reload it.
Credentials use the same global switch as every other asset type —
loader.setOptions({ withCredentials: true }), not an audio-specific option.
Buffered audio fetches through the shared loader path, so it honours it exactly
as an image or a JSON file does. A stream: true clip does not: it plays
through an <audio> element, which needs a crossorigin attribute rather than
fetch credentials. Preload a clip buffered if it sits behind a login.
Failing clips and setStopOnAudioError(false). The default is to fail the
preload when a clip cannot be loaded after its retries: the error reaches the
loader, so await loader.preload(...) rejects and your catch runs, and the
reason is logged. Turn the flag off and the engine mutes audio, warns
(failed loading <name>, disabling audio) and lets the load continue — the
preload finishes and the game reaches the next scene without sound.
Before 20.4 both halves were broken: with the flag off it disabled audio and failed the preload anyway, and a clip whose bytes arrived but would not decode (a missing file served as HTML by an SPA rewrite, say) reported nothing at all either way — blank screen, empty console.
Symptom → cause
| symptom | cause |
|---|---|
| audio assets 404 | src given as a full filename instead of a directory prefix |
target audio extension(s) should be set... thrown at preload |
audio.init() not called before preloading |
audio clip X does not exist |
the clip was never preloaded, or the name is mis-cased — every per-clip call throws this |
| nothing plays until the user clicks | browser autoplay policy — expected, design around it |
playing() is false right after play() |
playback starts asynchronously |
| music keeps playing after leaving a scene | used play instead of playTrack, or no stopTrack in teardown |
stereo / position do nothing |
the clip was loaded with stream: true — spatial is WebAudio-only |
| one format works, another does not | that codec is not in the audio.init() list |
| audio behind a login fails, or the preload hangs | loader.setOptions({ withCredentials: true }) — the same switch every asset type uses |
a streamed clip ignores withCredentials |
stream: true plays through an <audio> element; preload it buffered instead |
audio.tone(440, {...}) does nothing / errors |
tone takes a single options object with a required duration |
tone / noise are silent with no error |
no WebAudio context — getAudioContext() returned null |
Related skills
melonjs-getting-started— preloading and asset ordering