Changelog
Notable changes per release.
#6.3.0
- Per-format builds (
cargo truce build/install/package/run/screenshot) keep a plugin's non-format default features instead of dropping them;--no-default-featuresopts back into a minimal build. cargo truce validate --aaxruns Avid's full AAX Plug-In Validator whenAAX_VALIDATORpoints at itsdsh, selectable via--aax-tests load|fast|full|required|info(loadforces the lightweight pluginrunner check); withoutAAX_VALIDATORit falls back to pluginrunner.cargo truce doctorreports both tools' availability.- VST2: fixed a scan-time buffer overflow (string opcodes now copy no more than the spec buffer sizes) and made plugins load on Linux / windows-gnu (the
VSTPluginMainentry is a Rust trampoline, no longer demoted to a local symboldlsymcan't find).
#6.2.1
- AU: the oversized-block fail-safe and the not-yet-prepared bail no longer re-deliver the previous block's MIDI/SysEx output (duplicated note-ons, stuck notes); both clear the output events and reset the drain cursor before returning.
- Standalone: a failed output-device switch on the unnamed system default (Linux/ALSA) falls back to the default device instead of going silent.
- Standalone:
--input-fileon a layout with no main input bus is ignored instead of panicking on a divide-by-zero. - Standalone: the mic ring tracks a runtime bus-layout switch instead of freezing its channel width at launch, so a wider layout's extra channels aren't silent until mic off/on.
- Standalone: windowed
--output-filecapture also finalizes the WAV header on Ctrl-C, not just the headless runner.
#6.2.0
Breaking: MigratedState gains a persist field (the #[persist] block to restore), so a migrate_state impl that builds it with a full struct literal no longer compiles. Plugins that don't override migrate_state need no change.
Migration - fill the new field from the default instead of listing every field:
Some(MigratedState {
params,
- extra: None,
+ ..MigratedState::default()
})
To keep #[persist] fields across a plugin rename, forward the already-decoded bytes from ForeignState::MismatchedEnvelope:
ForeignState::MismatchedEnvelope { params, extra, persist, .. } => Some(MigratedState {
params: params.to_vec(),
extra: extra.map(<[u8]>::to_vec),
persist: persist.to_vec(),
}),
- State:
cargo truce screenshot --stateand the test driver'sstate_file/state_blobdecode the.pluginstateenvelope instead of passing it toload_state, so params and persist restore instead of rendering defaults; renamed-plugin migration carries the#[persist]block throughMigratedState. - AU: host SysEx input no longer races the render thread; blocks past
kAudioUnitProperty_MaximumFramesPerSlicefail safe instead of allocating; components report the plugin's real version instead of a hardcoded 1.0.0. - iOS / GUI editors: fixed a
CGImageuse-after-free on resize/close, frozen Slint timers/animations, a fixed-size letterbox gap, an unguarded GPU-editoron_eventpanic path, and per-openUIView/CADisplayLinkleaks (egui, iced, Slint). - Standalone host: clean Ctrl-C WAV finalize for live capture; lock-free mic ring; device- and bus-layout-switch failures fall back instead of going silent;
--input-filedecodes to the main-bus width; mic enable falls back to the capture device's config; key auto-repeat ignored (macOS / Windows). - LV2: output atom ports always advertise their real body size; the
ui:resizecallback is panic-firewalled. - VST3: editor state edits enqueue onto the audio-thread handoff (no aliasing
&mut/ TOCTOU against hostsetState/setActive);setContentScaleFactorand inactive-setStatenotify usetry_enter. - AAX: host state loads refresh the custom-state snapshot immediately; editor host-callbacks are invalidated on window close.
#6.1.10
- Audio-thread real-time safety:
#[persist]state loads off the audio thread (no priority-inversion dropout), wider-than-stereo buses don't allocate on the first block, the transport slot's seqlock got its missing memory fences (Apple Silicon torn reads), and asymmetric buses and analyzer taps are handled correctly (AudioBuffer::chunks_mut,AudioTap::drain_with). - VST3 host correctness: right transport flags (time signature / bar position / loop state), sample-accurate
CHUNKEDautomation timing, canonicalgetBusArrangementspeaker layouts, and UTF-8 metadata that no longer renders as mojibake. - Sample-accurate automation: sub-blocks split by a
CHUNKEDparam now advance the transport position, so tempo-synced plugins don't jitter phase when automation lands mid-block. - Stronger compile-time parameter checks: out-of-range defaults, non-4-char fourccs, and unknown
flagsare compile errors;skewed/sym_skewed/reversed/ rangeless params build again; a NaN normalized value collapses to the range's low end instead of silently resetting a param. - MIDI, LV2, tooling, and GUI: MIDI 1.0 output masks data bytes to 7 bits; LV2 nested-group ids match the live plugin;
cargo truce screenshotreturns a clean error instead of aborting; the SlintXYPadclips its dot and brackets a drag as one automation gesture (pressed/releasedcallbacks) so a drag latches over host automation.
#6.1.9
- CLAP plugins expose
remote-controlspages built from#[param(group = "...")], so hosts (e.g. Bitwig) map each group onto the eight controller knobs. Based on #178 by @lxndrbe.
#6.1.8
cargo truce build --targetnow stages VST3 and CLAP bundles for the target platform, not the build host, so cross-compiling a Windows VST3/CLAP from macOS produces the correct bundle layout.
#6.1.7
- VST3 plugins now cross-compile from macOS to Windows (
x86_64-pc-windows-gnu); the C++ shim build no longer hands a macOS-only flag to the mingw compiler.
#6.1.6
- Vizia widgets now repaint on host parameter automation, instead of showing stale values until the editor is reopened.
- iOS AUv3 user presets save and load correctly (resolve inside the app-extension sandbox container).
#6.1.5
- VST3 editors on HiDPI Linux hosts that report content scale late (e.g. REAPER) no longer render 1x inside a 2x frame with mismatched click targets; every editor backend now reconciles the late scale (relies on baseview-truce upgrade to 0.1.1-truce.13).
#6.1.4
cargo truce newnow scaffolds stateful plugins astype DspState = Selfinstead of an explicit DspState struct- Slint editors no longer panic on HiDPI displays.
#6.1.2
- A panic in your plugin's code - editor build/open/close, resize/scale, iOS touch and keyboard handling, or DSP
reset/prepare - is now caught at the host boundary on every format instead of crashing the DAW, and a caught editor panic no longer freezes or leaks the editor.request_resizeis safe to call from inside an editor method; it applies on the next frame. - Audio-thread robustness fixes: the standalone host runs input-only plugins (analyzers, meters, sinks), a truncated or malformed LV2 transport/parameter message can't cause an out-of-bounds read, and a VST2 switch to a wider channel layout no longer risks a mid-block allocation.
#6.1.1
- Standalone no longer crashes on MIDI-only plugins (zero output buses, e.g. transpose / arpeggio), which previously panicked on the first audio callback. MIDI output from the standalone isn't wired up yet, so their emitted events still go nowhere.
- Author-code and GUI-teardown panics can't abort the host: instance create/destroy and param format/parse callbacks are panic-guarded on every format, closing the remaining gaps on VST3, AAX, and CLAP.
- Parameter writes clamp to the declared range (float, int, and enum) and reject non-finite floats, and the smoother self-heals from a NaN accumulator, so a corrupt project or preset can't poison the audio path.
#6.1.0
Sidechain and aux inputs now reach your plugin as separate buses, so a with_sidechain_input(...) layout gives the host a real sidechain to route into.
- VST3, AU (v2/v3), and AAX advertise the sidechain as its own bus/port; the plugin sees main channels first, then sidechain. AAX uses Pro Tools' mono side-chain, duplicated across the declared width.
- VST2 exposes the sidechain as extra input pins, now labeled "Input N" / "Sidechain N" so hosts can route to them.
- Standalone
--sidechain-file <path>feeds the sidechain bus, independent of--input-file(main bus); works live and offline. - Test driver:
PluginDriver::sidechain(InputSource)drives the sidechain independently of the main input, width auto-detected from the layout. - New
truce-example-sidechain: stereo main + stereo sidechain with IN/SC meters and a Mix knob. - LV2 sidechain plugins run main-only for now (the TTL is generated from the plugin category and can't declare the sidechain ports yet); a one-time log notes the dropped sidechain.
#Fixes
- Host text entry into a parameter field parses back through the plugin, on every format.
- Stepped, enum, and toggle parameters are advertised as discrete/indexed/boolean with value labels, not continuous (VST3, AU, AAX, VST2, LV2); AAX step counts are no longer off by one.
EnumParamclamps out-of-range indices to the last variant.- Linear smoothing converges at the correct constant-increment rate.
- Transport position in seconds is reported on VST3, VST2, AU, AAX, and LV2 (was always zero); LV2 also reports beat/tempo position.
- Lock-free state save re-serializes only when state changes (opt-in
snapshot_version) or, for MB-scale state, publishes off the audio thread viaInitContext::snapshot_publisher()instead of copying the buffer every block;snapshot_prealloc_hintpre-warms the inline buffer. #[derive(State)]keyed envelope: add/remove/reorder-safe across sessions, rejects unknown format versions, and rename-type-safe (a changed field type falls back to its default rather than corrupting later fields).- Unconnected sidechains read as silence rather than misrouting or panicking; fixed sidechain buffer aliasing on AU v2, and output-buffer aliasing plus channel-count negotiation on VST3 wide layouts.
- Preset loading hardened; preset-handling fixes on CLAP and AU.
- AAX parameter taper corrected so non-linear ranges track in Pro Tools.
- Editor-lifecycle and parameter-format callbacks are panic-guarded on every format (editor build/open/close, value format/parse, reset, instantiation), matching the state save/load guards: an author-code panic returns a safe default instead of aborting the host.
- Per-format correctness fixes: VST3 result-code (ABI) values; CLAP latency/tail, category, modulatable-flag reporting; AU bus-layout and render-notify handling; VST2 chunk frees, time-signature, missing opcodes; LV2 latency reporting and bypass flag.
#6.0.0
Saving your plugin's state - a host session save, a preset capture, the editor reading state back - now runs completely off the audio thread. Even large custom state (a sampler's loaded audio, big wavetables) can no longer cause an audio glitch when the host saves during playback.
#Breaking
Only if your plugin saves custom state - anything beyond parameters, like a loaded file path, a view mode, or an analysis result - by overriding save_state. Plugins that keep everything in parameters (including #[persist] fields) need no change.
Custom state is now published through snapshot_into instead of save_state. snapshot_into writes your bytes into a buffer the framework hands you and reuses, and that is what lets a save run without ever pausing audio. save_state still exists and the framework still calls it (off the audio thread), but overriding it no longer reaches the host - implement snapshot_into instead.
#Migrating from 5.x
Move your save_state body into snapshot_into: write into the buffer instead of returning a Vec, and return true (or false for "no custom state"). load_state is unchanged.
-fn save_state(state: &MyDsp) -> Vec<u8> {
- encode(&state.extra)
-}
+fn snapshot_into(state: &MyDsp, buf: &mut Vec<u8>) -> bool {
+ encode_into(&state.extra, buf); // append your bytes to `buf`
+ true
+}
If your custom state is a #[derive(State)] struct, its generated serialize_into already has the right shape:
+fn snapshot_into(state: &MyDsp, buf: &mut Vec<u8>) -> bool {
+ state.extra.serialize_into(buf);
+ true
+}
#5.0.1
Multiple bus layouts working end-to-end on every format and background-task handlers choosing their own concurrency, plus new parameter range and smoothing shapes.
#Bus layouts
bus_layouts()now defaults toBusLayout::stereo_and_mono()(stereo and mono) instead of stereo-only. The default ensures audio effects show up on both mono and stereo tracks in Logic Pro (and other hosts that filter the insert menu by channel format) without the plugin declaring anything. You can still override it - returnvec![BusLayout::stereo()]for a stereo-only effect, or any set you want. Behavior change: with the new default a mono track handsprocessa 1-channel buffer, so loop overbuffer.channels()rather than assuming two (the shipped examples do; instruments and MIDI effects override the default as before).- Offer the host more than one channel layout: return several from
bus_layouts()and the host picks the one its track needs. This works on every format that can renegotiate ports - CLAP (audio-ports-config), VST3 (setBusArrangements, now tracked per instance so two instances on different-width tracks don't report each other's arrangement), AU (the channel-capability property, plus one AU v3 bus per layout), and AAX (one component per layout). VST2 and LV2 stay on the first layout. - AU and AAX now honor every declared layout: a mono/stereo/5.1 effect or a surround meter is offered each config and passes
auval, with no change to your plugin code. (AU had been advertising the extra configs but rejecting them at instantiation, soauvalfailed and the plugin never appeared; AU v3 crashed on surround layouts; AAX offered mono/stereo configs a surround-only plugin never declared.) - Declare a channel count AAX has no stem format for and you get a log line rather than the config silently vanishing from Pro Tools.
- The standalone host runs the exact
(in, out)of the selected layout (--bus-layout <index>,--list-bus-layoutsto enumerate, or the Settings > Bus Layout menu). It keeps the audio device at a width the hardware supports and maps the plugin onto it, so a mono layout plays through a stereo-only output instead of failing with "device doesn't offer 1 output channels", and an asymmetric layout (mono-in / stereo-out) runs at its real dimensions rather than a collapsed width. - Helpers for authoring across widths:
BusLayout::mono()/BusLayout::stereo_and_mono()constructors, andAudioBuffer::for_each_frame_io::<IN, OUT>(with afor_each_stereo_frameshorthand for the(2, 2)case) to run a fixed-shape DSP - areverb_stereograph, a stereo filter block - over any declared bus: a mono input fans into both graph inputs, a stereo bus maps 1:1, no per-width branch.
#Parameters
- Three more parameter range shapes:
skewed(min, max, factor)(power-law taper),sym_skewed(min, max, factor, center)(center-anchored, for pan and EQ-gain knobs), andreversed(<range>)(any range with its knob axis flipped). - Logarithmic (multiplicative) smoothing via
smooth = "log(<ms>)"- a constant perceived rate of change, for frequency and linear-gain params.
#Background tasks
- Each background-task type now chooses whether its handler may run concurrently with itself. Implement
BackgroundTaskon the task type and setconst SERIALIZED:false(default) - the shared pool may run your handler on two workers at once when a burst re-arms it. Fastest, and correct when the handler only talks to the audio thread through lock-free channels / atomics (the reverb example's handoff).true("one-slot") - the pool runs the handler one at a time for a given instance, so a handler that read-modify-writes a non-atomic scratch buffer or cache is safe with notry_lockof your own. Tasks are never dropped or reordered; only concurrency is bounded.
- A plugin can mix lanes. List several task types -
tasks: [Rebuild, Analyze]- and each gets its own queue and mode;ctx.tasks::<Rebuild>()selects the lane by type, so a serialized rebuild and a concurrent analyzer never queue behind each other. - The pool now warms when the plugin is instantiated, so a handler first scheduled from
process()(a filter that only rebuilds on a knob move) no longer spawns worker threads on the audio thread. A worker-spawn failure now drops the task rather than panicking across the plugin's C boundary.
#Breaking
Only if your plugin uses background tasks.
- The
BackgroundTaskstrait (implemented once on the plugin, withtype Taskandrun_task) is replaced byBackgroundTask, implemented on each task type withfn run(self, params)and an optionalconst SERIALIZED.truce::plugin!now takes a list of task types:tasks: [Rebuild].
#Migrating from 4.1
Move the impl from your plugin type onto the task type, and wrap the tasks: value in brackets. Plugins with no background tasks, and bus layouts, need no changes.
-impl BackgroundTasks for Reverb {
+impl BackgroundTask for Rebuild {
type Params = ReverbParams;
- type Task = Rebuild;
- fn run_task(task: Rebuild, params: &ReverbParams) {
- let graph = build_graph(task.sample_rate, task.time_s);
+ // const SERIALIZED: bool = true; // add for a non-reentrant handler
+ fn run(self, params: &ReverbParams) {
+ let graph = build_graph(self.sample_rate, self.time_s);
let _ = params.ready.force_push(graph);
}
}
-truce::plugin! { logic: Reverb, params: ReverbParams, tasks: Reverb }
+truce::plugin! { logic: Reverb, params: ReverbParams, tasks: [Rebuild] }
The old task argument becomes self, so task.field reads become self.field. Scheduling is unchanged: ctx.tasks::<Rebuild>() still returns the spawner.
#4.1.0
- Managed background tasks: offload work off the audio thread without hand-rolling a worker thread. Implement
BackgroundTasks(onerun_task(task, params)function), addtasks: MyPlugintotruce::plugin!, and schedule fromprocesswithctx.tasks::<T>()-try_spawnfor one-shot work,spawn_coalescingwhen only the newest request matters (a filter rebuild, a graph swap). A shared, bounded pool runs the handlers, and a panicking handler can't take the pool down. - Your handler reads plugin data through
&Params, so the audio thread and the worker share it through ordinary#[skip]fields - no bespoke channel to wire up. Editors can schedule the same way viactx.tasks::<T>()on the editor context. AudioTapfor the streaming shape - a spectrum analyzer, loudness meter, or oscilloscope.processpushes interleaved frames into a lock-free ring; a consumer drains and analyzes them off-thread. Whole-frame drop-on-full, so a stall can never desync channels.StreamWorker(tap.spawn_worker(...)): a dedicated consumer thread bound to oneAudioTap, for continuous work that shouldn't share the pool. It parks until the next block wakes it, drains in order, and keeps its state on the thread with no lock. Rule of thumb: the pool for bursty or discrete work, aStreamWorkerfor a continuous stream.initnow receives anInitContext, so you can kick off a first background build while constructing your DSP state.
#Breaking
Only if your plugin overrides init.
initgains an argument:fn init(params: &Params, cx: &InitContext) -> DspState. The newcxis the point of the change: it carries the task spawner, so you can start background work (a first graph build, a sample load) at construction and have it ready before the first block, instead of stalling the firstprocess. Plugins that don't overrideinitare unaffected.
#Migrating from 4.0
Add the cx: &InitContext parameter to your init. If you don't schedule startup work, that's the whole change (an unused cx is fine); process, reset, editor, and the rest are untouched.
- fn init(params: &MyParams) -> MyDspState {
+ fn init(params: &MyParams, cx: &InitContext) -> MyDspState {
let state = MyDspState::from(params);
+ // Optional: kick off startup work now that `cx` is available.
+ if let Some(tasks) = cx.tasks::<MyTask>() {
+ let _ = tasks.try_spawn(MyTask { /* ... */ });
+ }
state
}
#4.0.0
PluginLogicnow separates your code from your data. The trait is a stateless descriptor and its methods (process,reset, ...) are plain functions - no&self- each handed exactly the data it needs. Your plugin's data falls into three kinds, and the signatures keep them straight:- Parameters (
type Params) - your#[derive(Params)]knobs. The host automates and saves them for you; you get&Paramsinprocess,Arc<Params>in the editor. - Saved state - anything else the host should remember across a session or preset. Tag a
#[derive(Params)]field#[persist]and it's saved next to your params with no extra code - a view mode, an instance name, a picked file path. (Copyvalues go in a lock-freeAtomicCell, strings and vecs behindRwLock/Mutex; per-field keys let old and new sessions load each other.) For a larger or opaque blob,save_state/load_statewith#[derive(State)]still work. - DSP state (
type DspState) - your filters, phase, and voices. Never saved, and it lives outside the descriptor, so hot-reload keeps reverb tails and oscillator phase alive across an edit-and-reload.
- Parameters (
PurePluginLogicfor the common case: a stateless effect implements it instead ofPluginLogicand writes far less - noDspState, noinit, no state argument onprocess(andinit/resetare optional even onPluginLogic, since aDefaultstate and a no-opresetcover most plugins). Everything downstream (truce::plugin!, every format) treats it as an ordinaryPluginLogic.cargo truce newscaffoldsPluginLogicwith a#[derive(Default)]DSP-state struct and thestateargument pre-wired by default; pass--purefor the stateless version.- Offline-render awareness:
processandresetnow see aProcessMode(realtime / buffered / offline), so you can raise quality or relax realtime discipline during a bounce. On every format. - Automatic latency updates: change what
latency()returns and the host is told, so plugin delay compensation follows a mode switch. On every format (VST2 best-effort). truce_test::BlockRunner: run yourprocessone block at a time in a unit test - it owns the DSP state across calls and hands back the output audio and events - without spinning up the full driver.
#Breaking
- Your plugin type is now a stateless descriptor: move its DSP fields into
type DspState, drop the storedparamsArc, and takestate/paramsas arguments instead of&self. type DspStatemust implementDefault(that's the defaultinit) - your oldfn new(...)body usually becomes theDefaultimpl.resettakes&AudioConfiginstead of(sample_rate, max_block_size).
#Migrating from 3.x
Three moves, whatever your plugin does:
- Move the non-param fields (filter memory, phase, voices) off your plugin type - now a stateless descriptor - into
type DspState. Your#[derive(Params)]struct doesn't move. - Replace
fn new(params) -> Selfwith aDefaultimpl on the state type; writefn init(params)only if building it reads params. No more storedparamsArc. - Give each method
state/paramsinstead of&self, andresetan&AudioConfig.
The only choice left is where your DSP state lives: a pure effect implements PurePluginLogic and skips it entirely; a small effect can set type DspState = Self and keep its fields on the plugin struct; anything bigger uses a separate struct, like this:
-struct MyPlugin {
- params: Arc<MyParams>,
- filter: Filter,
- phase: f32,
-}
+struct MyPlugin; // stateless descriptor
+
+#[derive(Default)]
+struct MyDspState { // the former non-params fields
+ filter: Filter,
+ phase: f32,
+}
impl PluginLogic for MyPlugin {
type Params = MyParams;
+ type DspState = MyDspState;
- fn new(params: Arc<MyParams>) -> Self {
- Self { params, filter: Filter::default(), phase: 0.0 }
- }
-
- fn reset(&mut self, sample_rate: f32, max_block_size: usize) {
- self.filter.set_rate(sample_rate);
+ fn reset(state: &mut MyDspState, _params: &MyParams, config: &AudioConfig) {
+ state.filter.set_rate(config.sample_rate);
}
- fn process(&mut self, buffer: &mut AudioBuffer, events: &EventList, ctx: &mut ProcessContext) -> ProcessStatus {
+ fn process(state: &mut MyDspState, params: &MyParams, buffer: &mut AudioBuffer, events: &EventList, ctx: &mut ProcessContext) -> ProcessStatus {
// self.filter -> state.filter, self.params -> params
}
// editor is unchanged - still fn editor(params: Arc<Self::Params>) (see 3.0.0)
}
#3.1.4
- AU v2 host automation fix. Based on a fix by @fpbrault.
cargo truce installregenerates missing param-manifest sidecar files instead of failing- An AU v3 plugin with MIDI 2.0 output keeps a native 2.0 output stream even when the host negotiates 1.0 input
#3.1.3
- AU v3 editors resize to the host pane on iOS across all GUI backends (built-in, egui, iced, slint), honoring
resizable/min_size/max_size/aspect_ratio. Ported from a fork by @tothepoweroftom. - A stereo-output AAX instrument now advertises only its stereo configuration (no longer shows up as a multi-mono plugin).
- XY pad dots no longer clip at the pad edges (built-in, egui, iced, vizia).
#3.1.2
- rt-paranoid's checker mode is now set only with
truce::rt::set_mode; theTRUCE_RT_PARANOIDenvironment variable is removed. - rt-paranoid can also flag frees on the audio thread, not just allocations, via
truce::rt::set_check_dealloc(true)(off by default). - rt-paranoid gained lock detection:
truce::rt::Mutex/RwLockflag a lock taken inprocess, andtruce_test::assert_realtime_cleangates a test on allocations, frees, and locks together.
#3.1.0
- New
rt-paranoidfeature: a development-time checker that flags any allocation your DSP makes on the audio thread insideprocess, withtruce_test::assert_no_audio_allochelpers to gate it in tests. Off and zero-cost by default. cargo truce newwiresrt-paranoidinto fresh plugins (the feature plusenable_rt_paranoid!()), so a new project can gate on audio-thread allocations from its first test.
#3.0.0
Refactored editor into an associated function: it takes the parameter store as an argument (Arc<Self::Params>) instead of borrowing the plugin (&self). This turns a runtime convention into a compile-time guarantee - the editor is now, by construction, a function of its parameters, so building it can't take the plugin lock or reach into DSP state at all. An editor that genuinely needs shared DSP-derived state (an analyzer's spectrum, say) routes it through a #[skip] field on the params struct, keeping the store the single thing the editor sees.
#Breaking
editoris now an associated function. It receivesArc<Self::Params>instead of&self. Because it has no access to the plugin instance, editor construction provably never touches the plugin lock - a guarantee the type system now enforces rather than the framework upholding it at runtime. For a typical plugin this is a one-line signature change; see the migration steps below. Movingeditoralso shifted the hot-reload ABI, so a--shellshell and logic dylib must be rebuilt and reinstalled together (GUI edits still hot-reload on the next editor close and reopen; static shipped builds need nothing).
#Migrating from 2.x
-
Update each editor. Add
type Params, takeparamsinstead of&self, and use it in the body. For a typical plugin (editor only readsself.params) that's the whole migration:impl PluginLogic for MyPlugin { + type Params = MyParams; + // reset, process, ... - fn editor(&self) -> Box<dyn Editor> { - default_editor(self.params.clone(), layout()) + fn editor(params: Arc<MyParams>) -> Box<dyn Editor> { + default_editor(params, layout()) } }The same
self.params->paramsswap covers the other backends:.into_editor(¶ms),EguiEditor::new(params, ...),IcedEditor::new(params, ...), and so on. -
Only if an editor read live DSP state at construction (an analyzer handing its spectrum to the GUI, say): it no longer has
self. Route the shared handle through the params struct as a#[skip]field (a non-parameter), fill it innew(), and read it back ineditor:#[derive(Params)] pub struct MyParams { #[param(name = "Gain", /* ... */)] pub gain: FloatParam, + #[skip] + spectrum: Arc<OnceLock<Arc<Spectrum>>>, } fn new(params: Arc<MyParams>) -> Self { let spectrum = Arc::new(Spectrum::new()); + let _ = params.spectrum.set(spectrum.clone()); Self { params, spectrum, /* ... */ } } - fn editor(&self) -> Box<dyn Editor> { - MyEditor::new(self.params.clone(), self.spectrum.clone()) + fn editor(params: Arc<MyParams>) -> Box<dyn Editor> { + let spectrum = params.spectrum.get().expect("set in new()").clone(); + MyEditor::new(params.clone(), spectrum) } -
Rebuild
--shellpairs together. A 3.0 shell won't pair with a 2.x logic dylib; rebuild and reinstall both in one pass. Ordinary (non---shell) builds need nothing beyond step 1.
#Added
- Lock-free state save, opt-in. If a plugin has custom state (beyond parameters) that's expensive to serialize, it can override
snapshot_into(&self, buf)instead ofsave_stateto take the whole save off the plugin lock: the audio thread serializes the state into a lock-free slot each block and the host reads it back on save without ever locking the plugin. Because it runs on the audio thread every block, keep it cheap and allocation-free (bufarrives cleared with its capacity kept, so just refill it) - it's a win only when asave_statestall would otherwise be long. Overriding it is the entire opt-in: which method you implement is the choice, the defaultsave_statedelegates to it, and plugins that override neither (or stick withsave_state) are unchanged.
#2.0.2
- Audio Unit plugins now report their latency and tail time to the host, so lookahead limiters and linear-phase EQs stay time-aligned in the mix instead of playing early.
- Plugins ship correctly under any vendor identity: VST2 bundle IDs and Audio Unit registrations use your configured reverse-DNS vendor ID everywhere, so third-party plugins install, update, and uninstall cleanly (some IDs previously assumed a
com.truceprefix). - Plugin and vendor names with special characters (
&,{,<, quotes,$) no longer break installer generation on Windows, macOS, or Linux. - macOS
--userinstallers that include AAX, AU v3, or the standalone app now install those components instead of silently skipping them. cargo truce preset import/pull --categoryno longer overwrites a same-named preset that lives in a different category.- Loading a preset or session into an inactive plugin no longer drops the plugin's custom state on the next save.
- Stability fixes: a Windows standalone crash when an editor reports inconsistent size limits, an audio-thread hang when
min_subblock_samplesis set to 0, and a buffer-overrun risk in dense LV2 MIDI output; plus smoother real-time behavior under heavy MIDI-controller automation.
#2.0.1
- The built-in editor on Windows no longer occasionally opens stretched (until a manual resize) when the host resizes the editor right after open: resize requests that raced the threaded GPU init used to be dropped, leaving the swapchain at the open-time size for the compositor to stretch over the real window. They are now replayed once init lands, and the swapchain is additionally reconciled against the window's actual client rect on the first ready frame.
- Aspect-locked editors in the Linux standalone now hold their ratio during an edge-drag instead of letterboxing: the outer window's X11 size hints gained the missing
PAspectconstraint, so the window manager keeps the drag on ratio the way Windows (WM_SIZING) and macOS (setContentAspectRatio:) already did. Affects backends whoseset_sizetakes any size verbatim (egui / iced / Slint); honored by floating WMs, ignored by tiling ones as with the existing min/max hints. - Smoother real-time behavior on Linux: a host save or editor preset capture can no longer stall the audio thread behind a preempted GUI thread - the plugin lock now boosts the holder with the waiting audio thread's priority, matching what macOS already did.
- Fuzzing now verifies SysEx reassembly is the exact inverse of the encoder (not just crash-free), and one crashing fuzz target no longer eats the other targets' time.
#2.0.0
A MIDI overhaul: MIDI 2.0 / UMP and multiple MIDI ports, opt-in per plugin. Existing MIDI-1.0, single-port plugins are unchanged at runtime; the Event change below is a breaking API change.
#Breaking
Eventgained aportfield - the MIDI port an event arrived on / should go out on (0for single-port plugins). Migration: build events withEvent::new(offset, body)(port0, the common case) orEvent::on_port(offset, port, body); a struct literal must now spellportout. Reading events (event.port) is unaffected.- Every plugin identity derives from
bundle_id.clap_id/vst3_id/ the state-envelope hash no longer follow the display name, so renaming a plugin no longer changes its identity. Migration: hosts key sessions and presets to these ids, so a plugin already shipped under a 1.x name-derived id ("Truce Envelope" ->com.truce.truceenvelope) will appear as a new plugin after this change - to keep the old id, setbundle_idto the old name-derived slug.bundle_id's shape is validated at compile time (lowercasea-z0-9plus-/_/.separators, alphanumeric at both ends, no separator runs). EventList::sortis removed. It was a std stable sort, which allocates - unusable on the audio thread where wrappers sort merged input streams. Migration: callensure_sorted_by_offset(), the allocation-free equivalent with the same stable-by-offset semantics.
#Migrating from 1.x
- Pin your identity first. 2.0 derives
clap_id/vst3_id/ the state hash frombundle_id, not the display name. If a 1.x build ever shipped, setbundle_idto the old name-derived slug (the display name lowercased with spaces stripped: "Truce Envelope" ->truceenvelope); otherwise hosts treat the plugin as new and old sessions won't find it. Unshipped plugins pick any valid id. - Fix
Eventconstruction. Struct literals need the newportfield; preferEvent::new(offset, body)orEvent::on_port(offset, port, body). Reads are unaffected. - Replace
EventList::sort()withensure_sorted_by_offset(). - Rebuild everything together. The hot-reload canary and the AU v3 framework/appex handshake are version-checked: a 2.x shell refuses a 1.x logic dylib and the appex refuses a 1.x framework, so rebuild and reinstall all artifacts in one pass instead of mixing.
- Loads fail honestly now. State the plugin doesn't recognize no longer resets to defaults - implement
migrate_state(plus[plugin.legacy_state]keys for keyed formats) if you need to accept pre-truce or re-identified blobs; otherwise nothing to do. - MIDI stays opt-in. Single-port MIDI 1.0 plugins behave exactly as on 1.x; add
midi2/midi_input_ports/midi_output_portsonly when you want the new capabilities.
#MIDI 2.0 / UMP
- Opt in per direction in
truce.toml:midi2 = truesets both;midi2_input/midi2_outputoverride one. Without the opt-in, 2.0 input down-converts to 1.0 before delivery. Opting a port-less direction (or a port-less plugin) into 2.0 is a compile error, and port-less directions normalize to a 1.0 dialect. - CLAP decodes
CLAP_EVENT_MIDI2+ note expressions on input and emits CLAP-native output (notes, expressions, raw MIDI). - AU v3 declares
audioUnitMIDIProtocolfrom the input dialect; output rides the host'sMIDIEventListblock for every dialect, protocol-pure in the host'shostMIDIProtocol, with the byte block as fallback (macOS 12+ / iOS 15+). - VST3 carries per-note control as note expression both ways:
INoteExpressionControlleris declared, hostnoteIds resolve back to(channel, note), values cross at fullf64precision, and unmapped per-note CCs degrade to channel CCs. Channel-level MIDI input rides hidden per-portIMidiMappingproxy parameters so event-consuming plugins hear CC / pitch bend / pressure. - Per-note conventions are shared across formats: pitch-bend full-scale is ±48 semitones, volume crosses in the
0..=4linear-gain domain, and only registered per-note controllers map onto expressions and 1.0 CCs. - VST2 / AU v2 / AAX / LV2 stay MIDI 1.0.
#Multiple MIDI ports
- Declare
midi_input_ports = N; the plugin filters byevent.port. CLAP (note ports), VST3 (event buses), and LV2 (atom ports) deliver per-port; VST2 / AU / AAX clamp to one and log a skip. - A port count is authoritative - capability is
count > 0- and a count that contradictsmidi_input/midi_outputis a compile error. - The standalone maps one MIDI device per port:
--midi-inputis repeatable, and macOS / Windows grow a per-port device menu (sixteen-port menu cap; the CLI has none). truce-example-multiportis the reference; host routing quirks (REAPER's VST3-only bus mapping) are in the midi guide.midi_output_portsdeclares output ports the same way; treat multi-port output as wire-level plumbing for now.
#Legacy state migration
- New
migrate_statehook onPluginLogic: state that isn't this plugin's envelope (a pre-truce blob, or a truce envelope under another id) is offered to the hook instead of failing; keyed formats probe[plugin.legacy_state](au_keys/lv2_uris/aax_chunk_ids). Unrecognized state now fails the load honestly instead of resetting to defaults. See the state guide;truce-example-eq+assert_state_migrationare the reference.
#Native f64 processing
prelude64plugins take the host's 64-bit wire directly on VST3 (kSample64), VST2 (processDoubleReplacing), and CLAP (CLAP_AUDIO_PORT_SUPPORTS_64BITS); the other formats keep the f32 wire with the existing widen/narrow. f32 plugins are unchanged.
#Internals
- Audio-thread isolation: meters, state loads, and editor param edits reach the audio thread through lock-free handoffs; only a host state save or an editor preset capture briefly waits on a block boundary, with priority donation on macOS. (#175)
- The core is continuously checked with Miri (PR gate) and fuzzing (weekly) covering state envelopes, presets, MIDI/UMP decode, and SysEx reassembly.
- The built-in editor's font rasterizer changed (fontdue -> skrifa). Metrics are unchanged; glyph edges anti-alias subtly differently, so regenerate screenshot baselines.
- Hot reload refuses a logic dylib built against an incompatible truce version instead of crashing - rebuild both halves after upgrading.
EventBodyandTransportInfoderivePartialEq(assert on them directly in tests), andtruce_core::midiexposesupconvert_to_midi2plus the MIDI 2.0 up-scaling helpers.
#Fixes
- AU: generic views and control surfaces show formatted parameter values instead of raw numbers.
- AU: a plugin binary paired with a shim or appex from a different truce version fails loudly at build or instantiation instead of crashing the host through mismatched callbacks.
- LV2: percent parameters no longer display as
1%in Ardour. - VST3: processing with a never-negotiated sample size is rejected instead of misreading buffers.
- VST2: an output SysEx that overflows its scratch can no longer hand the host a garbage event pointer.
- CLAP: choked notes deliver a note-off instead of being dropped. (#174)
- CLAP: malformed note addresses are dropped instead of reaching the plugin as note/channel 255, and wildcard note-offs, chokes, and note expressions apply to every matching sounding note - hosts that address notes by id can't leave voices hanging or lose expression.
- CLAP: output events reach the host sorted by time, and note-port queries answer clap-validator's swapped-direction sweep.
- Preset menus and browsers list only presets the plugin can actually load; files from other plugins or older identities are hidden, and loading an older-identity preset goes through
migrate_statelike a session load. - Standalone: f64 plugins no longer allocate in the first audio callback (scratch is sized up front from the device's real maximum), and switching to a device with a larger buffer re-issues
resetso the plugin never sees blocks bigger than it prepared for. cargo truce runbuilds only the standalone (no format wrappers); a new--featuresflag re-adds extras.cargo truce validateno longer leaks cargo's dynamic-linker env vars into the validators (false failures on some setups).
#1.0.5
- Editor performance and stability overhaul on Windows. Every GPU call that can block inside the graphics driver - device creation, swapchain reconfigure / acquire / present - now runs off the host's GUI thread in all wgpu backends: egui renders on a dedicated thread, and the iced / Slint / built-in editors route those calls through a per-editor surface-pump thread that pre-acquires frames. Repaint-heavy editors (meters, animations) no longer bog down the host's UI; editors open without stalling the DAW; resizing reflows live and lands crisp on release instead of showing a stretched frame for seconds (dropping an acquired DX12 frame unpresented starves the swapchain's frame-latency wait - stale frames are now presented, keeping paints flowing through resize churn); and a wedged driver costs a blank or paused editor instead of a frozen, previously unkillable, DAW. Corrective resize requests also moved out of the host's resize dispatch, and an out-of-bounds host size is letterboxed rather than pushed back into a fight with the host. vizia (OpenGL, frame loop in upstream
vizia_baseview) probes for working WGL up front and keeps the editor closed when the GL driver is broken, rather than aborting the host from its window proc. - Editor size constraints are honored even when the host bypasses resize negotiation. Some hosts resize the plugin's embedded window directly at the windowing-system level (Bitwig on X11), skipping VST3
checkSizeConstraint/ CLAP resize hints - somin_size/max_size/aspect_ratiowere silently ignored. Every GUI backend's resize handler now fits the incoming size to the editor's declared bounds and pushes a corrective resize back (at most once per offending size, so an uncooperative host can't be spun into a feedback loop); a non-resizable editor reports its natural size as both min and max so it stays pinned rather than ratcheting larger. When a host still forces the window past the editor's max (or off its locked aspect), the editor renders at its fitted size anchored top-left with the surrounding area filled black instead of stretching to a blurry fill - reflowing to fill within[min, max]as before (the built-in editor centers its letterbox). Applies to egui / iced / Slint / built-in. The Windows and macOS standalones now also enforce min / max (and the aspect lock, for interactive drags) on their own window frames - Windows via aWM_GETMINMAXINFO/WM_SIZINGsubclass that also covers programmatic resizes, macOS viacontentMinSize/contentMaxSize/contentAspectRatio(which also bound the zoom button) plus a per-frame snap-back for the programmaticsetFrame:calls that bypass them - previously only backends whoseset_sizeclamps kept the frame honest. (#163) - HiDPI (2x) plugin editors render correctly on Linux. Under desktop display scaling (e.g. Ubuntu at 200%) embedded editors were mis-sized across every backend and format - rendered at half size or clipped, and in REAPER the LV2 editor opened at half size because REAPER reads the
ui:resizerequest as physical then divides by the scale to size its pane. The surface now tracks the window's physical size on every resize, the LV2 request is scaled to compensate, and standalone editors no longer jitter at 2x. (#163) - Right- and middle-click now reach iced and Slint editors. Both backends forwarded only the left button from baseview, so right-click-to-reset (and any custom-widget use of other buttons) never fired; all mouse buttons map through now. (#168)
- Fixed a crash when closing and reopening plugin editors on macOS. The editor's frame timer could fire after its window state was freed (a use-after-free most visible as an AU v3 editor crash on reopen); the timer is now invalidated at teardown and holds only a weak reference. Via the
baseview-truce0.1.1-truce.12 dependency, all GUI backends.
#1.0.4
cargo truce build/install/packageaccept--features. Pass extra Cargo features for the plugin crate (comma/space-separated); they apply to every underlying build (per-format, shell logic, iOS, standalone bin) so the set stays uniform. Format features (clap/vst3/...) are reserved for the format flags.- VST3 advertises all 16 MIDI channels. The event bus reported
channelCount = 1, which could make a channel-aware host route only MIDI channel 1; it now reports 16, matching the channels truce delivers and emits.
#1.0.3
- Fixed a black margin when resizing plugin editors on Linux. In hosts that resize the embedded plugin window directly instead of calling the plugin's resize API (Bitwig on Linux/X11), the editor's child window stayed at its old size while the host's frame grew, leaving the newly exposed area black until the window was moved. baseview now watches the host embed window and mirrors its size onto the editor's child, so editors follow a host-driven resize immediately. Fixes every GUI backend (egui / iced / slint / vizia) and both CLAP and VST3, via the
baseview-truce0.1.1-truce.11 dependency.
#1.0.2
- Fixed iOS AU v3 packaging bugs for App Store submission.
cargo truce package --ioscorrects code signing, framework embedding, entitlements, and the required Info.plist keys App Store Connect upload checks. - Fixed
TransportInfo.position_samplesstuck at 0 during playback. The VST3 shim read the host'sprojectTimeSamples(anint64) as adouble, so any realistic sample count decoded to ~0; it's read as an integer now. CLAP, which exposes no sample position, derives it from the seconds timeline instead of always reporting 0. - Vizia editors honor the host content-scale factor. The backend ignored
set_scale_factorand fell back to vizia's OS-detected scale, which can disagree with the host's on an embedded view (notably Windows DPI) and render the editor oversized and clipped. It now pins vizia's scale policy to the host's reported scale; the VST3 wrapper also replays a scale that arrived before the editor existed.
#1.0.1
IcedPlugin::needs_redrawkeeps streaming editors live. The iced editor's idle gate skips repaints when no UI input, param, or meter changed - which left a plugin streaming realtime data to its editor outside that system (a lock-free queue drained inview()) showing it late, only when a stray UI event forced a repaint. A plugin can now returntruefromneeds_redraw()while it has new data, so the editor repaints and drains promptly and idles again when there's nothing new.truce-example-midi-inspectoruses it so live MIDI scrolls in without lag.truce-example-envelopeemits sample-accurate CC. The envelope follower pushed one control-change per block atsample_offset: 0carrying the block's final value; it now emits on each value change at the exact sample offset, so the CC stream stays smooth (no per-block@0bursts or up-to-a-block latency) and tracks level changes within a block.
#1.0.0
#Breaking
- Auto-assigned parameter IDs are now a stable hash of the field name instead of a declaration-order counter. Reordering or inserting parameters - including inside
#[nested]groups - no longer shifts later IDs, so host automation and presets keep mapping to the right parameter across plugin versions. Explicit#[param(id = N)]still wins. Migration: a plugin already shipped with the old order-based IDs must add#[params(id_scheme = "ordinal")]to keep them, or its hosts' saved automation and presets will rebind to the wrong parameters. truce-icedno longer depends on theicedumbrella crate. It uses iced's sub-crates directly (iced_core/iced_widget/iced_renderer/iced_wgpu/iced_runtime/iced_futures), re-exported astruce_iced::iced, which drops the transitiveiced_winitdependency whose desktop-only keyboard API broke the iOS build. Migration: plugins that imported types from theicedcrate import the same ones fromtruce_iced::icedinstead.- Removed the deprecated
selector/param_selectorwidget. Theselector()layout builder and each backend'sparam_selector(egui / iced / vizia / slint), deprecated since 0.56, are gone along with the underlyingSelectorwidget. Migration: usedropdown()/param_dropdown- the same single-choice control, click-to-open. - Removed the deprecated
FloatParam::read_block. The const-Nblock read, deprecated since 0.53, is gone. Migration: useread_into(&mut scratch[..n]), which advances the smoother by exactly the number of samples consumed.
#Other changes
- iced editors run on iOS.
truce-icedrenders through aCAMetalLayer-backedUIViewunder AU v3 - touch input plus a soft keyboard for focusedtext_input- so iced plugins build and run on iOS like the egui backend.gain-iced,gui-zoo-iced, andmidi-inspectorare now iOS examples. - AU v3 factory presets ship on iOS.
cargo truce install --ios/package --iosnow bundle a plugin'spresets/library into the embedded framework'sPresets/, so AU v3 hosts list them on iOS like macOS. iOS frameworks are shallow bundles, so the presets sit in a flatPresets/directory rather thanResources/Presets/(aResources/subdir makes iOS installd reject the framework). - New
truce-example-midi-inspector(iced). An audio effect with MIDI in/out that passes the track's audio through untouched and decodes every event truce can deliver - MIDI 1.0 + 2.0 channel voice, SysEx (with manufacturer id + hex), transport, and param automation - into a live scrolling log (newest first), with a raw line for anything not yet richly interpreted. Forwards MIDI through (toggle with theMIDI Thruparam) and demonstrates streaming structured realtime data fromprocess()to an editor via a lock-free ring carried in a#[skip]params field. #[derive(Params)]supports#[skip]fields. A field marked#[skip]is not a parameter: it's plugin-owned state that the editor reaches through theArc<Params>both sides already hold (e.g. a lock-free queue of audio-thread events). The deriveDefault-initializes it innew()and excludes it from parameter ids, infos, state, and count.- CLAP and LV2 state save/load are now panic-guarded like VST3 / VST2 / AU / AAX: a panic in a user's
save_state/load_statereports failure to the host instead of unwinding across theextern "C"boundary and aborting it. - Rendering performance fixes on Windows. Embedded editors run their frame loop on the host's GUI thread, where the iced / egui backends repainted every tick and blocked on a vsync present - so a heavy editor (GUI Zoo) made the host (REAPER) laggy and could lock out other plugin windows. Editors now skip frames when nothing changed, present non-blocking on Windows, and skip rendering while the host window is hidden or minimized.
- Smoother editor repaints on Windows. Editors no longer render in slow, bursty spurts inside busy DAWs - the frame loop is now driven by a steady high-resolution timer (via the
baseview-truce0.1.1-truce.10 dependency), and the same fix closes a crash that heavy repainting could trigger. Applies to both the iced and egui backends.
#0.64.0
- Parameters can declare a default MIDI mapping (
#[param(..., midi_cc = N)]) - VST3, AU v2, and LV2 expose the binding to the host's MIDI controller assignment; CLAP / VST2 / AAX leave it host-driven. - VST3 accepts channel MIDI controller input - CC, program change, channel pressure, pitch bend, and poly pressure now reach the plugin, decoded from VST3's legacy MIDI controller event forms. (#145, by @Boscop)
- VST3 bridges host-mapped MIDI controllers back into MIDI events. A parameter bound via
#[param(midi_cc = N)]/#[param(midi_source = …)]is the target a VST3 host routes the matching controller to (overIMidiMapping); that parameter change is now also delivered as the correspondingEventBody(pitch bend, CC, channel pressure, program change), so plugins reading MIDI events - not the parameter - respond to the pitch and mod wheels under VST3 as they already do on AU / CLAP. - The synth example responds to the pitch and mod wheels.
truce-example-synthnow bends pitch (+/-2 semitones) and adds mod-wheel (CC1) vibrato. - SysEx input fixes. VST2 and AAX no longer drop a queued input SysEx to the per-block event-list clear; AU now accepts SysEx input (v2 via
MusicDeviceSysEx, v3 via UMP SysEx-7/8). - Plugin-process parameter changes notify the host. A parameter the plugin changes during
process()now updates the host's UI / automation on VST2, VST3, and AU (CLAP already did) - viaaudioMasterAutomate, the VST3 output parameter queue, andAUEventListenerNotifyrespectively. (#147, VST2 by @Boscop)
#0.63.3
cargo truce --shellfinds[profile.shell]at the Cargo workspace root - the preflight no longer rejects plugin crates that are members of a larger workspace. (#148)truce-viziano longer risks messaging a freed parentNSViewon editor teardown - the per-frame macOS re-anchor stops once the editor closes.viziais now documented as not recommended for production due to teardown/resize stability issues internal to vizia.
#0.63.1
- Slint editors receive keyboard input.
truce-slinttranslates native key events into SlintWindowEventkey events, soLineEdit/TextInputandFocusScopework in plugin editors (host focus permitting). - The slint
gui-zooexample demos native Slint widgets - button, checkbox, spinbox, slider, progress bar, image - plus a keyboard section, alongside the truce param widgets. - The vizia
gui-zooexample demos native Vizia widgets - button, checkbox, slider, image - plus a keyboard section, alongside the truce param widgets.
#0.63.0
- iced editors receive keyboard input.
truce-icednow forwards native key events into the iced widget tree (a focusedtext_inputor a custom key-captureWidgetreadingphysical_key/ logical key), andIcedPlugin::subscription()(e.g.iced::keyboard::listen,iced::event::listen_with) now fires via an event pump. Keys arrive when the host grants the editor window focus. (#134) gui-zooexamples (iced + egui) demo native framework widgets - button, checkbox, radio, slider, progress bar, image, etc. - plus a keyboard section, alongside the truce param widgets.
#0.62.0
- AU v3 and standalone are one macOS app. A plugin with a standalone bin ships a single
{name}.appthat is both the AU v3 container and the playable standalone host; the separate Standalone format collapses into it and the installer choice reads "AU3 + Standalone". Plugins without a standalone bin still ship the appex, in an informational stub app. - Fixed macOS standalone resize leaving a white margin around the editor; non-resizable editor windows are now pinned so they can't be zoomed open.
package --suite <name>matches a suite by bundle id or display name and errors on an unknown name instead of silently skipping the suite.
#0.61.0
- Reusable
#[nested]param groups. A nestedParamsstruct's ids are rebased by a per-group base (auto-packed, or pinned with#[nested(base = N)]), so the same group type can be nested more than once without id clashes and nested groups need no per-param ids. - Nested meters are caught at construction. Two
#[nested]groups that each declare a meter would have aliased on one id; the derive now panics at construction instead of corrupting state silently. - Nested params construct through the generated
new(). AParamsstruct that mixes its own params with#[nested]groups now default-initializes the nested fields innew(). (#137, by @jedStevens)
#0.60.1
#[derive(Params)]no longer trips a rust-analyzer error. Parameters with arange(or numericdefault) emitted suffixed literals like60f64, which rust-analyzer 1.96 rejected with "expected Expr" even though the code compiled; the derive now emits plain decimal literals. (#135)
#0.60.0
midi_input/midi_outputcapability flags. New[[plugin]]truce.toml keys let an instrument or audio effect opt into MIDI input/output (or opt out), overriding the category default, consistently across every format.- AU MusicEffect for MIDI-driven effects. An audio effect with
midi_input = truenow registers as anaumfMusicEffect, so AU hosts route MIDI to it instead of anaufx. - VST2 MIDI output fixed on 64-bit. The outbound
VstEventsblock placed its pointer array at the wrong offset, so hosts read a garbage event pointer; plugins now emit MIDI correctly. (#131) - VST3 emits non-note MIDI output. Control change, pitch bend, aftertouch, channel pressure, and program change reach the host now, not just note on/off. (#123)
- CLAP advertises the MIDI note dialect. Raw-MIDI output events (CC, pitch bend, SysEx) are no longer dropped by dialect-routing hosts.
- SysEx payloads under sample-accurate chunking. The chunking layer handed
process()a timing-rebased event list whose SysEx pool was empty, so a plugin reading a SysEx payload would index out of bounds; the rebased list now carries its own copy of the payload.
#0.59.0
- Honor cargo's real target directory.
cargo trucenow reads cargo'starget_directoryviacargo metadatainstead of assuming<plugin>/target, fixing installs when the plugin crate is a member of a larger workspace (artifacts land in the workspace-roottarget/). (#124)
#0.58.4
- Aspect-ratio editor resize fixes. Aspect-locked editors stay on-ratio inside the host window without clipping or juddering across CLAP, VST3, AU, and LV2; corner drags track the ratio smoothly and LV2 honors the aspect ratio and
max_size. - Skip rendering while the editor is occluded or detached (macOS). A minimized or fully-covered window can't present, so every rendered frame queued a GPU drawable that never drained. Editors now bail before rendering when the host window is occluded or has no window attached, across every GUI backend (built-in CPU / GPU, egui, iced, slint). (#126, by @tothepoweroftom)
TRUCE_AZURE_ENDPOINTfor Windows code signing. The Azure Trusted Signing endpoint was hardcoded to East US; set this to sign through another region's endpoint. Defaults to the previous value. (#128, by @tothepoweroftom)- iced enum selector option count. Fixed an off-by-one that left the iced enum selector (pick-list) with the wrong number of options. (#129, by @jedStevens)
#0.58.3
- Installers now ship your factory presets.
cargo truce packagenow carries presets for every format that has them, in both the per-plugin and suite installers, on macOS, Linux, and Windows: CLAP / AU / LV2 ride inside the bundle (sealed under the code signature), and VST3 presets are placed into the OS preset folder — merged in, so a user's own saved presets there are never wiped.
#0.58.2
- Standalone presets. A Presets menu in the standalone host (macOS / Windows): pick a preset from Load, step through them with Previous / Next, and Save / Save As your own. Save (Cmd/Ctrl+S) updates the preset you're editing; Save As (Cmd/Ctrl+Shift+S) names a new one. On a factory preset Save is grayed and points you to Save As, so you never overwrite a stock sound by accident. Presets you save show up in the menu right away, and they land where your DAW reads them. Linux drives it all from the shortcuts. Launch straight into a sound with
--preset <name>. .maximizable(true)to allow window maximize in the standalone. Resizable editors can no longer be maximized by default — maximizing would grow the window past the editor'smax_sizeand leave an empty margin around the clamped GUI. Opt back in with.maximizable(true)(built-in grid, egui, iced, slint) for editors that render at any size; the underlyingEditor::can_maximizehook drives it on Linux / macOS / Windows. When a window does end up bigger than the editor anyway, the GUI now sits centered instead of pinned to a corner, and the freed space is painted black rather than showing glitched leftovers.- LV2 presets now apply in REAPER. REAPER listed presets but selecting it didn't change anything. LV2 presets now carry each parameter as a control-port value (
pset:value), not only an opaquestate:stateblob, so port-based hosts apply them through the control ports — which updates the editor, the host's own parameter display, and the DSP together. State restore also gained a base64 fallback for hosts that hand back the preset'sxsd:base64Binaryliteral undecoded (REAPER) instead of as a raw chunk. - Fixed-size LV2 editors no longer stretch in REAPER. REAPER resizes an embedded LV2 X11 editor to fill its FX window, which bilinear-upscaled a non-resizable GUI (the built-in synth, etc.) into a blurry mess. The built-in editor now renders at its native size, centered, with black letterbox margins instead of stretching, and the wrapper pins the editor to its natural size at open. Resizable editors are unaffected.
- VST3 factory presets rejected on Windows ("doesn't appear to be for this plugin"). Installed
.vstpresetfiles stamped the plugin's class ID as a straight byte-for-byte hex dump. That matches how macOS / Linux hosts read it, but Windows hosts use the SDK'sCOM_COMPATIBLEFUID::toStringordering, which reads the first eight bytes as a little-endian GUID — so REAPER (and other Windows VST3 hosts) decoded a different class ID than the plugin reports and refused to load the preset. Emitted presets now match the host platform's ordering. Linux / macOS were unaffected.
#0.58.1
- Pin
timeto 0.3.47. 0.3.48 was yanked from crates.io.
#0.58.0
- Presets. Drop a
presets/directory of.presetTOML files next to a plugin crate (params keyed by yourParamsstruct's field names, resolved through the derive sidecars; numeric ids also accepted) andcargo truce installships them as factory presets in every format: CLAP (native files + the preset-discovery / preset-load extensions), AU v2/v3 (factory list in Logic / GarageBand), VST3 (.vstpresetin the OS preset locations), LV2 (pset:PresetTTL). Every container wraps the same state envelope as session recall, which also powers the newcargo truce presetCLI:convertbetween any two formats,pullto turn presets saved in your DAW into library files (uuid-stable updates, param-name comments),import/exportfor pack zips, andlist/init. Underneath,truce_core::presets::PresetStoremanages factory / user / pack scopes with uuid identity, so renames never break host references and user copies override factory ones.[plugin.presets]user_diroptionally replaces thetruce/<vendor>/<plugin>part of the user-scope path. cargo truce install --au2 --debuginstalled a stale release dylib. The AU installer now resolves the build profile like every other format.
#0.57.2
- CLAP editor clipped above the host's pane in REAPER. The layout's top edge (header / first row) was drifting off the visible plug-in pane as the canvas grew. Editors now stay pinned to the top of the pane at every size. Applies to every GUI backend (built-in, egui, iced, slint, vizia).
- LV2 editor opened at host's default size instead of natural. Resizable LV2 plug-ins (GUI Zoo, etc.) used to take whatever pane size the host defaulted to. The wrapper now hints the editor's preferred size on open; hosts that honor it (REAPER, Ardour) start the pane at natural. User-drag resize from the host's window still works.
- GPU editors use the device's actual wgpu limits. Every wgpu-backed renderer (built-in GPU, egui, iced, slint) now requests
adapter.limits()instead ofwgpu::Limits::downlevel_defaults(). The downlevel cap of 2048 px per axis was clipping tall Retina canvases and crashing the host; modern GPUs report 8192+ and the layout renders at full size instead. - AU class-name collision warnings cleaned up. Multiple installed Truce AU plug-ins no longer log
TruceAuFixedContainer is implemented in both ...on host launch.
#0.57.1
- AU v2 editor clipped in Ableton (macOS). Ableton was embedding the AU v2 view at a frame smaller than the editor's natural size, which left the editor's top edge (the title header and knob row) clipped off the visible plug-in window. The AU v2 shim now pins the container to the editor's
gui_get_sizeregardless of what the host requests, so the full editor is visible in every host. - AU v3 resize wired up on macOS. Logic Pro's host-driven resize now propagates to the editor (was a no-op before). The editor's first layout pass stays at natural size so opening the plug-in doesn't snap to Logic's full pane; subsequent layouts (real user resize) reflow the editor.
- Host crash on quit (REAPER, Cubase). A Rust panic in the editor's teardown chain (wgpu surface drop,
NSViewclose) previously propagated across the AU FFI boundary as an unhandled Obj-C exception and aborted the host on quit. The AU v2destroy/gui_closecallbacks now swallow panics at the boundary - host stays alive through quit. - Cubase crash with built-in editor. A pixmap allocation failure in the built-in
GridLayouteditor was panicking insiderender, which surfaced as a host crash in Cubase. The allocation path now logs and skips the frame instead; the next frame retries.
#0.57.0
- Resizable editors. Every GUI backend opts in with
.resizable(true).min_size((a, b)).max_size((a, b))on the editor / layout, and the CLAP, VST3, AU, and LV2 wrappers round-trip host requests to it. Logical points everywhere except the built-in grid, which takes(cols, rows)cell counts (it snaps to whole cells anyway). Vizia plugin-formset_sizeis a known gap. - New
Editortrait methods.size_increment() -> Option<(u32, u32)>(interactive-resize granularity in logical points; the X11 standalone maps it ontoPResizeIncso window-edge drags snap),aspect_ratio() -> Option<(u32, u32)>((num, denom); CLAP / VST3 / AU v3 / standalone honor it, VST2 / LV2 / AAX silently ignore), andprefers_pow2() -> bool(renderer wants power-of-two surface sizes). All have defaults so existing plugins compile unchanged. Matching builder methods on every editor type; iOS variants are no-op stubs. truce-vizia: XY pad label moved to bottom. Matches the other backends.truce-icedwidgets:meterandxy_padaccept.width(Length),.height(Length),.fill(). Lets the widget stretch with the parent column / row instead of using a fixed pixel size.truce-viziawidgets:level_meterheight andxy_padw/hnow takeimpl Into<Units>. PassPixels(n)for a fixed size,Stretch(1.0)to fill the parent layout. Breaking for callers passingf32directly: wrap inPixels(_).truce-slintwidgets:MeterandXYPadusepreferred-*+min-*/max-*sizing. A parentVerticalLayout/HorizontalLayoutcan stretch them; the floor sizes keep the widgets usable at the editor'smin_size.baseview-truce 0.1.1-truce.8. Adds the macOSsetFrameSize:Resizedevent + OpenGL drawable resize that host-driven editor resize depends on. To upstream to baseview.- Windows: wgpu editors pin the DX12 shader compiler to FXC. Fixes a blank editor (egui, built-in
GpuEditor) and a host crash (Slint) in Pro Tools. wgpu 29 defaults DX12 to a dynamically-loaded DXC (dxcompiler.dll); when the host process has already loaded its own incompatibledxcompiler.dll(Pro Tools does),DxcCreateInstancereturnsE_NOINTERFACEand the entire DX12 backend fails to initialize - the wgpu instance ends up with zero adapters, so surface creation fails (white editor, or a panic on Slint's.expect). Thetruce-gpu,truce-gui,truce-egui, andtruce-slinteditor instances now requestDx12Compiler::Fxc(d3dcompiler_47.dll, always present on Windows, never conflicts). iced (wgpu 0.19, FXC by default) and vizia (OpenGL) were never affected. The Windows editor instances also narrow toBackends::DX12(the only backend feature compiled in on Windows). Offscreen/headless screenshot paths are unchanged. - AAX: fix Pro Tools scan hang on Windows. AAX registration is now lazy - triggered by the first
truce_aax_get_descriptorquery - instead of running from a.CRT$XCUlibrary-load static initializer. On Windows the AAX shimLoadLibraryAs the cdylib from insideGetEffectDescriptionsduring scanning, so the old initializer ran plugin construction (viahas_editor_static's defaultcreate().editor(), which can touch user32 in editor setup) under the Windows loader lock and hung the scan. Now mirrors how VST3/CLAP register lazily on their first host entry point. No API change.
#0.56.0
truce-vizia:param_knoblayout matches the other backends. Reordered the cell as knob → value → name; previously the name label sat between the knob and the value, which inverted the cross-backend convention.selector/param_selectorwidgets deprecated. Marked#[deprecated(since = "0.56.0")]acrosstruce-gui-types,truce-egui,truce-iced, andtruce-vizia; the slintSelectorelement gets the same notice in its widget library header (slint markup has no attribute equivalent). Use the correspondingdropdown/param_dropdown/Dropdowninstead.
#0.55.0
- AU v3: sample-accurate parameter automation. The Swift shim now decodes
AURenderEvent.parameter/.parameterRampinto per-sampleParamChangeevents with the proper within-block offset, and the chunker subdivides the audio block at each automation point. AU v2 stays block-rate (itsAudioUnitSetParameterAPI carries no sample-offset). - LV2: sample-accurate parameter automation. The TTL now advertises each parameter as a
patch:writablelv2:Parameter, and the wrapper decodes host-emittedpatch:SetObjects from the input atom sequence into per-sampleParamChangeevents (the atom event'stime_framesbecomes the within-blocksample_offset). The legacylv2:ControlPortpath stays so older LV2 hosts still update params at block rate.
#0.54.0
- New
vst3_subcategorytruce.tomlkey. Emits the secondary VST3 "Plugin Type Categories" token (Fx|Delay,Fx|Reverb,Instrument|Synth, …). Without it, Cubase buckets the plugin under "Other". Optional; opt-in per plugin.
#0.53.0
- New
FloatParam::read_into(&mut [f32])smoother API. Slice-based block read; advances the smoother by exactlyout.len(). Same one-atomic-pair amortization asread_block, runtime length. - Deprecated
FloatParam::read_block::<N>(). Always advanced byNregardless of consumed samples, silently stepping the smoothed value at the next block boundary whenever the host's block size wasn't a multiple ofN. Audible as clicks on delay / LFO-rate / any timing-sensitive smoothed param.read_into(&mut scratch[..n])is the same code shape with the hazard removed. - New
truce_simd::math64module. f64 mirror oftruce_simd::math(db_to_linear_block,linear_to_db_block,exp2_block,log2_block,tanh_block).wide::f64x4lanes. eqexample uses SIMD math64. Output stage dB → linear runs throughmath64::db_to_linear_blockinstead of scalarf64::powfper sample.- Examples migrated (
block-gain,block-saturate,eq).
#0.52.0
- New GUI backend:
truce-vizia. Param-bound widgets, headless screenshot. Desktop only (no iOS, no Windows ARM64). - New examples:
truce-example-gain-vizia,truce-example-gui-zoo-vizia. - Sample-accurate parameter automation. Param changes apply at their
sample_offsetinstead of the start of the block; smoothers start ramping at the event sample. On by default. Tune via[automation] min_subblock_samplesintruce.tomlor opt out per-param with#[param(chunk = false)]. truce-iced:with_font(bytes)matches egui / vizia. Family name is now read from the TTF (waswith_font(family, bytes)).cargo truce install/package: dedupes duplicate archive members during macOS bundle link. Fixes theclang -bundleduplicate-symbol failure plugins withskia-bindings(vizia) could hit.
#0.49.23
keyboard-types0.6 -> 0.7 workspace-wide.
#0.49.22
- New example:
truce-example-dasp-bitcrusher. Showcasesdasp_sample::Samplebit-depth round-tripping for 8-bit / 16-bit quantization, with a sample-and-hold downsampler. truce-example-gui-zoo-iced: added a Dropdown section exercising theparam_dropdownalias added in 0.49.21.
#0.49.21
truce-egui: int sliders now snap and show plain integers.param_slideradapts toParamRange::Discrete(plainmin..=max, integer step, integer label) instead of the normalized [0, 1] display.truce-egui: multi-channel meters render past 2 bars. The widget allocated a fixed 16 px width regardless of channel count, so bars beyond ~3 were clipped. Width now grows with channel count.truce-egui: toggle height matches the selector so a row mixing them bottom-anchors labels on the same baseline.truce-iced:param_dropdownalias added forparam_selector, matching the egui / CPU APIs.truce-slint: newDropdownwidget (popup-style, wraps the std-widgetsComboBox). Available viaimport { Dropdown } from "@truce".truce-slint: pinned std-widgets style tofluenton every host OS. The default picked Cupertino on macOS, whoseComboBoxrendered its chevron region with a persistent accent-blue square.truce-slint:Toggleheight bumped 40 → 50 to align labels withSelector/Dropdownin mixed rows.- New examples:
truce-example-gui-zoo-egui/-iced/-slint. Each mirrors the CPUgui-zoo's param set so every renderer's widget surface is exercised against the same shape.
#0.49.20
- GUI: discrete params now snap during drag and wheel.
IntParam/EnumParamslider, knob, and XY-pad drags previously emitted continuous normalized values; storage snapped on writeback but the in-flight edit left UI and audio reads briefly out of phase.ParamSnapshot::snap_normalizednow snaps at emit time. - GUI: dropdown A→B switch now repaints on the CPU renderer. The editor's repaint gate diffs
dropdown_is_open()which staystrueacross a close-then-open inside one click;dropdown_closeandopen_dropdownnow flag the dirty bit explicitly. - Layout: labeled sections now start strictly below the previous section's tallest widget. Section breaks advanced
cursor_rowby 1, which packed the next section alongside a tall (rows = N) widget from the prior section. Now advances pastmax_occupied_row. - New
degunit.#[param(unit = "deg")](or"°") prints e.g.180.0°.ParamUnit::Degreesslots into the existing variant set. - New example:
truce-example-gui-zoo. Passthrough plugin that exercises every built-in widget kind across mixed spans and positions, everyParamUnitvariant, and the discrete-snap path. Lives inexamples/and is wired into desktop + iOS screenshot CI.
#0.49.19
#[param(default = std::f64::consts::*)]no longer tripsclippy::approx_constantin the macro expansion. The 0.49.18 parser resolved the path to itsf64value butquote!re-emitted it as a literal. The derive now embeds the original path tokens verbatim while keeping the resolvedf64for the compile-time range / shape checks.
#0.49.18
#[param(default = ...)]now acceptsstd::f64::consts::*. Alsocore::f64::consts::*and baref64::consts::*. Lets plugins writedefault = std::f64::consts::SQRT_2instead of a literal.
#0.49.17
truce-gui: fixed dropdown menu dirty-tracking bugs. Scroll wheel, touch-drag scroll, and per-option hover highlights mutated popup state without flagging the repaint gate, so on the CPU renderer they only became visible when an unrelated event tripped a repaint. (GPU renderer was unaffected: it re-renders every frame.)
#0.49.16
- macOS packaging: CLAP/VST3/VST2 bundles are now ~half the size. The bundle-bin link path inherited
-all_loadwithout a matching-dead_strip, so every staticlib object survived; AU/AAX cdylib links got-dead_stripfor free via rustc. Added-dead_stripto theclang -bundlestep.
#0.49.15
truce-gui: fixed a dropdown popup positioning bug. The popup no longer mis-anchors relative to its trigger widget.
#0.49.14
- Standalone: added MIDI device + channel selection via a unified Settings menu on macOS/Windows, or
--midi-input/--midi-channel(omnior1-16) on Linux.
#0.49.13
- VST2/AAX: extended the 0.49.9
set_statefix. The same path that dropped GUI-edited custom state in CLAP/VST3/AU was present in VST2 and AAX; they now route editor bytes toload_statelike the others. (LV2 is unaffected: its UI is out-of-process and never touchesset_statedirectly.)
#0.49.12
truce-slint: fixed a panic on HiDPI displays after a resize event. The render buffer kept the pre-resize physical extents while slint's window adopted the new ones, so the next frame tripped slint's buffer-too-small check.
#0.49.11
- Standalone: fixed the macOS device menu — the Input/Output Device submenus were grayed out and unopenable.
- Standalone: added input/output channel selection (mono channel or stereo pair) via the macOS/Windows menus or
--input-channels/--output-channels(the CLI is the picker on Linux).
#0.49.10
- Fixed a use-after-free crash when a host closes the editor without calling
close()(seen in Ableton with several plugins loaded). The macOS frame timer kept firing against the freed editor; every editor backend now cancels its window on drop. - Fixed meters not updating in the built-in (CPU) editor. The repaint gate only watched parameter changes, so a moving meter stayed frozen until an unrelated repaint (e.g. dragging a knob) fired; the editor now repaints when a meter value moves.
#0.49.9
- Fixed
#[derive(State)]custom state not persisting when edited from the GUI. The CLAP, VST3, and AU editorset_statepaths silently dropped GUI edits; they now reachload_statecorrectly.
#0.49.6
truce-standalone: theguifeature no longer pullstruce-gpu. Standalone builds compile the GPU backend only when the plugin opts in viatruce-gui = { …, features = ["gpu"] }.
#0.49.4
- Fixed a macOS stack-overflow crash in the built-in (CPU) GUI / standalone path. Moving the cursor over a freshly-opened editor window aborted the process. Fixed in
baseview-truce 0.1.1-truce.4.
#0.49.2
- Housekeeping: minor README updates and safety fixes.
- Standalone: Disable window resizing on Windows for now (mirrors other OSes).
- Standalone: Wire
windows_iconthrough to the window'sWM_SETICONso the title bar and taskbar show the app icon.
#0.49.0
- Breaking (
PluginLogic): the GUI surface collapses to a singleeditor()method. The oldlayout(),custom_editor(),render(),uses_custom_render(), andhit_test()methods are gone — every plugin now returns its editor from one place:rust fn editor(&self) -> Box<dyn Editor> { /* ... */ }editor()is required (there is no headless auto-fallback). Migration steps below. - Renderer split: the built-in GUI now defaults to CPU (tiny-skia); wgpu is opt-in. The CPU rasterizer moved into a new
truce-cpucrate, a peer oftruce-gpu. A layout-only plugin no longer compiles wgpu and its per-OS graphics backends unless it asks for them — smaller binaries and faster builds out of the box. Opt into GPU rendering withtruce-gui = { version = "0.49", features = ["gpu"] }; thegpupath doesn't pull the CPU dependency tree, and vice-versa. Plaintruce-gui = "0.49"keeps the CPU default with no change. - The
truceumbrella no longer pullstruce-guitransitively. Plugins using the built-in renderer declaretruce-guiexplicitly — newly scaffolded projects (cargo truce new) already do.
Both paths end with .into_editor(...) — a fluent terminal that
replaces the old Box::new(...) wrapper and keeps every editor()
impl looking the same.
#Migrating layout()
Move your GridLayout body into editor() and close it with
.into_editor(&self.params):
- fn layout(&self) -> GridLayout {
- GridLayout::build(vec![widgets(vec![
- knob(P::Gain, "Gain"),
- knob(P::Pan, "Pan"),
- ])])
- .with_title("GAIN")
- }
+ fn editor(&self) -> Box<dyn Editor> {
+ GridLayout::build(vec![widgets(vec![
+ knob(P::Gain, "Gain"),
+ knob(P::Pan, "Pan"),
+ ])])
+ .with_title("GAIN")
+ .into_editor(&self.params)
+ }
.into_editor(¶ms) comes from the truce_gui::IntoLayoutEditor
trait — add use truce_gui::IntoLayoutEditor; to your imports. It
picks the renderer from the truce-gui feature you enabled (cpu
by default, gpu if opted in), so the same editor() body covers
both. If your plugin previously depended only on truce-gui-types,
add truce-gui:
[dependencies]
truce-gui-types = { version = "0.49" }
+truce-gui = { version = "0.49" } # add features = ["gpu"] for wgpu
(truce_gui::default_editor(params, layout) is the equivalent
free function if you'd rather not import the trait.)
#Migrating custom_editor() (egui / iced / slint / hand-rolled)
Rename custom_editor to editor and return the Box<dyn Editor>
directly. The new method is non-optional, so drop the Some(...)
wrapper and finish the builder chain with .into_editor():
- fn custom_editor(&self) -> Option<Box<dyn Editor>> {
- Some(Box::new(
- EguiEditor::new(self.params.clone(), (W, H), gain_ui)
- .with_visuals(truce_egui::theme::dark()),
- ))
- }
+ fn editor(&self) -> Box<dyn Editor> {
+ EguiEditor::new(self.params.clone(), (W, H), gain_ui)
+ .with_visuals(truce_egui::theme::dark())
+ .into_editor()
+ }
The zero-arg .into_editor() is the blanket truce_core::IntoEditor
helper — it's in truce::prelude, so no extra import. The egui /
iced / slint editor constructors are otherwise unchanged. These
backends supply their own renderer, so they don't need truce-gui's
cpu / gpu features — a plugin using one of them can drop the
truce-gui dependency entirely.
#0.48.11
cargo-truce: Fix various install-path bugs on Windows. VST3 now installs to system scope by default — Ableton doesn't scan the per-user VST3 directory.truce-gui&truce-gpu: Minor built-in GUI rendering improvements.
#0.48.10
- Minor housekeeping.
cargo-truce: Replace non-Latin status glyphs incargo truce packageoutput with the same[ OK ]/[FAIL]ASCII tags used bycargo truce doctor. The Unicode✓/✗characters broke rendering in Windows 10 WSL terminals.
#0.48.9
- Examples: Fix blocksize bug in EQ example.
- AAX: Set
UNSAFE_OBJC2_ALLOW_CLASS_OVERRIDEwhen building AAX to prevent same-class-name crashes when multiple AAX plugins load into the same host process (e.g. Pro Tools loading two truce plugins). Details: https://github.com/rust-windowing/raw-window-metal/issues/29 cargo-truce: Make--iospackaging behavior and naming scheme consistent with the other formats / OSes — iterates every plugin in the workspace (no longer errors when more than one is declared) and writes the artifact totarget/dist/<crate>-<version>-ios.ipanext to the macOS.pkg/ Windows.exe/ Linux.tar.gz.
#0.48.8
- truce now fully published to crates.io.
cargo truce newscaffolds with atruce = { version = "0.48" }registry pin by default; the historicalgit = "...", tag = "v0.48.7"form stays available viacargo truce new --githubfor scaffolding against an unreleased checkout. - New
truce-aax-bridgecrate carries the C ABI header socargo-trucedoesn't transitively pull the fulltruce-aaxruntime stack.
#Migrating an existing plugin to the registry pin
In your plugin's Cargo.toml, replace each
truce-* = { git = "https://github.com/truce-audio/truce", tag = "v0.48.7"[, ...] }
line with truce-* = { version = "0.48"[, ...] }. Concretely:
[dependencies]
-truce = { git = "https://github.com/truce-audio/truce", tag = "v0.48.7" }
-truce-gui = { git = "https://github.com/truce-audio/truce", tag = "v0.48.7" }
-truce-clap = { git = "https://github.com/truce-audio/truce", tag = "v0.48.7", optional = true }
-truce-vst3 = { git = "https://github.com/truce-audio/truce", tag = "v0.48.7", optional = true }
-truce-standalone = { git = "https://github.com/truce-audio/truce", tag = "v0.48.7", features = ["gui"], optional = true }
+truce = { version = "0.48" }
+truce-gui = { version = "0.48" }
+truce-clap = { version = "0.48", optional = true }
+truce-vst3 = { version = "0.48", optional = true }
+truce-standalone = { version = "0.48", features = ["gui"], optional = true }
Preserve any optional = true / features = [...] keys that
were already on the line. Workspace-mode plugin Cargo.tomls
(truce-* = { workspace = true }) need no change — only the
workspace root [workspace.dependencies] block flips. Then:
cargo update -p truce
cargo build
Cargo's caret resolver expands "0.48" to >=0.48.0, <0.49.0,
so you'll pick up any future 0.48.x patch release without
re-editing.
#0.48.4
- Standalone: Fix default device selection on Linux (broke after cpal 0.17 update in 0.48.x).
#0.48.3
cargo-truce: Improve iOS screenshot reliability, clear any stale_truce_editor_frame.jsonbefore launch, extend the first-paint poll timeout for cold CI runners, and hard-fail with a diagnostic when the editor never renders (instead of silently proceeding to a misleading "screenshot size mismatch").
#0.48.2
truce-egui: Migrate to egui 0.34 (bumpswgputo 29 transitively).- Breaking (
truce-egui):EditorUi::uiand theEguiEditor::newclosure now receive&mut egui::Uiinstead of&egui::Contextegui 0.34 deprecatesContext::runplus the per-panelshow(ctx, …)entry points in favor ofContext::run_uiandshow_inside(ui, …). - Bump MSRV to 1.92.
- For egui-0.33 parity with
nih-plug, pin truce to0.47.0.
(0.48.0 and 0.48.1 were yanked, install 0.48.2.)
#0.47.0
truce-egui: Migrate to egui 0.33 (bumpswgputo 27 transitively).truce-gpu: Declare wgpu graphics backends per-OS (fixes Linux).
#0.46.0
truce-iced: Migrate to iced 0.14.truce-egui: Addparam_dropdownwidget (stock click-to-open dropdown wrappingegui::ComboBox).truce-egui:param_knobsnaps to discrete steps on enum / discrete params.- Examples: Tremolo refreshed (compact transport line, fractional rate labels, dropdown polish).
#0.45.4
- LV2: Fix install path on Windows.
- LV2: Fix param defaults (mirror other formats).
- Standalone: Disable window resizing on Linux (mirror other OSes).
- Examples: Fix
fourcccollision between two example plugins.
#0.45.3
cargo-truce: Fix plugin-name to path sanitization mismatch betweenbuild/installandpackage(display names with filesystem-reserved characters likeTruce Dry/Wetproduced a ~15 KB empty installer becauseproductbuild's distribution.xml referenced the raw name while the on-disk.pkgfiles used the sanitized name).
#0.45.2
- AAX: Fix ABI mismatch bug (broken since 0.43.0).
- AAX /
cargo-truce: Add Pro Toolspluginrunnertocargo truce validate(used if present). - LV2: Fix editor positioning quirks (some quirks remain but no showstoppers).
cargo-truce: Update help with--target-cpu.cargo-truce: Thread--target-cpuargs throughinstall,package, andruncommands (with sane defaults).cargo-truce:statusno longer runs auval (too slow for the purpose); usevalidatefor that.- Stylistic sweep.
#0.45.0
- New CI gate exercises every prior release's example crates against the current
truceHEAD, catching backwards-incompatible changes before they ship. - Initial SIMD block operations. New
truce-simdcrate shipswide-backedscale_block/mul_block/mix_block/mac_block/copy_block/zero_blockplustanh_block/db_to_linear_block/linear_to_db_blockmath helpers, with scalar fallbacks. Six new examples (gain-simd,saturate,drywet,gate,widen,surround-meter) demonstrate the shapes. cargo truce buildnow defaults x86_64 builds to-C target-cpu=x86-64-v3(AVX2 + FMA + BMI2) so the SIMD paths above activate without any per-developer config. New--target-cpu <value>flag acceptsbaseline(rustc default = SSE2-only),v2/v3/v4,nativefor the local-CPU dev-loop, or any literal rustc target-cpu name.- Plugin display names that contain filesystem-reserved characters (e.g.
Truce Dry/Wet) are now sanitized at the path-construction boundary, so the on-disk bundle lands atTruce Dry-Wet.aaxpluginwhile DAWs still display the raw name from the plist.
#0.44.0
- VST3 + CLAP on macOS now link as
MH_BUNDLEinstead ofMH_DYLIB. Fixes load under hosts that take the strictCFBundleLoadExecutablepath (DawDreamer's JUCE-based VST3 host is the one we validated against). Most desktop DAWs have more forgiving loaders and weren't affected, but the strict path is the correct Mach-O shape for a bundle. Built from a Ruststaticlibviaclang -bundle. Breaking change for pre-0.44.0 plugins: the plugin crate's[lib]block needscrate-type = ["cdylib", "staticlib", "rlib"](was["cdylib", "rlib"]).cargo truce install/packagefails loudly with the exact one-line edit if the staticlib is missing. - AU v3: Fix installs broken since 0.42.0.
- AU v2: Fix
PresentPresethandler broken since 0.40, auval passes again across all bundled examples. cargo truce package --formats <list>now works on Linux, matching the existing macOS / Windows behavior. Internally drives the underlyingcargo truce buildinvocation.- CI hardening: every
cargo trucesubcommand (install, validate, package, uninstall, doctor, status, reset-au) now runs on macOS, Linux, and Windows on every PR. New scaffold-and-round-trip workflow exercisescargo truce newagainst single-effect, single-instrument, and mixed-workspace shapes. - Doc sweep across the in-tree comments and rustdoc.
#0.43.0
- SysEx + UMP support (work in progress). Initial plumbing for System Exclusive messages and MIDI 2.0 UMPs
#0.42.1
- Params:
IntParamvalue displays no longer pick up theFloatParam{:.1}/{:.2}formatters, so transpose's semitone knob now reads0 stinstead of0.0 st. Internally,ParamInfogained akind: ParamValueKindfield set by#[derive(Params)]from the field type. - Example tidy: the
Mixknobs on both fundsp reverbs and theDepthknob on tremolo now declareunit = "%", so they render as25%/0%instead of0.25/0.00.
#0.42.0
- iOS support. AU v3 plug-ins now build, install, and run on both the iOS Simulator (
cargo truce install --ios) and tethered devices (cargo truce install --ios-device). Truce ships a Swift container app template with embedded editor, Play button, status label, info sheet, and a hamburger-menu landscape layout. New[[plugin]]knobs intruce.toml:ios_icon_set,ios_orientations,ios_scale_editor_to_fit(defaulttrue),ios_minimum_os_version,ios_app_group,ios_url. Touch input is pinned per-finger so multi-touch doesn't hijack an in-progress drag.mute_preview_outputworks on both standalone and the iOS container for analyzer-style plug-ins. Custom container apps and iced's iOS backend remain unsupported (latter blocked upstream). See the new iOS chapter. - iOS screenshot regression:
cargo truce screenshot --ioscaptures the simulator's actual rendered output (the only path that sees iOS-specific compositing);--checkgates baselines in CI.
#0.41.0
- AAX: Fix knobs sync bug on log-ranged parameters. The C++ shim defaulted to a linear taper for every param's normalize / denormalize, so AAX would round-trip a log-ranged knob through
RenderAudiointo a different plain value than the editor wrote. Wirerange_typethroughTruceAaxParamInfoso the shim picks the matchingAAX_ITaperDelegateper param. ABI bump:TRUCE_AAX_ABI_VERSIONto 2. - Standalone: Drop the "(standalone)" suffix from the window title.
- baseview: bump to the latest revision.
- Workspace: README status updated to stable;
repository/homepagemetadata added to every crate's Cargo.toml for crates.io publishing readiness.
#0.40.2
- Move example READMEs out to truce-website (no code impact).
- Wrap VST3 / VST2 / AU / AAX state-save and state-load callbacks in
catch_unwind. A panic from usersave_state/load_stateused to unwind across theextern "C"FFI boundary back into the host UB on most toolchains, abort on others. The save paths now pre-zero the host's out pointers so a panic mid-write leaves the host seeing an empty blob rather than a stale buffer.
#0.40.1
- AU v3: Wire
macos_iconthrough the bundle template. When set intruce.toml, the per-plugin.icnsis copied into the.app'sContents/Resources/andCFBundleIconFileis added to the outer Info.plist, matching the standalone-host behavior.
#0.40.0
- CLAP: Use the macOS bundle layout (
Contents/MacOS+Info.plist). Fixes load in Bitwig (#51). - CLAP: Wire stubs for
get_resize_hints,set_transient,suggest_title,set_size,adjust_sizeso the custom-editor button appears in Bitwig. - fundsp: New
truce-example-fundsp-reverb-workershowing a background-thread graph rebuild with a lock-free swap into the audio threadprocess()stays allocation-free. - fundsp: Rename the inline-rebuild example to
truce-example-fundsp-reverb-simple(pedagogical, rt-unsafe). - Follow stable Rust toolchain (unpin from 1.90).
- Dead-code removal, stylistic fixes.
#0.39.3
- New example integrating with fundsp; added small helpers.
- AU v2: Fix registration bug causing GUI init issues.
- LV2: Fix URI mismatch between manifest and runtime.
#0.39.2
- Consistent naming scheme for package installers across macOS, Windows, and Linux.
#0.39.1
- Standalone on macOS: Fix audio input after install, was missing the audio-input entitlement.
#0.39.0
- LV2: Add packaging support.
- Enable notarization for example plugins.
- Installer: Harden against permission issues from prior installs.
- Wire
macos_icon,windows_icon,welcome_bmp,welcome_htmlfor example plugins. - Installer / packaging bug fixes.
- Bump MSRV to 1.90.
#0.38.0
- LV2: Fix MIDI effect categorization.
- Improved precision ergonomics using fundsp-style preludes.
- Breaking: renamed
param.smoothed_next()toparam.read()to support consistent float precision use. Upgrade path is a mechanical. - Minor fixes.