2026-08-14 18:13:45 +02:00
|
|
|
<#
|
|
|
|
|
MIDI Grid installer.
|
|
|
|
|
|
|
|
|
|
Registers the scripts as Reaper actions and binds them, by editing
|
|
|
|
|
reaper-kb.ini directly. This is done rather than via "Import key map"
|
|
|
|
|
because Reaper's importer silently converts unresolved script bindings
|
|
|
|
|
into "No-op (no action)" entries -- see README, Installation notes.
|
|
|
|
|
|
|
|
|
|
REAPER MUST BE CLOSED. It rewrites reaper-kb.ini on exit and will
|
|
|
|
|
discard anything written while it is running.
|
|
|
|
|
|
|
|
|
|
Usage: powershell -ExecutionPolicy Bypass -File install.ps1
|
|
|
|
|
powershell -ExecutionPolicy Bypass -File install.ps1 -Uninstall
|
|
|
|
|
#>
|
|
|
|
|
[CmdletBinding()]
|
|
|
|
|
param(
|
2026-08-14 18:18:12 +02:00
|
|
|
[string] $ScriptDir,
|
2026-08-14 18:13:45 +02:00
|
|
|
[string] $ResourcePath = "$env:APPDATA\REAPER",
|
|
|
|
|
[switch] $Uninstall
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
|
|
2026-08-14 18:18:12 +02:00
|
|
|
# $PSScriptRoot is not reliably populated during parameter binding, so
|
|
|
|
|
# resolve the script's own directory here instead of in the param block.
|
|
|
|
|
if (-not $ScriptDir) {
|
|
|
|
|
$ScriptDir = if ($PSScriptRoot) { $PSScriptRoot }
|
|
|
|
|
else { Split-Path -Parent $MyInvocation.MyCommand.Definition }
|
|
|
|
|
}
|
|
|
|
|
if (-not $ScriptDir) { throw "Could not determine script directory; pass -ScriptDir." }
|
|
|
|
|
|
2026-08-14 18:13:45 +02:00
|
|
|
$kb = Join-Path $ResourcePath 'reaper-kb.ini'
|
|
|
|
|
if (-not (Test-Path $kb)) { throw "reaper-kb.ini not found at $kb" }
|
|
|
|
|
|
|
|
|
|
if (Get-Process reaper* -ErrorAction SilentlyContinue) {
|
|
|
|
|
throw "REAPER is running. Close it completely and run this again."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# id, section, description, script filename
|
|
|
|
|
$actions = @(
|
|
|
|
|
@('RSmidigrid_open', 0, 'MIDI Grid: Open selected item in grid mode', 'MidiGrid_OpenInGrid.lua'),
|
|
|
|
|
@('RSmidigrid_daemon', 0, 'MIDI Grid: Preview daemon (auto-started)', 'MidiGrid_Daemon.lua'),
|
|
|
|
|
@('RSmidigrid_mode', 32060, 'MIDI Grid: Toggle grid mode', 'MidiGrid_ToggleMode.lua'),
|
|
|
|
|
@('RSmidigrid_left', 32060, 'MIDI Grid: Cursor left one cell', 'MidiGrid_Left.lua'),
|
|
|
|
|
@('RSmidigrid_right', 32060, 'MIDI Grid: Cursor right one cell', 'MidiGrid_Right.lua'),
|
|
|
|
|
@('RSmidigrid_up', 32060, 'MIDI Grid: Pitch up one scale degree', 'MidiGrid_Up.lua'),
|
|
|
|
|
@('RSmidigrid_down', 32060, 'MIDI Grid: Pitch down one scale degree', 'MidiGrid_Down.lua'),
|
2026-08-24 01:15:57 +02:00
|
|
|
@('RSmidigrid_octup', 32060, 'MIDI Grid: Pitch up one octave', 'MidiGrid_OctaveUp.lua'),
|
|
|
|
|
@('RSmidigrid_octdn', 32060, 'MIDI Grid: Pitch down one octave', 'MidiGrid_OctaveDown.lua'),
|
|
|
|
|
@('RSmidigrid_colup', 32060, 'MIDI Grid: Next note above in this cell', 'MidiGrid_ColumnUp.lua'),
|
|
|
|
|
@('RSmidigrid_coldn', 32060, 'MIDI Grid: Next note below in this cell', 'MidiGrid_ColumnDown.lua'),
|
|
|
|
|
@('RSmidigrid_rownext',32060, 'MIDI Grid: Next note at this pitch', 'MidiGrid_RowNext.lua'),
|
|
|
|
|
@('RSmidigrid_rowprev',32060, 'MIDI Grid: Previous note at this pitch', 'MidiGrid_RowPrev.lua'),
|
|
|
|
|
@('RSmidigrid_notenext',32060,'MIDI Grid: Next note anywhere', 'MidiGrid_NoteNext.lua'),
|
|
|
|
|
@('RSmidigrid_noteprev',32060,'MIDI Grid: Previous note anywhere', 'MidiGrid_NotePrev.lua'),
|
2026-08-14 18:13:45 +02:00
|
|
|
@('RSmidigrid_cell', 32060, 'MIDI Grid: Toggle note at cursor cell', 'MidiGrid_ToggleCell.lua'),
|
|
|
|
|
@('RSmidigrid_hold', 32060, 'MIDI Grid: Toggle hold mode', 'MidiGrid_ToggleHold.lua'),
|
|
|
|
|
@('RSmidigrid_audit', 32060, 'MIDI Grid: Repeat current cell', 'MidiGrid_AuditionCell.lua'),
|
2026-08-14 18:18:12 +02:00
|
|
|
@('RSmidigrid_scale', 32060, 'MIDI Grid: Set root and scale', 'MidiGrid_SetScale.lua'),
|
|
|
|
|
@('RSmidigrid_triad', 32060, 'MIDI Grid: Toggle diatonic triad', 'MidiGrid_ChordTriad.lua'),
|
|
|
|
|
@('RSmidigrid_seventh',32060, 'MIDI Grid: Toggle diatonic seventh chord', 'MidiGrid_ChordSeventh.lua'),
|
|
|
|
|
@('RSmidigrid_velup', 32060, 'MIDI Grid: Velocity up', 'MidiGrid_VelUp.lua'),
|
2026-08-14 18:27:14 +02:00
|
|
|
@('RSmidigrid_veldn', 32060, 'MIDI Grid: Velocity down', 'MidiGrid_VelDown.lua'),
|
|
|
|
|
@('RSmidigrid_longer', 32060, 'MIDI Grid: Lengthen note one cell', 'MidiGrid_Lengthen.lua'),
|
|
|
|
|
@('RSmidigrid_shorter',32060, 'MIDI Grid: Shorten note one cell', 'MidiGrid_Shorten.lua'),
|
2026-08-14 18:46:33 +02:00
|
|
|
@('RSmidigrid_join', 32060, 'MIDI Grid: Join run of repeated notes', 'MidiGrid_Join.lua'),
|
|
|
|
|
@('RSmidigrid_shl', 32060, 'MIDI Grid: Shift all notes one cell left', 'MidiGrid_ShiftLeft.lua'),
|
|
|
|
|
@('RSmidigrid_shr', 32060, 'MIDI Grid: Shift all notes one cell right', 'MidiGrid_ShiftRight.lua'),
|
|
|
|
|
@('RSmidigrid_shlf', 32060, 'MIDI Grid: Shift all notes left (force)', 'MidiGrid_ShiftLeftForce.lua'),
|
2026-08-14 19:57:55 +02:00
|
|
|
@('RSmidigrid_shrf', 32060, 'MIDI Grid: Shift all notes right (force)', 'MidiGrid_ShiftRightForce.lua'),
|
2026-08-14 20:04:20 +02:00
|
|
|
@('RSmidigrid_playbar',32060, 'MIDI Grid: Play from start of current bar', 'MidiGrid_PlayFromBar.lua'),
|
2026-08-14 20:43:24 +02:00
|
|
|
@('RSmidigrid_playone',32060, 'MIDI Grid: Play current bar only', 'MidiGrid_PlayBarOnly.lua'),
|
|
|
|
|
@('RSmidigrid_loopbar',32060, 'MIDI Grid: Loop current bar', 'MidiGrid_LoopBar.lua')
|
2026-08-14 18:13:45 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# flags, keycode, section, id
|
|
|
|
|
# flags: 1 base, +4 Shift, +8 Ctrl, +16 Alt
|
|
|
|
|
# section: 0 = Main, 32060 = MIDI Editor
|
|
|
|
|
$keys = @(
|
|
|
|
|
@(21, 71, 0, 'RSmidigrid_open'),
|
|
|
|
|
@(17, 71, 32060, 'RSmidigrid_mode'),
|
|
|
|
|
@(1, 32805, 32060, 'RSmidigrid_left'),
|
|
|
|
|
@(1, 32807, 32060, 'RSmidigrid_right'),
|
|
|
|
|
@(1, 32806, 32060, 'RSmidigrid_up'),
|
|
|
|
|
@(1, 32808, 32060, 'RSmidigrid_down'),
|
2026-08-24 01:15:57 +02:00
|
|
|
@(25, 32806, 32060, 'RSmidigrid_octup'),
|
|
|
|
|
@(25, 32808, 32060, 'RSmidigrid_octdn'),
|
|
|
|
|
@(17, 32806, 32060, 'RSmidigrid_colup'),
|
|
|
|
|
@(17, 32808, 32060, 'RSmidigrid_coldn'),
|
|
|
|
|
@(17, 32807, 32060, 'RSmidigrid_rownext'),
|
|
|
|
|
@(17, 32805, 32060, 'RSmidigrid_rowprev'),
|
|
|
|
|
@(25, 32807, 32060, 'RSmidigrid_notenext'),
|
|
|
|
|
@(25, 32805, 32060, 'RSmidigrid_noteprev'),
|
2026-08-14 18:13:45 +02:00
|
|
|
@(1, 13, 32060, 'RSmidigrid_cell'),
|
|
|
|
|
@(17, 72, 32060, 'RSmidigrid_hold'),
|
|
|
|
|
@(17, 65, 32060, 'RSmidigrid_audit'),
|
2026-08-14 18:18:12 +02:00
|
|
|
@(17, 83, 32060, 'RSmidigrid_scale'),
|
|
|
|
|
@(17, 51, 32060, 'RSmidigrid_triad'),
|
|
|
|
|
@(17, 55, 32060, 'RSmidigrid_seventh'),
|
|
|
|
|
@(21, 32806, 32060, 'RSmidigrid_velup'),
|
2026-08-14 18:27:14 +02:00
|
|
|
@(21, 32808, 32060, 'RSmidigrid_veldn'),
|
|
|
|
|
@(1, 76, 32060, 'RSmidigrid_longer'),
|
|
|
|
|
@(5, 76, 32060, 'RSmidigrid_shorter'),
|
2026-08-14 18:46:33 +02:00
|
|
|
@(17, 74, 32060, 'RSmidigrid_join'),
|
|
|
|
|
@(13, 32805, 32060, 'RSmidigrid_shl'),
|
|
|
|
|
@(13, 32807, 32060, 'RSmidigrid_shr'),
|
|
|
|
|
@(29, 32805, 32060, 'RSmidigrid_shlf'),
|
2026-08-14 19:57:55 +02:00
|
|
|
@(29, 32807, 32060, 'RSmidigrid_shrf'),
|
2026-08-14 20:04:20 +02:00
|
|
|
@(17, 32, 32060, 'RSmidigrid_playbar'),
|
2026-08-14 20:43:24 +02:00
|
|
|
@(25, 32, 32060, 'RSmidigrid_playone'),
|
|
|
|
|
@(29, 32, 32060, 'RSmidigrid_loopbar')
|
2026-08-14 18:13:45 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$backup = "$kb.midigrid-backup"
|
|
|
|
|
if (-not (Test-Path $backup)) {
|
|
|
|
|
Copy-Item $kb $backup
|
|
|
|
|
Write-Host "Backed up original to $backup"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$lines = [System.Collections.Generic.List[string]]::new()
|
|
|
|
|
$lines.AddRange([System.IO.File]::ReadAllLines($kb))
|
|
|
|
|
|
|
|
|
|
# Drop any previous MIDI Grid lines so re-running is idempotent.
|
|
|
|
|
for ($i = $lines.Count - 1; $i -ge 0; $i--) {
|
|
|
|
|
if ($lines[$i] -match '(?i)RSmidigrid') { $lines.RemoveAt($i) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($Uninstall) {
|
|
|
|
|
[System.IO.File]::WriteAllLines($kb, $lines, (New-Object System.Text.UTF8Encoding($false)))
|
|
|
|
|
Write-Host "Removed MIDI Grid actions and bindings."
|
|
|
|
|
Write-Host "Note: keys it had taken over (arrows, Enter) are now unbound in the"
|
|
|
|
|
Write-Host "MIDI editor. Rebind them, or restore $backup."
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($a in $actions) {
|
|
|
|
|
$path = Join-Path $ScriptDir $a[3]
|
|
|
|
|
if (-not (Test-Path $path)) { throw "Missing script: $path" }
|
|
|
|
|
$lines.Insert(0, ('SCR 4 {0} {1} "{2}" {3}' -f $a[1], $a[0], $a[2], $path))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# KEY lines reference the *named command*, which is the SCR id with a
|
|
|
|
|
# leading underscore. Without it Reaper cannot resolve the binding and the
|
|
|
|
|
# key silently does nothing.
|
|
|
|
|
foreach ($k in $keys) {
|
|
|
|
|
$new = 'KEY {0} {1} _{3} {2}' -f $k[0], $k[1], $k[2], $k[3]
|
|
|
|
|
$pat = '^KEY {0} {1} \S+ {2}(\s|$)' -f $k[0], $k[1], $k[2]
|
|
|
|
|
$found = $false
|
|
|
|
|
for ($i = 0; $i -lt $lines.Count; $i++) {
|
|
|
|
|
if ($lines[$i] -match $pat) { $lines[$i] = $new; $found = $true; break }
|
|
|
|
|
}
|
|
|
|
|
if (-not $found) { $lines.Add($new) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[System.IO.File]::WriteAllLines($kb, $lines, (New-Object System.Text.UTF8Encoding($false)))
|
|
|
|
|
|
|
|
|
|
Write-Host "Installed $($actions.Count) actions and $($keys.Count) key bindings."
|
|
|
|
|
Write-Host "Start REAPER, select a MIDI item, and press Alt+Shift+G."
|