Skip to main content

hidpp/feature/equalizer/
mod.rs

1//! Implements the `Equalizer` feature (ID `0x8310`, version 2) that configures
2//! an audio device's equalizer (per-band gains) and microphone noise reduction.
3//!
4//! The device exposes a single EQ table of `band_count` frequency bands; each
5//! band has a fixed frequency (Hz) and an adjustable signed gain (dB). All
6//! frequencies are big-endian `u16`; gains are signed `i8`.
7
8#[cfg(test)]
9mod tests;
10
11use std::sync::Arc;
12
13use num_enum::{IntoPrimitive, TryFromPrimitive};
14
15use crate::{
16    channel::HidppChannel,
17    feature::{CreatableFeature, Feature, FeatureEndpoint},
18    protocol::v20::Hidpp20Error,
19};
20
21/// Maximum number of frequencies a single `getFrequencies` response carries.
22const FREQUENCIES_PER_PAGE: u8 = 7;
23
24bitflags::bitflags! {
25    /// How a device stores its EQ values, from
26    /// [`get_eq_info`](EqualizerFeature::get_eq_info).
27    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
29    pub struct EqCapabilities: u8 {
30        /// EQ values are stored as gains.
31        const STORED_AS_GAINS = 1 << 0;
32        /// EQ values are stored as coefficients.
33        const STORED_AS_COEFFICIENTS = 1 << 1;
34    }
35}
36
37/// Where [`get_frequency_gains`](EqualizerFeature::get_frequency_gains) reads from.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40#[non_exhaustive]
41#[repr(u8)]
42pub enum GainLocation {
43    /// The custom EQ stored in EEPROM (the version-0 default).
44    Eeprom = 0,
45    /// The active EQ in RAM.
46    Ram = 1,
47}
48
49/// How [`set_frequency_gains`](EqualizerFeature::set_frequency_gains) persists
50/// the gains.
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize))]
53#[non_exhaustive]
54#[repr(u8)]
55pub enum GainPersistence {
56    /// Volatile: applied to RAM only.
57    Volatile = 0,
58    /// Applied to RAM and stored in EEPROM.
59    VolatileAndNonVolatile = 1,
60    /// Stored in EEPROM only.
61    NonVolatileOnly = 2,
62}
63
64/// EQ table information from [`get_eq_info`](EqualizerFeature::get_eq_info).
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize))]
67#[non_exhaustive]
68pub struct EqInfo {
69    /// Number of frequency bands.
70    pub band_count: u8,
71    /// Gain range in dB; used as `±db_range` when `db_min`/`db_max` are both `0`.
72    pub db_range: u8,
73    /// How EQ values are stored.
74    pub capabilities: EqCapabilities,
75    /// Minimum gain in dB, or `0` to imply `-db_range`.
76    pub db_min: i8,
77    /// Maximum gain in dB, or `0` to imply `+db_range`.
78    pub db_max: i8,
79}
80
81impl EqInfo {
82    fn from_payload(payload: &[u8; 16]) -> Self {
83        Self {
84            band_count: payload[0],
85            db_range: payload[1],
86            capabilities: EqCapabilities::from_bits_retain(payload[2]),
87            db_min: payload[3] as i8,
88            db_max: payload[4] as i8,
89        }
90    }
91
92    /// The effective `(min, max)` gain range in dB.
93    ///
94    /// Resolves the "both zero implies `±db_range`" rule into concrete bounds.
95    #[must_use]
96    pub fn effective_range(&self) -> (i8, i8) {
97        if self.db_min == 0 && self.db_max == 0 {
98            let range = i8::try_from(self.db_range).unwrap_or(i8::MAX);
99            (-range, range)
100        } else {
101            (self.db_min, self.db_max)
102        }
103    }
104}
105
106/// Implements the `Equalizer` / `0x8310` feature.
107#[derive(Clone)]
108pub struct EqualizerFeature {
109    /// The endpoint this feature talks to.
110    endpoint: FeatureEndpoint,
111}
112
113impl CreatableFeature for EqualizerFeature {
114    const ID: u16 = 0x8310;
115    const STARTING_VERSION: u8 = 2;
116
117    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
118        Self {
119            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
120        }
121    }
122}
123
124impl Feature for EqualizerFeature {}
125
126impl EqualizerFeature {
127    /// Retrieves the EQ table's band count, gain range and storage capabilities.
128    pub async fn get_eq_info(&self) -> Result<EqInfo, Hidpp20Error> {
129        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
130        Ok(EqInfo::from_payload(&payload))
131    }
132
133    /// Retrieves the frequency (Hz) of every band.
134    ///
135    /// `band_count` is the value from [`EqInfo::band_count`]; the device returns
136    /// up to seven frequencies per response, so this pages through them until all
137    /// `band_count` are collected.
138    pub async fn get_frequencies(&self, band_count: u8) -> Result<Vec<u16>, Hidpp20Error> {
139        let mut frequencies = Vec::with_capacity(usize::from(band_count));
140        let mut index = 0u8;
141        while index < band_count {
142            let payload = self.endpoint.call(1, [index, 0, 0]).await?.extend_payload();
143            // The response echoes the requested band index in byte 0.
144            if payload[0] != index {
145                return Err(Hidpp20Error::UnsupportedResponse);
146            }
147            let page = (band_count - index).min(FREQUENCIES_PER_PAGE);
148            frequencies.extend(parse_frequency_page(&payload, page)?);
149            index += page;
150        }
151        Ok(frequencies)
152    }
153
154    /// Retrieves the active gain (dB) of every band from `location`.
155    ///
156    /// `band_count` is the value from [`EqInfo::band_count`] (at most 15, the
157    /// number of gains a single response carries).
158    pub async fn get_frequency_gains(
159        &self,
160        location: GainLocation,
161        band_count: u8,
162    ) -> Result<Vec<i8>, Hidpp20Error> {
163        let payload = self
164            .endpoint
165            .call(2, [location.into(), 0, 0])
166            .await?
167            .extend_payload();
168        parse_gains(&payload, 0, band_count)
169    }
170
171    /// Sets the per-band gains (dB) and returns the device's echo of them.
172    ///
173    /// `gains` holds one signed value per band (at most 15). The device rejects
174    /// out-of-range gains.
175    pub async fn set_frequency_gains(
176        &self,
177        persistence: GainPersistence,
178        gains: &[i8],
179    ) -> Result<Vec<i8>, Hidpp20Error> {
180        let count = u8::try_from(gains.len()).map_err(|_| Hidpp20Error::UnsupportedResponse)?;
181        let mut args = [0; 16];
182        args[0] = persistence.into();
183        // Gains follow the persistence byte; each is a signed value sent as a raw
184        // byte.
185        for (i, &gain) in gains.iter().enumerate() {
186            let slot = 1 + i;
187            if slot >= args.len() {
188                return Err(Hidpp20Error::UnsupportedResponse);
189            }
190            args[slot] = gain as u8;
191        }
192        let payload = self.endpoint.call_long(3, args).await?.extend_payload();
193        // The response echoes the request, so the gains start after the echoed
194        // persistence byte.
195        parse_gains(&payload, 1, count)
196    }
197
198    /// Retrieves whether hardware microphone noise reduction is enabled.
199    pub async fn get_mic_noise_reduction(&self) -> Result<bool, Hidpp20Error> {
200        let payload = self.endpoint.call(4, [0; 3]).await?.extend_payload();
201        Ok(payload[0] != 0)
202    }
203
204    /// Enables or disables hardware microphone noise reduction.
205    pub async fn set_mic_noise_reduction(&self, enabled: bool) -> Result<(), Hidpp20Error> {
206        self.endpoint.call(5, [u8::from(enabled), 0, 0]).await?;
207        Ok(())
208    }
209}
210
211/// Parses `count` big-endian `u16` frequencies from a `getFrequencies` response,
212/// which carries them starting at byte 1 (after the echoed band index).
213fn parse_frequency_page(payload: &[u8; 16], count: u8) -> Result<Vec<u16>, Hidpp20Error> {
214    let count = usize::from(count);
215    if 1 + 2 * count > payload.len() {
216        return Err(Hidpp20Error::UnsupportedResponse);
217    }
218    Ok((0..count)
219        .map(|i| u16::from_be_bytes([payload[1 + 2 * i], payload[2 + 2 * i]]))
220        .collect())
221}
222
223/// Parses `count` signed gains from a payload starting at `offset`.
224fn parse_gains(payload: &[u8; 16], offset: usize, count: u8) -> Result<Vec<i8>, Hidpp20Error> {
225    let count = usize::from(count);
226    if offset + count > payload.len() {
227        return Err(Hidpp20Error::UnsupportedResponse);
228    }
229    Ok(payload[offset..offset + count]
230        .iter()
231        .map(|&byte| byte as i8)
232        .collect())
233}