Accessible grid-based MIDI entry mode for REAPER
Keyboard- and speech-driven step entry for the MIDI editor, built for use with OSARA. Arrows walk grid cells and scale degrees, Enter toggles notes, and every position is spoken and auditioned. Uses REAPER's own edit cursor and active_note_row rather than private cursor state, so grid mode and OSARA editing stay in sync. Grid size comes from MIDI_GetGrid(), so existing grid keybindings drive it. When grid mode is off, every bound key forwards to its previous action. Preview timing lives in a background daemon so the action scripts can exit immediately -- a script still alive on the next keypress triggers REAPER's "already running" prompt and breaks key repeat.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# MIDI Grid — accessible quick MIDI entry for REAPER
|
||||
|
||||
A grid / step-sequencer entry mode for REAPER's MIDI editor, built for
|
||||
keyboard-and-speech use with [OSARA](https://osara.reaperaccessibility.com/).
|
||||
|
||||
Left and right walk the timeline one grid cell at a time. Up and down walk the
|
||||
pitch axis **by scale degree**. Enter toggles a note in the current cell.
|
||||
Everything you land on is spoken and sounded, so you can hear what is already
|
||||
there as you move.
|
||||
|
||||
It is **not** a replacement for the MIDI editor. It is a fast way to block out
|
||||
a clip; switch grid mode off and OSARA's normal editing is right there,
|
||||
unchanged, on the same cursor.
|
||||
|
||||
## Requirements
|
||||
|
||||
- REAPER
|
||||
- **OSARA** — for speech output
|
||||
- **SWS** — for the note-naming octave preference
|
||||
|
||||
No other extensions. No ReaPack, no js_ReaScriptAPI, no ReaImGui.
|
||||
|
||||
## Install
|
||||
|
||||
Clone or download this repository anywhere you like, then:
|
||||
|
||||
1. **Close REAPER completely.** It rewrites `reaper-kb.ini` when it exits and
|
||||
will throw away anything written while it is running.
|
||||
2. Run:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File install.ps1
|
||||
```
|
||||
|
||||
3. Start REAPER, select a MIDI item, press **`Alt+Shift+G`**.
|
||||
|
||||
You should hear something like *"Grid mode on, C major, grid 1/16, hold off,
|
||||
C5, 1.1.00"*.
|
||||
|
||||
To remove it again: `powershell -ExecutionPolicy Bypass -File install.ps1 -Uninstall`
|
||||
|
||||
The installer backs up `reaper-kb.ini` to `reaper-kb.ini.midigrid-backup`
|
||||
before its first change, and is safe to re-run.
|
||||
|
||||
### Why not "Import key map"?
|
||||
|
||||
`MidiGrid.ReaperKeyMap` is included for reference, but **importing it does not
|
||||
reliably work**, and the way it fails is silent and confusing: REAPER accepts
|
||||
the file, registers the script actions, and then converts every key binding it
|
||||
cannot resolve into a `No-op (no action)` entry. The keys appear bound in the
|
||||
action list but do nothing.
|
||||
|
||||
The cause is that a `KEY` line must reference the *named command* — the `SCR`
|
||||
id with a **leading underscore** (`_RSmidigrid_open`), not the bare id
|
||||
(`RSmidigrid_open`). `install.ps1` writes the underscored form directly.
|
||||
|
||||
If you would rather not run a script, you can bind the ten `MIDI Grid:`
|
||||
actions by hand in Actions → Show action list. That always works.
|
||||
|
||||
## Keys
|
||||
|
||||
**Main section**
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `Alt+Shift+G` | Open selected item in grid mode |
|
||||
|
||||
**MIDI editor section**
|
||||
|
||||
| Key | Grid mode ON | Grid mode OFF (passes through to) |
|
||||
|---|---|---|
|
||||
| `Left` / `Right` | Cursor back / forward one grid cell | OSARA previous / next chord |
|
||||
| `Up` / `Down` | Pitch up / down one scale degree | OSARA higher / lower note in chord |
|
||||
| `Enter` | Toggle note at cursor cell | Edit: Event properties |
|
||||
| `Alt+G` | **Toggle grid mode on/off** | — |
|
||||
| `Alt+H` | Toggle hold mode | — |
|
||||
| `Alt+A` | Repeat current cell (speak + sound) | — |
|
||||
| `Alt+S` | Set root and scale | — |
|
||||
| `1`–`9` | Grid size — REAPER's own grid actions | same |
|
||||
|
||||
`Alt+Shift+G` **opens** grid mode; it does not close it. Use `Alt+G` inside
|
||||
the editor to toggle back off.
|
||||
|
||||
## How it integrates
|
||||
|
||||
- **Grid size is REAPER's grid.** Read via `MIDI_GetGrid()`, so whatever you
|
||||
already have bound to the MIDI editor's grid actions (by default `1` = 1/1,
|
||||
`2` = 1/2, `4` = 1/4, `6` = 1/16, `8` = 1/8, `3` = 1/32) drives this too.
|
||||
Nothing new to learn.
|
||||
- **The cursor is REAPER's cursor.** Time is the native edit cursor; pitch is
|
||||
the MIDI editor's `active_note_row`. No private cursor state exists, so grid
|
||||
mode and OSARA cannot drift out of sync — you can switch between them
|
||||
mid-phrase.
|
||||
- **Turning grid mode off restores everything.** Each bound key forwards to
|
||||
what it did before, so nothing is permanently taken away.
|
||||
|
||||
## What gets spoken
|
||||
|
||||
- **Left / right** — cell position and everything sounding in it:
|
||||
`"3.2.00, C5, E5"`, and those notes are sounded together.
|
||||
- **Up / down** — the note name, plus `"on"` if this cell already holds that
|
||||
pitch: `"E5, on"`.
|
||||
- **Enter** — `"E5 on, added"` or `"E5 off, removed"`.
|
||||
|
||||
## Hold mode
|
||||
|
||||
With hold **off**, toggling adjacent cells at the same pitch gives separate
|
||||
repeated notes.
|
||||
|
||||
With hold **on**, a new note touching an existing note of the same pitch joins
|
||||
it into one sustained note. Toggling a cell off in the middle of a sustained
|
||||
run splits it in two; at either end, it shortens it. So you can draw a long
|
||||
note by arrowing right and pressing Enter across several cells.
|
||||
|
||||
## Scales
|
||||
|
||||
`Alt+S` prompts for a root (`C`, `F#`, `Bb`…) and a scale number. Up and down
|
||||
then move by scale degree, which is what makes entry fast — you cannot land on
|
||||
a wrong note by accident. Pick `chromatic` for plain semitone movement.
|
||||
|
||||
If the cursor sits on a note outside the current scale, up/down move to the
|
||||
nearest scale tone in that direction rather than getting stuck.
|
||||
|
||||
## Architecture notes
|
||||
|
||||
**The action scripts never call `reaper.defer`.** This matters. If a script is
|
||||
still alive when you press its key again, REAPER interrupts you with a "script
|
||||
is already running — terminate or launch new instance?" prompt, which makes
|
||||
fast key repeat impossible and, if you answer "terminate" and tick remember,
|
||||
permanently costs you every second keypress.
|
||||
|
||||
So preview note timing lives in `MidiGrid_Daemon.lua`, a single background loop
|
||||
that auto-starts on first use and relaunches itself if it ever dies. Action
|
||||
scripts post a request to transient `ExtState` and exit within a millisecond. A
|
||||
new request cuts the previous note, so fast arrowing retriggers cleanly. The
|
||||
daemon releases everything on `atexit`, so notes cannot hang.
|
||||
|
||||
Preview requests use **non-persisted** `ExtState` — persisting them would write
|
||||
to `reaper.ini` on every keypress.
|
||||
|
||||
## Tunables
|
||||
|
||||
In `ExtState` section `midigrid`:
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `vel` | 96 | Velocity of inserted notes |
|
||||
| `chan` | 0 | MIDI channel (0-based) |
|
||||
| `previewdur` | 0.4 | Preview note length, seconds |
|
||||
|
||||
## Tests
|
||||
|
||||
`test_lib.lua` covers the pure logic — scale stepping, note naming, grid
|
||||
labels — against a stubbed `reaper` table, so it runs outside REAPER:
|
||||
|
||||
```
|
||||
lua test_lib.lua
|
||||
```
|
||||
|
||||
Everything touching MIDI data or the REAPER API is only exercised in REAPER
|
||||
itself.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Preview uses `StuffMIDIMessage`, which plays through the track's instrument
|
||||
via the virtual-keyboard input, so the track must be armed / monitored —
|
||||
exactly as with REAPER's own MIDI preview.
|
||||
- The grid aligns to the project timeline (bar lines), not the item start. For
|
||||
an item starting off-grid, the first cell will be partial.
|
||||
- Note octave naming follows the `midioctoffs` preference; with the default,
|
||||
note 60 reads as C5.
|
||||
Reference in New Issue
Block a user