Claude can't click through REAPER, and it can't hear. It can drive REAPER through REAPER's own scripting API, and it can measure audio. So each piece is written as code that builds a REAPER session. REAPER renders it, a script measures the result, and you listen and give your verdict.
song file (code) → session file + job → REAPER builds and renders it → measure → you listen → revise
Run a second REAPER beside your own, with its own settings folder, so Claude's jobs never touch your setup:
REAPER -newinst -nosplash -cfgfile <folder>/reaper.ini bridge.lua
- The settings folder: copy your plug-in caches (
reaper-vstplugins*.ini,reaper-auplugins*.ini) and licence (reaper-reginfo2.ini) into it, so your plug-ins are found. In itsreaper.ini, turn off loading the last project and setrenderclosewhendoneso render dialogs close themselves. bridge.luais areaper.deferloop that watches a jobs folder.- A job is a Lua file that returns a table, run with
dofile. - The answer goes back as a JSON file: write it under a temporary name, then rename it, so the reader never sees half a file.
- Take jobs oldest first, so several Claude agents can queue for one REAPER.
- Keep the handlers in a second file that is re-read for every job, so you can change them without restarting REAPER.
- A job is a Lua file that returns a table, run with
local JOBS = os.getenv("JOBS") -- set it when launching REAPER
local here = debug.getinfo(1, "S").source:match("^@(.*/)") or "./"
local function loop()
reaper.EnumerateFiles(JOBS, -1) -- drop the cached listing
local f = reaper.EnumerateFiles(JOBS, 0) -- (sort by name for oldest-first)
if f and f:match("%.job%.lua$") then
local ok, res = pcall(function() return dofile(here .. "handlers.lua").handle(dofile(JOBS .. "/" .. f)) end)
os.remove(JOBS .. "/" .. f)
-- write { ok, res or error } to <name>.out.tmp, then os.rename it to <name>.out.json
end
reaper.defer(loop)
end
loop()Generate a skeleton .rpp (plain text) with the tracks, the MIDI and audio items, and any plug-in state you saved once from a track chunk. The job then does everything else through the API:
| Step | API |
|---|---|
| Open the skeleton | Main_openProject("noprompt:" .. path) |
| Fader, pan, folders, colour, track delay | SetMediaTrackInfo_Value: D_VOL, D_PAN, I_FOLDERDEPTH, D_PLAY_OFFSET |
| Add an effect by name | TrackFX_AddByName ("ReaEQ (Cockos)", "JS:<path>") |
| Set a parameter | TrackFX_SetParamNormalized, or by its shown value (below) |
| A sampler zone | ReaSamplOmatic5000, then the named configs FILE0 = path, DONE, MODE = 2 |
| Sends | CreateTrackSend, SetTrackSendInfo_Value |
| Volume and pan envelopes | actions 40406 and 40407 on the selected track, then InsertEnvelopePoint |
| An effect parameter's envelope | GetFXEnvelope(track, fx, param, true) |
| Markers, regions, notes | AddProjectMarker2, GetSetProjectNotes |
| Render | GetSetProjectInfo(_String): RENDER_FILE, RENDER_PATTERN, RENDER_FORMAT, RENDER_SETTINGS; then action 42230 |
- Set parameters in real units. Ask Claude to write a helper that binary-searches
TrackFX_FormatParamValueNormalizeduntil the shown value matches what you asked for ("−12 dB", "800 Hz"). Songs then say what you'd type. - Make a parameter catalog first. A job that adds each effect you'll use and dumps every parameter's name and shown range to JSON, so Claude never guesses a name.
- Render formats are base64 strings:
ZXZhdyAAAA==is WAV 32-bit float,ZXZhdxAAAA==WAV 16-bit. - What to render:
RENDER_SETTINGS0 renders the master; 3 renders each selected track on its own (stems). Render both: the mix to listen to, the stems to measure. - Save the project after rendering, so you can open it and hear it part by part.
- RS5K loads a file in "sample" mode. Set
MODEto 2, or every key plays at the recorded pitch. - ReaEQ's named configs take effect but report failure. Read the value back instead.
- ReaVerb can't load an impulse response without its window. Use a JSFX reverb, or another plug-in.
- "Volume (Pre-FX)" sits before the instrument on an instrument track, so riding it does nothing. Ride a gain plug-in placed after the instrument instead.
- For seamless loops, lay the loop down twice and keep the second pass. Tails, reverb and the master compressor then cross the seam as they really would. Blend the last few milliseconds into the first pass's end.
- Seed any randomness (humanised timing and velocity), so a rebuild is identical and changes are easy to compare.
- Never let two runs share a file. Give each render its own skeleton name.
Claude judges by measurement, and you judge by ear. Give it tools that print:
- The mix: integrated loudness and loudness range (
ffmpeg -af ebur128), peak, and balance by band (sub, low, mid, presence, air). - Each stem: its level while it plays, against the melody. This catches parts that are buried or too hot.
- Silent notes: a note whose stem stays quiet, which usually means it's out of the instrument's range.
- The seam: the step from the loop's last sample to its first.
- Before and after: render once without a set of tracks, then compare loudness and bands every half second. This shows what an added sound contributes, and whether it jumps out.
-
Build the toolkit first:
- the bridge;
- a small song API (tracks, notes, controller curves, clips, effects, sends, envelopes);
- a build command (lint, compile, render, report);
- the measuring tools.
Prove it with a short smoke song that uses every kind of part.
-
Write a composer's brief: the mood of each piece, the forms and lengths, the mix targets, the rules, and the workflow ("plan the form in the file's header, build, render, read the report, fix, render again").
-
One fresh subagent per piece, given the brief. A fresh context composes more freely than one that has just written five other songs.
-
Pilot one piece and give verdicts in your own words ("too slow", "stays too long on the same bass note"). Have Claude add each verdict to the brief, so every later piece starts from it.
-
Once the pilot is right, have Claude write down its principles and fan the rest out, a batch at a time, with a listening page that sets each new take beside the last.