Standalone
A standalone binary mode of your plugin: drives the DSP against your OS audio driver directly and opens your plugin's editor in its own window. Not a plugin format a DAW loads — a way to run the same crate as an app.
#Status
Shipped on all three platforms. Every documented example ships a standalone binary.
| Platform | Audio backend | GUI |
|---|---|---|
| macOS | CoreAudio (via cpal) | ✅ |
| Windows | WASAPI (via cpal) | ✅ |
| Linux | ALSA or PipeWire-JACK (via cpal) | ✅ (X11; Wayland via XWayland) |
#Enable
Plugins scaffolded with cargo truce new (or new --workspace) ship
the standalone host enabled out of the box — Cargo.toml lists
standalone in default = [...] and src/main.rs is pre-written.
If you opted out at scaffold time with --no-standalone, or you're
adding standalone to an existing crate, do the two mechanical
additions below. No lib.rs edits.
Cargo.toml:
[[bin]]
name = "<crate_name>-standalone"
path = "src/main.rs"
required-features = ["standalone"]
[features]
standalone = ["dep:truce-standalone"]
[dependencies]
truce-standalone = { workspace = true, features = ["gui"], optional = true }
<crate_name> is your crate's [package].name — the convention
(e.g. truce-example-gain → truce-example-gain-standalone) is what
the scaffolder emits. cargo truce run reads your Cargo.toml and
picks the [[bin]] entry whose required-features contains
"standalone", so the actual name can be anything; the convention just
keeps the cargo-built path predictable.
src/main.rs:
use my_plugin::Plugin;
fn main() {
truce_standalone::run::<Plugin>();
}
#Run
cargo truce run -p <crate> # build + stage + launch
cargo truce run -p <crate> -- --help # pass flags through to the binary
cargo truce run handles feature + bin selection and stages the
binary into target/bundles/{Plugin}.standalone/ alongside every
other truce-produced artifact.
#CLI flags
<plugin>-standalone [OPTIONS]
--headless Run audio only; no window
--list-devices List audio output + input devices and exit
--list-midi List MIDI input devices and exit
--output <name> Audio output device (substring match)
--input <name> Audio input device (for effect plugins)
--input-enabled <on|off> Enable mic input at launch (default: off).
Press `I` in the window to toggle live.
--output-enabled <on|off> Enable speaker output at launch (default: on).
Toggle live from the Plugin menu (Cmd+O / Ctrl+O).
--sample-rate <hz> e.g. 44100, 48000, 96000
--buffer <frames> Audio buffer size
--midi-input <name> MIDI input device (substring match)
--bpm <n> Transport BPM (default 120)
--state <path> Load a state file on launch (a .trucepreset
preset or a saved state blob)
--preset <name|uri> Load a library preset on launch, by name
(or its preset uri)
--list-presets List the plugin's presets and exit
-h, --help Show this message
#playback feature: WAV in / WAV out
The optional playback feature on truce-standalone adds three
flags for piping .wav files in and out of the plugin without an
audio interface — useful for snapshot regression tests, batch
rendering, and CI runs on headless build agents. Enable from your
plugin crate with:
[features]
standalone-playback = ["standalone", "truce-standalone/playback"]
…then build the binary with --features standalone-playback.
--input-file <path> Decode <path>.wav and feed it into the
plugin's input bus. One-shot — plays
once, then the file channel goes silent.
Mic + file sum when both are enabled.
Linear-interp resample if the file's SR
differs from the device's; channel-count
mismatches are soft-warned and adapted.
--output-file <path> Capture the plugin's output bus to
<path>.wav (32-bit float, pre-mute).
Implies --headless. Real-time by default
(cpal still drives the audio thread);
pair with --no-playback for offline.
--no-playback Bypass cpal entirely; render as fast as
the CPU allows. Requires both
--input-file and --output-file
(otherwise ignored with a warn).
Common shapes:
# Real-time capture: hear it while it records.
truce-example-gain-standalone --input-file in.wav --output-file out.wav
# Offline render — sub-real-time, no audio device touched.
# This is the CI / batch recipe.
truce-example-gain-standalone --no-playback --input-file in.wav --output-file out.wav
In offline mode the runner inherits the input WAV's sample rate
and channel count by default (override with --sample-rate if
needed); output WAV is always 32-bit float at the resolved SR.
Mute (--output-enabled off) silences the speakers but does
not affect what --output-file captures — bounce-to-disk
behaviour matches what every DAW does.
#Presets
The standalone host browses and saves your plugin's preset library through a native Presets menu (macOS and Windows):
- Load — pick a preset. The list also shows your own user presets and any installed packs, deduped by identity.
- Previous / Next — step through the list to audition sounds.
- Save — update the preset you're editing in place. With a
factory preset loaded (or nothing) there's nothing of yours to
overwrite, so Save is grayed and points you at Save As — you can't
clobber a stock sound by accident. The menu label shows the file
you'd write, e.g.
Save Preset (Glass.trucepreset). - Save As… — name a new preset. macOS / Windows open a native save panel (which confirms overwrites); the new preset is selected once saved.
Presets you save show up in the menu the next time you open it — no relaunch — and land in the same per-OS user location your DAW reads, so a sound you dial in here is immediately available in your projects.
Keyboard shortcuts work on every platform, and are the whole preset
interface on Linux (the X11 standalone has no menu bar): Cmd-S /
Ctrl-S to Save, Cmd-Shift-S / Ctrl-Shift-S to Save As. You can
also load a sound straight from the command line with
--preset <name>, or list what's available with --list-presets.
See the presets guide for authoring libraries.
#In-window hotkeys
- SPACE — toggle transport play / stop
- Cmd-S / Ctrl-S — save the current preset; Cmd-Shift-S / Ctrl-Shift-S — Save As (see Presets above)
- I — toggle mic input (effect plugins only). First press on macOS triggers the system permission prompt.
- Cmd-O / Ctrl-O — toggle audio output (mute / unmute). Plugin keeps processing — meters, transport, MIDI all still tick — only the speaker output is zeroed.
- Z / X — shift QWERTY-MIDI octave down / up
- A S D F G H J K L ; — white keys (C D E F G A B C D E)
- W E T Y U O P — black keys
The QWERTY-to-MIDI mapping is keyed on physical key positions, so AZERTY / Dvorak / etc. map to the same piano keys.
#Settings precedence
Each setting resolves first-match-wins:
- CLI flag (
--output "…") - Environment variable (
TRUCE_STANDALONE_OUTPUT="…") - Plugin-author defaults via
run_with::<P>(Defaults { … }) - Compiled runtime default (input off, output on, cpal-picked devices)
Plugin authors can pin input_enabled / output_enabled defaults
in code without giving up CLI / env override:
use truce_standalone::{run_with, Defaults};
fn main() {
run_with::<my_plugin::Plugin>(Defaults {
input_enabled: Some(true), // effect plugin wants mic on by default
..Defaults::default()
});
}
Other settings (device names, sample rate, buffer size, MIDI input, BPM, state path) are intentionally CLI/env-only — those are per-machine concerns the plugin author shouldn't be pinning in code.
#MIDI
midir-based input with substring matching on port names. A
background thread polls at 1 Hz for hot-plug; disconnect falls back
to QWERTY, reconnect is silent.
Supported: MIDI 1.0 note on / off (velocity-0 note-on decodes as note-off), CC, pitch bend, channel pressure. No sysex, no MPE, no MIDI 2.0.
#Transport
Minimal — sufficient for LFOs, tempo-synced delays, arpeggiators. Not a DAW timeline.
tempo: set via--bpmor config; default 120playing: SPACE toggles; default stoppedposition_beats: advances while playing
Atomic-backed — UI-thread toggles don't block the audio thread.
#Integration tests
The same engine that backs --no-playback also drives in-process
audio tests via truce_test::PluginDriver.
No cpal, no window, no devices — instantiate the plugin, feed
scripted audio + MIDI for a fixed duration, capture the output:
use std::time::Duration;
use truce_test::{assertions, driver};
let result = driver!(Plugin)
.sample_rate(48_000.0)
.duration(Duration::from_secs(3))
.script(|s| {
s.note_on(60, 0.8);
s.wait_ms(100);
s.note_off(60);
})
.run();
assertions::assert_no_nans(&result);
assertions::assert_nonzero(&result);
assertions::assert_silence_after(&result, Duration::from_millis(2_500));
Opt in from the plugin crate:
[dev-dependencies]
truce-test = { workspace = true }
Good for tail-silence / release-decay tests, sustained-load stability,
clipping guards, and MIDI-recorded regression tests. See
../guide/audio-testing for
the full builder surface — input shapes, state-file loading,
per-block meters, output-event capture.
#Distribution
cargo truce package ships the standalone alongside the plug-in formats —
no extra flag, no separate build step.
On macOS the standalone and AU v3 are the same app: when AU v3 is
also packaged, you get one /Applications/{Name}.app that is both the AU v3
container the DAW loads and the playable standalone, offered in the .pkg
as a single "AU3 + Standalone" choice. (With no AU v3 build the
standalone ships as its own .app.) See
Audio Unit › AU v3 and the standalone are one app.
On Windows the standalone .exe ships in the installer; on Linux
it ships in the .tar.gz (AppImage is still on the backlog).
#Limitations
- Wayland unparented top-level windows still lag X11; XWayland is the validated path.
- No parameter automation record / replay.
- State files have no migration layer: bumping
STATE_VERSIONinvalidates saved.statefiles; the plugin logs a clear error.
#See also
- Shipping — packaging, signing, installers
- CLI reference — every
cargo trucesubcommand README— format index