diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0d790195d0..2742c1a29f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "melonjs", - "version": "20.8.0", + "version": "20.9.0", "description": "Build games with melonJS — 23 guides to the 2D, 2.5D and 3D HTML5 game engine, its conventions and idioms, so generated code runs the first time.", "author": { "name": "melonJS", diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index a2a225330b..08587b2f42 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [20.9.0] (melonJS 2) - _unreleased_ + +### Fixed +- Skills: the UI skill listed `NineSliceSprite` panels among what it covers but never mentioned them. It now shows both ways to make one (`createSpriteFromName(name, { width, height }, true)` from an atlas, or the constructor from an image) and the three things that trip people up: `width` and `height` are the size to stretch to and are mandatory, `insetx` / `insety` default to a quarter of the image rather than its border, and the anchor is the centre +- Skills: the audio skill now covers what positional sound needs and did not say: the listener is fixed at the origin with no public call to move it, so positions are relative to the player; the panner defaults assume metres, so a sound placed in pixels is near silent a few tiles out (`refDistance: 1` puts one 100 px away at about 1%); and `stereo()` and `position()` drive the same per-instance panner, so whichever runs first makes the other a silent no-op. It also shows how to put reverb, echo or a compressor on the whole mix through `getMasterGain()`, noting that a streamed clip bypasses it, and documents `hasAudio()`, `hasFormat()`, runtime `audio.load()`, and the `rate` and `fade` ranges + ## [20.8.0] (melonJS 2) - _2026-10-06_ ### Added diff --git a/packages/melonjs/package.json b/packages/melonjs/package.json index adca8522cc..e9cfabebc4 100644 --- a/packages/melonjs/package.json +++ b/packages/melonjs/package.json @@ -1,6 +1,6 @@ { "name": "melonjs", - "version": "20.8.0", + "version": "20.9.0", "description": "melonJS Game Engine", "homepage": "http://www.melonjs.org/", "type": "module", diff --git a/packages/melonjs/skills/melonjs-audio/SKILL.md b/packages/melonjs/skills/melonjs-audio/SKILL.md index 2abca19fb6..0e98eaf0f8 100644 --- a/packages/melonjs/skills/melonjs-audio/SKILL.md +++ b/packages/melonjs/skills/melonjs-audio/SKILL.md @@ -1,6 +1,6 @@ --- name: melonjs-audio -description: "Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, and procedural tone/noise generation. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, sprite audio, stereo, panner, audio.tone, audio.noise, getAudioContext, autoplay." +description: "Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, procedural tone/noise generation, and effects such as reverb or a compressor on the whole mix. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, the fixed listener and pixel-scale distances of positional sound, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, fade, rate, pitch, sprite audio, stereo, panner, position, listener, refDistance, reverb, echo, delay, compressor, distortion, filter, audio effect, audio.tone, audio.noise, getAudioContext, getMasterGain, hasAudio, hasFormat, autoplay." license: MIT --- @@ -29,6 +29,10 @@ await loader.preload([ gives you a 404. A `data:audio/...` URL is the one exception: it is used verbatim, prefix and extension skipped. +`audio.hasAudio()` says whether the browser can play audio at all, and +`audio.hasFormat("ogg")` whether it can play one codec — useful for choosing +the `init` list or hiding a sound toggle on a device with no audio. + 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`, @@ -88,7 +92,9 @@ 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. +writes it; `audio.rate(name, r, id?)` changes speed and pitch together +(`0.5`..`4.0`, `1` is normal) and `audio.fade(name, from, to, ms, id?)` ramps +the volume (`0`..`1`, over milliseconds). ## Volume and muting @@ -120,6 +126,40 @@ 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. +### The listener is fixed at the origin + +The ear sits at `(0, 0, 0)` facing `-z`, and **there is no public call to move +it**. Positions are therefore *relative to the listener*: for a sound to come +from the left of the player, pass the source's position minus the player's, +every frame either one moves. + +```js +// in the emitter's update() +audio.position("engine", this.pos.x - player.pos.x, 0, -0.5, this.engineId); +``` + +### Distances are in the units you pass — and the defaults assume metres + +The panner defaults are `distanceModel: "inverse"`, `refDistance: 1`, +`rolloffFactor: 1`, `maxDistance: 10000`, `panningModel: "HRTF"`. With +`refDistance: 1`, a source 100 units away plays at about **1%** volume, so a +sound positioned in raw pixels is effectively silent a few tiles out. Either +set `refDistance` to the distance in pixels at which it should still be at full +volume, or scale positions down (divide by a tile size) before passing them: + +```js +audio.panner("engine", { refDistance: 200, rolloffFactor: 1 }); +// or: audio.position("engine", dx / 32, 0, -0.5); +``` + +### `stereo` and `position` are one or the other + +Both drive the same per-instance panner node, and **the first one called +decides what that node is**: a stereo panner after `stereo()`, a 3D panner after +`position()`. After that the other call silently does nothing on that instance. +For a 2D game, left-right `stereo()` is usually all you want; reach for +`position()` only when distance falloff matters, and then use it alone. + **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 @@ -167,6 +207,42 @@ 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`. +## Effects on all game audio: reverb, echo, compressor + +There is no built-in reverb, delay, distortion or compressor, and no per-clip +filter on file playback. There does not need to be one for the common case: +every buffered clip and every `tone` / `noise` passes through the master gain +on its way to the speakers, so an effect inserted **after** it applies to the +whole mix. + +```js +const ctx = audio.getAudioContext(); // creates the context if needed +const master = audio.getMasterGain(); +if (ctx && master) { // null when audio is unavailable + const comp = ctx.createDynamicsCompressor(); + master.disconnect(); // master -> speakers becomes + master.connect(comp); // master -> compressor -> speakers + comp.connect(ctx.destination); +} +``` + +Any Web Audio node or chain works the same way: a `ConvolverNode` with an +impulse response for reverb, a `DelayNode` with a feedback gain for echo, a +`BiquadFilterNode` for an underwater low-pass, a `WaveShaperNode` for +distortion. Do it once, at startup; the master gain lives as long as the +context. + +- **`setVolume` / `muteAll` still work**, because they act on the master gain, + which is now upstream of the effect. +- **Streamed clips are not affected.** A `stream: true` (`html5: true`) clip + plays through an `