# 07 — Reaper Integration ## Goal PatternTalk lives alongside Reaper, not inside it (for the 2-day hackathon). Integration happens at three levels: 1. **Auto-detect project tempo** via Reaper's Web Control surface 2. **MIDI export** that's drag-and-droppable into Reaper 3. **Optional ReaScript bridge** for richer control (stretch) The CLAP plugin wrap is a stretch goal — see [`02-architecture.md`](02-architecture.md#decision-5-reaper-first-daw-integration). ## Reaper Web Control surface Reaper has a built-in HTTP server you can enable in Preferences → Control Surfaces → Web Browser Interface. **Default URL:** `http://localhost:8080` **Default endpoints we care about:** | Endpoint | Returns | |---|---| | `GET /_/` | HTML control panel (we ignore this) | | `GET /_/action?name=...` | Run a named action | | `GET /_/set?param=value` | Set a parameter | | `WS ws://localhost:8080/_/` | WebSocket for live state | **Project tempo** is exposed via the WebSocket. On connect: ```json { "type": "state", "data": { "tempo": 174, "timesig": [4, 4], "playstate": 0, "position": 0 } } ``` We subscribe to `state` updates and use the current tempo as the default for new patterns. ## Client-side Reaper detection ```typescript // apps/web/lib/reaper/client.ts class ReaperClient { private socket: WebSocket | null = null; private listeners: Set<(state: ReaperState) => void> = new Set(); private state: ReaperState = { connected: false, tempo: null, timeSignature: null, }; async connect(): Promise { try { this.socket = new WebSocket("ws://localhost:8080/_/"); this.socket.onopen = () => { this.state.connected = true; this.notify(); }; this.socket.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === "state") { this.state.tempo = msg.data.tempo; this.state.timeSignature = msg.data.timesig; this.notify(); } }; this.socket.onerror = () => { this.state.connected = false; this.notify(); }; return true; } catch (e) { return false; } } getTempo(): number | null { return this.state.tempo; } isConnected(): boolean { return this.state.connected; } subscribe(listener: (state: ReaperState) => void): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); } private notify() { for (const listener of this.listeners) { listener(this.state); } } } interface ReaperState { connected: boolean; tempo: number | null; timeSignature: [number, number] | null; } export const reaper = new ReaperClient(); ``` ## Tempo resolution priority When the user generates a pattern, PatternTalk picks the tempo in this order: 1. **Explicit in prompt:** "set tempo to 160" or "180 BPM" → 160 or 180 2. **From Reaper project:** If connected → use project tempo 3. **From uploaded audio:** If user uploaded an audio file → detected BPM 4. **Default:** 120 BPM ```typescript // apps/web/lib/parser/tempo.ts export async function resolveTempo( parsed: ParsedRequest, reaper: ReaperClient, uploadedAudio: AudioBuffer | null ): Promise { if (parsed.tempo) { return { tempo: parsed.tempo, source: "prompt" }; } if (reaper.isConnected() && reaper.getTempo()) { return { tempo: reaper.getTempo()!, source: "reaper" }; } if (uploadedAudio) { const bpm = await detectBpm(uploadedAudio); if (bpm) { return { tempo: bpm, source: "audio" }; } } return { tempo: 120, source: "default" }; } ``` ## MIDI export The MIDI file is generated in-browser. The user downloads it as a `.mid` file, then drags it onto a Reaper track. ### File naming ``` patterntalk-{pattern-id}-{bars}bars-{bpm}bpm-{timestamp}.mid ``` Examples: - `patterntalk-d-beat-4bars-180bpm-2026-08-22T1430Z.mid` - `patterntalk-skank-4bars-120bpm-2026-08-22T1435Z.mid` ### Drag-and-drop into Reaper Reaper accepts MIDI files dropped from the file system onto a track. We make this explicit: ```typescript // apps/web/components/midi/DownloadButton.tsx function downloadMidi(events: MidiEvent[], meta: PatternMeta) { const midi = eventsToMidi(events, meta); const blob = new Blob([midi], { type: "audio/midi" }); const url = URL.createObjectURL(blob); const filename = `patterntalk-${meta.patternId}-${meta.bars}bars-${meta.bpm}bpm-${new Date().toISOString()}.mid`; // Trigger download const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); // Cleanup setTimeout(() => URL.revokeObjectURL(url), 1000); return filename; } ``` ### Drag-from-browser directly into Reaper For the smoothest demo, the MIDI file should be draggable from the browser window directly onto a Reaper track. Reaper accepts this if the file is exposed as a real file (not a Blob URL), which means we need to keep the file in memory and reference it via the DataTransfer API. ```typescript function makeMidiDraggable( events: MidiEvent[], meta: PatternMeta, element: HTMLElement ) { const midi = eventsToMidi(events, meta); const filename = `patterntalk-${meta.patternId}-${meta.bars}bars-${meta.bpm}bpm.mid`; element.draggable = true; element.ondragstart = (e) => { const file = new File([midi], filename, { type: "audio/midi" }); e.dataTransfer!.files = [file]; // Some browsers need this e.dataTransfer!.setData("DownloadURL", `audio/midi:${filename}:${e.dataTransfer!.getData("DownloadURL")}`); }; } ``` **Caveat:** Browser support for dragging real files (not blob URLs) into native apps varies. Test in Chrome on the demo machine before relying on this. If drag-from-browser is flaky, the fallback is "click to download, then drag from Downloads folder to Reaper." Annoying but reliable. ## Verifying MIDI works in Reaper Before the demo, smoke-test the MIDI export: 1. Open Reaper, create a new project at 180 BPM, 4/4 2. Add a track, load any drum sampler VST (free options: Drumgizmo, MT Power Drum Kit, or Reaper's built-in ReaDrumCrafter) 3. Download a PatternTalk MIDI file 4. Drag onto the track 5. Hit play — confirm the pattern plays correctly Common bugs to watch for: - Tempo mismatch (Reaper plays at project tempo; MIDI file tempo should match) - Wrong GM drum map notes (kick = 36, snare = 38, etc.) - Notes too short or too long (drum hits should be very short, ~1-10 ticks) - MIDI file doesn't import at all (file format corruption) ## ReaScript bridge (stretch) For richer integration, a small ReaScript can: - Auto-create a new track and load the MIDI when PatternTalk generates - Set the project tempo to match the pattern - Start playback automatically ```lua -- scripts/reaper/patterntalk_bridge.lua -- Receives HTTP requests from PatternTalk -- Endpoint: http://localhost:8081/... -- For each request, execute a Reaper action function onRequest(method, path, body) if path == "/import-midi" then local filepath = body.filepath local trackIndex = body.trackIndex or 0 Reaper.MIDI_InsertMedia(filepath, trackIndex) return { ok = true } end if path == "/set-tempo" then local tempo = body.tempo reaper.SetProjectTimeSignature(0, tempo, ...) return { ok = true } end end ``` This is a stretch goal. If we have a teammate who's comfortable with ReaScript, we ship it. If not, the Web Control surface + MIDI drag-and-drop is the integration story. ## Web Control endpoint reference The full Reaper Web Control API is documented at: https://www.reaper.fm/developers/webcontrol.php Key endpoints for PatternTalk: | Action | Endpoint | |---|---| | Get full state | `WS /_/` | | Run named action | `GET /_/action?name=` | | Set project tempo | `GET /_/set?project_tempo=` | | Get current tempo | (via WebSocket state) | | Transport play | `GET /_/action?name=40044` (transport: play) | | Transport stop | `GET /_/action?name=40044` (toggle, state-dependent) | **Note:** Reaper's Web Control API is reverse-engineered more than documented. The state format isn't formally specced. Expect some brittleness; verify on the actual Reaper version (currently 7.x). ## Demo integration flow For the live demo, the integration sequence is: 1. **Reaper is already open** with a project at ~174 BPM (a typical d-beat tempo) 2. **PatternTalk opens** in a browser window next to Reaper 3. **PatternTalk detects Reaper**, announces "Reaper connected at 174 BPM" 4. **User generates a pattern** — PatternTalk uses 174 from Reaper 5. **User downloads or drags MIDI** — drops into a Reaper track with a drum VST 6. **Reaper plays** — pattern sounds correct 7. **Optional:** Change Reaper project tempo to 160, regenerate in PatternTalk, drag new MIDI, plays at new tempo The whole sequence, with audio, takes about 30 seconds. Memorable. ## What this integration is NOT - Not a VST/AU/CLAP plugin (stretch goal only) - Not bidirectional — PatternTalk reads from Reaper, doesn't write project state - Not automatic — user still has to drag the MIDI file in - Not a replacement for Reaper's built-in features This is intentional. The 80/20 here is "auto-detect tempo + clean MIDI export." Everything else is stretch.