1407 lines
48 KiB
Rust
1407 lines
48 KiB
Rust
use strum::IntoEnumIterator;
|
|
|
|
use crate::{
|
|
common::{
|
|
errors::{
|
|
DecodingError,
|
|
MapperError,
|
|
ValidationError,
|
|
},
|
|
validator::{
|
|
validate_ascii_padded,
|
|
validate_range,
|
|
},
|
|
},
|
|
dsp408::{
|
|
protocol::{
|
|
constants as protocol_constants,
|
|
validators,
|
|
},
|
|
types,
|
|
},
|
|
};
|
|
|
|
use super::{
|
|
constants as mapper_constants,
|
|
decode_ascii,
|
|
encode_ascii,
|
|
};
|
|
|
|
// ------ CHANNEL ------
|
|
|
|
fn channel_to_raw_value(channel: types::Channel) -> u8 {
|
|
match channel {
|
|
types::Channel::Input(types::InputChannel::InA) => 0x00,
|
|
types::Channel::Input(types::InputChannel::InB) => 0x01,
|
|
types::Channel::Input(types::InputChannel::InC) => 0x02,
|
|
types::Channel::Input(types::InputChannel::InD) => 0x03,
|
|
types::Channel::Output(types::OutputChannel::Out1) => 0x04,
|
|
types::Channel::Output(types::OutputChannel::Out2) => 0x05,
|
|
types::Channel::Output(types::OutputChannel::Out3) => 0x06,
|
|
types::Channel::Output(types::OutputChannel::Out4) => 0x07,
|
|
types::Channel::Output(types::OutputChannel::Out5) => 0x08,
|
|
types::Channel::Output(types::OutputChannel::Out6) => 0x09,
|
|
types::Channel::Output(types::OutputChannel::Out7) => 0x0A,
|
|
types::Channel::Output(types::OutputChannel::Out8) => 0x0B,
|
|
}
|
|
}
|
|
|
|
fn raw_to_channel_value(value: u8) -> Option<types::Channel> {
|
|
match value {
|
|
0x00 => Some(types::Channel::Input(types::InputChannel::InA)),
|
|
0x01 => Some(types::Channel::Input(types::InputChannel::InB)),
|
|
0x02 => Some(types::Channel::Input(types::InputChannel::InC)),
|
|
0x03 => Some(types::Channel::Input(types::InputChannel::InD)),
|
|
0x04 => Some(types::Channel::Output(types::OutputChannel::Out1)),
|
|
0x05 => Some(types::Channel::Output(types::OutputChannel::Out2)),
|
|
0x06 => Some(types::Channel::Output(types::OutputChannel::Out3)),
|
|
0x07 => Some(types::Channel::Output(types::OutputChannel::Out4)),
|
|
0x08 => Some(types::Channel::Output(types::OutputChannel::Out5)),
|
|
0x09 => Some(types::Channel::Output(types::OutputChannel::Out6)),
|
|
0x0A => Some(types::Channel::Output(types::OutputChannel::Out7)),
|
|
0x0B => Some(types::Channel::Output(types::OutputChannel::Out8)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn channel_to_raw(channel: types::Channel) -> Result<u8, MapperError> {
|
|
Ok(channel_to_raw_value(channel))
|
|
}
|
|
|
|
pub fn raw_to_channel(value: u8) -> Result<types::Channel, MapperError> {
|
|
validators::validate_raw_channel(value)?;
|
|
|
|
raw_to_channel_value(value)
|
|
.ok_or_else(|| {
|
|
MapperError::Decoding(
|
|
DecodingError::new(
|
|
format!("Unknown channel value: {:#04x}", value)
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
pub fn input_channel_to_raw(channel: types::InputChannel) -> Result<u8, MapperError> {
|
|
Ok(channel_to_raw_value(types::Channel::Input(channel)))
|
|
}
|
|
|
|
pub fn output_channel_to_raw(channel: types::OutputChannel) -> Result<u8, MapperError> {
|
|
Ok(channel_to_raw_value(types::Channel::Output(channel)))
|
|
}
|
|
|
|
// ------ NAME ------
|
|
|
|
pub fn channel_name_to_raw(name: &str) -> Result<Vec<u8>, MapperError> {
|
|
validate_ascii_padded(
|
|
name.as_bytes(),
|
|
protocol_constants::CHANNEL_NAME_LENGTH,
|
|
protocol_constants::CHANNEL_NAME_PADDING,
|
|
"Channel name",
|
|
)?;
|
|
|
|
Ok(encode_ascii(name, protocol_constants::CHANNEL_NAME_LENGTH, protocol_constants::CHANNEL_NAME_PADDING)?)
|
|
}
|
|
|
|
pub fn raw_to_channel_name(raw_name: &[u8]) -> Result<String, MapperError> {
|
|
validators::validate_raw_channel_name(raw_name)?;
|
|
|
|
Ok(decode_ascii(raw_name, true)?)
|
|
}
|
|
|
|
// ------ ATTACK ------
|
|
|
|
pub fn attack_ms_to_raw(value: u16) -> Result<u16, MapperError> {
|
|
validate_range(
|
|
value,
|
|
mapper_constants::ATTACK_MS_RANGE,
|
|
|| format!("Invalid attack: {:#06x}", value),
|
|
)?;
|
|
|
|
Ok(value - 1)
|
|
}
|
|
|
|
pub fn raw_to_attack_ms(value: u16) -> Result<u16, MapperError> {
|
|
validators::validate_raw_attack(value)?;
|
|
|
|
Ok(value + 1)
|
|
}
|
|
|
|
// ------ RELEASE ------
|
|
|
|
pub fn release_ms_to_raw(value: u16) -> Result<u16, MapperError> {
|
|
validate_range(
|
|
value,
|
|
mapper_constants::RELEASE_MS_RANGE,
|
|
|| format!("Invalid release: {:#06x}", value),
|
|
)?;
|
|
|
|
Ok(value - 1)
|
|
}
|
|
|
|
pub fn raw_to_release_ms(value: u16) -> Result<u16, MapperError> {
|
|
validators::validate_raw_release(value)?;
|
|
|
|
Ok(value + 1)
|
|
}
|
|
|
|
// ------ HOLD ------
|
|
|
|
pub fn hold_ms_to_raw(hold: u16) -> Result<u16, MapperError> {
|
|
validate_range(
|
|
hold,
|
|
mapper_constants::HOLD_MS_RANGE,
|
|
|| format!("Invalid hold: {:#06x}", hold),
|
|
)?;
|
|
|
|
Ok(hold - 1)
|
|
}
|
|
|
|
pub fn raw_to_hold_ms(hold: u16) -> Result<u16, MapperError> {
|
|
validators::validate_raw_hold(hold)?;
|
|
|
|
Ok(hold + 1)
|
|
}
|
|
|
|
// ------ GATE THRESHOLD ------
|
|
|
|
pub fn gate_threshold_db_to_raw(db: f32) -> Result<u16, MapperError> {
|
|
validate_range(
|
|
db,
|
|
mapper_constants::GATE_THRESHOLD_DB_RANGE,
|
|
|| format!("Invalid gate threshold: {db}"),
|
|
)?;
|
|
|
|
let db = (db * 2.0).round() / 2.0;
|
|
|
|
Ok(((db - *mapper_constants::GATE_THRESHOLD_DB_RANGE.start()) * 2.0).round() as u16)
|
|
}
|
|
|
|
pub fn raw_to_gate_threshold_db(value: u16) -> Result<f32, MapperError> {
|
|
validators::validate_raw_gate_threshold(value)?;
|
|
|
|
Ok(*mapper_constants::GATE_THRESHOLD_DB_RANGE.start() + value as f32 / 2.0)
|
|
}
|
|
|
|
// ------ EQ GAIN ------
|
|
|
|
fn eq_gain_raw_steps() -> f32 {
|
|
(*protocol_constants::EQ_GAIN_RAW_RANGE.end()
|
|
- *protocol_constants::EQ_GAIN_RAW_RANGE.start()) as f32
|
|
}
|
|
|
|
fn eq_gain_step_db() -> f32 {
|
|
(*mapper_constants::EQ_GAIN_DB_RANGE.end()
|
|
- *mapper_constants::EQ_GAIN_DB_RANGE.start())
|
|
/ eq_gain_raw_steps()
|
|
}
|
|
|
|
fn eq_quantize(db: f32) -> f32 {
|
|
(db / eq_gain_step_db()).round() * eq_gain_step_db()
|
|
}
|
|
|
|
pub fn eq_gain_db_to_raw(gain: f32) -> Result<u16, MapperError> {
|
|
validate_range(
|
|
gain,
|
|
mapper_constants::EQ_GAIN_DB_RANGE,
|
|
|| format!("Invalid EQ gain: {gain}"),
|
|
)?;
|
|
|
|
let gain = eq_quantize(gain);
|
|
|
|
Ok(
|
|
*protocol_constants::EQ_GAIN_RAW_RANGE.start()
|
|
+ ((gain - *mapper_constants::EQ_GAIN_DB_RANGE.start())
|
|
/ eq_gain_step_db())
|
|
.round() as u16
|
|
)
|
|
}
|
|
|
|
pub fn raw_to_eq_gain(value: u16) -> Result<f32, MapperError> {
|
|
validators::validate_raw_eq_gain(value)?;
|
|
|
|
let gain =
|
|
*mapper_constants::EQ_GAIN_DB_RANGE.start()
|
|
+ (value - *protocol_constants::EQ_GAIN_RAW_RANGE.start()) as f32
|
|
* eq_gain_step_db();
|
|
|
|
Ok(eq_quantize(gain))
|
|
}
|
|
|
|
// ------ PEQ BAND ------
|
|
|
|
fn peq_band_to_raw_value(band: types::PEQBand) -> u8 {
|
|
match band {
|
|
types::PEQBand::B1 => 0,
|
|
types::PEQBand::B2 => 1,
|
|
types::PEQBand::B3 => 2,
|
|
types::PEQBand::B4 => 3,
|
|
types::PEQBand::B5 => 4,
|
|
types::PEQBand::B6 => 5,
|
|
types::PEQBand::B7 => 6,
|
|
types::PEQBand::B8 => 7,
|
|
types::PEQBand::B9 => 8,
|
|
}
|
|
}
|
|
|
|
pub fn peq_band_to_raw(band: types::PEQBand, channel: types::Channel) -> Result<u8, MapperError> {
|
|
match channel {
|
|
types::Channel::Input(_) => {
|
|
if matches!(band, types::PEQBand::B9) {
|
|
return Err(MapperError::Validation(ValidationError::Value(
|
|
"PEQ Band B9 is not available on input channels".to_string()
|
|
)));
|
|
}
|
|
}
|
|
|
|
types::Channel::Output(_) => {
|
|
// All PEQ bands are allowed
|
|
}
|
|
}
|
|
|
|
Ok(peq_band_to_raw_value(band))
|
|
}
|
|
|
|
// ------ GAIN ------
|
|
|
|
fn gain_split_raw() -> u16 {
|
|
((mapper_constants::GAIN_SPLIT_DB - mapper_constants::GAIN_DB_RANGE.start()) / mapper_constants::GAIN_LOW_STEP_DB).round() as u16
|
|
}
|
|
|
|
fn quantize_gain(db: f32) -> f32 {
|
|
if db < mapper_constants::GAIN_SPLIT_DB {
|
|
(db / mapper_constants::GAIN_LOW_STEP_DB).round() * mapper_constants::GAIN_LOW_STEP_DB
|
|
} else {
|
|
(db / mapper_constants::GAIN_HIGH_STEP_DB).round() * mapper_constants::GAIN_HIGH_STEP_DB
|
|
}
|
|
}
|
|
|
|
pub fn gain_db_to_raw(gain: f32) -> Result<u16, MapperError> {
|
|
validate_range(
|
|
gain,
|
|
mapper_constants::GAIN_DB_RANGE,
|
|
|| format!("Invalid channel gain: {gain}"),
|
|
)?;
|
|
|
|
let gain = quantize_gain(gain);
|
|
|
|
if gain < mapper_constants::GAIN_SPLIT_DB {
|
|
Ok(((gain - mapper_constants::GAIN_DB_RANGE.start()) / mapper_constants::GAIN_LOW_STEP_DB).round() as u16)
|
|
} else {
|
|
Ok(gain_split_raw() + ((gain - mapper_constants::GAIN_SPLIT_DB) / mapper_constants::GAIN_HIGH_STEP_DB).round() as u16)
|
|
}
|
|
}
|
|
|
|
pub fn raw_to_gain(raw_gain: u16) -> Result<f32, MapperError> {
|
|
validators::validate_raw_gain(raw_gain)?;
|
|
|
|
if raw_gain < gain_split_raw() {
|
|
Ok(mapper_constants::GAIN_DB_RANGE.start() + raw_gain as f32 * mapper_constants::GAIN_LOW_STEP_DB)
|
|
} else {
|
|
Ok(mapper_constants::GAIN_SPLIT_DB + (raw_gain - gain_split_raw()) as f32 * mapper_constants::GAIN_HIGH_STEP_DB)
|
|
}
|
|
}
|
|
|
|
// ------ CONTIGUOUS FREQUENCY ------
|
|
|
|
pub fn frequency_hz_to_raw(hz: f32) -> Result<u16, MapperError> {
|
|
|
|
validate_range(
|
|
hz,
|
|
mapper_constants::FREQUENCY_HZ_RANGE,
|
|
|| format!("Invalid frequency: {hz}"),
|
|
)?;
|
|
|
|
let device_steps = (protocol_constants::FREQUENCY_RAW_RANGE.end() - protocol_constants::FREQUENCY_RAW_RANGE.start()) as f32;
|
|
|
|
let raw = ((hz / mapper_constants::FREQUENCY_HZ_RANGE.start()).ln() / (mapper_constants::FREQUENCY_HZ_RANGE.end() / mapper_constants::FREQUENCY_HZ_RANGE.start()).ln()
|
|
* device_steps)
|
|
.round();
|
|
|
|
Ok(protocol_constants::FREQUENCY_RAW_RANGE.start() + raw as u16)
|
|
}
|
|
|
|
pub fn raw_to_frequency_hz(value: u16) -> Result<f32, MapperError> {
|
|
validators::validate_raw_frequency(value)?;
|
|
|
|
let device_steps = (protocol_constants::FREQUENCY_RAW_RANGE.end() - protocol_constants::FREQUENCY_RAW_RANGE.start()) as f32;
|
|
let normalized = (value - protocol_constants::FREQUENCY_RAW_RANGE.start()) as f32 / device_steps;
|
|
|
|
Ok(mapper_constants::FREQUENCY_HZ_RANGE.start() * (mapper_constants::FREQUENCY_HZ_RANGE.end() / mapper_constants::FREQUENCY_HZ_RANGE.start()).powf(normalized))
|
|
}
|
|
|
|
// ------ DISCRETE FREQUENCY ------
|
|
|
|
fn discrete_frequency_to_raw_value(frequency: types::DiscreteFrequency) -> u8 {
|
|
match frequency {
|
|
types::DiscreteFrequency::F20 => 0,
|
|
types::DiscreteFrequency::F25 => 1,
|
|
types::DiscreteFrequency::F31_5 => 2,
|
|
types::DiscreteFrequency::F40 => 3,
|
|
types::DiscreteFrequency::F50 => 4,
|
|
types::DiscreteFrequency::F63 => 5,
|
|
types::DiscreteFrequency::F80 => 6,
|
|
types::DiscreteFrequency::F100 => 7,
|
|
types::DiscreteFrequency::F125 => 8,
|
|
types::DiscreteFrequency::F160 => 9,
|
|
types::DiscreteFrequency::F200 => 10,
|
|
types::DiscreteFrequency::F250 => 11,
|
|
types::DiscreteFrequency::F315 => 12,
|
|
types::DiscreteFrequency::F400 => 13,
|
|
types::DiscreteFrequency::F500 => 14,
|
|
types::DiscreteFrequency::F630 => 15,
|
|
types::DiscreteFrequency::F800 => 16,
|
|
types::DiscreteFrequency::F1000 => 17,
|
|
types::DiscreteFrequency::F1250 => 18,
|
|
types::DiscreteFrequency::F1600 => 19,
|
|
types::DiscreteFrequency::F2000 => 20,
|
|
types::DiscreteFrequency::F2500 => 21,
|
|
types::DiscreteFrequency::F3150 => 22,
|
|
types::DiscreteFrequency::F4000 => 23,
|
|
types::DiscreteFrequency::F5000 => 24,
|
|
types::DiscreteFrequency::F6300 => 25,
|
|
types::DiscreteFrequency::F8000 => 26,
|
|
types::DiscreteFrequency::F10000 => 27,
|
|
types::DiscreteFrequency::F12500 => 28,
|
|
types::DiscreteFrequency::F16000 => 29,
|
|
types::DiscreteFrequency::F20000 => 30,
|
|
}
|
|
}
|
|
|
|
fn raw_to_discrete_frequency_value(raw: u8) -> Option<types::DiscreteFrequency> {
|
|
Some(match raw {
|
|
0 => types::DiscreteFrequency::F20,
|
|
1 => types::DiscreteFrequency::F25,
|
|
2 => types::DiscreteFrequency::F31_5,
|
|
3 => types::DiscreteFrequency::F40,
|
|
4 => types::DiscreteFrequency::F50,
|
|
5 => types::DiscreteFrequency::F63,
|
|
6 => types::DiscreteFrequency::F80,
|
|
7 => types::DiscreteFrequency::F100,
|
|
8 => types::DiscreteFrequency::F125,
|
|
9 => types::DiscreteFrequency::F160,
|
|
10 => types::DiscreteFrequency::F200,
|
|
11 => types::DiscreteFrequency::F250,
|
|
12 => types::DiscreteFrequency::F315,
|
|
13 => types::DiscreteFrequency::F400,
|
|
14 => types::DiscreteFrequency::F500,
|
|
15 => types::DiscreteFrequency::F630,
|
|
16 => types::DiscreteFrequency::F800,
|
|
17 => types::DiscreteFrequency::F1000,
|
|
18 => types::DiscreteFrequency::F1250,
|
|
19 => types::DiscreteFrequency::F1600,
|
|
20 => types::DiscreteFrequency::F2000,
|
|
21 => types::DiscreteFrequency::F2500,
|
|
22 => types::DiscreteFrequency::F3150,
|
|
23 => types::DiscreteFrequency::F4000,
|
|
24 => types::DiscreteFrequency::F5000,
|
|
25 => types::DiscreteFrequency::F6300,
|
|
26 => types::DiscreteFrequency::F8000,
|
|
27 => types::DiscreteFrequency::F10000,
|
|
28 => types::DiscreteFrequency::F12500,
|
|
29 => types::DiscreteFrequency::F16000,
|
|
30 => types::DiscreteFrequency::F20000,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
pub fn frequency_value_to_raw(frequency: types::DiscreteFrequency) -> Result<u8, MapperError> {
|
|
|
|
Ok(discrete_frequency_to_raw_value(frequency))
|
|
}
|
|
|
|
pub fn raw_to_frequency_value(frequency: u8) -> Result<types::DiscreteFrequency, MapperError> {
|
|
validators::validate_raw_discrete_frequency(frequency)?;
|
|
|
|
raw_to_discrete_frequency_value(frequency)
|
|
.ok_or_else(|| {
|
|
MapperError::Decoding(
|
|
DecodingError::new(
|
|
format!("Unknown frequency value: {:#04x}", frequency)
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
// ------ CROSSOVER ------
|
|
|
|
fn crossover_filter_to_raw_value(filter: types::CrossoverFilter) -> u8 {
|
|
match filter {
|
|
types::CrossoverFilter::Bypass => 0,
|
|
types::CrossoverFilter::Bw6 => 1,
|
|
types::CrossoverFilter::Bl6 => 2,
|
|
types::CrossoverFilter::Bw12 => 3,
|
|
types::CrossoverFilter::Bl12 => 4,
|
|
types::CrossoverFilter::Lk12 => 5,
|
|
types::CrossoverFilter::Bw18 => 6,
|
|
types::CrossoverFilter::Bl18 => 7,
|
|
types::CrossoverFilter::Bw24 => 8,
|
|
types::CrossoverFilter::Bl24 => 9,
|
|
types::CrossoverFilter::Lk24 => 10,
|
|
types::CrossoverFilter::Bw30 => 11,
|
|
types::CrossoverFilter::Bl30 => 12,
|
|
types::CrossoverFilter::Bw36 => 13,
|
|
types::CrossoverFilter::Bl36 => 14,
|
|
types::CrossoverFilter::Lk36 => 15,
|
|
types::CrossoverFilter::Bw42 => 16,
|
|
types::CrossoverFilter::Bl42 => 17,
|
|
types::CrossoverFilter::Bw48 => 18,
|
|
types::CrossoverFilter::Bl48 => 19,
|
|
types::CrossoverFilter::Lk48 => 20,
|
|
}
|
|
}
|
|
|
|
fn raw_to_crossover_filter_value(raw: u8) -> Option<types::CrossoverFilter> {
|
|
Some(match raw {
|
|
0 => types::CrossoverFilter::Bypass,
|
|
1 => types::CrossoverFilter::Bw6,
|
|
2 => types::CrossoverFilter::Bl6,
|
|
3 => types::CrossoverFilter::Bw12,
|
|
4 => types::CrossoverFilter::Bl12,
|
|
5 => types::CrossoverFilter::Lk12,
|
|
6 => types::CrossoverFilter::Bw18,
|
|
7 => types::CrossoverFilter::Bl18,
|
|
8 => types::CrossoverFilter::Bw24,
|
|
9 => types::CrossoverFilter::Bl24,
|
|
10 => types::CrossoverFilter::Lk24,
|
|
11 => types::CrossoverFilter::Bw30,
|
|
12 => types::CrossoverFilter::Bl30,
|
|
13 => types::CrossoverFilter::Bw36,
|
|
14 => types::CrossoverFilter::Bl36,
|
|
15 => types::CrossoverFilter::Lk36,
|
|
16 => types::CrossoverFilter::Bw42,
|
|
17 => types::CrossoverFilter::Bl42,
|
|
18 => types::CrossoverFilter::Bw48,
|
|
19 => types::CrossoverFilter::Bl48,
|
|
20 => types::CrossoverFilter::Lk48,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
pub fn crossover_filter_to_raw(filter: types::CrossoverFilter) -> Result<u8, MapperError> {
|
|
Ok(crossover_filter_to_raw_value(filter))
|
|
}
|
|
|
|
pub fn raw_to_crossover_filter(raw: u8) -> Result<types::CrossoverFilter, MapperError> {
|
|
validators::validate_raw_crossover_filter(raw)?;
|
|
|
|
raw_to_crossover_filter_value(raw)
|
|
.ok_or_else(|| {
|
|
MapperError::Decoding(
|
|
DecodingError::new(
|
|
format!("Unknown crossover filter value: {:#04x}", raw)
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
// ------ PEQ Q ------
|
|
|
|
pub fn peq_q_to_raw(q: f32, filter: types::PEQFilter) -> Result<u8, MapperError> {
|
|
validate_range(
|
|
q,
|
|
mapper_constants::peq_q_range(filter),
|
|
|| format!("Invalid PEQ Q: {q}"),
|
|
)?;
|
|
|
|
let raw_range = (protocol_constants::PEQ_Q_RAW_RANGE.end() - protocol_constants::PEQ_Q_RAW_RANGE.start()) as f32;
|
|
|
|
Ok(((q / mapper_constants::PEQ_Q_RANGE.start()).ln() / (mapper_constants::PEQ_Q_RANGE.end() / mapper_constants::PEQ_Q_RANGE.start()).ln() * raw_range).round() as u8
|
|
+ protocol_constants::PEQ_Q_RAW_RANGE.start())
|
|
}
|
|
|
|
pub fn raw_to_peq_q(value: u8) -> Result<f32, MapperError> {
|
|
validators::validate_raw_peq_q(value)?;
|
|
|
|
let raw_range = (protocol_constants::PEQ_Q_RAW_RANGE.end() - protocol_constants::PEQ_Q_RAW_RANGE.start()) as f32;
|
|
let normalized = (value - protocol_constants::PEQ_Q_RAW_RANGE.start()) as f32 / raw_range;
|
|
|
|
Ok(mapper_constants::PEQ_Q_RANGE.start() * (mapper_constants::PEQ_Q_RANGE.end() / mapper_constants::PEQ_Q_RANGE.start()).powf(normalized))
|
|
}
|
|
|
|
// ------ FILTER ------
|
|
|
|
fn peq_filter_to_raw_value(filter: types::PEQFilter) -> u8 {
|
|
match filter {
|
|
types::PEQFilter::Peak => 0,
|
|
types::PEQFilter::LowShelf => 1,
|
|
types::PEQFilter::HighShelf => 2,
|
|
types::PEQFilter::Lp6Db => 3,
|
|
types::PEQFilter::Lp12Db => 4,
|
|
types::PEQFilter::Hp6Db => 5,
|
|
types::PEQFilter::Hp12Db => 6,
|
|
types::PEQFilter::AllPass1 => 7,
|
|
types::PEQFilter::AllPass2 => 8,
|
|
}
|
|
}
|
|
|
|
fn raw_to_peq_filter_value(value: u8) -> Option<types::PEQFilter> {
|
|
Some(match value {
|
|
0 => types::PEQFilter::Peak,
|
|
1 => types::PEQFilter::LowShelf,
|
|
2 => types::PEQFilter::HighShelf,
|
|
3 => types::PEQFilter::Lp6Db,
|
|
4 => types::PEQFilter::Lp12Db,
|
|
5 => types::PEQFilter::Hp6Db,
|
|
6 => types::PEQFilter::Hp12Db,
|
|
7 => types::PEQFilter::AllPass1,
|
|
8 => types::PEQFilter::AllPass2,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
pub fn peq_filter_to_raw(filter: types::PEQFilter) -> Result<u8, MapperError> {
|
|
Ok(peq_filter_to_raw_value(filter))
|
|
}
|
|
|
|
pub fn raw_to_peq_filter(value: u8) -> Result<types::PEQFilter, MapperError> {
|
|
validators::validate_raw_peq_filter(value)?;
|
|
|
|
raw_to_peq_filter_value(value)
|
|
.ok_or_else(|| {
|
|
MapperError::Decoding(
|
|
DecodingError::new(
|
|
format!("Unknown PEQ filter value: {:#04x}", value)
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
// ------ RATIO ------
|
|
|
|
fn ratio_to_raw_value(ratio: types::Ratio) -> u16 {
|
|
match ratio {
|
|
types::Ratio::Ratio1_1 => 0,
|
|
types::Ratio::Ratio1_1_1 => 1,
|
|
types::Ratio::Ratio1_1_3 => 2,
|
|
types::Ratio::Ratio1_1_5 => 3,
|
|
types::Ratio::Ratio1_1_7 => 4,
|
|
types::Ratio::Ratio1_2 => 5,
|
|
types::Ratio::Ratio1_2_5 => 6,
|
|
types::Ratio::Ratio1_3 => 7,
|
|
types::Ratio::Ratio1_3_5 => 8,
|
|
types::Ratio::Ratio1_4 => 9,
|
|
types::Ratio::Ratio1_5 => 10,
|
|
types::Ratio::Ratio1_6 => 11,
|
|
types::Ratio::Ratio1_8 => 12,
|
|
types::Ratio::Ratio1_10 => 13,
|
|
types::Ratio::Ratio1_20 => 14,
|
|
types::Ratio::Limit => 15,
|
|
}
|
|
}
|
|
|
|
fn raw_to_ratio_value(value: u16) -> Option<types::Ratio> {
|
|
Some(match value {
|
|
0 => types::Ratio::Ratio1_1,
|
|
1 => types::Ratio::Ratio1_1_1,
|
|
2 => types::Ratio::Ratio1_1_3,
|
|
3 => types::Ratio::Ratio1_1_5,
|
|
4 => types::Ratio::Ratio1_1_7,
|
|
5 => types::Ratio::Ratio1_2,
|
|
6 => types::Ratio::Ratio1_2_5,
|
|
7 => types::Ratio::Ratio1_3,
|
|
8 => types::Ratio::Ratio1_3_5,
|
|
9 => types::Ratio::Ratio1_4,
|
|
10 => types::Ratio::Ratio1_5,
|
|
11 => types::Ratio::Ratio1_6,
|
|
12 => types::Ratio::Ratio1_8,
|
|
13 => types::Ratio::Ratio1_10,
|
|
14 => types::Ratio::Ratio1_20,
|
|
15 => types::Ratio::Limit,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
pub fn ratio_to_raw(ratio: types::Ratio) -> Result<u16, MapperError> {
|
|
Ok(ratio_to_raw_value(ratio))
|
|
}
|
|
|
|
pub fn raw_to_ratio(value: u16) -> Result<types::Ratio, MapperError> {
|
|
validators::validate_raw_compressor_ratio(value)?;
|
|
|
|
raw_to_ratio_value(value)
|
|
.ok_or_else(|| {
|
|
MapperError::Decoding(
|
|
DecodingError::new(
|
|
format!("Unknown ratio value: {:#04x}", value)
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
// ------ KNEE ------
|
|
|
|
pub fn knee_db_to_raw(db: u16) -> Result<u16, MapperError> {
|
|
validate_range(
|
|
db,
|
|
mapper_constants::KNEE_DB_RANGE,
|
|
|| format!("Invalid compressor knee: {db}"),
|
|
)?;
|
|
|
|
Ok(db)
|
|
}
|
|
|
|
pub fn raw_to_knee_db(value: u16) -> Result<u16, MapperError> {
|
|
validators::validate_raw_compressor_knee(value)?;
|
|
Ok(value)
|
|
}
|
|
|
|
// ------ THRESHOLD ------
|
|
|
|
pub fn threshold_db_to_raw(db: f32) -> Result<u16, MapperError> {
|
|
validate_range(
|
|
db,
|
|
mapper_constants::THRESHOLD_DB_RANGE,
|
|
|| format!("Invalid compressor threshold: {db}"),
|
|
)?;
|
|
|
|
let db = (db * 2.0).round() / 2.0;
|
|
|
|
Ok(((db - mapper_constants::THRESHOLD_DB_RANGE.start()) * 2.0).round() as u16)
|
|
}
|
|
|
|
pub fn raw_to_threshold_db(value: u16) -> Result<f32, MapperError> {
|
|
validators::validate_raw_threshold(value)?;
|
|
Ok(mapper_constants::THRESHOLD_DB_RANGE.start() + (value as f32 / 2.0))
|
|
}
|
|
|
|
// ------ LINK ------
|
|
|
|
pub fn link_channels_to_raw(
|
|
source: types::Channel,
|
|
destinations: &[types::Channel],
|
|
) -> Result<u8, MapperError> {
|
|
|
|
let mut mask = 0u8;
|
|
|
|
match source {
|
|
types::Channel::Input(source_channel) => {
|
|
for destination in destinations {
|
|
let types::Channel::Input(destination_channel) = destination else {
|
|
return Err(MapperError::Validation(
|
|
ValidationError::value(
|
|
"Input channel cannot link to output channel"
|
|
),
|
|
));
|
|
};
|
|
|
|
if *destination_channel < source_channel {
|
|
return Err(MapperError::Validation(
|
|
ValidationError::value(format!(
|
|
"{:?} cannot link to previous channel {:?}",
|
|
source_channel,
|
|
destination_channel
|
|
)),
|
|
));
|
|
}
|
|
|
|
mask |= 1 << (*destination_channel as u8);
|
|
}
|
|
}
|
|
|
|
types::Channel::Output(source_channel) => {
|
|
for destination in destinations {
|
|
let types::Channel::Output(destination_channel) = destination else {
|
|
return Err(MapperError::Validation(
|
|
ValidationError::value(
|
|
"Output channel cannot link to input channel"
|
|
),
|
|
));
|
|
};
|
|
|
|
if *destination_channel < source_channel {
|
|
return Err(MapperError::Validation(
|
|
ValidationError::value(format!(
|
|
"{:?} cannot link to previous channel {:?}",
|
|
source_channel,
|
|
destination_channel
|
|
)),
|
|
));
|
|
}
|
|
|
|
mask |= 1 << (*destination_channel as u8);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(mask)
|
|
}
|
|
|
|
pub fn raw_to_link_channels(source_raw: u8, destination_mask: u8) -> Result<Vec<types::Channel>, MapperError> {
|
|
validators::validate_raw_link_channel(source_raw, destination_mask)?;
|
|
|
|
let source = raw_to_channel(source_raw)?;
|
|
|
|
let mut destinations = Vec::new();
|
|
|
|
match source {
|
|
types::Channel::Input(_) => {
|
|
for (index, channel) in types::InputChannel::iter().enumerate() {
|
|
if destination_mask & (1 << index) != 0 {
|
|
destinations.push(types::Channel::Input(channel));
|
|
}
|
|
}
|
|
}
|
|
|
|
types::Channel::Output(_) => {
|
|
for (index, channel) in types::OutputChannel::iter().enumerate() {
|
|
if destination_mask & (1 << index) != 0 {
|
|
destinations.push(types::Channel::Output(channel));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(destinations)
|
|
}
|
|
|
|
// ------ MATRIX GAIN ------
|
|
|
|
fn matrix_gain_split_raw() -> u16 {
|
|
((mapper_constants::GAIN_SPLIT_DB - mapper_constants::MATRIX_GAIN_DB_RANGE.start()) / mapper_constants::GAIN_LOW_STEP_DB).round() as u16
|
|
}
|
|
|
|
fn quantize_matrix_gain(db: f32) -> f32 {
|
|
if db < mapper_constants::GAIN_SPLIT_DB {
|
|
(db / mapper_constants::GAIN_LOW_STEP_DB).round() * mapper_constants::GAIN_LOW_STEP_DB
|
|
} else {
|
|
(db / mapper_constants::GAIN_HIGH_STEP_DB).round() * mapper_constants::GAIN_HIGH_STEP_DB
|
|
}
|
|
}
|
|
|
|
pub fn matrix_gain_db_to_raw(gain: f32) -> Result<u16, MapperError> {
|
|
validate_range(
|
|
gain,
|
|
mapper_constants::MATRIX_GAIN_DB_RANGE,
|
|
|| format!("Invalid matrix gain: {gain}"),
|
|
)?;
|
|
|
|
let gain = quantize_matrix_gain(gain);
|
|
|
|
if gain < mapper_constants::GAIN_SPLIT_DB {
|
|
Ok(((gain - mapper_constants::MATRIX_GAIN_DB_RANGE.start()) / mapper_constants::GAIN_LOW_STEP_DB).round() as u16)
|
|
} else {
|
|
Ok(matrix_gain_split_raw()
|
|
+ ((gain - mapper_constants::GAIN_SPLIT_DB) / mapper_constants::GAIN_HIGH_STEP_DB).round() as u16)
|
|
}
|
|
}
|
|
|
|
pub fn raw_to_matrix_gain(raw_gain: u16) -> Result<f32, MapperError> {
|
|
validators::validate_raw_matrix_gain(raw_gain)?;
|
|
|
|
if raw_gain < matrix_gain_split_raw() {
|
|
Ok(mapper_constants::MATRIX_GAIN_DB_RANGE.start() + raw_gain as f32 * mapper_constants::GAIN_LOW_STEP_DB)
|
|
} else {
|
|
Ok(mapper_constants::GAIN_SPLIT_DB + (raw_gain - matrix_gain_split_raw()) as f32 * mapper_constants::GAIN_HIGH_STEP_DB)
|
|
}
|
|
}
|
|
|
|
// ------ MATRIX ------
|
|
|
|
fn matrix_input_mask(channel: types::InputChannel) -> u8 {
|
|
match channel {
|
|
types::InputChannel::InA => protocol_constants::MATRIX_INPUT_A_MASK,
|
|
types::InputChannel::InB => protocol_constants::MATRIX_INPUT_B_MASK,
|
|
types::InputChannel::InC => protocol_constants::MATRIX_INPUT_C_MASK,
|
|
types::InputChannel::InD => protocol_constants::MATRIX_INPUT_D_MASK,
|
|
}
|
|
}
|
|
|
|
pub fn matrix_to_raw(input_channels: &[types::InputChannel]) -> Result<u8, MapperError> {
|
|
let mut bitmask = 0x00;
|
|
|
|
for &channel in input_channels {
|
|
bitmask |= matrix_input_mask(channel);
|
|
}
|
|
|
|
Ok(bitmask)
|
|
}
|
|
|
|
pub fn raw_to_matrix(raw: u8) -> Result<Vec<types::InputChannel>, MapperError> {
|
|
validators::validate_raw_matrix_bitmask(raw)?;
|
|
|
|
Ok([types::InputChannel::InA, types::InputChannel::InB, types::InputChannel::InC, types::InputChannel::InD]
|
|
.into_iter()
|
|
.filter(|&c| raw & matrix_input_mask(c) != 0)
|
|
.collect())
|
|
}
|
|
|
|
// ------ DELAY ------
|
|
|
|
fn delay_raw_steps() -> f32 {
|
|
(protocol_constants::DELAY_RAW_RANGE.end() - protocol_constants::DELAY_RAW_RANGE.start()) as f32
|
|
}
|
|
|
|
pub fn delay_to_raw(delay: f32, unit: types::DelayUnit) -> Result<u8, MapperError> {
|
|
|
|
let (delay_min, delay_max) = mapper_constants::delay_range(unit);
|
|
|
|
validate_range(
|
|
delay,
|
|
delay_min..=delay_max,
|
|
|| format!("Invalid channel delay: {delay}")
|
|
)?;
|
|
|
|
let raw = *protocol_constants::DELAY_RAW_RANGE.start() as f32
|
|
+ ((delay - delay_min) / (delay_max - delay_min) * delay_raw_steps()).round();
|
|
|
|
Ok(raw as u8)
|
|
}
|
|
|
|
pub fn raw_to_delay(value: u8, unit: types::DelayUnit) -> Result<f32, MapperError> {
|
|
validators::validate_raw_channel_delay(value)?;
|
|
|
|
let (delay_min, delay_max) = mapper_constants::delay_range(unit);
|
|
|
|
Ok(delay_min + ((value - protocol_constants::DELAY_RAW_RANGE.start()) as f32 / delay_raw_steps()) * (delay_max - delay_min))
|
|
}
|
|
|
|
// ------ COPY CHANNEL ------
|
|
|
|
pub fn copy_channel_to_raw(source: types::Channel, destination: types::Channel) -> Result<(u8, u8), MapperError> {
|
|
if source == destination {
|
|
return Err(ValidationError::Value("Source channel and destination channel can't be the same".to_string()).into())
|
|
}
|
|
|
|
match (&source, &destination) {
|
|
(types::Channel::Input(_), types::Channel::Output(_))
|
|
| (types::Channel::Output(_), types::Channel::Input(_)) => {
|
|
return Err(ValidationError::Value(
|
|
"Channels need to be both inputs or outputs".to_string()).into(),
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
Ok((channel_to_raw_value(source), channel_to_raw_value(destination)))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// ------------------------------------------------------------------
|
|
// CHANNEL
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn channel_round_trip_all_inputs() {
|
|
let channels = [
|
|
types::InputChannel::InA,
|
|
types::InputChannel::InB,
|
|
types::InputChannel::InC,
|
|
types::InputChannel::InD,
|
|
];
|
|
for ch in channels {
|
|
let raw = input_channel_to_raw(ch).unwrap();
|
|
let decoded = raw_to_channel(raw).unwrap();
|
|
assert_eq!(decoded, types::Channel::Input(ch));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn channel_round_trip_all_outputs() {
|
|
let channels = [
|
|
types::OutputChannel::Out1,
|
|
types::OutputChannel::Out2,
|
|
types::OutputChannel::Out3,
|
|
types::OutputChannel::Out4,
|
|
types::OutputChannel::Out5,
|
|
types::OutputChannel::Out6,
|
|
types::OutputChannel::Out7,
|
|
types::OutputChannel::Out8,
|
|
];
|
|
for ch in channels {
|
|
let raw = output_channel_to_raw(ch).unwrap();
|
|
let decoded = raw_to_channel(raw).unwrap();
|
|
assert_eq!(decoded, types::Channel::Output(ch));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn channel_specific_raw_values() {
|
|
assert_eq!(channel_to_raw(types::Channel::Input(types::InputChannel::InA)).unwrap(), 0x00);
|
|
assert_eq!(channel_to_raw(types::Channel::Input(types::InputChannel::InD)).unwrap(), 0x03);
|
|
assert_eq!(channel_to_raw(types::Channel::Output(types::OutputChannel::Out1)).unwrap(), 0x04);
|
|
assert_eq!(channel_to_raw(types::Channel::Output(types::OutputChannel::Out8)).unwrap(), 0x0B);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// NAME
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn channel_name_round_trip() {
|
|
let name = "In ABCDE";
|
|
let raw = channel_name_to_raw(name).unwrap();
|
|
assert_eq!(raw.len(), protocol_constants::CHANNEL_NAME_LENGTH);
|
|
let decoded = raw_to_channel_name(&raw).unwrap();
|
|
assert_eq!(decoded, name);
|
|
}
|
|
|
|
#[test]
|
|
fn channel_name_padding_is_applied() {
|
|
let raw = channel_name_to_raw(" Kick ").unwrap();
|
|
assert_eq!(raw.len(), protocol_constants::CHANNEL_NAME_LENGTH);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// ATTACK / RELEASE / HOLD (simple offset-by-one mappings)
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn attack_ms_mapping() {
|
|
assert_eq!(attack_ms_to_raw(1).unwrap(), 0);
|
|
assert_eq!(attack_ms_to_raw(999).unwrap(), 998);
|
|
assert_eq!(raw_to_attack_ms(0).unwrap(), 1);
|
|
assert_eq!(raw_to_attack_ms(998).unwrap(), 999);
|
|
}
|
|
|
|
#[test]
|
|
fn attack_ms_round_trip() {
|
|
for ms in [1u16, 50, 500, 999] {
|
|
let raw = attack_ms_to_raw(ms).unwrap();
|
|
assert_eq!(raw_to_attack_ms(raw).unwrap(), ms);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn release_ms_mapping() {
|
|
assert_eq!(release_ms_to_raw(10).unwrap(), 9);
|
|
assert_eq!(release_ms_to_raw(3000).unwrap(), 2999);
|
|
assert_eq!(raw_to_release_ms(9).unwrap(), 10);
|
|
assert_eq!(raw_to_release_ms(2999).unwrap(), 3000);
|
|
}
|
|
|
|
#[test]
|
|
fn release_ms_round_trip() {
|
|
for ms in [10u16, 100, 1500, 3000] {
|
|
let raw = release_ms_to_raw(ms).unwrap();
|
|
assert_eq!(raw_to_release_ms(raw).unwrap(), ms);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hold_ms_mapping() {
|
|
assert_eq!(hold_ms_to_raw(10).unwrap(), 9);
|
|
assert_eq!(hold_ms_to_raw(999).unwrap(), 998);
|
|
assert_eq!(raw_to_hold_ms(9).unwrap(), 10);
|
|
assert_eq!(raw_to_hold_ms(998).unwrap(), 999);
|
|
}
|
|
|
|
#[test]
|
|
fn hold_ms_round_trip() {
|
|
for ms in [10u16, 250, 999] {
|
|
let raw = hold_ms_to_raw(ms).unwrap();
|
|
assert_eq!(raw_to_hold_ms(raw).unwrap(), ms);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// GATE THRESHOLD
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn gate_threshold_bounds() {
|
|
assert_eq!(gate_threshold_db_to_raw(-90.0).unwrap(), 0);
|
|
assert_eq!(gate_threshold_db_to_raw(0.0).unwrap(), 180); // (0 - -90) * 2
|
|
}
|
|
|
|
#[test]
|
|
fn gate_threshold_round_trip() {
|
|
for db in [-90.0f32, -45.5, -20.25, 0.0] {
|
|
let raw = gate_threshold_db_to_raw(db).unwrap();
|
|
let decoded = raw_to_gate_threshold_db(raw).unwrap();
|
|
assert!((decoded - (db * 2.0).round() / 2.0).abs() < 1e-4);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// EQ GAIN
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn eq_gain_round_trip_endpoints() {
|
|
for db in [-12.0f32, 0.0, 12.0] {
|
|
let raw = eq_gain_db_to_raw(db).unwrap();
|
|
let decoded = raw_to_eq_gain(raw).unwrap();
|
|
assert!((decoded - db).abs() < 1e-2);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eq_gain_round_trip_arbitrary() {
|
|
for db in [-7.3f32, 3.9, 11.99] {
|
|
let raw = eq_gain_db_to_raw(db).unwrap();
|
|
let decoded = raw_to_eq_gain(raw).unwrap();
|
|
// allow one quantization step of tolerance
|
|
assert!((decoded - db).abs() <= eq_gain_step_db() + 1e-4);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// PEQ BAND
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn peq_band_output_all_bands() {
|
|
let bands = [
|
|
types::PEQBand::B1, types::PEQBand::B2, types::PEQBand::B3,
|
|
types::PEQBand::B4, types::PEQBand::B5, types::PEQBand::B6,
|
|
types::PEQBand::B7, types::PEQBand::B8, types::PEQBand::B9,
|
|
];
|
|
let out_channel = types::Channel::Output(types::OutputChannel::Out1);
|
|
for (i, band) in bands.into_iter().enumerate() {
|
|
assert_eq!(peq_band_to_raw(band, out_channel).unwrap(), i as u8);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn peq_band_input_allowed_bands() {
|
|
let bands = [
|
|
types::PEQBand::B1, types::PEQBand::B2, types::PEQBand::B3,
|
|
types::PEQBand::B4, types::PEQBand::B5, types::PEQBand::B6,
|
|
types::PEQBand::B7, types::PEQBand::B8,
|
|
];
|
|
let in_channel = types::Channel::Input(types::InputChannel::InA);
|
|
for (i, band) in bands.into_iter().enumerate() {
|
|
assert_eq!(peq_band_to_raw(band, in_channel).unwrap(), i as u8);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// GAIN (channel gain)
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn gain_round_trip_low_range() {
|
|
for db in [-60.0f32, -40.0, -20.5] {
|
|
let raw = gain_db_to_raw(db).unwrap();
|
|
let decoded = raw_to_gain(raw).unwrap();
|
|
assert!((decoded - db).abs() < 1e-3);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn gain_round_trip_high_range() {
|
|
for db in [-19.9f32, 0.0, 12.0] {
|
|
let raw = gain_db_to_raw(db).unwrap();
|
|
let decoded = raw_to_gain(raw).unwrap();
|
|
assert!((decoded - db).abs() < 1e-3);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn gain_split_boundary_is_low_step() {
|
|
let below = gain_db_to_raw(-20.5).unwrap();
|
|
let above = gain_db_to_raw(-19.9).unwrap();
|
|
assert_eq!(raw_to_gain(below).unwrap(), -20.5);
|
|
assert!((raw_to_gain(above).unwrap() - (-19.9)).abs() < 1e-3);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// CONTIGUOUS FREQUENCY
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn frequency_round_trip_endpoints() {
|
|
for hz in [19.7f32, 20160.0] {
|
|
let raw = frequency_hz_to_raw(hz).unwrap();
|
|
let decoded = raw_to_frequency_hz(raw).unwrap();
|
|
// log scale quantization tolerance: within 1% of range
|
|
assert!((decoded - hz).abs() / hz < 0.01);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn frequency_round_trip_mid() {
|
|
let hz = 1000.0f32;
|
|
let raw = frequency_hz_to_raw(hz).unwrap();
|
|
let decoded = raw_to_frequency_hz(raw).unwrap();
|
|
assert!((decoded - hz).abs() / hz < 0.02);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// DISCRETE FREQUENCY
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn discrete_frequency_round_trip_all() {
|
|
let freqs = [
|
|
types::DiscreteFrequency::F20, types::DiscreteFrequency::F25,
|
|
types::DiscreteFrequency::F31_5, types::DiscreteFrequency::F40,
|
|
types::DiscreteFrequency::F50, types::DiscreteFrequency::F63,
|
|
types::DiscreteFrequency::F80, types::DiscreteFrequency::F100,
|
|
types::DiscreteFrequency::F125, types::DiscreteFrequency::F160,
|
|
types::DiscreteFrequency::F200, types::DiscreteFrequency::F250,
|
|
types::DiscreteFrequency::F315, types::DiscreteFrequency::F400,
|
|
types::DiscreteFrequency::F500, types::DiscreteFrequency::F630,
|
|
types::DiscreteFrequency::F800, types::DiscreteFrequency::F1000,
|
|
types::DiscreteFrequency::F1250, types::DiscreteFrequency::F1600,
|
|
types::DiscreteFrequency::F2000, types::DiscreteFrequency::F2500,
|
|
types::DiscreteFrequency::F3150, types::DiscreteFrequency::F4000,
|
|
types::DiscreteFrequency::F5000, types::DiscreteFrequency::F6300,
|
|
types::DiscreteFrequency::F8000, types::DiscreteFrequency::F10000,
|
|
types::DiscreteFrequency::F12500, types::DiscreteFrequency::F16000,
|
|
types::DiscreteFrequency::F20000,
|
|
];
|
|
for (i, f) in freqs.into_iter().enumerate() {
|
|
let raw = frequency_value_to_raw(f).unwrap();
|
|
assert_eq!(raw, i as u8);
|
|
assert_eq!(raw_to_frequency_value(raw).unwrap(), f);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// CROSSOVER FILTER
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn crossover_filter_round_trip_all() {
|
|
let filters = [
|
|
types::CrossoverFilter::Bypass, types::CrossoverFilter::Bw6,
|
|
types::CrossoverFilter::Bl6, types::CrossoverFilter::Bw12,
|
|
types::CrossoverFilter::Bl12, types::CrossoverFilter::Lk12,
|
|
types::CrossoverFilter::Bw18, types::CrossoverFilter::Bl18,
|
|
types::CrossoverFilter::Bw24, types::CrossoverFilter::Bl24,
|
|
types::CrossoverFilter::Lk24, types::CrossoverFilter::Bw30,
|
|
types::CrossoverFilter::Bl30, types::CrossoverFilter::Bw36,
|
|
types::CrossoverFilter::Bl36, types::CrossoverFilter::Lk36,
|
|
types::CrossoverFilter::Bw42, types::CrossoverFilter::Bl42,
|
|
types::CrossoverFilter::Bw48, types::CrossoverFilter::Bl48,
|
|
types::CrossoverFilter::Lk48,
|
|
];
|
|
for (i, f) in filters.into_iter().enumerate() {
|
|
let raw = crossover_filter_to_raw(f).unwrap();
|
|
assert_eq!(raw, i as u8);
|
|
assert_eq!(raw_to_crossover_filter(raw).unwrap(), f);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// PEQ Q
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn peq_q_round_trip_peak_filter() {
|
|
for q in [0.40f32, 1.0, 10.0, 128.0] {
|
|
let raw = peq_q_to_raw(q, types::PEQFilter::Peak).unwrap();
|
|
let decoded = raw_to_peq_q(raw).unwrap();
|
|
// log scale, allow 5% relative tolerance
|
|
assert!((decoded - q).abs() / q < 0.05);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn peq_q_round_trip_shelf_filter() {
|
|
for q in [0.40f32, 1.5, 3.0] {
|
|
let raw = peq_q_to_raw(q, types::PEQFilter::LowShelf).unwrap();
|
|
let decoded = raw_to_peq_q(raw).unwrap();
|
|
assert!((decoded - q).abs() / q < 0.05);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// PEQ FILTER
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn peq_filter_round_trip_all() {
|
|
let filters = [
|
|
types::PEQFilter::Peak, types::PEQFilter::LowShelf,
|
|
types::PEQFilter::HighShelf, types::PEQFilter::Lp6Db,
|
|
types::PEQFilter::Lp12Db, types::PEQFilter::Hp6Db,
|
|
types::PEQFilter::Hp12Db, types::PEQFilter::AllPass1,
|
|
types::PEQFilter::AllPass2,
|
|
];
|
|
for (i, f) in filters.into_iter().enumerate() {
|
|
let raw = peq_filter_to_raw(f).unwrap();
|
|
assert_eq!(raw, i as u8);
|
|
assert_eq!(raw_to_peq_filter(raw).unwrap(), f);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// RATIO
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn ratio_round_trip_all() {
|
|
let ratios = [
|
|
types::Ratio::Ratio1_1, types::Ratio::Ratio1_1_1, types::Ratio::Ratio1_1_3,
|
|
types::Ratio::Ratio1_1_5, types::Ratio::Ratio1_1_7, types::Ratio::Ratio1_2,
|
|
types::Ratio::Ratio1_2_5, types::Ratio::Ratio1_3, types::Ratio::Ratio1_3_5,
|
|
types::Ratio::Ratio1_4, types::Ratio::Ratio1_5, types::Ratio::Ratio1_6,
|
|
types::Ratio::Ratio1_8, types::Ratio::Ratio1_10, types::Ratio::Ratio1_20,
|
|
types::Ratio::Limit,
|
|
];
|
|
for (i, r) in ratios.into_iter().enumerate() {
|
|
let raw = ratio_to_raw(r).unwrap();
|
|
assert_eq!(raw, i as u16);
|
|
assert_eq!(raw_to_ratio(raw).unwrap(), r);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// KNEE
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn knee_is_identity_mapping() {
|
|
for db in [0u16, 6, 12] {
|
|
let raw = knee_db_to_raw(db).unwrap();
|
|
assert_eq!(raw, db);
|
|
assert_eq!(raw_to_knee_db(raw).unwrap(), db);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// THRESHOLD
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn threshold_round_trip() {
|
|
for db in [-90.0f32, -35.5, 0.0, 20.0] {
|
|
let raw = threshold_db_to_raw(db).unwrap();
|
|
let decoded = raw_to_threshold_db(raw).unwrap();
|
|
assert!((decoded - (db * 2.0).round() / 2.0).abs() < 1e-4);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// LINK
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn link_input_channels() {
|
|
let source = types::Channel::Input(types::InputChannel::InA);
|
|
let destinations = vec![
|
|
types::Channel::Input(types::InputChannel::InA),
|
|
types::Channel::Input(types::InputChannel::InC),
|
|
];
|
|
let mask = link_channels_to_raw(source, &destinations).unwrap();
|
|
assert_eq!(mask, 0b0000_0101);
|
|
|
|
let source_raw = channel_to_raw(source).unwrap();
|
|
let decoded = raw_to_link_channels(source_raw, mask).unwrap();
|
|
assert_eq!(decoded, destinations);
|
|
}
|
|
|
|
#[test]
|
|
fn link_output_channels() {
|
|
let source = types::Channel::Output(types::OutputChannel::Out2);
|
|
let destinations = vec![
|
|
types::Channel::Output(types::OutputChannel::Out2),
|
|
types::Channel::Output(types::OutputChannel::Out5),
|
|
types::Channel::Output(types::OutputChannel::Out8),
|
|
];
|
|
let mask = link_channels_to_raw(source, &destinations).unwrap();
|
|
assert_eq!(mask, (1 << 1) | (1 << 4) | (1 << 7));
|
|
|
|
let source_raw = channel_to_raw(source).unwrap();
|
|
let decoded = raw_to_link_channels(source_raw, mask).unwrap();
|
|
assert_eq!(decoded, destinations);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// MATRIX GAIN
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn matrix_gain_round_trip() {
|
|
for db in [-60.0f32, -30.25, -19.9, 0.0] {
|
|
let raw = matrix_gain_db_to_raw(db).unwrap();
|
|
let decoded = raw_to_matrix_gain(raw).unwrap();
|
|
assert!((decoded - db).abs() < 0.6);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// MATRIX
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn matrix_round_trip_all_inputs() {
|
|
let all = vec![
|
|
types::InputChannel::InA,
|
|
types::InputChannel::InB,
|
|
types::InputChannel::InC,
|
|
types::InputChannel::InD,
|
|
];
|
|
let raw = matrix_to_raw(&all).unwrap();
|
|
let mut decoded = raw_to_matrix(raw).unwrap();
|
|
decoded.sort_by_key(|c| format!("{:?}", c));
|
|
let mut expected = all.clone();
|
|
expected.sort_by_key(|c| format!("{:?}", c));
|
|
assert_eq!(decoded, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_round_trip_subset() {
|
|
let subset = vec![types::InputChannel::InB, types::InputChannel::InD];
|
|
let raw = matrix_to_raw(&subset).unwrap();
|
|
let decoded = raw_to_matrix(raw).unwrap();
|
|
assert_eq!(decoded.len(), 2);
|
|
assert!(decoded.contains(&types::InputChannel::InB));
|
|
assert!(decoded.contains(&types::InputChannel::InD));
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_empty() {
|
|
let raw = matrix_to_raw(&[]).unwrap();
|
|
assert_eq!(raw, 0);
|
|
assert!(raw_to_matrix(raw).unwrap().is_empty());
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// DELAY
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn delay_round_trip_milliseconds() {
|
|
for ms in [0.0f32, 340.0, 680.0] {
|
|
let raw = delay_to_raw(ms, types::DelayUnit::Millisecond).unwrap();
|
|
let decoded = raw_to_delay(raw, types::DelayUnit::Millisecond).unwrap();
|
|
assert!((decoded - ms).abs() < 5.0); // quantized to 8-bit resolution
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn delay_round_trip_meters() {
|
|
for m in [0.0f32, 100.0, 233.58] {
|
|
let raw = delay_to_raw(m, types::DelayUnit::Meter).unwrap();
|
|
let decoded = raw_to_delay(raw, types::DelayUnit::Meter).unwrap();
|
|
assert!((decoded - m).abs() < 2.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn delay_round_trip_feet() {
|
|
for ft in [0.0f32, 400.0, 766.329] {
|
|
let raw = delay_to_raw(ft, types::DelayUnit::Feet).unwrap();
|
|
let decoded = raw_to_delay(raw, types::DelayUnit::Feet).unwrap();
|
|
assert!((decoded - ft).abs() < 5.0);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// COPY CHANNEL
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn copy_channel_inputs() {
|
|
let source = types::Channel::Input(types::InputChannel::InA);
|
|
let dest = types::Channel::Input(types::InputChannel::InC);
|
|
let (raw_source, raw_dest) = copy_channel_to_raw(source, dest).unwrap();
|
|
assert_eq!(raw_source, 0x00);
|
|
assert_eq!(raw_dest, 0x02);
|
|
}
|
|
|
|
#[test]
|
|
fn copy_channel_outputs() {
|
|
let source = types::Channel::Output(types::OutputChannel::Out1);
|
|
let dest = types::Channel::Output(types::OutputChannel::Out8);
|
|
let (raw_source, raw_dest) = copy_channel_to_raw(source, dest).unwrap();
|
|
assert_eq!(raw_source, 0x04);
|
|
assert_eq!(raw_dest, 0x0B);
|
|
}
|
|
} |