Skip to main content

rustriff_lib/domain/
channel.rs

1use crate::domain::dto::effect::effect_dto::EffectDto;
2use crate::domain::dto::tone_stack_dto::ToneStackDto;
3use crate::domain::effect::Effect;
4use crate::domain::tone_stack::ToneStack;
5use atomic_float::AtomicF32;
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
8use std::sync::{Arc, Mutex};
9use tracing::{error, info};
10
11/// Atomic handles retained by `Channel` after the effect chain is moved to the
12/// audio worker thread.  Commands write through these Arcs; the audio thread
13/// reads from the same Arcs on every sample — completely lock-free.
14struct EffectHandles {
15    is_active: Arc<AtomicBool>,
16    /// Named f32 parameters (e.g. `"threshold"`). The Effect trait's
17    /// `f32_params()` method populates this, so no downcasting is needed.
18    params: HashMap<&'static str, ParamValue>,
19    /// Non-real-time cabinet metadata mirrored from the effect at mutation time.
20    /// Commands can read this without touching the hot `effect_chain` mutex.
21    cabinet_ir_file_path: Option<String>,
22}
23
24/// Represents an audio channel with atomic gain, master volume, and tone stack parameters.
25///
26/// `Channel` uses [`AtomicF32`] for lock-free updates of audio parameters from
27/// the UI thread while the audio processing thread reads them without waiting.
28/// This enables low-latency parameter changes without interrupting audio playback.
29///
30/// The tone stack provides equalization controls for bass (low frequencies), middle (mid-range frequencies),
31/// and treble (high frequencies), allowing fine-tuning of the audio signal's frequency response.
32/// These parameters are also updated atomically for low-latency changes.
33///
34/// Effect chain is the chain of effects where the signal is put through. Effects are applied in the order they are added to the chain.
35/// The `Channel` struct provides methods to add and remove effects from the chain, allowing dynamic modification of the audio processing pipeline.
36///
37/// Next effect_id is the unique identifier given to the next created effect in the chain.
38///
39/// Gain and volume are validated to be positive values (> 0.0); attempting to
40/// set a negative or zero value will panic.
41///
42/// Tone stack values are validated to be between 0.0 and 1.0; attempting to set a value outside this range will panic.
43pub struct Channel {
44    id: u32,
45    name: String,
46    gain: Arc<AtomicF32>,
47    tone_stack: Arc<ToneStack>,
48    volume: Arc<AtomicF32>,
49    effect_chain: Arc<Mutex<Vec<Box<dyn Effect>>>>,
50    /// Retained per-effect Arc handles indexed by effect id.
51    /// Stays populated even after `take_effect_chain` moves the effects to the audio thread.
52    effect_handles: HashMap<u32, EffectHandles>,
53    next_effect_id: u32,
54}
55
56impl Channel {
57    fn build_effect_handles(effect: &dyn Effect) -> EffectHandles {
58        let cabinet_ir_file_path = match effect.to_dto() {
59            EffectDto::Cabinet(cabinet) => Some(cabinet.ir_file_path),
60            _ => None,
61        };
62
63        let mut params = HashMap::new();
64
65        for (name, arc) in effect.f32_params() {
66            params.insert(name, ParamValue::Float(arc));
67        }
68
69        for (name, arc) in effect.u32_params() {
70            params.insert(name, ParamValue::Uint(arc));
71        }
72
73        EffectHandles {
74            is_active: effect.active_flag(),
75            params,
76            cabinet_ir_file_path,
77        }
78    }
79
80    /// Creates a new `Channel` with the given name and optional gain/master volume values.
81    ///
82    /// If `gain` or `master_volume` are not provided, they default to `1.0`.
83    /// The tone stack parameters (bass, middle, treble) are initialized to `1.0`.
84    /// The `effect chain` is initialized as an empty vector, and the next effect ID starts at `0`.
85    ///
86    /// # Arguments
87    ///
88    /// * `name` - A human-readable name for the channel (e.g., "Main", "Overdrive").
89    /// * `gain` - Optional initial gain value. Defaults to `1.0` if `None`.
90    /// * `master_volume` - Optional initial master volume value. Defaults to `1.0` if `None`.
91    pub fn new(id: u32, name: String, gain: Option<f32>, volume: Option<f32>) -> Self {
92        let gain = gain.unwrap_or(1.0);
93        let volume = volume.unwrap_or(1.0);
94
95        Self {
96            id,
97            name,
98            gain: Arc::new(AtomicF32::new(gain)),
99            tone_stack: Arc::new(ToneStack::new()),
100            volume: Arc::new(AtomicF32::new(volume)),
101            effect_chain: Arc::new(Mutex::new(Vec::new())),
102            effect_handles: HashMap::new(),
103            next_effect_id: 0,
104        }
105    }
106
107    // ── Gain ─────────────────────────────────────────────────────────────────
108
109    /// Sets the gain value for this channel.
110    ///
111    /// The gain value is atomically updated and will be read by the audio processing
112    /// thread on the next sample cycle.
113    ///
114    /// # Arguments
115    ///
116    /// * `gain` - The new gain value. Must be positive (> 0.0).
117    ///
118    /// # Panics
119    ///
120    /// Panics if `gain` is negative or zero.
121    pub fn set_gain(&self, gain: f32) {
122        if gain.is_sign_positive() {
123            self.gain.store(gain, Ordering::Relaxed);
124        } else {
125            error!("Gain must be a positive number");
126            panic!("Gain must be positive");
127        }
128    }
129
130    /// Returns a cloned [`Arc`] to the atomic gain value.
131    ///
132    /// Allows independent threads to share and read/write the gain parameter
133    /// without contention.
134    pub fn gain(&self) -> Arc<AtomicF32> {
135        Arc::clone(&self.gain)
136    }
137
138    // ── Tone stack ────────────────────────────────────────────────────────────
139
140    /// Sets the tone stack parameters from a [`ToneStackDto`].
141    ///
142    /// The bass, middle, and treble values in the DTO should be between 0.0 and 1.0.
143    ///
144    /// # Arguments
145    ///
146    /// * `tone_stack` - The tone stack data transfer object containing the new values.
147    ///
148    /// # Panics
149    ///
150    /// Panics if any value is outside the valid range.
151    pub fn set_tone_stack(&self, tone_stack: ToneStackDto) {
152        self.tone_stack.set_bass(tone_stack.bass);
153        self.tone_stack.set_middle(tone_stack.middle);
154        self.tone_stack.set_treble(tone_stack.treble);
155    }
156
157    /// Sets the bass level for the tone stack.
158    ///
159    /// The input value is expected to be normalized in the range 0.0-1.0.
160    ///
161    /// # Arguments
162    ///
163    /// * `bass` - The bass level (0.0-1.0).
164    ///
165    /// # Panics
166    ///
167    /// Panics if the scaled value is not between 0.0 and 1.0.
168    pub fn set_bass(&self, bass: f32) {
169        self.tone_stack.set_bass(bass);
170    }
171
172    /// Sets the middle level for the tone stack.
173    ///
174    /// The input value is expected to be normalized in the range 0.0-1.0.
175    ///
176    /// # Arguments
177    ///
178    /// * `middle` - The middle level (0.0-1.0).
179    ///
180    /// # Panics
181    ///
182    /// Panics if the value is not between 0.0 and 1.0.
183    pub fn set_middle(&self, middle: f32) {
184        self.tone_stack.set_middle(middle);
185    }
186
187    /// Sets the treble level for the tone stack.
188    ///
189    /// The input value is expected to be normalized in the range 0.0-1.0.
190    ///
191    /// # Arguments
192    ///
193    /// * `treble` - The treble level (0.0-1.0).
194    ///
195    /// # Panics
196    ///
197    /// Panics if the value is not between 0.0 and 1.0.
198    pub fn set_treble(&self, treble: f32) {
199        self.tone_stack.set_treble(treble);
200    }
201
202    /// Returns a cloned [`Arc`] to the tone stack.
203    ///
204    /// Allows independent threads to access the tone stack parameters for audio processing.
205    pub fn tone_stack(&self) -> Arc<ToneStack> {
206        Arc::clone(&self.tone_stack)
207    }
208
209    // ── Volume ────────────────────────────────────────────────────────────────
210
211    /// Sets the volume for this channel.
212    ///
213    /// #Arguments
214    ///
215    /// * `volume` - The volume level (must be positive)
216    ///
217    /// # Panics
218    ///
219    /// Panics if the volume is negative.
220    pub fn set_volume(&self, volume: f32) {
221        if volume.is_sign_positive() {
222            self.volume.store(volume, Ordering::Relaxed);
223        } else {
224            error!("Volume must be a positive number");
225            panic!("Volume must be positive");
226        }
227    }
228
229    /// Returns a cloned [`Arc`] to the atomic volume value.
230    ///
231    /// Allows independent threads to share and read/write the volume parameter without contention.
232    pub fn volume(&self) -> Arc<AtomicF32> {
233        Arc::clone(&self.volume)
234    }
235
236    // ── Metadata ──────────────────────────────────────────────────────────────
237
238    /// Sets the name of the Channel
239    ///
240    /// # Arguments
241    ///
242    /// * `name` - The name
243    pub fn set_name(&mut self, name: String) {
244        self.name = name;
245    }
246
247    /// Returns the name of the channel.
248    pub fn name(&self) -> &String {
249        &self.name
250    }
251
252    /// Returns the unique identifier of the channel.
253    pub fn id(&self) -> u32 {
254        self.id
255    }
256
257    // ── Effect chain ──────────────────────────────────────────────────────────
258
259    /// Returns an `Arc<Mutex<Vec<Box<dyn Effect>>>>` representing the effect chain for this channel.
260    pub fn effect_chain(&self) -> Arc<Mutex<Vec<Box<dyn Effect>>>> {
261        Arc::clone(&self.effect_chain)
262    }
263
264    /// Sets the effect chain to a new given chain of effects
265    pub fn restore_effect_chain(&mut self, effects: Vec<Box<dyn Effect>>) {
266        self.effect_handles.clear();
267        for effect in &effects {
268            self.effect_handles
269                .insert(effect.id(), Self::build_effect_handles(effect.as_ref()));
270        }
271
272        if let Ok(mut chain) = self.effect_chain.lock() {
273            *chain = effects;
274        }
275    }
276
277    /// Adds an effect, capturing its shared atomic handles so commands can reach
278    /// them after the chain has been moved to the audio thread.
279    ///
280    /// No downcasting — every effect self-reports its parameters via
281    /// [`Effect::f32_params`](crate::domain::effect::Effect::f32_params).
282    pub fn add_effect_to_chain(&mut self, effect: Box<dyn Effect>) {
283        info!(
284            "Added effect '{}' (id={}) to chain",
285            effect.name(),
286            effect.id()
287        );
288
289        let mut combined_params = HashMap::new();
290
291        // Collect f32 params
292        for (name, arc) in effect.f32_params() {
293            combined_params.insert(name, ParamValue::Float(arc));
294        }
295
296        // Collect u32 params (assuming this exists in your Effect trait)
297        for (name, arc) in effect.u32_params() {
298            combined_params.insert(name, ParamValue::Uint(arc));
299        }
300
301        self.effect_handles
302            .insert(effect.id(), Self::build_effect_handles(effect.as_ref()));
303
304        if let Ok(mut chain) = self.effect_chain.lock() {
305            chain.push(effect);
306            self.next_effect_id += 1;
307        }
308    }
309
310    /// Removes an effect from the channel's effect chain by its unique identifier.
311    ///
312    /// If the effect is found and removed, an informational message is logged. If the effect is not found, an error message is logged.
313    ///
314    /// # Arguments
315    ///
316    /// * `effect_id` - The unique identifier of the effect to remove from the chain
317    pub fn remove_effect_from_chain(&mut self, effect_id: u32) {
318        if let Ok(mut chain) = self.effect_chain.lock() {
319            if let Some(pos) = chain.iter().position(|e| e.id() == effect_id) {
320                info!("Removed effect: {} from chain", chain[pos].name());
321                chain.remove(pos);
322                self.effect_handles.remove(&effect_id);
323            } else {
324                error!("Effect with id {} not found in chain", effect_id);
325            }
326        }
327    }
328
329    /// Returns the next available unique identifier for an effect in this channel's effect chain.
330    pub fn next_effect_id(&self) -> u32 {
331        self.next_effect_id
332    }
333
334    /// Returns the set of cabinet IR file names referenced by this channel.
335    ///
336    /// This reads mirrored effect metadata from `effect_handles` and therefore
337    /// does not touch the real-time `effect_chain` mutex.
338    pub fn used_cabinet_ir_profiles(&self) -> std::collections::HashSet<String> {
339        self.effect_handles
340            .values()
341            .filter_map(|handles| handles.cabinet_ir_file_path.clone())
342            .collect()
343    }
344
345    // ── Effect controls (written from command handlers) ───────────────────────
346
347    /// Toggles the active state of an effect.
348    ///
349    /// Enables or disables audio processing for a specific effect in this channel's
350    /// effect chain. The change takes effect immediately on the audio thread (lock-free).
351    ///
352    /// # Arguments
353    /// * `effect_id` — Unique identifier of the effect to toggle
354    ///
355    /// # Returns
356    /// * `Ok(bool)` — The new active state (`true` = now active, `false` = now bypassed)
357    /// * `Err(String)` — Error message if effect ID not found in this channel
358    pub fn toggle_effect(&self, effect_id: u32) -> Result<bool, String> {
359        let h = self
360            .effect_handles
361            .get(&effect_id)
362            .ok_or_else(|| format!("No effect with id {effect_id}"))?;
363        let next = !h.is_active.load(Ordering::Relaxed);
364        h.is_active.store(next, Ordering::Relaxed);
365        info!(
366            "Effect {effect_id} → {}",
367            if next { "active" } else { "bypassed" }
368        );
369        Ok(next)
370    }
371
372    /// # Sets a Named Float32 and Uint32 Parameter on an Effect
373    /// Generic parameter update mechanism for effect settings. Parameters are identified
374    /// by string names and stored as lock-free atomics (`Arc<AtomicF32>`).
375    ///
376    /// ## Lock-Free Operation
377    ///
378    /// Uses `Ordering::Relaxed` atomic store — no synchronisation overhead:
379    /// - Write happens immediately on the calling thread
380    /// - Audio thread reads the updated value on next sample
381    /// - No locks or condition variables
382    ///
383    /// ## Parameter Discovery
384    ///
385    /// Parameters are exposed via `Effect::f32_params()` and `Effect::u32_params` which returns a HashMap.
386    ///
387    /// # Arguments
388    /// * `effect_id` — ID of the effect to modify
389    /// * `param` — Parameter name string (e.g., `"threshold"`, `"level"`)
390    /// * `value` — New parameter value as `f32`
391    ///
392    /// # Returns
393    /// * `Ok(())` — Parameter updated successfully
394    /// * `Err(String)` — Error if:
395    ///   - Effect with given ID not found
396    ///   - Parameter name not recognised by the effect
397    pub fn set_effect_param(
398        &self,
399        effect_id: u32,
400        param: &str,
401        value: impl Into<ParamInput>,
402    ) -> Result<(), String> {
403        let h = self
404            .effect_handles
405            .get(&effect_id)
406            .ok_or_else(|| format!("No effect with id {effect_id}"))?;
407
408        let variant = h
409            .params
410            .get(param)
411            .ok_or_else(|| format!("Param '{param}' not found on effect {effect_id}"))?;
412
413        match (variant, value.into()) {
414            (ParamValue::Float(arc), ParamInput::F32(v)) => {
415                arc.store(v, Ordering::Relaxed);
416            }
417            (ParamValue::Uint(arc), ParamInput::U32(v)) => {
418                arc.store(v, Ordering::Relaxed);
419            }
420            _ => return Err(format!("Type mismatch for parameter '{param}'")),
421        }
422
423        Ok(())
424    }
425}
426
427pub enum ParamInput {
428    F32(f32),
429    U32(u32),
430}
431
432pub enum ParamValue {
433    Float(Arc<AtomicF32>),
434    Uint(Arc<AtomicU32>),
435}
436
437impl From<f32> for ParamInput {
438    fn from(f: f32) -> Self {
439        Self::F32(f)
440    }
441}
442impl From<u32> for ParamInput {
443    fn from(u: u32) -> Self {
444        Self::U32(u)
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    mod success_path {
453        use super::*;
454        use crate::services::effects::cabinet::cabinet::Cabinet;
455        use crate::services::effects::distortion::hc_distortion::HCDistortion;
456
457        #[test]
458        fn gain_set_to_positive_value_should_succeed() {
459            let channel = Channel::new(1, "Test".to_string(), None, None);
460            channel.set_gain(0.5);
461            assert_eq!(channel.gain().load(Ordering::Relaxed), 0.5);
462        }
463
464        #[test]
465        fn volume_set_to_positive_value_should_succeed() {
466            let channel = Channel::new(1, "Test".to_string(), None, None);
467            channel.set_volume(0.5);
468            assert_eq!(channel.volume().load(Ordering::Relaxed), 0.5);
469        }
470
471        #[test]
472        fn toggle_effect_flips_active_state() {
473            let mut channel = Channel::new(0, "Test".to_string(), None, None);
474            let effect_id = channel.next_effect_id();
475            channel.add_effect_to_chain(Box::new(HCDistortion::new(
476                effect_id,
477                "Test Effect".to_string(),
478                false,
479                0.5,
480                0.0,
481                "#e67e22".to_string(),
482            )));
483
484            let was = channel.effect_handles[&effect_id]
485                .is_active
486                .load(Ordering::Relaxed);
487            let next = channel.toggle_effect(effect_id).unwrap();
488            assert_eq!(next, !was);
489        }
490
491        #[test]
492        fn set_effect_param_updates_threshold() {
493            let mut channel = Channel::new(0, "Test".to_string(), None, None);
494            let effect_id = channel.next_effect_id();
495            channel.add_effect_to_chain(Box::new(HCDistortion::new(
496                effect_id,
497                "Test Effect".to_string(),
498                false,
499                0.5,
500                0.0,
501                "#e67e22".to_string(),
502            )));
503
504            // Update the parameter
505            channel
506                .set_effect_param(effect_id, "threshold", 0.3f32)
507                .unwrap();
508
509            // Access the handle
510            let handle = &channel.effect_handles[&effect_id];
511            let param = &handle.params["threshold"];
512
513            // Match on the enum to load the value
514            if let ParamValue::Float(arc) = param {
515                let v = arc.load(Ordering::Relaxed);
516                assert!((v - 0.3).abs() < 1e-6);
517            } else {
518                panic!("Expected threshold to be a Float parameter");
519            }
520        }
521
522        #[test]
523        fn adding_effect_to_effect_chain_should_add_an_effect_to_effect_chain() {
524            let mut channel = Channel::new(1, "Test".to_string(), None, None);
525            let effect_id = channel.next_effect_id();
526
527            channel.add_effect_to_chain(Box::new(HCDistortion::new(
528                effect_id,
529                "Test Effect".to_string(),
530                false,
531                0.5,
532                0.0,
533                "#e67e22".to_string(),
534            )));
535
536            let chain = channel.effect_chain.lock().unwrap();
537            assert_eq!(chain.len(), 1);
538        }
539
540        #[test]
541        fn removing_effect_from_effect_chain_should_remove_an_effect_from_effect_chain() {
542            let mut channel = Channel::new(1, "Test".to_string(), None, None);
543            let effect_id = channel.next_effect_id();
544            let effect = Box::new(HCDistortion::new(
545                effect_id,
546                "Test Effect".to_string(),
547                false,
548                0.5,
549                0.0,
550                "#e67e22".to_string(),
551            ));
552
553            channel.add_effect_to_chain(effect);
554
555            {
556                let chain_before = channel.effect_chain.lock().unwrap();
557                assert_eq!(chain_before.len(), 1);
558            }
559
560            channel.remove_effect_from_chain(effect_id);
561
562            let chain_after = channel.effect_chain.lock().unwrap();
563            assert_eq!(chain_after.len(), 0);
564            assert!(!channel.effect_handles.contains_key(&effect_id));
565        }
566
567        #[test]
568        fn restore_effect_chain_replaces_and_reorders_existing_chain() {
569            let mut channel = Channel::new(1, "Test".to_string(), None, None);
570
571            let id_1 = channel.next_effect_id();
572            let effect_1 = Box::new(HCDistortion::new(
573                id_1,
574                "Effect 1".to_string(),
575                false,
576                0.5,
577                0.0,
578                "#color1".to_string(),
579            ));
580
581            let id_2 = channel.next_effect_id();
582            let effect_2 = Box::new(HCDistortion::new(
583                id_2,
584                "Effect 2".to_string(),
585                false,
586                0.5,
587                0.0,
588                "#color2".to_string(),
589            ));
590
591            channel.add_effect_to_chain(effect_1);
592            channel.add_effect_to_chain(effect_2);
593
594            let reordered_1 = Box::new(HCDistortion::new(
595                id_1,
596                "Effect 1".to_string(),
597                false,
598                0.5,
599                0.0,
600                "#color1".to_string(),
601            ));
602            let reordered_2 = Box::new(HCDistortion::new(
603                id_2,
604                "Effect 2".to_string(),
605                false,
606                0.5,
607                0.0,
608                "#color2".to_string(),
609            ));
610
611            let new_order: Vec<Box<dyn Effect>> = vec![reordered_2, reordered_1];
612
613            channel.restore_effect_chain(new_order);
614
615            let chain = channel.effect_chain.lock().unwrap();
616            assert_eq!(chain.len(), 2, "Chain should still have 2 effects");
617            assert_eq!(chain[0].id(), id_2, "First effect should now be ID 2");
618            assert_eq!(chain[1].id(), id_1, "Second effect should now be ID 1");
619        }
620
621        #[test]
622        fn used_cabinet_ir_profiles_tracks_added_and_removed_cabinet_effects_without_locking_chain()
623        {
624            let mut channel = Channel::new(1, "Test".to_string(), None, None);
625            let cabinet_id = channel.next_effect_id();
626            channel.add_effect_to_chain(Box::new(Cabinet::new(
627                cabinet_id,
628                "Cab A".to_string(),
629                true,
630                "#111111".to_string(),
631                "Vox-ac30.wav".to_string(),
632                48_000,
633            )));
634
635            let used = channel.used_cabinet_ir_profiles();
636            assert_eq!(used.len(), 1);
637            assert!(used.contains("Vox-ac30.wav"));
638
639            channel.remove_effect_from_chain(cabinet_id);
640
641            let used_after_remove = channel.used_cabinet_ir_profiles();
642            assert!(used_after_remove.is_empty());
643        }
644
645        #[test]
646        fn restore_effect_chain_refreshes_cabinet_ir_metadata_snapshot() {
647            let mut channel = Channel::new(1, "Test".to_string(), None, None);
648
649            channel.add_effect_to_chain(Box::new(Cabinet::new(
650                0,
651                "Old Cab".to_string(),
652                true,
653                "#111111".to_string(),
654                "Vox-ac30.wav".to_string(),
655                48_000,
656            )));
657
658            channel.restore_effect_chain(vec![Box::new(Cabinet::new(
659                7,
660                "New Cab".to_string(),
661                true,
662                "#222222".to_string(),
663                "Reverb-oxford-lean.wav".to_string(),
664                48_000,
665            ))]);
666
667            let used = channel.used_cabinet_ir_profiles();
668            assert_eq!(used.len(), 1);
669            assert!(used.contains("Reverb-oxford-lean.wav"));
670            assert!(!used.contains("Vox-ac30.wav"));
671        }
672    }
673
674    mod failure_path {
675        use super::*;
676        use crate::services::effects::distortion::hc_distortion::HCDistortion;
677
678        #[test]
679        #[should_panic(expected = "Gain must be positive")]
680        fn gain_set_to_negative_value_should_panic() {
681            let channel = Channel::new(1, "Test".to_string(), None, None);
682            channel.set_gain(-0.5);
683        }
684
685        #[test]
686        #[should_panic(expected = "Volume must be positive")]
687        fn volume_set_to_negative_value_should_panic() {
688            let channel = Channel::new(1, "Test".to_string(), None, None);
689            channel.set_volume(-0.5);
690        }
691
692        #[test]
693        fn toggle_unknown_effect_returns_err() {
694            let channel = Channel::new(1, "Test".to_string(), None, None);
695            assert!(channel.toggle_effect(999).is_err());
696        }
697
698        #[test]
699        fn removing_invalid_effect_id_should_not_remove_any_effect() {
700            let mut channel = Channel::new(1, "Test".to_string(), None, None);
701            let effect_id = channel.next_effect_id();
702            let effect = Box::new(HCDistortion::new(
703                effect_id,
704                "Test Effect".to_string(),
705                false,
706                0.5,
707                0.0,
708                "#e67e22".to_string(),
709            ));
710
711            channel.add_effect_to_chain(effect);
712
713            let len_before = channel.effect_chain.lock().unwrap().len();
714            channel.remove_effect_from_chain(effect_id + 1);
715
716            let len_after = channel.effect_chain.lock().unwrap().len();
717            assert_eq!(len_before, len_after);
718        }
719    }
720}