Files
dsp-thomann/src/dsp408/client/dsp408.rs
T
2026-10-02 22:50:39 +02:00

2274 lines
72 KiB
Rust

use std::{
collections::HashMap,
fmt,
io::{Read, Write},
net::{IpAddr, SocketAddr, TcpStream},
sync::{Arc, Mutex, OnceLock},
time::Duration,
};
use crate::{
common::errors::DSPError,
dsp408::{
client::default::{
default_dsp_state, default_input_meters, default_output_meters, random_input_meters,
random_output_meters,
},
mapper::{
self,
constants::{CONFIG_CHUNK_INDEX_MAX, PRESET_INDEX_MAX, delay_range},
response::map_response,
},
protocol::{commands, config::decode_config, frame::extract_frame},
types::{self, ConnectionStatus::Disconnected, DSPMode},
},
};
pub const SOCKET_TIMEOUT: Duration = Duration::from_secs(20);
pub const RECV_BUFSIZE: usize = 4096;
static INSTANCES: OnceLock<Mutex<HashMap<(IpAddr, u8), ()>>> = OnceLock::new();
fn instances() -> &'static Mutex<HashMap<(IpAddr, u8), ()>> {
INSTANCES.get_or_init(|| Mutex::new(HashMap::new()))
}
pub struct DSP408 {
connection: types::DeviceConnection,
socket: Arc<Mutex<Option<TcpStream>>>,
mode: DSPMode,
state: types::DSPState,
input_meters: types::InputMeters,
output_meters: types::OutputMeters,
}
impl DSP408 {
pub fn new(host: IpAddr, port: u16, device_id: u8) -> Result<Self, DSPError> {
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 {
mode: types::DSPMode::Normal,
connection: types::DeviceConnection {
host,
port,
device_id,
},
socket: Arc::new(Mutex::new(None)),
state: default_dsp_state(),
input_meters: default_input_meters(),
output_meters: default_output_meters(),
})
}
pub fn new_demo() -> Self {
Self {
mode: types::DSPMode::Demo,
connection: types::DeviceConnection {
host: IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
port: 0,
device_id: 1,
},
socket: Arc::new(Mutex::new(None)),
state: default_dsp_state(),
input_meters: default_input_meters(),
output_meters: default_output_meters(),
}
}
fn send_raw(socket: &mut TcpStream, data: &[u8]) -> Result<(), DSPError> {
socket.write_all(data)?;
Ok(())
}
fn recv_raw(socket: &mut TcpStream) -> Result<Vec<u8>, DSPError> {
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(&self, command: &[u8]) -> Result<types::DeviceResponseType, DSPError> {
let mut socket_guard = self
.socket
.lock()
.map_err(|e| DSPError::MutexPoisoned(e.to_string()))?;
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 response = map_response(&frame)?;
Ok(response)
}
pub fn mode(&self) -> types::DSPMode {
self.mode
}
pub fn connected(&self) -> bool {
self.state.connection == types::ConnectionStatus::Connected
}
pub fn demo(&self) -> bool {
self.mode == types::DSPMode::Demo
}
pub fn state(&self) -> &types::DSPState {
&self.state
}
pub fn input_meters(&self) -> &types::InputMeters {
&self.input_meters
}
pub fn output_meters(&self) -> &types::OutputMeters {
&self.output_meters
}
fn state_mut(&mut self) -> &mut types::DSPState {
&mut self.state
}
fn initialize_state(&mut self) -> Result<(), DSPError> {
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(PRESET_INDEX_MAX);
for i in 0..PRESET_INDEX_MAX {
preset_names.push(self.get_preset_name(i + 1)?);
}
let state = self.read_config_state()?;
self.state = types::DSPState {
name,
flags,
presets: types::PresetBank {
current_index: current_preset as usize,
names: preset_names,
modified: modified.modified,
},
current_config: state,
connection: Disconnected,
};
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> {
if self.mode == types::DSPMode::Demo {
self.state.connection = types::ConnectionStatus::Connected;
return Ok(());
}
if self.state.connection == types::ConnectionStatus::Connected {
return Err(DSPError::AlreadyConnected);
}
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(_)) {
if let Err(error) = self.initialize_state() {
let _ = self.disconnect();
return Err(error);
}
self.state.connection = types::ConnectionStatus::Connected;
instances()
.lock()
.map_err(|e| DSPError::InternalError(e.to_string()))?
.insert(key, ());
return Ok(());
}
}
self.disconnect()?;
Err(DSPError::HandshakeFailed)
}
// Command `0x11`
pub fn disconnect(&mut self) -> Result<(), DSPError> {
if self.mode == types::DSPMode::Demo {
self.state.connection = types::ConnectionStatus::Disconnected;
return Ok(());
}
let key = (self.connection.host, self.connection.device_id);
let mut socket_guard = self
.socket
.lock()
.map_err(|e| DSPError::MutexPoisoned(e.to_string()))?;
if let Some(mut socket) = socket_guard.take() {
let _ = socket.write_all(&commands::build_disconnect());
let _ = socket.shutdown(std::net::Shutdown::Both);
}
instances()
.lock()
.map_err(|e| DSPError::MutexPoisoned(e.to_string()))?
.remove(&key);
self.state.connection = types::ConnectionStatus::Disconnected;
Ok(())
}
// Command `0x12`
fn request_acknowledgement(&mut self) -> Result<bool, DSPError> {
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)
fn get_device_name(&mut self) -> Result<String, DSPError> {
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)
fn get_current_preset(&mut self) -> Result<u8, DSPError> {
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<bool, DSPError> {
let raw_unit = mapper::delay_unit_to_raw(unit)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
// Apply the state change in both Connected and Demo.
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)
}
// Command `0x20` (Recall)
pub fn recall_preset(&mut self, preset_index: usize) -> Result<bool, DSPError> {
let raw_index = mapper::preset_index_to_raw(preset_index)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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;
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
} else if self.mode == types::DSPMode::Demo {
self.state = default_dsp_state();
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut().presets.current_index = preset_index;
Ok(true)
}
// Command `0x21` (Store Preset)
pub fn store_current_configuration(
&mut self,
preset_index: usize,
force: bool,
) -> Result<bool, DSPError> {
let raw_index = mapper::preset_index_to_raw(preset_index)?;
if preset_index == 0 && !force {
return Ok(false);
}
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
if !self.request_acknowledgement()? {
return Err(DSPError::OperationFailed(
"Store Configuration failed".into(),
));
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut().presets.modified[preset_index] = true;
Ok(true)
}
// Command `0x22` (Get Preset Modification Status)
fn get_preset_modification_status(&mut self) -> Result<types::ModifiedPreset, DSPError> {
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<bool, DSPError> {
if !force {
return Ok(false);
}
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
let request = commands::build_factory_reset();
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
)));
}
}
} else if self.mode == types::DSPMode::Demo {
self.state = default_dsp_state();
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
Ok(true)
}
// Command `0x26` (Store Preset Name)
pub fn set_current_preset_name(&mut self, name: &str) -> Result<bool, DSPError> {
let raw_name = mapper::preset_name_to_raw(name)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
let current_index = self.state().presets.current_index;
if !self.store_current_configuration(current_index, true)? {
return Ok(false);
}
if !self.request_acknowledgement()? {
return Ok(false);
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
// Shared authoritative state update.
let current_index = self.state().presets.current_index;
let name = mapper::raw_to_preset_name(&raw_name)?;
self.state_mut().presets.names[current_index - 1] = name;
Ok(true)
}
// Command `0x27 - 0x24` (Get Config Chunk)
fn get_config_chunk(&mut self, index: usize) -> Result<types::ConfigChunk, DSPError> {
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)
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 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
))),
}
}
fn copy_channel_state(
&mut self,
source: types::Channel,
destination: types::Channel,
) -> Result<(), DSPError> {
match (source, destination) {
(types::Channel::Input(src), types::Channel::Input(dst)) => {
let src_state = self
.state
.current_config
.input_states
.get(&src)
.cloned()
.ok_or_else(|| DSPError::InternalError("missing input channel state".into()))?;
self.state
.current_config
.input_states
.insert(dst, src_state);
}
(types::Channel::Output(src), types::Channel::Output(dst)) => {
let src_state = self
.state
.current_config
.output_states
.get(&src)
.cloned()
.ok_or_else(|| {
DSPError::InternalError("missing output channel state".into())
})?;
self.state
.current_config
.output_states
.insert(dst, src_state);
}
_ => {
return Err(DSPError::InternalError(
"copy_channel requires source and destination to be the same channel kind"
.into(),
));
}
}
Ok(())
}
// Command `0x2A` (Copy Channel)
pub fn copy_channel(
&mut self,
channel_1: types::Channel,
channel_2: types::Channel,
) -> Result<bool, DSPError> {
let (raw_channel_1, raw_channel_2) = mapper::copy_channel_to_raw(channel_1, channel_2)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
let request = commands::build_copy_channel(raw_channel_1, raw_channel_2)?;
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
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.copy_channel_state(channel_1, channel_2)?;
Ok(true)
}
// Command `0x2C` (Device Flags)
fn get_device_flags(&mut self) -> Result<types::DeviceFlags, DSPError> {
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<bool, DSPError> {
let raw_password = mapper::password_to_raw(password)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected AuthenticationResult, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut().flags.is_locked = false;
Ok(true)
}
// Command `0x2F` (Lock Device)
pub fn lock_device(&mut self, password: &str) -> Result<bool, DSPError> {
let raw_password = mapper::password_to_raw(password)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut().flags.is_locked = true;
self.disconnect()?;
Ok(true)
}
// 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<bool, DSPError> {
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)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
// Shared authoritative state update.
self.state_mut()
.current_config
.output_state_mut(channel)
.compressor = types::Compressor {
threshold: mapper::raw_to_threshold_db(raw_threshold)?,
ratio: mapper::raw_to_ratio(raw_ratio)?,
attack: mapper::raw_to_attack_ms(raw_attack)?,
release: mapper::raw_to_release_ms(raw_release)?,
knee: mapper::raw_to_knee_db(raw_knee)?,
};
Ok(true)
}
// Command `0x31` (Set Low-Pass Filter)
pub fn set_low_pass(
&mut self,
channel: types::Channel,
freq: f32,
filter_type: types::CrossoverFilter,
) -> Result<bool, DSPError> {
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)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
let state = self.state_mut();
match channel {
types::Channel::Input(input) => {
state
.current_config
.input_state_mut(input)
.crossover
.low_pass = types::Crossover {
frequency: mapper::raw_to_frequency_hz(raw_freq)?,
slope: mapper::raw_to_crossover_filter(raw_filter)?,
};
}
types::Channel::Output(output) => {
state
.current_config
.output_state_mut(output)
.crossover
.low_pass = types::Crossover {
frequency: mapper::raw_to_frequency_hz(raw_freq)?,
slope: mapper::raw_to_crossover_filter(raw_filter)?,
};
}
}
Ok(true)
}
// Command `0x32` (Set High-Pass Filter)
pub fn set_high_pass(
&mut self,
channel: types::Channel,
freq: f32,
filter_type: types::CrossoverFilter,
) -> Result<bool, DSPError> {
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)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
let state = self.state_mut();
match channel {
types::Channel::Input(input) => {
state
.current_config
.input_state_mut(input)
.crossover
.high_pass = types::Crossover {
frequency: mapper::raw_to_frequency_hz(raw_freq)?,
slope: mapper::raw_to_crossover_filter(raw_filter)?,
};
}
types::Channel::Output(output) => {
state
.current_config
.output_state_mut(output)
.crossover
.high_pass = types::Crossover {
frequency: mapper::raw_to_frequency_hz(raw_freq)?,
slope: mapper::raw_to_crossover_filter(raw_filter)?,
};
}
}
Ok(true)
}
// Command `0x33` (Set PEQ Band)
#[allow(clippy::too_many_arguments)]
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<bool, DSPError> {
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)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
// Shared authoritative state update (Connected + Demo).
let state = self.state_mut();
let peq = types::PEQ {
gain: mapper::raw_to_eq_gain(raw_gain)?,
frequency: mapper::raw_to_frequency_hz(raw_frequency)?,
q: mapper::raw_to_peq_q(raw_q)?,
filter_type: mapper::raw_to_peq_filter(raw_filter)?,
bypass,
};
match channel {
types::Channel::Input(input) => {
state
.current_config
.input_state_mut(input)
.peq_chain
.bands
.insert(band, peq);
}
types::Channel::Output(output) => {
state
.current_config
.output_state_mut(output)
.peq_chain
.bands
.insert(band, peq);
}
}
Ok(true)
}
// Command `0x34` (Set Gain)
pub fn set_channel_gain(&mut self, channel: types::Channel, db: f32) -> Result<bool, DSPError> {
let raw_channel = mapper::channel_to_raw(channel)?;
let raw_db = mapper::gain_db_to_raw(db)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
let state = self.state_mut();
match channel {
types::Channel::Input(input) => {
state.current_config.input_state_mut(input).gain = mapper::raw_to_gain(raw_db)?;
}
types::Channel::Output(output) => {
state.current_config.output_state_mut(output).gain = mapper::raw_to_gain(raw_db)?;
}
}
Ok(true)
}
// Command `0x35` (Set Mute)
pub fn set_mute(&mut self, channel: types::Channel, muted: bool) -> Result<bool, DSPError> {
let raw_channel = mapper::channel_to_raw(channel)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
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)
}
// Command `0x36` (Invert Gain)
pub fn set_channel_inverse_gain(
&mut self,
channel: types::Channel,
inverted: bool,
) -> Result<bool, DSPError> {
let raw_channel = mapper::channel_to_raw(channel)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
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)
}
// Command `0x38` (Set Delay)
pub fn set_delay(
&mut self,
channel: types::Channel,
delay: f32,
unit: types::DelayUnit,
) -> Result<bool, DSPError> {
let raw_channel = mapper::channel_to_raw(channel)?;
let raw_delay = mapper::delay_to_raw(delay, unit)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
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)
}
// Command `0x39` (Set Input Source)
pub fn set_input_source(&mut self, input_source: types::InputSource) -> Result<bool, DSPError> {
let raw_source = mapper::input_source_to_raw(input_source.r#type)?;
let raw_frequency = mapper::frequency_value_to_raw(input_source.frequency)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut().current_config.input_source = types::InputSource {
r#type: mapper::raw_to_input_source(raw_source)?,
frequency: mapper::raw_to_frequency_value(raw_frequency)?,
};
Ok(true)
}
// Command `0x3A` (Matrix Routing)
pub fn set_matrix_routing(
&mut self,
output: types::OutputChannel,
inputs: Vec<types::InputChannel>,
) -> Result<bool, DSPError> {
let raw_output = mapper::output_channel_to_raw(output)?;
let raw_inputs = mapper::matrix_to_raw(&inputs)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut()
.current_config
.output_state_mut(output)
.matrix_routes
.connected = inputs;
Ok(true)
}
// Command `0x3B` (Link Channel)
pub fn link_channels(
&mut self,
source: types::Channel,
destinations: Vec<types::Channel>,
) -> Result<bool, DSPError> {
let raw_source = mapper::channel_to_raw(source)?;
let bitmask = mapper::link_channels_to_raw(source, &destinations)?;
// Copy source configuration to each destination first. This runs in
// both Connected and Demo mode (copy_channel handles that dispatch
// itself), and returns NotConnected for us if we're Disconnected.
for destination in &destinations {
if *destination == source {
continue;
}
if !self.copy_channel(source, *destination)? {
return Ok(false);
}
}
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
// 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);
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
let state = self.state_mut();
match source {
types::Channel::Input(s) => {
let input_destinations: Vec<types::InputChannel> = destinations
.iter()
.filter_map(|destination| match destination {
types::Channel::Input(input) => Some(*input),
types::Channel::Output(_) => None,
})
.collect();
let states = &mut state.current_config.input_states;
// Remove the source and destinations from all existing links.
for channel_state in states.values_mut() {
channel_state
.linked_channels
.retain(|channel| *channel != s && !input_destinations.contains(channel));
}
// Link source -> destinations.
states.get_mut(&s).unwrap().linked_channels = input_destinations;
}
types::Channel::Output(s) => {
let output_destinations: Vec<types::OutputChannel> = destinations
.iter()
.filter_map(|destination| match destination {
types::Channel::Output(output) => Some(*output),
types::Channel::Input(_) => None,
})
.collect();
let states = &mut state.current_config.output_states;
// Remove the source and destinations from all existing links.
for channel_state in states.values_mut() {
channel_state
.linked_channels
.retain(|channel| *channel != s && !output_destinations.contains(channel));
}
// Link source -> destinations.
states.get_mut(&s).unwrap().linked_channels = output_destinations;
}
}
Ok(true)
}
// Command `0x3C` (Bypass PEQ)
pub fn set_bypass_peq(
&mut self,
channel: types::Channel,
bypass: bool,
) -> Result<bool, DSPError> {
let raw_channel = mapper::channel_to_raw(channel)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
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)
}
// Command `0x3D` (Set Channel Name)
pub fn set_channel_name(
&mut self,
channel: types::Channel,
name: &str,
) -> Result<bool, DSPError> {
let raw_channel = mapper::channel_to_raw(channel)?;
let raw_name = mapper::channel_name_to_raw(name)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
let state = self.state_mut();
match channel {
types::Channel::Input(input) => {
state.current_config.input_state_mut(input).name =
mapper::raw_to_channel_name(&raw_name)?;
}
types::Channel::Output(output) => {
state.current_config.output_state_mut(output).name =
mapper::raw_to_channel_name(&raw_name)?;
}
}
Ok(true)
}
// Command `0x3E` (Set Gate Parameters)
pub fn set_gate(
&mut self,
channel: types::InputChannel,
threshold: f32,
attack: u16,
hold: u16,
release: u16,
) -> Result<bool, DSPError> {
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)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut()
.current_config
.input_state_mut(channel)
.gate = types::Gate {
attack: mapper::raw_to_attack_ms(raw_attack)?,
release: mapper::raw_to_release_ms(raw_release)?,
hold: mapper::raw_to_hold_ms(raw_hold)?,
threshold: mapper::raw_to_gate_threshold_db(raw_threshold)?,
};
Ok(true)
}
// Command `0x3F` (Set Output Limiter)
pub fn set_limiter(
&mut self,
channel: types::OutputChannel,
threshold: f32,
attack: u16,
release: u16,
) -> Result<bool, DSPError> {
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)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut()
.current_config
.output_state_mut(channel)
.limiter = types::Limiter {
threshold: mapper::raw_to_threshold_db(raw_threshold)?,
attack: mapper::raw_to_attack_ms(raw_attack)?,
release: mapper::raw_to_release_ms(raw_release)?,
};
Ok(true)
}
// Command `0x40` (Get Meter Level)
pub fn get_meter_levels(
&mut self,
) -> Result<(types::InputMeters, types::OutputMeters), DSPError> {
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Demo {
self.input_meters = random_input_meters();
self.output_meters = random_output_meters();
return Ok((self.input_meters.clone(), self.output_meters.clone()));
}
let request = commands::build_get_meter_levels();
let resp = self.send_recv(&request)?;
match resp {
types::DeviceResponseType::Meters(inputs, outputs) => {
self.input_meters = inputs.clone();
self.output_meters = outputs.clone();
Ok((inputs, outputs))
}
other => Err(DSPError::UnexpectedResponse(format!(
"Expected Meters, got {:?}",
other
))),
}
}
types::ConnectionStatus::Disconnected => Err(DSPError::NotConnected),
}
}
// Command `0x41` (Set Matrix Gain)
pub fn set_matrix_gain(
&mut self,
input_channel: types::InputChannel,
output_channel: types::OutputChannel,
gain: f32,
) -> Result<bool, DSPError> {
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)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut()
.current_config
.output_state_mut(output_channel)
.matrix_routes
.gains
.insert(input_channel, mapper::raw_to_matrix_gain(raw_gain)?);
Ok(true)
}
// Command `0x48` (Set GEQ Band)
pub fn set_geq_band(
&mut self,
input_channel: types::InputChannel,
frequency_band: types::DiscreteFrequency,
gain: f32,
) -> Result<bool, DSPError> {
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)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut()
.current_config
.input_state_mut(input_channel)
.geq
.gains
.insert(
mapper::raw_to_frequency_value(raw_frequency_band)?,
mapper::raw_to_eq_gain(raw_gain)?,
);
Ok(true)
}
// Command `0x49` (Set GEQ Bypass)
pub fn set_geq_bypass(
&mut self,
input_channel: types::InputChannel,
bypass: bool,
) -> Result<bool, DSPError> {
let raw_input_channel = mapper::input_channel_to_raw(input_channel)?;
match self.state.connection {
types::ConnectionStatus::Connected => {
if self.mode == types::DSPMode::Normal {
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);
}
}
other => {
return Err(DSPError::UnexpectedResponse(format!(
"Expected Acknowledgement, got {:?}",
other
)));
}
}
}
}
types::ConnectionStatus::Disconnected => {
return Err(DSPError::NotConnected);
}
}
self.state_mut()
.current_config
.input_state_mut(input_channel)
.geq
.bypass = bypass;
Ok(true)
}
fn read_config_state(&mut self) -> Result<types::DSPConfigState, DSPError> {
let mut raw = Vec::new();
for i in 0..=CONFIG_CHUNK_INDEX_MAX {
let chunk = self.get_config_chunk(i)?;
raw.extend_from_slice(&chunk.data);
}
let config = decode_config(&raw)?;
if !self.request_acknowledgement()? {
return Err(DSPError::OperationFailed("DSP initialization".into()));
}
Ok(config)
}
}
impl Drop for DSP408 {
fn drop(&mut self) {
if self.connected() {
let _ = 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()
}
}
#[cfg(feature = "hardware-tests")]
#[test]
fn full_command_sweep_against_real_hardware() {
use std::sync::Once;
use std::{env, str::FromStr};
static INIT: Once = Once::new();
fn init_test_env() {
INIT.call_once(|| {
dotenvy::dotenv().ok();
});
}
fn dsp_host() -> IpAddr {
IpAddr::from_str(&env::var("DSP_HOST").unwrap_or_else(|_| "192.168.49.30".into()))
.expect("DSP_HOST must be a valid IP")
}
fn dsp_port() -> u16 {
env::var("DSP_PORT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(9761)
}
fn dsp_device_id() -> u8 {
env::var("DSP_DEVICE_ID")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1)
}
fn destructive_allowed() -> bool {
matches!(
env::var("DSP_ALLOW_DESTRUCTIVE")
.unwrap_or_default()
.as_str(),
"1" | "true" | "yes"
)
}
init_test_env();
let host = dsp_host();
let port = dsp_port();
let device_id = dsp_device_id();
let mut dsp = DSP408::new(host, port, device_id).expect("invalid connection params");
// ---------- Connect ----------
dsp.connect().expect("connect() should succeed");
assert!(
dsp.connected(),
"device should report connected after connect()"
);
{
let state = dsp.state();
assert!(!state.name.is_empty(), "device name should be non-empty");
assert!(
!state.presets.names.is_empty(),
"preset name list should be populated"
);
assert!(
!state.current_config.input_states.is_empty(),
"input map should be populated"
);
assert!(
!state.current_config.output_states.is_empty(),
"output map should be populated"
);
}
// ---------- Delay unit ----------
let ok = dsp
.set_delay_unit(types::DelayUnit::Millisecond)
.expect("set_delay_unit");
assert!(ok);
assert_eq!(
dsp.state().current_config.delay_unit,
types::DelayUnit::Millisecond
);
// ---------- Input channel exercise ----------
let input = types::InputChannel::InA;
let in_ch = types::Channel::Input(input);
let ok = dsp.set_mute(in_ch, true).expect("set_mute");
assert!(ok);
assert!(dsp.state().current_config.input_states[&input].mute);
let ok = dsp.set_mute(in_ch, false).expect("unmute");
assert!(ok);
assert!(!dsp.state().current_config.input_states[&input].mute);
let ok = dsp.set_channel_gain(in_ch, -3.0).expect("set_channel_gain");
assert!(ok);
assert!((dsp.state().current_config.input_states[&input].gain - (-3.0)).abs() < 0.01);
let ok = dsp
.set_channel_inverse_gain(in_ch, true)
.expect("set_channel_inverse_gain");
assert!(ok);
assert!(dsp.state().current_config.input_states[&input].phase_inverted);
dsp.set_channel_inverse_gain(in_ch, false)
.expect("revert phase");
let ok = dsp
.set_delay(in_ch, 1.5, types::DelayUnit::Millisecond)
.expect("set_delay");
assert!(ok);
let ok = dsp
.set_channel_name(in_ch, "Test InA")
.expect("set_channel_name");
assert!(ok);
assert_eq!(
dsp.state().current_config.input_states[&input].name,
"Test InA"
);
let ok = dsp
.set_low_pass(in_ch, 120.0, types::CrossoverFilter::Bw24)
.expect("set_low_pass");
assert!(ok);
let ok = dsp
.set_high_pass(in_ch, 80.0, types::CrossoverFilter::Bw24)
.expect("set_high_pass");
assert!(ok);
let ok = dsp
.set_peq_band(
in_ch,
types::PEQBand::B1,
2.0,
1000.0,
1.0,
types::PEQFilter::Peak,
false,
)
.expect("set_peq_band");
assert!(ok);
let ok = dsp.set_bypass_peq(in_ch, true).expect("set_bypass_peq");
assert!(ok);
dsp.set_bypass_peq(in_ch, false).expect("un-bypass peq");
let ok = dsp.set_gate(input, -40.0, 5, 100, 200).expect("set_gate");
assert!(ok);
let ok = dsp
.set_geq_band(input, types::DiscreteFrequency::F100, 1.0)
.expect("set_geq_band");
assert!(ok);
let ok = dsp.set_geq_bypass(input, true).expect("set_geq_bypass");
assert!(ok);
dsp.set_geq_bypass(input, false).expect("un-bypass geq");
let ok = dsp
.set_input_source(types::InputSource {
r#type: types::InputType::Analog, // adjust to your enum
frequency: types::DiscreteFrequency::F1000, // adjust to your enum
})
.expect("set_input_source");
assert!(ok);
// // ---------- Output channel exercise ----------
let output = types::OutputChannel::Out1;
let out_ch = types::Channel::Output(output);
let ok = dsp
.set_compressor(output, types::Ratio::Limit, 5, 50, 2, -12.0)
.expect("set_compressor");
assert!(ok);
let ok = dsp.set_limiter(output, -3.0, 1, 50).expect("set_limiter");
assert!(ok);
let ok = dsp
.set_matrix_gain(input, output, -6.0)
.expect("set_matrix_gain");
assert!(ok);
let ok = dsp
.set_matrix_routing(output, vec![input])
.expect("set_matrix_routing");
assert!(ok);
assert_eq!(
dsp.state().current_config.output_states[&output]
.matrix_routes
.connected,
vec![input]
);
// // ---------- Copy / link channels ----------
let other_output = types::OutputChannel::Out2;
let ok = dsp
.copy_channel(out_ch, types::Channel::Output(other_output))
.expect("copy_channel");
assert!(ok);
let ok = dsp
.link_channels(out_ch, vec![types::Channel::Output(other_output)])
.expect("link_channels");
assert!(ok);
// // ---------- Preset name / modification status ----------
let ok = dsp
.set_current_preset_name("Integration Te")
.expect("set_current_preset_name");
assert!(ok);
let status = dsp
.get_preset_modification_status()
.expect("get_preset_modification_status");
println!("Preset modification status: {:?}", status);
let name = dsp.get_preset_name(1).expect("get_preset_name");
println!("Preset 1 name: {}", name);
// // ---------- Meters ----------
let meters = dsp.get_meter_levels().expect("get_meter_levels");
println!("Meters: {:?}", meters);
// // ---------- Destructive ops (opt-in only) ----------
if destructive_allowed() {
let ok = dsp
.store_current_configuration(1, false)
.expect("store_current_configuration");
assert!(ok);
assert!(dsp.state().presets.modified[1]);
let ok = dsp.recall_preset(1).expect("recall_preset");
assert!(ok);
assert_eq!(dsp.state().presets.current_index, 1);
// factory_reset intentionally left commented out — wipes the unit.
// let ok = dsp.factory_reset(true).expect("factory_reset");
// assert!(ok);
} else {
println!("Skipping destructive ops (set DSP_ALLOW_DESTRUCTIVE=1 to enable)");
}
// ---------- Disconnect ----------
assert!(dsp.disconnect().is_ok());
assert!(!dsp.connected(), "device should report disconnected");
}