Playing a single bar still triggered notes placed exactly on the next bar line. Two delays stack: the daemon polls at roughly frame rate, so it can notice up to ~33ms late, and REAPER renders audio ahead of the play position it reports, so the note has already reached the instrument before the stop lands. Stopping 60ms early clears both. The cost is a few milliseconds off the tail of the bar, which is inaudible, and the benefit is that the downbeat you were trying to exclude stays excluded. Tunable via the stoplead ExtState for setups with large audio buffers.
306 lines
13 KiB
Markdown
306 lines
13 KiB
Markdown
# 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 |
|
||
| `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 | — |
|
||
| `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 |
|
||
| `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.
|
||
|
||
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.
|
||
|
||
## Hearing it in context
|
||
|
||
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 at 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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
It stops **60 ms before** the bar line rather than on it. Two delays stack up
|
||
otherwise: the daemon runs at about frame rate, and REAPER renders audio ahead
|
||
of the play position it reports, so a note sitting exactly on the next bar line
|
||
has already reached the instrument by the time the stop lands — and you hear
|
||
the downbeat you were trying to exclude. Leading by more than both delays keeps
|
||
it silent, at the cost of a few inaudible milliseconds off the tail.
|
||
|
||
If a next-bar note still sneaks through — a large audio buffer will do it —
|
||
raise `stoplead` in `ExtState`.
|
||
|
||
## 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.
|
||
|
||
## Tunables
|
||
|
||
In `ExtState` section `midigrid`:
|
||
|
||
| Key | Default | Meaning |
|
||
|---|---|---|
|
||
| `vel` | 96 | Velocity of inserted notes |
|
||
| `velstep` | 8 | Velocity change per keypress |
|
||
| `stoplead` | 0.06 | 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
|
||
|
||
- 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.
|