@homie-rocks/studio 0.49.0 → 0.50.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/bin/homie-studio.mjs +5 -5
  3. package/functions/FUNCTIONS.md +71 -1
  4. package/guides/app/SKILL.md +130 -1
  5. package/guides/art/SKILL.md +154 -0
  6. package/guides/art/references/RIGHTS.md +20 -0
  7. package/guides/art/scripts/art.mjs +308 -0
  8. package/guides/models/SKILL.md +198 -0
  9. package/guides/models/references/RIGHTS.md +46 -0
  10. package/guides/models/references/models.json +175 -0
  11. package/guides/models/scripts/models.mjs +483 -0
  12. package/guides/music/SKILL.md +197 -0
  13. package/guides/music/references/COMPOSITION.md +59 -0
  14. package/guides/music/references/GAME-AUDIO.md +50 -0
  15. package/guides/music/references/RIGHTS.md +43 -0
  16. package/guides/music/scripts/lib/audio.mjs +258 -0
  17. package/guides/music/scripts/lib/eleven.mjs +260 -0
  18. package/guides/music/scripts/lib/studio.mjs +147 -0
  19. package/guides/music/scripts/music.mjs +644 -0
  20. package/guides/office/SKILL.md +99 -0
  21. package/guides/plan/SKILL.md +126 -0
  22. package/guides/plan/references/CODEX.md +105 -0
  23. package/guides/plan/references/INTERVIEW.md +86 -0
  24. package/guides/playtest/SKILL.md +205 -0
  25. package/guides/playtest/references/METHOD.md +124 -0
  26. package/guides/playtest/references/REVIEWER.md +78 -0
  27. package/guides/playtest/scripts/lib/exit.mjs +31 -0
  28. package/guides/playtest/scripts/lib/judge.mjs +502 -0
  29. package/guides/playtest/scripts/lib/pixels.mjs +128 -0
  30. package/guides/playtest/scripts/lib/preflight.mjs +69 -0
  31. package/guides/playtest/scripts/playtest.mjs +870 -0
  32. package/guides/servers/SKILL.md +111 -0
  33. package/guides/shop/SKILL.md +4 -0
  34. package/guides/sound/SKILL.md +156 -0
  35. package/guides/sound/references/GAME-SOUND.md +100 -0
  36. package/guides/sound/references/SCORE.md +131 -0
  37. package/guides/sound/references/TRAPS.md +93 -0
  38. package/guides/sound/scripts/lib/measure.mjs +252 -0
  39. package/guides/sound/scripts/lib/score.mjs +415 -0
  40. package/guides/sound/scripts/lib/sfx.mjs +222 -0
  41. package/guides/sound/scripts/lib/synth.mjs +509 -0
  42. package/guides/sound/scripts/sound.mjs +520 -0
  43. package/guides/style/SKILL.md +105 -0
  44. package/lib/app-needs.mjs +63 -0
  45. package/lib/check.mjs +10 -4
  46. package/lib/codex.mjs +95 -18
  47. package/lib/decisions.mjs +9 -7
  48. package/lib/functions-build.mjs +10 -0
  49. package/lib/handoff.mjs +1 -1
  50. package/lib/mcp-tools.mjs +10 -9
  51. package/lib/mcp.mjs +1 -1
  52. package/lib/scaffold.mjs +15 -7
  53. package/lib/studio.mjs +3 -3
  54. package/lib/template-history.json +84 -0
  55. package/lib/worker-config.mjs +1 -1
  56. package/package.json +1 -1
  57. package/shop/SHOP.md +4 -0
  58. package/shop/shop.ts +4 -4
  59. package/tools/TOOLS.md +21 -0
  60. package/tools/tools.ts +1 -1
  61. package/worker/build-id.mjs +1 -1
  62. package/worker/event-resources.mjs +116 -0
  63. package/worker/functions-schema.mjs +15 -0
  64. package/worker/functions.mjs +36 -22
  65. package/worker/index.mjs +23 -3
  66. package/worker/mcp-tools.mjs +14 -3
  67. package/worker/mcp.mjs +45 -8
  68. package/worker/shop-page.mjs +1 -1
  69. package/worker/shop.mjs +6 -2
  70. package/worker/version.mjs +1 -1
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: servers
3
+ description: Give a Homie studio game lasting servers - named communities with their own rooms and rules - humans-only, hybrid (seats reserved for AI companions), beginner (new players, AI guides, kids-safe) - plus agent passes, the AI skill dial, the party's level vote, and AI guides that talk (a game vocabulary; Cloudflare's Clef decision model on the studio's Workers AI by default, Llama or the owner's key instead, Clef on the person's own computer under dev, or the owner's own Claude in a seat). Use when someone asks for servers, realms or shards, human-only play, AI party members or companions, guides or helpers for new players or kids, guides that answer "help me with this quest", "fill empty spots with AI", letting Claude play as a guide, which model the guides think with, testing an agents.json, free local AI guides, or how strong the bots should be.
4
+ compatibility: The studio's own Cloudflare account, with Workers AI through its Worker (Clef by default), Wrangler for the model catalogue and the doctor's probe; optionally Ollama with clef-flash on the person's own computer, downloaded only after their yes.
5
+ metadata:
6
+ providers: cloudflare ollama
7
+ ---
8
+
9
+ # Servers and AI seats
10
+
11
+ Needs `@homie-rocks/studio` 0.16.0 or later (`npx --no-install homie-studio upgrade --apply`, then deploy); guides
12
+ that talk need 0.17.0; Clef, `agents try` and Clef on the person's own computer need 0.24.4.
13
+ Everything lives in the studio's own Worker and D1; homie.rocks stores none of it. **AI is ALWAYS marked AI**:
14
+ the relay names every agent "<label> · AI" and marks it in every roster and result. Never name an agent so it looks
15
+ human, and never try to get an agent into a humans-only server.
16
+
17
+ A **server** is a named, lasting pool of rooms for one game: strangers are matched only inside one server. Every
18
+ game already has one, **Quick play** (its public rooms). A server's page is `/<game>/s/<id>/`; `/<game>/servers/`
19
+ lists them; the landing gets a Servers band.
20
+
21
+ For new or rewritten rules games, read the game skill's RULES.md: `guide.view`, `guide.floor`,
22
+ `think` and `goalDone` replace browser-hosted `useAgents`; declared asks replace `net.decide`.
23
+ Use `world.level`, `world.guideLevel`, `world.guideSeats` and `world.kids` for the server policy.
24
+ A floor must play without a model. The legacy recipes below are only for unchanged older games.
25
+ Rooms currently hold at most 32 seats; a named server is a pool of rooms, not a larger room.
26
+
27
+ ## Do
28
+
29
+ | The person says | Run (or the MCP tool) | What happens |
30
+ |---|---|---|
31
+ | "Make a beginner server with two guide agents" | `npx --no-install homie-studio servers new <game> "<Name>" --policy beginner --guides 2` (`server_create`) | At once: the server and its page link. Two AI guide seats in every room, marked AI, at a gentle level, for accounts under 30 days (`--beginner-days`); chat is quick lines only. `--kids` adds handles only and keeps the AI's level at most 3. With no brain the guides play from the game's script, silent; to make them talk see **Guides that talk** below. |
32
+ | "A server with no AI at all" | `servers new <game> "<Name>" --policy humans-only` | No agent can join (the site refuses it, and so does the room); the game's practice bots are off (`--bots fill` turns them on, still marked AI). |
33
+ | "Keep two AI seats in every party" | `servers new <game> "<Name>" --policy hybrid --ai 2` | The top two seats of every room are AI companions; the party votes their level. A room of 8 holds 6 people. |
34
+ | "Make the bots easier / harder" | `servers set <game> <server> --level 2` (the server's default), `servers level <game> <room> 1-5` (one room now; `room_level`) | The dial: 1 Rookie, 2 Steady, 3 Fair, 4 Strong, 5 Maxed. Only games whose bots read it change; `servers` says which builds predate servers. `--level-max` caps what the party may vote. |
35
+ | "Only players with an account" / "invite-only server" | `servers set <game> <server> --door accounts` / `--door invite`, then `office invite <game> --server <server>` | A stricter door is an ASK (one tap). Invite codes and links for that server. |
36
+ | "Close that server" | `servers close <game> <server>` (`server_close`) | ASKED; its rooms finish their round, then everyone leaves with a thank-you. `--reopen` opens it again at once. Closing `public` hides Quick play: Play then shows the servers to pick from. |
37
+ | "Make <player> a mentor" | `servers member <game> <server> <player id> --role mentor` (`server_member`) | At once: a mentor gets into a beginner server, with a badge. `--remove` is an ASK. |
38
+ | "Can we sell things on this server?" / "a shop for the kids server" | nothing to run: the shop skill (`shop.json`) | A kids server (`--kids`) never shows a shop, takes no purchase and shows no supporter badges; a beginner server never offers, or counts as owned, an item with `"advantage": true`. Every other server shows the studio's shop sheet from the room button and the game's `shop.open()`. These are the protective shop preset defaults; the studio can edit policy in shop.json. |
39
+ | "Let my Claude play" | `agents pass <game> --label Claude [--server <id>]` (`agent_pass`) | At once: a pass shown ONCE (give it to the AI only; never write it into the repository or a chat). The AI sits with `POST /<game>/api/agent` (`Authorization: Bearer <pass>`), only in a room with people in it, as "Claude · AI", on an open, hybrid or beginner server. `agents revoke <id>` ends it (the AI leaves at once). |
40
+
41
+ ## Guides that talk (0.17.0)
42
+
43
+ A beginner server's guides get a **brain** that picks a goal every few seconds and, when the owner allows it, a
44
+ line to say: only the game's own goals and lines, from `games/<game>/agents.json` (its **vocabulary**). The game's
45
+ bot code is the guides' **hands**: they carry the goal out every frame at the party's dial. The AI never types.
46
+ A person's ask is always answered the way agents.json says, with the values they asked for (a fixed rule overrules
47
+ any other goal or value), and a house guide greets a newcomer once and otherwise stays quiet.
48
+
49
+ | The person says | Do | What happens |
50
+ |---|---|---|
51
+ | "Make the guides talk" / "guides that help with quests" | 1. If `games/<game>/agents.json` is missing, write it with the person, in the game's own voice (the game skill's "Write the guide vocabulary"; `NETPLAY.md` section 18), and the game's `useAgents` (Ember Vale is the reference); build. 2. `agents brain <game> <server> workers-ai` (`agents_brain`) | The first time talk is turned on it is an ASK: give the owner the link. Then `npm run deploy` once (it binds Workers AI), and `npx --no-install homie-studio doctor`: its Workers AI row checks the model answers on the account (one tiny call), and names a model Cloudflare moved to Workers Paid or retired, with the fix. Guides think with Cloudflare's Clef decision model on the studio's own Workers AI (`@cf/cloudflare/clef-flash`, about 9 neurons a decision), at most 8,000 neurons a day for the whole studio (about 900 decisions; the free allocation is 10,000 an account; `--budget <neurons>`), then the game's script until 00:00 UTC. |
52
+ | "Use my own Claude key for the guides" | `agents brain <game> <server> owner-key --budget 1`, only when a key is already configured; otherwise use the Cloudflare brain through the existing sign-in | Never ask the person to find or type a key; **never ask for a key in the chat**. claude-haiku-4-5, about $0.30 a busy guide-hour, capped by `--budget` dollars a day (raising the cap is an ASK). |
53
+ | "Guides should just play, quietly" / "AI talk off" | `agents brain <game> <server> script` | At once: every AI on the server is silent; the guides play from the game's script. |
54
+ | "Let my Claude be a guide" | the local MCP's `agent_sit { game, server }` (or `homie-studio agents sit <game> --server <id>` in a terminal) | Claude takes a guide's seat as "Claude · AI" in a room with people in it, makes way for nobody (a house guide yields to it), and decides with `agent_look` / `agent_do` about every 30 s while the game's hands play. `agent_stand` leaves; its one-day pass is revoked. |
55
+ | "A guide that thinks on my computer" / "free AI guide" | `agent_sit { game, server, brain: "local" }` (or `agents sit <game> --server <id> --brain local`) | Clef on this computer (Ollama with `clef-flash`) decides every few seconds, free, nothing sent to Cloudflare; `agent_look` shows its last decisions and `agent_do` overrides one. With no `clef-flash` it says what the download is: ask first (below). |
56
+ | "Does my agents.json work?" / "which model is better?" | `agents try <game> --view <file.json> [--ask <ask>[:<arg>=<value>] --from <seat>] [--model <id>]` | The studio's guide brain on one moment, no seat taken: the decision, the brain's own pick before the fixed rules, Clef's probabilities, the model's ms and the neurons, spent from the guides' day. `--model` compares one (Llama, the 27B Clef) before setting it. |
57
+ | "What are the guides doing?" / "how much AI did we use?" | `servers`, the office (`/_studio/office`) | Today's neurons or dollars against the budget, why a guide is scripted (no binding, no key, budget spent), and each room's last decisions (the model, how sure Clef was, and the brain's own pick when a rule overruled it). |
58
+
59
+ **The model.** Clef is the default because it answers each decision as typed Choices (a goal, its values, a line or
60
+ none) and never writes text: on 64 recorded Ember Vale moments it answered 36 of 40 open asks itself (Llama 3.1 8B: 0),
61
+ and blind judges preferred it 51 to 0 (13 ties), at about 9 neurons a decision to Llama's 4. `HOMIE_BRAIN_MODEL` in
62
+ `wrangler.jsonc` vars picks `@cf/cloudflare/clef` (the 27B) or `@cf/meta/llama-3.1-8b-instruct-fp8-fast` (the old
63
+ default); try one with `agents try --model` first. **Which Workers AI models exist now** is Cloudflare's to say, not
64
+ memory: `npx wrangler ai models list --json` and `npx wrangler ai models schema <model>` (free; the studio's Wrangler,
65
+ signed in). Pick one that is not marked Workers Paid; the doctor's Workers AI row then proves it answers on this account.
66
+ **Under `homie-studio dev`** guides, chat review and game decisions think with Clef on this computer when Ollama (0.35.1 or
67
+ later) has `clef-flash` (free, nothing sent to Cloudflare; `dev --no-local-ai` turns it off), else from the script;
68
+ `dev --remote-ai` uses the real Workers AI (billed). The doctor's "Clef on this computer" row says what is there.
69
+ **Ask first.** Nothing downloads a model by itself: before `ollama pull clef-flash`, tell the person it downloads about
70
+ 11 GB (`clef`, the 27B, about 18 GB) and wait for their yes. Never pull it to make a test pass; the Homie mod (Claude Code) and
71
+ Homie's hooks (Codex) hold the pull until they say Proceed.
72
+ **Adopting guides in an existing RPG** (zones, quests, a party): `node_modules/@homie-rocks/studio/agents/GUIDES.md`
73
+ is the short path, in the game's own repository and with its owner's say.
74
+
75
+ **Asks.** Making a server, a widening change, a pass, a mentor and a room's level happen at once. A change that
76
+ narrows who may come in (humans-only, a stricter door, fewer rooms), closing a server, removing a member, and the
77
+ first time AI guides may talk only ASK: the command prints a one-time link the owner opens; one tap does it. You
78
+ cannot confirm it.
79
+
80
+ **Tell the person plainly** which builds predate servers (`servers` and the office say "This build predates
81
+ servers"): such a game still plays on every server (doors, the people's cap, speech rules and AI names work), but
82
+ it keeps no AI seats and its bots ignore the dial until it is rebuilt with 0.16.0. A game whose bots are its own
83
+ code needs a few lines: the dial snippet in `node_modules/@homie-rocks/studio/netplay/NETPLAY.md` section 17
84
+ (`net.skillOf(slot)`, and `caps: ['skill', 'agents']`); a port on `BotBrain` gets it with `skill: () =>
85
+ room.skillOf(body)`.
86
+
87
+ **Money.** A paid "fill a spot" service (an AI seated in an empty spot for a price) is not offered: the office shows
88
+ it as coming later, and nothing issues its passes. Pricing it is the studio owner's own decision, never yours.
89
+
90
+
91
+ ## Tell my AI when a booking comes in
92
+
93
+ Read `node_modules/@homie-rocks/studio/functions/FUNCTIONS.md` for the worked
94
+ booking example and delivery expectations. Opt in with
95
+ `mcp.events.bookings: {event:"record.changed", app:"cafe", collection:"bookings", audience:"owner"}`
96
+ in `studio.json` (use the studio's real app and collection), then build. The owner's
97
+ AI connects to the studio's `/mcp` with explicit OAuth browser approval, calls
98
+ `resources/subscribe` for `studio://events/bookings`, and holds the authenticated
99
+ Streamable HTTP SSE GET. Notifications use `notifications/resources/updated`;
100
+ `params._meta["studio/event"]` carries untrusted event data. Filter booking inserts,
101
+ deduplicate by event ID, and resume with `Last-Event-ID`. No polling or Homie relay.
102
+ Nothing is enabled by default; a declared feed retains its events without a Homie
103
+ count or retention cap. This release does not include outbound webhooks.
104
+
105
+ Use owner tool `app_role` to grant/revoke staff by verified email or exact sign-in
106
+ name (`signIn`, `app`, `role`, `revoke`); the person signs in first. Staff event
107
+ feeds need the explicit app/role audience and collection read permission. Grants
108
+ are checked again for live delivery and replay. `app_records` supports deletion
109
+ with `operation:"delete"` and the current `version` where the role allows it.
110
+ Reload on the returned conflict message. An office owner session and an
111
+ owner-marked player account both authorize owner MCP access.
@@ -147,3 +147,7 @@ Keyless has no per-session expiry or provider-read reconciliation. The keyed pat
147
147
  carts, cap reservations, local refunds. Managed Payments is supported on Payment Links when Stripe enables it for the account. The studio chooses its needs.
148
148
 
149
149
  For a cause, use `shop init --giving --currency <organisation currency>`: one-time and monthly suggested gifts, no badge or game access. Read `giving.receiptText` from the organisation; leave it empty until provided. Local /shop/ previews before Stripe. Monthly gifts use the existing keyless Payment Links connection and Stripe handles renewals.
150
+
151
+ ## App payment references
152
+
153
+ Create the booking/ticket/gift intention first. `shop.buy('full-tune', 1, undefined, { reference: booking.id })` or `shop.checkout({ reference: booking.id })` sends the app's own reference through checkout. Existing boolean returns are unchanged: checkout opened is not paid. The verified durable `order.paid` event carries `data.order`, `data.reference` (null if omitted) and `data.app`. Reopened Payment Links retain the reference. A reference is buyer-supplied correlation, not authorization: verify the record, app, amount and currency, deduplicate by event/order ID, then fulfil it. Keep personal data out of references. Before Stripe is connected, local prices and unpaid requests can be demonstrated; actual payment and recurring collection remain unverified. Test events must be labelled simulations.
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: sound
3
+ description: Make a game's sound effects and synthesized music on the creator's own computer, free, with no provider or account — a set of effects (jump, coin, hit, explosion, win, lose, UI) rendered from presets, a short synthesized theme or score from chords and patterns with stems and seamless section loops, a mix of any audio files and expressions — measured for loudness, clipping, late starts and what a phone speaker loses, then wired into the game with a small player that starts on the first touch and switches music on bar lines. Use when someone asks for sound effects, SFX, game audio, a chiptune or synth theme, background music without a paid service, music that changes with the game, or says a game is silent.
4
+ ---
5
+
6
+ # Sound for a studio's game
7
+
8
+ Everything here runs on this computer with Node 22 and ffmpeg: nothing is sent anywhere and nothing
9
+ costs money. Sung songs and produced music from a model are the `music` skill (ElevenLabs, paid);
10
+ this skill is effects and synthesized music. They share the studio's `music/` folder and manifest, so
11
+ a synthesized theme gets a song page just like a rendered one.
12
+
13
+ One script: `scripts/sound.mjs` in this skill's folder (Claude Code:
14
+ `node "${CLAUDE_PLUGIN_ROOT}/skills/sound/scripts/sound.mjs" <command>`), run from inside the studio
15
+ (the folder with `studio.json`). Every command takes `--json`.
16
+
17
+ ```sh
18
+ node <sound.mjs> check # node, ffmpeg and its encoders, the studio and its games
19
+ node <sound.mjs> presets # every effect preset, instrument, groove and pattern, one line each
20
+ ```
21
+
22
+ No ffmpeg: offer `brew install ffmpeg` (macOS) or the system package; the person approves.
23
+ Finish in this turn: an effect set takes seconds, a one-minute score under a minute.
24
+
25
+ ## 1. Effects
26
+
27
+ ```sh
28
+ node <sound.mjs> sfx <set> --kit arcade --for-game <id> # a kit: arcade, platformer, shooter, party, ui
29
+ node <sound.mjs> sfx <set> --only jump,coin,hit,win --style retro # just these, crunchy 8-bit
30
+ node <sound.mjs> sfx <set> --spec music/<set>/effects.json # your own list (below)
31
+ ```
32
+
33
+ It writes `music/<set>/sfx/<name>-<n>.wav` and an Ogg copy of each, `music/<set>/sfx.json` (the
34
+ spec and every file's measured level), a reel to listen to (`<set>.mp3`) and its picture
35
+ (`<set>-reel.png`, a spectrogram over the waveform). **Open the picture and look**: a sound you
36
+ cannot hear is still a sound you can see. Then read the warnings; each one is something a person will
37
+ hear wrong, with the fix.
38
+
39
+ A spec is `{ "style": "clean", "variants": 3, "effects": [ { "name": "stomp", "preset": "land", "pitch": -3, "length": 1.2, "level": "big" } ] }`:
40
+ `preset` is any name from `presets` (the effect's name is used when it is one), `pitch` in
41
+ semitones, `length` a stretch, `seed` another take, `style` clean, retro or soft, `level` ui,
42
+ small, normal, big or a number in dB. Name effects after what happens in the game (`stomp`,
43
+ `gem`, `ko`), not after the preset.
44
+
45
+ - **Variants** (three by default) are the same recipe a touch higher or lower, so a sound heard fifty
46
+ times a round does not machine-gun. Signals heard once a round (win, lose, go, countdown) get one take.
47
+ - **Levels** are set by the loudest 50 ms of each effect into four classes (ui -20, small -16,
48
+ normal -12, big -9 dB), then held under -1 dBFS. The set sits together in a mix without balancing.
49
+ - **Every effect must start at once** (a hit that starts 30 ms late feels laggy) and **must reach a
50
+ phone speaker**: under about 300 Hz a phone plays almost nothing, so a thud needs a knock on top.
51
+ The warnings measure both.
52
+
53
+ ## 2. A synthesized theme or score
54
+
55
+ Write `music/<slug>/score.json` (the full language, with a worked example: `references/SCORE.md`):
56
+ tempo, key, instruments (presets with a level, pan, echo in beats, reverb send, a duck under the
57
+ kick), sections in whole bars with chords and one pattern per instrument, an arrangement, and the
58
+ sections to loop. Then:
59
+
60
+ ```sh
61
+ node <sound.mjs> score <slug> --spec music/<slug>/score.json --loops main,chase
62
+ ```
63
+
64
+ - It renders the mix (-14 LUFS for a page; `--lufs -18` for a bed under a game's effects), the page
65
+ mp3, a 24-bit master kept off the site, one stem per instrument plus the room (they sum to the
66
+ mix), and **seamless loops** of the sections you name: each is rendered three times in a row and
67
+ the middle copy kept, so reverb and echoes that ring over the loop point are already in its head.
68
+ The report gives each loop's seam (1 or less is seamless) and its exact bar length. A loop over 1
69
+ is in `warnings` and the summary says NOT SEAMLESS (`loopsSeamless: false`): an empty warnings
70
+ list means the loops were checked and pass. The master's limiter is applied across the three
71
+ copies, so a loud loop keeps its seam; nothing is faded at the loop point and no bar is shortened.
72
+ - `plan.json` holds the bar grid, so the `video` skill cuts a trailer on this music's bar lines.
73
+ - Look at `<slug>.png` and read the numbers: loudness, true peak, and what it measures through a phone
74
+ speaker. Say them in your report, not adjectives.
75
+ - Write music a person would hum: a hook in the first bars, a clear melody over simple chords, a
76
+ change every 4 or 8 bars. A score for a game is sections of the same tempo and key (calm, chase,
77
+ final seconds) so the game can move between them on a bar line.
78
+
79
+ ## 3. A mix of anything
80
+
81
+ ```sh
82
+ node <sound.mjs> mix <slug> --spec music/<slug>/mix.json
83
+ ```
84
+
85
+ Parts are audio files (a recording, a render, a loop), ffmpeg `aevalsrc` expressions (one, or two for
86
+ left and right), or effect presets, each placed (`at` seconds), levelled (`db`), panned, faded, repeated
87
+ and filtered with an ffmpeg `-af` chain. They are summed at exactly the levels written (no automatic
88
+ normalisation), limited, and brought to `master.lufs` if you give one. Expressions have traps that
89
+ fail silently: read `references/TRAPS.md` before writing one.
90
+
91
+ ## 4. Into the game
92
+
93
+ ```sh
94
+ node <sound.mjs> wire <set> <score slug> --game <id>
95
+ ```
96
+
97
+ It copies the files into the game (`public/sound/`, or `sound/` for a static game), writes
98
+ `sound.json` (what exists) and `sound.js` (the player) and prints the two lines to add. Then, in the
99
+ game's code:
100
+
101
+ ```js
102
+ import { createSound } from './sound.js';
103
+ const sound = createSound({ base: 'sound/' });
104
+ sound.music('<score slug>'); // once, at the top: it starts on the first touch
105
+ sound.play('coin'); // where the thing happens
106
+ sound.play('coin', { pitch: combo }); // a climbing combo; pan -1..1 for where it is
107
+ sound.section('chase'); // the next loop, exactly on the next bar line
108
+ sound.duck(0.4, 0.5); // music down for a big hit
109
+ ```
110
+
111
+ - **Sound starts on the first touch or key, with no "tap for sound" screen**: the player resumes audio
112
+ inside the first gesture. A sound asked for before that is dropped, not queued.
113
+ - **Every browser plays its own sounds** for what it sees: call `play` where the each browser
114
+ draw the event (a pickup that shows up in a snapshot, a hit that lands on screen), not only in the
115
+ rules handlers. In a rules game emit a declared effect and play it from room.on in the view; the server has no speaker.
116
+ - Loops go out as Ogg with a WAV fallback (older Safari cannot decode Ogg): the player takes the first
117
+ one the browser decodes. `wire` prints the real download size; keep a game's sound under about 3 MB.
118
+ - Each variant of an effect, and each loop, is a list of files tried in order (`["jump.ogg", "jump.wav"]`);
119
+ a list of one file is fine.
120
+ - A trailer filmed frame by frame gets its sound from this player's own log of what it scheduled
121
+ (off unless a recorder asks; `references/GAME-SOUND.md`, "A log for a recorder"): sound that goes
122
+ through `sound.play` and `sound.music` is in the trailer, sound made any other way is not.
123
+ - Rules for game sound (every action answers, what repeats, music under effects, intensity changes on
124
+ bar lines, the live-loop traps): `references/GAME-SOUND.md`.
125
+ - For sound made live in the browser (a synth voice per player, pitch that follows speed, a beat
126
+ clock), the open engine package `@homie-rocks/audio` has the building blocks; install it exactly
127
+ pinned (`npm install --save-exact @homie-rocks/audio@0.1.0`).
128
+
129
+ ## 5. Prove it, and publish it
130
+
131
+ Build, run the game, and let the `playtest` skill's sound row listen: it copies what the game really
132
+ sends to the speaker while it is played, and says how loud it is, whether it clips, what a phone loses,
133
+ whether actions answer with a sound, and whether the music ever started. Never say a game "has sound"
134
+ from the code alone.
135
+
136
+ ```sh
137
+ node <sound.mjs> add <slug> --title "<Title>" --blurb "<one line>" [--for-game <id>] --publish
138
+ node <sound.mjs> publish <slug> # rebuild and redeploy the site; the page answers 200
139
+ ```
140
+
141
+ An effect set gets a page with its reel and every effect to download; a score gets its player, its
142
+ loops and its stems. The credits line says they were synthesized from code (no samples, no model).
143
+ Commit `music/<slug>/` specs and the manifest; the audio files stay out of git (the studio's
144
+ `.gitignore`) and regenerate from the spec, byte for byte.
145
+
146
+ ## Tell the person
147
+
148
+ The page or the game, what was made (how many effects, the theme's length and bars), the loudness
149
+ numbers, the warnings you fixed and any you did not, and that it cost nothing.
150
+
151
+ ## Never
152
+
153
+ - Never ship a sound you have not measured and looked at.
154
+ - Never use a mp3 for an effect or a loop: encoders pad the start with silence (a late hit) and the
155
+ ends (a gap at the loop). WAV or Ogg.
156
+ - Never imitate a real song, artist or brand jingle.
@@ -0,0 +1,100 @@
1
+ # Sound in a game
2
+
3
+ What makes a web game sound finished, and the ways a correct-looking sound system goes quiet. Each
4
+ rule below was learned by a game that broke it.
5
+
6
+ ## What the game owes a player
7
+
8
+ - **Every action answers with a sound**, on the frame it happens: a press that does something makes
9
+ a sound; a press that is refused makes a different one (`deny`). Movement itself does not need a
10
+ sound per press, but landing, bumping and stopping do.
11
+ - **Sound starts on the first touch**, with no "tap for sound" screen: browsers keep audio suspended
12
+ until a gesture, so resume inside the first `pointerdown`, `touchend` or `keydown` (the player's
13
+ `sound.js`, or the port toolkit, does it). On iOS a held stick is only a gesture when the finger
14
+ lifts, so the first tap or release is when sound begins; the silent switch mutes Web Audio unless
15
+ `navigator.audioSession.type = 'playback'`.
16
+ - **Music carries the round**: calm at the start, a change when it gets tense, something for the last
17
+ ten seconds, a stinger for the result. A round that is all one intensity has no shape; silence
18
+ followed by a hit is worth more than a louder loop.
19
+ - **It sounds good on a phone speaker and on a TV.** A phone plays almost nothing under 300 Hz: every
20
+ important sound needs energy between 500 Hz and 4 kHz (a knock on a thud, an upper harmonic on a
21
+ bass). Measure it: `sound.mjs analyze` gives the loss through a phone-speaker curve.
22
+ - **Nothing clips** when many things happen at once: sounds share a bus that ends in a gentle limiter,
23
+ and each sound is capped at a few voices (`sound.js` keeps four of a name and 24 in all).
24
+
25
+ ## Levels that work
26
+
27
+ | what | level |
28
+ | --- | --- |
29
+ | music bed under play | -18 to -20 LUFS (`score --lufs -18`) |
30
+ | title or menu music | -14 LUFS |
31
+ | effects | the four classes: ui -20, small -16, normal -12, big -9 dB (loudest 50 ms) |
32
+ | a whole game while played | -16 to -12 LUFS measured off its output (the playtest's sound row) |
33
+
34
+ Duck the music for the moments that matter (`sound.duck(0.4, 0.5)` for a knockout or a line of
35
+ speech) instead of raising the effect.
36
+
37
+ ## Repetition
38
+
39
+ - A sound heard many times a round needs **variants** (three takes a touch apart in pitch) and a
40
+ little random pitch on each play; the same sample fired twenty times in a row reads as a machine gun.
41
+ - Signals the player must learn (start, win, lose, a warning) stay **the same every time**.
42
+ - A long session should not loop one 8-bar clip for ten minutes: make two or three sections and move
43
+ between them with the game's state.
44
+
45
+ ## Changing music with the game
46
+
47
+ - Every loop the same tempo and key; switch on the **next bar line**
48
+ (`next = start + ceil((now - start) / bar) * bar` on the audio clock), which `sound.section()` does.
49
+ - Use the audio clock (`AudioContext.currentTime`), not timers, to schedule anything musical. A timer
50
+ or a `requestAnimationFrame` pump skips beats it sees late, and a loaded machine makes it late: a
51
+ bar counter that rides a pump lost three quarters of its sections under load while the audio
52
+ itself played on. Work the bar out from the audio clock each time you need it.
53
+ - A live arrangement's position is `bar % total bars`: changing the total while it plays makes the
54
+ playhead jump. Change the content of a section, not the arrangement's length.
55
+ - A section that plays once as an intro and a lap that repeats are different lengths: compute the lap
56
+ from the repeating part only.
57
+ - A cue that names a sound that does not exist fails silently in most engines: check every name a
58
+ cue or a section uses against what was loaded, once, at start.
59
+ - A gain, a bus or a send changed after a sound was wired may never reach the running graph (a loader
60
+ that skips "already wired" sounds). Set levels before the first play, or rebuild the node.
61
+ - An `AudioContext` clock stalls while the machine is busy (a render, a capture), so a long set drifts
62
+ from wall time: correct its start offset, never its cues.
63
+ - A module served from cache after a change runs the old code: version its URL (`?v=2`) when you
64
+ change it on a live page.
65
+
66
+ ## Events, not counts
67
+
68
+ When sounds follow a game's event list (hits, pickups), give each event a rising sequence number and
69
+ play every event newer than the last one played, in order. A list's length is not a signal: an
70
+ expired event replaced at the same length, or two events in one frame, are lost otherwise.
71
+
72
+ ## Mixing effects into a running bed
73
+
74
+ Send one-shots (hits, chimes, slams) straight to the final bus, not through the music's filters: a hit
75
+ routed through a lowpass meant for the bed disappears into it. Bright noise belongs on the bright
76
+ one-shots only. Never raise the master to make hits stand out; lower the bed or duck it.
77
+
78
+ ## Proving it
79
+
80
+ Scheduling a node is not proof that anything is audible. Three instruments, cheapest first:
81
+
82
+ 1. `sound.mjs analyze <file>` on each asset: level, start, clipping, phone loss, and its picture.
83
+ 2. The `playtest` skill's sound row: the game's real output copied off its graph while it is played;
84
+ loudness, clipping, gaps, whether actions answer, whether music ever started.
85
+ 3. An offline render of the game's own audio graph (`OfflineAudioContext` in headless Chrome) with a
86
+ clock you can set: it finds NaN voices (a NaN anywhere silences the whole graph) and feedback loops
87
+ that double their gain, without playing anything out loud. `currentTime` stays 0 offline, so give
88
+ the game's audio a clock the render can drive.
89
+
90
+ ## A log for a recorder
91
+
92
+ `sound.js` keeps a log of everything it schedules when a recorder asks for one, and only then: the
93
+ recorder sets `window.__homieSoundCapture = { events: [] }` before the page's scripts run, and each
94
+ `play`, each music loop, each stop, duck and fader move is pushed onto `events` with `performance.now()`
95
+ and what is needed to mix it again (the file's address, gain, playback rate, pan, loop, delay). The
96
+ head of `sound.js` lists the events. The `video` skill's `trailer` films the game frame by frame and
97
+ rebuilds its sound from that log and the game's own files. A game with audio code of its own can push
98
+ the same events, and only what is in the log is in the trailer's sound.
99
+
100
+ A visual check can never hear a game. Report numbers, and look at the spectrogram.
@@ -0,0 +1,131 @@
1
+ # The score language
2
+
3
+ A score is JSON: tempo and key, instruments, sections in whole bars, an arrangement. `sound.mjs
4
+ score` renders it the same way every time (every random choice is seeded), so the score IS the
5
+ music: commit it, and the audio regenerates byte for byte.
6
+
7
+ ## A worked example (48 s, three sections, two loops)
8
+
9
+ ```json
10
+ {
11
+ "title": "Night Market Theme",
12
+ "bpm": 128, "key": "A minor", "beatsPerBar": 4, "stepsPerBeat": 4, "swing": 0,
13
+ "instruments": {
14
+ "lead": { "preset": "pulse-lead", "db": -9, "pan": 0.1, "stepsPerBeat": 2, "echo": { "beats": 0.75, "feedback": 0.3, "mix": 0.2 }, "reverb": 0.15 },
15
+ "arp": { "preset": "pluck", "db": -15, "pan": -0.35, "reverb": 0.2 },
16
+ "bass": { "preset": "saw-bass", "db": -10, "duck": 0.35, "reverb": 0 },
17
+ "pad": { "preset": "pad", "db": -17, "duck": 0.5, "reverb": 0.3 },
18
+ "drums": { "preset": "kit", "db": -7, "reverb": 0.05 }
19
+ },
20
+ "sections": [
21
+ { "name": "intro", "bars": 4, "chords": "Am F C G",
22
+ "play": { "pad": "chords", "arp": "arp:16", "drums": "hats" } },
23
+ { "name": "main", "bars": 8, "chords": "Am F C G",
24
+ "play": {
25
+ "lead": "A4 . C5 . E5 - D5 C5 | B4 . G4 . B4*2 C5 D5 | E5 - . C5 A4 . C5 D5 | E5*3 . G5*2 E5*2 | F5 . E5 D5 C5 - A4 . | C5 . B4 . A4*2 G4*2 | A4 . C5 . E5 . A5 - | G5*4 E5*4",
26
+ "bass": "root8", "pad": "chords", "arp": "arp:16", "drums": "four" } },
27
+ { "name": "chase", "bars": 4, "chords": "Dm E Am Am",
28
+ "play": {
29
+ "lead": { "notes": "D5 E5 F5 A5 | G#5*2 E5*2 | A5 E5 C5 A4 | E5*4", "stepsPerBeat": 1 },
30
+ "bass": "pulse", "arp": "arp-updown:16", "drums": "break+fill" } }
31
+ ],
32
+ "arrangement": ["intro", "main", "chase", "main"],
33
+ "loops": ["main", "chase"],
34
+ "master": { "lufs": -14 }
35
+ }
36
+ ```
37
+
38
+ ## The top
39
+
40
+ | field | meaning |
41
+ | --- | --- |
42
+ | `bpm` | 40 to 240 |
43
+ | `beatsPerBar` | 4 by default (3 for a waltz) |
44
+ | `stepsPerBeat` | the grid of a notes pattern: 4 = sixteenth notes (default), 2 = eighths, 1 = quarters. An instrument or a single pattern can set its own |
45
+ | `swing` | 0 to 0.5: every second step comes that fraction of a step late (0.2 to 0.33 is a shuffle) |
46
+ | `arrangement` | section names in playing order; a section can repeat. Default: the sections in order |
47
+ | `loops` | sections to render as seamless loops for the game (`--loops` overrides; `all` for every one) |
48
+ | `master.lufs` | -14 for a page or a trailer, -18 to -20 for a bed under game effects (`--lufs` overrides) |
49
+ | `room` | the shared reverb's size, 0 to 1 (0.55 by default) |
50
+
51
+ ## Instruments
52
+
53
+ `{ "preset", "db", "pan", "octave", "legato", "echo", "reverb", "duck", "stepsPerBeat", "tone" }`
54
+
55
+ - `db` the level (-10 by default). Start the lead around -9, drums -7, bass -10, pads -17, and move
56
+ one at a time while listening to the numbers (`analyze` a stem).
57
+ - `pan` -1 (left) to 1 (right). Keep the bass and kick in the middle.
58
+ - `octave` shifts every note of the instrument by octaves.
59
+ - `legato` how much of each step a note holds (0.92 by default; 0.5 is staccato).
60
+ - `echo` `{ beats, feedback, mix, pingpong }`: a tempo-synced echo (0.75 beats is the dotted eighth).
61
+ - `reverb` the send to the shared room, 0 to 1.
62
+ - `duck` 0 to 0.95: the instrument dips on every kick and comes back within half a beat (the pump
63
+ that makes a pad breathe with the drums). Only when the section has a kick.
64
+ - `tone` (drums) shifts the kit's pitch in semitones.
65
+
66
+ Presets (`sound.mjs presets` lists them with what each is for): leads `pulse-lead`, `square-lead`,
67
+ `saw-lead`, `soft-lead`, `bell`; keys `epiano`, `pluck`, `organ`; pads `pad`, `warm-pad`; basses
68
+ `saw-bass`, `square-bass`, `triangle-bass`, `sub-bass`; drums `kit`, `chip-kit`.
69
+
70
+ ## Chords
71
+
72
+ `"chords": "Am F C G"` is one chord a bar, repeated to fill the section. With bars marked,
73
+ `"Am F | C G | Dm | E7"` puts two chords in a bar when you write two. Qualities: major (`C`),
74
+ `m`, `7`, `maj7`, `m7`, `m9`, `9`, `add9`, `6`, `m6`, `5`, `dim`, `dim7`, `m7b5`, `aug`, `sus2`,
75
+ `sus4`, `7sus4`; a slash puts another note in the bass (`C/E`). Sharps and flats: `F#m`, `Bb`.
76
+
77
+ ## Patterns
78
+
79
+ A pattern is either generated from the chords, or written note by note.
80
+
81
+ **From the chords**: `root` (the root held for the chord), `root8` (eighth-note roots; `root8:16`
82
+ for sixteenths), `pulse` (sixteenth-note roots, a driving bass), `octaves` (root and its octave in
83
+ turn), `chords` (the chord held: a pad), `stabs` (short chords on the off-beats), `arp`, `arp-down`,
84
+ `arp-updown` (the chord's notes one at a time; `:16` sixteenths by default, `:8` eighths). The bass
85
+ plays low, pads in the middle, arpeggios an octave up; `octave` moves any of them.
86
+
87
+ **Notes**: one token per step of the pattern's grid.
88
+
89
+ | token | meaning |
90
+ | --- | --- |
91
+ | `A4`, `C#5`, `Bb3` | a note (A4 = 440 Hz; octaves change at C) |
92
+ | `.` | a rest |
93
+ | `-` | hold the previous note one more step |
94
+ | `A4*3`, `.*4` | a note or rest lasting 3 (or 4) steps |
95
+ | `[A3 C4 E4]` | a chord |
96
+ | `A4!` | an accent (louder); with a length: `A4!*2` |
97
+ | a vertical bar | a bar line: optional, but when you write them every bar must be full, which catches a miscounted bar |
98
+
99
+ A pattern shorter than its section repeats (a one-bar riff over eight bars). With the default grid a
100
+ bar of 4/4 is 16 tokens; for a melody in eighth notes give the instrument `"stepsPerBeat": 2` (8
101
+ tokens a bar), and a single section can differ with `{ "notes": "…", "stepsPerBeat": 1 }`.
102
+
103
+ **Drums**: a groove by name, `four` (four on the floor), `backbeat`, `half` (half-time), `break`,
104
+ `shuffle`, `hats`, `pulse` (kick only), `none`; `+fill` puts a snare and tom fill in the section's last
105
+ bar (`"break+fill"`). Or write the bar yourself, one line per drum, `x` a hit, `X` an accent, `.`
106
+ nothing, 16 cells a bar in 4/4 (the line repeats every bar):
107
+
108
+ ```json
109
+ "drums": { "kick": "x.....x...x.....", "snare": "....x.......x...", "hat": "x.x.x.x.x.x.x.x.", "openhat": "..............x.", "fill": true }
110
+ ```
111
+
112
+ Drums: `kick`, `snare`, `clap`, `hat`, `openhat`, `tom`, `rim`, `shaker`.
113
+
114
+ ## What makes it good
115
+
116
+ - **A hook in the first bars.** The main melody should be singable: mostly steps, one leap, a note
117
+ held at the end of a phrase. Repeat it; change the last bar of the repeat.
118
+ - **Contrast between sections**: the calm section drops the drums or the bass; the chase doubles
119
+ the bass rhythm and adds the break; the final seconds go up a key or add the open hat.
120
+ - **Space for the game's effects.** A bed at -18 LUFS with the lead an octave away from the effects'
121
+ range; `duck` the pad and bass under the kick, and `sound.duck()` the music for big hits.
122
+ - **Loops in 4 or 8 bars**, every loop the same tempo and key, so the game can switch on a bar line.
123
+ - Listen with numbers: `analyze` each stem; a stem more than 12 dB under its neighbours is inaudible
124
+ in the mix, and a struck sound (a pluck, a bell) sits about 9 dB under a held one at the same peak,
125
+ so lengthen its decay instead of pushing its level.
126
+
127
+ ## Errors that name the fix
128
+
129
+ A bar with the wrong number of steps, an unknown chord quality, a section that plays an instrument not
130
+ in `instruments`, an arrangement naming a missing section: each stops the render with a sentence
131
+ saying what and where. Fix that and render again; a render is free.
@@ -0,0 +1,93 @@
1
+ # Traps: ffmpeg, expressions and synthesis
2
+
3
+ Each of these produced silence, the wrong level or a click while every command exited 0.
4
+
5
+ ## ffmpeg
6
+
7
+ - **Exit code 0 is not audio.** A graph that failed part-way still writes a file whose header promises
8
+ sound. Read it back: `ffprobe -v error -show_entries format=duration:stream=sample_rate,channels -of json <file>`,
9
+ and measure it (`sound.mjs analyze`). A duration of 0, or one far from what you asked for, is a
10
+ failed render with the right name.
11
+ - **`c=2` on a source makes one channel.** `aevalsrc` and friends want a layout name (`c=stereo`,
12
+ `c=mono`) or `2c`; a bare number is read as a channel mask. Ask for the count on the output
13
+ (`-ac 2 -ar 48000`) and the source cannot be wrong about it.
14
+ - **`amix` divides by the number of inputs** unless `normalize=0`: the levels you asked for never
15
+ arrive and nothing says so. (`sound.mjs mix` sums in JavaScript at the levels written.)
16
+ - **`adelay` takes one value per channel** (`adelay=500|500`); a single value delays the left channel only.
17
+ - **mp3 pads both ends with silence**: an effect starts late and a loop gaps at the wrap. Effects and
18
+ loops are WAV or Ogg; mp3 is for a page's player only.
19
+ - **`<audio loop>` gaps at the wrap** in every browser: loop a decoded buffer in Web Audio
20
+ (`source.loop = true`).
21
+ - There may be no `drawtext` in the user's ffmpeg build (it needs freetype): draw labels in a browser
22
+ or an image instead.
23
+ - `loudnorm` needs 400 ms blocks: integrated LUFS of a 0.2 s effect is meaningless. Level short
24
+ effects by their loudest 50 ms (what `sound.mjs` does).
25
+ - Encoders differ: many ffmpeg builds have no `libvorbis`; `libopus` in an `.ogg` is the fallback.
26
+ Older Safari cannot decode Ogg at all, so ship a WAV beside it.
27
+
28
+ ## aevalsrc expressions
29
+
30
+ - **No comparison operators.** `t>=1` fails with a misleading "Missing ')'". Use `gte(t,1)`,
31
+ `lte()`, `gt()`, `lt()`, `between(t,a,b)`. There is no `sqr()`: `pow(x,2)`.
32
+ - **`0*inf` is NaN and a NaN silences the whole sum.** An envelope like `exp(-(t-a)*k)*gte(t,a)`
33
+ overflows before the gate: write `exp(-max(0,t-a)*k)`.
34
+ - **`random(x)` keeps its state in variable slot x** and overwrites it; put every `random` in one slot
35
+ (9) and keep `st()/ld()` state in 0..7. There is one random stream: to decorrelate a right channel,
36
+ advance it (`0*random(9)+…`). The first use of a slot must be its `st()`.
37
+ - Quotes, backslashes, semicolons and brackets belong to ffmpeg's graph parser, not the expression;
38
+ keep expressions to arithmetic and functions. Very long expressions (a few thousand characters)
39
+ are fragile: split the terms across parts, they sum to the same thing.
40
+ - `PI` may be missing: write `6.28318` for 2π.
41
+ - One expression renders mono; two (left, right) render true stereo.
42
+
43
+ ## Pitch and phase
44
+
45
+ - **In `synth.mjs` a number is hertz, always.** `freq: 110` is 110 Hz and `slide.to: 45` is 45 Hz.
46
+ A note is a name (`freq: "A2"`) or a MIDI number said out loud (`midi: 45`, `slide.toMidi: 45`,
47
+ or `hzOfMidi(45)`). There is no guessing by size: a number of 127 or less used to be read as a
48
+ MIDI note, and every bass note is a frequency under 127 Hz, so a 110 Hz bass came out at 4.7 kHz.
49
+ - **A pitch bend is a change of frequency, integrated into phase.** `sin(2*PI*f(t)*t)` with a
50
+ changing `f` sweeps at twice the intended rate and runs backwards on the way down; the phase must be
51
+ the integral of the frequency (what `synth.mjs` does every sample). A scoop from below in closed
52
+ form is `+2π·df·τ·exp(-(t-a)/τ)` added to the phase.
53
+ - Vibrato depth scales with pitch: ±0.4% of the frequency is about ±7 cents at any note.
54
+ - A tremolo on one note is one voice whose envelope clock wraps (`mod(max(0,t-a)*rate,1)/rate`),
55
+ with the phase left running; restarting the phase each time clicks.
56
+ - An uneven pulse wave (duty other than 50%) has a DC offset: subtract `2·duty - 1`, or high-pass the
57
+ result, or it clicks at every start and stop.
58
+
59
+ ## Loops and width
60
+
61
+ - **A held sound loops without a click only if every frequency in it, beating pairs included, is a
62
+ whole multiple of 1/loop-length** (0.125 Hz for an 8 s loop) and no channel starts at a phase
63
+ offset. Notes whose envelope closes before the loop point are exempt. Or render the section three
64
+ times and keep the middle (what `sound.mjs score` does).
65
+ - **Limit a loop before cutting it, not after.** A limiter run over the cut loop starts at full gain
66
+ and ends wherever the last bar left it: a step of gain at the wrap. `score` limits across the
67
+ three copies. A note, an echo or a reverb longer than the whole section still cannot repeat: the
68
+ seam goes over 1 and the report warns; make the section longer or the tail shorter.
69
+ - **A 1-3 ms delay between channels widens a transient but cancels a held tone** in mono (a phone
70
+ speaker folds to mono): correlation fell to -0.2 and the note lost 4 dB. Widen held sounds by
71
+ detuning or by panning different notes, never by a channel offset; offset only the attacks.
72
+ - Measure width with the stereo correlation and the mono loss (`analyze` gives both). Negative
73
+ correlation means parts cancel on a phone.
74
+
75
+ ## Levels
76
+
77
+ - A struck sound (a pluck, a bell, a mallet) sits about 9 dB RMS under a held one at the same peak.
78
+ Lengthen its decay rather than raising its fader, then compare stems with their gains applied.
79
+ - A generated room tone or ambience can carry a steady hum (a narrow line in the spectrum): `analyze`
80
+ lists narrow tones; notch them or regenerate. Each one comes with `prominenceDb` (over its
81
+ neighbours, which a filter does not change: the neighbours fall with it), `levelDb` (how loud it
82
+ is, dBFS), `energyShare` and `presentShare` (the share of the file it is there). Only a tone that
83
+ is prominent, above -60 dBFS and there 60% of the time or more is called steady; a melody's brief
84
+ high partial is listed and not warned about. Read its level and share to judge it.
85
+ - Check every render at its head and tail as well as its peak: silence at the start of an effect is
86
+ latency; a tail cut mid-ring is a click.
87
+
88
+ ## Provider renders (when a model made the audio)
89
+
90
+ - A paid render can land minutes after the command that asked for it timed out or failed. Before
91
+ asking again, list the output folder by time: the same request twice is twice the bill.
92
+ - Before cutting a straight loop out of a swung track, find the onsets in the rendered file: a hit a
93
+ few ms early is a musical flam, not an error, and triplet grids suit a shuffle.