Skip to content

Instantly share code, notes, and snippets.

@endel
Last active October 15, 2026 14:24
Show Gist options
  • Select an option

  • Save endel/59f9ec3fa15507b4a8469003e339154e to your computer and use it in GitHub Desktop.

Select an option

Save endel/59f9ec3fa15507b4a8469003e339154e to your computer and use it in GitHub Desktop.
The approach Claude Code is taking to generate my soundtracks using Reaper DAW

Making a soundtrack with Claude Code and REAPER

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

1. A REAPER kept open for Claude

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 its reaper.ini, turn off loading the last project and set renderclosewhendone so render dialogs close themselves.
  • bridge.lua is a reaper.defer loop 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.
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()

2. What a build job does

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_FormatParamValueNormalized until 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_SETTINGS 0 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.

3. Things that will bite

  • RS5K loads a file in "sample" mode. Set MODE to 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.

4. Listening by numbers

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.

5. Working with Claude Code

  1. 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.

  2. 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").

  3. One fresh subagent per piece, given the brief. A fresh context composes more freely than one that has just written five other songs.

  4. 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.

  5. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment