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