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)
}
pub fn device_from_mix_plugin(plug: &MixPlugin) -> Option<Device> {
match plug.info.id1 {
DXMO => {
let data = plug.data.as_ref()?;
match plug.info.id2 {
ID_COMPRESSOR => read_dmo_floats(data, 6).map(|p| dmo_compressor(&p)),
ID_DISTORTION => read_dmo_floats(data, 5).map(|p| dmo_distortion(&p)),
ID_ECHO => read_dmo_floats(data, 5).map(|p| dmo_echo(&p)),
ID_CHORUS => read_dmo_floats(data, 7).map(|p| dmo_chorus(&p)),
ID_FLANGER => read_dmo_floats(data, 7).map(|p| dmo_flanger(&p)),
ID_PARAMEQ => read_dmo_floats(data, 3).map(|p| dmo_parameq(&p)),
ID_WAVESREVERB => read_dmo_floats(data, 4).map(|p| dmo_wavesreverb(&p)),
ID_I3DL2REVERB => read_dmo_floats(data, 13).map(|p| dmo_i3dl2(&p)),
ID_GARGLE => read_dmo_floats(data, 2).map(|p| dmo_gargle(&p)),
_ => None,
}
}
VSTP => match plug.info.id2 {
VST_FREEVERB => plug
.data
.as_ref()
.and_then(|d| read_dmo_floats(d, 7))
.map(|p| vst_freeverb(&p)),
VST_JS_COMPRESSOR => plug
.data
.as_ref()
.and_then(|d| read_dmo_floats(d, 3))
.map(|p| vst_js_compresseur(&p)),
_ => None,
},
_ => None,
}
}
pub fn project_channel_inserts(plugins: &MixPlugins, num_channels: usize) -> Vec<DeviceChain> {
let mut out = vec![DeviceChain::default(); num_channels];
let n_plugins = plugins.plugins.len();
for (ch, chain) in out.iter_mut().enumerate() {
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];
if plug.info.routing_flags & IR_BYPASS == 0 {
if let Some(dev) = device_from_mix_plugin(plug) {
chain.devices.push(dev);
}
}
let out_r = plug.info.output_routing;
if out_r & 0x80 != 0 {
slot1 = (out_r & 0x7F) as usize + 1;
} else {
break;
}
guard += 1;
}
}
out
}
pub fn project_master_chain(plugins: &MixPlugins) -> DeviceChain {
let n = plugins.plugins.len();
let mut visited = vec![false; n];
let mut chain = DeviceChain::default();
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];
if plug.info.routing_flags & IR_BYPASS == 0 {
if let Some(dev) = device_from_mix_plugin(plug) {
chain.devices.push(dev);
}
}
let out_r = plug.info.output_routing;
if out_r & 0x80 != 0 {
slot1 = (out_r & 0x7F) as usize + 1;
} else {
break;
}
guard += 1;
}
}
chain
}
pub fn convert_mix_plugins_to_devices(module: &mut Module) -> usize {
let Some(mp) = module.mix_plugins.clone() else {
return 0;
};
let num_channels = module.get_num_channels();
let inserts = project_channel_inserts(&mp, num_channels);
let master = project_master_chain(&mp);
let mut touched = inserts.iter().filter(|c| !c.devices.is_empty()).count();
if !master.devices.is_empty() {
touched += 1;
}
module.channel_inserts = inserts;
module.master_chain.devices.extend(master.devices);
touched
}
#[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());
}
}