use std::sync::Mutex;
use derive_more::Debug;
use tokio::sync::Mutex as AsyncMutex;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use wayle_core::Property;
use wayle_traits::ServiceMonitoring;
use crate::{
Error,
builder::CavaServiceBuilder,
types::{BarCount, Framerate, InputMethod},
};
pub(crate) const DEFAULT_AUTOSENS: bool = true;
pub(crate) const DEFAULT_STEREO: bool = false;
pub(crate) const DEFAULT_NOISE_REDUCTION: f64 = 0.77;
pub(crate) const DEFAULT_MONSTERCAT: f64 = 0.0;
pub(crate) const DEFAULT_WAVES: u32 = 0;
pub(crate) const DEFAULT_INPUT: InputMethod = InputMethod::PipeWire;
pub(crate) const DEFAULT_SOURCE: &str = "auto";
pub(crate) const DEFAULT_LOW_CUTOFF: u32 = 50;
pub(crate) const DEFAULT_HIGH_CUTOFF: u32 = 10000;
pub(crate) const DEFAULT_SAMPLERATE: u32 = 44100;
#[derive(Debug)]
pub struct CavaService {
#[debug(skip)]
pub(crate) cancellation_token: Mutex<CancellationToken>,
#[debug(skip)]
pub(crate) restart_lock: AsyncMutex<()>,
pub values: Property<Vec<f64>>,
pub bars: Property<BarCount>,
pub autosens: Property<bool>,
pub stereo: Property<bool>,
pub noise_reduction: Property<f64>,
pub monstercat: Property<f64>,
pub waves: Property<u32>,
pub framerate: Property<Framerate>,
pub input: Property<InputMethod>,
pub source: Property<String>,
pub low_cutoff: Property<u32>,
pub high_cutoff: Property<u32>,
pub samplerate: Property<u32>,
}
impl CavaService {
#[instrument]
pub async fn new() -> Result<Self, Error> {
CavaServiceBuilder::new().build().await
}
pub fn builder() -> CavaServiceBuilder {
CavaServiceBuilder::new()
}
pub async fn set_bars(&self, bars: impl Into<BarCount>) -> Result<(), Error> {
self.bars.set(bars.into());
self.restart().await
}
pub async fn set_autosens(&self, autosens: bool) -> Result<(), Error> {
self.autosens.set(autosens);
self.restart().await
}
pub async fn set_stereo(&self, stereo: bool) -> Result<(), Error> {
self.stereo.set(stereo);
self.restart().await
}
pub async fn set_noise_reduction(&self, noise_reduction: f64) -> Result<(), Error> {
self.noise_reduction.set(noise_reduction);
self.restart().await
}
pub async fn set_monstercat(&self, monstercat: f64) -> Result<(), Error> {
if monstercat < 0.0 {
return Err(Error::InvalidParameter("monstercat must be >= 0.0".into()));
}
self.monstercat.set(monstercat);
self.restart().await
}
pub async fn set_waves(&self, waves: u32) -> Result<(), Error> {
self.waves.set(waves);
self.restart().await
}
pub async fn set_framerate(&self, framerate: impl Into<Framerate>) -> Result<(), Error> {
self.framerate.set(framerate.into());
self.restart().await
}
pub async fn set_input(&self, input: InputMethod) -> Result<(), Error> {
self.input.set(input);
self.restart().await
}
pub async fn set_source(&self, source: impl Into<String>) -> Result<(), Error> {
self.source.set(source.into());
self.restart().await
}
pub async fn set_low_cutoff(&self, low_cutoff: u32) -> Result<(), Error> {
if low_cutoff == 0 {
return Err(Error::InvalidParameter(
"low_cutoff must be greater than 0".into(),
));
}
self.low_cutoff.set(low_cutoff);
self.restart().await
}
pub async fn set_high_cutoff(&self, high_cutoff: u32) -> Result<(), Error> {
if high_cutoff == 0 {
return Err(Error::InvalidParameter(
"high_cutoff must be greater than 0".into(),
));
}
self.high_cutoff.set(high_cutoff);
self.restart().await
}
pub async fn set_samplerate(&self, samplerate: u32) -> Result<(), Error> {
if samplerate == 0 {
return Err(Error::InvalidParameter(
"samplerate must be greater than 0".into(),
));
}
self.samplerate.set(samplerate);
self.restart().await
}
async fn restart(&self) -> Result<(), Error> {
let _ = self.restart_lock.lock().await;
{
let mut token = self
.cancellation_token
.lock()
.map_err(|_| Error::InitFailed("cannot lock cancellation token".to_string()))?;
token.cancel();
*token = CancellationToken::new();
}
self.start_monitoring().await
}
}
impl Drop for CavaService {
fn drop(&mut self) {
if let Ok(token) = self.cancellation_token.lock() {
token.cancel();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const VALID_BARS: BarCount = BarCount::DEFAULT;
const VALID_FRAMERATE: Framerate = Framerate::DEFAULT;
const VALID_LOW_CUTOFF: u32 = 50;
const VALID_HIGH_CUTOFF: u32 = 10000;
const VALID_SAMPLERATE: u32 = 44100;
const VALID_NOISE_REDUCTION: f64 = 0.77;
const ZERO_LOW_CUTOFF: u32 = 0;
const ZERO_HIGH_CUTOFF: u32 = 0;
const ZERO_SAMPLERATE: u32 = 0;
fn valid_builder() -> CavaServiceBuilder {
CavaServiceBuilder::new()
.bars(VALID_BARS)
.framerate(VALID_FRAMERATE)
.low_cutoff(VALID_LOW_CUTOFF)
.high_cutoff(VALID_HIGH_CUTOFF)
.samplerate(VALID_SAMPLERATE)
.noise_reduction(VALID_NOISE_REDUCTION)
}
#[tokio::test]
async fn builder_build_with_zero_low_cutoff_returns_error() {
let builder = valid_builder().low_cutoff(ZERO_LOW_CUTOFF);
let result = builder.build().await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidParameter(_)));
}
#[tokio::test]
async fn builder_build_with_zero_high_cutoff_returns_error() {
let builder = valid_builder().high_cutoff(ZERO_HIGH_CUTOFF);
let result = builder.build().await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidParameter(_)));
}
#[tokio::test]
async fn builder_build_with_zero_samplerate_returns_error() {
let builder = valid_builder().samplerate(ZERO_SAMPLERATE);
let result = builder.build().await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidParameter(_)));
}
#[tokio::test]
async fn builder_build_with_high_cutoff_less_than_or_equal_to_low_cutoff_returns_error() {
let low = VALID_LOW_CUTOFF;
let high_equal = low;
let high_less = low - 1;
let result_equal = valid_builder()
.low_cutoff(low)
.high_cutoff(high_equal)
.build()
.await;
assert!(result_equal.is_err());
assert!(matches!(
result_equal.unwrap_err(),
Error::InvalidParameter(_)
));
let result_less = valid_builder()
.low_cutoff(low)
.high_cutoff(high_less)
.build()
.await;
assert!(result_less.is_err());
assert!(matches!(
result_less.unwrap_err(),
Error::InvalidParameter(_)
));
}
#[tokio::test]
async fn builder_build_with_samplerate_violating_nyquist_returns_error() {
let high_cutoff = VALID_HIGH_CUTOFF;
let invalid_samplerate = high_cutoff * 2;
let result = valid_builder()
.high_cutoff(high_cutoff)
.samplerate(invalid_samplerate)
.build()
.await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidParameter(_)));
}
#[tokio::test]
async fn builder_build_with_noise_reduction_below_zero_returns_error() {
let below_zero = -0.1;
let result = valid_builder().noise_reduction(below_zero).build().await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidParameter(_)));
}
#[tokio::test]
async fn builder_build_with_noise_reduction_above_one_returns_error() {
let above_one = 1.1;
let result = valid_builder().noise_reduction(above_one).build().await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidParameter(_)));
}
#[tokio::test]
async fn builder_build_with_negative_monstercat_returns_error() {
let negative = -0.1;
let result = valid_builder().monstercat(negative).build().await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidParameter(_)));
}
}