Files
midigrid/README.md
T

488 lines
22 KiB
Markdown
Raw Normal View History

# 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.
With a modifier the same arrows jump an octave, or straight to the next note
that actually exists — up the cell, along the pitch, or anywhere in the take.
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 the installer for your platform:
**Windows**
```powershell
powershell -ExecutionPolicy Bypass -File install.ps1
```
**macOS / Linux**
```bash
chmod +x install.sh # first time only
./install.sh
```
3. Start REAPER, select a MIDI item, press **`Alt+Shift+G`** (`Option+Shift+G`
on a Mac).
You should hear something like *"Grid mode on, C major, grid 1/16, hold off,
C5, 1.1.00"*.
To remove it again:
```powershell
powershell -ExecutionPolicy Bypass -File install.ps1 -Uninstall # Windows
```
```bash
./install.sh --uninstall # macOS / Linux
```
Both installers back up `reaper-kb.ini` to `reaper-kb.ini.midigrid-backup`
before their first change, and are safe to re-run.
### macOS notes
`install.sh` does exactly what `install.ps1` does — same actions, same
bindings — but finds the resource folder at
`~/Library/Application Support/REAPER` (falling back to `~/.config/REAPER` on
Linux). If yours is elsewhere, find it via **Options → Show REAPER resource
path** and pass it:
```bash
./install.sh --resource-path "/path/to/REAPER"
```
**Modifiers.** In `reaper-kb.ini` the modifier bit that means Ctrl on Windows
means **Command** on macOS; the physical Control key is a different bit. So by
default every `Ctrl+…` binding in the [key table](#keys) is `Command+…` on a
Mac, which is what Mac users expect. To bind the physical Control key instead:
```bash
./install.sh --real-control
```
`Alt` in the key table is the `Option` key on a Mac either way.
### Manual installation
If you would rather not run a script — or you want a different key layout —
you can do the whole thing from REAPER's UI. Nothing about MIDI Grid depends
on the installer; it only writes action registrations and key bindings.
1. **Register the scripts.** In REAPER, open **Actions → Show action list**,
set the section selector to **Main**, and choose **New action → Load
ReaScript…**. Select these two files from wherever you put the repository:
- `MidiGrid_OpenInGrid.lua`
- `MidiGrid_Daemon.lua`
Then switch the section selector to **MIDI Editor** and load the remaining
`MidiGrid_*.lua` files the same way (everything except `midigrid_lib.lua`
and `test_lib.lua`, which are not actions). The section matters: an action
loaded into the wrong section will not be offered for the keys you want.
2. **Bind the keys.** Still in the action list, select an action, click
**Add…** under Shortcuts, and press the key combination. Use the
[key table](#keys) below for the intended layout; you can of course choose
your own. Only `Alt+Shift+G` needs to be in the Main section — everything
else belongs in MIDI Editor.
3. **Say yes when REAPER warns about a key that is already bound.** Grid mode
deliberately takes over `Left`, `Right`, `Up`, `Down` and `Enter` in the
MIDI editor; each of those forwards to its previous behaviour when grid
mode is off, so nothing is lost.
You do not need to bind everything. The minimum useful set is
`Alt+Shift+G` (open), `Alt+G` (toggle), the four arrows, and `Enter`.
`MidiGrid_Daemon.lua` needs to be **registered** as an action but never needs
a key — it auto-starts.
### 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`). Both installers write the underscored form directly.
If you would rather not run a script, see [Manual installation](#manual-installation)
above. Binding by hand in the action list 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 |
| `Ctrl+Alt+Up` / `Ctrl+Alt+Down` | Pitch up / down one **octave** | OSARA higher / lower note in chord |
| `Alt+Up` / `Alt+Down` | Jump to the next note above / below **in this cell** | OSARA higher / lower note in chord |
| `Alt+Left` / `Alt+Right` | Jump to the previous / next note **at this pitch** | OSARA previous / next chord |
| `Ctrl+Alt+Left` / `Ctrl+Alt+Right` | Jump to the previous / next note **anywhere** | OSARA previous / next chord |
| `Enter` | Toggle note at cursor cell | Edit: Event properties |
2026-08-14 18:46:33 +02:00
| `Ctrl+Shift+Left` / `Ctrl+Shift+Right` | Shift **all** notes one cell, refusing if any would leave the item | — |
| `Ctrl+Alt+Shift+Left` / `Ctrl+Alt+Shift+Right` | Same, but shift anyway | — |
| `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 | — |
| `Alt+3` | Toggle diatonic triad on cursor pitch | — |
| `Alt+7` | Toggle diatonic seventh chord | — |
| `Alt+Shift+Up` / `Alt+Shift+Down` | Velocity up / down | — |
| `Alt+G` | **Toggle grid mode on/off** | — |
| `Alt+H` | Toggle hold mode | — |
2026-08-14 20:04:20 +02:00
| `Alt+Space` | Play from start of the current bar, onwards | works in both modes |
| `Ctrl+Alt+Space` | Play the current bar only, then stop | works in both modes |
| `Ctrl+Alt+Shift+Space` | Loop the current bar (exact) | works in both modes |
| `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.
- **Opening keeps your place.** `Alt+Shift+G` leaves the edit cursor where it
already was, rounded to the nearest grid line, instead of dropping it back to
the start of the item — so you can listen to a bar, hit it, and start writing
right there. A cursor outside the item lands on the item's first or last cell.
- **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"`.
## Octaves
`Ctrl+Alt+Up` and `Ctrl+Alt+Down` move a whole octave, keeping the pitch class.
This needs no scale awareness, because an octave is transpositionally neutral:
a scale tone stays a scale tone, and a note you deliberately put outside the
key stays outside it. It is also the quick way to cover distance — seven
presses of `Up` to cross an octave in a major scale, one press of
`Ctrl+Alt+Up`.
At either end of the MIDI range it says *"top"* or *"bottom"* and stays put,
rather than clamping to a partial jump.
## Jumping to notes that exist
Arrowing cell by cell is right inside a dense bar and tedious across an empty
one. Three pairs of keys jump straight to a note that is really there, one
pair for each axis you might be looking along.
- **`Alt+Up` / `Alt+Down` — the column.** The next note above or below *in the
cell you are already on*; time does not move. This is how you walk a chord
you have stacked up — it stops only where notes are, so you never count
scale degrees through the gaps. Speaks the note, as `Up` and `Down` do:
*"E5, on"*.
- **`Alt+Left` / `Alt+Right` — the row.** The next note *at the pitch you are
on*, backwards or forwards in time; the pitch cursor stays put. Use it to
follow one line — a bass note, a hi-hat — through a bar with plenty else
going on. Speaks the cell it landed in, as `Left` and `Right` do:
*"2.1.00, C5"*.
- **`Ctrl+Alt+Left` / `Ctrl+Alt+Right` — anywhere.** The next note in the take
whatever its pitch; both cursors move. This is the one for crossing empty
space. When it lands in a chord it takes whichever note is nearest the pitch
you came from, rather than always the bottom one, and names it when the cell
holds more than one: *"1.1.00, C5, E5, G5, cursor G5"*.
Two rules they all share.
**Muted notes are invisible to them.** A muted note makes no sound, so stopping
on one and announcing it would be a lie.
**A sustained note is one stop, not one per cell it covers.** The row and
any-note jumps anchor on note *starts*, so a note held across eight cells is a
single destination. Jumping backwards from inside a held note therefore takes
you to its beginning, which is usually where you wanted to go.
When there is nothing that way they say so and stay where they are — *"No note
above in this cell"*, *"No later E5"*, *"No later notes"* — so a key that does
not move you still tells you why.
These four and the octave keys sit on `Ctrl+Alt` rather than plain `Ctrl`
because REAPER already uses `Ctrl`+arrows to move between items, which is
worth keeping.
## 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.
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 19:57:55 +02:00
## Hearing it in context
2026-08-14 20:04:20 +02:00
Both of these play from the **top of the bar the cursor is in**, and say which
bar:
- **`Alt+Space`** — plays from the bar line and keeps going. Stop with `Space`.
- **`Ctrl+Alt+Space`** — plays that bar **only**, stopping just before the next
bar line. Nothing to stop by hand, so you can fire it repeatedly while
nudging a note and hear the same bar each time.
- **`Ctrl+Alt+Shift+Space`** — **loops** that bar, exactly. Repeats until you
press `Space`.
### Which of the two bar actions to use
`Ctrl+Alt+Space` stops by polling, and a polling loop can only be as precise as
its own interval — about 33 ms at frame rate. It therefore stops a little
early, and notes falling inside that window do not sound. In practice:
| Tempo | 16th note | 32nd note | 35 ms lead is |
|---|---|---|---|
| 120 BPM | 125 ms | 62 ms | 0.28 of a 16th, 0.56 of a 32nd |
| 180 BPM | 83 ms | 42 ms | 0.42 of a 16th, 0.84 of a 32nd |
| 240 BPM | 62 ms | 31 ms | 0.56 of a 16th, 1.12 of a 32nd |
So it is safe for 16ths at any sane tempo, and safe for 32nds up to about
214 BPM. Past that, the final 32nd of the bar is inside the lead and will not
be heard.
`Ctrl+Alt+Shift+Space` has no such limit. It hands the bar boundary to REAPER's
audio engine through the loop points, which is sample accurate, so the next
bar's downbeat can never leak in at any tempo or grid size. Use it when you are
working at 32nds or faster, or whenever you want certainty. Looping also suits
the job well — leave it running and edit while it plays.
It borrows the project's loop range and repeat state, and the daemon restores
both once transport stops, including when you stop with `Space`.
2026-08-14 19:57:55 +02:00
Auditioning a cell tells you what a note *is*; playing from the downbeat tells
you whether it is in the right *place*. At 16ths or 32nds that difference is
the whole question, and it cannot be judged from a note in isolation.
Your position is preserved. The edit cursor doubles as the grid cursor, so it
is moved to the bar line only long enough to start playback and then put
straight back — you carry on editing exactly where you were.
2026-08-14 20:04:20 +02:00
Both work whether or not grid mode is on, since they are listening aids rather
than editing actions.
The bar-only stop is carried out by the preview daemon, not by the action
script — a script that stayed alive waiting for the bar to end would trigger
REAPER's "already running" prompt on the next press. Stopping can move the
edit cursor depending on your preferences, so the daemon puts it back.
2026-08-14 19:57:55 +02:00
It stops **35 ms before** the bar line rather than on it, or a note sitting
exactly on the next bar line would sound. The test uses `GetPlayPosition2`,
which reports the block REAPER is about to render rather than what you are
currently hearing — that removes render-ahead from the equation, so the lead
only has to cover the daemon's own polling interval. (Testing against
`GetPlayPosition` instead needed 60 ms to cover both, which was enough to
swallow a whole 32nd note from 125 BPM upward.)
`stoplead` in `ExtState` tunes it. Raising it is the fix if a next-bar note
still sneaks through; lowering it recovers a little more of the bar's tail at
the risk of leaks.
2026-08-14 18:46:33 +02:00
## Shifting notes in time
If a whole clip landed a couple of cells off the beat, `Ctrl+Shift+Left` and
`Ctrl+Shift+Right` move **every note in the take** by one grid cell.
Because the entire take moves together, notes can never collide with each
other. What is at stake is the **item boundary**: notes pushed past either end
stop sounding. They are *not* deleted — shifting back the other way brings
them straight back — but a clip that has quietly lost its first bar is not
something you want to discover two hours later.
So the plain keys **refuse** the shift if any note would end up outside, and
say how many: *"Not shifted left, 3 notes would fall outside the item. Add Alt
to force."*
Adding **Alt** (`Ctrl+Alt+Shift+Left` / `Right`) does it anyway, reporting the
consequence: *"Shifted left, 3 notes now outside the item and silent."*
Positions are converted through quarter-notes rather than offset in raw ticks,
so a shift stays musically correct across tempo changes.
## 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.
## 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.
**Preview routing.** Preview notes go out through `StuffMIDIMessage`, i.e.
REAPER's virtual MIDI keyboard, which is only heard by tracks that are armed
and monitoring — normally not the track you are editing, so notes would sound
through whatever unrelated instrument happened to be armed. Grid mode therefore
borrows the record routing: on entering grid mode, and again whenever the
editor moves to an item on a different track, the edited take's track is armed
and monitoring with MIDI input, and every other armed track is stood down so it
cannot answer as well. The previous arm, monitor and input setting of every
track touched is remembered and handed straight back when grid mode is switched
off, when the MIDI editor closes, or when REAPER exits — the daemon does the
restoring, in the same way it restores the loop and repeat state the loop-bar
action borrows.
## Tunables
In `ExtState` section `midigrid`:
| Key | Default | Meaning |
|---|---|---|
| `vel` | 96 | Velocity of inserted notes |
| `velstep` | 8 | Velocity change per keypress |
| `stoplead` | 0.035 | How early to stop, in seconds, when playing one bar |
| `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
- While grid mode is on, the edited track is held record-armed and monitoring
with MIDI input so that preview can reach its instrument (see *Preview
routing* above), and any other armed track is stood down. Both are handed
back when grid mode ends, but a REAPER crash mid-session would lose that
arm state.
- 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.