BAEScript is a lightweight scripting language embedded in NeoBAE for real-time MIDI and mixer manipulation. Scripts execute once per playback tick (roughly every 15 ms) and can read/write song, channel, and mixer state.
- Create a script file with extension
.bscript. - Run playbae with
--script.
Example:
neobae/bin/playbae -f content/midi/1barloop.mid --script neobae/examples/advanced_controls.bscript -t 5BAEScripts can also be used in the zefidi GUI, with a syntax-highlighting and linting editor.
var x = 42;
x = x + 1;if (midi.timestamp < 5000) {
ch[0].volume = 100;
} else {
ch[0].volume = 80;
}while (i < 4) {
ch[i].pan = 64;
i = i + 1;
}
for (i = 0; i < 4; i++) {
ch[i].reverb = 48;
}Notes:
forsupportsi++andi--in the step clause.whileloops are guarded to max 10000 iterations per tick.
- Arithmetic:
+ - * / % - Comparison:
== != < > <= >= - Logical:
&& || !
- Numbers: integer (
42,0xFF) - Strings:
'text'or"text" - Booleans:
true,false
// line comment
/* block comment */noteOn(ch, note, velocity);
noteOff(ch, note, velocity);print("Tempo", midi.tempo);
help();abs(x)
min(a, b)
max(a, b)
clamp(x, lo, hi)BAEScript supports declarative event handlers:
on.start({
print("Playback started");
});Supported events:
on.script({ ... });on.start({ ... });on.pause({ ... });on.resume({ ... });on.stop({ ... });on.loop({ ... });on.seek({ ... });
Semantics:
- Event handlers are one-shot per occurrence.
- Handlers do not execute every tick unless the event re-occurs.
- If the same event is declared multiple times, the last declaration wins.
Notes:
on.scriptfires once after script bind.on.startfires when playback session starts.on.pauseandon.resumefire on state transitions.on.stopfires when the song reaches/stops at end.on.loopfires when playback naturally wraps from end back to start.on.seekfires when position jumps, including script-driven seeks viamidi.timestamp,midi.position, ormidi.tickswrites.
N is MIDI channel index used by the existing BAEScript integration (typically 0..15).
Readable and writable:
ch[N].instrument(0..127)ch[N].volume(CC7, 0..127)ch[N].pan(CC10, 0..127)ch[N].expression(CC11, 0..127)ch[N].pitchbend(0..16383, center 8192)ch[N].mute(0/1)ch[N].reverb(CC91, 0..127)ch[N].chorus(CC93, 0..127)ch[N].solo(0/1)
Properties:
midi.timestamp(ms, read/write)midi.position(alias of timestamp)midi.ticks(raw MIDI ticks, read/write)midi.length(ms, read-only)midi.exporting(0/1, read-only)midi.volume(song volume percent, read/write)midi.tempo(master tempo factor in percent, read/write,100= normal speed)midi.tempobpm(raw BPM, read/write)midi.transpose(semitones, read/write)
Commands:
midi.stop()midi.allnotesoff()
midi.tempo and midi.tempobpm are related but distinct:
midi.tempocontrols master tempo factor (percent scale).midi.tempobpmcontrols the song tempo directly in BPM.
Use midi.tempobpm when you want explicit musical tempo values like 120.
midi.timestampandmidi.positionare millisecond-based.midi.ticksis raw MIDI tick position (sequencer tick domain), not microseconds.- Use
midi.tickswhen you need deterministic musical-grid positioning.
Properties:
mixer.volume(global volume percent, read/write)mixer.voices(currently active voices, read-only)mixer.reverbtype(default reverb enum value, read/write)mixer.classicchorus(0/1, read/write)mixer.stereodcpanfix(0/1, read/write)
Command:
mixer.reset()
exporter.loopcount(Sets the number of loops when exporting)
BAEScript runs every tick, not once.
That means top-level statements are re-executed continuously while the song is running. Event declarations are registered and triggered separately by transitions.
Example:
var x = 0;The declaration is evaluated each tick; if it includes an initializer it can reset values repeatedly. Guard one-time initialization with conditions based on runtime state.
if (midi.timestamp == 0 && mixer.volume != 90) {
mixer.reset();
mixer.volume = 90;
}on.script({
mixer.reset();
midi.tempobpm = 120;
});
on.loop({
print("looped");
});
on.seek({
print("seek", midi.ticks);
});if (midi.timestamp < 4000) {
midi.tempo = clamp(100 + (midi.timestamp * 40 / 4000), 100, 140);
}if (midi.timestamp == 0) {
midi.tempobpm = 120;
}// Jump to MIDI tick 960 (often one quarter-note at TPQ=960, depending on file).
if (midi.timestamp == 0) {
midi.ticks = 960;
}for (i = 0; i < 4; i++) {
ch[i].reverb = 40;
ch[i].chorus = 24;
}on.script({
exporter.loopcount = 2;
});- Parser errors report line/column to stderr.
- Undefined variable reads print an error and return
0. - Maximum script variables: 256.
- Safety cap: while/for execution is limited by the while guard (10000 iterations per tick).
- Some properties are read-only and ignore writes (
midi.length,midi.exporting,mixer.voices).
neobae/src/script/baescript.hneobae/src/script/baescript_internal.hneobae/src/script/baescript_lexer.cneobae/src/script/baescript_parser.cneobae/src/script/baescript_vm.cneobae/src/script/baescript.c
neobae/examples/mixer_controls.bscriptneobae/examples/advanced_controls.bscript
These examples are good starting points for automation and live playback control.