Play an AudioUnit instrument from the command line and write the result to a WAV file. No DAW, no window, no audio device — just a plugin, some notes, and a file.
$ aubounce bounce "DLSMusicDevice" --note C4 --velocity 100 -o note.wavRendering a plugin to audio normally means opening a DAW and doing it by hand. That is fine once and unbearable a thousand times. aubounce makes it a command, so it can go in a script, a Makefile, or a CI job.
It renders offline, far faster than real time — a large sampler measured here runs at roughly 2500x, so a second of audio costs well under a millisecond.
cargo install --git https://github.com/libraz/aubounceList the instruments installed on this machine:
$ aubounce list
aumu:dls :appl Apple: DLSMusicDevice
aumu:msyn:appl Apple: AUMIDISynth
aumu:samp:appl Apple: AUSamplerInspect one — channels, presets, parameters and what it keeps in its state:
$ aubounce info "aumu:dls :appl"
$ aubounce info "aumu:dls :appl" --params
$ aubounce info "HALion 7" --params "Volume"Render a note, a chord, a note shaped by a controller, or a whole MIDI file:
$ aubounce bounce "aumu:dls :appl" --note C4 --gate-ms 500 -o note.wav
$ aubounce bounce "aumu:dls :appl" --note C4 --note E4 --note G4 -o chord.wav
$ aubounce bounce "aumu:dls :appl" --note C4 --cc 11=0..127@2s -o swell.wav
$ aubounce bounce "aumu:dls :appl" --midi song.mid -o song.wavA MIDI file brings its own channels, programs, controllers and tempo, so the arguments that set those do not apply to it.
Bend a note, or strike it more than once:
$ aubounce bounce "aumu:dls :appl" --note C4 --bend 0..+1@2s -o bend.wav
$ aubounce bounce "aumu:dls :appl" --note C4 --repeat 6 --repeat-interval-ms 600 -o roll.wavA bend is written as a lever position rather than an interval — -1 fully
down, 0 at rest, +1 fully up. How far an instrument moves for a given
position is a setting of its own that it does not publish: the stock GM
instrument measured here bends two semitones at +1, which is a common
default and not a promise.
Striking one note repeatedly is how a round robin shows itself, since a sampler that alternates between recordings renders a different waveform each time and one that does not renders the same one. The interval is measured note-on to note-on and has to be at least the gate: a strike landing before the last one was released would be silenced by that one's note-off, and nothing in the recording would say why.
Choose a timbre by program change, by a preset the plugin advertises, by a saved state, by a VST3 preset file, or by setting one parameter:
$ aubounce bounce "aumu:dls :appl" --program 19 --note C4 -o organ.wav
$ aubounce bounce "aumu:dls :appl" --note C4 --param "Reverb Volume=12" -o wet.wav
$ aubounce bounce "aumu:dls :appl" --note C4 --state saved.aupreset -o restored.wav
$ aubounce bounce "The Grand 3" --note C4 --preset piano.vstpreset --settle-ms 8000 -o grand.wavParameters take a name or the identifier info lists, and a name only has to
be unambiguous — which matters, because a large sampler can advertise well
over a thousand of them.
--preset reads a .vstpreset file, and does so through one vendor's
convention rather than a standard. The two chunks such a file carries are put
into the plugin's own class-info dictionary under Processor State and
Controller State, which is where Steinberg's audio-unit wrapper keeps them —
so a sound saved from the plugin's own interface can be replayed here without
opening it, for plugins built that way. An audio unit that keeps its state
under other keys will accept the dictionary and ignore the chunks, and the
render will be of whatever it was already set to. aubounce info <plugin>
prints the keys a plugin actually uses, which is how to tell in advance.
Presets a plugin keeps inside its own content archive are not reachable this way either; those have to be saved out first.
A rack saved as one file answers to a different sound on each of its MIDI
channels, and an audio unit has no way to say how many of them are filled or
what is on them. info asks three different ways, none of which knows anything
about a particular plugin:
$ aubounce info "HALion 7" --preset rack.vstpreset --settle-ms 8000 --probe-channels
$ aubounce info "HALion 7" --preset rack.vstpreset --settle-ms 8000 --probe-slots
$ aubounce info "HALion 7" --preset rack.vstpreset --settle-ms 8000 --values element
$ aubounce info "HALion 7" --preset rack.vstpreset --settle-ms 8000 --strings harpsi--probe-channels plays a note down each of the sixteen channels and reports
which ones made a sound. It assumes only that a filled channel answers a note,
so it works on any multitimbral instrument. The silence before each note is
measured too: a channel carrying a long reverb is still ringing when the next
note starts, and without that the quiet channel behind it reads as empty.
--probe-slots goes further and says which slot owns which channel. A rack has
a solo control per slot, so raising one and playing the channels answers it.
The controls are found by searching the parameter names — Solo by default —
rather than being known in advance, which is what keeps this from being about
one plugin. The channels are narrowed first by probing with nothing soloed, and
the search stops once every sounding channel is accounted for; the report says
how many controls it got through, because the ones after that were not tried.
A rack of eight took nineteen seconds, most of it the settle wait.
--values reads the plugin's class-info dictionary through, values and all.
That is a standard property, so every plugin answers it — but what is worth
reading there is the plugin's to decide. HALion 7, for one, puts its output bus
names in it and nothing about its slots.
--strings reads the text out of the opaque chunks. A sampler saves its rack
as a blob only it can parse, and the names of what is loaded are inside; this
decompresses what is compressed and reports what is readable, so it finds those
names without following anyone's private format. It assumes a chunk may be
compressed and a name may be stored as text — both narrow enough to hold widely
— and reads ASCII and UTF-16 alike. What comes back is unlabelled: it says what
a rack contains, never what is on a given slot. Pair it with the probe, which
answers the other half.
These have been tried against HALion 7 and The Grand 3. Nothing in them is specific to either, and nothing in them is guaranteed to find something in a plugin that stores its names another way.
--settle-ms gives the plugin the main thread for a while before anything is
played. A sampler that streams from disk finishes loading on the main run
loop, which a command-line host would otherwise never turn — without this it
accepts a preset, reports itself correctly, and then renders silence for as
long as you ask. Several seconds is usual for a large library, and the result
is the same bytes every time once it has settled.
Such an instrument also needs --realtime. Driven at full speed it plays only
the part it holds in memory and then goes silent mid-note, with the right peak,
the right length and no error — a recording that looks fine and is missing most
of the note. aubounce checks for this: silence inside a note that had already
started is reported as dropout_ms, and warned about.
A note that simply ends while the key is still down is not that, and is not reported. Held long enough, plenty of instruments finish and write silence for the rest of the key press — a piano's top octave is over in about a second. The two are the same run of zeros, and what separates them is the level the sound had just before it stopped: a note that ended got quiet first, and a sampler that ran out was still playing.
By default the output is 48 kHz 32-bit float, is not normalized, and is not trimmed: note-on always lands at the same offset, so a batch of renders shares one time axis. The tail runs until the sound decays rather than to a fixed length. Float rather than an integer depth because a plugin can render past full scale, and clipping a recording quietly is the one thing this tool should never do.
Each render is its own process, and that is deliberate: a plugin is not necessarily identical to itself across instances, so rendering two timbres in one run risks charging the difference to the timbre.
A jobs file is an array of the arguments above, and introduces no vocabulary of
its own: every key is the name of a bounce option. Only id belongs to the
batch, and it names both the job and the file it writes.
defaults:
plugin: "aumu:dls :appl"
note: C4
gate-ms: 500
jobs:
- id: gm-000
program: 0
- id: gm-019
program: 19
- id: chord
note: [C4, E4, G4]
velocity: 90$ aubounce batch --jobs gm.yaml -o ./out
$ aubounce batch --jobs gm.yaml -o ./out --dry-run # check it firstEach job is rendered by its own bounce process, and each one that finishes is
appended to a manifest before the next starts — so a run that is interrupted
leaves an exact record of what it got through, and --resume picks up from
there. Jobs that failed are tried again; jobs that worked are not.
--shard i/n renders part of a batch, and is how a batch is rendered in
parallel: run n of them at once. Shards share the output directory and
nothing else, each writing a manifest of its own, and merge puts those
together afterwards.
$ for i in 1 2 3 4; do aubounce batch --jobs gm.yaml -o ./out --shard $i/4 & done; wait
$ aubounce merge -o ./outA sweep is the product of what varies: sources of timbre, stimuli, notes and
velocities. The options inside it are the same bounce options a jobs file
uses; what a sweep adds is only the grid and the naming.
plugin: "aumu:dls :appl"
options:
gate-ms: 1000
tail: "until-silence@-80dB"
sources:
- id: gm
timbres:
- id: "000"
options: { program: 0 }
- id: "019"
options: { program: 19 }
stimuli:
- id: decay-basic
notes: { from: 48, to: 84, step: 12 }
velocities: [32, 80, 112]$ aubounce sweep --config gm.yaml -o ./dataset --dry-run
$ aubounce sweep --config gm.yaml -o ./datasetEach render lands at <source>/<timbre>/<stimulus>/n<note>_v<velocity>.wav, so
a path says what it is. Alongside them goes dataset.json, holding the plugin
version and the digest of the definition: an instrument that updates, or a
definition that is edited, makes a different dataset out of the same command,
and nothing in the audio would say so.
Sharding, resuming and merge work exactly as they do for a batch.
$ aubounce verify -o ./dataset
$ aubounce selftest -o ./dataset -n 20verify reads every file back and looks for the failures that leave a
plausible file behind — a preset that did not take, a sampler that stopped
streaming, a velocity that went nowhere, a note number the plugin ignored.
None of these raise an error when they happen; each one produces audio of the
right length with a reasonable peak.
It separates what is certainly wrong from what is only suspicious. A file that
is missing, has changed since it was recorded, or is digital silence is a
fault, and verify exits non-zero. A recording that clips, is not silent
before the note, gets quieter as velocity rises, or does not sound higher as
the note number rises is a doubt — reported, but an instrument is allowed to
surprise. Glockenspiels, harmonics and sound effects legitimately have no
fundamental to rise.
selftest renders part of the dataset a second time and compares it byte for
byte. Determinism is the assumption everything else rests on, and it belongs
to the plugin rather than to this tool: an update can take it away.
- macOS. aubounce hosts AudioUnits, which exist only here
- Rust 1.88 or later to build
aubounce instantiates a plugin synchronously, which covers version 2 audio
units and the version 3 ones packaged to load into the calling process. A
version 3 audio unit that can only run in an extension process is registered
as requiring asynchronous instantiation; list marks it as one that cannot be
hosted, and asking for it by triple says the same thing rather than failing
with a status code.
Audio produced from a plugin's content is covered by that plugin's licence, not by this one. aubounce is a tool for rendering instruments you have installed, on your own machine; whether you may pass the result on is between you and whoever made the instrument. Sample libraries in particular rarely allow it.
Early, but usable. list, info, bounce, batch, sweep, verify,
selftest and merge all work: enumeration, instantiation, MIDI, parameters,
VST3 presets, offline rendering, state capture and restore, and WAV output —
from a note on the command line, from a Standard MIDI File, from a jobs file
rendered a shard at a time, or from a sweep definition expanded into a checked
dataset.
MIT. See LICENSE.