Repository navigation
EN Create Music
Français · How-to + reference · Published, partly reviewed by a person · Applies from: 4.2.5
A game names its tracks by id in audio.music and plays them with titleScreen.music, a room's music or the
music command. This page says where those files come from: the engine repository's audio pipeline turns a MIDI into
a Sega Mega Drive arrangement, renders it, checks it, and can split it into stems that the game remixes as it is
played. It is also the reference of audio.scores and audio.transitions.
Audience, context, and the statuses of what this page rests on
Audience: author · Context: engine repository, created project
A track in games/<id>/audio/music/, declared in audio.music, that sounds like the rest of the game (same palette as
the sound effects), with a QA report that says ISSUES: none; optionally its stems and a score whose mix follows the
game's state.
- The pipeline (
npm run audio) runs in the engine repository only. A created project has noaudiocommand (npx quest-n-click helplists none): render the track in a clone of the engine repository, then copy the.mp3(and its stems) into the project'sgame/audio/music/. - Declaring and playing music (
audio.music,audio.scores,audio.transitions) works the same in both. - Stems and the music director exist since 3.5, transitions between scores since 3.6, music resumed from a save since
3.6.1. This page is true from 4.2.5 (the
quest-n-clickcommand name).
- Python 3 with numpy,
ffmpegandfluidsynth(brew install ffmpeg fluid-synthon macOS). See Audio toolchain. - Furnace 0.6.8.3, downloaded by
npm run audio -- setupintotools/audio/.furnace(not in the repository). On a systemsetupdoes not cover, install Furnace yourself and pointFURNACE_BINat it. - For an audio source (not a MIDI) only: a Python 3.11 virtual environment for the transcription (step Audio sources).
At the root of the engine repository, on your machine. <id> is the game's folder under games/, <slug> the
project's name.
tools/audio turns a MIDI (or an audio file) into a Sega Mega Drive arrangement: YM2612 FM, SN76489 PSG and DAC
drums. It synthesises the sound effects from the same bank of sounds, so every track and every blip of a game share
one identity, the way npm run prompts gives every image one style. The decisions are a JSON file (spec.json), the
rest is code: an AI assistant or a person follows the same steps.
npm run audio -- setup # once per machine: Furnace 0.6.8.3 + a GM SoundFont
npm run audio -- ingest song.mid games/<id>/audio/projects/<slug> # MIDI (or .wav/.mp3: see "Audio sources")
npm run audio -- all games/<id>/audio/projects/<slug>/spec.json # .fur + .wav + .vgm + .mp3 + QA report
npm run audio -- sfx games/<id>/audio/sfx.json # the game's sound effects → audio/sfx/*.mp3
npm run assets # ships audio/ to public/assetsThe other subcommands: analyze <file.mid> [--dump N] [--drums N], transcribe <audio> -o <out.mid> [--bpm N] [--no-separate], build <spec.json> (the .fur only), qa <spec.json> (the report only), stems <spec.json> [--out <dir>]. Sound effects have their own page: Sound effects.
-
Sources. A track's source must be yours, in the public domain, or under a licence that allows a derived work
(a MIDI of a 19th-century piece is fine; a film theme is not). Say where it comes from in
spec.json(author,comment), in aSOURCE.mdnext tosource.mid, and inCREDITS.md. A transcription has a licence of its own, and the arrangement inherits it. The sample game's theme is Tchaikovsky's Swan Lake (public domain), its notes written out for the project by a script (games/demo/audio/projects/swan-theme/compose.pywritessource.mid): no transcription to inherit from, so it is CC BY 4.0 and a game may be sold with it (3.7; the theme before it came from a CC BY-NC MIDI). Licences in general: Licences and credits. -
One palette.
tools/audio/palette.jsonholds the FM patches, the PSG envelopes, the DAC drum kit and the mix targets. Every project and every sound effect builds from it. A missing sound is a new patch in the palette (with adesc), never an ad-hoc patch in one spec; a patch that shipped tracks rely on is not edited (add one, or bumpversion). - Re-orchestrate, never convert track by track. Keep the melody, the harmony that matters, the structure and the tempo; reduce the polyphony on purpose (merge doublings, drop inner voices, turn held chords into arpeggios).
-
A build is done when the QA report says
ISSUES: none. The report measures levels against the palette's targets, pitch accuracy and clipping. -
The rendered files are committed (
audio/music/*.mp3,audio/sfx/*.mp3, the project'sspec.json,source.mid,analysis.md,qa.txt); the heavy intermediates are not (out/*.wav,reference.wav).
Limit ·
qa-does-not-listenThe QA report measures; it does not listen. A build atISSUES: nonecan still sound wrong: say so, and tell the person what to check by ear (lead and bass balance, the echo level, the stereo split).
-
Ingest. Copy the source into a new project:
# in the engine repository npm run audio -- ingest song.mid games/<id>/audio/projects/<slug>
It copies the MIDI to
source.mid, writesanalysis.mdandanalysis.json(tracks, ranges, polyphony, detected doublings, a bar-by-bar activity map that shows the sections, tempo and meter changes), aspec.jsonskeleton (ten channels with their default roles, and a drum map when a track is drums), and rendersreference.wavwith a GM SoundFont for A/B listening.
-
Analyse. Read
analysis.mdfully. Decide what is essential (melody, bass, defining riffs, harmonic rhythm) and what can go. A build does not handle tempo changes: cut the piece at a tempo change (bars), or make one project per section.npm run audio -- analyze source.mid --dump Nlists the notes of track N bar by bar.
-
Arrange: write
spec.json. The default channel plan, to adapt and justify:Channel Role ( role= QA target)Typical content FM1 lead the melody ( lead_brass,synth_lead,bright_strings)FM2 double, pan L the melody an octave away on a contrasting patch, detuned ( fx_init: [["E5", "86"]])FM3 bass the lowest voice ( slap_bass,finger_bass)FM4 harmony horn / countermelody / chord voice; the patch can change per section FM5 counter, pan R a secondary line, a chord tone FM6 drums (DAC) kick / snare / toms; an FM part where there are no drums PSG1 echo the melody delayed delay_rows: 4at vol ~8PSG2 arp type: arp(00 7Cpower arps,00 37/00 47minor / major)PSG3 accent brass bite +12, a chord tone NOISE noise hats, open hat, crash GM family → palette: brass →
lead_brass; horn / trombone / tuba →horn_section; strings / pads →bright_stringsorsoft_pad; piano →e_piano; mallets / bells →bell; organ →organ(a soft drawbar, for chords) orchurch_organ(a full pipe organ that carries a lead); distorted guitar →power_guitar(feed it the low octave: it adds the octave and the fifth); clean guitar →muted_pluck; bass →slap_bass(busy) orfinger_bass(sustained); synth lead →synth_lead; orchestra hits →orch_hit.The fields are in The spec reference below;
games/demo/audio/projects/swan-theme/spec.jsonis a complete example.
-
Build, render, check, iterate.
# in the engine repository npm run audio -- all games/<id>/audio/projects/<slug>/spec.json
It writes
out/<slug>.fur(editable in Furnace),.wav,.vgm(emulators, real hardware players),.mp3(−14 LUFS),<slug>_patterns.txtandqa.txt. Fix whatISSUESlists and build again: a level off target → the part'svol(FM: one step ≈ 0.75 dB at the top of the range, more lower down; PSG: one step = 2 dB); a low pitch match → a wrongtranspose, two parts overlapping on one channel, orvoicepicking the wrong line;note collision→ two notes on one row.
-
Deliver. Copy
out/<slug>.mp3togames/<id>/audio/music/<id>.mp3, declare it inaudio.music, play it (titleScreen.music, a room'smusic, amusiccommand), then ship it:# in the engine repository npm run assetsTell the person which channel plays what, which liberties the arrangement took, and what to check by ear.
- Top level:
title,author,comment,slug,source,bars(how many bars to build),rows_per_beat(8 = 32nd-note rows),pattern_rows(64),hz(60),extra_bars(ring-out),stop_at_end,sections{ name: [first, last] },bpm(override),stems(see Stems). -
channels.<CH>:name,role,pan(L/R/C, FM only),fx_init[["E5", "86"]],fx[["bar:beat", "04", "00"]],parts. - A part:
track(an index, a name fragment, or a list to merge),voice(high/low/latest/rankN),bars([a, b], a section name, or a list of either) orfrom/to("bar:beat"),exclude_bars,ins(palette key),vol(FM 0–127, PSG 0–15),weak_vol(off-beat accent),accent_long,transpose,delay_rows,vibrato(hex"34", on notes ≥ 1.5 beats),offs. - An arp part:
type: "arp",root_track,voice,bars,every_rows,transpose,arp(hex xy),ins,vol,weak_vol. -
drums:track,dac,noise,accent: "beat",map(GM note →kick/snare/ghost/tom:G3/hat/ohat/crash/noise_snare),hat_fill,extra_crash,noise_vols. -
extras: single notes for endings and stabs{ ch, at: "bar:beat", note: "C5", beats, ins, vol }.
The hardware facts the builder handles for you (Furnace's PSG octave, the DAC without volume) are on Audio toolchain.
A .wav, .mp3, .flac (or .ogg, .m4a, .aac, .aif, .aiff, .opus) is first transcribed to MIDI by
basic-pitch. It needs a Python 3.11 virtual environment, made once:
# in the engine repository
uv venv --python 3.11 tools/audio/.venv && uv pip install --python tools/audio/.venv/bin/python -r tools/audio/requirements-audio.txtDemucs stem separation is optional and heavy (transcribe --no-separate skips it). The result is a draft: keep the
melody, the bass and the chord roots, delete ghost notes, check the tempo (--bpm), rewrite the drums as a pattern.
Prefer a MIDI when one exists.
A track of audio.music can also have a score: its stems, played together by the music director, the mix
following the game: it changes with the room, a flag or the active character (audio.scores).
# in the engine repository
npm run audio -- stems games/<id>/audio/projects/<slug>/spec.jsonIt renders the arrangement channel by channel (Furnace's per-channel output) and sums the channels into stems: the
spec's "stems": { "<name>": ["FM1", "PSG1"], … }, or, without it, by channel role (melody: lead and echo; harmony:
double, harmony, counter, accent, arp; bass; drums: the DAC and the noise). Every stem comes from the same render, so
all have the same length and start, and their sum is the mix. One gain, the same for every stem, brings the sum to
−14 LUFS: never a per-stem normalisation, the balance is the arrangement's. It writes
games/<id>/audio/music/<slug>-stems/<stem>.mp3 (or --out <dir>) and a score.json to paste under audio.scores,
by the id of the single mix. The tempo in it is measured from the render (bars × beats over its length); pcmBytes
is the stems' decoded weight at 48 kHz.
Group channels by what the game will switch on and off: the sample game's theme has melody, strings, harp and bass, and plays only the harp and the bass while Biscuit is active. A stem that is nearly silent (a noise channel alone, −54 LUFS) is a wasted download: fold it into another one.
Stems made by hand are welcome; npm run validate -- --release measures them with ffprobe (3.6). Every stem must have
the same rate, channels and number of samples, the loop must end inside them, and pcmBytes must be what they decode
to (1 % of slack for an encoder's padding).
Guarantee ·
stems-consistent· referenced · executed on301f199dc2e3· scope reviewed The stem check reports stems that would drift apart: a different rate, number of channels or length, each mismatch named.
audio: {
music: { theme: 'swan_theme.mp3' }, // the single mix: Save-Data, low-end devices, no Web Audio
scores: {
theme: {
stems: { melody: 'swan-theme-stems/melody.mp3', strings: 'swan-theme-stems/strings.mp3', harp: 'swan-theme-stems/harp.mp3', bass: 'swan-theme-stems/bass.mp3' },
bpm: 80, beatsPerBar: 4, // from score.json
states: [ // the first that holds sets the mix; none: every stem
{ if: { player: 'biscuit' }, stems: ['harp', 'bass'] },
{ if: { room: 'garden' }, stems: ['strings', 'harp', 'bass'] },
],
// quantize: 'bar' (default) or 'beat'; fadeBeats: 2; loop: [first bar, end bar)
},
},
}| Field | Form | Default |
|---|---|---|
stems |
stem id → file under audio/music/
|
required |
bpm, beatsPerBar
|
tempo; beats in a bar |
beatsPerBar 4 |
loop |
[first, end) in bars, counted from 0 |
the whole file |
states |
{ if?, stems }[], the first whose condition holds wins (no if: always) |
every stem |
quantize |
where a change of mix lands: bar or beat
|
bar |
fadeBeats |
the crossfade of a change of mix, in beats | 2 |
pcmBytes |
the stems decoded, in bytes | none |
markers |
named bars, counted from 0, where a transition may land | none |
phraseBars |
a phrase's length in bars, for at: 'phrase'
|
4 |
A state is any condition of the DSL (a flag, the room, the active character; see Conditions).
A change of state takes effect on the next bar (or beat), crossfaded; another room with the same track keeps the
music going and only changes the mix. npm run validate checks the stems, the tempo, the states' stems and
conditions, and that the single mix exists. The Studio's Music tab plays a score and each state's mix
(Studio: music).
Guarantee ·
score-first-state-wins· referenced · executed on301f199dc2e3· scope reviewed The mix is the first state whose condition holds, in the order written; when none holds, every stem plays.
From one score to another (3.6), a rule says where the old one lets the new one in, on its own grid:
audio: {
scores: { day: { …, markers: { calm: 8 }, phraseBars: 4 }, night: { … } },
transitions: [
{ from: 'day', to: 'night', at: 'calm', bridge: 'dusk_sting' }, // on bar 8 of "day", then the bridge, then "night"
{ from: '*', to: 'day', at: 'phrase', fadeBeats: 2 }, // on the next phrase, crossfaded over 2 beats
],
maxDecodedMB: 160, // decoded audio kept (default 160)
}-
atisbeat,bar(default),phraseor a marker of the old score, counted through its loop. A marker the loop never reaches again falls back to the next bar. -
bridgeis a track ofaudio.music, played once between the two;fadeBeatsdefaults to 0 (on the downbeat). - The first rule naming both scores (or
*) applies:validaterefuses a rule an earlier one covers, and a marker from'*'that a score it can leave lacks. - Without a rule, a score replaces another at once, faded, as in 3.5.
- A save keeps where the music is; loading it resumes there, cut, never through a transition (3.6.1).
-
maxDecodedMB(default 160): past it, the director lets go of the scores played least recently, and a score larger on its own plays as its single mix.
Limit ·
not-imuseThis is an adaptive stem mixer with transitions, not iMUSE: no tempo changes and no branches inside a score. The single mix (no director) ignores transitions.
The single mix in audio.music always stays: the director plays the stems only where they fit
(src/engine/dom/director.ts, directorFits):
- no Web Audio (
AudioContext): the single mix; - the browser asks to save data (Save-Data): the single mix;
- a low-end device (2 GB of memory or less, or 2 cores or less): the single mix;
- a browser that does not tell its memory (Safari, iOS included): the stems only when every score declares
pcmBytesand the largest is at most 128 MB, otherwise the single mix.
No music at all plays before the player's first tap, click or key press: browsers refuse sound before a gesture. A track asked for earlier starts at that first gesture.
The Studio's Assets tab lists the music and sound ids a game declares and whether a file exists for each;
npm run assets ships what is there and reports what is missing. The Music tab is described on
Studio: music. The MCP read_doc tool served the old page of this subject as AUDIO.
A loop per room, set with music on the room: short and quiet, since the player hears it for fifteen minutes. The
sample game has none yet and says so in game.ts: silence is better than a bad loop. The rest of the sound design
(an effect on every change of state, the hint voice) is on Puzzle design.
-
qa.txtingames/<id>/audio/projects/<slug>/out/ends withISSUES: none. -
npm run assetsreports no missingmusic:file;npm run validatereports noaudio.scoreserror. - In the game, the track plays after your first tap; with a score, the mix changes on the next bar when the state changes.
| Symptom | Cause | Fix |
|---|---|---|
| "Furnace not found: run `python -m mdpipe setup` or set FURNACE_BIN" | Furnace not installed |
npm run audio -- setup, or FURNACE_BIN=<path>
|
unsupported source type .xyz |
ingest takes a MIDI or an audio file only |
Convert the source to MIDI or WAV |
ISSUES lists a level off target |
A part's vol
|
Change vol (FM ≈ 0.75 dB a step at the top, PSG 2 dB) |
note collision |
Two notes on one row of one channel | Split the parts, or change voice
|
audio.scores.<id>.stems.<s> › "<file>" is missing or not readable audio (validate --release) |
A stem file missing or broken | Render the stems again, or fix the path |
| The music never starts | No gesture yet, or music off in the pause menu | Tap the screen; check the Music setting |
games/<id>/audio/projects/<slug>/ (source.mid, analysis.md, analysis.json, spec.json, out/, qa.txt),
games/<id>/audio/music/ (the .mp3, the <slug>-stems/ folder), game.ts (audio), and public/assets/audio/
with games/<id>/assets.gen.json after npm run assets. Commit the rendered .mp3, spec.json, source.mid,
analysis.md and qa.txt; not out/*.wav nor reference.wav.
A build overwrites out/ only; the previous track stays in audio/music/ until you copy the new one over it. Git
holds the committed versions: git checkout -- games/<id>/audio/music/<file>.mp3 brings one back.
-
tools/audio/mdpipe/__main__.py,tools/audio/mdpipe/stems.py,tools/audio/palette.json -
src/engine/core/types/audio.ts,src/engine/core/score.ts,src/engine/tools/stems.ts - Sound effects, Voices, Audio toolchain
Generated publication · publication générée — current documentation · documentation courante: quest'n'click 4.5.5 r1, documentation revision · révision de la documentation 301f199dc2e38aa35a552a22ef87fd1e0f1f50f7.
Latest documentation · dernière documentation: https://quest-n-click.cleverapps.io/docs · https://quest-n-click.cleverapps.io/fr/docs; earlier versions · versions précédentes: each release's quest-n-click-docs-<version>.tar.gz · l'archive quest-n-click-docs-<version>.tar.gz de chaque release, https://github.com/Quest-n-Click/quest-n-click/releases
An edit made in this wiki is overwritten at the next publication · une modification faite dans ce wiki est écrasée à la publication suivante.
Edit the source · Modifier la source · Report an error · Signaler une erreur
Index (6)
Start (6)
Play (9)
Install (18)
- Requirements
- Install from the release archive
- Install from npm
- Install from the repository to work on the engine
- doctor: checking your machine
- Image toolchain
- Audio toolchain: ffmpeg, fluidsynth, Furnace, uv
- Upgrade
- Upgrade notes: 4.5
- Upgrade notes: 4.4
- Upgrade notes: 4.3
- Upgrade notes: 4.2
- Upgrade notes: 4.1
- Upgrade notes: v3
- Upgrade notes: v2 to v3
- Coming from web-scumm
- Package reference
- Install troubleshooting
Create a game (55)
- Your first game
- Layout of a created project and of the engine repository
- The working loop
- Content principles and the identity contract
- Rooms
- Geometry: placement, walk zones, masks, layers, camera
- Something is visible but unreachable
- Reactions
- Game commands
- Conditions
- Flags, variables and events: which to use
- Custom code
- Characters
- Items and inventory
- World map
- Dialogues
- Scripts and events
- Moving characters: patrols and walks
- Exits
- Chapters and objectives
- State and saves: persistent or temporary, renaming an id, migrations
- Staging: wide rooms and animations
- The stage: lights, particles, floors
- Skin and fonts
- Title and credits
- Heroes: several characters and hero choice
- Built-in minigames
- A custom minigame
- Translations
- Music in stems
- Sound effects
- Voices: per language and per hero
- Images: prompts, cutting sheets, transparency, scale
- Asset provenance
- The storyboard, source of truth
- Puzzle design
- Classic mechanics
- Review pages
- DSL reference
- Checking your game
- What the solver proves
- Solver options
- Playtests
- Remix
- Speedrun for authors
- The sealed ending
- Production template
- Example games
- Working with an AI
- Build and release a game
- Publish on GitHub Pages
- The static game on Clever Cloud
- Other static hosts
- Sub-path, cache and service worker, incomplete deploys, going back
- Testing on a phone over the LAN
Local Studio (24)
- The local Studio: overview and launch
- Edit a room
- Placement and stage
- Preview, apply, undo
- Storyboard
- Assets and cutting
- Check: validator, solver, critical path, heat, health, coverage
- Before and after captures
- Semantic diff
- Play and replay a session
- Rule explainer
- Language and objectives
- Voices and the heroes × languages matrix
- Map
- Notes
- Remix in the Studio
- Music
- The Assistant: bring your own key
- Demo mode and studio-apply
- Local and LAN security
- Studio API
- Local MCP: setting up each client
- MCP tools
- Studio troubleshooting
Team Studio (21)
- What runs where
- Concepts: revision, transaction, span, lease, preview, build
- Starting as an invited dev
- Working together day to day
- Roles and actions: the full matrix
- Invitations
- Managing members
- Write refused on the hosted Studio: stale base revision (409)
- Write refused on the hosted Studio: span locked (423)
- Recovering after a lost lease, a disconnection or a stale preview
- Comments
- Map and semantic diff
- Publishing builds
- Remote MCP
- The game in Git
- Upgrading the engine of a hosted game
- Hosted assets
- The server's command line
- Health and metrics routes
- Trying it locally with compose.yml
- Known limits
Clever Cloud (23)
- Clever Cloud: overview, cost and prerequisites
- Apps and add-ons
- Domains and TLS
- Keycloak
- Generic OIDC and GitLab
- Configuration
- First deployment
- Backups
- RPO and RTO
- Restore the Studio on a fresh installation
- Redeploys
- Upgrading the server and checking state after a migration
- Limits of going back
- Monitoring
- Security
- Rotating keys
- Recovering admin access
- Changing domain or OIDC provider
- Worker, PostgreSQL or Cellar down
- Exporting the game and leaving the installation
- Retention
- Clever Cloud troubleshooting
- Costs and teardown
Reality Bridge and connectors (16)
- The Reality Bridge: what it is
- Quick start
- Guarantees
- Install and run the Bridge
- Backup and restore
- Several tenants
- Signals and the manifest
- The Bridge's command line
- Connectors overview
- Email connector
- Telnet connector
- SSH connector
- Open Badges connector
- The connector SDK
- Security: Biscuit and signatures
- Bridge and connector troubleshooting
Contribute (25)
- Welcome
- Developer setup
- The dev loop
- Before a pull request
- Conventions
- Writing documentation
- Architecture
- Code tour
- Import boundaries
- Which tests run when
- Mutation testing
- The coverage ratchet
- The quality baseline
- CI explained
- Field passes
- Benchmarks
- Releasing
- npm packages
- Decisions and ADRs
- Governance
- Bot pull requests
- Contributing with AI agents
- Security policy
- Code of conduct
- Licences and credits