Running Scripts
A Rhai script can add nodes, wire ports, set properties, drive the transport, and toggle UI state - a broad subset of what the GUI does, but deterministic and repeatable.
The full API reference is at the scripting guide. This page covers how to run scripts day-to-day.
Opening the Console
Aether's console panel hosts a Rhai REPL. Toggle it from the command palette (press Space Space, type "Console") or through the view menu.
Type a line, press Return - it evaluates immediately. Results print below. Errors point at the offending line.
Minimum Working Script
let osc = Oscillator.add("lead");
let sink = AudioSink.first();
osc.output("Audio Out").port().connect(sink.input("Audio In").port());
Paste this into the console on a blank canvas, press Return. An oscillator at 440 Hz (its default frequency) is wired to the existing default AudioSink and the tone plays the moment the connection lands - Oscillator is free-running, so no play() is required to hear it.
Why AudioSink.first() and not AudioSink.add(...)? The engine spawns a default AudioSink on startup and routes audio from that specific sink to the speakers. Adding a second AudioSink via Type.add(label) creates a node that receives audio but isn't the one the audio thread reads from - you'd hear nothing. Type.first() returns the existing default sink, so the connection lands on the right node.
If you add transport-driven nodes (Sequencer, Arranger) to the chain later, follow up with a play() call so the transport actually advances.
Note the pattern: Type.add("label") creates a new node (or returns the existing one if a node with that label already exists - idempotent by label). Type.first() returns the first existing node of that type without creating anything. .output("...") / .input("...") resolves a port handle by display name. .connect() joins two port handles.
Running a Script File
To run a .rhai file rather than individual console lines, use include with a URI:
include("external://scripts/demos/lofi-chill");
include("script://my_project_script");
Two URI forms:
external://<key>/<relative-path>- loads from a registered filesystem path key.scriptsis pre-registered when Aether runs from the.appbundle (it points atContents/Resources/scripts/, which holds the bundled tutorial, features, and demos). Additional path keys can be registered from the command line with--path <key>=<dir>when launching from source. The.rhaiextension is optional - it's added automatically if missing.script://<name>- loads a script stored inside the currently open project's database.
A bare filename like include("file.rhai") errors - the URI scheme is required.
Bundled Scripts
Contents/Resources/scripts/ inside the .app ships with three groups:
tutorial/- lesson progression. The 0.1.0 demo bundles the first couple of lessons (01-hello-sound.rhai,02-properties.rhai); more land over time.features/- targeted demos of specific node patterns (arranger gate, gated sequencer, help system, region fade helpers, track-named ports).demos/- full musical patches (lofi chill, gamelan, drum kit, grain cloud, harmonic pad, showcase).
There's no in-editor file browser for bundled scripts in 0.1.0 - run one with include("external://scripts/<group>/<name>") from the console, or read the files at the filesystem path above to copy-paste. The Project Explorer's Script resource type lists scripts stored inside the current project's database, not the bundled filesystem scripts.
Saving a Patch as a Script
export_graph() serialises the current graph to a Rhai script and returns it as a string. Save it yourself:
let source = export_graph();
// Copy from the console output, or write it to a file via the editor.
The returned string contains explicit Type.add, .connect, and .set calls that rebuild the graph verbatim when re-run. For long-term storage, saving a patch as a script is more durable than the .aether binary format - see Working with Projects.
Common Traps
- Case-sensitive node types.
Oscillator.add(...)is right;oscillator.add(...)fails (Rhai is case-sensitive for identifiers). - Lenient port name lookup, but still be specific. Port lookup in
.output("...")/.input("...")tries exact match, case-insensitive match, underscores-as-spaces, then substring - so"Audio Out","audio_out", and"audio"all resolve to theAudio Outport. A typo that doesn't partial-match any port name fails with "Output port 'X' not found". Check the node reference for the canonical names. - Continuous vs gated sources.
Oscillator,LFO,Noise,DroneOscillator,ShepardTonerun continuously - connect and they play. The gated synths need their triggers wired correctly:Polysynthhas aMIDI Inport and takes any MIDI stream;DrumVoicehas both a requiredGate Inand an optionalMIDI In(either fires the drum);SamplePlayerhas only a requiredGate In- to drive it from a MIDI source you need aMIDI → CVnode (theMidiToCvtype) in between to convert MIDI note-on events into gate pulses. - The transport.
play()starts,stop()stops. Many nodes respect the transport state; some (free-running oscillators) do not. If a graph is silent, check the transport.
Headless Running
Headless execution is available in the source tree (cargo run -p aether-headless -- --script path.rhai --fast) but the headless binary does not ship in the .app bundle. For end users, scripts run from inside the editor via include or from the console REPL.
Related
- Scripting reference - every function, method, and constant the Rhai runtime exposes.
- Working with Projects - why
export_graph()is the durable backup path. - MIDI Setup - hardware and on-screen note sources for gated synths.