2026-08-14 22:09:03 +02:00
|
|
|
# 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
|
2026-08-14 22:13:59 +02:00
|
|
|
powershell -ExecutionPolicy Bypass -File install.ps1 # install on Windows (REAPER must be CLOSED)
|
2026-08-14 22:09:03 +02:00
|
|
|
powershell -ExecutionPolicy Bypass -File install.ps1 -Uninstall
|
2026-08-14 22:13:59 +02:00
|
|
|
./install.sh # install on macOS/Linux
|
|
|
|
|
./install.sh --uninstall
|
2026-08-14 22:09:03 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
2026-08-24 01:15:57 +02:00
|
|
|
can only be exercised inside REAPER, though the note-jump tests show how far
|
|
|
|
|
a stubbed `MIDI_GetNote` will get you. It resolves `midigrid_lib.lua`
|
|
|
|
|
relative to its own path, so it runs from any checkout.
|
2026-08-14 22:09:03 +02:00
|
|
|
|
|
|
|
|
## 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`,
|
2026-08-24 00:55:25 +02:00
|
|
|
`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.
|
2026-08-14 22:09:03 +02:00
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
2026-08-14 22:13:59 +02:00
|
|
|
`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).
|
2026-08-14 22:09:03 +02:00
|
|
|
|
2026-08-14 22:13:59 +02:00
|
|
|
Adding a new action means adding a row to **both** tables in each installer: the
|
2026-08-14 22:09:03 +02:00
|
|
|
`$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.
|