respeaker 0.1.0

USB control library for Seeed ReSpeaker Mic Array v2.0 (LEDs + XVF-3000 DSP tuning)
Documentation
//! DSP parameter tuning — protocol from Seeed's `tuning.py`.

use crate::error::{Error, Result};
use crate::params::{
    find_parameter, Access, ParamKind, ParamValue, Parameter, PARAMETERS,
};
use crate::{TUNING_TIMEOUT, VENDOR_IN, VENDOR_OUT};
use rusb::{DeviceHandle, UsbContext};

/// Borrowed DSP tuning interface for an open ReSpeaker handle.
pub struct Tuning<'a, T: UsbContext> {
    handle: &'a DeviceHandle<T>,
}

impl<'a, T: UsbContext> Tuning<'a, T> {
    pub(crate) fn new(handle: &'a DeviceHandle<T>) -> Self {
        Self { handle }
    }

    /// All known parameters (same set as `tuning.py -p`).
    pub fn parameters(&self) -> &'static [Parameter] {
        PARAMETERS
    }

    /// Firmware / protocol version byte (`tuning.py` `version` property).
    pub fn version(&self) -> Result<u8> {
        let mut buf = [0u8; 1];
        self.handle
            .read_control(VENDOR_IN, 0, 0x80, 0, &mut buf, TUNING_TIMEOUT)?;
        Ok(buf[0])
    }

    /// Direction of arrival in degrees `0..=359`.
    pub fn doa_angle(&self) -> Result<i32> {
        match self.read("DOAANGLE")? {
            ParamValue::Int(v) => Ok(v),
            ParamValue::Float(v) => Ok(v as i32),
        }
    }

    /// Whether voice activity is currently detected.
    pub fn is_voice(&self) -> Result<bool> {
        Ok(self.read("VOICEACTIVITY")?.as_i32().unwrap_or(0) != 0)
    }

    /// Set VAD threshold (`GAMMAVAD_SR`).
    pub fn set_vad_threshold(&self, db: f64) -> Result<()> {
        self.write("GAMMAVAD_SR", ParamValue::Float(db))
    }

    /// Read a named parameter (case-insensitive).
    pub fn read(&self, name: &str) -> Result<ParamValue> {
        let param = find_parameter(name)
            .ok_or_else(|| Error::UnknownParameter(name.to_string()))?;
        self.read_param(param)
    }

    /// Write a named parameter (case-insensitive). Fails if read-only.
    pub fn write(&self, name: &str, value: ParamValue) -> Result<()> {
        let param = find_parameter(name)
            .ok_or_else(|| Error::UnknownParameter(name.to_string()))?;
        if param.access == Access::ReadOnly {
            return Err(Error::ReadOnly(param.name.to_string()));
        }
        self.write_param(param, value)
    }

    /// Parse `text` as int or float according to the parameter kind, then write.
    pub fn write_str(&self, name: &str, text: &str) -> Result<()> {
        let param = find_parameter(name)
            .ok_or_else(|| Error::UnknownParameter(name.to_string()))?;
        let value = match param.kind {
            ParamKind::Int => {
                let v: i32 = text
                    .parse()
                    .map_err(|e| Error::InvalidValue(format!("expected int: {e}")))?;
                ParamValue::Int(v)
            }
            ParamKind::Float => {
                let v: f64 = text
                    .parse()
                    .map_err(|e| Error::InvalidValue(format!("expected float: {e}")))?;
                ParamValue::Float(v)
            }
        };
        self.write(param.name, value)
    }

    fn read_param(&self, param: &Parameter) -> Result<ParamValue> {
        let mut cmd = 0x80u16 | u16::from(param.offset);
        if matches!(param.kind, ParamKind::Int) {
            cmd |= 0x40;
        }

        let mut response = [0u8; 8];
        self.handle.read_control(
            VENDOR_IN,
            0,
            cmd,
            param.id,
            &mut response,
            TUNING_TIMEOUT,
        )?;

        let value0 = i32::from_le_bytes(response[0..4].try_into().unwrap());
        let value1 = i32::from_le_bytes(response[4..8].try_into().unwrap());

        Ok(match param.kind {
            ParamKind::Int => ParamValue::Int(value0),
            ParamKind::Float => ParamValue::Float((value0 as f64) * 2f64.powi(value1)),
        })
    }

    fn write_param(&self, param: &Parameter, value: ParamValue) -> Result<()> {
        let mut payload = [0u8; 12];
        payload[0..4].copy_from_slice(&i32::from(param.offset).to_le_bytes());

        match param.kind {
            ParamKind::Int => {
                let v = value
                    .as_i32()
                    .ok_or_else(|| Error::InvalidValue("expected int value".into()))?;
                payload[4..8].copy_from_slice(&v.to_le_bytes());
                payload[8..12].copy_from_slice(&1i32.to_le_bytes());
            }
            ParamKind::Float => {
                let v = value.as_f64() as f32;
                payload[4..8].copy_from_slice(&v.to_le_bytes());
                payload[8..12].copy_from_slice(&0i32.to_le_bytes());
            }
        }

        self.handle.write_control(
            VENDOR_OUT,
            0,
            0,
            param.id,
            &payload,
            TUNING_TIMEOUT,
        )?;
        Ok(())
    }
}