Skip to content

Music: Ogg and a limiter, instruments and effects, songs of sections, MIDI, music that follows the game - #36

Merged
jeany55 merged 14 commits into
trigscript-4from
music-quality
Oct 20, 2026
Merged

jeany55 merged 14 commits into
trigscript-4from
music-quality

Conversation

@jeany55

@jeany55 jeany55 commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

The music library's quality work, steps 1–4, each played in StarCraft: Remastered and checked against a recording. The details, the numbers and how to make it all again are in docs/v4-plan.md ("Sounds: step 1" … "Sounds: how to make the Korobeiniki work again").

Also a fix trigscript-4 needs regardless: compiler/ranges.ts read process.env in the browser's compile worker, where there is no process, so every program failed to compile in the editor (1c75089, its own commit).

Step 1: format and master

  • Clips have one or two channels; pan, mono; render at 44.1 kHz; sinc resampling.
  • A look-ahead limiter at −1.5 dB. It keeps the wave between samples under the ceiling too (true peak).
  • Ogg Vorbis (libvorbis as wasm, MIT/BSD, dist/vorbis.wasm) for program clips of 2 s or more, or any clip made ogg(quality). Encoded only when a build writes the map, never during a check. The output is deterministic.
  • An Ogg always goes in with two channels: the game plays a one-channel Ogg 2.4 dB quieter.

Step 2: instruments and effects

  • Effects:
    • filter (swept, resonant) and EQ
    • drive
    • echo and ping-pong
    • Dattorro plate reverb
    • chorus and compressor
  • Sounds: FM, plucked strings, unison and glides.
  • Presets, and a synthesized drum kit.
  • Mixing: track fx, a shared reverb send, and a master.
  • Lazy clips: samples are worked out only when a build needs them. A check takes 0.4–1.1 s instead of 8 s.

Step 3: writing songs

  • part / section / arrange, and section.with() for a section played again changed.
  • Chords by name, templates, voice leading.
  • transpose, stretch, @ accents, swing.
  • Section effects and volumes; riser, impact, pump, a crash.
  • MIDI import: Import Video… takes a .mid and makes a .notes file; music.midi() turns it into a song.

Step 4: music that follows the game

  • music.stems(song, { layers, every }) cuts a song into a clip per piece per layer, each carrying its tails.
  • together() turns them all down by one shared gain, so the sum never clips.
  • A Band made in the program, played by conduct:
    • jump to a section, or turn a layer on or off, where the next piece starts
    • stingers on the next beat
  • Half bars (every: 2) are the choice for the showpiece.

How to repeat the checks

  • scripts/sound-dump.mts writes out the sounds a build puts in the map.
  • scripts/recording-check.mts checks a recording against them, piece by piece too.
  • Probe answer sheets are in probes/answers/.

The Korobeiniki probes use the folk melody only, which is in the public domain; everything else in them is ours.

1169 tests pass; tsc is clean; every probe runs clean in the lifted game.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG

jeany55 and others added 14 commits October 19, 2026 18:13
The value ranges' debug line read process.env, which a compile worker in the
browser does not have: every script with a program failed to compile in the
editor with "process is not defined". It is read only in Node now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
…for long clips

A clip has one channel or two: pan() places it (a balance, 0 as it was), mono()
folds it, and a mix with a two-sided part is two-sided. limit() keeps a clip
under a ceiling (−1.5 dB, room for Ogg's overshoot) with a 5 ms look-ahead and
a 0.1 s release; normalize() brings its peak there. Resampling is a windowed
sinc now, so a note pitched up no longer folds its top back down.

A clip a program plays that lasts 2 seconds or more goes in the map as Ogg
Vorbis (libvorbis in wasm, from wasm-media-encoders, copied to dist/vorbis.wasm
by the build), encoded by the compile worker only for a build, never for a
check as the user types. Shorter clips, untouched imports, wav() clips and any
clip a trigger() plays stay WAVs.

music.render() makes the notes again at 44 100 Hz and limits the mix; tracks
take { pan, volume }. probes/korobeiniki-ab.ts plays the old format against the
new one in the game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
…es, unclipped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
…r, FM, plucks, presets, drums

Clips get filter() (a state-variable filter, swept by points, and shelf and peak
biquads), drive(), delay(), reverb() (a Dattorro plate), chorus() and
compress(). tone() glides and stacks detuned voices across both sides; fm() and
pluck() are new sources. The music library's synth has a filter with its own
envelope, unison, drive and glide; fmSynth and strings are new instruments;
music.presets and music.drums() are ready-made, written on the public API.
Tracks take fx and a reverb send, render a shared reverb and a master chain. A
bar is a whole number of frames, a step need not be; play refuses more than 32
voices, render does not.

Clips are lazy: their length and recipe are known at once, their samples only
when a build writes the map or a script reads peak. A check names each file by
its recipe and makes no sample, so a song's mix no longer reruns as the user
types; builds get 60 seconds.

probes/korobeiniki.ts arranges the folk song on all of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
A song is written as sections that fill named parts (a pattern again
until the section is full), with section effects and volumes; chords by
name with templates that draw their notes by number, voice leading;
transpose, stretch, repeat, beat; @ for how loud a step is; swing;
riser, impact, the kit's crash and pump. Step 2's arrangement written
again as sections gives the same file to the byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
Import Video… takes a .mid: its tracks are read (formats 0 and 1, the
first tempo, the time signature, names, programs, the drums on channel
10) and kept as a .notes file of the script. Clicking one shows its notes
on a roll and plays them roughly. music.midi(notes, { Piano: keys })
plays each track named with an instrument, a kit (General MIDI drum
numbers) or a part, quantized to the song's steps, velocities as accents,
overlapping notes split into voices.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
Judged by its samples, a limited mix peaked 1.1 to 1.6 dB higher between
them, and a dense one decoded at 0.99. Each sample's need now comes from
its true peak (windowed sinc, 16 a side, each eighth of a sample); the
ceiling stays −1.5 dB; decoded peaks −0.7 dB at the most. ENGINE is
sound 3, so maps make their files again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
music.stems cuts a song of sections into a clip a bar a layer, each
with its notes' tails, echoes and reverb; a Band made in the program
says which section and layers play, and conduct plays them at each bar
line, with stingers. together() turns clips that play together down by
one gain; Clip.ogg(quality) puts a clip in as Ogg Vorbis at its own
quality, however short.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
The game plays an Ogg of one channel 2.4 dB quieter than it was mixed,
so one channel is written twice. together() takes clips that may play
on top at any of their times and leaves room for them; stems passes its
stingers at every bar line, turned down with the bars. Step 4 as played.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
conduct wakes every beat: a stinger plays on the next one. stems takes
every (beats a piece), so a change can wait half a bar instead of a
whole one, for more files and more sounds at once. The probe cuts in
half bars.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
scripts/sound-dump.mts writes the sounds a build makes (or names them);
scripts/recording-check.mts checks a recording of a probe against them,
piece by piece too; stems.playthrough is the song as the game plays it.
The probes' answer sheets beside them, and the plan says how it all
goes together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUvpRKSfm2yqit18wN5muG
@jeany55
jeany55 merged commit dfb9dcf into trigscript-4 Oct 20, 2026
1 check passed
@jeany55
jeany55 deleted the music-quality branch October 20, 2026 00:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant