commit ad7da5f7ee66a72085f9aa608053a075806f5b46 Author: LucasDLTG Date: Mon Jul 20 19:29:57 2026 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b2e0887 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ecb8674 --- /dev/null +++ b/Cargo.toml @@ -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" \ No newline at end of file diff --git a/src/common/errors.rs b/src/common/errors.rs new file mode 100644 index 0000000..8587e8e --- /dev/null +++ b/src/common/errors.rs @@ -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) -> 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) -> 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) -> Self { + Self::Value(msg.into()) + } + + pub fn type_(msg: impl Into) -> 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) -> 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) -> 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), +} diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..b0f995d --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,2 @@ +pub mod errors; +pub(crate) mod validator; \ No newline at end of file diff --git a/src/common/validator.rs b/src/common/validator.rs new file mode 100644 index 0000000..500675a --- /dev/null +++ b/src/common/validator.rs @@ -0,0 +1,54 @@ +use super::errors::ValidationError; + +pub(crate) fn validate_range( + value: T, + range: R, + message: impl Fn() -> String, +) -> Result<(), ValidationError> +where + T: PartialOrd, + R: std::ops::RangeBounds, +{ + 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(()) +} diff --git a/src/dsp408/client/constants.rs b/src/dsp408/client/constants.rs new file mode 100644 index 0000000..f8752dc --- /dev/null +++ b/src/dsp408/client/constants.rs @@ -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 = 1..=999; +pub const RELEASE_MS_RANGE: RangeInclusive = 10..=3000; +pub const HOLD_MS_RANGE: RangeInclusive = 10..=999; +pub const GATE_THRESHOLD_DB_RANGE: RangeInclusive = -90.0..=0.0; +pub const EQ_GAIN_DB_RANGE: RangeInclusive = -12.0..=12.0; +pub const COMPRESSOR_KNEE_DB_RANGE: RangeInclusive = 0..=12; +pub const THRESHOLD_DB_RANGE: RangeInclusive = -90.0..=20.0; + +pub const FREQUENCY_HZ_RANGE: RangeInclusive = 19.7..=20160.0; + +pub const PEQ_Q_RANGE: RangeInclusive = 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 = -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 = -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 { + 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 + } + } +} diff --git a/src/dsp408/client/dsp408.rs b/src/dsp408/client/dsp408.rs new file mode 100644 index 0000000..1830df9 --- /dev/null +++ b/src/dsp408/client/dsp408.rs @@ -0,0 +1,2015 @@ +use std::{ + collections::HashMap, io::{Read, Write}, net::{IpAddr, SocketAddr, TcpStream}, sync::{Mutex, OnceLock}, time::Duration, fmt, +}; + +use strum::{EnumCount, IntoEnumIterator}; + +use crate::common::errors::{DSPError, ValidationError}; +use crate::dsp408::client::types; +use crate::dsp408::client::constants; +use crate::dsp408::protocol::frame::extract_frame; +use crate::dsp408::protocol::commands; +use crate::dsp408::mapper::response::map_response; +use crate::dsp408::mapper; + +pub const SOCKET_TIMEOUT: Duration = Duration::from_secs(20); +pub const RECV_BUFSIZE: usize = 4096; + +static INSTANCES: OnceLock>> = OnceLock::new(); + +fn instances() -> &'static Mutex> { + INSTANCES.get_or_init(|| Mutex::new(HashMap::new())) +} + +pub struct DSP408 { + connection: types::DeviceConnection, + socket: Option, + lock: Mutex<()>, + connected: bool, + state: Option, +} + +impl DSP408 { + + pub fn new( + host: IpAddr, + port: u16, + device_id: u8, + ) -> Result { + + if matches!(host, IpAddr::V6(_)) { + return Err(DSPError::InvalidHost( + "DSP doesn't support IPv6" + )); + } + + if port == 0 { + return Err(DSPError::InvalidPort( + "Port must be between 1 and 65535" + )); + } + + + if device_id == 0 || device_id > 254 { + return Err(DSPError::InvalidDeviceId( + "Device ID must be between 1 and 254" + )); + } + + + Ok(Self { + connection: types::DeviceConnection { + host, + port, + device_id, + }, + + socket: None, + + lock: Mutex::new(()), + + connected: false, + + state: None, + }) + } + + fn send_raw( + &mut self, + data: &[u8], + ) -> Result<(), DSPError> { + + + let _guard = + self.lock.lock().unwrap(); + + + let socket = + self.socket + .as_mut() + .ok_or( + DSPError::NotConnected + )?; + + + socket.write_all(data)?; + + Ok(()) + } + + fn recv_raw( + &mut self + ) -> Result, DSPError> { + + let socket = + self.socket + .as_mut() + .ok_or( + DSPError::NotConnected + )?; + + + let mut buffer = + vec![0u8; RECV_BUFSIZE]; + + + match socket.read(&mut buffer) { + + Ok(0) => + Err(DSPError::ConnectionClosed), + + Ok(size) => { + buffer.truncate(size); + Ok(buffer) + }, + + Err(e) + if e.kind() + == + std::io::ErrorKind::TimedOut => + { + Err(DSPError::Timeout) + }, + + Err(e) => + Err(DSPError::Io(e)), + } + } + + pub fn send_recv( + &mut self, + command: &[u8], + ) -> Result { + + self.send_raw(command)?; + + let raw = + self.recv_raw()?; + + let frame = + extract_frame(&raw)?; + + let response = + map_response(&frame)?; + + Ok(response) + } + + pub fn connected(&self) -> bool { + self.connected + } + + pub fn state(&self) -> Result<&types::DSPState, DSPError> { + self.state + .as_ref() + .ok_or(DSPError::NotConnected) + } + + fn state_mut(&mut self) -> Result<&mut types::DSPState, DSPError> { + self.state + .as_mut() + .ok_or(DSPError::NotConnected) + } + + // ---------- Commands ---------- + // Command `0x10` + pub fn connect(&mut self) -> Result<(), DSPError> { + + let key = ( + self.connection.host, + self.connection.device_id + ); + + { + let map = instances() + .lock() + .unwrap(); + + if map.contains_key(&key) { + return Err(DSPError::AlreadyConnected); + } + } + + let addr = SocketAddr::new( + self.connection.host, + self.connection.port + ); + + let stream = + TcpStream::connect_timeout( + &addr, + SOCKET_TIMEOUT + )?; + + stream.set_read_timeout(Some(SOCKET_TIMEOUT))?; + + self.socket = Some(stream); + + let handshake = commands::build_handshake(); + + self.send_raw(&handshake)?; + + let start = std::time::Instant::now(); + + while start.elapsed() < SOCKET_TIMEOUT { + + let raw = self.recv_raw()?; + let frame = extract_frame(&raw)?; + let response = map_response(&frame)?; + + if matches!(response, types::DeviceResponseType::HandshakeAck(_)) { + + instances() + .lock() + .unwrap() + .insert(key, ()); + + + self.connected = true; + + let name = self.get_device_name()?; + let flags = self.get_device_flags()?; + let current_preset = self.get_current_preset()?; + let modified = self.get_preset_modification_status()?; + let mut preset_names = Vec::with_capacity(constants::PRESET_INDEX_MAX); + + for i in 0..constants::PRESET_INDEX_MAX { + preset_names.push( + self.get_preset_name(i + 1)? + ); + } + + let state = self.read_config_state()?; + + self.state = Some(types::DSPState { + name, + flags, + presets: types::PresetBank { + current_index: current_preset as usize, + names: preset_names, + modified: modified.modified, + }, + current_config: state, + }); + + return Ok(()); + } + } + + self.disconnect(); + + Err(DSPError::HandshakeFailed) + } + + // Command `0x11` + pub fn disconnect(&mut self) { + + let key = ( + self.connection.host, + self.connection.device_id + ); + + if let Some(mut socket) = self.socket.take() { + + let _ = + socket.write_all( + &commands::build_disconnect() + ); + + let _ = + socket.shutdown( + std::net::Shutdown::Both + ); + } + + + instances() + .lock() + .unwrap() + .remove(&key); + + + self.connected = false; + } + + // Command `0x12` + fn request_acknowledgement(&mut self) -> Result { + let request = commands::build_acknowledgement(); + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => Ok(ack.success), + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x13` (Device Name) + pub fn get_device_name(&mut self) -> Result { + let request = commands::build_request_device_name(); + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::DeviceInfo(info) => Ok(info.name), + other => Err(DSPError::UnexpectedResponse(format!( + "Expected DeviceInfo, got {:?}", + other + ))), + } + } + + // Command `0x14` (Current Preset Index) + pub fn get_current_preset(&mut self) -> Result { + let request = commands::build_get_current_preset(); + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::CurrentPreset(preset) => Ok(preset.index), + other => Err(DSPError::UnexpectedResponse(format!( + "Expected CurrentPreset, got {:?}", + other + ))), + } + } + + // Command `0x15` (Set Delay Unit) + pub fn set_delay_unit(&mut self, unit: types::DelayUnit) -> Result { + let raw_unit = mapper::delay_unit_to_raw(unit)?; + + let request = commands::build_set_delay_unit(raw_unit)?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + self.state_mut()?.current_config.delay_unit = unit; + + Ok(true) + } + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x20` (Recall) + pub fn recall_preset(&mut self, preset_index: usize) -> Result { + let raw_index = mapper::preset_index_to_raw(preset_index)?; + + let request = commands::build_recall(raw_index)?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let config = self.read_config_state()?; + + self.state_mut()?.current_config = config; + self.state_mut()?.presets.current_index = preset_index; + + Ok(true) + } + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x21` (Store Preset) + pub fn store_current_configuration( + &mut self, + preset_index: usize, + force: bool, + ) -> Result { + let raw_index = mapper::preset_index_to_raw(preset_index)?; + + if preset_index == 0 && !force { + return Ok(false); + } + + let request = commands::build_store_current_configuration(raw_index)?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + if !self.request_acknowledgement()? { + return Err(DSPError::OperationFailed( + "Store Configuration fails".into() + )); + } + + let state = self.state_mut()?; + state.presets.modified[state.presets.current_index] = true; + + Ok(true) + } + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x22` (Get Preset Modification Status) + pub fn get_preset_modification_status(&mut self) -> Result { + let request = commands::build_preset_modification_status(); + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::ModifiedPreset(status) => Ok(status), + other => Err(DSPError::UnexpectedResponse(format!( + "Expected ModifiedPreset, got {:?}", + other + ))), + } + } + + // Command `0x23` (Factory Reset) + pub fn factory_reset(&mut self, force: bool) -> Result { + if !force { + return Ok(false); + } + + let request = commands::build_factory_reset(); + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + Ok(ack.success) + } + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x26` (Store Preset Name) + pub fn set_current_preset_name(&mut self, name: &str) -> Result { + let raw_name = mapper::preset_name_to_raw(name)?; + + let request = commands::build_store_current_preset_name(&raw_name)?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + state.presets.names[state.presets.current_index] = name.to_string(); + + Ok(true) + } + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x27 - 0x24` (Get Config Chunk) + fn get_config_chunk(&mut self, index: usize) -> Result { + let raw_index = mapper::config_chunk_to_raw(index)?; + + let request = commands::build_config_chunk(raw_index)?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::ConfigChunk(config) => { + Ok(config) + } + other => Err(DSPError::UnexpectedResponse(format!( + "Expected ConfigChunk, got {:?}", + other + ))), + } + } + + // Command `0x29` (Preset Name) + pub fn get_preset_name(&mut self, preset_index: usize) -> Result { + let raw_index = mapper::preset_index_without_factory_to_raw(preset_index)?; + + let request = commands::build_get_preset_name(raw_index)?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::PresetName(name) => Ok(name.name), + other => Err(DSPError::UnexpectedResponse(format!( + "Expected PresetName, got {:?}", + other + ))), + } + } + + // Command `0x2A` (Copy Channel) + pub fn copy_channel( + &mut self, + channel_1: types::Channel, + channel_2: types::Channel, + ) -> Result { + + let (raw_channel_1, raw_channel_2) = mapper::copy_channel_to_raw(channel_1, channel_2)?; + + let request = commands::build_copy_channel( + raw_channel_1, + raw_channel_2, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + Ok(ack.success) + } + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x2C` (Device Flags) + pub fn get_device_flags(&mut self) -> Result { + let request = commands::build_get_device_flags(); + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::DeviceFlags(flags) => Ok(flags), + other => Err(DSPError::UnexpectedResponse(format!( + "Expected DeviceFlags, got {:?}", + other + ))), + } + } + + // Command `0x2D` (Unlock Device) + pub fn unlock_device(&mut self, password: &str) -> Result { + let raw_password = mapper::password_to_raw(password)?; + + let request = commands::build_unlock_device(&raw_password)?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::AuthenticationResult(result) => { + if !result.success { + return Ok(false); + } + + let state = self.state_mut()?; + state.flags.is_locked = false; + + Ok(true) + } + other => Err(DSPError::UnexpectedResponse(format!( + "Expected AuthenticationResult, got {:?}", + other + ))), + } + } + + // Command `0x2F` (Lock Device) + pub fn lock_device(&mut self, password: &str) -> Result { + let raw_password = mapper::password_to_raw(password)?; + + let request = commands::build_lock_device(&raw_password)?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + state.flags.is_locked = true; + + Ok(true) + } + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x30` (Set Compressor) + pub fn set_compressor( + &mut self, + channel: types::OutputChannel, + ratio: types::Ratio, + attack: u16, + release: u16, + knee: u16, + threshold: f32, + ) -> Result { + + let raw_channel = mapper::output_channel_to_raw(channel)?; + let raw_ratio = mapper::ratio_to_raw(ratio)?; + let raw_attack = mapper::attack_ms_to_raw(attack)?; + let raw_release = mapper::release_ms_to_raw(release)?; + let raw_knee = mapper::knee_db_to_raw(knee)?; + let raw_threshold = mapper::threshold_db_to_raw(threshold)?; + + let request = commands::build_set_compressor( + raw_channel, + raw_ratio, + raw_attack, + raw_release, + raw_knee, + raw_threshold, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + state + .current_config + .output_state_mut(channel) + .compressor = types::Compressor { + threshold, + ratio, + attack, + release, + knee, + }; + + Ok(true) + } + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x31` (Set Low-Pass Filter) + pub fn set_low_pass( + &mut self, + channel: types::Channel, + freq: f32, + filter_type: types::CrossoverFilter, + ) -> Result { + + let raw_channel = mapper::channel_to_raw(channel)?; + let raw_freq = mapper::frequency_hz_to_raw(freq)?; + let raw_filter = mapper::crossover_filter_to_raw(filter_type)?; + + let request = commands::build_low_pass( + raw_channel, + raw_freq, + raw_filter, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + match channel { + types::Channel::Input(input) => { + state + .current_config + .input_state_mut(input) + .crossover + .low_pass = types::Crossover { + frequency: freq, + slope: filter_type, + }; + } + + types::Channel::Output(output) => { + state + .current_config + .output_state_mut(output) + .crossover + .low_pass = types::Crossover { + frequency: freq, + slope: filter_type, + }; + } + } + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x32` (Set High-Pass Filter) + pub fn set_high_pass( + &mut self, + channel: types::Channel, + freq: f32, + filter_type: types::CrossoverFilter, + ) -> Result { + + let raw_channel = mapper::channel_to_raw(channel)?; + let raw_freq = mapper::frequency_hz_to_raw(freq)?; + let raw_filter = mapper::crossover_filter_to_raw(filter_type)?; + + let request = commands::build_high_pass( + raw_channel, + raw_freq, + raw_filter, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + match channel { + types::Channel::Input(input) => { + state + .current_config + .input_state_mut(input) + .crossover + .high_pass = types::Crossover { + frequency: freq, + slope: filter_type, + }; + } + + types::Channel::Output(output) => { + state + .current_config + .output_state_mut(output) + .crossover + .high_pass = types::Crossover { + frequency: freq, + slope: filter_type, + }; + } + } + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x33` (Set PEQ Band) + pub fn set_peq_band( + &mut self, + channel: types::Channel, + band: types::PEQBand, + gain_db: f32, + freq_hz: f32, + q: f32, + filter_type: types::PEQFilter, + bypass: bool, + ) -> Result { + + let raw_channel = mapper::channel_to_raw(channel)?; + let raw_band = mapper::peq_band_to_raw(band, channel)?; + let raw_gain = mapper::eq_gain_db_to_raw(gain_db)?; + let raw_frequency = mapper::frequency_hz_to_raw(freq_hz)?; + let raw_q = mapper::peq_q_to_raw(q, filter_type)?; + let raw_filter = mapper::peq_filter_to_raw(filter_type)?; + + let request = commands::build_peq_band( + raw_channel, + raw_band, + raw_gain, + raw_frequency, + raw_q, + raw_filter, + bypass as u8, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + let peq = types::PEQ { + gain: gain_db, + frequency: freq_hz, + q, + filter_type: filter_type, + bypass, + }; + + match channel { + types::Channel::Input(input) => { + state + .current_config + .input_state_mut(input) + .peq_chain + .bands[band as usize] = peq; + } + + types::Channel::Output(output) => { + state + .current_config + .output_state_mut(output) + .peq_chain + .bands[band as usize] = peq; + } + } + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x34` (Set Gain) + pub fn set_channel_gain( + &mut self, + channel: types::Channel, + db: f32, + ) -> Result { + + let raw_channel = mapper::channel_to_raw(channel)?; + let raw_db = mapper::gain_db_to_raw(db)?; + + let request = commands::build_gain( + raw_channel, + raw_db, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + match channel { + types::Channel::Input(input) => { + state + .current_config + .input_state_mut(input) + .gain = db; + } + + types::Channel::Output(output) => { + state + .current_config + .output_state_mut(output) + .gain = db; + } + } + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x35` (Set Mute) + pub fn set_mute( + &mut self, + channel: types::Channel, + muted: bool, + ) -> Result { + + let raw_channel = mapper::channel_to_raw(channel)?; + + let request = commands::build_mute( + raw_channel, + muted as u8, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + match channel { + types::Channel::Input(input) => { + state + .current_config + .input_state_mut(input) + .mute = muted; + } + + types::Channel::Output(output) => { + state + .current_config + .output_state_mut(output) + .mute = muted; + } + } + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x36` (Invert Gain) + pub fn set_channel_inverse_gain( + &mut self, + channel: types::Channel, + inverted: bool, + ) -> Result { + + let raw_channel = mapper::channel_to_raw(channel)?; + + let request = commands::build_inverted_gain( + raw_channel, + inverted as u8, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + match channel { + types::Channel::Input(input) => { + state + .current_config + .input_state_mut(input) + .phase_inverted = inverted; + } + + types::Channel::Output(output) => { + state + .current_config + .output_state_mut(output) + .phase_inverted = inverted; + } + } + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x38` (Set Delay) + pub fn set_channel_delay( + &mut self, + channel: types::Channel, + delay: f32, + unit: types::DelayUnit, + ) -> Result { + + let raw_channel = mapper::channel_to_raw(channel)?; + let raw_delay = mapper::delay_to_raw(delay, unit)?; + + let request = commands::build_set_delay( + raw_channel, + raw_delay, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + let delay = mapper::raw_to_delay(raw_delay, unit)?; + + match channel { + types::Channel::Input(input) => { + state + .current_config + .input_state_mut(input) + .delay = delay; + } + + types::Channel::Output(output) => { + state + .current_config + .output_state_mut(output) + .delay = delay; + } + } + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x39` (Set Input Source) + pub fn set_input_source( + &mut self, + input_source: types::InputSource, + ) -> Result { + + let raw_source = mapper::input_source_to_raw(input_source.r#type)?; + let raw_frequency = mapper::frequency_value_to_raw(input_source.frequency)?; + + let request = commands::build_set_input_source( + raw_source, + raw_frequency, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + state.current_config.input_source = input_source; + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x3A` (Matrix Routing) + pub fn set_matrix_routing( + &mut self, + output: types::OutputChannel, + inputs: Vec, + ) -> Result { + + let raw_output = mapper::output_channel_to_raw(output)?; + let raw_inputs = mapper::matrix_to_raw(&inputs)?; + + let request = commands::build_matrix_routing( + raw_output, + raw_inputs, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + state + .current_config + .output_state_mut(output) + .matrix_routes + .connected = inputs; + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x3B` (Link Channel) + pub fn link_channels( + &mut self, + source: types::Channel, + destinations: Vec, + ) -> Result { + + let raw_source = mapper::channel_to_raw(source)?; + let bitmask = mapper::link_channels_to_raw(source, &destinations)?; + + // Copy source configuration before linking + for destination in &destinations { + if *destination == source { + continue; + } + + if !self.copy_channel(source, *destination)? { + return Ok(false); + } + } + + // Link source + let request = commands::build_link_channel( + raw_source, + bitmask, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + } + + other => { + return Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))); + } + } + + + // Clear destination links + for destination in &destinations { + if *destination == source { + continue; + } + + let raw_destination = mapper::channel_to_raw(*destination)?; + + let request = commands::build_link_channel( + raw_destination, + 0x00, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + } + + other => { + return Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))); + } + } + } + + if !self.request_acknowledgement()? { + return Ok(false); + } + + let config = self.read_config_state()?; + + self.state_mut()?.current_config = config; + + Ok(true) + } + + // Command `0x3C` (Bypass PEQ) + pub fn set_bypass_peq( + &mut self, + channel: types::Channel, + bypass: bool, + ) -> Result { + + let raw_channel = mapper::channel_to_raw(channel)?; + + let request = commands::build_bypass_peq( + raw_channel, + bypass as u8, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + match channel { + types::Channel::Input(input) => { + state.current_config.input_state_mut(input) + .peq_chain + .bypass = bypass; + } + + types::Channel::Output(output) => { + state.current_config.output_state_mut(output) + .peq_chain + .bypass = bypass; + } + } + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x3D` (Set Channel Name) + pub fn set_channel_name( + &mut self, + channel: types::Channel, + name: &str, + ) -> Result { + + let raw_channel = mapper::channel_to_raw(channel)?; + let raw_name = mapper::channel_name_to_raw(name)?; + + let request = commands::build_store_channel_name( + raw_channel, + &raw_name, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + match channel { + types::Channel::Input(input) => { + state.current_config.input_state_mut(input) + .name = name.to_string(); + } + + types::Channel::Output(output) => { + state.current_config.output_state_mut(output) + .name = name.to_string(); + } + } + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x3E` (Set Gate Parameters) + pub fn set_gate( + &mut self, + channel: types::InputChannel, + threshold: f32, + attack: u16, + hold: u16, + release: u16, + ) -> Result { + + let raw_channel = mapper::input_channel_to_raw(channel)?; + let raw_threshold = mapper::gate_threshold_db_to_raw(threshold)?; + let raw_attack = mapper::attack_ms_to_raw(attack)?; + let raw_hold = mapper::hold_ms_to_raw(hold)?; + let raw_release = mapper::release_ms_to_raw(release)?; + + let request = commands::build_set_gate( + raw_channel, + raw_threshold, + raw_attack, + raw_hold, + raw_release, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + state + .current_config + .input_state_mut(channel) + .gate = types::Gate { + attack, + release, + hold, + threshold, + }; + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x3F` (Set Output Limiter) + pub fn set_limiter( + &mut self, + channel: types::OutputChannel, + threshold: f32, + attack: u16, + release: u16, + ) -> Result { + + let raw_channel = mapper::output_channel_to_raw(channel)?; + let raw_threshold = mapper::threshold_db_to_raw(threshold)?; + let raw_attack = mapper::attack_ms_to_raw(attack)?; + let raw_release = mapper::release_ms_to_raw(release)?; + + let request = commands::build_set_limiter( + raw_channel, + raw_threshold, + raw_attack, + raw_release, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + state + .current_config + .output_state_mut(channel) + .limiter = types::Limiter { + threshold, + attack, + release, + }; + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x40` (Get Level Meters) + pub fn get_meter_levels( + &mut self, + ) -> Result { + + let request = commands::build_get_meter_levels(); + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Meters(meters) => Ok(meters), + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Meters, got {:?}", + other + ))), + } + } + + // Command `0x41` (Set Matrix Gain) + pub fn set_matrix_gain( + &mut self, + input_channel: types::InputChannel, + output_channel: types::OutputChannel, + gain: f32, + ) -> Result { + + let raw_input_channel = mapper::input_channel_to_raw(input_channel)?; + let raw_output_channel = mapper::output_channel_to_raw(output_channel)?; + let raw_gain = mapper::matrix_gain_db_to_raw(gain)?; + + let request = commands::build_matrix_gain( + raw_input_channel, + raw_output_channel, + raw_gain, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + let index = types::InputChannel::iter() + .position(|x| x == input_channel) + .ok_or_else(|| DSPError::Validation(ValidationError::Value( + "Invalid input channel".to_string() + )))?; + + state + .current_config + .output_state_mut(output_channel) + .matrix_routes + .gains[index] = gain; + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x48` (Set GEQ Band) + pub fn set_geq_band( + &mut self, + input_channel: types::InputChannel, + frequency_band: types::DiscreteFrequency, + gain: f32, + ) -> Result { + + let raw_input_channel = mapper::input_channel_to_raw(input_channel)?; + let raw_frequency_band = mapper::frequency_value_to_raw(frequency_band)?; + let raw_gain = mapper::eq_gain_db_to_raw(gain)?; + + let request = commands::build_set_geq_band( + raw_input_channel, + raw_frequency_band, + raw_gain, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + let index = types::DiscreteFrequency::iter() + .position(|x| x == frequency_band) + .ok_or_else(|| DSPError::Validation(ValidationError::Value( + "Invalid GEQ frequency band".to_string() + )))?; + + state + .current_config + .input_state_mut(input_channel) + .geq + .gains[index] = gain; + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + // Command `0x49` (Set GEQ Bypass) + pub fn set_geq_bypass( + &mut self, + input_channel: types::InputChannel, + bypass: bool, + ) -> Result { + + let raw_input_channel = mapper::input_channel_to_raw(input_channel)?; + + let request = commands::build_set_geq_bypass( + raw_input_channel, + bypass as u8, + )?; + + let resp = self.send_recv(&request)?; + + match resp { + types::DeviceResponseType::Acknowledgement(ack) => { + if !ack.success { + return Ok(false); + } + + let state = self.state_mut()?; + + state + .current_config + .input_state_mut(input_channel) + .geq + .bypass = bypass; + + Ok(true) + } + + other => Err(DSPError::UnexpectedResponse(format!( + "Expected Acknowledgement, got {:?}", + other + ))), + } + } + + fn read_config_state(&mut self) -> Result { + let mut raw = Vec::new(); + + for i in 0..=constants::CONFIG_CHUNK_INDEX_MAX { + let chunk = self.get_config_chunk(i)?; + raw.extend_from_slice(&chunk.data); + } + + let config = build_config_from_chunk(&raw)?; + + if !self.request_acknowledgement()? { + return Err(DSPError::OperationFailed( + "DSP initialization".into() + )); + } + + Ok(config) + } + +} + +impl Drop for DSP408 { + fn drop(&mut self) { + self.disconnect(); + } +} + +impl fmt::Debug for DSP408 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DSP408") + .field("host", &self.connection.host) + .field("port", &self.connection.port) + .field("device_id", &self.connection.device_id) + .field("connected", &self.connected()) + .finish() + } +} + +struct BinaryReader<'a> { + data: &'a [u8], +} + +impl<'a> BinaryReader<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data } + } + + fn pop(&mut self, n: usize) -> Result<&'a [u8], DSPError> { + if self.data.len() < n { + return Err(DSPError::InvalidRequest( + "buffer underflow".into() + )); + } + + let (value, rest) = self.data.split_at(n); + self.data = rest; + + Ok(value) + } + + fn read_u8(&mut self) -> Result { + Ok(self.pop(1)?[0]) + } + + fn read_u16(&mut self) -> Result { + Ok(u16::from_le_bytes( + self.pop(2)?.try_into().unwrap() + )) + } + + fn skip(&mut self, n: usize) -> Result<(), DSPError> { + self.pop(n)?; + Ok(()) + } +} + +fn apply_flag( + r: &mut BinaryReader, + targets: &mut [T], + setter: impl Fn(&mut T, bool), +) -> Result<(), DSPError> { + let bitmap = r.read_u16()?; + + for (index, target) in targets.iter_mut().enumerate() { + setter(target, bitmap & (1 << index) != 0); + } + + Ok(()) +} + +fn read_gate(r: &mut BinaryReader) -> Result { + Ok(types::Gate { + attack: mapper::raw_to_attack_ms(r.read_u16()?)?, + release: mapper::raw_to_release_ms(r.read_u16()?)?, + hold: mapper::raw_to_hold_ms(r.read_u16()?)?, + threshold: mapper::raw_to_gate_threshold_db(r.read_u16()?)?, + }) +} + +fn read_geq( + r: &mut BinaryReader, +) -> Result { + let gains = types::DiscreteFrequency::iter() + .map(|_| { + let raw = r.read_u16()?; + Ok(mapper::raw_to_eq_gain(raw)?) + }) + .collect::, DSPError>>()?; + + Ok(types::GraphicEQ { + gains, + bypass: false, + }) +} + +fn read_peq_chain( + r: &mut BinaryReader, + band_count: usize, +) -> Result { + let bands = (0..band_count) + .map(|_| { + Ok(types::PEQ { + gain: mapper::raw_to_eq_gain(r.read_u16()?)?, + frequency: mapper::raw_to_frequency_hz(r.read_u16()?)?, + q: mapper::raw_to_peq_q(r.read_u8()?)?, + filter_type: mapper::raw_to_peq_filter(r.read_u8()?)?, + bypass: false, + }) + }) + .collect::, DSPError>>()?; + + Ok(types::PEQChain { + bands, + bypass: false, + }) +} + +fn read_crossover( + r: &mut BinaryReader, +) -> Result { + let hp_freq = r.read_u16()?; + let lp_freq = r.read_u16()?; + let hp_filter = r.read_u8()?; + let lp_filter = r.read_u8()?; + Ok(types::CrossoverFilters { + high_pass: types::Crossover { + frequency: mapper::raw_to_frequency_hz(hp_freq)?, + slope: mapper::raw_to_crossover_filter(hp_filter)?, + }, + low_pass: types::Crossover { + frequency: mapper::raw_to_frequency_hz(lp_freq)?, + slope: mapper::raw_to_crossover_filter(lp_filter)?, + }, + }) +} + +fn read_compressor( + r: &mut BinaryReader, +) -> Result { + Ok(types::Compressor { + ratio: mapper::raw_to_ratio(r.read_u16()?)?, + attack: mapper::raw_to_attack_ms(r.read_u16()?)?, + release: mapper::raw_to_release_ms(r.read_u16()?)?, + knee: mapper::raw_to_knee_db(r.read_u16()?)?, + threshold: mapper::raw_to_threshold_db(r.read_u16()?)?, + }) +} + +fn read_limiter( + r: &mut BinaryReader, +) -> Result { + let attack = mapper::raw_to_attack_ms(r.read_u16()?)?; + let release = mapper::raw_to_release_ms(r.read_u16()?)?; + + r.skip(2)?; + + let threshold = mapper::raw_to_threshold_db(r.read_u16()?)?; + + Ok(types::Limiter { + attack, + release, + threshold, + }) +} + +fn build_config_from_chunk(payload: &[u8]) -> Result { + let mut r = BinaryReader::new(payload); + + r.skip(2)?; // flags (unused) + + let _preset_name = mapper::raw_to_preset_name(r.pop(constants::PRESET_NAME_LENGTH)?)?; + + let mut input_channels = vec![types::InputChannelState::default(); types::InputChannel::COUNT]; + + let mut output_channels = vec![types::OutputChannelState::default(); types::OutputChannel::COUNT]; + + let mut input_delays_raw: Vec = Vec::new(); + let mut output_delays_raw: Vec = Vec::new(); + + // ============================ + // INPUT CHANNELS + // ============================ + + for (channel, inp) in types::InputChannel::iter().zip(input_channels.iter_mut()) { + inp.name = mapper::raw_to_channel_name(r.pop(constants::CHANNEL_NAME_LENGTH)?)?; + + inp.gate = read_gate(&mut r)?; + + inp.geq = read_geq(&mut r)?; + + inp.peq_chain = read_peq_chain(&mut r, constants::PEQ_INPUT_BAND_COUNT)?; + + inp.crossover = read_crossover(&mut r)?; + + inp.gain = mapper::raw_to_gain(r.read_u16()?)?; + + inp.phase_inverted = r.read_u8()? != 0; + + r.skip(2)?; // reserved + + input_delays_raw.push(r.read_u8()?); + + r.skip(1)?; + + inp.linked_channels = mapper::raw_to_link_channels(mapper::channel_to_raw(types::Channel::Input(channel))?, r.read_u8()?)?; + } + + // ============================ + // OUTPUT CHANNELS + // ============================ + + for (channel, out) in types::OutputChannel::iter().zip(output_channels.iter_mut()) { + out.name = mapper::raw_to_channel_name(r.pop(constants::CHANNEL_NAME_LENGTH)?)?; + + r.skip(1)?; + + out.matrix_routes = types::MatrixRoutes { + connected: + mapper::raw_to_matrix( + r.read_u8()? + )?, + + gains: (0..4) + .map(|_| { + let raw = r.read_u16()?; + Ok::(mapper::raw_to_matrix_gain(raw)?) + }) + .collect::, _>>()?, + }; + + out.crossover = read_crossover(&mut r)?; + + out.peq_chain = read_peq_chain(&mut r, constants::PEQ_OUTPUT_BAND_COUNT)?; + + out.compressor = read_compressor(&mut r)?; + + out.limiter = read_limiter(&mut r)?; + + out.gain = mapper::raw_to_gain(r.read_u16()?)?; + + out.phase_inverted = r.read_u8()? != 0; + + r.skip(2)?; // reserved + + output_delays_raw.push(r.read_u8()?); + + r.skip(1)?; + + out.linked_channels = mapper::raw_to_link_channels(mapper::channel_to_raw(types::Channel::Output(channel))?, r.read_u8()?)?; + } + + // ============================ + // MUTE FLAGS + // ============================ + + apply_flag( + &mut r, + &mut input_channels, + |ch, value| ch.mute = value, + )?; + + apply_flag( + &mut r, + &mut output_channels, + |ch, value| ch.mute = value, + )?; + + // ============================ + // PEQ BYPASS FLAGS + // ============================ + + for inp in &mut input_channels { + apply_flag( + &mut r, + inp.peq_chain.bands.as_mut_slice(), + |band, value| band.bypass = value, + )?; + } + + for out in &mut output_channels { + apply_flag( + &mut r, + out.peq_chain.bands.as_mut_slice(), + |band, value| band.bypass = value, + )?; + } + + // ============================ + // INPUT SOURCE + // ============================ + + let input_source = types::InputSource { + r#type: mapper::raw_to_input_source( + r.read_u16()? as u8, + )?, + + frequency: mapper::raw_to_frequency_value( + r.read_u16()? as u8, + )?, + }; + + // ============================ + // DELAY UNIT + // ============================ + + let delay_unit = mapper::raw_to_delay_unit( + r.read_u16()? as u8, + )?; + + // ============================ + // APPLY DELAYS + // ============================ + + for (inp, raw_delay) in input_channels.iter_mut().zip(input_delays_raw) + { + inp.delay = mapper::raw_to_delay( + raw_delay, + delay_unit, + )?; + } + + for (out, raw_delay) in output_channels.iter_mut().zip(output_delays_raw) + { + out.delay = mapper::raw_to_delay( + raw_delay, + delay_unit, + )?; + } + + // ============================ + // PEQ / GEQ BYPASS + // ============================ + + let input_peq_bitmap = r.read_u16()?; + + for (index, inp) in input_channels.iter_mut().enumerate() { + let bypass = input_peq_bitmap & (1 << index) != 0; + + for peq in inp.peq_chain.bands.iter_mut() { + peq.bypass = bypass; + } + } + + + let output_peq_bitmap = r.read_u16()?; + + for (index, out) in output_channels.iter_mut().enumerate() { + let bypass = output_peq_bitmap & (1 << index) != 0; + + for peq in out.peq_chain.bands.iter_mut() { + peq.bypass = bypass; + } + } + + let bitmap = r.read_u16()?; + + for (index, inp) in input_channels.iter_mut().enumerate() { + inp.geq.bypass = bitmap & (1 << index) != 0; + } + + // ============================ + // BUILD CHANNEL MAP + // ============================ + + let mut input_states = HashMap::new(); + + for (channel, state) in types::InputChannel::iter().zip(input_channels) { + input_states.insert( + channel, + state, + ); + } + + let mut output_states = HashMap::new(); + + for (channel, state) in types::OutputChannel::iter().zip(output_channels) { + output_states.insert( + channel, + state, + ); + } + + Ok(types::DSPConfigState { + input_states, + output_states, + delay_unit: delay_unit, + input_source: input_source, + }) +} diff --git a/src/dsp408/client/mod.rs b/src/dsp408/client/mod.rs new file mode 100644 index 0000000..a53a705 --- /dev/null +++ b/src/dsp408/client/mod.rs @@ -0,0 +1,5 @@ +pub mod constants; +pub mod types; +pub mod dsp408; + +pub use dsp408::DSP408; \ No newline at end of file diff --git a/src/dsp408/client/types.rs b/src/dsp408/client/types.rs new file mode 100644 index 0000000..4e7fc80 --- /dev/null +++ b/src/dsp408/client/types.rs @@ -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, +} + +#[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, +} + +#[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, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConfigChunk { + pub sub_index: u8, + pub data: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnknownResponse { + pub cmd: String, + pub raw: Vec, +} + +#[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, + 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, + pub gains: Vec, +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct PEQChain { + pub bands: Vec, + pub bypass: bool, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PresetBank { + pub current_index: usize, + pub names: Vec, + pub modified: Vec, +} + +#[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, + 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, + pub mute: bool, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct DSPConfigState { + pub(crate) input_states: HashMap, + pub(crate) output_states: HashMap, + 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, +} diff --git a/src/dsp408/mapper/channel.rs b/src/dsp408/mapper/channel.rs new file mode 100644 index 0000000..744d3a5 --- /dev/null +++ b/src/dsp408/mapper/channel.rs @@ -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 { + 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 { + Ok(channel_to_raw_value(channel)) +} + +pub fn raw_to_channel(value: u8) -> Result { + 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 { + Ok(channel_to_raw_value(types::Channel::Input(channel))) +} + +pub fn output_channel_to_raw(channel: types::OutputChannel) -> Result { + Ok(channel_to_raw_value(types::Channel::Output(channel))) +} + +// ------ NAME ------ + +pub fn channel_name_to_raw(name: &str) -> Result, 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 { + validators::validate_raw_channel_name(raw_name)?; + + Ok(decode_ascii(raw_name, true)?) +} + +// ------ ATTACK ------ + +pub fn attack_ms_to_raw(value: u16) -> Result { + 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 { + validators::validate_raw_attack(value)?; + + Ok(value + 1) +} + +// ------ RELEASE ------ + +pub fn release_ms_to_raw(value: u16) -> Result { + 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 { + validators::validate_raw_release(value)?; + + Ok(value + 1) +} + +// ------ HOLD ------ + +pub fn hold_ms_to_raw(hold: u16) -> Result { + 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 { + validators::validate_raw_hold(hold)?; + + Ok(hold + 1) +} + +// ------ GATE THRESHOLD ------ + +pub fn gate_threshold_db_to_raw(db: f32) -> Result { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + + 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 { + 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 { + 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 { + + Ok(discrete_frequency_to_raw_value(frequency)) +} + +pub fn raw_to_frequency_value(frequency: u8) -> Result { + 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 { + 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 { + Ok(crossover_filter_to_raw_value(filter)) +} + +pub fn raw_to_crossover_filter(raw: u8) -> Result { + 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 { + 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 { + 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 { + 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 { + Ok(peq_filter_to_raw_value(filter)) +} + +pub fn raw_to_peq_filter(value: u8) -> Result { + 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 { + 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 { + Ok(ratio_to_raw_value(ratio)) +} + +pub fn raw_to_ratio(value: u16) -> Result { + 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 { + 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 { + validators::validate_raw_compressor_knee(value)?; + Ok(value) +} + +// ------ THRESHOLD ------ + +pub fn threshold_db_to_raw(db: f32) -> Result { + 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 { + 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 { + + 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, 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 { + 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 { + 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 { + let mut bitmask = 0x00; + + for &channel in input_channels { + bitmask |= matrix_input_mask(channel); + } + + Ok(bitmask) +} + +pub fn raw_to_matrix(raw: u8) -> Result, 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 { + + 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 { + 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))) +} diff --git a/src/dsp408/mapper/encoding.rs b/src/dsp408/mapper/encoding.rs new file mode 100644 index 0000000..22b0ad9 --- /dev/null +++ b/src/dsp408/mapper/encoding.rs @@ -0,0 +1,74 @@ +use crate::common::errors::{EncodingError, DecodingError}; + +// ------ STRING ------ + +pub fn encode_ascii(value: &str, length: usize, padding: u8) -> Result, 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 { + 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 { + 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) + } +} diff --git a/src/dsp408/mapper/global.rs b/src/dsp408/mapper/global.rs new file mode 100644 index 0000000..273945d --- /dev/null +++ b/src/dsp408/mapper/global.rs @@ -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 { + 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 { + Ok(delay_unit_to_raw_value(unit)) +} + +pub fn raw_to_delay_unit( + value: u8, +) -> Result { + 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 { + 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 { + 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 { + 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, 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 { + 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 { + 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 { + Ok(input_source_to_raw_value(source)) +} + +pub fn raw_to_input_source(value: u8) -> Result { + 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, 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)?) +} diff --git a/src/dsp408/mapper/mod.rs b/src/dsp408/mapper/mod.rs new file mode 100644 index 0000000..e4596d6 --- /dev/null +++ b/src/dsp408/mapper/mod.rs @@ -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::*; \ No newline at end of file diff --git a/src/dsp408/mapper/response.rs b/src/dsp408/mapper/response.rs new file mode 100644 index 0000000..c24bd10 --- /dev/null +++ b/src/dsp408/mapper/response.rs @@ -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 { + 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 = 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 = (0..36) + .step_by(3) + .map(|i| decode_float16(payload[i], payload[i + 1]).unwrap_or_default()) + .collect(); + + let meters: Vec = 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::::new()); + } + other => panic!("expected ConfigChunk, got {:?}", other), + } + } +} diff --git a/src/dsp408/mod.rs b/src/dsp408/mod.rs new file mode 100644 index 0000000..2a7325e --- /dev/null +++ b/src/dsp408/mod.rs @@ -0,0 +1,5 @@ +pub mod client; +pub mod mapper; +pub mod protocol; + +pub use client::DSP408; \ No newline at end of file diff --git a/src/dsp408/protocol/commands.rs b/src/dsp408/protocol/commands.rs new file mode 100644 index 0000000..6b0441b --- /dev/null +++ b/src/dsp408/protocol/commands.rs @@ -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 { + build_frame(0x10, &[]) +} + +// Command 0x11 Disconnect +pub fn build_disconnect() -> Vec { + build_frame(0x11, &[]) +} + +// Command 0x12 Acknowledgement +pub fn build_acknowledgement() -> Vec { + build_frame(0x12, &[]) +} + +// Command 0x13 Device Name +pub fn build_request_device_name() -> Vec { + build_frame(0x13, &[]) +} + +// Command 0x14 Current Preset +pub fn build_get_current_preset() -> Vec { + build_frame(0x14, &[]) +} + +// Command 0x15 Delay Unit +pub fn build_set_delay_unit( + raw_unit: u8 +) -> Result, ValidationError> { + validate_raw_delay_unit(raw_unit)?; + + Ok(build_frame( + 0x15, + &[raw_unit] + )) +} + +// Command 0x20 Recall +pub fn build_recall( + raw_index:u8 +)->Result,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,ValidationError>{ + + validate_raw_store_preset_index(raw_index)?; + + Ok(build_frame( + 0x21, + &[raw_index] + )) +} + +// Command 0x22 +pub fn build_preset_modification_status() -> Vec { + build_frame(0x22,&[]) +} + +// Command 0x23 +pub fn build_factory_reset() -> Vec { + build_frame(0x23,&[]) +} + +// Command 0x26 +pub fn build_store_current_preset_name( + raw_name:&[u8] +)->Result,ValidationError>{ + + validate_raw_preset_name(raw_name)?; + + Ok(build_frame( + 0x26, + raw_name + )) +} + +// Command 0x27 +pub fn build_config_chunk( + raw_index:u8 +)->Result,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,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, ValidationError> { + + validate_raw_copy_channels(source, destination)?; + + Ok(build_frame( + 0x2A, + &[source, destination], + )) +} + +// Command 0x2C +pub fn build_get_device_flags() -> Vec { + build_frame(0x2c,&[]) +} + +// Command 0x2D +pub fn build_unlock_device( + password:&[u8] +)->Result,ValidationError>{ + + validate_raw_password(password)?; + + Ok(build_frame( + 0x2d, + &password + )) +} + +// Command 0x2F +pub fn build_lock_device( + password:&[u8] +)->Result,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, 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, 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, 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, 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, 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,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,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,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, 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, 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, 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, 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, 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, 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, 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 { + 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, 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, 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, ValidationError> { + + validate_raw_input_channel(raw_channel)?; + + Ok(build_frame( + 0x49, + &[ + raw_channel, + bypass, + ], + )) +} diff --git a/src/dsp408/protocol/constants.rs b/src/dsp408/protocol/constants.rs new file mode 100644 index 0000000..f9a1575 --- /dev/null +++ b/src/dsp408/protocol/constants.rs @@ -0,0 +1,79 @@ +use std::ops::RangeInclusive; + +pub const DELAY_UNIT_RAW_RANGE: RangeInclusive = 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 = 0x00..=0x0B; +pub const INPUT_CHANNELS: RangeInclusive = 0x00..=0x03; +pub const OUTPUT_CHANNELS: RangeInclusive = 0x04..=0x0B; + +pub const RATIO_RAW_RANGE: RangeInclusive = 0x0000..=0x000F; +pub const ATTACK_RAW_RANGE: RangeInclusive = 0x0000..=0x03E6; +pub const RELEASE_RAW_RANGE: RangeInclusive = 0x0009..=0x0BB7; +pub const KNEE_RAW_RANGE: RangeInclusive = 0x0000..=0x000C; +pub const THRESHOLD_RAW_RANGE: RangeInclusive = 0x0000..=0x00DC; + +pub const FREQUENCY_RAW_RANGE: RangeInclusive = 0x0000..=0x012C; + +pub const CROSSOVER_FILTER_RAW_RANGE: RangeInclusive = 0x00..=0x14; + +pub const EQ_GAIN_RAW_RANGE: RangeInclusive = 0x0000..=0x00F0; +pub const PEQ_Q_RAW_RANGE: RangeInclusive = 0x00..=0x64; +pub const PEQ_INPUT_BAND_RANGE: RangeInclusive = 0x00..=0x07; +pub const PEQ_OUTPUT_BAND_RANGE:RangeInclusive = 0x00..=0x08; +pub const PEQ_FILTER_RAW_RANGE :RangeInclusive = 0x00..=0x08; + +pub const CHANNEL_GAIN_RAW_RANGE: RangeInclusive = 0x0000..=0x0190; + +pub const DELAY_RAW_RANGE: RangeInclusive = 0x00..=0xFF; + +pub const INPUT_SOURCE_TYPE_RAW_RANGE: RangeInclusive = 0x00..=0x03; + +pub const DISCRETE_FREQUENCY_RAW_RANGE: RangeInclusive = 0x00..=0x1E; + +pub const MATRIX_INPUT_MASK_RANGE: RangeInclusive = 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 = 0x0000..=0x00B4; +pub const HOLD_RAW_RANGE: RangeInclusive = 0x0000..=0x03E6; + +pub const MATRIX_GAIN_RAW_RANGE: RangeInclusive = 0x0000..=0x0118; + +pub fn input_link_mask(channel: u8) -> Option { + 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 { + 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, + } +} diff --git a/src/dsp408/protocol/frame.rs b/src/dsp408/protocol/frame.rs new file mode 100644 index 0000000..b7cfdbe --- /dev/null +++ b/src/dsp408/protocol/frame.rs @@ -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 { + 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, +} + +pub fn extract_frame(data: &[u8]) -> Result { + 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 { + 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")); + } +} \ No newline at end of file diff --git a/src/dsp408/protocol/mod.rs b/src/dsp408/protocol/mod.rs new file mode 100644 index 0000000..f0d57ef --- /dev/null +++ b/src/dsp408/protocol/mod.rs @@ -0,0 +1,4 @@ +pub mod frame; +pub mod commands; +pub mod constants; +pub mod validators; \ No newline at end of file diff --git a/src/dsp408/protocol/validators.rs b/src/dsp408/protocol/validators.rs new file mode 100644 index 0000000..4e08d4d --- /dev/null +++ b/src/dsp408/protocol/validators.rs @@ -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), + ) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b68df8e --- /dev/null +++ b/src/lib.rs @@ -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::*; +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..119a15f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,40 @@ +use std::net::IpAddr; +use std::str::FromStr; + +use dsp_thomann::DSP408; +use dsp_thomann::types; + +fn main() -> Result<(), Box> { + + 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(()) +} \ No newline at end of file