jam-buddy / docs /07-reaper-integration.md
salgadev's picture
Sync from GitHub 6feaf31d
b2e4883 verified
|
Raw
History Blame Contribute Delete
9.32 kB

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.

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:

{
  "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

// 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<boolean> {
    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
// apps/web/lib/parser/tempo.ts

export async function resolveTempo(
  parsed: ParsedRequest,
  reaper: ReaperClient,
  uploadedAudio: AudioBuffer | null
): Promise<number> {
  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:

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

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
-- 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=<action_id>
Set project tempo GET /_/set?project_tempo=<bpm>
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.