use super::dls::DlsArticulationBlock;
use super::sample_voice::{EnvelopeParams, FilterParams, ModEnvParams, VibratoParams};
pub const CONN_SRC_NONE: u16 = 0x0000;
pub const CONN_SRC_LFO: u16 = 0x0001;
pub const CONN_SRC_KEYONVELOCITY: u16 = 0x0002;
pub const CONN_SRC_KEYNUMBER: u16 = 0x0003;
pub const CONN_SRC_EG1: u16 = 0x0004;
pub const CONN_SRC_EG2: u16 = 0x0005;
pub const CONN_SRC_PITCHWHEEL: u16 = 0x0006;
pub const CONN_SRC_POLYPRESSURE: u16 = 0x0007;
pub const CONN_SRC_CHANNELPRESSURE: u16 = 0x0008;
pub const CONN_SRC_VIBRATO: u16 = 0x0009;
pub const CONN_SRC_CC1: u16 = 0x0081;
pub const CONN_SRC_CC7: u16 = 0x0087;
pub const CONN_SRC_CC10: u16 = 0x008A;
pub const CONN_SRC_CC11: u16 = 0x008B;
pub const CONN_SRC_CC91: u16 = 0x00DB;
pub const CONN_SRC_CC93: u16 = 0x00DD;
pub const CONN_SRC_RPN0: u16 = 0x0100;
pub const CONN_SRC_RPN1: u16 = 0x0101;
pub const CONN_SRC_RPN2: u16 = 0x0102;
pub const CONN_DST_NONE: u16 = 0x0000;
pub const CONN_DST_GAIN: u16 = 0x0001;
pub const CONN_DST_PITCH: u16 = 0x0003;
pub const CONN_DST_PAN: u16 = 0x0004;
pub const CONN_DST_KEYNUMBER: u16 = 0x0005;
pub const CONN_DST_ATTENUATION: u16 = CONN_DST_GAIN;
pub const CONN_DST_LEFT: u16 = 0x0010;
pub const CONN_DST_RIGHT: u16 = 0x0011;
pub const CONN_DST_CENTER: u16 = 0x0012;
pub const CONN_DST_LFE_CHANNEL: u16 = 0x0013;
pub const CONN_DST_LEFTREAR: u16 = 0x0014;
pub const CONN_DST_RIGHTREAR: u16 = 0x0015;
pub const CONN_DST_CHORUS: u16 = 0x0080;
pub const CONN_DST_REVERB: u16 = 0x0081;
pub const CONN_DST_LFO_FREQUENCY: u16 = 0x0104;
pub const CONN_DST_LFO_STARTDELAY: u16 = 0x0105;
pub const CONN_DST_VIB_FREQUENCY: u16 = 0x0114;
pub const CONN_DST_VIB_STARTDELAY: u16 = 0x0115;
pub const CONN_DST_EG1_ATTACKTIME: u16 = 0x0206;
pub const CONN_DST_EG1_DECAYTIME: u16 = 0x0207;
pub const CONN_DST_EG1_RELEASETIME: u16 = 0x0209;
pub const CONN_DST_EG1_SUSTAINLEVEL: u16 = 0x020A;
pub const CONN_DST_EG1_DELAYTIME: u16 = 0x020B;
pub const CONN_DST_EG1_HOLDTIME: u16 = 0x020C;
pub const CONN_DST_EG1_SHUTDOWNTIME: u16 = 0x020D;
pub const CONN_DST_EG2_ATTACKTIME: u16 = 0x030A;
pub const CONN_DST_EG2_DECAYTIME: u16 = 0x030B;
pub const CONN_DST_EG2_RELEASETIME: u16 = 0x030D;
pub const CONN_DST_EG2_SUSTAINLEVEL: u16 = 0x030E;
pub const CONN_DST_EG2_DELAYTIME: u16 = 0x030F;
pub const CONN_DST_EG2_HOLDTIME: u16 = 0x0310;
pub const CONN_DST_FILTER_CUTOFF: u16 = 0x0500;
pub const CONN_DST_FILTER_Q: u16 = 0x0501;
pub const CONN_TRN_NONE: u16 = 0x0000;
pub const CONN_TRN_CONCAVE: u16 = 0x0001;
pub const CONN_TRN_CONVEX: u16 = 0x0002;
pub const CONN_TRN_SWITCH: u16 = 0x0003;
pub const ABSOLUTE_ZERO: i32 = i32::MIN;
fn time_cents_to_secs(scale: i32) -> f32 {
if scale == ABSOLUTE_ZERO {
return 0.0;
}
let tc = scale as f64;
let secs = (tc / (1200.0 * 65536.0)).exp2();
secs.clamp(0.0, 60.0) as f32
}
fn abs_pitch_to_cents(scale: i32) -> f32 {
if scale == ABSOLUTE_ZERO {
return 0.0;
}
((scale as f64) / 65536.0).clamp(-14_400.0, 14_400.0) as f32
}
fn abs_pitch_to_hz(scale: i32) -> f32 {
if scale == ABSOLUTE_ZERO {
return 0.0;
}
let tc = scale as f64;
let hz = (tc / (1200.0 * 65536.0)).exp2();
hz.clamp(0.0, 50.0) as f32
}
fn sustain_pct_to_linear(scale: i32) -> f32 {
if scale == ABSOLUTE_ZERO {
return 1.0; }
(scale as f32 / 1000.0).clamp(0.0, 1.0)
}
fn gain_to_linear(scale: i32) -> f32 {
if scale == ABSOLUTE_ZERO {
return 1.0;
}
let db = scale as f32 / 655_360.0;
let db = db.clamp(-96.0, 48.0);
10.0f32.powf(db / 20.0)
}
#[derive(Clone, Copy, Debug)]
pub struct Articulation {
pub vol_delay_s: f32,
pub vol_attack_s: f32,
pub vol_hold_s: f32,
pub vol_decay_s: f32,
pub vol_sustain_level: f32,
pub vol_release_s: f32,
pub vol_overridden: bool,
pub vol_velocity_to_attack_tc: i32,
pub mod_delay_tc: i32,
pub mod_attack_tc: i32,
pub mod_hold_tc: i32,
pub mod_decay_tc: i32,
pub mod_sustain_pct: i32,
pub mod_release_tc: i32,
pub mod_to_pitch_cents: f32,
pub mod_to_filter_cents: f32,
pub mod_lfo_freq_hz: f32,
pub mod_lfo_delay_s: f32,
pub mod_lfo_to_pitch_cents: f32,
pub mod_lfo_to_gain_db: f32,
pub vib_lfo_freq_hz: f32,
pub vib_lfo_delay_s: f32,
pub vib_lfo_to_pitch_cents: f32,
pub filter_cutoff_cents: f32,
pub filter_q_centibels: f32,
pub tuning_cents: f32,
pub gain_linear: f32,
pub pan_pct: f32, }
impl Default for Articulation {
fn default() -> Self {
let env = EnvelopeParams::default();
let vib = VibratoParams::default();
Self {
vol_delay_s: env.delay_s,
vol_attack_s: env.attack_s,
vol_hold_s: env.hold_s,
vol_decay_s: env.decay_s,
vol_sustain_level: env.sustain_level,
vol_release_s: env.release_s,
vol_overridden: false,
vol_velocity_to_attack_tc: 0,
mod_delay_tc: ABSOLUTE_ZERO,
mod_attack_tc: ABSOLUTE_ZERO,
mod_hold_tc: ABSOLUTE_ZERO,
mod_decay_tc: ABSOLUTE_ZERO,
mod_sustain_pct: ABSOLUTE_ZERO,
mod_release_tc: ABSOLUTE_ZERO,
mod_to_pitch_cents: 0.0,
mod_to_filter_cents: 0.0,
mod_lfo_freq_hz: 5.0,
mod_lfo_delay_s: 0.010,
mod_lfo_to_pitch_cents: 0.0,
mod_lfo_to_gain_db: 0.0,
vib_lfo_freq_hz: 5.0,
vib_lfo_delay_s: 0.010,
vib_lfo_to_pitch_cents: vib.depth_cents,
filter_cutoff_cents: 0.0,
filter_q_centibels: 0.0,
tuning_cents: 0.0,
gain_linear: 1.0,
pan_pct: 0.0,
}
}
}
impl Articulation {
pub fn evaluate(
region_blocks: &[DlsArticulationBlock],
instrument_blocks: &[DlsArticulationBlock],
) -> Self {
let mut art = Articulation::default();
let mut overridden = OverrideMask::default();
art.apply_blocks(region_blocks, &mut overridden, true);
art.apply_blocks(instrument_blocks, &mut overridden, false);
art
}
fn apply_blocks(
&mut self,
blocks: &[DlsArticulationBlock],
overridden: &mut OverrideMask,
is_region: bool,
) {
for b in blocks {
self.apply_block(b, overridden, is_region);
}
}
fn apply_block(
&mut self,
b: &DlsArticulationBlock,
overridden: &mut OverrideMask,
is_region: bool,
) {
let _out_transform = b.transform & 0x000F;
macro_rules! set {
($field:ident, $mask:ident, $val:expr) => {{
if is_region || !overridden.$mask {
self.$field = $val;
if is_region {
overridden.$mask = true;
}
}
}};
}
if b.source == CONN_SRC_NONE && b.control == CONN_SRC_NONE {
match b.destination {
CONN_DST_EG1_DELAYTIME => {
set!(vol_delay_s, vol_delay, time_cents_to_secs(b.scale));
self.vol_overridden = true;
}
CONN_DST_EG1_ATTACKTIME => {
set!(vol_attack_s, vol_attack, time_cents_to_secs(b.scale));
self.vol_overridden = true;
}
CONN_DST_EG1_HOLDTIME => {
set!(vol_hold_s, vol_hold, time_cents_to_secs(b.scale));
self.vol_overridden = true;
}
CONN_DST_EG1_DECAYTIME => {
set!(vol_decay_s, vol_decay, time_cents_to_secs(b.scale));
self.vol_overridden = true;
}
CONN_DST_EG1_SUSTAINLEVEL => {
set!(
vol_sustain_level,
vol_sustain,
sustain_pct_to_linear(b.scale)
);
self.vol_overridden = true;
}
CONN_DST_EG1_RELEASETIME => {
set!(vol_release_s, vol_release, time_cents_to_secs(b.scale));
self.vol_overridden = true;
}
CONN_DST_EG2_DELAYTIME => set!(mod_delay_tc, mod_delay, b.scale),
CONN_DST_EG2_ATTACKTIME => set!(mod_attack_tc, mod_attack, b.scale),
CONN_DST_EG2_HOLDTIME => set!(mod_hold_tc, mod_hold, b.scale),
CONN_DST_EG2_DECAYTIME => set!(mod_decay_tc, mod_decay, b.scale),
CONN_DST_EG2_SUSTAINLEVEL => set!(mod_sustain_pct, mod_sustain, b.scale),
CONN_DST_EG2_RELEASETIME => set!(mod_release_tc, mod_release, b.scale),
CONN_DST_LFO_FREQUENCY => {
let hz = abs_pitch_to_hz(b.scale);
if hz > 0.0 {
set!(mod_lfo_freq_hz, mod_lfo_freq, hz);
}
}
CONN_DST_LFO_STARTDELAY => {
set!(mod_lfo_delay_s, mod_lfo_delay, time_cents_to_secs(b.scale));
}
CONN_DST_VIB_FREQUENCY => {
let hz = abs_pitch_to_hz(b.scale);
if hz > 0.0 {
set!(vib_lfo_freq_hz, vib_lfo_freq, hz);
}
}
CONN_DST_VIB_STARTDELAY => {
set!(vib_lfo_delay_s, vib_lfo_delay, time_cents_to_secs(b.scale));
}
CONN_DST_FILTER_CUTOFF => {
set!(
filter_cutoff_cents,
filter_cutoff,
abs_pitch_to_cents(b.scale)
);
}
CONN_DST_FILTER_Q => {
set!(filter_q_centibels, filter_q, b.scale as f32 / 65_536.0);
}
CONN_DST_PITCH => set!(tuning_cents, tuning, abs_pitch_to_cents(b.scale)),
CONN_DST_GAIN => set!(gain_linear, gain, gain_to_linear(b.scale)),
CONN_DST_PAN => set!(pan_pct, pan, (b.scale as f32 / 10.0).clamp(-50.0, 50.0)),
_ => {}
}
return;
}
if b.source == CONN_SRC_LFO && b.control == CONN_SRC_NONE && b.destination == CONN_DST_PITCH
{
set!(
mod_lfo_to_pitch_cents,
mod_lfo_to_pitch,
abs_pitch_to_cents(b.scale)
);
return;
}
if b.source == CONN_SRC_LFO && b.control == CONN_SRC_NONE && b.destination == CONN_DST_GAIN
{
set!(
mod_lfo_to_gain_db,
mod_lfo_to_gain,
b.scale as f32 / 655_360.0
);
return;
}
if b.source == CONN_SRC_VIBRATO
&& b.control == CONN_SRC_NONE
&& b.destination == CONN_DST_PITCH
{
set!(
vib_lfo_to_pitch_cents,
vib_lfo_to_pitch,
abs_pitch_to_cents(b.scale)
);
return;
}
if b.source == CONN_SRC_KEYONVELOCITY
&& b.control == CONN_SRC_NONE
&& b.destination == CONN_DST_EG1_ATTACKTIME
{
set!(vol_velocity_to_attack_tc, vol_velocity_to_attack, b.scale);
return;
}
if b.source == CONN_SRC_EG2 && b.control == CONN_SRC_NONE && b.destination == CONN_DST_PITCH
{
set!(
mod_to_pitch_cents,
mod_to_pitch,
abs_pitch_to_cents(b.scale)
);
return;
}
if b.source == CONN_SRC_EG2
&& b.control == CONN_SRC_NONE
&& b.destination == CONN_DST_FILTER_CUTOFF
{
set!(
mod_to_filter_cents,
mod_to_filter,
abs_pitch_to_cents(b.scale)
);
return;
}
let _ = (b.kind, _out_transform);
}
pub fn envelope(&self) -> EnvelopeParams {
EnvelopeParams {
delay_s: self.vol_delay_s,
attack_s: self.vol_attack_s.max(0.0001),
hold_s: self.vol_hold_s,
decay_s: self.vol_decay_s.max(0.0001),
sustain_level: self.vol_sustain_level.clamp(0.0, 1.0),
release_s: self.vol_release_s.max(0.0001),
}
}
pub fn vibrato(&self) -> VibratoParams {
let depth_cents = if self.vib_lfo_to_pitch_cents.abs() > 0.0 {
self.vib_lfo_to_pitch_cents
} else {
self.mod_lfo_to_pitch_cents
};
let freq_hz = if self.vib_lfo_to_pitch_cents.abs() > 0.0 {
self.vib_lfo_freq_hz
} else {
self.mod_lfo_freq_hz
};
let delay_s = if self.vib_lfo_to_pitch_cents.abs() > 0.0 {
self.vib_lfo_delay_s
} else {
self.mod_lfo_delay_s
};
VibratoParams {
freq_hz,
depth_cents,
delay_s,
}
}
pub fn mod_env(&self) -> ModEnvParams {
ModEnvParams {
delay_s: time_cents_to_secs(self.mod_delay_tc),
attack_s: time_cents_to_secs(self.mod_attack_tc),
hold_s: time_cents_to_secs(self.mod_hold_tc),
decay_s: time_cents_to_secs(self.mod_decay_tc),
sustain_level: sustain_pct_to_linear(self.mod_sustain_pct),
release_s: time_cents_to_secs(self.mod_release_tc),
to_filter_cents: self.mod_to_filter_cents.round() as i32,
}
}
pub fn filter(&self) -> FilterParams {
let cutoff_cents = if self.filter_cutoff_cents == 0.0 {
13_500
} else {
self.filter_cutoff_cents.round() as i32
};
let q_centibels = self.filter_q_centibels.round() as i32;
FilterParams {
cutoff_cents,
q_centibels,
kind: super::sample_voice::FilterType::TwoPoleLowPass,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
struct OverrideMask {
vol_delay: bool,
vol_attack: bool,
vol_hold: bool,
vol_decay: bool,
vol_sustain: bool,
vol_release: bool,
vol_velocity_to_attack: bool,
mod_delay: bool,
mod_attack: bool,
mod_hold: bool,
mod_decay: bool,
mod_sustain: bool,
mod_release: bool,
mod_to_pitch: bool,
mod_to_filter: bool,
mod_lfo_freq: bool,
mod_lfo_delay: bool,
mod_lfo_to_pitch: bool,
mod_lfo_to_gain: bool,
vib_lfo_freq: bool,
vib_lfo_delay: bool,
vib_lfo_to_pitch: bool,
filter_cutoff: bool,
filter_q: bool,
tuning: bool,
gain: bool,
pan: bool,
}
#[cfg(test)]
mod tests {
use super::super::dls::DlsArtKind;
use super::*;
fn block(
source: u16,
control: u16,
destination: u16,
transform: u16,
scale: i32,
) -> DlsArticulationBlock {
DlsArticulationBlock {
kind: DlsArtKind::Art1,
source,
control,
destination,
transform,
scale,
}
}
#[test]
fn empty_lists_yield_defaults() {
let a = Articulation::evaluate(&[], &[]);
let env = a.envelope();
assert!((env.attack_s - 0.005).abs() < 1e-6);
assert!(!a.vol_overridden);
assert_eq!(a.tuning_cents, 0.0);
assert_eq!(a.gain_linear, 1.0);
}
#[test]
fn region_vol_eg_attack_override() {
let secs = 0.1f64;
let tc = (secs.log2() * 1200.0 * 65536.0) as i32;
let blocks = vec![block(
CONN_SRC_NONE,
CONN_SRC_NONE,
CONN_DST_EG1_ATTACKTIME,
CONN_TRN_NONE,
tc,
)];
let a = Articulation::evaluate(&blocks, &[]);
assert!((a.vol_attack_s - 0.1).abs() < 0.002);
assert!(a.vol_overridden);
}
#[test]
fn region_overrides_instrument() {
let inst_tc = (1.0f64.log2() * 1200.0 * 65536.0) as i32; let reg_tc = (0.05f64.log2() * 1200.0 * 65536.0) as i32; let inst = vec![block(
CONN_SRC_NONE,
CONN_SRC_NONE,
CONN_DST_EG1_RELEASETIME,
CONN_TRN_NONE,
inst_tc,
)];
let region = vec![block(
CONN_SRC_NONE,
CONN_SRC_NONE,
CONN_DST_EG1_RELEASETIME,
CONN_TRN_NONE,
reg_tc,
)];
let a = Articulation::evaluate(®ion, &inst);
assert!((a.vol_release_s - 0.05).abs() < 0.002);
}
#[test]
fn instrument_fallback_when_region_silent() {
let tc = (0.2f64.log2() * 1200.0 * 65536.0) as i32;
let inst = vec![block(
CONN_SRC_NONE,
CONN_SRC_NONE,
CONN_DST_EG1_ATTACKTIME,
CONN_TRN_NONE,
tc,
)];
let a = Articulation::evaluate(&[], &inst);
assert!((a.vol_attack_s - 0.2).abs() < 0.005);
}
#[test]
fn tuning_in_cents() {
let blocks = vec![block(
CONN_SRC_NONE,
CONN_SRC_NONE,
CONN_DST_PITCH,
CONN_TRN_NONE,
100 * 65_536,
)];
let a = Articulation::evaluate(&blocks, &[]);
assert!((a.tuning_cents - 100.0).abs() < 0.01);
}
#[test]
fn lfo_pitch_routes_to_vibrato_depth() {
let blocks = vec![block(
CONN_SRC_LFO,
CONN_SRC_NONE,
CONN_DST_PITCH,
CONN_TRN_NONE,
50 * 65_536,
)];
let a = Articulation::evaluate(&blocks, &[]);
let vib = a.vibrato();
assert!((vib.depth_cents - 50.0).abs() < 0.05);
assert!((vib.freq_hz - 5.0).abs() < 0.01);
}
#[test]
fn vibrato_lfo_dls2_takes_precedence_over_mod_lfo() {
let blocks = vec![
block(
CONN_SRC_LFO,
CONN_SRC_NONE,
CONN_DST_PITCH,
CONN_TRN_NONE,
10 * 65_536,
),
block(
CONN_SRC_VIBRATO,
CONN_SRC_NONE,
CONN_DST_PITCH,
CONN_TRN_NONE,
75 * 65_536,
),
];
let a = Articulation::evaluate(&blocks, &[]);
let vib = a.vibrato();
assert!((vib.depth_cents - 75.0).abs() < 0.05);
}
#[test]
fn gain_destination_attenuates() {
let blocks = vec![block(
CONN_SRC_NONE,
CONN_SRC_NONE,
CONN_DST_GAIN,
CONN_TRN_NONE,
-6 * 655_360,
)];
let a = Articulation::evaluate(&blocks, &[]);
assert!((a.gain_linear - 0.5011872).abs() < 0.001);
}
#[test]
fn absolute_zero_skipped() {
let blocks = vec![block(
CONN_SRC_NONE,
CONN_SRC_NONE,
CONN_DST_EG1_ATTACKTIME,
CONN_TRN_NONE,
ABSOLUTE_ZERO,
)];
let a = Articulation::evaluate(&blocks, &[]);
let env = a.envelope();
assert!(env.attack_s > 0.0);
assert!(env.attack_s < 0.001);
}
#[test]
fn unknown_connections_dropped_silently() {
let blocks = vec![block(
CONN_SRC_CC1,
CONN_SRC_NONE,
CONN_DST_FILTER_CUTOFF,
CONN_TRN_NONE,
12_345,
)];
let a = Articulation::evaluate(&blocks, &[]);
assert_eq!(a.filter_cutoff_cents, 0.0);
}
#[test]
fn default_mod_env_is_inert() {
let a = Articulation::default();
let me = a.mod_env();
assert!(
me.is_inert(),
"default articulation must yield inert mod-env"
);
assert_eq!(me.to_filter_cents, 0);
assert!((me.sustain_level - 1.0).abs() < 1e-6);
}
#[test]
fn default_filter_is_open_sentinel() {
let a = Articulation::default();
let f = a.filter();
assert_eq!(f.cutoff_cents, 13_500);
assert_eq!(f.q_centibels, 0);
}
#[test]
fn eg2_to_filter_routing_lands_on_mod_env() {
let blocks = vec![block(
CONN_SRC_EG2,
CONN_SRC_NONE,
CONN_DST_FILTER_CUTOFF,
CONN_TRN_NONE,
6_000 * 65_536,
)];
let a = Articulation::evaluate(&blocks, &[]);
let me = a.mod_env();
assert!(
!me.is_inert(),
"EG2 → filter routing should activate mod-env"
);
assert_eq!(me.to_filter_cents, 6_000);
}
#[test]
fn filter_cutoff_override_lands_on_filter() {
let blocks = vec![block(
CONN_SRC_NONE,
CONN_SRC_NONE,
CONN_DST_FILTER_CUTOFF,
CONN_TRN_NONE,
5_938 * 65_536,
)];
let a = Articulation::evaluate(&blocks, &[]);
let f = a.filter();
assert_eq!(f.cutoff_cents, 5_938);
assert_eq!(f.q_centibels, 0);
}
#[test]
fn eg2_attack_time_lands_on_mod_env_attack() {
let tc: i32 = (0.2f64.log2() * 1200.0 * 65536.0) as i32;
let blocks = vec![block(
CONN_SRC_NONE,
CONN_SRC_NONE,
CONN_DST_EG2_ATTACKTIME,
CONN_TRN_NONE,
tc,
)];
let a = Articulation::evaluate(&blocks, &[]);
let me = a.mod_env();
assert!(
(me.attack_s - 0.200).abs() < 0.005,
"EG2 attack should resolve to ~200 ms; got {} s",
me.attack_s
);
}
}