rustriff_lib/services/effects/distortion/
sc_distortion.rs1use crate::domain::audio_processor::AudioProcessor;
2use crate::domain::dto::effect::effect_dto::EffectDto;
3use crate::domain::dto::effect::scdistortion_dto::ScDistortionDto;
4use crate::domain::effect::Effect;
5use crate::services::processors::gain::gain_processor::GainProcessor;
6use atomic_float::AtomicF32;
7use std::collections::HashMap;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10
11pub struct SCDistortion {
12 id: u32,
13 name: String,
14 is_active: Arc<AtomicBool>,
15 limit: Arc<AtomicF32>,
18 level: Arc<AtomicF32>,
21 level_gain: GainProcessor,
24 smoothing: Arc<AtomicF32>,
27 color: String,
29}
30
31impl SCDistortion {
32 pub fn new(
33 id: u32,
34 name: String,
35 is_active: bool,
36 threshold: f32,
37 level: f32,
38 smoothing: f32,
39 color: String,
40 ) -> Self {
41 let gain_value = 1.0 + level.clamp(0.0, 1.0); let level_arc = Arc::new(AtomicF32::new(gain_value));
43 let level_gain = GainProcessor::new(Arc::clone(&level_arc));
44 Self {
45 id,
46 name,
47 is_active: Arc::new(AtomicBool::new(is_active)),
48 limit: Arc::new(AtomicF32::new(threshold.clamp(0.001, 1.0))),
49 level: level_arc,
50 level_gain,
51 smoothing: Arc::new(AtomicF32::new(smoothing.clamp(1.0, 10.0))),
52 color,
53 }
54 }
55
56 pub fn threshold(&self) -> f32 {
62 self.limit.load(Ordering::Relaxed)
63 }
64
65 pub fn set_threshold(&self, threshold: f32) {
73 self.limit
74 .store(threshold.clamp(0.001, 1.0), Ordering::Relaxed);
75 }
76
77 pub fn level(&self) -> f32 {
86 (self.level.load(Ordering::Relaxed) - 1.0).clamp(0.0, 1.0)
87 }
88
89 pub fn set_level(&self, level: f32) {
98 self.level
99 .store(1.0 + level.clamp(0.0, 1.0), Ordering::Relaxed);
100 }
101
102 pub fn smoothing(&self) -> f32 {
103 self.smoothing.load(Ordering::Relaxed)
104 }
105
106 pub fn set_smoothing(&self, smoothing: f32) {
107 self.smoothing
108 .store(smoothing.clamp(1.0, 10.0), Ordering::Relaxed);
109 }
110}
111
112impl AudioProcessor for SCDistortion {
113 fn process(&mut self, sample: f32) -> f32 {
130 let limit = self.limit.load(Ordering::Relaxed);
131 let smoothing = self.smoothing.load(Ordering::Relaxed);
132 let normalized_sample = sample / limit;
133 let abs_normalized_sample = normalized_sample.abs();
134 let smoothed =
135 normalized_sample / (1.0 + abs_normalized_sample.powf(smoothing)).powf(1.0 / smoothing);
136 let denormalized_sample = smoothed * limit;
137 self.level_gain.process(denormalized_sample)
138 }
139}
140
141impl Effect for SCDistortion {
142 fn id(&self) -> u32 {
143 self.id
144 }
145 fn name(&self) -> &str {
146 &self.name
147 }
148 fn get_color(&self) -> String {
149 self.color.clone()
150 }
151 fn to_dto(&self) -> EffectDto {
159 EffectDto::SCDistortion(ScDistortionDto {
160 id: self.id,
161 name: self.name.clone(),
162 is_active: self.is_active.load(Ordering::Relaxed),
163 color: self.color.clone(),
164 threshold: self.limit.load(Ordering::Relaxed),
165 level: self.level(),
166 smoothing: self.smoothing.load(Ordering::Relaxed),
167 })
168 }
169
170 fn active_flag(&self) -> Arc<AtomicBool> {
171 Arc::clone(&self.is_active)
172 }
173
174 fn f32_params(&self) -> HashMap<&'static str, Arc<AtomicF32>> {
190 let mut map = HashMap::new();
191 map.insert("threshold", Arc::clone(&self.limit));
192 map.insert("level", Arc::clone(&self.level));
194 map.insert("smoothing", Arc::clone(&self.smoothing));
195 map
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 fn distortion(threshold: f32, smoothing: f32) -> SCDistortion {
204 SCDistortion::new(
205 0,
206 "SC".to_string(),
207 true,
208 threshold,
209 0.0,
210 smoothing,
211 "#e67e22".to_string(),
212 )
213 }
214
215 mod success_path {
216 use super::*;
217
218 #[test]
219 fn sample_within_threshold_is_slightly_compressed() {
220 let mut fx = distortion(1.0, 1.0);
221 for _ in 0..10_000 {
222 fx.process(0.0);
223 }
224
225 let input = 0.1;
226 let output = fx.process(input);
227
228 assert!(output < input);
229 assert!((output - input).abs() < 0.01);
230 }
231
232 #[test]
233 fn sample_is_pushed_towards_limit() {
234 let limit = 0.5;
235 let mut fx = distortion(limit, 5.0);
236 for _ in 0..10_000 {
237 fx.process(0.0);
238 }
239
240 let out = fx.process(100.0);
241
242 assert!(out <= limit);
243 assert!((out - limit).abs() < 0.01);
244 }
245
246 #[test]
247 fn smoothing_parameter_affects_curve() {
248 let mut soft_fx = distortion(1.0, 1.0); let mut hard_fx = distortion(1.0, 10.0); for _ in 0..10_000 {
252 soft_fx.process(0.0);
253 hard_fx.process(0.0);
254 }
255
256 let input = 0.5;
257 let soft_out = soft_fx.process(input);
258 let hard_out = hard_fx.process(input);
259
260 assert!(soft_out < hard_out);
261 }
262
263 #[test]
264 fn process_if_active_passes_through_when_inactive() {
265 let mut fx = distortion(0.5, 10.0);
266 fx.set_active(false);
267 assert_eq!(fx.process_if_active(0.9), 0.9);
268 }
269
270 #[test]
271 fn set_threshold_updates_clip_level() {
272 let mut fx = distortion(0.8, 10.0);
273
274 fx.set_threshold(0.3);
275 assert!((fx.threshold() - 0.3).abs() < 1e-6);
276
277 for _ in 0..10_000 {
278 fx.process(0.0);
279 }
280
281 let output = fx.process(0.9);
282
283 assert!(output < 0.9);
284 assert!(
285 (output - 0.3).abs() < 0.05,
286 "Expected output to be near 0.3, got {}",
287 output
288 );
289
290 let massive_input = 100.0;
291 let limited_output = fx.process(massive_input);
292 assert!(limited_output <= 0.30001);
293 }
294
295 #[test]
296 fn level_boost_doubles_output_at_max() {
297 let mut fx = SCDistortion::new(
298 0,
299 "HC".to_string(),
300 true,
301 1.0,
302 1.0,
303 10.0,
304 "#e67e22".to_string(),
305 );
306 for _ in 0..20_000 {
308 fx.process(0.0);
309 }
310 let out = fx.process(0.3);
311 assert!((out - 0.6).abs() < 0.01, "expected ≈0.6, got {out}");
312 }
313
314 #[test]
315 fn level_unity_at_zero() {
316 let mut fx = distortion(1.0, 10.0); for _ in 0..10_000 {
318 fx.process(0.0);
319 }
320 let out = fx.process(0.4);
321 assert!((out - 0.4).abs() < 0.01, "expected ≈0.4, got {out}");
322 }
323 }
324
325 mod failure_path {
326 use super::*;
327 #[test]
328 fn threshold_above_one_is_clamped_to_one() {
329 let fx = distortion(2.0, 1.0);
330 assert_eq!(fx.threshold(), 1.0);
331 }
332
333 #[test]
334 fn threshold_of_zero_is_clamped_to_minimum() {
335 let fx = distortion(0.0, 1.0);
336 assert!(fx.threshold() > 0.0);
337 }
338
339 #[test]
340 fn smoothing_above_ten_is_clamped_to_ten() {
341 let fx = distortion(1.0, 11.0);
342 assert_eq!(fx.smoothing(), 10.0);
343 }
344
345 #[test]
346 fn smoothing_of_zero_is_clamped_to_minimum() {
347 let fx = distortion(1.0, 0.0);
348 assert!(fx.smoothing() > 0.0);
349 }
350 }
351}