Initial commit

This commit is contained in:
2026-07-20 19:29:57 +02:00
commit ad7da5f7ee
23 changed files with 5271 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
Generated
+102
View File
@@ -0,0 +1,102 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "dsp_thomann"
version = "0.1.0"
dependencies = [
"strum",
"strum_macros",
"thiserror",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "dsp_thomann"
version = "0.1.0"
edition = "2024"
[dependencies]
thiserror = "2.0.19"
strum = "0.28.0"
strum_macros = "0.28.0"
[lib]
name = "dsp_thomann"
path = "src/lib.rs"
+136
View File
@@ -0,0 +1,136 @@
use thiserror::Error;
// ------ PROTOCOL ERROR ------
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("{0}")]
pub struct ProtocolError(pub String);
impl ProtocolError {
pub fn new(msg: impl Into<String>) -> Self {
Self(msg.into())
}
}
// ------ FRAME ERROR ------
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("{0}")]
pub struct FrameError(pub String);
impl FrameError {
pub fn new(msg: impl Into<String>) -> Self {
Self(msg.into())
}
}
// ------ VALIDATION ERROR ------
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ValidationError {
#[error("{0}")]
Value(String),
#[error("{0}")]
Type(String),
}
impl ValidationError {
pub fn value(msg: impl Into<String>) -> Self {
Self::Value(msg.into())
}
pub fn type_(msg: impl Into<String>) -> Self {
Self::Type(msg.into())
}
}
// ------ ENCODING ERROR ------
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("{0}")]
pub struct EncodingError(pub String);
impl EncodingError {
pub fn new(msg: impl Into<String>) -> Self {
Self(msg.into())
}
}
// ------ DECODING ERROR ------
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("{0}")]
pub struct DecodingError(pub String);
impl DecodingError {
pub fn new(msg: impl Into<String>) -> Self {
Self(msg.into())
}
}
// ------ MAPPER ERROR ------
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum MapperError {
#[error("validation error: {0}")]
Validation(#[from] ValidationError),
#[error("encoding error: {0}")]
Encoding(#[from] EncodingError),
#[error("decoding error: {0}")]
Decoding(#[from] DecodingError),
}
// ------ DSP ERROR ------
#[derive(Debug, Error)]
pub enum DSPError {
#[error("device is not connected")]
NotConnected,
#[error("device is already connected")]
AlreadyConnected,
#[error("handshake failed")]
HandshakeFailed,
#[error("operation timed out")]
Timeout,
#[error("connection closed by remote device")]
ConnectionClosed,
#[error("invalid host: {0}")]
InvalidHost(&'static str),
#[error("invalid port: {0}")]
InvalidPort(&'static str),
#[error("invalid device id: {0}")]
InvalidDeviceId(&'static str),
#[error("invalid request: {0}")]
InvalidRequest(String),
#[error("unexpected response: {0}")]
UnexpectedResponse(String),
#[error("validation error: {0}")]
Validation(#[from] ValidationError),
#[error("operation failed: {0}")]
OperationFailed(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("mapper error: {0}")]
Mapper(#[from] MapperError),
#[error("protocol error: {0}")]
Protocol(#[from] ProtocolError),
#[error("frame error: {0}")]
Frame(#[from] FrameError),
}
+2
View File
@@ -0,0 +1,2 @@
pub mod errors;
pub(crate) mod validator;
+54
View File
@@ -0,0 +1,54 @@
use super::errors::ValidationError;
pub(crate) fn validate_range<T, R>(
value: T,
range: R,
message: impl Fn() -> String,
) -> Result<(), ValidationError>
where
T: PartialOrd,
R: std::ops::RangeBounds<T>,
{
if !range.contains(&value) {
return Err(ValidationError::value(message()));
}
Ok(())
}
pub(crate) fn validate_ascii_padded(
value: &[u8],
length: usize,
padding: u8,
field: &str,
) -> Result<(), ValidationError> {
if value.len() != length {
return Err(ValidationError::value(format!(
"{} must be exactly {} bytes",
field, length
)));
}
for &byte in value {
if !((0x20..=0x7E).contains(&byte) || byte == padding) {
return Err(ValidationError::value(format!(
"Invalid ASCII character: {:#04x}",
byte
)));
}
}
Ok(())
}
pub(crate) fn validate_bitmask(
value: u8,
allowed_bits: u8,
message: impl Fn() -> String,
) -> Result<(), ValidationError> {
if value & !allowed_bits != 0 {
return Err(ValidationError::value(message()));
}
Ok(())
}
+69
View File
@@ -0,0 +1,69 @@
use std::ops::RangeInclusive;
use super::types::{DelayUnit, PEQFilter};
pub const PRESET_INDEX_MAX: usize = 20;
pub const CONFIG_CHUNK_INDEX_MAX: usize = 28;
pub const PRESET_NAME_LENGTH: usize = 14;
pub const PRESET_NAME_PADDING: u8 = 0x20;
pub const PASSWORD_LENGTH: usize = 4;
pub const PASSWORD_PADDING: u8 = 0x00;
pub const ATTACK_MS_RANGE: RangeInclusive<u16> = 1..=999;
pub const RELEASE_MS_RANGE: RangeInclusive<u16> = 10..=3000;
pub const HOLD_MS_RANGE: RangeInclusive<u16> = 10..=999;
pub const GATE_THRESHOLD_DB_RANGE: RangeInclusive<f32> = -90.0..=0.0;
pub const EQ_GAIN_DB_RANGE: RangeInclusive<f32> = -12.0..=12.0;
pub const COMPRESSOR_KNEE_DB_RANGE: RangeInclusive<u16> = 0..=12;
pub const THRESHOLD_DB_RANGE: RangeInclusive<f32> = -90.0..=20.0;
pub const FREQUENCY_HZ_RANGE: RangeInclusive<f32> = 19.7..=20160.0;
pub const PEQ_Q_RANGE: RangeInclusive<f32> = 0.40..=128.0;
pub const PEQ_INPUT_BAND_COUNT: usize = 8;
pub const PEQ_OUTPUT_BAND_COUNT:usize = 9;
pub const GAIN_DB_RANGE: RangeInclusive<f32> = -60.0..=12.0;
pub const GAIN_SPLIT_DB: f32 = -20.0;
pub const GAIN_LOW_STEP_DB: f32 = 0.5;
pub const GAIN_HIGH_STEP_DB: f32 = 0.1;
pub const CHANNEL_NAME_LENGTH: usize = 8;
pub const CHANNEL_NAME_PADDING: u8 = 0x00;
pub const METERS_SPLIT: f32 = 1.0;
pub const METERS_SPLIT_LEVEL: f32 = -18.0;
pub const METERS_LOW_STEP: f32 = 0.0104;
pub const METERS_HIGH_STEP: f32 = 0.0208;
pub const MATRIX_GAIN_DB_RANGE: RangeInclusive<f32> = -60.0..=0.0;
pub fn delay_range(unit: DelayUnit) -> (f32, f32) {
match unit {
DelayUnit::Millisecond => (0.000, 680.000),
DelayUnit::Meter => (0.000, 233.580),
DelayUnit::Feet => (0.000, 766.329),
}
}
pub fn peq_q_range(filter: PEQFilter) -> RangeInclusive<f32> {
match filter {
PEQFilter::Peak
| PEQFilter::AllPass1
| PEQFilter::AllPass2 => {
0.40..=128.0
}
PEQFilter::LowShelf
| PEQFilter::HighShelf
| PEQFilter::Lp6Db
| PEQFilter::Lp12Db
| PEQFilter::Hp6Db
| PEQFilter::Hp12Db => {
0.40..=3.0
}
}
}
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
pub mod constants;
pub mod types;
pub mod dsp408;
pub use dsp408::DSP408;
+383
View File
@@ -0,0 +1,383 @@
use std::{collections::HashMap, net::IpAddr};
use strum_macros::{EnumIter, EnumCount};
// ------ Enums ------
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DelayUnit {
Millisecond,
Meter,
Feet,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, EnumCount, PartialOrd, Ord)]
pub enum InputChannel {
InA,
InB,
InC,
InD,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, EnumCount, PartialOrd, Ord)]
pub enum OutputChannel {
Out1,
Out2,
Out3,
Out4,
Out5,
Out6,
Out7,
Out8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Channel {
Input(InputChannel),
Output(OutputChannel),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Ratio {
#[default]
Ratio1_1,
Ratio1_1_1,
Ratio1_1_3,
Ratio1_1_5,
Ratio1_1_7,
Ratio1_2,
Ratio1_2_5,
Ratio1_3,
Ratio1_3_5,
Ratio1_4,
Ratio1_5,
Ratio1_6,
Ratio1_8,
Ratio1_10,
Ratio1_20,
Limit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum CrossoverFilter {
#[default]
Bypass,
Bw6,
Bw12,
Bw18,
Bw24,
Bw30,
Bw36,
Bw42,
Bw48,
Lk12,
Lk24,
Lk36,
Lk48,
Bl6,
Bl12,
Bl18,
Bl24,
Bl30,
Bl36,
Bl42,
Bl48,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum PEQBand {
B1 = 1,
B2,
B3,
B4,
B5,
B6,
B7,
B8,
B9,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PEQFilter {
Peak,
LowShelf,
HighShelf,
Lp6Db,
Lp12Db,
Hp6Db,
Hp12Db,
AllPass1,
AllPass2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum DiscreteFrequency {
F20,
F25,
F31_5,
F40,
F50,
F63,
F80,
F100,
F125,
F160,
F200,
F250,
F315,
F400,
F500,
F630,
F800,
F1000,
F1250,
F1600,
F2000,
F2500,
F3150,
F4000,
F5000,
F6300,
F8000,
F10000,
F12500,
F16000,
F20000,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InputType {
Analog,
PinkNoise,
WhiteNoise,
SineWave,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct InputSource {
pub r#type: InputType,
pub frequency: DiscreteFrequency,
}
// ------ Response types ------
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Acknowledgement {
pub success: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NotImplemented;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HandshakeAck {
pub ack: Option<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceInfo {
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CurrentPreset {
pub index: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModifiedPreset {
pub modified: Vec<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PresetName {
pub index: u8,
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeviceFlags {
pub is_locked: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AuthenticationResult {
pub success: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Meters {
pub levels: Vec<f32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigChunk {
pub sub_index: u8,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownResponse {
pub cmd: String,
pub raw: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DeviceResponseType {
Acknowledgement(Acknowledgement),
NotImplemented(NotImplemented),
HandshakeAck(HandshakeAck),
DeviceInfo(DeviceInfo),
PresetName(PresetName),
DeviceFlags(DeviceFlags),
AuthenticationResult(AuthenticationResult),
Meters(Meters),
CurrentPreset(CurrentPreset),
ModifiedPreset(ModifiedPreset),
ConfigChunk(ConfigChunk),
UnknownResponse(UnknownResponse),
}
// ------ State ------
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Gate {
pub attack: u16,
pub release: u16,
pub hold: u16,
pub threshold: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Compressor {
pub threshold: f32,
pub ratio: Ratio,
pub attack: u16,
pub release: u16,
pub knee: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Limiter {
pub threshold: f32,
pub attack: u16,
pub release: u16,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct GraphicEQ {
pub gains: Vec<f32>,
pub bypass: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PEQ {
pub gain: f32,
pub frequency: f32,
pub q: f32,
pub filter_type: PEQFilter,
pub bypass: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct CrossoverFilters {
pub high_pass: Crossover,
pub low_pass: Crossover,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Crossover {
pub frequency: f32,
pub slope: CrossoverFilter,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct MatrixRoutes {
pub connected: Vec<InputChannel>,
pub gains: Vec<f32>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct PEQChain {
pub bands: Vec<PEQ>,
pub bypass: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PresetBank {
pub current_index: usize,
pub names: Vec<String>,
pub modified: Vec<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DeviceConnection {
pub host: IpAddr,
pub port: u16,
pub device_id: u8,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct InputChannelState {
pub name: String,
pub gain: f32,
pub phase_inverted: bool,
pub delay: f32,
pub gate: Gate,
pub geq: GraphicEQ,
pub peq_chain: PEQChain,
pub crossover: CrossoverFilters,
pub linked_channels: Vec<Channel>,
pub mute: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct OutputChannelState {
pub name: String,
pub matrix_routes: MatrixRoutes,
pub crossover: CrossoverFilters,
pub peq_chain: PEQChain,
pub compressor: Compressor,
pub limiter: Limiter,
pub gain: f32,
pub phase_inverted: bool,
pub delay: f32,
pub linked_channels: Vec<Channel>,
pub mute: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DSPConfigState {
pub(crate) input_states: HashMap<InputChannel, InputChannelState>,
pub(crate) output_states: HashMap<OutputChannel, OutputChannelState>,
pub(crate) input_source: InputSource,
pub(crate) delay_unit: DelayUnit,
}
impl DSPConfigState {
pub fn input_state_mut(
&mut self,
channel: InputChannel,
) -> &mut InputChannelState {
self.input_states
.get_mut(&channel)
.expect("Invalid input channel state")
}
pub fn output_state_mut(
&mut self,
channel: OutputChannel,
) -> &mut OutputChannelState {
self.output_states
.get_mut(&channel)
.expect("Invalid output channel state")
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DSPState {
pub(crate) name: String,
pub(crate) flags: DeviceFlags,
pub(crate) presets: PresetBank,
pub(crate) current_config: DSPConfigState,
}
+858
View File
@@ -0,0 +1,858 @@
use strum::IntoEnumIterator;
use crate::common::validator::{validate_ascii_padded, validate_range};
use crate::dsp408::client::constants::{self as client_constants};
use crate::dsp408::protocol::constants as protocol_constants;
use crate::dsp408::protocol::validators;
use crate::common::errors::{MapperError, DecodingError, ValidationError};
use super::{encode_ascii, decode_ascii};
use crate::dsp408::client::types;
// ------ 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(),
client_constants::CHANNEL_NAME_LENGTH,
client_constants::CHANNEL_NAME_PADDING,
"Channel name",
)?;
Ok(encode_ascii(name, client_constants::CHANNEL_NAME_LENGTH, client_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,
client_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,
client_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,
client_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,
client_constants::GATE_THRESHOLD_DB_RANGE,
|| format!("Invalid gate threshold: {db}"),
)?;
let db = (db * 2.0).round() / 2.0;
Ok(((db - *client_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(*client_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 {
(*client_constants::EQ_GAIN_DB_RANGE.end()
- *client_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,
client_constants::EQ_GAIN_DB_RANGE,
|| format!("Invalid EQ gain: {gain}"),
)?;
let gain = eq_quantize(gain);
Ok(
*protocol_constants::EQ_GAIN_RAW_RANGE.start()
+ ((gain - *client_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 =
*client_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 {
((client_constants::GAIN_SPLIT_DB - client_constants::GAIN_DB_RANGE.start()) / client_constants::GAIN_LOW_STEP_DB).round() as u16
}
fn quantize_gain(db: f32) -> f32 {
if db < client_constants::GAIN_SPLIT_DB {
(db / client_constants::GAIN_LOW_STEP_DB).round() * client_constants::GAIN_LOW_STEP_DB
} else {
(db / client_constants::GAIN_HIGH_STEP_DB).round() * client_constants::GAIN_HIGH_STEP_DB
}
}
pub fn gain_db_to_raw(gain: f32) -> Result<u16, MapperError> {
validate_range(
gain,
client_constants::GAIN_DB_RANGE,
|| format!("Invalid channel gain: {gain}"),
)?;
let gain = quantize_gain(gain);
if gain < client_constants::GAIN_SPLIT_DB {
Ok(((gain - client_constants::GAIN_DB_RANGE.start()) / client_constants::GAIN_LOW_STEP_DB).round() as u16)
} else {
Ok(gain_split_raw() + ((gain - client_constants::GAIN_SPLIT_DB) / client_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(client_constants::GAIN_DB_RANGE.start() + raw_gain as f32 * client_constants::GAIN_LOW_STEP_DB)
} else {
Ok(client_constants::GAIN_SPLIT_DB + (raw_gain - gain_split_raw()) as f32 * client_constants::GAIN_HIGH_STEP_DB)
}
}
// ------ CONTIGUOUS FREQUENCY ------
pub fn frequency_hz_to_raw(hz: f32) -> Result<u16, MapperError> {
validate_range(
hz,
client_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 / client_constants::FREQUENCY_HZ_RANGE.start()).ln() / (client_constants::FREQUENCY_HZ_RANGE.end() / client_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(client_constants::FREQUENCY_HZ_RANGE.start() * (client_constants::FREQUENCY_HZ_RANGE.end() / client_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,
client_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 / client_constants::PEQ_Q_RANGE.start()).ln() / (client_constants::PEQ_Q_RANGE.end() / client_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(client_constants::PEQ_Q_RANGE.start() * (client_constants::PEQ_Q_RANGE.end() / client_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,
client_constants::COMPRESSOR_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,
client_constants::THRESHOLD_DB_RANGE,
|| format!("Invalid compressor threshold: {db}"),
)?;
let db = (db * 2.0).round() / 2.0;
Ok(((db - client_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(client_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 {
((client_constants::GAIN_SPLIT_DB - client_constants::MATRIX_GAIN_DB_RANGE.start()) / client_constants::GAIN_LOW_STEP_DB).round() as u16
}
fn quantize_matrix_gain(db: f32) -> f32 {
if db < client_constants::GAIN_SPLIT_DB {
(db / client_constants::GAIN_LOW_STEP_DB).round() * client_constants::GAIN_LOW_STEP_DB
} else {
(db / client_constants::GAIN_HIGH_STEP_DB).round() * client_constants::GAIN_HIGH_STEP_DB
}
}
pub fn matrix_gain_db_to_raw(gain: f32) -> Result<u16, MapperError> {
validate_range(
gain,
client_constants::MATRIX_GAIN_DB_RANGE,
|| format!("Invalid matrix gain: {gain}"),
)?;
let gain = quantize_matrix_gain(gain);
if gain < client_constants::GAIN_SPLIT_DB {
Ok(((gain - client_constants::MATRIX_GAIN_DB_RANGE.start()) / client_constants::GAIN_LOW_STEP_DB).round() as u16)
} else {
Ok(matrix_gain_split_raw()
+ ((gain - client_constants::GAIN_SPLIT_DB) / client_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(client_constants::MATRIX_GAIN_DB_RANGE.start() + raw_gain as f32 * client_constants::GAIN_LOW_STEP_DB)
} else {
Ok(client_constants::GAIN_SPLIT_DB + (raw_gain - matrix_gain_split_raw()) as f32 * client_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) = client_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) = client_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)))
}
+74
View File
@@ -0,0 +1,74 @@
use crate::common::errors::{EncodingError, DecodingError};
// ------ STRING ------
pub fn encode_ascii(value: &str, length: usize, padding: u8) -> Result<Vec<u8>, EncodingError> {
if length == 0 {
return Err(EncodingError::new(format!("Invalid string length: {}", length)));
}
if !value.is_ascii() {
return Err(EncodingError::new("String must contain ASCII characters only"));
}
let raw = value.as_bytes();
if raw.len() > length {
return Err(EncodingError::new(format!(
"String too long: got {}, expected {}",
raw.len(),
length
)));
}
let mut out = Vec::with_capacity(length);
out.extend_from_slice(raw);
out.resize(length, padding);
Ok(out)
}
pub fn decode_ascii(raw: &[u8], strip: bool) -> Result<String, DecodingError> {
if !raw.is_ascii() {
return Err(DecodingError::new("Invalid ASCII data"));
}
let result = String::from_utf8(raw.to_vec()).expect("raw is ASCII, so always valid UTF-8");
Ok(if strip {
result.trim_end_matches(['\u{0020}', '\u{0000}']).to_string()
} else {
result
})
}
// ------ FLOAT ------
pub fn decode_float16(lo: u8, hi: u8) -> Result<f32, DecodingError> {
let value = lo as u16 | ((hi as u16) << 8);
let sign = (value >> 15) & 1;
let exponent = (value >> 10) & 0x1F;
let mantissa = value & 0x3FF;
let result = if exponent == 0 {
// subnormal numbers
(mantissa as f32 / 1024.0) * 2f32.powi(-14)
} else if exponent == 31 {
if mantissa == 0 {
return if sign == 1 {
Ok(f32::NEG_INFINITY)
} else {
Ok(f32::INFINITY)
};
} else {
return Ok(f32::NAN);
}
} else {
(1.0 + mantissa as f32 / 1024.0)
* 2f32.powi(exponent as i32 - 15)
};
if sign == 1 {
Ok(-result)
} else {
Ok(result)
}
}
+148
View File
@@ -0,0 +1,148 @@
use crate::common::validator::{validate_ascii_padded, validate_range};
use crate::dsp408::client::constants as client_constants;
use crate::dsp408::client::types;
use crate::dsp408::protocol::validators;
use crate::common::errors::{MapperError, DecodingError};
use crate::dsp408::protocol::constants as protocol_constants;
use super::{encode_ascii, decode_ascii};
// ------ DELAY ------
fn delay_unit_to_raw_value(unit: types::DelayUnit) -> u8 {
match unit {
types::DelayUnit::Millisecond => *protocol_constants::DELAY_UNIT_RAW_RANGE.start(),
types::DelayUnit::Meter => *protocol_constants::DELAY_UNIT_RAW_RANGE.start() + 1,
types::DelayUnit::Feet => *protocol_constants::DELAY_UNIT_RAW_RANGE.start() + 2,
}
}
fn raw_to_delay_unit_value(value: u8) -> Option<types::DelayUnit> {
match value - *protocol_constants::DELAY_UNIT_RAW_RANGE.start() {
0 => Some(types::DelayUnit::Millisecond),
1 => Some(types::DelayUnit::Meter),
2 => Some(types::DelayUnit::Feet),
_ => None,
}
}
pub fn delay_unit_to_raw(
unit: types::DelayUnit,
) -> Result<u8, MapperError> {
Ok(delay_unit_to_raw_value(unit))
}
pub fn raw_to_delay_unit(
value: u8,
) -> Result<types::DelayUnit, MapperError> {
validators::validate_raw_delay_unit(value)?;
raw_to_delay_unit_value(value)
.ok_or_else(|| {
MapperError::Decoding(
DecodingError::new(
format!("Unknown delay unit value: {:#04x}", value),
),
)
})
}
// ------ PRESET INDEX ------
pub fn preset_index_to_raw(index: usize) -> Result<u8, MapperError> {
validate_range(
index,
0..=client_constants::PRESET_INDEX_MAX,
|| format!("Invalid preset index {}", index)
)?;
Ok(index as u8)
}
pub fn preset_index_without_factory_to_raw(index: usize) -> Result<u8, MapperError> {
validate_range(
index,
1..=client_constants::PRESET_INDEX_MAX,
|| format!("Invalid preset index {}", index)
)?;
Ok(index as u8 - 1)
}
// ------ CONFIG CHUNK ------
pub fn config_chunk_to_raw(index: usize) -> Result<u8, MapperError> {
validate_range(
index,
0..=client_constants::CONFIG_CHUNK_INDEX_MAX,
|| format!("Invalid config index: {:#04x}", index)
)?;
Ok(index as u8)
}
// ------ PRESET NAME ------
pub fn preset_name_to_raw(name: &str) -> Result<Vec<u8>, MapperError> {
validate_ascii_padded(
name.as_bytes(),
client_constants::PRESET_NAME_LENGTH,
client_constants::PRESET_NAME_PADDING,
"Preset name"
)?;
Ok(encode_ascii(name, client_constants::PRESET_NAME_LENGTH, client_constants::PRESET_NAME_PADDING)?)
}
pub fn raw_to_preset_name(raw_name: &[u8]) -> Result<String, MapperError> {
validators::validate_raw_preset_name(raw_name)?;
Ok(decode_ascii(raw_name, true)?)
}
// ------ INPUT SOURCE ------
fn input_source_to_raw_value(source: types::InputType) -> u8 {
match source {
types::InputType::Analog => *protocol_constants::INPUT_SOURCE_TYPE_RAW_RANGE.start(),
types::InputType::PinkNoise => *protocol_constants::INPUT_SOURCE_TYPE_RAW_RANGE.start() + 1,
types::InputType::WhiteNoise => *protocol_constants::INPUT_SOURCE_TYPE_RAW_RANGE.start() + 2,
types::InputType::SineWave => *protocol_constants::INPUT_SOURCE_TYPE_RAW_RANGE.start() + 3,
}
}
fn raw_to_input_source_value(value: u8) -> Option<types::InputType> {
match value - protocol_constants::INPUT_SOURCE_TYPE_RAW_RANGE.start() {
0 => Some(types::InputType::Analog),
1 => Some(types::InputType::PinkNoise),
2 => Some(types::InputType::WhiteNoise),
3 => Some(types::InputType::SineWave),
_ => None,
}
}
pub fn input_source_to_raw(source: types::InputType) -> Result<u8, MapperError> {
Ok(input_source_to_raw_value(source))
}
pub fn raw_to_input_source(value: u8) -> Result<types::InputType, MapperError> {
validators::validate_raw_input_source(value)?;
raw_to_input_source_value(value)
.ok_or_else(|| {
MapperError::Decoding(
DecodingError::new(
format!("Unknown input value: {:#04x}", value)
)
)
})
}
// ------ PASSWORD ------
pub fn password_to_raw(password: &str) -> Result<Vec<u8>, MapperError> {
validate_ascii_padded(
password.as_bytes(),
client_constants::PASSWORD_LENGTH,
client_constants::PASSWORD_PADDING,
"Password"
)?;
Ok(encode_ascii(password, client_constants::PASSWORD_LENGTH, client_constants::PASSWORD_PADDING)?)
}
+9
View File
@@ -0,0 +1,9 @@
pub mod channel;
pub mod encoding;
pub mod global;
pub mod response;
pub use channel::*;
pub use encoding::*;
pub use global::*;
+234
View File
@@ -0,0 +1,234 @@
use crate::dsp408::client::constants::{METERS_HIGH_STEP, METERS_LOW_STEP, METERS_SPLIT, METERS_SPLIT_LEVEL};
use crate::dsp408::client::types::{
Acknowledgement, AuthenticationResult, ConfigChunk, CurrentPreset, DeviceFlags, DeviceInfo,
DeviceResponseType, HandshakeAck, Meters, ModifiedPreset, NotImplemented, PresetName,
UnknownResponse,
};
use crate::common::errors::ProtocolError;
use crate::dsp408::protocol::frame::Frame;
use super::encoding::{decode_ascii, decode_float16};
pub fn map_response(frame: &Frame) -> Result<DeviceResponseType, ProtocolError> {
let cmd = frame.command;
let payload = &frame.payload;
// Command `0x01`
if cmd == 0x01 {
return Ok(DeviceResponseType::Acknowledgement(Acknowledgement {
success: true,
}));
}
// Command `0x02`
if cmd == 0x02 {
return Ok(DeviceResponseType::NotImplemented(NotImplemented));
}
// Command `0x10` (Start)
if cmd == 0x10 {
if payload.len() != 1 {
return Err(ProtocolError::new(format!(
"Invalid Start response payload length: {}",
payload.len()
)));
}
return Ok(DeviceResponseType::HandshakeAck(HandshakeAck {
ack: payload.first().copied(),
}));
}
// Command `0x13` (Device Name)
if cmd == 0x13 {
if payload.is_empty() {
return Err(ProtocolError::new("Missing device name payload"));
}
let name = decode_ascii(payload, false)
.map_err(|e| ProtocolError::new(e.0))?;
return Ok(DeviceResponseType::DeviceInfo(DeviceInfo { name }));
}
// Command `0x14` (Current Preset Index)
if cmd == 0x14 {
if payload.len() != 1 {
return Err(ProtocolError::new(format!(
"Invalid Current Preset payload length: {}",
payload.len()
)));
}
return Ok(DeviceResponseType::CurrentPreset(CurrentPreset {
index: payload[0],
}));
}
// Command `0x22` (Get Preset Modification Status)
if cmd == 0x22 {
if payload.len() != 20 {
return Err(ProtocolError::new(format!(
"Invalid Modified Preset payload length: {}",
payload.len()
)));
}
let flags: Vec<bool> = payload.iter().map(|&b| b == 0xFF).collect();
return Ok(DeviceResponseType::ModifiedPreset(ModifiedPreset {
modified: flags,
}));
}
// Command `0x24` (Get Config Chunk)
if cmd == 0x24 {
// Mirrors Python's `int.from_bytes(payload[:1], "little")`, which
// is `0` for an empty payload rather than an error — Python slices
// never panic. `payload.get(1..)` below mirrors the same
// forgiving-slice behavior for the remainder.
let index = payload.first().copied().unwrap_or(0);
if index > 0x1c {
return Err(ProtocolError::new(format!(
"Invalid Config Chunk Index: {}",
index
)));
}
let data = payload.get(1..).unwrap_or(&[]).to_vec();
return Ok(DeviceResponseType::ConfigChunk(ConfigChunk {
sub_index: index,
data,
}));
}
// Command `0x29` (Preset Name)
if cmd == 0x29 {
if payload.is_empty() {
return Err(ProtocolError::new("Missing preset index"));
}
let name = decode_ascii(&payload[1..], false)
.map_err(|e| ProtocolError::new(e.0))?;
return Ok(DeviceResponseType::PresetName(PresetName {
index: payload[0],
name,
}));
}
// Command `0x2C` (Device Flags)
if cmd == 0x2C {
if payload.len() < 6 {
return Err(ProtocolError::new(format!(
"Invalid Device Flags payload length: {}",
payload.len()
)));
}
if payload[5] != 0x00 && payload[5] != 0x01 {
return Err(ProtocolError::new(format!(
"Invalid lock flag: {:#04x}",
payload[5]
)));
}
return Ok(DeviceResponseType::DeviceFlags(DeviceFlags {
is_locked: payload[5] == 0x01,
}));
}
// Command `0x2D` (Unlock Device)
if cmd == 0x2D {
if payload.len() < 2 {
return Err(ProtocolError::new("Missing authentication result"));
}
if payload[1] != 0x00 && payload[1] != 0x01 {
return Err(ProtocolError::new(format!(
"Invalid authentication flag: {:#04x}",
payload[1]
)));
}
return Ok(DeviceResponseType::AuthenticationResult(AuthenticationResult {
success: payload[1] == 0x01,
}));
}
// Command `0x40` (Get Level Meters)
if cmd == 0x40 {
if payload.len() != 38 {
return Err(ProtocolError::new("Not enough bytes for meter levels"));
}
let floats: Vec<f32> = (0..36)
.step_by(3)
.map(|i| decode_float16(payload[i], payload[i + 1]).unwrap_or_default())
.collect();
let meters: Vec<f32> = floats
.into_iter()
.map(|f| {
if f < METERS_SPLIT {
METERS_SPLIT_LEVEL + (f - METERS_SPLIT) / METERS_LOW_STEP
} else {
METERS_SPLIT_LEVEL + (f - METERS_SPLIT) / METERS_HIGH_STEP
}
})
.collect();
return Ok(DeviceResponseType::Meters(Meters { levels: meters }));
}
Ok(DeviceResponseType::UnknownResponse(UnknownResponse {
cmd: format!("{:#04x}", cmd),
raw: payload.clone(),
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn acknowledgement() {
let frame = Frame { command: 0x01, payload: vec![] };
assert_eq!(
map_response(&frame).unwrap(),
DeviceResponseType::Acknowledgement(Acknowledgement { success: true })
);
}
#[test]
fn current_preset_wrong_length_errors() {
let frame = Frame { command: 0x14, payload: vec![1, 2] };
assert!(map_response(&frame).is_err());
}
#[test]
fn unknown_command_falls_through() {
let frame = Frame { command: 0x99, payload: vec![0xAA] };
match map_response(&frame).unwrap() {
DeviceResponseType::UnknownResponse(r) => {
assert_eq!(r.cmd, "0x99");
assert_eq!(r.raw, vec![0xAA]);
}
other => panic!("expected UnknownResponse, got {:?}", other),
}
}
#[test]
fn config_chunk_empty_payload_defaults_to_zero() {
let frame = Frame { command: 0x24, payload: vec![] };
match map_response(&frame).unwrap() {
DeviceResponseType::ConfigChunk(c) => {
assert_eq!(c.sub_index, 0);
assert_eq!(c.data, Vec::<u8>::new());
}
other => panic!("expected ConfigChunk, got {:?}", other),
}
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod client;
pub mod mapper;
pub mod protocol;
pub use client::DSP408;
+598
View File
@@ -0,0 +1,598 @@
use super::frame::{build_frame};
use crate::common::errors::ValidationError;
use super::validators::*;
fn split_u16(value: u16) -> [u8; 2] {
[
(value & 0xff) as u8,
(value >> 8) as u8,
]
}
// Command 0x10 Start
pub fn build_handshake() -> Vec<u8> {
build_frame(0x10, &[])
}
// Command 0x11 Disconnect
pub fn build_disconnect() -> Vec<u8> {
build_frame(0x11, &[])
}
// Command 0x12 Acknowledgement
pub fn build_acknowledgement() -> Vec<u8> {
build_frame(0x12, &[])
}
// Command 0x13 Device Name
pub fn build_request_device_name() -> Vec<u8> {
build_frame(0x13, &[])
}
// Command 0x14 Current Preset
pub fn build_get_current_preset() -> Vec<u8> {
build_frame(0x14, &[])
}
// Command 0x15 Delay Unit
pub fn build_set_delay_unit(
raw_unit: u8
) -> Result<Vec<u8>, ValidationError> {
validate_raw_delay_unit(raw_unit)?;
Ok(build_frame(
0x15,
&[raw_unit]
))
}
// Command 0x20 Recall
pub fn build_recall(
raw_index:u8
)->Result<Vec<u8>,ValidationError>{
validate_raw_preset_index(raw_index)?;
Ok(build_frame(
0x20,
&[raw_index]
))
}
// Command 0x21 Store preset
pub fn build_store_current_configuration(
raw_index:u8
)->Result<Vec<u8>,ValidationError>{
validate_raw_store_preset_index(raw_index)?;
Ok(build_frame(
0x21,
&[raw_index]
))
}
// Command 0x22
pub fn build_preset_modification_status() -> Vec<u8> {
build_frame(0x22,&[])
}
// Command 0x23
pub fn build_factory_reset() -> Vec<u8> {
build_frame(0x23,&[])
}
// Command 0x26
pub fn build_store_current_preset_name(
raw_name:&[u8]
)->Result<Vec<u8>,ValidationError>{
validate_raw_preset_name(raw_name)?;
Ok(build_frame(
0x26,
raw_name
))
}
// Command 0x27
pub fn build_config_chunk(
raw_index:u8
)->Result<Vec<u8>,ValidationError>{
validate_raw_config_chunk_index(raw_index)?;
Ok(build_frame(
0x27,
&[raw_index]
))
}
// Command 0x29
pub fn build_get_preset_name(
raw_index:u8
)->Result<Vec<u8>,ValidationError>{
validate_raw_get_preset_index(raw_index)?;
Ok(build_frame(
0x29,
&[raw_index]
))
}
// Command 0x2A
pub fn build_copy_channel(
source: u8,
destination: u8,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_copy_channels(source, destination)?;
Ok(build_frame(
0x2A,
&[source, destination],
))
}
// Command 0x2C
pub fn build_get_device_flags() -> Vec<u8> {
build_frame(0x2c,&[])
}
// Command 0x2D
pub fn build_unlock_device(
password:&[u8]
)->Result<Vec<u8>,ValidationError>{
validate_raw_password(password)?;
Ok(build_frame(
0x2d,
&password
))
}
// Command 0x2F
pub fn build_lock_device(
password:&[u8]
)->Result<Vec<u8>,ValidationError>{
validate_raw_password(password)?;
Ok(build_frame(
0x2f,
password
))
}
// Command 0x30 (Set Compressor)
pub fn build_set_compressor(
raw_channel: u8,
raw_ratio: u16,
raw_attack: u16,
raw_release: u16,
raw_knee: u16,
raw_threshold: u16,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_output_channel(raw_channel)?;
validate_raw_compressor_ratio(raw_ratio)?;
validate_raw_attack(raw_attack)?;
validate_raw_release(raw_release)?;
validate_raw_compressor_knee(raw_knee)?;
validate_raw_threshold(raw_threshold)?;
let ratio = split_u16(raw_ratio);
let attack = split_u16(raw_attack);
let release = split_u16(raw_release);
let knee = split_u16(raw_knee);
let threshold = split_u16(raw_threshold);
Ok(build_frame(
0x30,
&[
raw_channel,
ratio[0],
ratio[1],
attack[0],
attack[1],
release[0],
release[1],
knee[0],
knee[1],
threshold[0],
threshold[1],
],
))
}
// Command 0x31 (Set Low Pass Filter)
pub fn build_low_pass(
raw_channel: u8,
raw_freq: u16,
raw_filter_type: u8,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_channel(raw_channel)?;
validate_raw_frequency(raw_freq)?;
validate_raw_crossover_filter(raw_filter_type)?;
let freq = split_u16(raw_freq);
Ok(build_frame(
0x31,
&[
raw_channel,
freq[0],
freq[1],
raw_filter_type,
],
))
}
// Command 0x32 (Set High Pass Filter)
pub fn build_high_pass(
raw_channel: u8,
raw_freq: u16,
raw_filter_type: u8,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_channel(raw_channel)?;
validate_raw_frequency(raw_freq)?;
validate_raw_crossover_filter(raw_filter_type)?;
let freq = split_u16(raw_freq);
Ok(build_frame(
0x32,
&[
raw_channel,
freq[0],
freq[1],
raw_filter_type,
],
))
}
// Command 0x33 (Set PEQ Band)
pub fn build_peq_band(
raw_channel: u8,
raw_band: u8,
raw_gain: u16,
raw_freq: u16,
raw_q: u8,
raw_filter_type: u8,
bypass: u8,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_channel(raw_channel)?;
validate_raw_peq_band(raw_channel, raw_band)?;
validate_raw_eq_gain(raw_gain)?;
validate_raw_frequency(raw_freq)?;
validate_raw_peq_q(raw_q)?;
validate_raw_peq_filter(raw_filter_type)?;
let gain = split_u16(raw_gain);
let freq = split_u16(raw_freq);
Ok(build_frame(
0x33,
&[
raw_channel,
raw_band,
gain[0],
gain[1],
freq[0],
freq[1],
raw_q,
raw_filter_type,
bypass,
],
))
}
// Command 0x34 (Set Gain)
pub fn build_gain(
channel: u8,
raw_gain: u16,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_channel(channel)?;
validate_raw_gain(raw_gain)?;
let gain = split_u16(raw_gain);
Ok(build_frame(
0x34,
&[
channel,
gain[0],
gain[1],
],
))
}
// Command 0x35 (Set Mute)
pub fn build_mute(
channel:u8,
muted:u8,
)->Result<Vec<u8>,ValidationError>{
validate_raw_channel(channel)?;
Ok(build_frame(
0x35,
&[
channel,
muted,
]
))
}
// Command 0x36 (Invert Gain)
pub fn build_inverted_gain(
channel:u8,
inverted:u8,
)->Result<Vec<u8>,ValidationError>{
validate_raw_channel(channel)?;
Ok(build_frame(
0x36,
&[
channel,
inverted,
]
))
}
// Command 0x38 (Set Delay)
pub fn build_set_delay(
channel:u8,
delay:u8,
)->Result<Vec<u8>,ValidationError>{
validate_raw_channel(channel)?;
validate_raw_channel_delay(delay)?;
Ok(build_frame(
0x38,
&[
channel,
0x00,
delay
]
))
}
// Command 0x39 (Set Input Source)
pub fn build_set_input_source(
input_type: u8,
frequency: u8,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_input_source(input_type)?;
validate_raw_discrete_frequency(frequency)?;
Ok(build_frame(
0x39,
&[
input_type,
frequency,
],
))
}
// Command 0x3A (Matrix Routing)
pub fn build_matrix_routing(
output: u8,
input_bitmask: u8,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_output_channel(output)?;
validate_raw_matrix_bitmask(input_bitmask)?;
Ok(build_frame(
0x3A,
&[
output,
input_bitmask,
],
))
}
// Command 0x3B (Link Channel)
pub fn build_link_channel(
source_channel: u8,
destination_channel_bitmask: u8,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_link_channel(source_channel, destination_channel_bitmask)?;
Ok(build_frame(
0x3B,
&[
source_channel,
destination_channel_bitmask,
],
))
}
// Command 0x3C (Bypass EQ)
pub fn build_bypass_peq(
channel: u8,
bypass: u8,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_channel(channel)?;
Ok(build_frame(
0x3C,
&[
channel,
bypass,
],
))
}
// Command 0x3D (Store Channel Name)
pub fn build_store_channel_name(
channel: u8,
raw_name: &[u8],
) -> Result<Vec<u8>, ValidationError> {
validate_raw_channel(channel)?;
validate_raw_channel_name(raw_name)?;
let mut payload = vec![channel];
payload.extend_from_slice(raw_name);
Ok(build_frame(
0x3D,
&payload,
))
}
// Command 0x3E (Set Gate Parameters)
pub fn build_set_gate(
raw_channel: u8,
raw_threshold: u16,
raw_attack: u16,
raw_hold: u16,
raw_release: u16,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_input_channel(raw_channel)?;
validate_raw_gate_threshold(raw_threshold)?;
validate_raw_attack(raw_attack)?;
validate_raw_hold(raw_hold)?;
validate_raw_release(raw_release)?;
let threshold = split_u16(raw_threshold);
let attack = split_u16(raw_attack);
let hold = split_u16(raw_hold);
let release = split_u16(raw_release);
Ok(build_frame(
0x3E,
&[
raw_channel,
attack[0],
attack[1],
release[0],
release[1],
hold[0],
hold[1],
threshold[0],
threshold[1],
],
))
}
// Command 0x3F (Set Output Limiter)
pub fn build_set_limiter(
raw_channel: u8,
raw_threshold: u16,
raw_attack: u16,
raw_release: u16,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_output_channel(raw_channel)?;
validate_raw_threshold(raw_threshold)?;
validate_raw_attack(raw_attack)?;
validate_raw_release(raw_release)?;
let threshold = split_u16(raw_threshold);
let attack = split_u16(raw_attack);
let release = split_u16(raw_release);
Ok(build_frame(
0x3F,
&[
raw_channel,
attack[0],
attack[1],
release[0],
release[1],
0x00,
0x00,
threshold[0],
threshold[1],
],
))
}
// Command 0x40 (Get Level Meters)
pub fn build_get_meter_levels() -> Vec<u8> {
build_frame(
0x40,
&[],
)
}
// Command 0x41 (Set Matrix Gain)
pub fn build_matrix_gain(
raw_input_channel: u8,
raw_output_channel: u8,
raw_gain: u16,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_input_channel(raw_input_channel)?;
validate_raw_output_channel(raw_output_channel)?;
validate_raw_matrix_gain(raw_gain)?;
let gain = split_u16(raw_gain);
Ok(build_frame(
0x41,
&[
raw_output_channel,
raw_input_channel,
gain[0],
gain[1],
],
))
}
// Command 0x48 (Set GEQ Band)
pub fn build_set_geq_band(
raw_channel: u8,
raw_frequency_band: u8,
raw_gain: u16,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_input_channel(raw_channel)?;
validate_raw_discrete_frequency(raw_frequency_band)?;
validate_raw_eq_gain(raw_gain)?;
let gain = split_u16(raw_gain);
Ok(build_frame(
0x48,
&[
raw_channel,
raw_frequency_band,
gain[0],
gain[1],
],
))
}
// Command 0x49 (Set GEQ Bypass)
pub fn build_set_geq_bypass(
raw_channel: u8,
bypass: u8,
) -> Result<Vec<u8>, ValidationError> {
validate_raw_input_channel(raw_channel)?;
Ok(build_frame(
0x49,
&[
raw_channel,
bypass,
],
))
}
+79
View File
@@ -0,0 +1,79 @@
use std::ops::RangeInclusive;
pub const DELAY_UNIT_RAW_RANGE: RangeInclusive<u8> = 0x00..=0x03;
pub const PRESET_INDEX_MAX: u8 = 20;
pub const CONFIG_CHUNK_INDEX_MAX: u8 = 0x1C;
pub const PRESET_NAME_LENGTH: usize = 14;
pub const PRESET_NAME_PADDING: u8 = 0x20;
pub const PASSWORD_LENGTH: usize = 4;
pub const PASSWORD_PADDING: u8 = 0x00;
pub const CHANNELS_RANGE: RangeInclusive<u8> = 0x00..=0x0B;
pub const INPUT_CHANNELS: RangeInclusive<u8> = 0x00..=0x03;
pub const OUTPUT_CHANNELS: RangeInclusive<u8> = 0x04..=0x0B;
pub const RATIO_RAW_RANGE: RangeInclusive<u16> = 0x0000..=0x000F;
pub const ATTACK_RAW_RANGE: RangeInclusive<u16> = 0x0000..=0x03E6;
pub const RELEASE_RAW_RANGE: RangeInclusive<u16> = 0x0009..=0x0BB7;
pub const KNEE_RAW_RANGE: RangeInclusive<u16> = 0x0000..=0x000C;
pub const THRESHOLD_RAW_RANGE: RangeInclusive<u16> = 0x0000..=0x00DC;
pub const FREQUENCY_RAW_RANGE: RangeInclusive<u16> = 0x0000..=0x012C;
pub const CROSSOVER_FILTER_RAW_RANGE: RangeInclusive<u8> = 0x00..=0x14;
pub const EQ_GAIN_RAW_RANGE: RangeInclusive<u16> = 0x0000..=0x00F0;
pub const PEQ_Q_RAW_RANGE: RangeInclusive<u8> = 0x00..=0x64;
pub const PEQ_INPUT_BAND_RANGE: RangeInclusive<u8> = 0x00..=0x07;
pub const PEQ_OUTPUT_BAND_RANGE:RangeInclusive<u8> = 0x00..=0x08;
pub const PEQ_FILTER_RAW_RANGE :RangeInclusive<u8> = 0x00..=0x08;
pub const CHANNEL_GAIN_RAW_RANGE: RangeInclusive<u16> = 0x0000..=0x0190;
pub const DELAY_RAW_RANGE: RangeInclusive<u8> = 0x00..=0xFF;
pub const INPUT_SOURCE_TYPE_RAW_RANGE: RangeInclusive<u8> = 0x00..=0x03;
pub const DISCRETE_FREQUENCY_RAW_RANGE: RangeInclusive<u8> = 0x00..=0x1E;
pub const MATRIX_INPUT_MASK_RANGE: RangeInclusive<u8> = 0x00..=0x0F;
pub const MATRIX_INPUT_A_MASK: u8 = 0x01;
pub const MATRIX_INPUT_B_MASK: u8 = 0x02;
pub const MATRIX_INPUT_C_MASK: u8 = 0x04;
pub const MATRIX_INPUT_D_MASK: u8 = 0x08;
pub const CHANNEL_NAME_LENGTH: usize = 8;
pub const CHANNEL_NAME_PADDING: u8 = 0x00;
pub const GATE_THRESHOLD_RAW_RANGE: RangeInclusive<u16> = 0x0000..=0x00B4;
pub const HOLD_RAW_RANGE: RangeInclusive<u16> = 0x0000..=0x03E6;
pub const MATRIX_GAIN_RAW_RANGE: RangeInclusive<u16> = 0x0000..=0x0118;
pub fn input_link_mask(channel: u8) -> Option<u8> {
match channel {
0x00 => Some(0x0F), // A,B,C,D
0x01 => Some(0x0E), // B,C,D
0x02 => Some(0x0C), // C,D
0x03 => Some(0x08), // D
_ => None,
}
}
pub fn output_link_mask(channel: u8) -> Option<u8> {
match channel {
0x04 => Some(0xFF), // 1..8
0x05 => Some(0xFE), // 2..8
0x06 => Some(0xFC), // 3..8
0x07 => Some(0xF8), // 4..8
0x08 => Some(0xF0), // 5..8
0x09 => Some(0xE0), // 6..8
0x0A => Some(0xC0), // 7..8
0x0B => Some(0x80), // 8
_ => None,
}
}
+119
View File
@@ -0,0 +1,119 @@
use crate::common::errors::FrameError;
pub fn checksum(payload: &[u8]) -> u8 {
let mut cs: u8 = 0x01;
for &b in payload {
cs ^= b;
}
cs
}
pub fn build_frame(cmd: u8, args: &[u8]) -> Vec<u8> {
let mut payload_body = Vec::with_capacity(1 + args.len());
payload_body.push(cmd);
payload_body.extend_from_slice(args);
let length = payload_body.len() as u8;
let mut inner = Vec::with_capacity(3 + payload_body.len());
inner.push(0x00);
inner.push(0x01);
inner.push(length);
inner.extend_from_slice(&payload_body);
let cs = checksum(&inner);
let mut frame = Vec::with_capacity(2 + inner.len() + 3);
frame.push(0x10);
frame.push(0x02);
frame.extend_from_slice(&inner);
frame.push(0x10);
frame.push(0x03);
frame.push(cs);
frame
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame {
pub command: u8,
pub payload: Vec<u8>,
}
pub fn extract_frame(data: &[u8]) -> Result<Frame, FrameError> {
if data.len() < 8 {
return Err(FrameError::new(format!(
"Frame too short: expected at least 8 bytes, got {}",
data.len()
)));
}
if data[0] != 0x10 || data[1] != 0x02 {
return Err(FrameError::new(format!(
"Invalid frame header: {:#04x} {:#04x}",
data[0], data[1]
)));
}
if data[2] == 0x00 {
return Err(FrameError::new("Not a response frame"));
}
if data[3] != 0x00 {
return Err(FrameError::new(format!(
"Invalid device id byte: {:#04x}",
data[3]
)));
}
let footer = find_footer(data)
.ok_or_else(|| FrameError::new("Missing frame footer"))?;
let expected = checksum(&data[2..footer]);
let received = data[data.len() - 1];
if expected != received {
return Err(FrameError::new(format!(
"Invalid checksum: expected 0x{:02X}, received 0x{:02X}",
expected, received
)));
}
Ok(Frame {
command: data[5],
payload: data[6..footer].to_vec(),
})
}
fn find_footer(data: &[u8]) -> Option<usize> {
if data.len() < 2 {
return None;
}
for i in 2..data.len() - 1 {
if data[i] == 0x10 && data[i + 1] == 0x03 {
return Some(i);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_then_extract_round_trips() {
let cmd = 0x34;
let args = [0x01, 0x02, 0x03];
let frame = build_frame(cmd, &args);
let decoded = extract_frame(&frame).unwrap();
assert_eq!(decoded.command, cmd);
assert_eq!(decoded.payload, vec![0x01, 0x02, 0x03]);
}
#[test]
fn too_short_is_an_error() {
let err = extract_frame(&[0x10, 0x02, 0x01]).unwrap_err();
assert!(err.0.contains("too short"));
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod frame;
pub mod commands;
pub mod constants;
pub mod validators;
+315
View File
@@ -0,0 +1,315 @@
use crate::common::errors::ValidationError;
use crate::common::validator::{validate_ascii_padded, validate_bitmask, validate_range};
use super::constants::*;
pub fn validate_raw_delay_unit(raw_unit: u8) -> Result<(), ValidationError> {
validate_range(
raw_unit,
DELAY_UNIT_RAW_RANGE,
|| format!("Invalid delay unit: {:#04x}", raw_unit)
)
}
pub fn validate_raw_preset_index(raw_index: u8) -> Result<(), ValidationError> {
validate_range(
raw_index,
0..=PRESET_INDEX_MAX,
|| format!("Invalid preset index {}", raw_index)
)
}
pub fn validate_raw_store_preset_index(raw_index: u8) -> Result<(), ValidationError> {
validate_range(
raw_index,
1..=PRESET_INDEX_MAX,
|| format!("Invalid preset index {}", raw_index)
)
}
pub fn validate_raw_get_preset_index(raw_index: u8) -> Result<(), ValidationError> {
validate_range(
raw_index,
0..PRESET_INDEX_MAX,
|| format!("Invalid preset index {}", raw_index)
)
}
pub fn validate_raw_preset_name(raw_name: &[u8]) -> Result<(), ValidationError> {
validate_ascii_padded(
raw_name,
PRESET_NAME_LENGTH,
PRESET_NAME_PADDING,
"Preset name"
)
}
pub fn validate_raw_config_chunk_index(raw_index: u8) -> Result<(), ValidationError> {
validate_range(
raw_index,
0..=CONFIG_CHUNK_INDEX_MAX,
|| format!("Invalid config index: {:#04x}", raw_index)
)
}
pub fn validate_raw_channel(channel: u8) -> Result<(), ValidationError> {
validate_range(
channel,
CHANNELS_RANGE,
|| format!("Invalid Channel: {:#04x}", channel),
)
}
pub fn validate_raw_input_channel(channel: u8) -> Result<(), ValidationError> {
validate_range(
channel,
INPUT_CHANNELS,
|| format!("Invalid Channel: {:#04x}", channel),
)
}
pub fn validate_raw_output_channel(channel: u8) -> Result<(), ValidationError> {
validate_range(
channel,
OUTPUT_CHANNELS,
|| format!("Invalid Channel: {:#04x}", channel),
)
}
pub fn validate_raw_copy_channels(source: u8, destination: u8) -> Result<(), ValidationError> {
validate_range(
source,
CHANNELS_RANGE,
|| format!("Invalid source channel: {:#04x}", source),
)?;
validate_range(
destination,
CHANNELS_RANGE,
|| format!("Invalid destination channel: {:#04x}", destination),
)?;
if source == destination {
return Err(ValidationError::value(
"Source channel and destination channel can't be the same"
));
}
let source_is_input = INPUT_CHANNELS.contains(&source);
let destination_is_input = INPUT_CHANNELS.contains(&destination);
if source_is_input != destination_is_input {
return Err(ValidationError::value(
"Channels need to be both inputs or outputs"
));
}
Ok(())
}
pub fn validate_raw_password(password: &[u8]) -> Result<(), ValidationError> {
validate_ascii_padded(
password,
PASSWORD_LENGTH,
PASSWORD_PADDING,
"Password"
)
}
pub fn validate_raw_compressor_ratio(raw_ratio: u16) -> Result<(), ValidationError> {
validate_range(
raw_ratio,
RATIO_RAW_RANGE,
|| format!("Invalid compressor ratio: {:#06x}", raw_ratio),
)
}
pub fn validate_raw_attack(raw_attack: u16) -> Result<(), ValidationError> {
validate_range(
raw_attack,
ATTACK_RAW_RANGE,
|| format!("Invalid attack: {:#06x}", raw_attack),
)
}
pub fn validate_raw_release(raw_release: u16) -> Result<(), ValidationError> {
validate_range(
raw_release,
RELEASE_RAW_RANGE,
|| format!("Invalid release: {:#06x}", raw_release),
)
}
pub fn validate_raw_compressor_knee(raw_knee: u16) -> Result<(), ValidationError> {
validate_range(
raw_knee,
KNEE_RAW_RANGE,
|| format!("Invalid compressor knee: {:#06x}", raw_knee),
)
}
pub fn validate_raw_threshold(raw_threshold: u16) -> Result<(), ValidationError> {
validate_range(
raw_threshold,
THRESHOLD_RAW_RANGE,
|| format!("Invalid compressor threshold: {:#06x}", raw_threshold),
)
}
pub fn validate_raw_frequency(raw_freq: u16) -> Result<(), ValidationError> {
validate_range(
raw_freq,
FREQUENCY_RAW_RANGE,
|| format!("Invalid frequency: {:#06x}", raw_freq),
)
}
pub fn validate_raw_crossover_filter(raw_filter_type: u8) -> Result<(), ValidationError> {
validate_range(
raw_filter_type,
CROSSOVER_FILTER_RAW_RANGE,
|| format!("Invalid filter: {:#04x}", raw_filter_type),
)
}
pub fn validate_raw_peq_band(raw_channel: u8, raw_band: u8) -> Result<(), ValidationError> {
if INPUT_CHANNELS.contains(&raw_channel) {
validate_range(
raw_band,
PEQ_INPUT_BAND_RANGE,
|| format!("Invalid input PEQ band: {:#04x}", raw_band),
)?;
} else if OUTPUT_CHANNELS.contains(&raw_channel) {
validate_range(
raw_band,
PEQ_OUTPUT_BAND_RANGE,
|| format!("Invalid output PEQ band: {:#04x}", raw_band),
)?;
}
Ok(())
}
pub fn validate_raw_peq_q(raw_q: u8) -> Result<(), ValidationError> {
validate_range(
raw_q,
PEQ_Q_RAW_RANGE,
|| format!("Invalid PEQ Q: {:#04x}", raw_q),
)
}
pub fn validate_raw_peq_filter(raw_filter_type: u8) -> Result<(), ValidationError> {
validate_range(
raw_filter_type,
PEQ_FILTER_RAW_RANGE,
|| format!("Invalid PEQ filter: {:#04x}", raw_filter_type),
)
}
pub fn validate_raw_eq_gain(raw_gain: u16) -> Result<(), ValidationError> {
validate_range(
raw_gain,
EQ_GAIN_RAW_RANGE,
|| format!("Invalid EQ gain: {:#06x}", raw_gain),
)
}
pub fn validate_raw_gain(raw_gain: u16) -> Result<(), ValidationError> {
validate_range(
raw_gain,
CHANNEL_GAIN_RAW_RANGE,
|| format!("Invalid channel gain: {:#06x}", raw_gain),
)
}
pub fn validate_raw_channel_delay(delay: u8) -> Result<(), ValidationError> {
validate_range(
delay,
DELAY_RAW_RANGE,
|| format!("Invalid channel delay: {:#04x}", delay)
)
}
pub fn validate_raw_input_source(input_type: u8) -> Result<(), ValidationError> {
validate_range(
input_type,
INPUT_SOURCE_TYPE_RAW_RANGE,
|| format!("Invalid source type: {:#04x}", input_type),
)
}
pub fn validate_raw_discrete_frequency(frequency: u8) -> Result<(), ValidationError> {
validate_range(
frequency,
DISCRETE_FREQUENCY_RAW_RANGE,
|| format!("Invalid frequency: {:#04x}", frequency),
)
}
pub fn validate_raw_matrix_bitmask(input_bitmask: u8) -> Result<(), ValidationError> {
validate_range(
input_bitmask,
MATRIX_INPUT_MASK_RANGE,
|| format!("Invalid matrix bitmask: {:#04x}", input_bitmask),
)
}
pub fn validate_raw_link_channel(
source_channel: u8,
destination_channel_bitmask: u8,
) -> Result<(), ValidationError> {
validate_raw_channel(source_channel)?;
let allowed_mask = if INPUT_CHANNELS.contains(&source_channel) {
input_link_mask(source_channel)
} else {
output_link_mask(source_channel)
}
.ok_or_else(|| {
ValidationError::value(
format!("Invalid link source channel: {:#04x}", source_channel)
)
})?;
validate_bitmask(
destination_channel_bitmask,
allowed_mask,
|| format!(
"Invalid link bitmask {:#04x} for source channel {:#04x}",
destination_channel_bitmask,
source_channel
),
)
}
pub fn validate_raw_channel_name(raw_name: &[u8]) -> Result<(), ValidationError> {
validate_ascii_padded(
raw_name,
CHANNEL_NAME_LENGTH,
CHANNEL_NAME_PADDING,
"Channel name",
)
}
pub fn validate_raw_gate_threshold(raw_threshold: u16) -> Result<(), ValidationError> {
validate_range(
raw_threshold,
GATE_THRESHOLD_RAW_RANGE,
|| format!("Invalid gate threshold: {:#06x}", raw_threshold),
)
}
pub fn validate_raw_hold(raw_hold: u16) -> Result<(), ValidationError> {
validate_range(
raw_hold,
HOLD_RAW_RANGE,
|| format!("Invalid hold: {:#06x}", raw_hold),
)
}
pub fn validate_raw_matrix_gain(raw_gain: u16) -> Result<(), ValidationError> {
validate_range(
raw_gain,
MATRIX_GAIN_RAW_RANGE,
|| format!("Invalid matrix gain: {:#06x}", raw_gain),
)
}
+8
View File
@@ -0,0 +1,8 @@
mod common;
mod dsp408;
pub use common::errors::DSPError;
pub use dsp408::DSP408;
pub mod types {
pub use crate::dsp408::client::types::*;
}
+40
View File
@@ -0,0 +1,40 @@
use std::net::IpAddr;
use std::str::FromStr;
use dsp_thomann::DSP408;
use dsp_thomann::types;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let host = IpAddr::from_str("192.168.49.30")?;
let mut dsp = DSP408::new(
host,
9761,
1,
)?;
println!("Connecting to DSP...");
match dsp.connect() {
Ok(_) => {
println!("Connected successfully");
}
Err(e) => {
eprintln!("Connection failed: {}", e);
return Err(Box::new(e));
}
}
println!("DSP connected.");
dsp.set_mute(types::Channel::Input(types::InputChannel::InA), true)?;
dbg!(dsp.state()?);
println!("Disconnecting...");
dsp.disconnect();
println!("Disconnected");
Ok(())
}