How "Dead at Midnight" was made

This records how the Halloween set in circuits/halloween-*.ttl became a three minute track, so the work can be repeated, changed or done differently. The brief was in docs/halloween-examples.md: build a set of instruments with MCP tools, then use them to make a Halloween song in Reaper.

The result is in ~/Music/dead-at-midnight/. The sleeve is docs/dead-at-midnight-sleeve.svg and the words are in docs/dead-at-midnight-lyrics.md.

The short version

  1. Each instrument was designed and checked through the Valis MCP server (describe_element, load_file, lint, render, analyze, check_stability) and saved as a Turtle file in circuits/.
  2. The song was written as a Python script that writes one MIDI file per instrument.
  3. Each circuit was rendered offline against its MIDI file with valis-render, giving one audio stem per instrument.
  4. reaper-mcp built a Reaper project, imported the stems and set the mix.

Reaper does not run the instruments. It holds and mixes recordings of them. The reason is in Why the stems are not live plug-ins.

The instruments

file what it is MIDI notes
halloween-drums-low.ttl kick with a long sub tone, two low toms into a dark hall, a noise slam, through tape and a limiter 36 kick, 39 slam, 41 and 45 toms
halloween-drums-high.ttl closed hat, open hat through a feedback frequency shifter, shaker, wood block into an echo, clap into a hall 39 clap, 42, 46, 70, 75
halloween-bass.ttl three layers: sine sub an octave down, detuned saws through a ladder filter with a tempo-synced wobble, clipped wavetable growl play from E2 (40) up; the sub sounds an octave below
halloween-organ.ttl eight voices of six sine drawbars at the Hammond footages, key click, overdrive, chorus as a scanner vibrato, large room polyphonic
halloween-guitar.ttl a plucked string (a comb filter with allpass dispersion, close to Karplus-Strong) with pickup, amp clip, spring-style reverb and a dotted-eighth echo in tune near G3 to G4
halloween-theremin.ttl sine with a scoop into each note, vibrato and drift, phaser, echo, hall one note at a time
halloween-ornaments1.ttl two drifting vowel wavetables and a modal glass bell, with chorus, spectral delay and hall one note at a time
halloween-ornaments2.ttl a falling-saucer dive, wavefolded, bit-crushed and smeared by a feedback frequency shifter, with a stuttering echo short notes

Each header comment says how to play it. All of them lint clean, pass check_stability, and are covered by the existing tests that run over every file in circuits/.

Design notes worth keeping

The song

140 BPM, 4/4, E minor, half-time feel (kick on the first beat, snare on the third). Chords change every two bars: Em, D, C, B. 104 bars, about 2:58. A bar lasts 1.714 seconds.

section bars time what plays
intro 1 to 8 0:00 organ, ghost choir, theremin from bar 5, a few toms
verse 1 9 to 24 0:14 low drums, hats, guitar riff, bass from bar 13, organ, ghost choir every four bars
build 25 to 32 0:41 kick speeds up, hat and snare crescendo, saucer stabs
drop 1 33 to 48 0:55 full bass, full drums, guitar, organ
break 49 to 56 1:22 organ, theremin, ghost choir, ornaments, sparse drums
verse 2 57 to 72 1:36 as verse 1, with the theremin from bar 65
drop 2 73 to 88 2:03 as drop 1, with the theremin lead
outro 89 to 104 2:31 layers fall away

The notes are written by score/song.py, which places every note on a sixteenth-note grid, one bar at a time. Hi-hat velocities use a fixed random seed, so a run always gives the same MIDI.

Making the stems

valis-render gained a --midi <file> option for this. It reads every note-on and note-off in a standard MIDI file and places each on the sample timeline at --tempo. An event fires in the block that contains it, at its own offset, never by comparing against a block start (see the rule on this in CLAUDE.md). Only note events and one fixed tempo are read.

cmake --build build                       # build everything first; see below
D=~/Music/dead-at-midnight
build/valis_render_artefacts/Debug/valis_render circuits/halloween-bass.ttl \
    --midi $D/midi/bass.mid --tempo 140 --rolling --seconds 186 -o $D/stems/bass.wav

The Reaper project

reaper-mcp created dead-at-midnight.rpp at 140 BPM with eight audio tracks, imported each stem at 0:00, and set these levels and pans:

track volume pan
Drums Low -6 dB centre
Drums High -4 dB 25% right
Bass -3 dB centre
Organ +4 dB centre
Guitar -6 dB 30% left
Theremin -6 dB 20% right
Ornaments 1 Ghost Choir -8 dB centre
Ornaments 2 Saucer -5 dB centre

The master is at -5 dB with a ReaLimit limiter. The summed stems peak above 0 dB, so the master level and limiter matter. The master has two ReaLimit plug-ins because of a reaper-mcp bug (TODO.md in that repository).

Reaper's own render through reaper-mcp failed, so the first mix was made by summing the stems in Python with the same gains and pans (dead-at-midnight-reference-mix.wav). The project was then rendered by hand from Reaper's own Render dialog, which worked: dead-at-midnight-stems.wav, 186 seconds, 24-bit stereo at 48 kHz, peak -0.01 dBFS, loudness about -11 dB RMS with nothing clipped. That is the file to use. Saving the project with its media copied gave dead-at-midnight-stems.RPP and a Media/ folder holding the stems, so that project is self-contained and dead-at-midnight.rpp (which points at stems/) is the lighter one. The render is 186 seconds because the stems are; trim the silent tail after the last echo if a release needs an exact length.

The MIDI project

dead-at-midnight-midi.rpp holds the arrangement itself rather than the recorded stems. It has the same eight tracks, named as in the table above, at 140 BPM. Each track has one item of about 178.6 seconds that reads its notes from midi/<instrument>.mid, so the notes stay in the files that song.py wrote and are not copied into the project.

scripts/halloween/mkrpp.py writes the project text directly, because reaper-mcp has no tool to import a MIDI file. Reaper then loaded it through load_project, and the levels and pans in the table were applied with set_track_volume and set_track_pan. Those settings are not in mkrpp.py, so running it again gives a project at unity gain and centre pan.

The tracks make no sound on their own. To hear them, add a Valis plug-in to each track and load the matching circuits/halloween-*.ttl. See the next section for why that step is still manual.

Why the stems are not live plug-ins

When the song was made, each Valis plug-in instance started its own MCP server on the same port (7676 by default), and a circuit chosen over MCP lives only in that instance. So an agent could not give eight instances in one Reaper session eight different circuits. The port half of this is now fixed: an instance whose port is taken moves up to the next free one, and /health reports the port and the host's track name (see docs/manual/mcp.md, "Several instances").

What is still missing is a way to give an instance its circuit without opening its window: a circuit path in the plug-in's saved state, so a project can be written by a tool. Until then, a project holds recordings, and a session with eight live tracks would need each circuit loaded over its own port after the project is opened. This is in TODO.md.

Setting up the tools

What had to be true before any of this worked. None of it is obvious from the error messages.

Valis MCP server.

reaper-mcp (/chalet/github/reaper-mcp).

Things that went wrong

All are in TODO.md in this repository or in reaper-mcp's TODO.md. The ones that cost time:

Where everything is

In this repository:

In ~/Music/dead-at-midnight/:

To change something