Compare commits

..
6 Commits
Author SHA1 Message Date
LucasDLTG af2e7dc602 feat: adapt to real usage
CI / Rust CI (push) Failing after 1m3s
2026-09-06 12:29:45 +02:00
LucasDLTG e2fee36cdb feat: rms + peak meters 2026-08-01 12:45:58 +02:00
LucasDLTG 27821c1866 fix: remove warnings 2026-07-31 14:03:52 +02:00
LucasDLTG 3d94df0b7a feat: decode meters clip indicators 2026-07-31 13:54:48 +02:00
LucasDLTG f6b6beda5e fix: prevent race condition on socket + remove panicking code 2026-07-21 23:38:35 +02:00
LucasDLTG a9a49bef89 fix: thread lock in tcp send/receive 2026-07-21 14:47:47 +02:00
16 changed files with 2846 additions and 2591 deletions
Generated
+63
View File
@@ -13,6 +13,8 @@ name = "dsp_thomann"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"dotenvy", "dotenvy",
"serde",
"serde_json",
"strum", "strum",
"strum_macros", "strum_macros",
"thiserror", "thiserror",
@@ -24,6 +26,18 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@@ -42,6 +56,49 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.28.0" version = "0.28.0"
@@ -107,3 +164,9 @@ name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+4
View File
@@ -10,6 +10,8 @@ dotenvy = "0.15"
thiserror = "2.0.19" thiserror = "2.0.19"
strum = "0.28.0" strum = "0.28.0"
strum_macros = "0.28.0" strum_macros = "0.28.0"
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
[lib] [lib]
name = "dsp_thomann" name = "dsp_thomann"
@@ -17,3 +19,5 @@ path = "src/lib.rs"
[features] [features]
hardware-tests = [] hardware-tests = []
default = []
serialization = ["dep:serde", "dep:serde_json"]
+1 -1
View File
@@ -1868,7 +1868,7 @@ Sine Wave:
|---|---|---|---| |---|---|---|---|
| `Byte 1-36` | 12 meter channels | 3 bytes × 12 | [Enc §8.1 Meter Level](#81-meter-level) | | `Byte 1-36` | 12 meter channels | 3 bytes × 12 | [Enc §8.1 Meter Level](#81-meter-level) |
| `Byte 37` | Input clip indicator | 1 byte bitmask | Same OR bitmask pattern as [Enc §4.1](#41-matrix-input-mask) | | `Byte 37` | Input clip indicator | 1 byte bitmask | Same OR bitmask pattern as [Enc §4.1](#41-matrix-input-mask) |
| `Byte 38` | Output clip indicator | 1 byte bitmask | Same OR bitmask pattern as [Enc §4.1](#41-matrix-input-mask) | | `Byte 38` | Output limit indicator | 1 byte bitmask | Same OR bitmask pattern as [Enc §4.1](#41-matrix-input-mask) |
**Example — Query** **Example — Query**
+2
View File
@@ -13,6 +13,8 @@
- Figure out Meter level encoding - Figure out Meter level encoding
- Full test of client (Improve existing one) - Full test of client (Improve existing one)
- Write correct readme.md - Write correct readme.md
- Make client dsp408 thread safe
- Absolutely, send command to avoid tcp keep alive, that make the dsp crash
Rust library for remote LAN control of Thomann DSP408 digital signal processors. Rust library for remote LAN control of Thomann DSP408 digital signal processors.
+12
View File
@@ -122,6 +122,12 @@ pub enum DSPError {
#[error("operation failed: {0}")] #[error("operation failed: {0}")]
OperationFailed(String), OperationFailed(String),
#[error("internal error: {0}")]
InternalError(String),
#[error("mutex poisoned: {0}")]
MutexPoisoned(String),
#[error("I/O error: {0}")] #[error("I/O error: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
@@ -134,3 +140,9 @@ pub enum DSPError {
#[error("frame error: {0}")] #[error("frame error: {0}")]
Frame(#[from] FrameError), Frame(#[from] FrameError),
} }
impl<T> From<std::sync::PoisonError<T>> for DSPError {
fn from(err: std::sync::PoisonError<T>) -> Self {
DSPError::MutexPoisoned(err.to_string())
}
}
+172 -96
View File
@@ -3,22 +3,20 @@ use std::{
fmt, fmt,
io::{Read, Write}, io::{Read, Write},
net::{IpAddr, SocketAddr, TcpStream}, net::{IpAddr, SocketAddr, TcpStream},
sync::{Mutex, OnceLock}, sync::{Arc, Mutex, OnceLock},
time::Duration, time::Duration,
}; };
use strum::IntoEnumIterator;
use crate::{ use crate::{
common::errors::{DSPError, ValidationError}, common::errors::DSPError,
dsp408::{ dsp408::{
mapper::{ mapper::{
self, self,
constants::{CONFIG_CHUNK_INDEX_MAX, PRESET_INDEX_MAX}, constants::{CONFIG_CHUNK_INDEX_MAX, PRESET_INDEX_MAX, delay_range},
response::map_response, response::map_response,
}, },
protocol::{commands, config::decode_config, frame::extract_frame}, protocol::{commands, config::decode_config, frame::extract_frame},
types, types::{self},
}, },
}; };
@@ -32,9 +30,8 @@ fn instances() -> &'static Mutex<HashMap<(IpAddr, u8), ()>> {
} }
pub struct DSP408 { pub struct DSP408 {
connection: types::DeviceConnection, connection: types::DeviceConnection, // read-only so thread protected
socket: Option<TcpStream>, socket: Arc<Mutex<Option<TcpStream>>>, // thread protected
lock: Mutex<()>,
connected: bool, connected: bool,
state: Option<types::DSPState>, state: Option<types::DSPState>,
} }
@@ -62,9 +59,7 @@ impl DSP408 {
device_id, device_id,
}, },
socket: None, socket: Arc::new(Mutex::new(None)),
lock: Mutex::new(()),
connected: false, connected: false,
@@ -72,19 +67,13 @@ impl DSP408 {
}) })
} }
fn send_raw(&mut self, data: &[u8]) -> Result<(), DSPError> { fn send_raw(socket: &mut TcpStream, data: &[u8]) -> Result<(), DSPError> {
let _guard = self.lock.lock().unwrap();
let socket = self.socket.as_mut().ok_or(DSPError::NotConnected)?;
socket.write_all(data)?; socket.write_all(data)?;
Ok(()) Ok(())
} }
fn recv_raw(&mut self) -> Result<Vec<u8>, DSPError> { fn recv_raw(socket: &mut TcpStream) -> Result<Vec<u8>, DSPError> {
let socket = self.socket.as_mut().ok_or(DSPError::NotConnected)?;
let mut buffer = vec![0u8; RECV_BUFSIZE]; let mut buffer = vec![0u8; RECV_BUFSIZE];
match socket.read(&mut buffer) { match socket.read(&mut buffer) {
@@ -101,10 +90,17 @@ impl DSP408 {
} }
} }
pub fn send_recv(&mut self, command: &[u8]) -> Result<types::DeviceResponseType, DSPError> { pub fn send_recv(&self, command: &[u8]) -> Result<types::DeviceResponseType, DSPError> {
self.send_raw(command)?; let mut socket_guard = self
.socket
.lock()
.map_err(|e| DSPError::MutexPoisoned(e.to_string()))?;
let raw = self.recv_raw()?; let socket = socket_guard.as_mut().ok_or(DSPError::NotConnected)?;
Self::send_raw(socket, command)?;
let raw = Self::recv_raw(socket)?;
let frame = extract_frame(&raw)?; let frame = extract_frame(&raw)?;
@@ -117,6 +113,10 @@ impl DSP408 {
self.connected self.connected
} }
pub fn connection(&self) -> &types::DeviceConnection {
&self.connection
}
pub fn state(&self) -> Result<&types::DSPState, DSPError> { pub fn state(&self) -> Result<&types::DSPState, DSPError> {
self.state.as_ref().ok_or(DSPError::NotConnected) self.state.as_ref().ok_or(DSPError::NotConnected)
} }
@@ -125,43 +125,7 @@ impl DSP408 {
self.state.as_mut().ok_or(DSPError::NotConnected) self.state.as_mut().ok_or(DSPError::NotConnected)
} }
// ---------- Commands ---------- fn initialize_state(&mut self) -> Result<(), DSPError> {
// 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 name = self.get_device_name()?;
let flags = self.get_device_flags()?; let flags = self.get_device_flags()?;
let current_preset = self.get_current_preset()?; let current_preset = self.get_current_preset()?;
@@ -185,28 +149,121 @@ impl DSP408 {
current_config: state, current_config: state,
}); });
Ok(())
}
fn convert_delay_unit(value: f32, from: types::DelayUnit, to: types::DelayUnit) -> f32 {
let (_, from_max) = delay_range(from);
let (_, to_max) = delay_range(to);
(value / from_max) * to_max
}
// ---------- Commands ----------
// Command `0x10`
pub fn connect(&mut self) -> Result<(), DSPError> {
let key = (self.connection.host, self.connection.device_id);
{
let map = instances()
.lock()
.map_err(|e| DSPError::InternalError(e.to_string()))?;
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))?;
{
let mut socket = self
.socket
.lock()
.map_err(|e| DSPError::InternalError(e.to_string()))?;
*socket = Some(stream);
}
let handshake = commands::build_handshake();
{
let mut socket = self
.socket
.lock()
.map_err(|e| DSPError::InternalError(e.to_string()))?;
let socket = socket.as_mut().ok_or(DSPError::NotConnected)?;
Self::send_raw(socket, &handshake)?;
}
let start = std::time::Instant::now();
while start.elapsed() < SOCKET_TIMEOUT {
let raw = {
let mut socket = self
.socket
.lock()
.map_err(|e| DSPError::InternalError(e.to_string()))?;
let socket = socket.as_mut().ok_or(DSPError::NotConnected)?;
Self::recv_raw(socket)?
};
let frame = extract_frame(&raw)?;
let response = map_response(&frame)?;
if matches!(response, types::DeviceResponseType::HandshakeAck(_)) {
let result = self.initialize_state();
if result.is_err() {
self.disconnect()?;
return result;
}
instances()
.lock()
.map_err(|e| DSPError::InternalError(e.to_string()))?
.insert(key, ());
self.connected = true;
return Ok(()); return Ok(());
} }
} }
self.disconnect(); self.disconnect()?;
Err(DSPError::HandshakeFailed) Err(DSPError::HandshakeFailed)
} }
// Command `0x11` // Command `0x11`
pub fn disconnect(&mut self) { pub fn disconnect(&mut self) -> Result<(), DSPError> {
let key = (self.connection.host, self.connection.device_id); let key = (self.connection.host, self.connection.device_id);
if let Some(mut socket) = self.socket.take() { let mut socket_guard = self.socket.lock()?;
let _ = socket.write_all(&commands::build_disconnect());
let _ = socket.shutdown(std::net::Shutdown::Both); if let Some(mut socket) = socket_guard.take() {
socket
.write_all(&commands::build_disconnect())
.map_err(DSPError::Io)?;
socket
.shutdown(std::net::Shutdown::Both)
.map_err(DSPError::Io)?;
} }
instances().lock().unwrap().remove(&key); instances().lock()?.remove(&key);
self.connected = false; self.connected = false;
Ok(())
} }
// Command `0x12` // Command `0x12`
@@ -225,7 +282,7 @@ impl DSP408 {
} }
// Command `0x13` (Device Name) // Command `0x13` (Device Name)
pub fn get_device_name(&mut self) -> Result<String, DSPError> { fn get_device_name(&mut self) -> Result<String, DSPError> {
let request = commands::build_request_device_name(); let request = commands::build_request_device_name();
let resp = self.send_recv(&request)?; let resp = self.send_recv(&request)?;
@@ -240,7 +297,7 @@ impl DSP408 {
} }
// Command `0x14` (Current Preset Index) // Command `0x14` (Current Preset Index)
pub fn get_current_preset(&mut self) -> Result<u8, DSPError> { fn get_current_preset(&mut self) -> Result<u8, DSPError> {
let request = commands::build_get_current_preset(); let request = commands::build_get_current_preset();
let resp = self.send_recv(&request)?; let resp = self.send_recv(&request)?;
@@ -268,7 +325,20 @@ impl DSP408 {
return Ok(false); return Ok(false);
} }
self.state_mut()?.current_config.delay_unit = unit; let state = self.state_mut()?;
let old_unit = state.current_config.delay_unit;
if old_unit != unit {
for input in state.current_config.input_states.values_mut() {
input.delay = Self::convert_delay_unit(input.delay, old_unit, unit);
}
for output in state.current_config.output_states.values_mut() {
output.delay = Self::convert_delay_unit(output.delay, old_unit, unit);
}
}
state.current_config.delay_unit = unit;
Ok(true) Ok(true)
} }
@@ -348,7 +418,7 @@ impl DSP408 {
} }
// Command `0x22` (Get Preset Modification Status) // Command `0x22` (Get Preset Modification Status)
pub fn get_preset_modification_status(&mut self) -> Result<types::ModifiedPreset, DSPError> { fn get_preset_modification_status(&mut self) -> Result<types::ModifiedPreset, DSPError> {
let request = commands::build_preset_modification_status(); let request = commands::build_preset_modification_status();
let resp = self.send_recv(&request)?; let resp = self.send_recv(&request)?;
@@ -395,8 +465,20 @@ impl DSP408 {
return Ok(false); return Ok(false);
} }
let state = self.state_mut()?; let current_index = {
let state = self.state()?;
state.presets.current_index
};
if !self.store_current_configuration(current_index, true)? {
return Ok(false);
}
if !self.request_acknowledgement()? {
return Ok(false);
}
let state = self.state_mut()?;
state.presets.names[state.presets.current_index] = name.to_string(); state.presets.names[state.presets.current_index] = name.to_string();
Ok(true) Ok(true)
@@ -426,7 +508,7 @@ impl DSP408 {
} }
// Command `0x29` (Preset Name) // Command `0x29` (Preset Name)
pub fn get_preset_name(&mut self, preset_index: usize) -> Result<String, DSPError> { fn get_preset_name(&mut self, preset_index: usize) -> Result<String, DSPError> {
let raw_index = mapper::preset_index_without_factory_to_raw(preset_index)?; let raw_index = mapper::preset_index_without_factory_to_raw(preset_index)?;
let request = commands::build_get_preset_name(raw_index)?; let request = commands::build_get_preset_name(raw_index)?;
@@ -464,7 +546,7 @@ impl DSP408 {
} }
// Command `0x2C` (Device Flags) // Command `0x2C` (Device Flags)
pub fn get_device_flags(&mut self) -> Result<types::DeviceFlags, DSPError> { fn get_device_flags(&mut self) -> Result<types::DeviceFlags, DSPError> {
let request = commands::build_get_device_flags(); let request = commands::build_get_device_flags();
let resp = self.send_recv(&request)?; let resp = self.send_recv(&request)?;
@@ -746,8 +828,12 @@ impl DSP408 {
match channel { match channel {
types::Channel::Input(input) => { types::Channel::Input(input) => {
state.current_config.input_state_mut(input).peq_chain.bands state
[band as usize] = peq; .current_config
.input_state_mut(input)
.peq_chain
.bands
.insert(band, peq);
} }
types::Channel::Output(output) => { types::Channel::Output(output) => {
@@ -755,7 +841,8 @@ impl DSP408 {
.current_config .current_config
.output_state_mut(output) .output_state_mut(output)
.peq_chain .peq_chain
.bands[band as usize] = peq; .bands
.insert(band, peq);
} }
} }
@@ -883,7 +970,7 @@ impl DSP408 {
} }
// Command `0x38` (Set Delay) // Command `0x38` (Set Delay)
pub fn set_channel_delay( pub fn set_delay(
&mut self, &mut self,
channel: types::Channel, channel: types::Channel,
delay: f32, delay: f32,
@@ -1286,19 +1373,12 @@ impl DSP408 {
let state = self.state_mut()?; 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 state
.current_config .current_config
.output_state_mut(output_channel) .output_state_mut(output_channel)
.matrix_routes .matrix_routes
.gains[index] = gain; .gains
.insert(input_channel, gain);
Ok(true) Ok(true)
} }
@@ -1334,19 +1414,13 @@ impl DSP408 {
let state = self.state_mut()?; 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 state
.current_config .current_config
.input_state_mut(input_channel) .input_state_mut(input_channel)
.geq .geq
.gains[index] = gain; .gains
.insert(frequency_band, gain);
Ok(true) Ok(true)
} }
@@ -1413,7 +1487,9 @@ impl DSP408 {
impl Drop for DSP408 { impl Drop for DSP408 {
fn drop(&mut self) { fn drop(&mut self) {
self.disconnect(); if self.connected {
let _ = self.disconnect();
}
} }
} }
+43 -23
View File
@@ -1,14 +1,11 @@
use crate::{ use crate::{
common::errors::ProtocolError, common::errors::ProtocolError,
dsp408::{ dsp408::{
protocol::{ protocol::frame::Frame,
constants::{METERS_HIGH_STEP, METERS_LOW_STEP, METERS_SPLIT, METERS_SPLIT_LEVEL_DB},
frame::Frame,
},
types::{ types::{
Acknowledgement, AuthenticationResult, ConfigChunk, CurrentPreset, DeviceFlags, Acknowledgement, AuthenticationResult, ConfigChunk, CurrentPreset, DeviceFlags,
DeviceInfo, DeviceResponseType, HandshakeAck, Meters, ModifiedPreset, NotImplemented, DeviceInfo, DeviceResponseType, HandshakeAck, Meter, Meters, ModifiedPreset,
PresetName, UnknownResponse, NotImplemented, PresetName, UnknownResponse,
}, },
}, },
}; };
@@ -88,10 +85,6 @@ pub fn map_response(frame: &Frame) -> Result<DeviceResponseType, ProtocolError>
// Command `0x24` (Get Config Chunk) // Command `0x24` (Get Config Chunk)
if cmd == 0x24 { 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); let index = payload.first().copied().unwrap_or(0);
if index > 0x1c { if index > 0x1c {
@@ -166,27 +159,54 @@ pub fn map_response(frame: &Frame) -> Result<DeviceResponseType, ProtocolError>
// Command `0x40` (Get Level Meters) // Command `0x40` (Get Level Meters)
if cmd == 0x40 { if cmd == 0x40 {
const X0: f32 = -25.0; // breakpoint, in dB
const Y0: f32 = 1.0; // raw value at the breakpoint
const K1: f32 = 0.0104; // slope below breakpoint (raw < Y0)
const K2: f32 = 0.0208; // slope above breakpoint (raw >= Y0)
const MIN_DB: f32 = -60.0;
const MAX_DB: f32 = 12.0;
fn float_to_db(raw: f32) -> f32 {
let db = if raw < Y0 {
X0 + (raw - Y0) / K1
} else {
X0 + (raw - Y0) / K2
};
// Clamping naturally absorbs noise-floor outliers (e.g. raw ≈ 0.385
// resolves to well below -60 dB here, so it correctly floors to -60).
db.clamp(MIN_DB, MAX_DB)
}
if payload.len() != 38 { if payload.len() != 38 {
return Err(ProtocolError::new("Not enough bytes for meter levels")); return Err(ProtocolError::new("Not enough bytes for meter levels"));
} }
let floats: Vec<f32> = (0..36) let channels: Vec<Meter> = (0..36)
.step_by(3) .step_by(3)
.map(|i| decode_float16(payload[i], payload[i + 1]).unwrap_or_default()) .enumerate()
.collect(); .map(|(index, i)| {
let rms = decode_float16(payload[i], payload[i + 1]).unwrap_or_default();
let peak = decode_float16(payload[i + 2], payload[i + 1]).unwrap_or_default();
let meters: Vec<f32> = floats let rms_db = float_to_db(rms);
.into_iter() let peak_db = float_to_db(peak);
.map(|f| {
if f < METERS_SPLIT { let clip = if index < 4 {
METERS_SPLIT_LEVEL_DB + (f - METERS_SPLIT) / METERS_LOW_STEP payload[36] & (1 << index) != 0
} else { } else {
METERS_SPLIT_LEVEL_DB + (f - METERS_SPLIT) / METERS_HIGH_STEP payload[37] & (1 << (index - 4)) != 0
};
Meter {
rms: rms_db,
peak: peak_db,
clip,
} }
}) })
.collect(); .collect();
return Ok(DeviceResponseType::Meters(Meters { levels: meters })); return Ok(DeviceResponseType::Meters(Meters { channels }));
} }
Ok(DeviceResponseType::UnknownResponse(UnknownResponse { Ok(DeviceResponseType::UnknownResponse(UnknownResponse {
@@ -415,10 +435,10 @@ mod tests {
match result { match result {
DeviceResponseType::Meters(m) => { DeviceResponseType::Meters(m) => {
assert_eq!(m.levels.len(), 12); assert_eq!(m.channels.len(), 12);
for level in m.levels { for channel in m.channels {
assert!(level.is_finite()); assert!(channel.rms.is_finite());
} }
} }
other => panic!("unexpected {:?}", other), other => panic!("unexpected {:?}", other),
+63 -26
View File
@@ -35,12 +35,14 @@ fn read_gate(r: &mut BinaryReader) -> Result<types::Gate, DSPError> {
} }
fn read_geq(r: &mut BinaryReader) -> Result<types::GraphicEQ, DSPError> { fn read_geq(r: &mut BinaryReader) -> Result<types::GraphicEQ, DSPError> {
let gains = types::DiscreteFrequency::iter() let mut gains = std::collections::HashMap::new();
.map(|_| {
for frequency in types::DiscreteFrequency::iter() {
let raw = r.read_u16()?; let raw = r.read_u16()?;
Ok(mapper::raw_to_eq_gain(raw)?) let gain = mapper::raw_to_eq_gain(raw)?;
})
.collect::<Result<Vec<_>, DSPError>>()?; gains.insert(frequency, gain);
}
Ok(types::GraphicEQ { Ok(types::GraphicEQ {
gains, gains,
@@ -49,17 +51,20 @@ fn read_geq(r: &mut BinaryReader) -> Result<types::GraphicEQ, DSPError> {
} }
fn read_peq_chain(r: &mut BinaryReader, band_count: usize) -> Result<types::PEQChain, DSPError> { fn read_peq_chain(r: &mut BinaryReader, band_count: usize) -> Result<types::PEQChain, DSPError> {
let bands = (0..band_count) let bands = types::PEQBand::iter()
.map(|_| { .take(band_count)
Ok(types::PEQ { .map(|band| {
let peq = types::PEQ {
gain: mapper::raw_to_eq_gain(r.read_u16()?)?, gain: mapper::raw_to_eq_gain(r.read_u16()?)?,
frequency: mapper::raw_to_frequency_hz(r.read_u16()?)?, frequency: mapper::raw_to_frequency_hz(r.read_u16()?)?,
q: mapper::raw_to_peq_q(r.read_u8()?)?, q: mapper::raw_to_peq_q(r.read_u8()?)?,
filter_type: mapper::raw_to_peq_filter(r.read_u8()?)?, filter_type: mapper::raw_to_peq_filter(r.read_u8()?)?,
bypass: false, bypass: false,
};
Ok((band, peq))
}) })
}) .collect::<Result<HashMap<_, _>, DSPError>>()?;
.collect::<Result<Vec<_>, DSPError>>()?;
Ok(types::PEQChain { Ok(types::PEQChain {
bands, bands,
@@ -67,6 +72,21 @@ fn read_peq_chain(r: &mut BinaryReader, band_count: usize) -> Result<types::PEQC
}) })
} }
fn apply_peq_bypass(
r: &mut BinaryReader,
bands: &mut HashMap<types::PEQBand, types::PEQ>,
) -> Result<(), DSPError> {
let bitmap = r.read_u16()?;
for (index, band) in types::PEQBand::iter().enumerate() {
if let Some(peq) = bands.get_mut(&band) {
peq.bypass = bitmap & (1 << index) != 0;
}
}
Ok(())
}
fn read_crossover(r: &mut BinaryReader) -> Result<types::CrossoverFilters, DSPError> { fn read_crossover(r: &mut BinaryReader) -> Result<types::CrossoverFilters, DSPError> {
let hp_freq = r.read_u16()?; let hp_freq = r.read_u16()?;
let lp_freq = r.read_u16()?; let lp_freq = r.read_u16()?;
@@ -152,7 +172,13 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
inp.linked_channels = mapper::raw_to_link_channels( inp.linked_channels = mapper::raw_to_link_channels(
mapper::channel_to_raw(types::Channel::Input(channel))?, mapper::channel_to_raw(types::Channel::Input(channel))?,
r.read_u8()?, r.read_u8()?,
)?; )?
.into_iter()
.filter_map(|link| match link {
types::Channel::Input(input) => Some(input),
types::Channel::Output(_) => None,
})
.collect();
} }
// ============================ // ============================
@@ -162,17 +188,26 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
for (channel, out) in types::OutputChannel::iter().zip(output_channels.iter_mut()) { 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)?)?; out.name = mapper::raw_to_channel_name(r.pop(constants::CHANNEL_NAME_LENGTH)?)?;
r.skip(1)?;
out.matrix_routes = types::MatrixRoutes { out.matrix_routes = types::MatrixRoutes {
connected: mapper::raw_to_matrix(r.read_u8()?)?, connected: mapper::raw_to_matrix(r.read_u8()?)?,
gains: (0..4) gains: {
.map(|_| { r.skip(1)?;
[
types::InputChannel::InA,
types::InputChannel::InB,
types::InputChannel::InC,
types::InputChannel::InD,
]
.into_iter()
.map(|input| {
let raw = r.read_u16()?; let raw = r.read_u16()?;
Ok::<f32, DSPError>(mapper::raw_to_matrix_gain(raw)?) let gain = mapper::raw_to_matrix_gain(raw)?;
Ok::<(types::InputChannel, f32), DSPError>((input, gain))
}) })
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<HashMap<_, _>, _>>()?
},
}; };
out.crossover = read_crossover(&mut r)?; out.crossover = read_crossover(&mut r)?;
@@ -196,7 +231,13 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
out.linked_channels = mapper::raw_to_link_channels( out.linked_channels = mapper::raw_to_link_channels(
mapper::channel_to_raw(types::Channel::Output(channel))?, mapper::channel_to_raw(types::Channel::Output(channel))?,
r.read_u8()?, r.read_u8()?,
)?; )?
.into_iter()
.filter_map(|link| match link {
types::Channel::Output(output) => Some(output),
types::Channel::Input(_) => None,
})
.collect();
} }
// ============================ // ============================
@@ -212,15 +253,11 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
// ============================ // ============================
for inp in &mut input_channels { for inp in &mut input_channels {
apply_flag(&mut r, inp.peq_chain.bands.as_mut_slice(), |band, value| { apply_peq_bypass(&mut r, &mut inp.peq_chain.bands)?;
band.bypass = value
})?;
} }
for out in &mut output_channels { for out in &mut output_channels {
apply_flag(&mut r, out.peq_chain.bands.as_mut_slice(), |band, value| { apply_peq_bypass(&mut r, &mut out.peq_chain.bands)?;
band.bypass = value
})?;
} }
// ============================ // ============================
@@ -260,7 +297,7 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
for (index, inp) in input_channels.iter_mut().enumerate() { for (index, inp) in input_channels.iter_mut().enumerate() {
let bypass = input_peq_bitmap & (1 << index) != 0; let bypass = input_peq_bitmap & (1 << index) != 0;
for peq in inp.peq_chain.bands.iter_mut() { for peq in inp.peq_chain.bands.values_mut() {
peq.bypass = bypass; peq.bypass = bypass;
} }
} }
@@ -270,7 +307,7 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
for (index, out) in output_channels.iter_mut().enumerate() { for (index, out) in output_channels.iter_mut().enumerate() {
let bypass = output_peq_bitmap & (1 << index) != 0; let bypass = output_peq_bitmap & (1 << index) != 0;
for peq in out.peq_chain.bands.iter_mut() { for peq in out.peq_chain.bands.values_mut() {
peq.bypass = bypass; peq.bypass = bypass;
} }
} }
-5
View File
@@ -77,8 +77,3 @@ pub fn output_link_mask(channel: u8) -> Option<u8> {
_ => None, _ => None,
} }
} }
pub const METERS_SPLIT: f32 = 1.0;
pub const METERS_SPLIT_LEVEL_DB: f32 = -18.0;
pub const METERS_LOW_STEP: f32 = 0.0104;
pub const METERS_HIGH_STEP: f32 = 0.0208;
+45 -8
View File
@@ -1,8 +1,12 @@
use std::{collections::HashMap, net::IpAddr}; use std::{collections::HashMap, net::IpAddr};
use strum_macros::{EnumCount, EnumIter}; use strum_macros::{EnumCount, EnumIter};
#[cfg(feature = "serialization")]
use serde::{Deserialize, Serialize};
// ------ Enums ------ // ------ Enums ------
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DelayUnit { pub enum DelayUnit {
Millisecond, Millisecond,
@@ -10,6 +14,7 @@ pub enum DelayUnit {
Feet, Feet,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, EnumCount, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, EnumCount, PartialOrd, Ord)]
pub enum InputChannel { pub enum InputChannel {
InA, InA,
@@ -18,6 +23,7 @@ pub enum InputChannel {
InD, InD,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, EnumCount, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, EnumCount, PartialOrd, Ord)]
pub enum OutputChannel { pub enum OutputChannel {
Out1, Out1,
@@ -30,12 +36,14 @@ pub enum OutputChannel {
Out8, Out8,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Channel { pub enum Channel {
Input(InputChannel), Input(InputChannel),
Output(OutputChannel), Output(OutputChannel),
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Ratio { pub enum Ratio {
#[default] #[default]
@@ -57,6 +65,7 @@ pub enum Ratio {
Limit, Limit,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum CrossoverFilter { pub enum CrossoverFilter {
#[default] #[default]
@@ -83,6 +92,7 @@ pub enum CrossoverFilter {
Bl48, Bl48,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum PEQBand { pub enum PEQBand {
B1 = 1, B1 = 1,
@@ -96,6 +106,7 @@ pub enum PEQBand {
B9, B9,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PEQFilter { pub enum PEQFilter {
Peak, Peak,
@@ -109,6 +120,7 @@ pub enum PEQFilter {
AllPass2, AllPass2,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum DiscreteFrequency { pub enum DiscreteFrequency {
F20, F20,
@@ -144,6 +156,7 @@ pub enum DiscreteFrequency {
F20000, F20000,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InputType { pub enum InputType {
Analog, Analog,
@@ -152,6 +165,7 @@ pub enum InputType {
SineWave, SineWave,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct InputSource { pub struct InputSource {
pub r#type: InputType, pub r#type: InputType,
@@ -194,6 +208,7 @@ pub struct PresetName {
pub name: String, pub name: String,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeviceFlags { pub struct DeviceFlags {
pub is_locked: bool, pub is_locked: bool,
@@ -204,9 +219,18 @@ pub struct AuthenticationResult {
pub success: bool, pub success: bool,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Meter {
pub rms: f32,
pub peak: f32,
pub clip: bool,
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Meters { pub struct Meters {
pub levels: Vec<f32>, pub channels: Vec<Meter>,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -238,7 +262,7 @@ pub enum DeviceResponseType {
} }
// ------ State ------ // ------ State ------
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Gate { pub struct Gate {
pub attack: u16, pub attack: u16,
@@ -247,6 +271,7 @@ pub struct Gate {
pub threshold: f32, pub threshold: f32,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Compressor { pub struct Compressor {
pub threshold: f32, pub threshold: f32,
@@ -256,6 +281,7 @@ pub struct Compressor {
pub knee: u16, pub knee: u16,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Limiter { pub struct Limiter {
pub threshold: f32, pub threshold: f32,
@@ -263,12 +289,14 @@ pub struct Limiter {
pub release: u16, pub release: u16,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct GraphicEQ { pub struct GraphicEQ {
pub gains: Vec<f32>, pub gains: HashMap<DiscreteFrequency, f32>,
pub bypass: bool, pub bypass: bool,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct PEQ { pub struct PEQ {
pub gain: f32, pub gain: f32,
@@ -278,30 +306,35 @@ pub struct PEQ {
pub bypass: bool, pub bypass: bool,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct CrossoverFilters { pub struct CrossoverFilters {
pub high_pass: Crossover, pub high_pass: Crossover,
pub low_pass: Crossover, pub low_pass: Crossover,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Crossover { pub struct Crossover {
pub frequency: f32, pub frequency: f32,
pub slope: CrossoverFilter, pub slope: CrossoverFilter,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct MatrixRoutes { pub struct MatrixRoutes {
pub connected: Vec<InputChannel>, pub connected: Vec<InputChannel>,
pub gains: Vec<f32>, pub gains: HashMap<InputChannel, f32>,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct PEQChain { pub struct PEQChain {
pub bands: Vec<PEQ>, pub bands: HashMap<PEQBand, PEQ>,
pub bypass: bool, pub bypass: bool,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct PresetBank { pub struct PresetBank {
pub current_index: usize, pub current_index: usize,
@@ -309,6 +342,7 @@ pub struct PresetBank {
pub modified: Vec<bool>, pub modified: Vec<bool>,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DeviceConnection { pub struct DeviceConnection {
pub host: IpAddr, pub host: IpAddr,
@@ -316,6 +350,7 @@ pub struct DeviceConnection {
pub device_id: u8, pub device_id: u8,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct InputChannelState { pub struct InputChannelState {
pub name: String, pub name: String,
@@ -326,10 +361,11 @@ pub struct InputChannelState {
pub geq: GraphicEQ, pub geq: GraphicEQ,
pub peq_chain: PEQChain, pub peq_chain: PEQChain,
pub crossover: CrossoverFilters, pub crossover: CrossoverFilters,
pub linked_channels: Vec<Channel>, pub linked_channels: Vec<InputChannel>,
pub mute: bool, pub mute: bool,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Default, PartialEq)] #[derive(Debug, Clone, Default, PartialEq)]
pub struct OutputChannelState { pub struct OutputChannelState {
pub name: String, pub name: String,
@@ -341,10 +377,11 @@ pub struct OutputChannelState {
pub gain: f32, pub gain: f32,
pub phase_inverted: bool, pub phase_inverted: bool,
pub delay: f32, pub delay: f32,
pub linked_channels: Vec<Channel>, pub linked_channels: Vec<OutputChannel>,
pub mute: bool, pub mute: bool,
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct DSPConfigState { pub struct DSPConfigState {
pub(crate) input_states: HashMap<InputChannel, InputChannelState>, pub(crate) input_states: HashMap<InputChannel, InputChannelState>,
@@ -367,11 +404,11 @@ impl DSPConfigState {
} }
} }
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct DSPState { pub struct DSPState {
pub(crate) name: String, pub(crate) name: String,
pub(crate) flags: DeviceFlags, pub(crate) flags: DeviceFlags,
pub(crate) presets: PresetBank, pub(crate) presets: PresetBank,
pub(crate) current_config: DSPConfigState, pub(crate) current_config: DSPConfigState,
} }
+12 -3
View File
@@ -3,6 +3,7 @@ use std::str::FromStr;
use dsp_thomann::dsp408::DSP408; use dsp_thomann::dsp408::DSP408;
use dsp_thomann::dsp408::types; use dsp_thomann::dsp408::types;
use dsp_thomann::dsp408::types::InputChannel::InA;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let host = IpAddr::from_str("192.168.49.30")?; let host = IpAddr::from_str("192.168.49.30")?;
@@ -22,13 +23,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
println!("DSP connected."); println!("DSP connected.");
dsp.set_mute(types::Channel::Input(types::InputChannel::InA), true)?; // dsp.set_mute(types::Channel::Input(types::InputChannel::InA), true)?;
dbg!(dsp.state()?); let r = dsp.set_peq_band(types::Channel::Input(InA), types::PEQBand::B1, 3.0, 1400.0, 2.1, types::PEQFilter::LowShelf, false)?;
dbg!(r);
// dbg!(dsp.state()?);
// let s = &dsp.state()?.current_config.input_states[&InputChannel::InA].peq_chain;
// dbg!(s)
println!("Disconnecting..."); println!("Disconnecting...");
dsp.disconnect(); dsp.disconnect()?;
println!("Disconnected"); println!("Disconnected");
Ok(()) Ok(())
} }
// 10020100 27 40 263df7 263dfd 263dfd 263dfd 063d8d 063d63 263dcf 263dfd 263df6 263dcf 263dcf 263dcf 0300 10038b