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-features opts back into a minimal build.
  • cargo truce validate --aax runs Avid's full AAX Plug-In Validator when AAX_VALIDATOR points at its dsh, selectable via --aax-tests load|fast|full|required|info (load forces the lightweight pluginrunner check); without AAX_VALIDATOR it falls back to pluginrunner. cargo truce doctor reports 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 VSTPluginMain entry is a Rust trampoline, no longer demoted to a local symbol dlsym can'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-file on 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-file capture 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 --state and the test driver's state_file / state_blob decode the .pluginstate envelope instead of passing it to load_state, so params and persist restore instead of rendering defaults; renamed-plugin migration carries the #[persist] block through MigratedState.
  • AU: host SysEx input no longer races the render thread; blocks past kAudioUnitProperty_MaximumFramesPerSlice fail safe instead of allocating; components report the plugin's real version instead of a hardcoded 1.0.0.
  • iOS / GUI editors: fixed a CGImage use-after-free on resize/close, frozen Slint timers/animations, a fixed-size letterbox gap, an unguarded GPU-editor on_event panic path, and per-open UIView / CADisplayLink leaks (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-file decodes 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:resize callback is panic-firewalled.
  • VST3: editor state edits enqueue onto the audio-thread handoff (no aliasing &mut / TOCTOU against host setState / setActive); setContentScaleFactor and inactive-setState notify use try_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 CHUNKED automation timing, canonical getBusArrangement speaker layouts, and UTF-8 metadata that no longer renders as mojibake.
  • Sample-accurate automation: sub-blocks split by a CHUNKED param 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 flags are 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 screenshot returns a clean error instead of aborting; the Slint XYPad clips its dot and brackets a drag as one automation gesture (pressed / released callbacks) so a drag latches over host automation.

#6.1.9

  • CLAP plugins expose remote-controls pages 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 --target now 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 new now scaffolds stateful plugins as type DspState = Self instead 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_resize is 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.
  • EnumParam clamps 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 via InitContext::snapshot_publisher() instead of copying the buffer every block; snapshot_prealloc_hint pre-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 to BusLayout::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 - return vec![BusLayout::stereo()] for a stereo-only effect, or any set you want. Behavior change: with the new default a mono track hands process a 1-channel buffer, so loop over buffer.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, so auval failed 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-layouts to 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, and AudioBuffer::for_each_frame_io::<IN, OUT> (with a for_each_stereo_frame shorthand for the (2, 2) case) to run a fixed-shape DSP - a reverb_stereo graph, 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), and reversed(<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 BackgroundTask on the task type and set const 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 no try_lock of 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 BackgroundTasks trait (implemented once on the plugin, with type Task and run_task) is replaced by BackgroundTask, implemented on each task type with fn run(self, params) and an optional const 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 (one run_task(task, params) function), add tasks: MyPlugin to truce::plugin!, and schedule from process with ctx.tasks::<T>() - try_spawn for one-shot work, spawn_coalescing when 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 via ctx.tasks::<T>() on the editor context.
  • AudioTap for the streaming shape - a spectrum analyzer, loudness meter, or oscilloscope. process pushes 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 one AudioTap, 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, a StreamWorker for a continuous stream.
  • init now receives an InitContext, so you can kick off a first background build while constructing your DSP state.

#Breaking

Only if your plugin overrides init.

  • init gains an argument: fn init(params: &Params, cx: &InitContext) -> DspState. The new cx is 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 first process. Plugins that don't override init are 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

  • PluginLogic now 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 &Params in process, 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. (Copy values go in a lock-free AtomicCell, strings and vecs behind RwLock / Mutex; per-field keys let old and new sessions load each other.) For a larger or opaque blob, save_state / load_state with #[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.
  • PurePluginLogic for the common case: a stateless effect implements it instead of PluginLogic and writes far less - no DspState, no init, no state argument on process (and init / reset are optional even on PluginLogic, since a Default state and a no-op reset cover most plugins). Everything downstream (truce::plugin!, every format) treats it as an ordinary PluginLogic. cargo truce new scaffolds PluginLogic with a #[derive(Default)] DSP-state struct and the state argument pre-wired by default; pass --pure for the stateless version.
  • Offline-render awareness: process and reset now see a ProcessMode (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 your process one 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 stored params Arc, and take state / params as arguments instead of &self.
  • type DspState must implement Default (that's the default init) - your old fn new(...) body usually becomes the Default impl.
  • reset takes &AudioConfig instead 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) -> Self with a Default impl on the state type; write fn init(params) only if building it reads params. No more stored params Arc.
  • Give each method state / params instead of &self, and reset an &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 install regenerates 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; the TRUCE_RT_PARANOID environment 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 / RwLock flag a lock taken in process, and truce_test::assert_realtime_clean gates a test on allocations, frees, and locks together.

#3.1.0

  • New rt-paranoid feature: a development-time checker that flags any allocation your DSP makes on the audio thread inside process, with truce_test::assert_no_audio_alloc helpers to gate it in tests. Off and zero-cost by default.
  • cargo truce new wires rt-paranoid into fresh plugins (the feature plus enable_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

  • editor is now an associated function. It receives Arc<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. Moving editor also shifted the hot-reload ABI, so a --shell shell 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

  1. Update each editor. Add type Params, take params instead of &self, and use it in the body. For a typical plugin (editor only reads self.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 -> params swap covers the other backends: .into_editor(&params), EguiEditor::new(params, ...), IcedEditor::new(params, ...), and so on.

  2. 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 in new(), and read it back in editor:

     #[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)
         }
  3. Rebuild --shell pairs 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 of save_state to 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 (buf arrives cleared with its capacity kept, so just refill it) - it's a win only when a save_state stall would otherwise be long. Overriding it is the entire opt-in: which method you implement is the choice, the default save_state delegates to it, and plugins that override neither (or stick with save_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.truce prefix).
  • Plugin and vendor names with special characters (&, {, <, quotes, $) no longer break installer generation on Windows, macOS, or Linux.
  • macOS --user installers that include AAX, AU v3, or the standalone app now install those components instead of silently skipping them.
  • cargo truce preset import / pull --category no 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_samples is 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 PAspect constraint, so the window manager keeps the drag on ratio the way Windows (WM_SIZING) and macOS (setContentAspectRatio:) already did. Affects backends whose set_size takes 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

  • Event gained a port field - the MIDI port an event arrived on / should go out on (0 for single-port plugins). Migration: build events with Event::new(offset, body) (port 0, the common case) or Event::on_port(offset, port, body); a struct literal must now spell port out. 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, set bundle_id to the old name-derived slug. bundle_id's shape is validated at compile time (lowercase a-z0-9 plus -/_/. separators, alphanumeric at both ends, no separator runs).
  • EventList::sort is removed. It was a std stable sort, which allocates - unusable on the audio thread where wrappers sort merged input streams. Migration: call ensure_sorted_by_offset(), the allocation-free equivalent with the same stable-by-offset semantics.

#Migrating from 1.x

  1. Pin your identity first. 2.0 derives clap_id / vst3_id / the state hash from bundle_id, not the display name. If a 1.x build ever shipped, set bundle_id to 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.
  2. Fix Event construction. Struct literals need the new port field; prefer Event::new(offset, body) or Event::on_port(offset, port, body). Reads are unaffected.
  3. Replace EventList::sort() with ensure_sorted_by_offset().
  4. 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.
  5. 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.
  6. MIDI stays opt-in. Single-port MIDI 1.0 plugins behave exactly as on 1.x; add midi2 / midi_input_ports / midi_output_ports only when you want the new capabilities.

#MIDI 2.0 / UMP

  • Opt in per direction in truce.toml: midi2 = true sets both; midi2_input / midi2_output override 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 audioUnitMIDIProtocol from the input dialect; output rides the host's MIDIEventList block for every dialect, protocol-pure in the host's hostMIDIProtocol, with the byte block as fallback (macOS 12+ / iOS 15+).
  • VST3 carries per-note control as note expression both ways: INoteExpressionController is declared, host noteIds resolve back to (channel, note), values cross at full f64 precision, and unmapped per-note CCs degrade to channel CCs. Channel-level MIDI input rides hidden per-port IMidiMapping proxy 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..=4 linear-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 by event.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 contradicts midi_input / midi_output is a compile error.
  • The standalone maps one MIDI device per port: --midi-input is repeatable, and macOS / Windows grow a per-port device menu (sixteen-port menu cap; the CLI has none).
  • truce-example-multiport is the reference; host routing quirks (REAPER's VST3-only bus mapping) are in the midi guide.
  • midi_output_ports declares output ports the same way; treat multi-port output as wire-level plumbing for now.

#Legacy state migration

  • New migrate_state hook on PluginLogic: 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_migration are the reference.

#Native f64 processing

  • prelude64 plugins 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.
  • EventBody and TransportInfo derive PartialEq (assert on them directly in tests), and truce_core::midi exposes upconvert_to_midi2 plus 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_state like 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 reset so the plugin never sees blocks bigger than it prepared for.
  • cargo truce run builds only the standalone (no format wrappers); a new --features flag re-adds extras.
  • cargo truce validate no 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 - so min_size / max_size / aspect_ratio were 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 a WM_GETMINMAXINFO / WM_SIZING subclass that also covers programmatic resizes, macOS via contentMinSize / contentMaxSize / contentAspectRatio (which also bound the zoom button) plus a per-frame snap-back for the programmatic setFrame: calls that bypass them - previously only backends whose set_size clamps 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:resize request 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-truce 0.1.1-truce.12 dependency, all GUI backends.

#1.0.4

  • cargo truce build / install / package accept --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-truce 0.1.1-truce.11 dependency.

#1.0.2

  • Fixed iOS AU v3 packaging bugs for App Store submission. cargo truce package --ios corrects code signing, framework embedding, entitlements, and the required Info.plist keys App Store Connect upload checks.
  • Fixed TransportInfo.position_samples stuck at 0 during playback. The VST3 shim read the host's projectTimeSamples (an int64) as a double, 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_factor and 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_redraw keeps 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 in view()) showing it late, only when a stray UI event forced a repaint. A plugin can now return true from needs_redraw() while it has new data, so the editor repaints and drains promptly and idles again when there's nothing new. truce-example-midi-inspector uses it so live MIDI scrolls in without lag.
  • truce-example-envelope emits sample-accurate CC. The envelope follower pushed one control-change per block at sample_offset: 0 carrying 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 @0 bursts 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-iced no longer depends on the iced umbrella crate. It uses iced's sub-crates directly (iced_core / iced_widget / iced_renderer / iced_wgpu / iced_runtime / iced_futures), re-exported as truce_iced::iced, which drops the transitive iced_winit dependency whose desktop-only keyboard API broke the iOS build. Migration: plugins that imported types from the iced crate import the same ones from truce_iced::iced instead.
  • Removed the deprecated selector / param_selector widget. The selector() layout builder and each backend's param_selector (egui / iced / vizia / slint), deprecated since 0.56, are gone along with the underlying Selector widget. Migration: use dropdown() / param_dropdown - the same single-choice control, click-to-open.
  • Removed the deprecated FloatParam::read_block. The const-N block read, deprecated since 0.53, is gone. Migration: use read_into(&mut scratch[..n]), which advances the smoother by exactly the number of samples consumed.

#Other changes

  • iced editors run on iOS. truce-iced renders through a CAMetalLayer-backed UIView under AU v3 - touch input plus a soft keyboard for focused text_input - so iced plugins build and run on iOS like the egui backend. gain-iced, gui-zoo-iced, and midi-inspector are now iOS examples.
  • AU v3 factory presets ship on iOS. cargo truce install --ios / package --ios now bundle a plugin's presets/ library into the embedded framework's Presets/, so AU v3 hosts list them on iOS like macOS. iOS frameworks are shallow bundles, so the presets sit in a flat Presets/ directory rather than Resources/Presets/ (a Resources/ 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 the MIDI Thru param) and demonstrates streaming structured realtime data from process() 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 the Arc<Params> both sides already hold (e.g. a lock-free queue of audio-thread events). The derive Default-initializes it in new() 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_state reports failure to the host instead of unwinding across the extern "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-truce 0.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 (over IMidiMapping); that parameter change is now also delivered as the corresponding EventBody (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-synth now 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) - via audioMasterAutomate, the VST3 output parameter queue, and AUEventListenerNotify respectively. (#147, VST2 by @Boscop)

#0.63.3

  • cargo truce --shell finds [profile.shell] at the Cargo workspace root - the preflight no longer rejects plugin crates that are members of a larger workspace. (#148)
  • truce-vizia no longer risks messaging a freed parent NSView on editor teardown - the per-frame macOS re-anchor stops once the editor closes.
  • vizia is 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-slint translates native key events into Slint WindowEvent key events, so LineEdit / TextInput and FocusScope work in plugin editors (host focus permitting).
  • The slint gui-zoo example demos native Slint widgets - button, checkbox, spinbox, slider, progress bar, image - plus a keyboard section, alongside the truce param widgets.
  • The vizia gui-zoo example 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-iced now forwards native key events into the iced widget tree (a focused text_input or a custom key-capture Widget reading physical_key / logical key), and IcedPlugin::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-zoo examples (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}.app that 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 nested Params struct'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(). A Params struct that mixes its own params with #[nested] groups now default-initializes the nested fields in new(). (#137, by @jedStevens)

#0.60.1

  • #[derive(Params)] no longer trips a rust-analyzer error. Parameters with a range (or numeric default) emitted suffixed literals like 60f64, 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_output capability 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 = true now registers as an aumf MusicEffect, so AU hosts route MIDI to it instead of an aufx.
  • VST2 MIDI output fixed on 64-bit. The outbound VstEvents block 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 truce now reads cargo's target_directory via cargo metadata instead of assuming <plugin>/target, fixing installs when the plugin crate is a member of a larger workspace (artifacts land in the workspace-root target/). (#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_ENDPOINT for 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 package now 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's max_size and 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 underlying Editor::can_maximize hook 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 opaque state:state blob, 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's xsd:base64Binary literal 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 .vstpreset files 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's COM_COMPATIBLE FUID::toString ordering, 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 time to 0.3.47. 0.3.48 was yanked from crates.io.

#0.58.0

  • Presets. Drop a presets/ directory of .preset TOML files next to a plugin crate (params keyed by your Params struct's field names, resolved through the derive sidecars; numeric ids also accepted) and cargo truce install ships 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 (.vstpreset in the OS preset locations), LV2 (pset:Preset TTL). Every container wraps the same state envelope as session recall, which also powers the new cargo truce preset CLI: convert between any two formats, pull to turn presets saved in your DAW into library files (uuid-stable updates, param-name comments), import / export for pack zips, and list / init. Underneath, truce_core::presets::PresetStore manages factory / user / pack scopes with uuid identity, so renames never break host references and user copies override factory ones. [plugin.presets] user_dir optionally replaces the truce/<vendor>/<plugin> part of the user-scope path.
  • cargo truce install --au2 --debug installed 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 of wgpu::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_size regardless 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, NSView close) previously propagated across the AU FFI boundary as an unhandled Obj-C exception and aborted the host on quit. The AU v2 destroy / gui_close callbacks 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 GridLayout editor was panicking inside render, 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-form set_size is a known gap.
  • New Editor trait methods. size_increment() -> Option<(u32, u32)> (interactive-resize granularity in logical points; the X11 standalone maps it onto PResizeInc so window-edge drags snap), aspect_ratio() -> Option<(u32, u32)> ((num, denom); CLAP / VST3 / AU v3 / standalone honor it, VST2 / LV2 / AAX silently ignore), and prefers_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-iced widgets: meter and xy_pad accept .width(Length), .height(Length), .fill(). Lets the widget stretch with the parent column / row instead of using a fixed pixel size.
  • truce-vizia widgets: level_meter height and xy_pad w / h now take impl Into<Units>. Pass Pixels(n) for a fixed size, Stretch(1.0) to fill the parent layout. Breaking for callers passing f32 directly: wrap in Pixels(_).
  • truce-slint widgets: Meter and XYPad use preferred-* + min-* / max-* sizing. A parent VerticalLayout / HorizontalLayout can stretch them; the floor sizes keep the widgets usable at the editor's min_size.
  • baseview-truce 0.1.1-truce.8. Adds the macOS setFrameSize: Resized event + 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 incompatible dxcompiler.dll (Pro Tools does), DxcCreateInstance returns E_NOINTERFACE and 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). The truce-gpu, truce-gui, truce-egui, and truce-slint editor instances now request Dx12Compiler::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 to Backends::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_descriptor query - instead of running from a .CRT$XCU library-load static initializer. On Windows the AAX shim LoadLibraryAs the cdylib from inside GetEffectDescriptions during scanning, so the old initializer ran plugin construction (via has_editor_static's default create().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_knob layout 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_selector widgets deprecated. Marked #[deprecated(since = "0.56.0")] across truce-gui-types, truce-egui, truce-iced, and truce-vizia; the slint Selector element gets the same notice in its widget library header (slint markup has no attribute equivalent). Use the corresponding dropdown / param_dropdown / Dropdown instead.

#0.55.0

  • AU v3: sample-accurate parameter automation. The Swift shim now decodes AURenderEvent.parameter / .parameterRamp into per-sample ParamChange events with the proper within-block offset, and the chunker subdivides the audio block at each automation point. AU v2 stays block-rate (its AudioUnitSetParameter API carries no sample-offset).
  • LV2: sample-accurate parameter automation. The TTL now advertises each parameter as a patch:writable lv2:Parameter, and the wrapper decodes host-emitted patch:Set Objects from the input atom sequence into per-sample ParamChange events (the atom event's time_frames becomes the within-block sample_offset). The legacy lv2:ControlPort path stays so older LV2 hosts still update params at block rate.

#0.54.0

  • New vst3_subcategory truce.toml key. 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 exactly out.len(). Same one-atomic-pair amortization as read_block, runtime length.
  • Deprecated FloatParam::read_block::<N>(). Always advanced by N regardless of consumed samples, silently stepping the smoothed value at the next block boundary whenever the host's block size wasn't a multiple of N. 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::math64 module. f64 mirror of truce_simd::math (db_to_linear_block, linear_to_db_block, exp2_block, log2_block, tanh_block). wide::f64x4 lanes.
  • eq example uses SIMD math64. Output stage dB → linear runs through math64::db_to_linear_block instead of scalar f64::powf per 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_offset instead of the start of the block; smoothers start ramping at the event sample. On by default. Tune via [automation] min_subblock_samples in truce.toml or opt out per-param with #[param(chunk = false)].
  • truce-iced: with_font(bytes) matches egui / vizia. Family name is now read from the TTF (was with_font(family, bytes)).
  • cargo truce install / package: dedupes duplicate archive members during macOS bundle link. Fixes the clang -bundle duplicate-symbol failure plugins with skia-bindings (vizia) could hit.

#0.49.23

  • keyboard-types 0.6 -> 0.7 workspace-wide.

#0.49.22

  • New example: truce-example-dasp-bitcrusher. Showcases dasp_sample::Sample bit-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 the param_dropdown alias added in 0.49.21.

#0.49.21

  • truce-egui: int sliders now snap and show plain integers. param_slider adapts to ParamRange::Discrete (plain min..=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_dropdown alias added for param_selector, matching the egui / CPU APIs.
  • truce-slint: new Dropdown widget (popup-style, wraps the std-widgets ComboBox). Available via import { Dropdown } from "@truce".
  • truce-slint: pinned std-widgets style to fluent on every host OS. The default picked Cupertino on macOS, whose ComboBox rendered its chevron region with a persistent accent-blue square.
  • truce-slint: Toggle height bumped 40 → 50 to align labels with Selector / Dropdown in mixed rows.
  • New examples: truce-example-gui-zoo-egui / -iced / -slint. Each mirrors the CPU gui-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 / EnumParam slider, 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_normalized now 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 stays true across a close-then-open inside one click; dropdown_close and open_dropdown now flag the dirty bit explicitly.
  • Layout: labeled sections now start strictly below the previous section's tallest widget. Section breaks advanced cursor_row by 1, which packed the next section alongside a tall (rows = N) widget from the prior section. Now advances past max_occupied_row.
  • New deg unit. #[param(unit = "deg")] (or "°") prints e.g. 180.0°. ParamUnit::Degrees slots 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, every ParamUnit variant, and the discrete-snap path. Lives in examples/ and is wired into desktop + iOS screenshot CI.

#0.49.19

  • #[param(default = std::f64::consts::*)] no longer trips clippy::approx_constant in the macro expansion. The 0.49.18 parser resolved the path to its f64 value but quote! re-emitted it as a literal. The derive now embeds the original path tokens verbatim while keeping the resolved f64 for the compile-time range / shape checks.

#0.49.18

  • #[param(default = ...)] now accepts std::f64::consts::*. Also core::f64::consts::* and bare f64::consts::*. Lets plugins write default = std::f64::consts::SQRT_2 instead 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_load without a matching -dead_strip, so every staticlib object survived; AU/AAX cdylib links got -dead_strip for free via rustc. Added -dead_strip to the clang -bundle step.

#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 (omni or 1-16) on Linux.

#0.49.13

  • VST2/AAX: extended the 0.49.9 set_state fix. The same path that dropped GUI-edited custom state in CLAP/VST3/AU was present in VST2 and AAX; they now route editor bytes to load_state like the others. (LV2 is unaffected: its UI is out-of-process and never touches set_state directly.)

#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 editor set_state paths silently dropped GUI edits; they now reach load_state correctly.

#0.49.6

  • truce-standalone: the gui feature no longer pulls truce-gpu. Standalone builds compile the GPU backend only when the plugin opts in via truce-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_icon through to the window's WM_SETICON so the title bar and taskbar show the app icon.

#0.49.0

  • Breaking (PluginLogic): the GUI surface collapses to a single editor() method. The old layout(), custom_editor(), render(), uses_custom_render(), and hit_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-cpu crate, a peer of truce-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 with truce-gui = { version = "0.49", features = ["gpu"] }; the gpu path doesn't pull the CPU dependency tree, and vice-versa. Plain truce-gui = "0.49" keeps the CPU default with no change.
  • The truce umbrella no longer pulls truce-gui transitively. Plugins using the built-in renderer declare truce-gui explicitly — 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(&params) 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 in cargo truce package output with the same [ OK ] / [FAIL] ASCII tags used by cargo 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_OVERRIDE when 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 --ios packaging 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 to target/dist/<crate>-<version>-ios.ipa next to the macOS .pkg / Windows .exe / Linux .tar.gz.

#0.48.8

  • truce now fully published to crates.io. cargo truce new scaffolds with a truce = { version = "0.48" } registry pin by default; the historical git = "...", tag = "v0.48.7" form stays available via cargo truce new --github for scaffolding against an unreleased checkout.
  • New truce-aax-bridge crate carries the C ABI header so cargo-truce doesn't transitively pull the full truce-aax runtime 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.json before 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 (bumps wgpu to 29 transitively).
  • Breaking (truce-egui): EditorUi::ui and the EguiEditor::new closure now receive &mut egui::Ui instead of &egui::Context egui 0.34 deprecates Context::run plus the per-panel show(ctx, …) entry points in favor of Context::run_ui and show_inside(ui, …).
  • Bump MSRV to 1.92.
  • For egui-0.33 parity with nih-plug, pin truce to 0.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 (bumps wgpu to 27 transitively).
  • truce-gpu: Declare wgpu graphics backends per-OS (fixes Linux).

#0.46.0

  • truce-iced: Migrate to iced 0.14.
  • truce-egui: Add param_dropdown widget (stock click-to-open dropdown wrapping egui::ComboBox).
  • truce-egui: param_knob snaps 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 fourcc collision between two example plugins.

#0.45.3

  • cargo-truce: Fix plugin-name to path sanitization mismatch between build / install and package (display names with filesystem-reserved characters like Truce Dry/Wet produced a ~15 KB empty installer because productbuild's distribution.xml referenced the raw name while the on-disk .pkg files used the sanitized name).

#0.45.2

  • AAX: Fix ABI mismatch bug (broken since 0.43.0).
  • AAX / cargo-truce: Add Pro Tools pluginrunner to cargo 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-cpu args through install, package, and run commands (with sane defaults).
  • cargo-truce: status no longer runs auval (too slow for the purpose); use validate for that.
  • Stylistic sweep.

#0.45.0

  • New CI gate exercises every prior release's example crates against the current truce HEAD, catching backwards-incompatible changes before they ship.
  • Initial SIMD block operations. New truce-simd crate ships wide-backed scale_block / mul_block / mix_block / mac_block / copy_block / zero_block plus tanh_block / db_to_linear_block / linear_to_db_block math helpers, with scalar fallbacks. Six new examples (gain-simd, saturate, drywet, gate, widen, surround-meter) demonstrate the shapes.
  • cargo truce build now 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 accepts baseline (rustc default = SSE2-only), v2 / v3 / v4, native for 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 at Truce Dry-Wet.aaxplugin while DAWs still display the raw name from the plist.

#0.44.0

  • VST3 + CLAP on macOS now link as MH_BUNDLE instead of MH_DYLIB. Fixes load under hosts that take the strict CFBundleLoadExecutable path (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 Rust staticlib via clang -bundle. Breaking change for pre-0.44.0 plugins: the plugin crate's [lib] block needs crate-type = ["cdylib", "staticlib", "rlib"] (was ["cdylib", "rlib"]). cargo truce install / package fails loudly with the exact one-line edit if the staticlib is missing.
  • AU v3: Fix installs broken since 0.42.0.
  • AU v2: Fix PresentPreset handler 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 underlying cargo truce build invocation.
  • CI hardening: every cargo truce subcommand (install, validate, package, uninstall, doctor, status, reset-au) now runs on macOS, Linux, and Windows on every PR. New scaffold-and-round-trip workflow exercises cargo truce new against 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: IntParam value displays no longer pick up the FloatParam {:.1} / {:.2} formatters, so transpose's semitone knob now reads 0 st instead of 0.0 st. Internally, ParamInfo gained a kind: ParamValueKind field set by #[derive(Params)] from the field type.
  • Example tidy: the Mix knobs on both fundsp reverbs and the Depth knob on tremolo now declare unit = "%", so they render as 25% / 0% instead of 0.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 in truce.toml: ios_icon_set, ios_orientations, ios_scale_editor_to_fit (default true), 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_output works 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 --ios captures the simulator's actual rendered output (the only path that sees iOS-specific compositing); --check gates 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 RenderAudio into a different plain value than the editor wrote. Wire range_type through TruceAaxParamInfo so the shim picks the matching AAX_ITaperDelegate per param. ABI bump: TRUCE_AAX_ABI_VERSION to 2.
  • Standalone: Drop the "(standalone)" suffix from the window title.
  • baseview: bump to the latest revision.
  • Workspace: README status updated to stable; repository / homepage metadata 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 user save_state / load_state used to unwind across the extern "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_icon through the bundle template. When set in truce.toml, the per-plugin .icns is copied into the .app's Contents/Resources/ and CFBundleIconFile is 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_size so the custom-editor button appears in Bitwig.
  • fundsp: New truce-example-fundsp-reverb-worker showing a background-thread graph rebuild with a lock-free swap into the audio thread process() 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_html for 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() to param.read() to support consistent float precision use. Upgrade path is a mechanical.
  • Minor fixes.