2026-08-14 18:13:45 +02:00
|
|
|
|
# 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 |
|
2026-08-14 18:27:14 +02:00
|
|
|
|
| `L` / `Shift+L` | Lengthen / shorten note by one cell | Edit: Lengthen / shorten notes one grid unit |
|
|
|
|
|
|
| `Alt+J` | Join a run of repeated notes into one | — |
|
2026-08-14 18:18:12 +02:00
|
|
|
|
| `Alt+3` | Toggle diatonic triad on cursor pitch | — |
|
|
|
|
|
|
| `Alt+7` | Toggle diatonic seventh chord | — |
|
|
|
|
|
|
| `Alt+Shift+Up` / `Alt+Shift+Down` | Velocity up / down | — |
|
2026-08-14 18:13:45 +02:00
|
|
|
|
| `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.
|
|
|
|
|
|
|
2026-08-14 18:27:14 +02:00
|
|
|
|
Hold mode governs **entry only** — it never retroactively merges notes that
|
|
|
|
|
|
are already there. To sustain what you have already written, use the length
|
|
|
|
|
|
keys below.
|
|
|
|
|
|
|
|
|
|
|
|
## Note length — sustaining notes you already entered
|
|
|
|
|
|
|
|
|
|
|
|
- **`L`** — lengthen the note under the cursor by one grid cell.
|
|
|
|
|
|
- **`Shift+L`** — shorten it by one cell. It never shrinks below a single
|
|
|
|
|
|
cell; it says *"minimum"* rather than deleting the note.
|
|
|
|
|
|
- **`Alt+J`** — fuse an entire run of repeated notes at this pitch into one
|
|
|
|
|
|
sustained note, in a single keystroke.
|
|
|
|
|
|
|
|
|
|
|
|
Lengthening **swallows** any same-pitch note it runs into, taking that note's
|
|
|
|
|
|
tail if it reached further. So pressing `L` across a row of repeated notes
|
|
|
|
|
|
fuses them rather than producing overlaps.
|
|
|
|
|
|
|
|
|
|
|
|
Each press reports the resulting length — *"E5, 3 cells"*, or *"E5, 2 cells,
|
|
|
|
|
|
joined 1"* when it absorbed something.
|
|
|
|
|
|
|
|
|
|
|
|
### Why not re-toggle with hold on instead?
|
|
|
|
|
|
|
|
|
|
|
|
Turning hold on and re-entering existing notes does work. But toggling a note
|
|
|
|
|
|
off and on **deletes and recreates it, resetting its velocity to the default**
|
|
|
|
|
|
— if you have shaped dynamics, that quietly discards them.
|
|
|
|
|
|
|
|
|
|
|
|
The length keys edit the note in place, so velocity, channel and note identity
|
|
|
|
|
|
survive. They also need no mode: `L` behaves identically whether hold is on or
|
|
|
|
|
|
off, so there is no state to set up beforehand and none to remember to turn
|
|
|
|
|
|
off afterwards.
|
|
|
|
|
|
|
2026-08-14 18:18:12 +02:00
|
|
|
|
## Chords
|
|
|
|
|
|
|
|
|
|
|
|
`Alt+3` stamps a **diatonic triad** on the cursor pitch; `Alt+7` a seventh
|
|
|
|
|
|
chord. Both are built by stacking scale thirds, so the quality follows the
|
|
|
|
|
|
degree automatically — in C major, `Alt+3` on C gives C major, on D gives D
|
|
|
|
|
|
minor, on B gives B diminished. There is no chord-quality table to configure
|
|
|
|
|
|
and no way to land on a chord outside the key.
|
|
|
|
|
|
|
|
|
|
|
|
If every tone of the chord is already in the cell, the keystroke removes them
|
|
|
|
|
|
all; otherwise it fills in whatever is missing. Chord tones obey hold mode,
|
|
|
|
|
|
splitting and trimming exactly like single notes.
|
|
|
|
|
|
|
|
|
|
|
|
You can also build chords by hand at any time — arrow to a pitch, press Enter,
|
|
|
|
|
|
arrow to another, press Enter. The cursor never moves horizontally when you
|
|
|
|
|
|
toggle, so stacking notes in one cell has always worked. `Alt+3` and `Alt+7`
|
|
|
|
|
|
are just the one-keystroke version.
|
|
|
|
|
|
|
|
|
|
|
|
In the chromatic scale, "stacking thirds" degenerates into stacked whole
|
|
|
|
|
|
tones. Predictable, but not musically useful — pick a real scale first.
|
|
|
|
|
|
|
|
|
|
|
|
## Velocity
|
|
|
|
|
|
|
|
|
|
|
|
`Alt+Shift+Up` and `Alt+Shift+Down` change velocity in steps of 8.
|
|
|
|
|
|
|
|
|
|
|
|
The key does one of two things depending on where you are, and says which:
|
|
|
|
|
|
|
|
|
|
|
|
- **On a note** — changes that note's velocity: *"E5 velocity 104"*, and
|
|
|
|
|
|
replays it at the new level so you can hear the change.
|
|
|
|
|
|
- **On an empty cell** — changes the default for newly inserted notes:
|
|
|
|
|
|
*"Default velocity 104"*.
|
|
|
|
|
|
|
|
|
|
|
|
So you can set a level and then draw at it, or draw first and shape the
|
|
|
|
|
|
dynamics afterwards, without a mode switch either way.
|
|
|
|
|
|
|
|
|
|
|
|
Set `velstep` in `ExtState` if steps of 8 are too coarse or too fine.
|
|
|
|
|
|
|
2026-08-14 18:13:45 +02:00
|
|
|
|
## 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 |
|
2026-08-14 18:18:12 +02:00
|
|
|
|
| `velstep` | 8 | Velocity change per keypress |
|
2026-08-14 18:13:45 +02:00
|
|
|
|
| `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.
|