Files
TalonandClaude Opus 5 0f06554a18 Keep the edit cursor when opening an item in grid mode
Opening an item in the MIDI editor parks the edit cursor at the item
start, and the open action then made that stick. Listening to a phrase
and hitting Alt+Shift+G to write on top of it meant arrowing back to
where you already were, one cell at a time, on every entry.

The cursor now stays where it was and is only tidied onto the nearest
grid line, so the first left/right press moves a whole cell rather than
a remainder. The action reads the cursor before the open command runs,
since the open command is what moves it.

snapCursorToCell rounds in quarter notes, like everything else here, so
the landing spot survives a tempo change, and clamps the cell index into
the item: the floor is the cell containing the item start, which is
where the action used to land, and the ceiling is the last cell that
starts inside the item. Clamping the index rather than the time means a
cursor parked before an off-grid item start rounds forward into the
first whole cell instead of into the partial one, where a note would
begin outside the item.

The action also re-reads the take from the editor handle now, so the
grid size and the spoken position come from the take actually being
edited rather than from the selected item's active take.

Toggling grid mode on with Alt+G is left alone: it does not move the
cursor at all, and cell() floors, so an off-grid cursor still works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 01:21:21 +02:00

22 KiB
Raw Permalink Blame 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.

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 -ExecutionPolicy Bypass -File install.ps1
    

    macOS / Linux

    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 -ExecutionPolicy Bypass -File install.ps1 -Uninstall   # Windows
./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:

./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 is Command+… on a Mac, which is what Mac users expect. To bind the physical Control key instead:

./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 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 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
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
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
19 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.

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 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+Spaceloops 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.

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 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.

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.