1use audioadapter::{Adapter, AdapterMut};
2use bevy_platform::sync::{
3 Arc,
4 atomic::{AtomicBool, Ordering},
5};
6use core::num::NonZeroU32;
7use ringbuf::traits::Producer;
8use thunderdome::Arena;
9
10#[cfg(not(feature = "std"))]
11use bevy_platform::prelude::{Box, Vec};
12
13use bevy_platform::time::Instant;
14
15use firewheel_core::{
16 StreamInfo,
17 clock::InstantSamples,
18 dsp::{
19 buffer::ConstSequentialBuffer,
20 declick::{DeclickValues, Declicker},
21 },
22 event::{NodeEvent, ProcEventsIndex},
23 node::{AudioNodeProcessor, ProcExtra},
24};
25
26use crate::{
27 backend::BackendProcessInfo,
28 context::{FirewheelBitFlags, ProcessorChannel},
29 graph::ScheduleHeapData,
30 processor::{
31 event_scheduler::{EventScheduler, NodeEventSchedulerData},
32 profiling::ProfilerTx,
33 },
34};
35
36pub use profiling::ProfilingData;
37
38#[cfg(feature = "scheduled_events")]
39use crate::context::ClearScheduledEventsType;
40#[cfg(feature = "scheduled_events")]
41use firewheel_core::node::NodeID;
42#[cfg(feature = "scheduled_events")]
43use smallvec::SmallVec;
44
45#[cfg(feature = "musical_transport")]
46use firewheel_core::clock::{InstantMusical, TransportState};
47
48mod event_scheduler;
49mod handle_messages;
50mod process;
51pub(crate) mod profiling;
52
53#[cfg(feature = "musical_transport")]
54mod transport;
55#[cfg(feature = "musical_transport")]
56use transport::ProcTransportState;
57
58pub struct FirewheelProcessor {
59 inner: Option<FirewheelProcessorInner>,
60 drop_tx: ringbuf::HeapProd<FirewheelProcessorInner>,
61 drop_flag: Arc<AtomicBool>,
62}
63
64impl Drop for FirewheelProcessor {
65 fn drop(&mut self) {
66 self.drop_inner();
67 }
68}
69
70impl FirewheelProcessor {
71 pub(crate) fn new(
72 processor: FirewheelProcessorInner,
73 drop_tx: ringbuf::HeapProd<FirewheelProcessorInner>,
74 drop_flag: Arc<AtomicBool>,
75 ) -> Self {
76 Self {
77 inner: Some(processor),
78 drop_tx,
79 drop_flag,
80 }
81 }
82
83 pub fn process(
84 &mut self,
85 input: &dyn Adapter<f32>,
86 output: &mut dyn AdapterMut<f32>,
87 info: BackendProcessInfo,
88 ) {
89 self.poll_drop_flag();
90
91 if let Some(inner) = &mut self.inner {
92 inner.process(input, output, info);
93 } else {
94 output.fill_frames_with(0, info.frames, &0.0);
95 }
96 }
97
98 fn poll_drop_flag(&mut self) {
99 if self.inner.is_some() && self.drop_flag.load(Ordering::Relaxed) {
100 self.drop_inner();
101 }
102 }
103
104 fn drop_inner(&mut self) {
105 let Some(mut inner) = self.inner.take() else {
106 return;
107 };
108
109 inner.stream_stopped();
110
111 #[cfg(feature = "std")]
113 if std::thread::panicking() {
114 inner.poisoned = true;
115 }
116
117 let _ = self.drop_tx.try_push(inner);
118 }
119}
120
121pub(crate) struct FirewheelProcessorInner {
122 nodes: Arena<NodeEntry>,
123 schedule_data: Option<Box<ScheduleHeapData>>,
124
125 from_graph_rx: ringbuf::HeapCons<ContextToProcessorMsg>,
126 to_graph_tx: ringbuf::HeapProd<ProcessorToContextMsg>,
127
128 event_scheduler: EventScheduler,
129 proc_event_queue: Vec<ProcEventsIndex>,
130
131 sample_rate: NonZeroU32,
132 sample_rate_recip: f64,
133 max_block_frames: usize,
134
135 clock_samples: InstantSamples,
136 shared_clock_input: triple_buffer::Input<SharedClock>,
137 profiler_tx: ProfilerTx,
138
139 #[cfg(feature = "musical_transport")]
140 proc_transport_state: ProcTransportState,
141
142 flags: FirewheelBitFlags,
143 shared_flags: Arc<SharedFlags>,
144 clamp_graph_inputs_below_amp: Option<f32>,
145
146 last_input_overflow_log_instant: Option<Instant>,
147 last_output_underflow_log_instant: Option<Instant>,
148
149 pub(crate) extra: ProcExtra,
150
151 pub(crate) poisoned: bool,
155}
156
157pub(crate) struct FirewheelProcessorConfig {
158 pub flags: FirewheelBitFlags,
159 pub immediate_event_buffer_capacity: usize,
160 pub buffer_out_of_space_mode: BufferOutOfSpaceMode,
161 pub clamp_graph_inputs_below_amp: Option<f32>,
162 pub node_event_buffer_capacity: usize,
163 #[cfg(feature = "scheduled_events")]
164 pub scheduled_event_buffer_capacity: usize,
165}
166
167impl FirewheelProcessorInner {
168 pub(crate) fn new(
170 config: FirewheelProcessorConfig,
171 proc_channel: ProcessorChannel,
172 stream_info: &StreamInfo,
173 ) -> Self {
174 let FirewheelProcessorConfig {
175 flags,
176 immediate_event_buffer_capacity,
177 buffer_out_of_space_mode,
178 clamp_graph_inputs_below_amp,
179 node_event_buffer_capacity,
180 #[cfg(feature = "scheduled_events")]
181 scheduled_event_buffer_capacity,
182 } = config;
183
184 let ProcessorChannel {
185 shared_flags,
186 from_context_rx,
187 to_context_tx,
188 logger,
189 store,
190 profiler_tx,
191 shared_clock_input,
192 } = proc_channel;
193
194 Self {
195 nodes: Arena::new(),
196 schedule_data: None,
197 from_graph_rx: from_context_rx,
198 to_graph_tx: to_context_tx,
199 event_scheduler: EventScheduler::new(
200 immediate_event_buffer_capacity,
201 #[cfg(feature = "scheduled_events")]
202 scheduled_event_buffer_capacity,
203 buffer_out_of_space_mode,
204 ),
205 proc_event_queue: Vec::with_capacity(node_event_buffer_capacity),
206 sample_rate: stream_info.sample_rate,
207 sample_rate_recip: stream_info.sample_rate_recip,
208 max_block_frames: stream_info.max_block_frames.get() as usize,
209 clock_samples: InstantSamples(0),
210 shared_clock_input,
211 profiler_tx,
212 #[cfg(feature = "musical_transport")]
213 proc_transport_state: ProcTransportState::new(),
214 flags,
215 shared_flags,
216 clamp_graph_inputs_below_amp,
217 last_input_overflow_log_instant: None,
218 last_output_underflow_log_instant: None,
219 extra: ProcExtra {
220 scratch_buffers: ConstSequentialBuffer::new(
221 stream_info.max_block_frames.get() as usize
222 ),
223 declick_values: DeclickValues::new(stream_info.declick_frames),
224 logger,
225 store,
226 },
227 poisoned: false,
228 }
229 }
230}
231
232pub(crate) struct NodeEntry {
233 pub processor: Box<dyn AudioNodeProcessor>,
234 pub bypass_declick: Declicker,
235 pub is_bypassed: bool,
236 pub is_first_process: bool,
237 pub in_place_buffers: bool,
238
239 event_data: NodeEventSchedulerData,
240}
241
242pub(crate) enum ContextToProcessorMsg {
243 EventGroup(Vec<NodeEvent>),
244 NewSchedule(Box<ScheduleHeapData>),
245 SetFlags(FirewheelBitFlags),
246 #[cfg(feature = "musical_transport")]
247 SetTransportState(Box<TransportState>),
248 #[cfg(feature = "scheduled_events")]
249 ClearScheduledEvents(SmallVec<[ClearScheduledEventsEvent; 1]>),
250}
251
252#[allow(clippy::enum_variant_names)]
253pub(crate) enum ProcessorToContextMsg {
254 DropEventGroup(Vec<NodeEvent>),
255 DropSchedule(Box<ScheduleHeapData>),
256 #[cfg(feature = "musical_transport")]
257 DropTransportState(Box<TransportState>),
258 #[cfg(feature = "scheduled_events")]
259 DropClearScheduledEvents(SmallVec<[ClearScheduledEventsEvent; 1]>),
260}
261
262#[cfg(feature = "scheduled_events")]
263pub(crate) struct ClearScheduledEventsEvent {
264 pub node_id: Option<NodeID>,
266 pub event_type: ClearScheduledEventsType,
267}
268
269#[derive(Clone)]
270pub(crate) struct SharedClock {
271 pub clock_samples: InstantSamples,
272 #[cfg(feature = "scheduled_events")]
273 pub update_instant: Instant,
274 #[cfg(feature = "musical_transport")]
275 pub current_playhead: Option<InstantMusical>,
276 #[cfg(feature = "musical_transport")]
277 pub speed_multiplier: f64,
278 #[cfg(feature = "musical_transport")]
279 pub transport_is_playing: bool,
280}
281
282impl Default for SharedClock {
283 fn default() -> Self {
284 Self {
285 clock_samples: InstantSamples(0),
286 #[cfg(feature = "scheduled_events")]
287 update_instant: Instant::now(),
288 #[cfg(feature = "musical_transport")]
289 current_playhead: None,
290 #[cfg(feature = "musical_transport")]
291 speed_multiplier: 1.0,
292 #[cfg(feature = "musical_transport")]
293 transport_is_playing: false,
294 }
295 }
296}
297
298#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
300#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
301#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
302pub enum BufferOutOfSpaceMode {
303 #[default]
304 AllocateOnAudioThread,
309 Panic,
312 DropEvents,
318}
319
320#[derive(Default)]
321pub(crate) struct SharedFlags {
322 pub clipping_occurred: AtomicBool,
323}