Skip to content

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

neuroscript
# Keep only channel 1 notes and tame velocity peaks
keep note where ch == 1
when vel > 110: event(vel = 110)

Statements

Declarations

neuroscript
var threshold
const max_vel = 110

keep / drop

neuroscript
keep note, cc
drop clock, realtime
keep note where ch == 2 and vel > 80

when

neuroscript
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:

neuroscript
event(note = clamp(note + 12, 0, 127))
event(vel = clamp(vel + 10, 1, 127))
event(ch = 2)

emit

Create new events:

neuroscript
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):

neuroscript
send
send through
send after 1/8

drop (event)

Drop the current event immediately:

neuroscript
drop

routing sinks

Route values into registers, counters, variables, or fields:

neuroscript
counter("notes") + 1 -> counter("notes")
42 -> r("global", "latch")
note + 12 -> note
vel -> value

Selectors

Selectors are event-type filters used with keep/drop:

  • note, cc, pc, bend, aftertouch
  • clock, start, stop, continue, realtime
  • all

Combine selectors with commas:

neuroscript
drop clock, start, stop, continue, realtime

Conditions

Fields

  • type — Event type
  • ch — Channel (1-16)
  • note — Note number
  • vel — Velocity
  • cc — CC number
  • value — CC value
  • prog — Program number
  • bend — Pitch bend value

Operators

  • Equality: ==, !=
  • Comparison: <, <=, >, >=
  • Membership: in
  • Boolean: and, or, not

Examples:

neuroscript
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)

neuroscript
when type == note : {
  transpose(+12)  # Up one octave
  send
}

transposeWrap(semitones) - Shift note pitch with wrap-around

neuroscript
transposeWrap(+15)  # Wraps around if result > 127

transposeReject(semitones) - Shift note pitch, drop event if out of range

neuroscript
transposeReject(-5)  # Drops event if result < 0 or > 127

transposeMode(semitones, mode) - Shift with explicit mode constant

Mode constants: clamp (0), wrap (1), reject (2)

neuroscript
transposeMode(+12, wrap)    # Wrap around if out of range
transposeMode(-5, reject)   # Drop event if out of range

octave(n) - Shift by n octaves (clamped to 0-127)

neuroscript
when type == note : {
  octave(+1)  # Up one octave (same as transpose(+12))
  send
}

octaveWrap(n) - Shift by n octaves with wrap-around

neuroscript
octaveWrap(+2)  # Wraps around if result > 127

octaveReject(n) - Shift by n octaves, drop if out of range

neuroscript
octaveReject(-2)  # Drops event if result < 0 or > 127

octaveMode(n, mode) - Shift by n octaves with explicit mode constant

neuroscript
octaveMode(+1, wrap)    # Wrap around if out of range
octaveMode(-1, reject)  # Drop event if out of range

clampNote(min, max) - Clamp note to range

neuroscript
clampNote(C2, C6)  # Constrain to two-octave range

wrapNote(min, max) - Wrap note into range

neuroscript
wrapNote(C3, C5)  # Wrap around within range

Velocity Transform Functions

velScale(factor) - Scale velocity by factor (clamped to 1-127)

neuroscript
when type == note : {
  velScale(0.8)  # Reduce to 80%
  send
}

velAdd(delta) - Add to velocity (clamped to 1-127)

neuroscript
velAdd(-10)  # Reduce velocity by 10

velClamp(min, max) - Clamp velocity to range

neuroscript
velClamp(30, 100)  # Constrain velocity dynamics

velFixed(value) - Set velocity to fixed value

neuroscript
velFixed(80)  # All notes at velocity 80

Channel Transform Functions

chSet(channel) - Set MIDI channel (1-16)

neuroscript
chSet(2)  # Force all events to channel 2

chClamp(min, max) - Clamp channel to range

neuroscript
chClamp(1, 8)  # Constrain to channels 1-8

Utility Functions

clamp(value, min, max) - Clamp value to range [min, max]

neuroscript
event(vel = clamp(vel + 20, 1, 127))

scale(value, inMin, inMax, outMin, outMax) - Scale value from input range to output range

neuroscript
# 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

neuroscript
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

neuroscript
when type == note : {
  # Only emit every 4th note
  when counter("every4") % 4 == 0 : send
}

Register Functions

r(scope, key) - Read register value with scope

neuroscript
r("global", "lastNote")
r("source", "accumulator")
r("channel", "count")

r(key) - Read register in default scope (route-local)

neuroscript
r("lastVel")

Register Scopes

Registers are isolated by scope to prevent state leakage between routes, sources, and destinations.

ScopeLifetimeIsolationUse Case
globalApp lifetimeShared across all routesGlobal counters, app state
sourcePer MIDI sourceIsolated per input deviceTrack per-source state
routePer routeIsolated per route configDefault scope, route-local state
localPer routeAlias for routeSame as route
channelPer source+channelIsolated per input channelPer-channel state tracking
destPer destinationIsolated per output deviceTrack per-dest state
dest_channelPer dest+channelIsolated per output channelPer-output-channel state
pathPer route pathIsolated per source→dest pairTrack per-connection state
path_channelPer path+channelIsolated per source→dest+channelFine-grained path state

Register Scope Examples

neuroscript
# 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

neuroscript
emit.noteOn(1, note + 12, vel)  # Octave doubler

emit.noteOff(channel, note, velocity) - Send Note Off

neuroscript
emit.noteOff(1, note, 0)

Control Change

emit.cc(channel, controller, value) - Send 7-bit Control Change

neuroscript
when type == cc and cc == 1: emit.cc(74, value)  # Mod wheel → cutoff

emit.cc14(channel, controller, value) - Send 14-bit Control Change (high-resolution)

neuroscript
emit.cc14(1, 1, 16383)  # Full range 14-bit modulation

Pitch & Program

emit.bend(channel, value) - Send Pitch Bend (-8192 to 8191)

neuroscript
emit.bend(1, 0)  # Center pitch

emit.pc(channel, program) - Send Program Change (0-127)

neuroscript
emit.pc(1, 5)  # Select program 5

emit.bank(channel, msb, lsb) - Send Bank Select (CC#0 + CC#32)

neuroscript
emit.bank(1, 0, 8)  # Bank 0, sub-bank 8

Pressure

emit.aftertouch(channel, value) - Send Channel Pressure/Aftertouch

neuroscript
emit.aftertouch(1, 64)

System Exclusive

emit.sysex(bytes) - Send System Exclusive message

neuroscript
emit.sysex([0xF0, 0x43, 0x10, 0x7F, 0x00, 0xF7])

NRPN

emit.nrpn(channel, parameter, value) - Send Non-Registered Parameter Number

neuroscript
emit.nrpn(1, 128, 64)

Send Behavior

send

Emits the current event and stops further rule processing.

neuroscript
when type == note and note < 60 : {
  event(ch = 2)
  send  # Send to destination, stop processing
}
# Rules below this won't execute for notes < 60

send through

Emits the current event and continues processing subsequent rules.

neuroscript
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):

neuroscript
send after 100ms
send after 1/8   # Eighth note delay
send after 1/4   # Quarter note delay

Complete Examples

Lead Zone with Boost

neuroscript
keep note where ch == 1
event(note = clamp(note + 12, 0, 127))
event(vel = clamp(vel + 10, 1, 127))

Drop Drums and CC Noise

neuroscript
drop note where ch == 10
drop cc where cc in 64..127

Mod Wheel to Cutoff

neuroscript
when type == cc and cc == 1: emit.cc(74, value)

For single-event testing, use the Neuroscript Simulator.

Built with ❤️ for musicians