|
| 1 | +# P(Doom)1 -- Music design spec (adaptive doom-band music) |
| 2 | + |
| 3 | +> Music is the AUDIO expression of the existing doom-intensity spec |
| 4 | +> (`docs/art/PALETTE_AND_DOOM_INTENSITY.md`). Same two axes, same five bands, same |
| 5 | +> "cozy competence sliding toward existential dread" arc. This document is the reference |
| 6 | +> for the musicians composing stems and for anyone touching |
| 7 | +> `godot/autoload/music_manager.gd`. Companion docs: `STEM_CATALOGUE.md` (what audio we |
| 8 | +> own today), `TRACK_ANALYSIS.md` (the taste reference method). |
| 9 | +
|
| 10 | +## 1. The model in one paragraph |
| 11 | + |
| 12 | +Mick-Gordon / DOOM-style adaptive music: a small set of MUSIC TIERS (moods), each built |
| 13 | +from LAYERED STEMS that add/subtract as doom rises. The game's doom value (0-100) is |
| 14 | +read -- never written -- by the music system, quantized to the canonical doom bands, and |
| 15 | +mapped to a tier. Tier changes crossfade horizontally (mood shift); within a tier, stems |
| 16 | +stack vertically (intensity texture). Players should be able to close their eyes and |
| 17 | +know roughly where the doom meter sits. |
| 18 | + |
| 19 | +## 2. Axes -> music mapping |
| 20 | + |
| 21 | +The art spec defines two independent axes plus combined bands. Each gets a musical |
| 22 | +dimension: |
| 23 | + |
| 24 | +| Axis (art spec) | Visual expression | MUSICAL expression | |
| 25 | +|---|---|---| |
| 26 | +| A. Catastrophe (amber -> red -> fire) | glow sours, strobes, literal fire | RHYTHM: percussive density and drive. Cosy = sparse/ambient pulse; alarmed = insistent percussion; fire = a dedicated "things are on fire" layer (alarm-adjacent textures, driving low end) entering at high tiers only | |
| 27 | +| B. Weirdness (green -> blue -> violet) | clean -> glitchy -> eldritch glow | TIMBRE: clean/tonal instruments -> detuned, bit-crushed, glitch-stuttered -> dissonant clusters, wrong-scale intervals, reversed/smeared textures | |
| 28 | +| Doom bands (0-4) | which register the office renders in | WHICH LAYERS ARE LIVE + overall mood/key territory | |
| 29 | + |
| 30 | +Discipline rule, borrowed from the art spec's lighting rule ("at most ONE ambient + |
| 31 | +ONE weird glow source"): at most ONE rhythmic driver and ONE weird/timbral accent |
| 32 | +prominent at a time. Do not stack every layer at full volume even at terminal doom -- |
| 33 | +terminal reads as oppressive SPACE plus the fire layer, not as maximum loudness. |
| 34 | +Purple is expensive; so is dissonance. Reserve overt eldritch harmony for tiers 3-4. |
| 35 | + |
| 36 | +## 3. Music tiers (the five bands, made audible) |
| 37 | + |
| 38 | +The game's canonical thresholds live in `ThemeManager.DOOM_STATUS_BANDS` (7 UI bands). |
| 39 | +Music collapses them onto the art spec's 5 doom-intensity bands: |
| 40 | + |
| 41 | +| Music tier | Name | UI bands (doom %) | Mood target | Rhythm (axis A) | Timbre (axis B) | |
| 42 | +|---|---|---|---|---|---| |
| 43 | +| M0 | cosy | NOMINAL (<15) | warm competence, focus | soft pulse or none | clean, tonal, warm (amber) | |
| 44 | +| M1 | uneasy | ELEVATED + HIGH (15-52) | something is off | pulse gains weight, occasional stumble | faint detune/flicker creeping in | |
| 45 | +| M2 | spooky | SEVERE + EXTREME (52-80) | shadows lengthen | insistent percussion, darker low end | glitch artifacts, blue-arcing synth | |
| 46 | +| M3 | eldritch | CATASTROPHIC (80-92) | it has gone wrong in a wrong way | percussion fractures/polyrhythm | dissonant, wrong-scale, violet | |
| 47 | +| M4 | terminal | TERMINAL (92+) | apocalyptic; the end is audible | FIRE layer in; driving or collapsed | full eldritch smear + alarms | |
| 48 | + |
| 49 | +Numbers come from `ThemeManager` at runtime -- the music system holds NO thresholds of |
| 50 | +its own, only the band->tier table `MUSIC_TIER_BY_BAND = [0,1,1,2,2,3,4]`. If Pip |
| 51 | +retunes the bands, music follows automatically. |
| 52 | + |
| 53 | +## 4. Layer/stem model (what the musicians compose) |
| 54 | + |
| 55 | +Each tier is a STEM GROUP played in sync. Target stems per tier (a tier may omit some): |
| 56 | + |
| 57 | +| Stem slot | Carries | Axis | Notes | |
| 58 | +|---|---|---|---| |
| 59 | +| BASE | harmony/bed, the mood itself | bands | always on within its tier; must loop seamlessly | |
| 60 | +| PULSE | percussion/rhythm | A (catastrophe) | density scales with tier; absent at M0 is fine | |
| 61 | +| WEIRD | timbral accent (detune/glitch/dissonance) | B (weirdness) | the "one weird glow" -- keep it a single identifiable voice | |
| 62 | +| FIRE | the things-are-on-fire topper | A, high end | ONLY M3/M4 (like purple in the palette: expensive) | |
| 63 | + |
| 64 | +Composition constraints so stems can be swapped/stacked mechanically: |
| 65 | +- Every stem in a tier: same tempo, same key (or deliberate fixed relationship), same |
| 66 | + length or integer multiple, seamless loop, no reverb tails crossing the loop point |
| 67 | + (bake tails into the loop musically or keep them short). |
| 68 | +- Adjacent tiers should share tempo or have a planned tempo relationship (e.g. M0-M2 at |
| 69 | + one tempo, M3-M4 at another) so crossfades between tiers do not churn. |
| 70 | +- Deliver as separate files (wav or ogg preferred; mp3 acceptable), loudness roughly |
| 71 | + matched (about -16 LUFS integrated per stem; final balance happens in-engine). |
| 72 | +- Names stay in the ML/AI-safety pun family (see `STEM_CATALOGUE.md`). |
| 73 | + |
| 74 | +## 5. Runtime architecture (what is built, Godot 4.5.1 native) |
| 75 | + |
| 76 | +No FMOD/Wwise. Two native primitives, already implemented in |
| 77 | +`godot/autoload/music_manager.gd`: |
| 78 | + |
| 79 | +- **Horizontal (tier moods): `AudioStreamInteractive`** -- one clip per tier, named |
| 80 | + `cosy/uneasy/spooky/eldritch/terminal`, with an any-to-any crossfade transition |
| 81 | + (8 beats at a nominal 120 BPM = 4 s). Tier changes call |
| 82 | + `AudioStreamPlaybackInteractive.switch_to_clip()`. |
| 83 | +- **Vertical (stems within a tier): `AudioStreamSynchronized`** -- each clip's stream is |
| 84 | + a synchronized group of that tier's stems with per-stem volume offsets. Today most |
| 85 | + tiers hold a single placeholder track; the `terminal` tier already layers two tracks |
| 86 | + to keep the synchronized path exercised. |
| 87 | + |
| 88 | +Signal flow (read-only, view-layer): |
| 89 | + |
| 90 | +``` |
| 91 | +GameManager.game_state_updated(state) # existing broadcast, emitted per turn |
| 92 | + -> MusicManager._on_game_state_for_music # reads state["doom"], nothing else |
| 93 | + -> ThemeManager.get_doom_band_index(doom) # canonical band (0..6) |
| 94 | + -> MUSIC_TIER_BY_BAND[band] # music tier (0..4) |
| 95 | + -> switch_to_clip(tier) # native crossfade if tier changed |
| 96 | +``` |
| 97 | + |
| 98 | +### Hard rules (ADR-0006: the replay string is the canonical run artifact) |
| 99 | +- Audio is a PURE view-layer side-effect. `MusicManager` only LISTENS to |
| 100 | + `game_state_updated`; it never calls into game code, never touches the seeded RNG, |
| 101 | + the turn loop, replay, or any state that could perturb determinism. |
| 102 | +- Never block or crash on missing audio: a missing stem is skipped, an empty tier |
| 103 | + inherits its neighbour's stream, a fully-empty build falls back to the legacy |
| 104 | + playlist, and no music at all is silence, not an error. |
| 105 | +- `randi()` use for playlist shuffle is the GLOBAL RNG, not the seeded game RNG -- |
| 106 | + keep it that way; never "fix" music shuffle by borrowing the game's RNG. |
| 107 | + |
| 108 | +## 6. Transition rules |
| 109 | + |
| 110 | +Current prototype: single any-to-any rule -- switch immediately, crossfade over 8 beats |
| 111 | +(4 s), restart target clip from its start. Deliberately simple; placeholder tracks share |
| 112 | +no tempo grid, so beat-aligned transitions would be false precision. |
| 113 | + |
| 114 | +Target rules once composed stems exist (all native `AudioStreamInteractive` features): |
| 115 | +- UP-tier (doom rose): transition at NEXT BEAT, short crossfade (1-2 bars). Rising dread |
| 116 | + should feel prompt -- the ratchet. |
| 117 | +- DOWN-tier (doom fell -- rare, earned): transition at NEXT BAR or phrase end, long lazy |
| 118 | + crossfade. Relief arrives slower than fear. |
| 119 | +- M3 -> M4 (terminal): consider a FILLER CLIP sting (a one-shot "it is happening" |
| 120 | + hit) via the native filler-clip transition option. |
| 121 | +- Hysteresis: band quantization already gives dead zones; if doom oscillates across one |
| 122 | + boundary each turn and audibly churns, add a 1-turn dwell before down-transitions |
| 123 | + (do NOT add one for up-transitions; the ratchet must feel immediate). |
| 124 | + |
| 125 | +## 7. Where composed tracks slot in |
| 126 | + |
| 127 | +One place: `MUSIC_TIER_STEMS` in `godot/autoload/music_manager.gd` -- an array of five |
| 128 | +stem lists, `{"path": ..., "volume_db": ...}` per stem. Replacing a placeholder track |
| 129 | +with real per-tier stems is a data edit, no logic change. Contexts outside GAMEPLAY |
| 130 | +(menu / victory / defeat) stay on the existing `music_library` playlists; a future |
| 131 | +victory track and a composed defeat sting slot in there. |
| 132 | + |
| 133 | +Menu music stays NON-adaptive on purpose: the menu has no doom. |
| 134 | + |
| 135 | +## 8. Out of scope / anti-goals |
| 136 | + |
| 137 | +- No AI-generated finished music (rights are murky; Pip values owning rights). |
| 138 | +- No per-action stingers wired to game logic (would tempt writes into the turn loop; |
| 139 | + any future stinger hooks must also be listen-only). |
| 140 | +- No rewrite of the audio bus layout; the Music bus (index 2) is the only one touched. |
0 commit comments