Skip to main content

beamer_core/
config.rs

1//! Plugin configuration.
2//!
3//! This module provides unified plugin configuration that covers shared
4//! metadata and format-specific identifiers (AU four-char codes, VST3 UIDs).
5//!
6//! # Example
7//!
8//! ```ignore
9//! use beamer_core::{Config, config::Category};
10//!
11//! pub static CONFIG: Config = Config::new("My Plugin", Category::Effect, "Mfgr", "plgn")
12//!     .with_vendor("My Company")
13//!     .with_version("1.0.0");
14//! ```
15
16// =========================================================================
17// FourCharCode
18// =========================================================================
19
20/// Four-character code (FourCC) for AU identifiers.
21///
22/// Used for manufacturer codes and subtype codes in AU registration.
23/// Must be exactly 4 ASCII characters.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct FourCharCode(pub [u8; 4]);
26
27impl FourCharCode {
28    /// Create a new FourCharCode from a 4-byte array.
29    ///
30    /// # Panics
31    /// Debug builds will panic if any byte is not ASCII.
32    pub const fn new(bytes: &[u8; 4]) -> Self {
33        debug_assert!(bytes[0].is_ascii(), "FourCC bytes must be ASCII");
34        debug_assert!(bytes[1].is_ascii(), "FourCC bytes must be ASCII");
35        debug_assert!(bytes[2].is_ascii(), "FourCC bytes must be ASCII");
36        debug_assert!(bytes[3].is_ascii(), "FourCC bytes must be ASCII");
37        Self(*bytes)
38    }
39
40    /// Get the FourCC as a 32-bit value (big-endian).
41    pub const fn as_u32(&self) -> u32 {
42        u32::from_be_bytes(self.0)
43    }
44
45    /// Get the FourCC as a string slice.
46    pub fn as_str(&self) -> &str {
47        std::str::from_utf8(&self.0).unwrap_or("????")
48    }
49
50    /// Get the raw bytes.
51    pub const fn as_bytes(&self) -> &[u8; 4] {
52        &self.0
53    }
54}
55
56impl std::fmt::Display for FourCharCode {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{}", self.as_str())
59    }
60}
61
62/// Macro for creating FourCharCode at compile time with validation.
63///
64/// # Example
65///
66/// ```ignore
67/// use beamer_core::fourcc;
68///
69/// const MANUFACTURER: FourCharCode = fourcc!(b"Demo");
70/// const SUBTYPE: FourCharCode = fourcc!(b"gain");
71/// ```
72#[macro_export]
73macro_rules! fourcc {
74    ($s:literal) => {{
75        const BYTES: &[u8] = $s;
76        const _: () = assert!(BYTES.len() == 4, "FourCC must be exactly 4 bytes");
77        const _: () = assert!(BYTES[0].is_ascii(), "FourCC byte 0 must be ASCII");
78        const _: () = assert!(BYTES[1].is_ascii(), "FourCC byte 1 must be ASCII");
79        const _: () = assert!(BYTES[2].is_ascii(), "FourCC byte 2 must be ASCII");
80        const _: () = assert!(BYTES[3].is_ascii(), "FourCC byte 3 must be ASCII");
81        $crate::config::FourCharCode::new(&[BYTES[0], BYTES[1], BYTES[2], BYTES[3]])
82    }};
83}
84
85// =========================================================================
86// Subcategory
87// =========================================================================
88
89/// Plugin subcategory for more specific classification.
90///
91/// These map directly to VST3 subcategories and AU tags.
92/// Use with `Config::with_subcategories()` to specify plugin characteristics.
93///
94/// # Example
95///
96/// ```ignore
97/// pub static CONFIG: Config = Config::new("My Compressor", Category::Effect)
98///     .with_subcategories(&[Subcategory::Dynamics]);
99/// ```
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum Subcategory {
102    // === Effect Subcategories ===
103    /// Scope, FFT-Display, Loudness Processing
104    Analyzer,
105    /// Tools dedicated to Bass Guitar
106    Bass,
107    /// Channel strip tools
108    ChannelStrip,
109    /// Delay, Multi-tap Delay, Ping-Pong Delay
110    Delay,
111    /// Amp Simulator, Sub-Harmonic, SoftClipper
112    Distortion,
113    /// Tools dedicated to Drums
114    Drums,
115    /// Compressor, Expander, Gate, Limiter, Maximizer
116    Dynamics,
117    /// Equalization, Graphical EQ
118    Eq,
119    /// WahWah, ToneBooster, Specific Filter
120    Filter,
121    /// Tone Generator, Noise Generator
122    Generator,
123    /// Tools dedicated to Guitar
124    Guitar,
125    /// Dither, Noise Shaping
126    Mastering,
127    /// Tools dedicated to Microphone
128    Microphone,
129    /// Phaser, Flanger, Chorus, Tremolo, Vibrato, AutoPan
130    Modulation,
131    /// Network-based effects
132    Network,
133    /// Pitch Processing, Pitch Correction, Vocal Tuning
134    PitchShift,
135    /// Denoiser, Declicker
136    Restoration,
137    /// Reverberation, Room Simulation, Convolution Reverb
138    Reverb,
139    /// MonoToStereo, StereoEnhancer
140    Spatial,
141    /// LFE Splitter, Bass Manager
142    Surround,
143    /// Volume, Mixer, Tuner
144    Tools,
145    /// Tools dedicated to Vocals
146    Vocals,
147
148    // === Instrument Subcategories ===
149    /// Instrument for Drum sounds
150    Drum,
151    /// External wrapped hardware
152    External,
153    /// Instrument for Piano sounds
154    Piano,
155    /// Instrument based on Samples
156    Sampler,
157    /// Instrument based on Synthesis
158    Synth,
159
160    // === Channel Configuration ===
161    /// Mono only plug-in
162    Mono,
163    /// Stereo only plug-in
164    Stereo,
165    /// Ambisonics channel
166    Ambisonics,
167    /// Mixconverter, Up-Mixer, Down-Mixer
168    UpDownMix,
169
170    // === Processing Constraints ===
171    /// Supports only realtime processing
172    OnlyRealTime,
173    /// Offline processing only
174    OnlyOfflineProcess,
175    /// Works as normal insert plug-in only (no offline)
176    NoOfflineProcess,
177}
178
179impl Subcategory {
180    /// Get the VST3 subcategory string.
181    pub const fn to_vst3(&self) -> &'static str {
182        match self {
183            // Effect subcategories
184            Subcategory::Analyzer => "Analyzer",
185            Subcategory::Bass => "Bass",
186            Subcategory::ChannelStrip => "Channel Strip",
187            Subcategory::Delay => "Delay",
188            Subcategory::Distortion => "Distortion",
189            Subcategory::Drums => "Drums",
190            Subcategory::Dynamics => "Dynamics",
191            Subcategory::Eq => "EQ",
192            Subcategory::Filter => "Filter",
193            Subcategory::Generator => "Generator",
194            Subcategory::Guitar => "Guitar",
195            Subcategory::Mastering => "Mastering",
196            Subcategory::Microphone => "Microphone",
197            Subcategory::Modulation => "Modulation",
198            Subcategory::Network => "Network",
199            Subcategory::PitchShift => "Pitch Shift",
200            Subcategory::Restoration => "Restoration",
201            Subcategory::Reverb => "Reverb",
202            Subcategory::Spatial => "Spatial",
203            Subcategory::Surround => "Surround",
204            Subcategory::Tools => "Tools",
205            Subcategory::Vocals => "Vocals",
206            // Instrument subcategories
207            Subcategory::Drum => "Drum",
208            Subcategory::External => "External",
209            Subcategory::Piano => "Piano",
210            Subcategory::Sampler => "Sampler",
211            Subcategory::Synth => "Synth",
212            // Channel configuration
213            Subcategory::Mono => "Mono",
214            Subcategory::Stereo => "Stereo",
215            Subcategory::Ambisonics => "Ambisonics",
216            Subcategory::UpDownMix => "Up-Downmix",
217            // Processing constraints
218            Subcategory::OnlyRealTime => "OnlyRT",
219            Subcategory::OnlyOfflineProcess => "OnlyOfflineProcess",
220            Subcategory::NoOfflineProcess => "NoOfflineProcess",
221        }
222    }
223
224    /// Get the AU tag string.
225    ///
226    /// AU tags are simpler and don't have all VST3 distinctions.
227    /// Returns `None` for subcategories that don't map to AU tags.
228    pub const fn to_au_tag(&self) -> Option<&'static str> {
229        match self {
230            Subcategory::Analyzer => Some("Analyzer"),
231            Subcategory::Delay => Some("Delay"),
232            Subcategory::Distortion => Some("Distortion"),
233            Subcategory::Dynamics => Some("Dynamics"),
234            Subcategory::Eq => Some("EQ"),
235            Subcategory::Filter => Some("Filter"),
236            Subcategory::Mastering => Some("Mastering"),
237            Subcategory::Modulation => Some("Modulation"),
238            Subcategory::PitchShift => Some("Pitch Shift"),
239            Subcategory::Restoration => Some("Restoration"),
240            Subcategory::Reverb => Some("Reverb"),
241            Subcategory::Drum => Some("Drums"),
242            Subcategory::Sampler => Some("Sampler"),
243            Subcategory::Synth => Some("Synth"),
244            Subcategory::Piano => Some("Piano"),
245            Subcategory::Generator => Some("Generator"),
246            // These don't have direct AU tag equivalents
247            _ => None,
248        }
249    }
250}
251
252/// Plugin type - determines how hosts categorize and use the plugin.
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum Category {
255    /// Audio effect (EQ, compressor, reverb, delay)
256    Effect,
257    /// Virtual instrument (synth, sampler, drum machine)
258    Instrument,
259    /// MIDI processor (arpeggiator, chord generator)
260    MidiEffect,
261    /// Audio generator (test tones, noise, file player)
262    Generator,
263}
264
265impl Category {
266    /// Convert to AU component type code (FourCC as u32, big-endian)
267    pub const fn to_au_component_type(&self) -> u32 {
268        match self {
269            Category::Effect => u32::from_be_bytes(*b"aufx"),
270            Category::Instrument => u32::from_be_bytes(*b"aumu"),
271            Category::MidiEffect => u32::from_be_bytes(*b"aumi"),
272            Category::Generator => u32::from_be_bytes(*b"augn"),
273        }
274    }
275
276    /// Convert to VST3 base category string
277    pub const fn to_vst3_category(&self) -> &'static str {
278        match self {
279            Category::Effect | Category::MidiEffect => "Fx",
280            Category::Instrument => "Instrument",
281            Category::Generator => "Generator",
282        }
283    }
284
285    /// Check if this type accepts MIDI input
286    pub const fn accepts_midi(&self) -> bool {
287        matches!(self, Category::Instrument | Category::MidiEffect)
288    }
289
290    /// Check if this type can produce MIDI output
291    pub const fn produces_midi(&self) -> bool {
292        matches!(self, Category::Instrument | Category::MidiEffect)
293    }
294}
295
296/// Default number of SysEx output slots per process block.
297pub const DEFAULT_SYSEX_SLOTS: usize = 16;
298
299/// Default SysEx buffer size in bytes per slot.
300pub const DEFAULT_SYSEX_BUFFER_SIZE: usize = 512;
301
302/// Unified plugin configuration.
303///
304/// Contains all plugin metadata: shared fields (name, vendor, category),
305/// plugin identity (AU four-char codes), and format-specific settings
306/// (VST3 component UIDs). The VST3 component UID is derived automatically
307/// from the AU codes via FNV-1a hash unless explicitly overridden.
308///
309/// # Example
310///
311/// ```ignore
312/// use beamer_core::{Config, config::Category};
313///
314/// pub static CONFIG: Config = Config::new("My Plugin", Category::Effect, "Mfgr", "plgn")
315///     .with_vendor("My Company")
316///     .with_version(env!("CARGO_PKG_VERSION"));
317/// ```
318#[derive(Debug, Clone)]
319pub struct Config {
320    /// Plugin name displayed in the DAW.
321    pub name: &'static str,
322
323    /// Plugin category (effect, instrument, etc.)
324    pub category: Category,
325
326    /// Vendor/company name.
327    pub vendor: &'static str,
328
329    /// Vendor URL.
330    pub url: &'static str,
331
332    /// Vendor email.
333    pub email: &'static str,
334
335    /// Plugin version string.
336    pub version: &'static str,
337
338    /// Whether this plugin has an editor/GUI.
339    pub has_editor: bool,
340
341    /// Plugin subcategories for more specific classification.
342    pub subcategories: &'static [Subcategory],
343
344    /// Manufacturer code (4-character identifier for your company/brand).
345    pub manufacturer: FourCharCode,
346
347    /// Subtype code (4-character identifier for this specific plugin).
348    pub subtype: FourCharCode,
349
350    /// Explicit VST3 component UID override. When `None`, the UID is
351    /// derived from the manufacturer and subtype codes via FNV-1a hash.
352    pub vst3_id: Option<[u32; 4]>,
353
354    /// Explicit VST3 controller UID. When `None`, the plugin uses the
355    /// combined component pattern (processor + controller in one object).
356    pub vst3_controller_id: Option<[u32; 4]>,
357
358    /// Number of SysEx output slots per process block (AU and VST3).
359    pub sysex_slots: usize,
360
361    /// Maximum size of each SysEx message in bytes (AU and VST3).
362    pub sysex_buffer_size: usize,
363}
364
365/// Helper to convert a string literal to a 4-byte array at compile time.
366const fn str_to_four_bytes(s: &str) -> [u8; 4] {
367    let bytes = s.as_bytes();
368    assert!(bytes.len() == 4, "FourCC string must be exactly 4 bytes");
369    [bytes[0], bytes[1], bytes[2], bytes[3]]
370}
371
372// =========================================================================
373// VST3 UUID derivation (FNV-1a 128-bit)
374// =========================================================================
375
376/// Beamer namespace salt for VST3 UID derivation.
377const BEAMER_VST3_NAMESPACE: &[u8; 15] = b"beamer-vst3-uid";
378
379/// Derive VST3 UID parts from a namespace and FourCC codes.
380const fn derive_vst3_uid(namespace: &[u8], manufacturer: &[u8; 4], subtype: &[u8; 4]) -> [u32; 4] {
381    // Build input: namespace + manufacturer + subtype
382    // Use a fixed-size buffer large enough for any namespace we use (max 16 bytes + 8)
383    let ns_len = namespace.len();
384    let total_len = ns_len + 8;
385    let mut data = [0u8; 24]; // max size
386    let mut i = 0;
387    while i < ns_len {
388        data[i] = namespace[i];
389        i += 1;
390    }
391    data[ns_len] = manufacturer[0];
392    data[ns_len + 1] = manufacturer[1];
393    data[ns_len + 2] = manufacturer[2];
394    data[ns_len + 3] = manufacturer[3];
395    data[ns_len + 4] = subtype[0];
396    data[ns_len + 5] = subtype[1];
397    data[ns_len + 6] = subtype[2];
398    data[ns_len + 7] = subtype[3];
399
400    // Hash only the relevant bytes
401    let hash = fnv1a_128_len(&data, total_len);
402    [
403        (hash >> 96) as u32,
404        (hash >> 64) as u32,
405        (hash >> 32) as u32,
406        hash as u32,
407    ]
408}
409
410/// FNV-1a 128-bit hash with explicit length (for fixed-size buffer usage in const fn).
411const fn fnv1a_128_len(data: &[u8], len: usize) -> u128 {
412    const OFFSET: u128 = 0x6c62272e07bb0142_62b821756295c58d;
413    const PRIME: u128 = 0x0000000001000000_000000000000013B;
414    let mut hash = OFFSET;
415    let mut i = 0;
416    while i < len {
417        hash ^= data[i] as u128;
418        hash = hash.wrapping_mul(PRIME);
419        i += 1;
420    }
421    hash
422}
423
424// =========================================================================
425// UUID string parsing (compile-time)
426// =========================================================================
427
428/// Parse a hex character to its numeric value.
429const fn hex_to_u8(c: u8) -> u8 {
430    match c {
431        b'0'..=b'9' => c - b'0',
432        b'A'..=b'F' => c - b'A' + 10,
433        b'a'..=b'f' => c - b'a' + 10,
434        _ => panic!("Invalid hex character in UUID"),
435    }
436}
437
438/// Parse 8 hex digits (skipping dashes) into a u32.
439const fn parse_uuid_u32(bytes: &[u8], start: usize) -> u32 {
440    let mut result: u32 = 0;
441    let mut i = 0;
442    let mut hex_count = 0;
443    while hex_count < 8 {
444        let c = bytes[start + i];
445        if c != b'-' {
446            result = (result << 4) | (hex_to_u8(c) as u32);
447            hex_count += 1;
448        }
449        i += 1;
450    }
451    result
452}
453
454/// Parse a UUID string ("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") into [u32; 4].
455const fn parse_uuid(uuid: &str) -> [u32; 4] {
456    let bytes = uuid.as_bytes();
457    assert!(
458        bytes.len() == 36,
459        "vst3_id must be a UUID in format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
460    );
461    [
462        parse_uuid_u32(bytes, 0),
463        parse_uuid_u32(bytes, 9),
464        parse_uuid_u32(bytes, 19),
465        parse_uuid_u32(bytes, 28),
466    ]
467}
468
469impl Config {
470    /// Create a new plugin configuration.
471    ///
472    /// # Arguments
473    ///
474    /// * `name` - Plugin name displayed in the DAW
475    /// * `category` - Plugin category (effect, instrument, etc.)
476    /// * `manufacturer_code` - 4-character manufacturer code (e.g., "Bmer")
477    /// * `plugin_code` - 4-character plugin code (e.g., "gain")
478    ///
479    /// # Panics
480    /// Panics at compile time if manufacturer_code or plugin_code are not exactly 4 ASCII characters.
481    ///
482    /// # Example
483    ///
484    /// ```ignore
485    /// pub static CONFIG: Config = Config::new("My Plugin", Category::Effect, "Mfgr", "plgn")
486    ///     .with_vendor("My Company")
487    ///     .with_version(env!("CARGO_PKG_VERSION"));
488    /// ```
489    pub const fn new(name: &'static str, category: Category, manufacturer_code: &str, plugin_code: &str) -> Self {
490        Self {
491            name,
492            category,
493            vendor: "Unknown Vendor",
494            url: "",
495            email: "",
496            version: "1.0.0",
497            has_editor: false,
498            subcategories: &[],
499            manufacturer: FourCharCode::new(&str_to_four_bytes(manufacturer_code)),
500            subtype: FourCharCode::new(&str_to_four_bytes(plugin_code)),
501            vst3_id: None,
502            vst3_controller_id: None,
503            sysex_slots: DEFAULT_SYSEX_SLOTS,
504            sysex_buffer_size: DEFAULT_SYSEX_BUFFER_SIZE,
505        }
506    }
507
508    /// Set the vendor name.
509    #[doc(hidden)]
510    pub const fn with_vendor(mut self, vendor: &'static str) -> Self {
511        self.vendor = vendor;
512        self
513    }
514
515    /// Set the vendor URL.
516    #[doc(hidden)]
517    pub const fn with_url(mut self, url: &'static str) -> Self {
518        self.url = url;
519        self
520    }
521
522    /// Set the vendor email.
523    #[doc(hidden)]
524    pub const fn with_email(mut self, email: &'static str) -> Self {
525        self.email = email;
526        self
527    }
528
529    /// Set the version string.
530    #[doc(hidden)]
531    pub const fn with_version(mut self, version: &'static str) -> Self {
532        self.version = version;
533        self
534    }
535
536    /// Enable the editor/GUI.
537    #[doc(hidden)]
538    pub const fn with_editor(mut self) -> Self {
539        self.has_editor = true;
540        self
541    }
542
543    /// Set the plugin subcategories.
544    ///
545    /// Subcategories provide more specific classification beyond the main category.
546    /// They are used for VST3 subcategory strings and AU tags.
547    #[doc(hidden)]
548    pub const fn with_subcategories(mut self, subcategories: &'static [Subcategory]) -> Self {
549        self.subcategories = subcategories;
550        self
551    }
552
553    /// Override the auto-derived VST3 component UID with an explicit UUID.
554    ///
555    /// By default, the VST3 UID is derived from the manufacturer and subtype
556    /// codes. Use this only when you need a specific UUID (e.g., matching an
557    /// existing shipped plugin).
558    ///
559    /// # Arguments
560    ///
561    /// * `uuid` - UUID string in format "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
562    #[doc(hidden)]
563    pub const fn with_vst3_id(mut self, uuid: &'static str) -> Self {
564        self.vst3_id = Some(parse_uuid(uuid));
565        self
566    }
567
568    /// Set an explicit VST3 controller UID to enable split component/controller mode.
569    ///
570    /// By default, plugins use the combined component pattern (processor and
571    /// controller in one object). Use this for split architecture.
572    ///
573    /// # Arguments
574    ///
575    /// * `uuid` - UUID string in format "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
576    #[doc(hidden)]
577    pub const fn with_vst3_controller_id(mut self, uuid: &'static str) -> Self {
578        self.vst3_controller_id = Some(parse_uuid(uuid));
579        self
580    }
581
582    /// Set the number of SysEx output slots per process block (AU and VST3).
583    ///
584    /// Higher values allow more concurrent SysEx messages but use more memory.
585    /// Default is 16 slots.
586    #[doc(hidden)]
587    pub const fn with_sysex_slots(mut self, slots: usize) -> Self {
588        self.sysex_slots = slots;
589        self
590    }
591
592    /// Set the maximum size of each SysEx message in bytes (AU and VST3).
593    ///
594    /// Messages larger than this will be truncated. Default is 512 bytes.
595    #[doc(hidden)]
596    pub const fn with_sysex_buffer_size(mut self, size: usize) -> Self {
597        self.sysex_buffer_size = size;
598        self
599    }
600
601    /// Get VST3 component UID as [u32; 4].
602    ///
603    /// Returns the explicit override if set via `with_vst3_id()`, otherwise
604    /// derives a UID from the manufacturer and subtype codes via FNV-1a hash.
605    pub const fn vst3_uid_parts(&self) -> [u32; 4] {
606        match self.vst3_id {
607            Some(parts) => parts,
608            None => derive_vst3_uid(
609                BEAMER_VST3_NAMESPACE.as_slice(),
610                self.manufacturer.as_bytes(),
611                self.subtype.as_bytes(),
612            ),
613        }
614    }
615
616    /// Get VST3 controller UID as [u32; 4], if split component/controller mode is enabled.
617    pub const fn vst3_controller_uid_parts(&self) -> Option<[u32; 4]> {
618        self.vst3_controller_id
619    }
620
621    /// Get the manufacturer code as a u32.
622    pub const fn manufacturer_u32(&self) -> u32 {
623        self.manufacturer.as_u32()
624    }
625
626    /// Get the subtype code as a u32.
627    pub const fn subtype_u32(&self) -> u32 {
628        self.subtype.as_u32()
629    }
630
631    /// Build the VST3 subcategories string.
632    ///
633    /// Combines the main category with subcategories using pipe separators.
634    /// For example: `Category::Effect` with `[Subcategory::Dynamics]` becomes `"Fx|Dynamics"`.
635    pub fn vst3_subcategories(&self) -> String {
636        let mut result = String::from(self.category.to_vst3_category());
637        for sub in self.subcategories {
638            result.push('|');
639            result.push_str(sub.to_vst3());
640        }
641        result
642    }
643
644    /// Get AU tags derived from subcategories.
645    ///
646    /// Returns tags for subcategories that have AU equivalents.
647    /// Subcategories without AU mappings are skipped.
648    pub fn au_tags(&self) -> Vec<&'static str> {
649        self.subcategories
650            .iter()
651            .filter_map(|sub| sub.to_au_tag())
652            .collect()
653    }
654}