Files
midigrid/CLAUDE.md
T
TalonandClaude Opus 5 0acb7d58f6 Play preview through the edited track's instrument
StuffMIDIMessage feeds the virtual MIDI keyboard, which only reaches
tracks that are armed and monitoring -- normally not the track whose
item you are editing. Opening an item in grid mode therefore sounded
every preview through whatever instrument happened to be armed, which
is confusing when the notes you are writing come back in the wrong
voice.

Grid mode now borrows the record routing: the edited take's track is
armed and monitoring with all MIDI inputs (anything narrower can
exclude the virtual keyboard), and every other armed track is stood
down so it cannot answer as well. The prior arm, monitor and input of
each track touched is saved by GUID in transient ExtState.

routePreview re-points lazily, so moving the editor to an item on
another track costs nothing until the target actually changes. The
daemon hands the routing back when grid mode goes off, when the editor
closes, or on exit -- the same way it already restores the loop and
repeat state the loop-bar action borrows -- so the project is put back
even if grid mode is never toggled off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 00:55:25 +02:00

96 lines
5.1 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A REAPER MIDI-editor grid/step-entry mode written in Lua, built for
keyboard-and-speech use with OSARA. Pure ReaScript — no ReaPack, no
js_ReaScriptAPI, no ReaImGui. `README.md` is the user manual and is unusually
detailed; read the relevant section before changing behaviour, and update it
when behaviour changes.
## Commands
```
lua test_lib.lua # run the tests
powershell -ExecutionPolicy Bypass -File install.ps1 # install on Windows (REAPER must be CLOSED)
powershell -ExecutionPolicy Bypass -File install.ps1 -Uninstall
./install.sh # install on macOS/Linux
./install.sh --uninstall
```
There is no build, no lint, no package manager, no test framework. `test_lib.lua`
is a single script of `eq(got, want, label)` assertions printing `ALL PASS` or a
failure count; to run one case, comment out the others. It stubs a global
`reaper` table, so it only covers the pure logic (scale stepping, note naming,
grid labels, chord construction). Anything touching MIDI data or the REAPER API
can only be exercised inside REAPER. Note it `dofile`s an **absolute** path to
`midigrid_lib.lua`.
## Architecture
**`midigrid_lib.lua`** holds essentially all the logic as module `M`. Every
action script is a thin adapter: resolve its own directory from
`reaper.get_action_context()`, `dofile` the lib, check `G.isActive()`, and either
`G.passThrough(...)` or call a couple of lib functions. New behaviour belongs in
the lib; new action scripts should stay ~10 lines.
**No private cursor state.** Time is REAPER's edit cursor; pitch is the MIDI
editor's `active_note_row`; cell size is `MIDI_GetGrid()`. This is what lets grid
mode and plain OSARA editing interleave without drifting, and why the user's
existing grid-size keybindings just work. Do not introduce a cursor of your own.
**Action scripts must never call `reaper.defer`.** A still-running script makes
REAPER show its "already running — terminate or launch new instance?" prompt on
the next keypress, which destroys key repeat. All timing lives in
`MidiGrid_Daemon.lua`, one background loop that auto-starts (`G.ensureDaemon()`),
heartbeats via ExtState, refuses to start a second copy, and releases notes on
`atexit`. Actions post a request to ExtState and exit immediately. If you need
something to happen later — note release, stop-at-bar-end, restoring borrowed
loop/repeat state — add it to the daemon loop, not to an action.
**ExtState section `midigrid`** is the only state. Persisted (`M.set`/`M.get`):
`active`, `hold`, `root`, `scale`, `vel`, `velstep`, `chan`, `previewdur`,
`stoplead`. Transient (`M.setTemp`/`M.getTemp`, persist flag false): `seq`,
`pitches`, `dur`, `hb`, `stopat`, `stoparm`, `savedloop`, `savedrep`,
`savedarm`, `routed`. Preview traffic must stay transient — persisting it
would write `reaper.ini` on every keypress.
**Preview is routed to the edited track.** `StuffMIDIMessage` feeds the virtual
MIDI keyboard, which only reaches armed, monitoring tracks, so `M.routePreview`
borrows the record arm/monitor/input of the take's track (standing down other
armed tracks) and `M.restoreRouting` hands it all back. `M.preview` re-points
lazily on every call, so switching items just works. The daemon restores when
grid mode goes off, the editor closes, or REAPER exits.
**Every grid key falls back.** When grid mode is off, each bound key forwards to
what it did before via `G.passThrough` (e.g. `_OSARA_PREVCHORD`, or a numeric
command id), so nothing is permanently taken away.
**All feedback is speech**, via `G.say``osara_outputMessage`, falling back to
the console when OSARA is absent. There is no window and no `gfx`.
Musical positions are converted through quarter-notes (`MIDI_GetPPQPosFromProjQN`
and friends), not raw ticks, so edits stay correct across tempo changes.
## Installation model
`install.ps1` (Windows) and `install.sh` (macOS/Linux) edit `reaper-kb.ini`
directly and are idempotent (they strip all `RSmidigrid` lines first, and back
up to `reaper-kb.ini.midigrid-backup`). REAPER rewrites that file on exit, so
it must be closed or changes are lost. The two scripts are deliberate mirrors:
same action list, same key table, same output. **Any change to one must be made
to the other.** They differ only in resource-path discovery
(`%APPDATA%\REAPER` vs `~/Library/Application Support/REAPER`) and in
`install.sh --real-control`, which exists because modifier bit 8 means Ctrl on
Windows but Command on macOS (physical Control is bit 32).
Adding a new action means adding a row to **both** tables in each installer: the
`$actions` table (`SCR 4 <section> <id> "<description>" <path>`) and, if it needs
a key, `$keys` (flags: 1 base, +4 Shift, +8 Ctrl, +16 Alt; section 0 = Main,
32060 = MIDI Editor). `KEY` lines must reference the named command — the SCR id
with a **leading underscore** (`_RSmidigrid_open`). Without the underscore REAPER
silently converts the binding to a no-op; this is exactly why importing
`MidiGrid.ReaperKeyMap` does not work and the file is kept for reference only.