Skip to main content

firewheel_graph/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3pub mod backend;
4mod context;
5pub mod error;
6pub mod graph;
7pub mod processor;
8mod time;
9
10#[cfg(feature = "unsafe_flush_denormals_to_zero")]
11mod ftz;
12
13#[cfg(feature = "scheduled_events")]
14pub use context::ClearScheduledEventsType;
15pub use context::{ActivateInfo, ContextQueue, FirewheelConfig, FirewheelContext, FirewheelFlags};
16
17extern crate alloc;
18
19#[cfg(test)]
20mod tests {
21    use crate::{backend::BackendProcessInfo, processor::FirewheelProcessor};
22    use audioadapter_buffers::direct::InterleavedSlice;
23    use bevy_platform::sync::{
24        Arc,
25        atomic::{AtomicBool, Ordering},
26    };
27    use core::{num::NonZeroU32, time::Duration};
28    use firewheel_core::node::{
29        AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, EmptyConfig,
30        NodeError, StreamStatus,
31    };
32
33    use super::*;
34
35    #[test]
36    // Firewheel is designed with
37    // [CLAP's threading model](https://github.com/free-audio/clap/blob/main/include/clap/plugin.h)
38    // in mind. This allows one to more easily create a custom node that hosts a 3rd party CLAP
39    // plugin binary.
40    //
41    // The purpose of this test is to ensure that the order in which nodes are dropped fit the
42    // CLAP threading model.
43    fn clap_drop_ordering() {
44        struct DummyClapPlugin {}
45        struct DummyClapPluginProcessor {
46            state: CustomState,
47        }
48        #[derive(Clone)]
49        struct CustomState {
50            plugin_main_dropped: Arc<AtomicBool>,
51            plugin_processor_dropped: Arc<AtomicBool>,
52        }
53
54        impl Drop for DummyClapPluginProcessor {
55            fn drop(&mut self) {
56                assert!(!self.state.plugin_main_dropped.load(Ordering::SeqCst));
57                self.state
58                    .plugin_processor_dropped
59                    .store(true, Ordering::SeqCst);
60            }
61        }
62
63        impl Drop for CustomState {
64            fn drop(&mut self) {
65                assert!(self.plugin_processor_dropped.load(Ordering::SeqCst));
66                self.plugin_main_dropped.store(true, Ordering::SeqCst);
67            }
68        }
69
70        impl AudioNode for DummyClapPlugin {
71            type Configuration = EmptyConfig;
72
73            fn info(&self, _: &Self::Configuration) -> Result<AudioNodeInfo, NodeError> {
74                Ok(AudioNodeInfo::new().custom_state(CustomState {
75                    plugin_main_dropped: Arc::new(AtomicBool::new(false)),
76                    plugin_processor_dropped: Arc::new(AtomicBool::new(false)),
77                }))
78            }
79
80            fn construct_processor(
81                &self,
82                _: &Self::Configuration,
83                cx: ConstructProcessorContext,
84            ) -> Result<impl AudioNodeProcessor, NodeError> {
85                let state = cx.custom_state::<CustomState>().unwrap().clone();
86
87                Ok(DummyClapPluginProcessor { state })
88            }
89        }
90
91        impl AudioNodeProcessor for DummyClapPluginProcessor {}
92
93        const DUMMY_OUT_LEN: usize = 1024;
94        let mut dummy_out_buffer = vec![0.0; DUMMY_OUT_LEN];
95
96        let activate_info = ActivateInfo {
97            sample_rate: NonZeroU32::new(44100).unwrap(),
98            max_block_frames: NonZeroU32::new(DUMMY_OUT_LEN as u32).unwrap(),
99            num_stream_in_channels: 0,
100            num_stream_out_channels: 1,
101            input_to_output_latency_seconds: 0.0,
102        };
103        let process_info = BackendProcessInfo {
104            frames: DUMMY_OUT_LEN,
105            process_timestamp: None,
106            duration_since_stream_start: Duration::default(),
107            input_stream_status: StreamStatus::empty(),
108            output_stream_status: StreamStatus::empty(),
109            dropped_frames: 0,
110            process_to_playback_delay: None,
111        };
112
113        let mut process = |processor: &mut FirewheelProcessor| {
114            processor.process(
115                &InterleavedSlice::new(&[], 0, 0).unwrap(),
116                &mut InterleavedSlice::new_mut(&mut dummy_out_buffer, 1, DUMMY_OUT_LEN).unwrap(),
117                process_info.clone(),
118            );
119        };
120
121        // Test dropping by removing node manually
122        {
123            let mut context = FirewheelContext::new(Default::default());
124            let node_id = context.add_node(DummyClapPlugin {}, None).unwrap();
125
126            let plugin_main_dropped = context
127                .node_state::<CustomState>(node_id)
128                .unwrap()
129                .plugin_main_dropped
130                .clone();
131
132            let mut processor = context.activate(activate_info.clone()).unwrap();
133
134            context.update().unwrap();
135
136            process(&mut processor);
137
138            context.remove_node(node_id).unwrap();
139
140            context.update().unwrap();
141
142            process(&mut processor);
143
144            context.update().unwrap();
145
146            assert!(plugin_main_dropped.load(Ordering::SeqCst));
147        }
148
149        // Test dropping processor before context
150        {
151            let mut context = FirewheelContext::new(Default::default());
152            context.add_node(DummyClapPlugin {}, None).unwrap();
153
154            let mut processor = context.activate(activate_info.clone()).unwrap();
155
156            context.update().unwrap();
157
158            process(&mut processor);
159
160            context.update().unwrap();
161
162            let _ = processor;
163            let _ = context;
164        }
165
166        // Test dropping processor after context
167        {
168            let mut context = FirewheelContext::new(Default::default());
169            context.add_node(DummyClapPlugin {}, None).unwrap();
170
171            let mut processor = context.activate(activate_info.clone()).unwrap();
172
173            context.update().unwrap();
174
175            process(&mut processor);
176
177            context.update().unwrap();
178
179            context.request_deactivate();
180
181            // The processor must process at least once to deactivate.
182            process(&mut processor);
183
184            let _ = context;
185            let _ = processor;
186        }
187    }
188}