1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
use std::sync::Mutex;
use tokio::sync::Mutex as AsyncMutex;
use tokio_util::sync::CancellationToken;
use tracing::{instrument, warn};
use wayle_core::Property;
use wayle_traits::ServiceMonitoring;
use crate::{
Error,
service::{
CavaService, DEFAULT_AUTOSENS, DEFAULT_HIGH_CUTOFF, DEFAULT_INPUT, DEFAULT_LOW_CUTOFF,
DEFAULT_MONSTERCAT, DEFAULT_NOISE_REDUCTION, DEFAULT_SAMPLERATE, DEFAULT_SOURCE,
DEFAULT_STEREO, DEFAULT_WAVES,
},
types::{BarCount, Framerate, InputMethod},
};
/// Builder for configuring and creating a [`CavaService`].
///
/// Provides a fluent interface for setting audio visualization parameters.
/// All parameters have sensible defaults and can be selectively overridden.
pub struct CavaServiceBuilder {
bars: BarCount,
autosens: bool,
stereo: bool,
noise_reduction: f64,
monstercat: f64,
waves: u32,
framerate: Framerate,
input: InputMethod,
source: String,
low_cutoff: u32,
high_cutoff: u32,
samplerate: u32,
}
impl Default for CavaServiceBuilder {
fn default() -> Self {
Self::new()
}
}
impl CavaServiceBuilder {
/// Creates a new builder with default audio visualization settings.
pub fn new() -> Self {
Self {
bars: BarCount::DEFAULT,
autosens: DEFAULT_AUTOSENS,
stereo: DEFAULT_STEREO,
noise_reduction: DEFAULT_NOISE_REDUCTION,
monstercat: DEFAULT_MONSTERCAT,
waves: DEFAULT_WAVES,
framerate: Framerate::DEFAULT,
input: DEFAULT_INPUT,
source: DEFAULT_SOURCE.to_string(),
low_cutoff: DEFAULT_LOW_CUTOFF,
high_cutoff: DEFAULT_HIGH_CUTOFF,
samplerate: DEFAULT_SAMPLERATE,
}
}
/// Sets the number of frequency bars to generate.
///
/// Accepts [`BarCount`] (clamped to 1-256) or raw `usize`/`u16`.
pub fn bars(mut self, bars: impl Into<BarCount>) -> Self {
self.bars = bars.into();
self
}
/// Enables or disables automatic sensitivity adjustment.
///
/// When enabled, CAVA automatically adjusts sensitivity to keep values in 0-1 range.
pub fn autosens(mut self, autosens: bool) -> Self {
self.autosens = autosens;
self
}
/// Enables or disables stereo channel visualization.
///
/// When enabled, splits bars between left and right audio channels.
pub fn stereo(mut self, stereo: bool) -> Self {
self.stereo = stereo;
self
}
/// Sets the noise reduction filter strength.
///
/// Range: 0.0 (fast, noisy) to 1.0 (slow, smooth).
pub fn noise_reduction(mut self, noise_reduction: f64) -> Self {
self.noise_reduction = noise_reduction;
self
}
/// Sets monstercat-style smoothing across adjacent bars.
///
/// 0.0 disables monstercat smoothing. Higher values produce smoother falloff.
pub fn monstercat(mut self, monstercat: f64) -> Self {
self.monstercat = monstercat;
self
}
/// Sets wave-style smoothing count.
///
/// 0 disables wave smoothing. Mutually exclusive with monstercat
/// (monstercat takes priority if both non-zero).
pub fn waves(mut self, waves: u32) -> Self {
self.waves = waves;
self
}
/// Sets the visualization update rate in frames per second.
///
/// Accepts [`Framerate`] (clamped to 1-360) or raw `u32`.
pub fn framerate(mut self, framerate: impl Into<Framerate>) -> Self {
self.framerate = framerate.into();
self
}
/// Sets the audio input method/backend.
///
/// Determines which audio system to use for capturing audio (PipeWire, PulseAudio, ALSA, etc.).
pub fn input(mut self, input: InputMethod) -> Self {
self.input = input;
self
}
/// Sets the audio source identifier.
///
/// Source string format depends on the input method. Use "auto" for automatic selection.
pub fn source(mut self, source: impl Into<String>) -> Self {
self.source = source.into();
self
}
/// Sets the low frequency cutoff in Hz.
///
/// Frequencies below this value are filtered out. Must be greater than 0.
pub fn low_cutoff(mut self, low_cutoff: u32) -> Self {
self.low_cutoff = low_cutoff;
self
}
/// Sets the high frequency cutoff in Hz.
///
/// Frequencies above this value are filtered out. Must be greater than low_cutoff
/// and less than samplerate/2.
pub fn high_cutoff(mut self, high_cutoff: u32) -> Self {
self.high_cutoff = high_cutoff;
self
}
/// Sets the audio sample rate in Hz.
///
/// Should match the input audio sample rate. Must be greater than 0 and at least
/// 2*high_cutoff (Nyquist theorem).
pub fn samplerate(mut self, samplerate: u32) -> Self {
self.samplerate = samplerate;
self
}
/// Builds and initializes the CAVA service with the configured parameters.
///
/// `bars` and `framerate` are validated by their newtypes (clamped on construction).
/// Cross-field constraints (cutoffs, samplerate, noise_reduction) are validated here.
///
/// # Errors
/// Returns error if:
/// - `low_cutoff`, `high_cutoff`, or `samplerate` is 0
/// - `high_cutoff` is not greater than `low_cutoff`
/// - `samplerate` is not greater than 2 * `high_cutoff` (Nyquist theorem)
/// - `noise_reduction` is not in range 0.0-1.0
/// - `monstercat` is negative
/// - Audio initialization fails or input method is unavailable
#[instrument(skip(self))]
pub async fn build(self) -> Result<CavaService, Error> {
if self.low_cutoff == 0 {
return Err(Error::InvalidParameter(
"low_cutoff must be greater than 0".into(),
));
}
if self.high_cutoff == 0 {
return Err(Error::InvalidParameter(
"high_cutoff must be greater than 0".into(),
));
}
if self.samplerate == 0 {
return Err(Error::InvalidParameter(
"samplerate must be greater than 0".into(),
));
}
if self.high_cutoff <= self.low_cutoff {
return Err(Error::InvalidParameter(format!(
"high_cutoff ({}) must be greater than low_cutoff ({})",
self.high_cutoff, self.low_cutoff
)));
}
if self.samplerate / 2 <= self.high_cutoff {
return Err(Error::InvalidParameter(format!(
"samplerate ({}) must be greater than 2 * high_cutoff ({})",
self.samplerate, self.high_cutoff
)));
}
if !(0.0..=1.0).contains(&self.noise_reduction) {
return Err(Error::InvalidParameter(format!(
"noise_reduction must be between 0.0 and 1.0, got {}",
self.noise_reduction
)));
}
if self.monstercat < 0.0 {
return Err(Error::InvalidParameter("monstercat must be >= 0.0".into()));
}
let effective_bars = self.bars.adjusted_for_stereo(self.stereo);
if effective_bars != self.bars {
warn!(
requested = %self.bars,
adjusted = %effective_bars,
"odd bar count rounded up for stereo output"
);
}
let bar_count = effective_bars.value() as usize;
let service = CavaService {
cancellation_token: Mutex::new(CancellationToken::new()),
restart_lock: AsyncMutex::new(()),
values: Property::new(vec![0.0; bar_count]),
bars: Property::new(self.bars),
autosens: Property::new(self.autosens),
stereo: Property::new(self.stereo),
noise_reduction: Property::new(self.noise_reduction),
monstercat: Property::new(self.monstercat),
waves: Property::new(self.waves),
framerate: Property::new(self.framerate),
input: Property::new(self.input),
source: Property::new(self.source),
low_cutoff: Property::new(self.low_cutoff),
high_cutoff: Property::new(self.high_cutoff),
samplerate: Property::new(self.samplerate),
};
service.start_monitoring().await?;
Ok(service)
}
}