use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use crate::core::daw::device::{Device, DeviceChain, ModDelayMode, ReverbMode};
use crate::core::daw::dsp;
use crate::core::fixed::fixed::Q15;
use crate::core::module::Module;
use crate::tracker::mix_plugin::{MixPlugin, MixPlugins};
const DXMO: u32 = 0x4458_4D4F;
const VSTP: u32 = 0x5673_7450;
const ID_COMPRESSOR: u32 = 0xEF01_1F79;
const ID_DISTORTION: u32 = 0xEF11_4C90;
const ID_ECHO: u32 = 0xEF3E_932C;
const ID_CHORUS: u32 = 0xEFE6_629C;
const ID_FLANGER: u32 = 0xEFCA_3D92;
const ID_PARAMEQ: u32 = 0x120C_ED89;
const ID_WAVESREVERB: u32 = 0x87FC_0268;
const ID_I3DL2REVERB: u32 = 0xEF98_5E71;
const ID_GARGLE: u32 = 0xDAFD_8210;
const VST_JS_COMPRESSOR: u32 = 0x4761_696E; const VST_FREEVERB: u32 = 0x4A7A_5232;
const DMO_PARAM_BASE: usize = 4;
const IR_BYPASS: u8 = 0x02;
const IR_APPLY_TO_MASTER: u8 = 0x01;
#[inline]
fn clampf(x: f32, lo: f32, hi: f32) -> f32 {
if x < lo {
lo
} else if x > hi {
hi
} else {
x
}
}
const Q15_FULL_SCALE_F32: f32 = 32_767.0;
#[inline]
fn q15(x: f32) -> Q15 {
Q15::from_raw((clampf(x, 0.0, 1.0) * Q15_FULL_SCALE_F32) as i16)
}
#[inline]
fn q15s(x: f32) -> Q15 {
Q15::from_raw((clampf(x, -1.0, 1.0) * Q15_FULL_SCALE_F32) as i16)
}
fn read_dmo_floats(data: &[u8], n: usize) -> Option<[f32; 13]> {
if data.len() < 4 {
return None;
}
if u32::from_le_bytes(data[0..4].try_into().ok()?) != 0 {
return None;
}
if data.len() < DMO_PARAM_BASE + 4 * n {
return None;
}
let mut out = [0f32; 13];
for (i, slot) in out.iter_mut().enumerate().take(n) {
let o = DMO_PARAM_BASE + 4 * i;
*slot = f32::from_le_bytes(data[o..o + 4].try_into().ok()?);
}
Some(out)
}
fn dmo_compressor(p: &[f32; 13]) -> Device {
let makeup_mdb = ((-60.0 + p[0] * 120.0) * 1000.0) as i32;
let attack_us = ((0.01 + p[1] * 499.99) * 1000.0) as u32;
let release_us = ((50.0 + p[2] * 2950.0) * 1000.0) as u32;
let thr_mdb = ((-60.0 + p[3] * 60.0) * 1000.0) as i32;
let ratio_q8_8 = ((1.0 + p[4] * 99.0) * 256.0) as u16;
Device::compressor(thr_mdb, ratio_q8_8, attack_us, release_us, makeup_mdb)
}
fn dmo_distortion(p: &[f32; 13]) -> Device {
let gain_mdb = ((-60.0 + p[0] * 60.0) * 1000.0) as i32;
let pre_lp = (100.0 + p[2] * 7900.0) as u16;
let post_c = (100.0 + p[3] * 7900.0) as u16;
let post_bw = (100.0 + p[4] * 7900.0) as u16;
Device::distortion(gain_mdb, q15(p[1]), post_c, post_bw, pre_lp)
}
fn dmo_echo(p: &[f32; 13]) -> Device {
Device::echo(
q15(p[0]),
q15(p[1]),
(1.0 + p[2] * 1999.0) as u16,
(1.0 + p[3] * 1999.0) as u16,
p[4] > 0.5,
)
}
fn dmo_chorus(p: &[f32; 13]) -> Device {
Device::mod_delay(
ModDelayMode::Chorus,
q15(p[0]),
q15(p[1]),
q15s((-99.0 + p[5] * 198.0) / 100.0),
(p[2] * 10.0 * 1000.0) as u32,
p[3] >= 0.5,
(p[6] * 20.0 * 256.0) as u16,
p[4] != 0.5,
)
}
fn dmo_flanger(p: &[f32; 13]) -> Device {
Device::mod_delay(
ModDelayMode::Flanger,
q15(p[0]),
q15(p[3]),
q15s((-99.0 + p[5] * 198.0) / 100.0),
(p[2] * 10.0 * 1000.0) as u32,
p[1] >= 0.5,
(p[6] * 4.0 * 256.0) as u16,
p[4] != 0.5,
)
}
fn dmo_parameq(p: &[f32; 13]) -> Device {
Device::param_eq(
(80.0 + p[0] * 15920.0) as u16,
(1.0 + p[1] * 35.0 + 0.5) as u8,
((p[2] - 0.5) * 30.0 * 1000.0) as i16,
)
}
fn dmo_wavesreverb(p: &[f32; 13]) -> Device {
Device::reverb(
ReverbMode::WavesReverb,
q15(p[2]),
q15(1.0 - p[3]),
Q15::ONE,
q15(p[1] / 3.0), q15((1.0 - p[1]) / 2.0), )
}
fn dmo_gargle(p: &[f32; 13]) -> Device {
let rate_hz = (p[0] * 999.0 + 0.5) as u16 + 1;
Device::gargle(rate_hz, p[1] >= 0.5)
}
fn dmo_i3dl2(p: &[f32; 13]) -> Device {
Device::i3dl2_reverb(
(-10000.0 + p[0] * 10000.0) as i16,
((0.1 + p[3] * 19.9) * 1000.0) as u16,
((0.1 + p[4] * 1.9) * 1000.0) as u16,
(-10000.0 + p[5] * 11000.0) as i16,
(p[6] * 0.3 * 1000.0) as u16,
(-10000.0 + p[7] * 12000.0) as i16,
(p[8] * 0.1 * 1000.0) as u16,
)
}
fn vst_freeverb(p: &[f32; 13]) -> Device {
Device::reverb(
ReverbMode::Freeverb,
q15(p[0]), q15(p[1]), Q15::ONE, q15(p[5]), q15(p[6]), )
}
fn lin_to_milli_db(lin: f32) -> i32 {
let x_q16_16 = (clampf(lin, 0.0001, 64.0) * Q15_FULL_SCALE_F32 * 2.0) as i64;
((dsp::log2_q16_16(x_q16_16) as i64 * 6021) / 65536) as i32
}
fn vst_js_compresseur(p: &[f32; 13]) -> Device {
const GAIN_SCALE: f32 = 4.0; let gain_in_mdb = lin_to_milli_db(p[0] * GAIN_SCALE);
let threshold_mdb = (lin_to_milli_db(p[1]) - gain_in_mdb).clamp(-60_000, 0);
let makeup_mdb = lin_to_milli_db(p[2] * GAIN_SCALE).clamp(-60_000, 60_000);
Device::compressor(threshold_mdb, 20 * 256, 10, 50_000, makeup_mdb)
}
type PluginDecoder = (usize, fn(&[f32; 13]) -> Device);
fn known_decoder(id1: u32, id2: u32) -> Option<PluginDecoder> {
let entry: PluginDecoder = match (id1, id2) {
(DXMO, ID_COMPRESSOR) => (6, dmo_compressor),
(DXMO, ID_DISTORTION) => (5, dmo_distortion),
(DXMO, ID_ECHO) => (5, dmo_echo),
(DXMO, ID_CHORUS) => (7, dmo_chorus),
(DXMO, ID_FLANGER) => (7, dmo_flanger),
(DXMO, ID_PARAMEQ) => (3, dmo_parameq),
(DXMO, ID_WAVESREVERB) => (4, dmo_wavesreverb),
(DXMO, ID_I3DL2REVERB) => (13, dmo_i3dl2),
(DXMO, ID_GARGLE) => (2, dmo_gargle),
(VSTP, VST_FREEVERB) => (7, vst_freeverb),
(VSTP, VST_JS_COMPRESSOR) => (3, vst_js_compresseur),
_ => return None,
};
Some(entry)
}
pub fn device_from_mix_plugin(plug: &MixPlugin) -> Option<Device> {
let (nparams, build) = known_decoder(plug.info.id1, plug.info.id2)?;
let data = plug.data.as_ref()?;
read_dmo_floats(data, nparams).map(|p| build(&p))
}
pub fn project_channel_inserts(plugins: &MixPlugins, num_channels: usize) -> Vec<DeviceChain> {
let mut out = vec![DeviceChain::default(); num_channels];
for_each_channel_slot(plugins, num_channels, |ch, _slot1, plug| {
if plug.info.routing_flags & IR_BYPASS == 0 {
if let Some(dev) = device_from_mix_plugin(plug) {
out[ch].devices.push(dev);
}
}
});
out
}
fn for_each_channel_slot(
plugins: &MixPlugins,
num_channels: usize,
mut f: impl FnMut(usize, usize, &MixPlugin),
) {
let n_plugins = plugins.plugins.len();
for ch in 0..num_channels {
let mut slot1 = plugins.channel_assignments.get(ch).copied().unwrap_or(0) as usize;
let mut guard = 0;
while slot1 >= 1 && slot1 <= n_plugins && guard < 64 {
let plug = &plugins.plugins[slot1 - 1];
f(ch, slot1, plug);
let out_r = plug.info.output_routing;
if out_r & 0x80 != 0 {
slot1 = (out_r & 0x7F) as usize + 1;
} else {
break;
}
guard += 1;
}
}
}
pub fn project_master_chain(plugins: &MixPlugins) -> DeviceChain {
let mut chain = DeviceChain::default();
for_each_master_slot(plugins, |_slot1, plug| {
if plug.info.routing_flags & IR_BYPASS == 0 {
if let Some(dev) = device_from_mix_plugin(plug) {
chain.devices.push(dev);
}
}
});
chain
}
fn for_each_master_slot(plugins: &MixPlugins, mut f: impl FnMut(usize, &MixPlugin)) {
let n = plugins.plugins.len();
let mut visited = vec![false; n];
for start in 0..n {
if plugins.plugins[start].info.routing_flags & IR_APPLY_TO_MASTER == 0 {
continue;
}
let mut slot1 = start + 1;
let mut guard = 0;
while slot1 >= 1 && slot1 <= n && guard < 64 {
if visited[slot1 - 1] {
break;
}
visited[slot1 - 1] = true;
let plug = &plugins.plugins[slot1 - 1];
f(slot1, plug);
let out_r = plug.info.output_routing;
if out_r & 0x80 != 0 {
slot1 = (out_r & 0x7F) as usize + 1;
} else {
break;
}
guard += 1;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginSkip {
UnknownPlugin,
UndecodableBlob,
Bypassed,
Unrouted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkippedPlugin {
pub slot: usize,
pub id1: u32,
pub id2: u32,
pub shell_plugin_id: u32,
pub name: String,
pub library_name: String,
pub reason: PluginSkip,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PluginReport {
pub chains_touched: usize,
pub skipped: Vec<SkippedPlugin>,
}
impl PluginReport {
pub fn is_complete(&self) -> bool {
self.skipped.is_empty()
}
pub fn unrecognised(&self) -> impl Iterator<Item = &SkippedPlugin> {
self.skipped
.iter()
.filter(|s| s.reason == PluginSkip::UnknownPlugin)
}
}
fn slot_is_occupied(p: &MixPlugin) -> bool {
p.info.id1 != 0 || p.info.id2 != 0 || p.data.is_some()
}
pub fn inspect_mix_plugins(module: &Module) -> PluginReport {
let Some(mp) = module.mix_plugins.as_ref() else {
return PluginReport::default();
};
let num_channels = module.get_num_channels();
let mut reached = vec![false; mp.plugins.len()];
for_each_channel_slot(mp, num_channels, |_ch, slot1, _p| reached[slot1 - 1] = true);
for_each_master_slot(mp, |slot1, _p| reached[slot1 - 1] = true);
let inserts = project_channel_inserts(mp, num_channels);
let master = project_master_chain(mp);
let mut chains_touched = inserts.iter().filter(|c| !c.devices.is_empty()).count();
if !master.devices.is_empty() {
chains_touched += 1;
}
let mut skipped = Vec::new();
for (i, plug) in mp.plugins.iter().enumerate() {
if !slot_is_occupied(plug) {
continue;
}
let reason = if known_decoder(plug.info.id1, plug.info.id2).is_none() {
PluginSkip::UnknownPlugin
} else if device_from_mix_plugin(plug).is_none() {
PluginSkip::UndecodableBlob
} else if plug.info.routing_flags & IR_BYPASS != 0 {
PluginSkip::Bypassed
} else if !reached[i] {
PluginSkip::Unrouted
} else {
continue; };
skipped.push(SkippedPlugin {
slot: i + 1,
id1: plug.info.id1,
id2: plug.info.id2,
shell_plugin_id: plug.info.shell_plugin_id,
name: plug.info.name.clone(),
library_name: plug.info.library_name.clone(),
reason,
});
}
PluginReport {
chains_touched,
skipped,
}
}
pub fn convert_mix_plugins_to_devices(module: &mut Module) -> PluginReport {
let Some(mp) = module.mix_plugins.clone() else {
return PluginReport::default();
};
let report = inspect_mix_plugins(module);
let num_channels = module.get_num_channels();
module.channel_inserts = project_channel_inserts(&mp, num_channels);
module
.master_chain
.devices
.extend(project_master_chain(&mp).devices);
report
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tracker::mix_plugin::MixPluginInfo;
fn dmo_blob(params: &[f32]) -> Vec<u8> {
let mut v = 0u32.to_le_bytes().to_vec(); for &p in params {
v.extend_from_slice(&p.to_le_bytes());
}
v
}
fn dmo_plugin(id2: u32, params: &[f32]) -> MixPlugin {
MixPlugin {
info: MixPluginInfo {
id1: DXMO,
id2,
..Default::default()
},
data: Some(dmo_blob(params)),
}
}
#[test]
fn decode_compressor_params() {
let p = [0.5, 0.02, 0.0508, 40.0 / 60.0, 3.0 / 99.0, 0.25];
let dev = device_from_mix_plugin(&dmo_plugin(ID_COMPRESSOR, &p)).unwrap();
match dev {
Device::Compressor {
threshold_milli_db,
ratio_q8_8,
..
} => {
assert!(
(threshold_milli_db + 20_000).abs() < 200,
"thr {threshold_milli_db}"
);
assert!(
(ratio_q8_8 as i32 - 4 * 256).abs() < 8,
"ratio {ratio_q8_8}"
);
}
_ => panic!("expected Compressor"),
}
}
#[test]
fn decode_parameq_center_and_gain() {
let p = [(8000.0 - 80.0) / 15920.0, 0.314, 0.5 + 6.0 / 30.0];
let dev = device_from_mix_plugin(&dmo_plugin(ID_PARAMEQ, &p)).unwrap();
match dev {
Device::ParamEq {
center_hz,
gain_milli_db,
..
} => {
assert!((center_hz as i32 - 8000).abs() < 60, "center {center_hz}");
assert!(
(gain_milli_db as i32 - 6000).abs() < 60,
"gain {gain_milli_db}"
);
}
_ => panic!("expected ParamEq"),
}
}
#[test]
fn decode_echo_delays() {
let p = [0.5, 0.5, 499.0 / 1999.0, 249.0 / 1999.0, 1.0];
let dev = device_from_mix_plugin(&dmo_plugin(ID_ECHO, &p)).unwrap();
match dev {
Device::Echo {
left_delay_ms,
right_delay_ms,
pan_delay,
..
} => {
assert!((left_delay_ms as i32 - 500).abs() <= 1);
assert!((right_delay_ms as i32 - 250).abs() <= 1);
assert!(pan_delay);
}
_ => panic!("expected Echo"),
}
}
#[test]
fn vst_chunk_blob_is_not_decoded_as_dmo() {
let plug = MixPlugin {
info: MixPluginInfo {
id1: DXMO,
id2: ID_ECHO,
..Default::default()
},
data: Some(b"fEvN\0\0\0\0".to_vec()),
};
assert!(device_from_mix_plugin(&plug).is_none());
}
#[test]
fn vst_without_data_is_none() {
let comp = MixPlugin {
info: MixPluginInfo {
id1: VSTP,
id2: VST_JS_COMPRESSOR,
..Default::default()
},
data: None,
};
assert!(device_from_mix_plugin(&comp).is_none());
}
#[test]
fn routing_one_plugin_per_channel() {
let plugins = MixPlugins {
channel_assignments: vec![0, 1, 0, 2],
plugins: vec![
dmo_plugin(ID_ECHO, &[0.5, 0.5, 0.25, 0.25, 0.0]),
dmo_plugin(ID_PARAMEQ, &[0.5, 0.3, 0.5]),
],
};
let inserts = project_channel_inserts(&plugins, 4);
assert!(inserts[0].devices.is_empty());
assert_eq!(inserts[1].devices.len(), 1); assert!(inserts[2].devices.is_empty());
assert_eq!(inserts[3].devices.len(), 1); }
#[test]
fn routing_follows_output_chain() {
let mut p0 = dmo_plugin(ID_ECHO, &[0.5, 0.5, 0.25, 0.25, 0.0]);
p0.info.output_routing = 0x80 | 1; let plugins = MixPlugins {
channel_assignments: vec![1],
plugins: vec![p0, dmo_plugin(ID_PARAMEQ, &[0.5, 0.3, 0.5])],
};
let inserts = project_channel_inserts(&plugins, 1);
assert_eq!(inserts[0].devices.len(), 2, "serial chain not followed");
}
#[test]
fn decode_gargle_rate_and_shape() {
let p = [(200.0 - 1.0) / 999.0, 1.0];
let dev = device_from_mix_plugin(&dmo_plugin(ID_GARGLE, &p)).unwrap();
match dev {
Device::Gargle {
rate_hz,
wave_square,
..
} => {
assert!((rate_hz as i32 - 200).abs() <= 1, "rate {rate_hz}");
assert!(wave_square);
}
_ => panic!("expected Gargle"),
}
}
#[test]
fn decode_vst_freeverb_params() {
let p = [0.681, 0.5, 0.0, 1.0, 0.0, 0.25, 0.0];
let plug = MixPlugin {
info: MixPluginInfo {
id1: VSTP,
id2: VST_FREEVERB,
..Default::default()
},
data: Some(dmo_blob(&p)),
};
match device_from_mix_plugin(&plug).unwrap() {
Device::Reverb {
mode,
room_size,
wet,
dry,
..
} => {
assert!(matches!(mode, ReverbMode::Freeverb));
assert_eq!(room_size, q15(0.681)); assert_eq!(wet, q15(0.25)); assert_eq!(dry, Q15::ZERO); }
_ => panic!("expected Reverb"),
}
}
#[test]
fn decode_vst_js_compresseur_params() {
let p = [0.25, 0.5, 0.2498];
let plug = MixPlugin {
info: MixPluginInfo {
id1: VSTP,
id2: VST_JS_COMPRESSOR,
..Default::default()
},
data: Some(dmo_blob(&p)),
};
match device_from_mix_plugin(&plug).unwrap() {
Device::Compressor {
threshold_milli_db,
makeup_milli_db,
..
} => {
assert!(
(threshold_milli_db + 6_021).abs() < 300,
"thr {threshold_milli_db}"
);
assert!(makeup_milli_db.abs() < 300, "makeup {makeup_milli_db}");
}
_ => panic!("expected Compressor"),
}
}
#[test]
fn master_flagged_plugin_goes_to_master_chain() {
let mut p = dmo_plugin(ID_COMPRESSOR, &[0.5, 0.02, 0.05, 0.5, 0.03, 0.25]);
p.info.routing_flags = IR_APPLY_TO_MASTER;
let plugins = MixPlugins {
channel_assignments: vec![0, 0],
plugins: vec![p],
};
assert!(project_channel_inserts(&plugins, 2)
.iter()
.all(|c| c.devices.is_empty()));
let master = project_master_chain(&plugins);
assert_eq!(master.devices.len(), 1);
}
#[test]
fn bypassed_plugin_is_skipped() {
let mut p = dmo_plugin(ID_ECHO, &[0.5, 0.5, 0.25, 0.25, 0.0]);
p.info.routing_flags = IR_BYPASS;
let plugins = MixPlugins {
channel_assignments: vec![1],
plugins: vec![p],
};
let inserts = project_channel_inserts(&plugins, 1);
assert!(inserts[0].devices.is_empty());
}
fn module_with(plugins: Vec<MixPlugin>) -> Module {
Module {
mix_plugins: Some(MixPlugins {
channel_assignments: Vec::new(),
plugins,
}),
..Module::default()
}
}
fn on_master(mut p: MixPlugin) -> MixPlugin {
p.info.routing_flags |= IR_APPLY_TO_MASTER;
p
}
#[test]
fn report_is_clean_when_everything_converts() {
let m = module_with(vec![on_master(dmo_plugin(
ID_ECHO,
&[0.5, 0.5, 0.25, 0.25, 0.0],
))]);
let r = inspect_mix_plugins(&m);
assert!(r.is_complete(), "{:?}", r.skipped);
assert_eq!(r.chains_touched, 1);
}
#[test]
fn report_names_an_unknown_plugin_for_the_census() {
let plug = on_master(MixPlugin {
info: MixPluginInfo {
id1: VSTP,
id2: 0x1234_5678,
shell_plugin_id: 7,
name: "My Chorus".into(),
library_name: "shell.dll".into(),
..Default::default()
},
data: Some(dmo_blob(&[0.5])),
});
let r = inspect_mix_plugins(&module_with(vec![plug]));
assert!(!r.is_complete());
let s = &r.skipped[0];
assert_eq!(s.reason, PluginSkip::UnknownPlugin);
assert_eq!((s.slot, s.id2, s.shell_plugin_id), (1, 0x1234_5678, 7));
assert_eq!(s.library_name, "shell.dll");
assert_eq!(r.unrecognised().count(), 1);
}
#[test]
fn report_separates_an_undecodable_blob_from_an_unknown_id() {
let plug = on_master(MixPlugin {
info: MixPluginInfo {
id1: DXMO,
id2: ID_ECHO,
..Default::default()
},
data: Some(b"fEvN\0\0\0\0".to_vec()),
});
let r = inspect_mix_plugins(&module_with(vec![plug]));
assert_eq!(r.skipped[0].reason, PluginSkip::UndecodableBlob);
assert_eq!(r.unrecognised().count(), 0);
}
#[test]
fn report_distinguishes_bypassed_from_unrouted() {
let mut bypassed = on_master(dmo_plugin(ID_ECHO, &[0.5, 0.5, 0.25, 0.25, 0.0]));
bypassed.info.routing_flags |= IR_BYPASS;
let orphan = dmo_plugin(ID_PARAMEQ, &[0.5, 0.3, 0.5]);
let r = inspect_mix_plugins(&module_with(vec![bypassed, orphan]));
assert_eq!(r.skipped.len(), 2);
assert_eq!(r.skipped[0].reason, PluginSkip::Bypassed);
assert_eq!(r.skipped[1].reason, PluginSkip::Unrouted);
assert_eq!(r.chains_touched, 0);
}
#[test]
fn report_ignores_the_blank_slots_of_the_fixed_table() {
let mut plugins = vec![MixPlugin::default(); 64];
plugins[0] = on_master(dmo_plugin(ID_PARAMEQ, &[0.5, 0.3, 0.5]));
let r = inspect_mix_plugins(&module_with(plugins));
assert!(r.is_complete(), "{:?}", r.skipped);
}
#[test]
fn inspect_agrees_with_conversion_and_leaves_the_table_alone() {
let mut m = module_with(vec![
on_master(dmo_plugin(ID_ECHO, &[0.5, 0.5, 0.25, 0.25, 0.0])),
dmo_plugin(ID_PARAMEQ, &[0.5, 0.3, 0.5]), ]);
let before = inspect_mix_plugins(&m);
let returned = convert_mix_plugins_to_devices(&mut m);
assert_eq!(before, returned);
assert!(m.mix_plugins.is_some());
assert_eq!(inspect_mix_plugins(&m), returned);
assert_eq!(m.master_chain.devices.len(), 1);
}
}