#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FourCharCode(pub [u8; 4]);
impl FourCharCode {
pub const fn new(bytes: &[u8; 4]) -> Self {
debug_assert!(bytes[0].is_ascii(), "FourCC bytes must be ASCII");
debug_assert!(bytes[1].is_ascii(), "FourCC bytes must be ASCII");
debug_assert!(bytes[2].is_ascii(), "FourCC bytes must be ASCII");
debug_assert!(bytes[3].is_ascii(), "FourCC bytes must be ASCII");
Self(*bytes)
}
pub const fn as_u32(&self) -> u32 {
u32::from_be_bytes(self.0)
}
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.0).unwrap_or("????")
}
pub const fn as_bytes(&self) -> &[u8; 4] {
&self.0
}
}
impl std::fmt::Display for FourCharCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[macro_export]
macro_rules! fourcc {
($s:literal) => {{
const BYTES: &[u8] = $s;
const _: () = assert!(BYTES.len() == 4, "FourCC must be exactly 4 bytes");
const _: () = assert!(BYTES[0].is_ascii(), "FourCC byte 0 must be ASCII");
const _: () = assert!(BYTES[1].is_ascii(), "FourCC byte 1 must be ASCII");
const _: () = assert!(BYTES[2].is_ascii(), "FourCC byte 2 must be ASCII");
const _: () = assert!(BYTES[3].is_ascii(), "FourCC byte 3 must be ASCII");
$crate::config::FourCharCode::new(&[BYTES[0], BYTES[1], BYTES[2], BYTES[3]])
}};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Subcategory {
Analyzer,
Bass,
ChannelStrip,
Delay,
Distortion,
Drums,
Dynamics,
Eq,
Filter,
Generator,
Guitar,
Mastering,
Microphone,
Modulation,
Network,
PitchShift,
Restoration,
Reverb,
Spatial,
Surround,
Tools,
Vocals,
Drum,
External,
Piano,
Sampler,
Synth,
Mono,
Stereo,
Ambisonics,
UpDownMix,
OnlyRealTime,
OnlyOfflineProcess,
NoOfflineProcess,
}
impl Subcategory {
pub const fn to_vst3(&self) -> &'static str {
match self {
Subcategory::Analyzer => "Analyzer",
Subcategory::Bass => "Bass",
Subcategory::ChannelStrip => "Channel Strip",
Subcategory::Delay => "Delay",
Subcategory::Distortion => "Distortion",
Subcategory::Drums => "Drums",
Subcategory::Dynamics => "Dynamics",
Subcategory::Eq => "EQ",
Subcategory::Filter => "Filter",
Subcategory::Generator => "Generator",
Subcategory::Guitar => "Guitar",
Subcategory::Mastering => "Mastering",
Subcategory::Microphone => "Microphone",
Subcategory::Modulation => "Modulation",
Subcategory::Network => "Network",
Subcategory::PitchShift => "Pitch Shift",
Subcategory::Restoration => "Restoration",
Subcategory::Reverb => "Reverb",
Subcategory::Spatial => "Spatial",
Subcategory::Surround => "Surround",
Subcategory::Tools => "Tools",
Subcategory::Vocals => "Vocals",
Subcategory::Drum => "Drum",
Subcategory::External => "External",
Subcategory::Piano => "Piano",
Subcategory::Sampler => "Sampler",
Subcategory::Synth => "Synth",
Subcategory::Mono => "Mono",
Subcategory::Stereo => "Stereo",
Subcategory::Ambisonics => "Ambisonics",
Subcategory::UpDownMix => "Up-Downmix",
Subcategory::OnlyRealTime => "OnlyRT",
Subcategory::OnlyOfflineProcess => "OnlyOfflineProcess",
Subcategory::NoOfflineProcess => "NoOfflineProcess",
}
}
pub const fn to_au_tag(&self) -> Option<&'static str> {
match self {
Subcategory::Analyzer => Some("Analyzer"),
Subcategory::Delay => Some("Delay"),
Subcategory::Distortion => Some("Distortion"),
Subcategory::Dynamics => Some("Dynamics"),
Subcategory::Eq => Some("EQ"),
Subcategory::Filter => Some("Filter"),
Subcategory::Mastering => Some("Mastering"),
Subcategory::Modulation => Some("Modulation"),
Subcategory::PitchShift => Some("Pitch Shift"),
Subcategory::Restoration => Some("Restoration"),
Subcategory::Reverb => Some("Reverb"),
Subcategory::Drum => Some("Drums"),
Subcategory::Sampler => Some("Sampler"),
Subcategory::Synth => Some("Synth"),
Subcategory::Piano => Some("Piano"),
Subcategory::Generator => Some("Generator"),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
Effect,
Instrument,
MidiEffect,
Generator,
}
impl Category {
pub const fn to_au_component_type(&self) -> u32 {
match self {
Category::Effect => u32::from_be_bytes(*b"aufx"),
Category::Instrument => u32::from_be_bytes(*b"aumu"),
Category::MidiEffect => u32::from_be_bytes(*b"aumi"),
Category::Generator => u32::from_be_bytes(*b"augn"),
}
}
pub const fn to_vst3_category(&self) -> &'static str {
match self {
Category::Effect | Category::MidiEffect => "Fx",
Category::Instrument => "Instrument",
Category::Generator => "Generator",
}
}
pub const fn accepts_midi(&self) -> bool {
matches!(self, Category::Instrument | Category::MidiEffect)
}
pub const fn produces_midi(&self) -> bool {
matches!(self, Category::Instrument | Category::MidiEffect)
}
}
pub const DEFAULT_SYSEX_SLOTS: usize = 16;
pub const DEFAULT_SYSEX_BUFFER_SIZE: usize = 512;
#[derive(Debug, Clone)]
pub struct Config {
pub name: &'static str,
pub category: Category,
pub vendor: &'static str,
pub url: &'static str,
pub email: &'static str,
pub version: &'static str,
pub has_editor: bool,
pub subcategories: &'static [Subcategory],
pub manufacturer: FourCharCode,
pub subtype: FourCharCode,
pub vst3_id: Option<[u32; 4]>,
pub vst3_controller_id: Option<[u32; 4]>,
pub sysex_slots: usize,
pub sysex_buffer_size: usize,
}
const fn str_to_four_bytes(s: &str) -> [u8; 4] {
let bytes = s.as_bytes();
assert!(bytes.len() == 4, "FourCC string must be exactly 4 bytes");
[bytes[0], bytes[1], bytes[2], bytes[3]]
}
const BEAMER_VST3_NAMESPACE: &[u8; 15] = b"beamer-vst3-uid";
const fn derive_vst3_uid(namespace: &[u8], manufacturer: &[u8; 4], subtype: &[u8; 4]) -> [u32; 4] {
let ns_len = namespace.len();
let total_len = ns_len + 8;
let mut data = [0u8; 24]; let mut i = 0;
while i < ns_len {
data[i] = namespace[i];
i += 1;
}
data[ns_len] = manufacturer[0];
data[ns_len + 1] = manufacturer[1];
data[ns_len + 2] = manufacturer[2];
data[ns_len + 3] = manufacturer[3];
data[ns_len + 4] = subtype[0];
data[ns_len + 5] = subtype[1];
data[ns_len + 6] = subtype[2];
data[ns_len + 7] = subtype[3];
let hash = fnv1a_128_len(&data, total_len);
[
(hash >> 96) as u32,
(hash >> 64) as u32,
(hash >> 32) as u32,
hash as u32,
]
}
const fn fnv1a_128_len(data: &[u8], len: usize) -> u128 {
const OFFSET: u128 = 0x6c62272e07bb0142_62b821756295c58d;
const PRIME: u128 = 0x0000000001000000_000000000000013B;
let mut hash = OFFSET;
let mut i = 0;
while i < len {
hash ^= data[i] as u128;
hash = hash.wrapping_mul(PRIME);
i += 1;
}
hash
}
const fn hex_to_u8(c: u8) -> u8 {
match c {
b'0'..=b'9' => c - b'0',
b'A'..=b'F' => c - b'A' + 10,
b'a'..=b'f' => c - b'a' + 10,
_ => panic!("Invalid hex character in UUID"),
}
}
const fn parse_uuid_u32(bytes: &[u8], start: usize) -> u32 {
let mut result: u32 = 0;
let mut i = 0;
let mut hex_count = 0;
while hex_count < 8 {
let c = bytes[start + i];
if c != b'-' {
result = (result << 4) | (hex_to_u8(c) as u32);
hex_count += 1;
}
i += 1;
}
result
}
const fn parse_uuid(uuid: &str) -> [u32; 4] {
let bytes = uuid.as_bytes();
assert!(
bytes.len() == 36,
"vst3_id must be a UUID in format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
);
[
parse_uuid_u32(bytes, 0),
parse_uuid_u32(bytes, 9),
parse_uuid_u32(bytes, 19),
parse_uuid_u32(bytes, 28),
]
}
impl Config {
pub const fn new(name: &'static str, category: Category, manufacturer_code: &str, plugin_code: &str) -> Self {
Self {
name,
category,
vendor: "Unknown Vendor",
url: "",
email: "",
version: "1.0.0",
has_editor: false,
subcategories: &[],
manufacturer: FourCharCode::new(&str_to_four_bytes(manufacturer_code)),
subtype: FourCharCode::new(&str_to_four_bytes(plugin_code)),
vst3_id: None,
vst3_controller_id: None,
sysex_slots: DEFAULT_SYSEX_SLOTS,
sysex_buffer_size: DEFAULT_SYSEX_BUFFER_SIZE,
}
}
#[doc(hidden)]
pub const fn with_vendor(mut self, vendor: &'static str) -> Self {
self.vendor = vendor;
self
}
#[doc(hidden)]
pub const fn with_url(mut self, url: &'static str) -> Self {
self.url = url;
self
}
#[doc(hidden)]
pub const fn with_email(mut self, email: &'static str) -> Self {
self.email = email;
self
}
#[doc(hidden)]
pub const fn with_version(mut self, version: &'static str) -> Self {
self.version = version;
self
}
#[doc(hidden)]
pub const fn with_editor(mut self) -> Self {
self.has_editor = true;
self
}
#[doc(hidden)]
pub const fn with_subcategories(mut self, subcategories: &'static [Subcategory]) -> Self {
self.subcategories = subcategories;
self
}
#[doc(hidden)]
pub const fn with_vst3_id(mut self, uuid: &'static str) -> Self {
self.vst3_id = Some(parse_uuid(uuid));
self
}
#[doc(hidden)]
pub const fn with_vst3_controller_id(mut self, uuid: &'static str) -> Self {
self.vst3_controller_id = Some(parse_uuid(uuid));
self
}
#[doc(hidden)]
pub const fn with_sysex_slots(mut self, slots: usize) -> Self {
self.sysex_slots = slots;
self
}
#[doc(hidden)]
pub const fn with_sysex_buffer_size(mut self, size: usize) -> Self {
self.sysex_buffer_size = size;
self
}
pub const fn vst3_uid_parts(&self) -> [u32; 4] {
match self.vst3_id {
Some(parts) => parts,
None => derive_vst3_uid(
BEAMER_VST3_NAMESPACE.as_slice(),
self.manufacturer.as_bytes(),
self.subtype.as_bytes(),
),
}
}
pub const fn vst3_controller_uid_parts(&self) -> Option<[u32; 4]> {
self.vst3_controller_id
}
pub const fn manufacturer_u32(&self) -> u32 {
self.manufacturer.as_u32()
}
pub const fn subtype_u32(&self) -> u32 {
self.subtype.as_u32()
}
pub fn vst3_subcategories(&self) -> String {
let mut result = String::from(self.category.to_vst3_category());
for sub in self.subcategories {
result.push('|');
result.push_str(sub.to_vst3());
}
result
}
pub fn au_tags(&self) -> Vec<&'static str> {
self.subcategories
.iter()
.filter_map(|sub| sub.to_au_tag())
.collect()
}
}