seq7 — a virtual MIDI device scriptable with Scheme and Lua.
It creates a virtual ALSA MIDI port and runs a Lua 5.4 engine (mlua) alongside the Scheme engine (steel-core), both able to send raw MIDI messages. A 1ms tick loop lets you write sequenced, algorithmic, or interactive MIDI logic in either language.
cargo run [script.scm | script.lua]
With a path argument, the script is loaded automatically before showing the command prompt; the language is picked by file extension (.lua → Lua, anything else → Scheme). Examples:
cargo run -- scm/simple.scm
cargo run -- lua/tick.lua
This opens a command prompt with full readline support (line editing, history via up/down arrows, Ctrl-A/E/U/W/K, etc.):
load <file>— load and evaluate a script (.lua→ Lua, anything else → Scheme).scheme— enter the live Scheme REPL (typeschemeagain to leave).lua— enter the live Lua REPL (typeschemeto leave).dbg— toggle console output: off (default) silences script prints and the MIDI hex dump; on shows both.help— show commands.quit— exit.- Press Enter on an empty line to start the tick loop.
- During the tick loop, press Space to stop it;
Ctrl-Cquits.
Anything else at the command prompt (e.g. raw Scheme text) is rejected with unknown command: <text> — Scheme evaluation happens only inside the REPL.
In scheme, type Scheme expressions and press Enter to evaluate them. In lua, type Lua expressions or statements (see Lua API below).
While the tick loop is running, seq7 calls the last-loaded script's tickfn on every tick (1ms by default, adjustable with set-tick-speed) — (define (tickfn) ...) in Scheme, function tickfn() ... end in Lua. Use raw-midi-write / raw_midi_write to send MIDI bytes to the virtual port.
| Function | Description |
|---|---|
(tickfn) |
Called every tick when the loop is running — define this in your script (default: 1ms) |
(set-tick-speed ms) |
Set tick interval in milliseconds (minimum 1) |
(raw-midi-write bytes) |
Sends a list of bytes to the virtual MIDI port |
| Function | Description |
|---|---|
(log-raw bytes) |
Prints a hex dump of bytes to the terminal (only while dbg is on) |
(display ...) |
Standard Scheme display; silenced while dbg is off |
(newline) |
Standard Scheme newline, rewritten to write \r\n for raw mode; silenced while dbg is off |
(rng->gen-range lo hi) — random integer in [lo, hi) — and (rng->gen-usize) — random unsigned integer — are provided by steel-core's steel/random module, not seq7. Enable them with (require-builtin steel/random).
Library helpers are provided in scm/lib/; load them all with (load "scm/lib/lib.scm").
A minimal script needs no explicit setup beyond the core functions:
(load "scm/lib/lib.scm")
(set-tick-speed 250)
(define t 0)
(define (tickfn)
(midi-note-on 0 (modulo (+ t 21) 60) 127)
(set! t (+ 1 t)))Run it with cargo run -- scm/simple.scm, then press Enter on an empty line to start the tick loop.
Scripts ending in .lua run on a Lua 5.4 VM that coexists with the Scheme engine (everything without a .lua extension is Scheme). load <file> and the cargo run <script> argument pick the language by file extension, and the tick loop calls whichever tickfn the last-loaded script defined — define function tickfn() ... end in a .lua file exactly like (define (tickfn) ...) in Scheme.
| Global | Description |
|---|---|
tickfn() |
Called every tick when the loop is running — define this in your script (default: 1ms) |
set_tick_speed(ms) |
Set tick interval in milliseconds (minimum 1) |
raw_midi_write(...) |
Sends MIDI bytes to the virtual port, one byte per vararg |
| Global | Description |
|---|---|
log_raw(...) |
Prints a hex dump of the bytes (only while dbg is on) |
print(...) / io.write(...) |
Console output; silenced while dbg is off (best-effort, like the Scheme console port) |
tickfn and set_tick_speed are available at load time, so a .lua script needs no explicit setup:
dofile("lua/lib/lib.lua")
set_tick_speed(250)
local t = 0
function tickfn()
midi_note_on(0, 21 + t % 60, 127) -- Lua % is 0-based → notes 21..80
t = t + 1
endRun it with cargo run -- lua/tick.lua, then press Enter on an empty line to start the tick loop.
The lua command at the prompt enters a live Lua REPL (scheme returns to the command prompt). Each line is evaluated as an expression (printing its value when non-nil) or run as a statement. Note standard Lua semantics: local variables don't survive across REPL lines.
Libraries are provided in lua/lib/; load them all with dofile("lua/lib/lib.lua") (the Scheme and Lua library trees are independent — each language has its own VM, global environment, and libraries). Editor users on the Lua language server get the seq7 API globals declared in .luarc.json.
Scheme's helpers are not available to Lua scripts, and vice versa.
MIDI channels (1–16) are embedded in the status byte — there is no separate "channel" argument. The status byte's low nibble selects the channel:
| Message | Channel 1 | Channel 2 | ... | Channel 16 |
|---|---|---|---|---|
| Note Off | #x80 |
#x81 |
... | #x8F |
| Note On | #x90 |
#x91 |
... | #x9F |
| Poly Aftertouch | #xA0 |
#xA1 |
... | #xAF |
| Control Change | #xB0 |
#xB1 |
... | #xBF |
| Program Change | #xC0 |
#xC1 |
... | #xCF |
| Channel Aftertouch | #xD0 |
#xD1 |
... | #xDF |
| Pitch Bend | #xE0 |
#xE1 |
... | #xEF |
For example, (raw-midi-write (list #x91 60 100)) sends Note On, channel 2, note 60, velocity 100.
The virtual port appears as "seq7" in your ALSA MIDI connections. Route it to a synthesizer with aconnect:
aconnect seq7:0 <synth>:0
Examples for both languages live in scm/ and lua/. Run one with cargo run -- scm/<name>.scm (or lua/<name>.lua), then press Enter on an empty line to start the tick loop and Space to stop it.
- steel-core — Scheme interpreter
- mlua — Lua 5.4 interpreter (vendored Lua sources; a C compiler is required to build)
- midir — MIDI I/O (ALSA backend on Linux)
- crossterm — raw terminal mode and key handling (tick loop only)
- rustyline — readline support for the command prompt and REPL