Skip to main content

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