truce_core/info.rs
1/// Wire dialect a MIDI port speaks. `Midi1` is the default and covers
2/// every format today; `Midi2` opts a port into MIDI 2.0 / UMP so the
3/// plugin receives the native 16/32-bit + per-note + group-addressed
4/// variants of [`crate::events::EventBody`] instead of the MIDI 1.0
5/// down-conversion. Only formats with a UMP transport (CLAP's MIDI2
6/// note dialect) honor `Midi2`; others clamp to MIDI 1.0.
7#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
8pub enum MidiDialect {
9 #[default]
10 Midi1,
11 Midi2,
12}
13
14/// Static metadata about a plugin.
15#[derive(Clone, Debug)]
16pub struct PluginInfo {
17 pub name: &'static str,
18 pub vendor: &'static str,
19 pub url: &'static str,
20 pub version: &'static str,
21 pub category: PluginCategory,
22
23 /// Whether the host should route MIDI / note events *into* this
24 /// plugin. Defaults to `true` for instruments and note effects;
25 /// `truce.toml`'s `midi_input` overrides the derived value (e.g.
26 /// an audio effect that reacts to CC). Every format wrapper gates
27 /// its MIDI input port / bus / capability on this one flag.
28 pub accepts_midi_in: bool,
29
30 /// Whether this plugin emits MIDI / note events *to* the host.
31 /// Defaults to `true` for note effects only; `truce.toml`'s
32 /// `midi_output` overrides the derived value (e.g. an instrument
33 /// or effect that also emits MIDI). Every format wrapper gates its
34 /// MIDI output port / bus / capability on this one flag, so the
35 /// host actually reads what `process()` pushes to `output_events`.
36 pub emits_midi: bool,
37
38 /// Dialect the (single) MIDI input port speaks. Defaults to
39 /// [`MidiDialect::Midi1`]; a plugin opts into MIDI 2.0 with the
40 /// `midi2` key in `truce.toml`. Only honored by formats with a UMP
41 /// transport (CLAP); others deliver MIDI 1.0 regardless.
42 pub midi_input_dialect: MidiDialect,
43
44 /// Dialect the (single) MIDI output port speaks. See
45 /// [`Self::midi_input_dialect`].
46 pub midi_output_dialect: MidiDialect,
47
48 /// Number of MIDI input ports the plugin exposes. `0` when it
49 /// accepts no MIDI, `1` for the ordinary single-port case, `>1` for
50 /// a multi-port plugin (e.g. a merger). Derived from the MIDI-input
51 /// capability by default; `midi_input_ports` in `truce.toml` raises
52 /// it. Formats without a multi-port MIDI transport clamp to `1` and
53 /// route everything to [`crate::Event::port`] `0`. Always `>= 1` when
54 /// [`Self::accepts_midi_in`] is set, and `0` otherwise.
55 pub midi_input_ports: u8,
56
57 /// Number of MIDI output ports the plugin exposes. See
58 /// [`Self::midi_input_ports`]; mirrors [`Self::emits_midi`].
59 pub midi_output_ports: u8,
60
61 /// Short identifier (`bundle_id` in `truce.toml`). Used to derive
62 /// the LV2 plugin URI (`{vendor.url}/lv2/{bundle_id}`); also a
63 /// stable, vendor-agnostic key for "this plugin" that doesn't
64 /// drift with display-name changes the way `clap_id` does.
65 pub bundle_id: &'static str,
66
67 // Format-specific IDs
68 pub vst3_id: &'static str,
69 pub clap_id: &'static str,
70 pub fourcc: [u8; 4],
71 pub au_type: [u8; 4],
72 pub au_manufacturer: [u8; 4],
73 pub aax_id: Option<&'static str>,
74 /// AAX plugin category string (e.g. "EQ", "Dynamics", "Reverb").
75 /// Maps to `AAX_ePlugInCategory` constants.
76 pub aax_category: Option<&'static str>,
77 /// VST3 "Plugin Type Categories" secondary token. The wrapper
78 /// emits this after the primary token (`Fx|<sub>`,
79 /// `Instrument|<sub>`) so hosts like Cubase route to the right
80 /// submenu. Values from the VST3 SDK list: `Delay`, `Distortion`,
81 /// `Dynamics`, `EQ`, `Filter`, `Mastering`, `Modulation`,
82 /// `Pitch Shift`, `Restoration`, `Reverb`, `Analyzer`, `Tools`,
83 /// `Surround`. Optional; when `None` only the primary token is
84 /// emitted and Cubase will fall back to "Other".
85 pub vst3_subcategory: Option<&'static str>,
86
87 /// Per-format display-name overrides, populated by
88 /// `truce::plugin_info!()` from the matching `truce.toml` keys.
89 /// Format wrappers fall back to `name` when the override is `None`.
90 /// Baked at compile time so back-to-back plugin builds with
91 /// different overrides don't invalidate the format wrapper's
92 /// build fingerprint.
93 ///
94 /// `au3_name` is exposed for parity with the other formats and
95 /// for user introspection, but `truce-au`'s `resolved_plugin_name`
96 /// reads `au_name` for both v2 and v3 builds - the v3 host's
97 /// displayed label comes from the appex `Info.plist`'s `AUNAME`
98 /// (which `cargo truce install --au3` populates from `au3_name`),
99 /// not from `g_descriptor->name`.
100 /// `[plugin.presets]` `user_dir` from `truce.toml`: replaces the
101 /// `truce/<vendor>/<plugin>` subpath of the user-scope preset
102 /// root. `truce_utils::presets::user_preset_root` documents
103 /// where the path resolves on each OS. Effectively permanent
104 /// once a plugin ships - changing it orphans previously saved
105 /// user presets and packs.
106 pub preset_user_dir: Option<&'static str>,
107
108 pub vst3_name: Option<&'static str>,
109 pub clap_name: Option<&'static str>,
110 pub vst2_name: Option<&'static str>,
111 pub au_name: Option<&'static str>,
112 pub au3_name: Option<&'static str>,
113 pub aax_name: Option<&'static str>,
114 pub lv2_name: Option<&'static str>,
115
116 /// Standalone-only. Format wrappers MUST NOT read this - it
117 /// exists for preview hosts (truce-standalone, the iOS `AUv3`
118 /// container app) that need a TOML-driven way to mute the
119 /// plug-in's audio output while keeping `process()` ticking, so
120 /// editors that visualise an input signal (analyzers, tuners,
121 /// spectrum displays) update from mic / file input without
122 /// closing a mic → speakers feedback loop. Set from
123 /// `mute_preview_output` in `truce.toml`. Real DAW hosts own
124 /// their own output graph; consulting this flag from a wrapper
125 /// would let plug-in authors silence the DAW's mix bus, which
126 /// is never what they want.
127 #[doc(hidden)]
128 pub mute_preview_output: bool,
129
130 /// Sample-accurate automation chunking tunables. Read by the
131 /// `chunked_process::process_chunked` helper that every format
132 /// wrapper routes `process()` through. Populated by
133 /// `truce::plugin_info!()` from `truce.toml`'s `[automation]`
134 /// table; defaults to [`AutomationConfig::DEFAULT`] when the
135 /// table is absent.
136 pub automation: AutomationConfig,
137
138 /// AU `ClassInfo` dictionary keys a pre-truce build stored its
139 /// state under. Probed by `truce-au` when truce's own data key is
140 /// absent; a hit feeds the plugin's `migrate_state` hook. From
141 /// `truce.toml`'s `[plugin.legacy_state]` `au_keys`; empty when
142 /// undeclared (no probing).
143 pub legacy_au_keys: &'static [&'static str],
144 /// LV2 state property URIs a pre-truce build stored its state
145 /// under. See [`Self::legacy_au_keys`].
146 pub legacy_lv2_uris: &'static [&'static str],
147 /// AAX chunk fourccs a pre-truce build stored its state under.
148 /// See [`Self::legacy_au_keys`].
149 pub legacy_aax_chunk_ids: &'static [&'static str],
150}
151
152/// Sample-accurate chunking tunables baked into [`PluginInfo`] at
153/// compile time. Mirrors `truce_build::AutomationConfig` (the
154/// derive-time view of the same TOML key) but lives in `truce-core`
155/// so wrappers can read it without a `truce-build` dep.
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub struct AutomationConfig {
158 /// Smallest sub-block size in samples. The chunker only splits
159 /// the audio block at split-eligible events whose `sample_offset`
160 /// is at least `block_start + min_subblock_samples` past the
161 /// current sub-block start; closer events are coalesced. Default
162 /// 32 (set via [`AutomationConfig::DEFAULT`]).
163 pub min_subblock_samples: u32,
164}
165
166impl AutomationConfig {
167 /// Default used when `truce.toml` omits the `[automation]` table.
168 pub const DEFAULT: Self = Self {
169 min_subblock_samples: 32,
170 };
171}
172
173impl Default for AutomationConfig {
174 fn default() -> Self {
175 Self::DEFAULT
176 }
177}
178
179/// Resolve a format's display-name override. Each wrapper picks its
180/// own `<format>_name` field off `PluginInfo` and passes the result
181/// here along with the `PluginInfo::name` fallback. Empty overrides
182/// (unset or set to `""`) fall through to `fallback`.
183#[must_use]
184pub fn resolve_name_override(
185 override_value: Option<&'static str>,
186 fallback: &'static str,
187) -> &'static str {
188 match override_value {
189 Some(s) if !s.is_empty() => s,
190 _ => fallback,
191 }
192}
193
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub enum PluginCategory {
196 Effect,
197 Instrument,
198 /// MIDI note effect (e.g., transpose, arpeggiator). Processes MIDI events.
199 NoteEffect,
200 Analyzer,
201 Tool,
202}
203
204/// Convert a category string to [`PluginCategory`] at compile time.
205/// Used by the `plugin_info!()` macro.
206#[must_use]
207pub const fn category_from_str(s: &str) -> PluginCategory {
208 match s.as_bytes() {
209 b"Instrument" => PluginCategory::Instrument,
210 b"NoteEffect" => PluginCategory::NoteEffect,
211 b"Analyzer" => PluginCategory::Analyzer,
212 b"Tool" => PluginCategory::Tool,
213 _ => PluginCategory::Effect,
214 }
215}
216
217/// Helper to convert a 4-char string literal to `[u8; 4]` at compile time.
218/// Panics if the string is not exactly 4 ASCII bytes.
219///
220/// # Panics
221///
222/// Panics at compile time when used in a `const` context (preferred)
223/// or at runtime if `s.len() != 4`. ASCII-ness isn't checked here -
224/// callers that need it should validate separately.
225#[must_use]
226pub const fn fourcc(s: &[u8]) -> [u8; 4] {
227 assert!(s.len() == 4, "FourCC must be exactly 4 bytes");
228 [s[0], s[1], s[2], s[3]]
229}