use base::LimitedAsciiString; use std::fmt; use std::fmt::{Display, Formatter}; #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum PluginId { Vst2 { vst_magic_number: i32 }, Vst3 { vst_uid: [u32; 4] }, Clap { clap_id: LimitedAsciiString<100> }, Js { js_id: LimitedAsciiString<100> }, } impl PluginId { pub fn vst2(vst_magic_number: i32) -> Self { Self::Vst2 { vst_magic_number } } pub fn vst3(vst_uid: [u32; 4]) -> Self { Self::Vst3 { vst_uid } } pub fn clap(id_expression: &str) -> Result { let id = Self::Clap { clap_id: LimitedAsciiString::try_from_str(id_expression)?, }; Ok(id) } pub fn js(id_expression: &str) -> Result { // It's better to normalize to lowercase. When loading the FX via TrackFX_AddByName, // REAPER is case-insensitive anyway and we should be as well when doing comparisons. let lowercase_id_expression = id_expression.to_lowercase(); let id = Self::Js { js_id: LimitedAsciiString::try_from_str(&lowercase_id_expression)?, }; Ok(id) } pub fn parse_from_rxml_line(line: &str) -> Result { let line = line.trim(); let mut tokens = splitty::split_unquoted_whitespace(line).unwrap_quotes(true); let tag_opener = tokens.next().ok_or("missing FX tag opener")?; match tag_opener { " { // Examples: // - "" // - 5ch)" "Pianoteq 8.vst3" 0 "" 1031062328{565354507438717069616E6F74657120} "" // Skip plug-in name, file, zero, custom name for _ in 0..4 { tokens.next(); } // Process ID expression let id_expression = tokens.next().ok_or("missing VST ID expression")?; if let Some((_, remainder)) = id_expression.split_once('{') { // VST3 let vst3_uid_string = remainder.strip_suffix('}').unwrap_or(remainder); let uid = parse_vst3_uid(vst3_uid_string)?; Ok(Self::vst3(uid)) } else if let Some((magic_number_string, _)) = id_expression.split_once('<') { // VST2 let magic_number = parse_vst2_magic_number(magic_number_string)?; Ok(Self::vst2(magic_number)) } else { Err("couldn't process VST ID expression") } } " { // Example: { // Example: Err("unknown FX tag opener"), } } pub fn kind(&self) -> PluginKind { match self { PluginId::Vst2 { .. } => PluginKind::Vst2, PluginId::Vst3 { .. } => PluginKind::Vst3, PluginId::Clap { .. } => PluginKind::Clap, PluginId::Js { .. } => PluginKind::Js, } } pub fn content_formatted_for_reaper(&self) -> String { PluginIdContentInReaperFormat(self).to_string() } } /// Example: `1967946098` for a VST2 plug-in ID. pub struct PluginIdContentInReaperFormat<'a>(pub &'a PluginId); impl Display for PluginIdContentInReaperFormat<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self.0 { PluginId::Clap { clap_id } => clap_id.fmt(f), PluginId::Js { js_id } => js_id.fmt(f), PluginId::Vst2 { vst_magic_number } => vst_magic_number.fmt(f), PluginId::Vst3 { vst_uid } => { // D39D5B69 D6AF42FA 12345678 534D4433 write!( f, "{:X}{:X}{:X}{:X}", vst_uid[0], vst_uid[1], vst_uid[2], vst_uid[3], ) } } } } /// Example: `vst|1967946098` for a VST2 plug-in ID. pub struct PluginIdInPipeFormat<'a>(pub &'a PluginId); impl Display for PluginIdInPipeFormat<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let kind = self.0.kind(); let kind = kind.as_ref(); let content = PluginIdContentInReaperFormat(self.0); write!(f, "{kind}|{content}") } } /// When adding a new variant, the serialization should correspond to the string which is used /// as prefix for the ini file names in "REAPER_RESOURCE_PATH/presets". #[derive(Copy, Clone, Eq, PartialEq, Debug, strum::AsRefStr, strum::EnumString)] pub enum PluginKind { #[strum(serialize = "vst")] Vst2, #[strum(serialize = "vst3")] Vst3, #[strum(serialize = "clap")] Clap, #[strum(serialize = "js")] Js, } impl PluginKind { pub fn name(&self) -> &'static str { match self { Self::Vst2 => "VST", Self::Vst3 => "VST3", Self::Clap => "CLAP", Self::Js => "JS", } } /// Need to put some random string in front of "<" due to bug in REAPER < 6.69, /// otherwise loading by VST2 magic number doesn't work. pub fn reaper_add_by_name_prefix_fix(&self) -> &'static str { match self { Self::Vst2 | Self::Vst3 => "i7zh34z", Self::Clap | Self::Js => "", } } pub fn formatted_for_reaper(&self) -> &'static str { match self { Self::Vst2 => "<", Self::Vst3 => "{", Self::Clap | Self::Js => "", } } } /// "1397572658" => 1397572658 pub fn parse_vst2_magic_number(expression: &str) -> Result { expression .parse() .map_err(|_| "couldn't parse VST2 magic number") } /// "565354507438717069616E6F74657120" => [0x56535450, 0x74387170, 0x69616E6F, 0x74657120] pub fn parse_vst3_uid(expression: &str) -> Result<[u32; 4], &'static str> { fn parse_component(text: &str, i: usize) -> Result { let from = i * 8; let until = from + 8; let parsed = u32::from_str_radix(&text[from..until], 16) .map_err(|_| "couldn't parse VST3 uid component")?; Ok(parsed) } let uid = [ parse_component(expression, 0)?, parse_component(expression, 1)?, parse_component(expression, 2)?, parse_component(expression, 3)?, ]; Ok(uid) } #[cfg(test)] mod tests { use crate::PluginId; #[test] pub fn vst2() { assert_eq!( PluginId::parse_from_rxml_line( r#" """# ), Ok(PluginId::vst2(1397572658)) ); } #[test] pub fn vst3() { assert_eq!( PluginId::parse_from_rxml_line( r#"5ch)" "Pianoteq 8.vst3" 0 "" 1031062328{565354507438717069616E6F74657120} """# ), Ok(PluginId::vst3([ 0x56535450, 0x74387170, 0x69616E6F, 0x74657120 ])) ); } #[test] pub fn clap() { assert_eq!( PluginId::parse_from_rxml_line( r#"