Neuroscript Language Reference (Rust v3)
Neuroscript is the canonical, Rust-backed DSL for MIDI transforms in Neurode MIDI. Scripts are compiled off realtime threads into deterministic programs.
Quick Example
# Keep only channel 1 notes and tame velocity peaks
keep note where ch == 1
when vel > 110: event(vel = 110)Statements
Declarations
var threshold
const max_vel = 110keep / drop
keep note, cc
drop clock, realtime
keep note where ch == 2 and vel > 80when
when note in C3..C4: event(note = note + 12)
when type == cc and cc == 1: emit.cc(74, value)event
Edit the current event in-place:
event(note = clamp(note + 12, 0, 127))
event(vel = clamp(vel + 10, 1, 127))
event(ch = 2)emit
Create new events:
emit.cc(74, value)
emit.noteOn(note, vel)
emit.noteOff(note)
emit.bend(0)
emit.pc(12)
emit.aftertouch(64)send
Send the current event (optionally delayed):
send
send through
send after 1/8drop (event)
Drop the current event immediately:
droprouting sinks
Route values into registers, counters, variables, or fields:
counter("notes") + 1 -> counter("notes")
42 -> r("global", "latch")
note + 12 -> note
vel -> valueSelectors
Selectors are event-type filters used with keep/drop:
note,cc,pc,bend,aftertouchclock,start,stop,continue,realtimeall
Combine selectors with commas:
drop clock, start, stop, continue, realtimeConditions
Fields
type— Event typech— Channel (1-16)note— Note numbervel— Velocitycc— CC numbervalue— CC valueprog— Program numberbend— Pitch bend value
Operators
- Equality:
==,!= - Comparison:
<,<=,>,>= - Membership:
in - Boolean:
and,or,not
Examples:
keep note where ch == 1 and vel > 90
drop note where vel < 20
drop cc
when note in 60..72: event(vel = clamp(vel + 5, 1, 127))Expressions
Literals
- Integers:
0,127,-1 - Notes:
C4,F#3,Bb-1 - Strings:
"global","latch"
Built-in Functions
Musical Transform Functions
transpose(semitones) - Shift note pitch by semitones (clamped to 0-127)
when type == note : {
transpose(+12) # Up one octave
send
}transposeWrap(semitones) - Shift note pitch with wrap-around
transposeWrap(+15) # Wraps around if result > 127transposeReject(semitones) - Shift note pitch, drop event if out of range
transposeReject(-5) # Drops event if result < 0 or > 127transposeMode(semitones, mode) - Shift with explicit mode constant
Mode constants: clamp (0), wrap (1), reject (2)
transposeMode(+12, wrap) # Wrap around if out of range
transposeMode(-5, reject) # Drop event if out of rangeoctave(n) - Shift by n octaves (clamped to 0-127)
when type == note : {
octave(+1) # Up one octave (same as transpose(+12))
send
}octaveWrap(n) - Shift by n octaves with wrap-around
octaveWrap(+2) # Wraps around if result > 127octaveReject(n) - Shift by n octaves, drop if out of range
octaveReject(-2) # Drops event if result < 0 or > 127octaveMode(n, mode) - Shift by n octaves with explicit mode constant
octaveMode(+1, wrap) # Wrap around if out of range
octaveMode(-1, reject) # Drop event if out of rangeclampNote(min, max) - Clamp note to range
clampNote(C2, C6) # Constrain to two-octave rangewrapNote(min, max) - Wrap note into range
wrapNote(C3, C5) # Wrap around within rangeVelocity Transform Functions
velScale(factor) - Scale velocity by factor (clamped to 1-127)
when type == note : {
velScale(0.8) # Reduce to 80%
send
}velAdd(delta) - Add to velocity (clamped to 1-127)
velAdd(-10) # Reduce velocity by 10velClamp(min, max) - Clamp velocity to range
velClamp(30, 100) # Constrain velocity dynamicsvelFixed(value) - Set velocity to fixed value
velFixed(80) # All notes at velocity 80Channel Transform Functions
chSet(channel) - Set MIDI channel (1-16)
chSet(2) # Force all events to channel 2chClamp(min, max) - Clamp channel to range
chClamp(1, 8) # Constrain to channels 1-8Utility Functions
clamp(value, min, max) - Clamp value to range [min, max]
event(vel = clamp(vel + 20, 1, 127))scale(value, inMin, inMax, outMin, outMax) - Scale value from input range to output range
# Scale CC value 0-127 to velocity range 40-127
event(vel = scale(value, 0, 127, 40, 127))wrap(value, min, max) - Wrap value circularly within range
event(note = wrap(note + 12, 0, 127))min(a, b) - Return minimum of two values
max(a, b) - Return maximum of two values
abs(value) - Return absolute value
Counter Functions
counter(name) - Read and increment named counter
when type == note : {
# Only emit every 4th note
when counter("every4") % 4 == 0 : send
}Register Functions
r(scope, key) - Read register value with scope
r("global", "lastNote")
r("source", "accumulator")
r("channel", "count")r(key) - Read register in default scope (route-local)
r("lastVel")Register Scopes
Registers are isolated by scope to prevent state leakage between routes, sources, and destinations.
| Scope | Lifetime | Isolation | Use Case |
|---|---|---|---|
global | App lifetime | Shared across all routes | Global counters, app state |
source | Per MIDI source | Isolated per input device | Track per-source state |
route | Per route | Isolated per route config | Default scope, route-local state |
local | Per route | Alias for route | Same as route |
channel | Per source+channel | Isolated per input channel | Per-channel state tracking |
dest | Per destination | Isolated per output device | Track per-dest state |
dest_channel | Per dest+channel | Isolated per output channel | Per-output-channel state |
path | Per route path | Isolated per source→dest pair | Track per-connection state |
path_channel | Per path+channel | Isolated per source→dest+channel | Fine-grained path state |
Register Scope Examples
# Global counter (shared across all routes)
when type == note : {
r("global", "totalNotes") = r("global", "totalNotes") + 1
}
# Source-specific state
when type == note : {
r("source", "lastNote") = note
}
# Channel-isolated state
when type == note and ch == 1 : {
r("channel", "leadCount") = r("channel", "leadCount") + 1
}
# Path-specific routing state
when type == cc : {
r("path", "lastCC") = cc
}Emit Functions
Emit functions create new MIDI events independent of the current event.
Note Events
emit.noteOn(channel, note, velocity) - Send Note On
emit.noteOn(1, note + 12, vel) # Octave doubleremit.noteOff(channel, note, velocity) - Send Note Off
emit.noteOff(1, note, 0)Control Change
emit.cc(channel, controller, value) - Send 7-bit Control Change
when type == cc and cc == 1: emit.cc(74, value) # Mod wheel → cutoffemit.cc14(channel, controller, value) - Send 14-bit Control Change (high-resolution)
emit.cc14(1, 1, 16383) # Full range 14-bit modulationPitch & Program
emit.bend(channel, value) - Send Pitch Bend (-8192 to 8191)
emit.bend(1, 0) # Center pitchemit.pc(channel, program) - Send Program Change (0-127)
emit.pc(1, 5) # Select program 5emit.bank(channel, msb, lsb) - Send Bank Select (CC#0 + CC#32)
emit.bank(1, 0, 8) # Bank 0, sub-bank 8Pressure
emit.aftertouch(channel, value) - Send Channel Pressure/Aftertouch
emit.aftertouch(1, 64)System Exclusive
emit.sysex(bytes) - Send System Exclusive message
emit.sysex([0xF0, 0x43, 0x10, 0x7F, 0x00, 0xF7])NRPN
emit.nrpn(channel, parameter, value) - Send Non-Registered Parameter Number
emit.nrpn(1, 128, 64)Send Behavior
send
Emits the current event and stops further rule processing.
when type == note and note < 60 : {
event(ch = 2)
send # Send to destination, stop processing
}
# Rules below this won't execute for notes < 60send through
Emits the current event and continues processing subsequent rules.
when type == note and note < 60 : {
emit.noteOn(1, note + 12, vel) # Add octave
send through # Continue to next rules
}
# This rule WILL execute even for notes < 60
when type == note : {
event(vel = velScale(0.8))
send
}Use send through when you want to:
- Add harmony notes while keeping the original
- Apply multiple transformations in sequence
- Route to multiple destinations with different processing
send after
Schedule delayed send (requires beat sync or millisecond delay):
send after 100ms
send after 1/8 # Eighth note delay
send after 1/4 # Quarter note delayComplete Examples
Lead Zone with Boost
keep note where ch == 1
event(note = clamp(note + 12, 0, 127))
event(vel = clamp(vel + 10, 1, 127))Drop Drums and CC Noise
drop note where ch == 10
drop cc where cc in 64..127Mod Wheel to Cutoff
when type == cc and cc == 1: emit.cc(74, value)For single-event testing, use the Neuroscript Simulator.
