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}