Exact source used by this build. SHA-256: 482872b6e478f51c9f02204f83313400246b9bb6b7e06e64b5d8a150ea0278fc.

1 # Tamarack procedural audio
2 
3 An original, free, offline audio workshop for the Vermont valley setting. All
4 waveforms and scores are authored here; no recordings, sampled audio, MIDI,
5 downloaded sounds, existing melodies, or paid generation services are used.
6 The generated assets are CC0; provenance is in `assets/CREDITS.md`.
7 
8 This collection is separate from the running game. Only audio tools, exported
9 assets, documentation, credits and root scripts change. The JSON files in
10 `tools/audio/` are offline synthesis/score inputs, following the art pipeline's
11 catalog pattern; future gameplay bindings belong in the content package.
12 
13 ## Build and preview
14 
15 Prerequisites: the project's installed Bun/TypeScript and local
16 `/opt/homebrew/bin/ffmpeg` plus `/opt/homebrew/bin/ffprobe`. This implementation
17 uses FFmpeg's `libopus` and native `aac` encoders. No new dependencies are added.
18 
19 ```sh
20 bun run audio:sfx
21 bun run audio:music
22 # Or rebuild both:
23 bun run audio:all
24 bun run audio:preview
25 # Open http://127.0.0.1:4174/preview.html
26 
27 bun test tools/audio
28 bun run audio:verify
29 bun run check
30 ```
31 
32 `audio:sfx` and `audio:music` preserve the other collection's manifest entries.
33 Run both after checkout, or use `audio:all`. Run generators sequentially, since
34 they share the manifest. `AUDIO_PREVIEW_PORT` changes the
35 local preview port. The server binds only to 127.0.0.1.
36 
37 The static preview embeds the full manifest, lists every cue with a play button,
38 and provides search, category, codec, master-volume, loop and character-voice
39 controls. Starting a cue stops the previous one. It uses no CDN or outside
40 service. Local `file://` playback can use an audio-element fallback; localhost
41 uses Web Audio for accurate loop timing and codec-padding removal.
42 
43 ## Collection
44 
45 **113 sound cues and seven music loops**, each in both formats. The sound cues
46 include 24 footsteps (four distinct seeded/pitched performances on grass, dirt,
47 wood, stone, sand and snow); six monster cues (hit/death for moss, stone and wing
48 families); seven dialogue timbres; ten environmental loops; seven animal calls;
49 and all requested tools, actions, fishing, combat, UI and reward moments.
50 
51 The six requested music loops are accompanied by beach music to cover the
52 additional area in the feel checklist. Melodies, rhythms and forms are explicitly
53 authored, with gentle variation in timing and intensity.
54 
55 | Cue | Title | Duration | Character |
56 | --- | --- | ---: | --- |
57 | `music.spring` | Sap and Sunlight | 76.80 s | Hopeful flute and fingerpicked strings |
58 | `music.summer` | Porch Shade | 87.27 s | Unhurried porch tune with offbeat answers |
59 | `music.fall` | Leaves on the Covered Bridge | 75.79 s | Nostalgic three-beat air |
60 | `music.winter` | White Pine Stillness | 84.71 s | Sparse felted notes and quiet strings |
61 | `music.mines` | Lantern under Slate | 72.00 s | Low open harmony, distant room reflections |
62 | `music.town.evening` | The Last Porch Light | 72.00 s | Warm strings and a restrained flute reply |
63 | `music.beach` | Pebbles at the Waterline | 68.57 s | Light, spacious shoreline picking |
64 
65 Environmental loops are eight seconds each: rain, wind, spring/summer morning
66 birds, summer-night crickets, fall-night owl, stream, ocean, fireplace, mine drip.
67 Animal calls are chicken, cow, sheep, duck, dog, cat and goat. These are stylized
68 synthetic voices, not recordings or species-identification material.
69 
70 ### Feel-checklist coverage
71 
72 The source document's cue names identify actions, not imported assets or runtime
73 aliases. The original names in the manifest are the stable IDs for later use.
74 
75 | Checklist moment | Original cue(s) |
76 | --- | --- |
77 | Grass/wood/stone/sandy footsteps | `footstep.{grass,wood,stone,dirt,sand,snow}.{1..4}` |
78 | Hoe, planting, watering, axe, hammer | `tool.hoe.*`, `item.seed.plant`, `tool.watering`, `tool.axe.wood`, `tool.pickaxe.*` |
79 | Leaves and stone cracks | `world.leaf.rustle`, `world.stone.crack` |
80 | Pickup, coin, harvest | `item.pickup`, `item.coin`, `item.harvest` |
81 | Ship and chest | `world.shipping.deposit`, `world.shipping.lid`, `world.chest.open` |
82 | Large/small select, menu thunk | `ui.select.large`, `ui.select.small`, `ui.thunk` |
83 | Scroll, recipe/page, trash | `ui.scroll`, `ui.recipe`, `ui.trash` |
84 | Cast, bite, rod bend, water catch | `fishing.cast`, `fishing.bite`, `fishing.rod.bend`, `fishing.caught`, `world.splash` |
85 | Sword, dagger, club, slime-like hit | `combat.sword.*`, `combat.dagger.swing`, `combat.club.hit`, `combat.monster.moss.hit` |
86 | Crit, parry, debuff | `combat.critical`, `combat.parry`, `combat.debuff` |
87 | Eat, gulp | `item.eat`, `item.gulp` |
88 | Special item, quest, achievement, secret | `jingle.special.item`, `jingle.quest.complete`, `jingle.achievement`, `jingle.secret` |
89 | Doors | `world.door.open`, `world.door.close` |
90 | Rain, thunder, small thunder | `ambient.rain`, `world.thunder.near`, `world.thunder.distant` |
91 | Animals, birds, crickets | `animal.*`, `ambient.birds.*`, `ambient.crickets.summer` |
92 | Seasons, town, mines, beach | All seven music IDs above |
93 | Per-character dialogue blip | `dialogue.blip`, six alternative voice profiles, stable ID-derived pitch |
94 
95 ## Synthesis and mastering
96 
97 `synth.ts` supplies 24 kHz mono Float32 buffers, a local seeded Mulberry32 RNG,
98 band-limited additive sine/triangle/soft-saw oscillators, exponential pitch
99 sweeps, simple FM, vibrato, smooth attack/release envelopes, filtered noise,
100 mixing and PCM16 WAV encoding. The music engine adds fractional-delay plucked
101 strings, felted harmonic tones, breath flute, soft pads, brushes and circular
102 room reflections. Tone `decay` is an exponential coefficient per second:
103 `exp(-decay * t)`. `vibratoDepth` is a fractional frequency deviation.
104 
105 Each voice has its own seed. No wall-clock time or unseeded randomness affects
106 rendering. DC offset is removed; the level target is **active-sample RMS**:
107 -21 dBFS for actions/voices, -25 dBFS for ambient beds, -23 dBFS for music.
108 Active samples exceed 5% of that cue's peak. Gain is capped to retain at least
109 3.5 dB of PCM peak headroom. This is a documented RMS normalization method,
110 not a claim of broadcast LUFS normalization. No hard clipping is applied.
111 Manifest `volume` is an additional suggested mixing gain for later integration.
112 
113 One-shots receive a 3 ms edge taper. Loop voices/events and music/reverb tails
114 wrap into the beginning of the period; an 8 ms cubic endpoint correction removes
115 the residual sample discontinuity without fading the loop to silence. Loops
116 retain their full duration and the music ends on harmony that returns to bar one.
117 
118 WAV intermediates are written to task-owned `tamarack-audio-*` directories in the
119 system temp directory and removed after each encoding. The temporary directory
120 is removed in a `finally` block. Rendering, encoding and verification process one
121 cue at a time, with one FFmpeg thread; complete scores release their temporary
122 buffers before the next render. Run one heavy command at a time on the shared
123 16 GB Mac, including the test suite and rebuild verifier. Opus is stored in
124 `.ogg` (48 kb/s SFX, 64 kb/s music); AAC-LC
125 is stored in `.m4a` (64 kb/s). Both are mono. Bitexact encoding, fixed Ogg serial,
126 one encoder thread and stripped metadata make repeated outputs byte-identical
127 with the same Bun/FFmpeg versions. Codec upgrades or another math/runtime build
128 can change bytes; the manifest records the FFmpeg version. There are no timestamps
129 in generated metadata. Size limits use conservative decimal bytes: less than
130 150,000 per SFX file and 2,000,000 per music file.
131 
132 ## Manifest and later integration
133 
134 `apps/client/public/assets/audio/manifest.json` contains `cues`, `groups`,
135 `dialogue`, `encoding` and provenance. Every cue has:
136 
137 - Stable ID, title, category, description, seed, duration and optional group/variant.
138 - Relative `files.ogg` / `files.m4a`, byte counts and SHA-256 hashes for PCM, WAV
139   and both encodings.
140 - `volume`, fractional `pitchVariance` (for example 0.04 means ±4%), and `loop`.
141 - `sampleRate`, exact `frames`, `loopStart`/`loopEnd` in source frames and measured
142   peak, total RMS and active RMS levels.
143 
144 Prefer Opus where supported and AAC otherwise. After decoding, resample the
145 authored frame count to the AudioContext rate, trim padding, and set loop start
146 to zero and loop end to `frames / sampleRate`. Ogg/Opus decodes natively at
147 48 kHz; the authored WAV is 24 kHz. AAC can retain a partial trailing packet.
148 The preview trims this and corrects the decoded join over 8 ms because lossy
149 codecs can slightly alter boundary samples. An ordinary HTML audio element's
150 `loop` is not a guarantee of sample-accurate timing. Use the preview's Web Audio
151 path as the reference for eventual integration.
152 
153 Dialogue pitch is independent of a roster: `dialoguePitch(characterId)` uses
154 unsigned FNV-1a over Unicode code points, then
155 `0.84 + (hash % 321) / 1000` (0.84–1.16×). The manifest records this rule and the
156 preview auditions it. The six alternate timbres are available without inventing
157 new NPC records. Rate-limit typewriter blips during integration instead of
158 playing one for every displayed character. Footstep groups expose four authored
159 variants; avoid immediate repeats and add bounded pitch variance at playback.
160 Music has zero pitch variance. Rain-related music ducking, event timing and
161 runtime routing are future integration work.
162 
163 ## Add or tweak a sound
164 
165 1. Edit/add the cue descriptor in `tools/audio/sfx-catalog.json`. Use a unique
166    dotted original ID, explicit seed, category, description, duration, volume,
167    pitch variance and loop flag. Keep ambient loops short enough for the budget.
168 2. Add its recipe branch in `sfx.ts`. Layer restrained tones, material-colored
169    noise, rounded transients and small delayed details. Use `seededRandom`, never
170    `Math.random`. Give repeated voices distinct seeds. For a loop use wrapped
171    events and circular/noise beds; do not add a whole-loop fade.
172 3. Rebuild with `bun run audio:sfx`, then audition the cue and adjacent sounds in
173    the preview with both codecs. Test the join by letting loops repeat. Use
174    manifest volume for relative prominence; change the recipe to alter timbre.
175 4. Run `bun test tools/audio`, `bun run audio:verify` and `bun run check`. Add
176    coverage assertions for a new required family. All output comes from source;
177    editing an exported file by hand will fail the hash check.
178 
179 To alter music, edit `music-score.json`. `patterns` are four-bar tracker cells;
180 each bar holds `[beat, MIDI pitch, durationInBeats, optionalVelocity]` events.
181 Omitted beats are rests. `chords` contain an explicitly voiced bass followed by
182 upper notes. Each phrase chooses a pattern, four chords and an intensity.
183 The engine validates notes, bar lengths, harmony references and the 60–120 s
184 duration. Its style chooses an accompaniment rhythm; the melody and form remain
185 authored. Use the last phrase to lead naturally into the first, and allow tails
186 to wrap. `audio:music` rebuilds the music while retaining SFX manifest entries.
187 
188 ## Validation and storage
189 
190 `bun test tools/audio` hashes every complete SFX and score twice, checks coverage,
191 distinct variants, envelopes, WAV headers, voice identity, levels and loop seams,
192 and encodes representative short/noisy/looping sounds twice in both codecs. When
193 exports exist it also verifies every manifest file/hash/size and preview entry.
194 A clean source checkout skips only that existing-export check; synthesis and
195 codec tests still run. Audio strict TypeScript checking is part of root `check`.
196 
197 `audio:verify` requires the full exports. It probes and decodes every file, checks
198 codec, duration, clipping, silence and joins, then rebuilds the **entire** asset
199 collection in system temporary scratch space and compares SHA-256 for every Ogg, M4A,
200 manifest and preview. Temporary rebuilds are removed even on failure.
201 `audio:verify --quick` checks the existing collection without a rebuild.
202 
203 The exported Ogg/M4A files, manifest and preview are deliberately ignored by a
204 local `.gitignore`, matching the art export policy. Source, catalog, scores,
205 tests and tools remain small and reviewable. The final encoded assets stay on
206 disk because they are the requested deliverable. No Git writes or commits are
207 made for this task.
208 
209 Current rendered totals: **10,974,227 bytes** of encoded audio (10.97 MB), or
210 **11,306,672 bytes** including the manifest and static preview. The largest SFX
211 encoding is **69,663 bytes** and the largest music encoding is **706,550 bytes**.
212 Even counting both encodings together, the largest SFX pair is 118,548 bytes and
213 the largest music pair is 1,411,651 bytes. There are 240 audio files and 242
214 generated deliverables in total. A full rebuild matched all 242 SHA-256 hashes;
215 all files decoded without clipping or silence failures. The audio suite passes
216 14 tests with 1,633 assertions.
217 
218 Final validation after switching to sequential execution and system-temp scratch:
219 
220 | Command | Result |
221 | --- | --- |
222 | `bun run audio:typecheck` | Passed strict TypeScript |
223 | `bun test tools/audio/audio.test.ts` | 14 passed, 1,633 assertions |
224 | `bun run audio:verify` | All files decoded; 242 byte-identical rebuild hashes |
225 | `bun test apps/server/test/server.test.ts` | 2 passed after repairing existing dependency links |
226 | `bun run check` | All typechecks, 158 tests / 7,147 assertions, and 12 content domains passed |
227 
228 The incomplete local dependency layout was repaired using links to already
229 installed packages, including the lockfile's Wrangler 4.152.0. No package versions,
230 lockfile, game source, credentials or deployment configuration changed.
231 
232 ## Five weakest approximations
233 
234 These are provisional synthesis/design judgments, not a claim of a human listening
235 panel: animal and large physical sounds are hardest to convey without recordings.
236 
237 1. `animal.cow`: the rounded formant sweep conveys a low call but lacks the
238    irregular breath and throat detail of a real moo.
239 2. `animal.cat`: a pitched mew is recognizable as a stylized voice, with limited
240    natural vocal inflection.
241 3. `animal.sheep`: regular synthetic throat flutter lacks some of the uneven
242    breath and changing resonance of a real bleat.
243 4. `world.tree.fall`: staged creaks, chips and low impact suggest the event, but
244    do not reproduce a large tree's complex branches and spatial movement.
245 5. `ambient.ocean`: a shaped noise wash and bubbles give a soft shoreline; the
246    eight-second period limits longer, irregular sets of waves.
247 
248 All assets are mono. Review through headphones and phone speakers before setting
249 final in-game balances; the current collection is ready for that integration pass.
250