firewheel_core/
node.rs

1use std::{any::Any, fmt::Debug, hash::Hash, ops::Range};
2
3use crate::{
4    channel_config::{ChannelConfig, ChannelCount},
5    clock::{ClockSamples, ClockSeconds, MusicalTime, MusicalTransport},
6    dsp::declick::DeclickValues,
7    event::{NodeEvent, NodeEventList, NodeEventType},
8    SilenceMask, StreamInfo,
9};
10
11pub mod dummy;
12
13/// A globally unique identifier for a node.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct NodeID(pub thunderdome::Index);
16
17impl NodeID {
18    pub const DANGLING: Self = Self(thunderdome::Index::DANGLING);
19}
20
21impl Default for NodeID {
22    fn default() -> Self {
23        Self::DANGLING
24    }
25}
26
27/// Information about an [`AudioNode`].
28///
29/// This struct enforces the use of the builder pattern for future-proofness, as
30/// it is likely that more fields will be added in the future.
31#[derive(Debug)]
32pub struct AudioNodeInfo {
33    debug_name: &'static str,
34    channel_config: ChannelConfig,
35    uses_events: bool,
36    call_update_method: bool,
37    custom_state: Option<Box<dyn Any>>,
38}
39
40impl AudioNodeInfo {
41    /// Construct a new [`AudioNodeInfo`] builder struct.
42    pub const fn new() -> Self {
43        Self {
44            debug_name: "unnamed",
45            channel_config: ChannelConfig {
46                num_inputs: ChannelCount::ZERO,
47                num_outputs: ChannelCount::ZERO,
48            },
49            uses_events: false,
50            call_update_method: false,
51            custom_state: None,
52        }
53    }
54
55    /// A unique name for this type of node, used for debugging purposes.
56    pub const fn debug_name(mut self, debug_name: &'static str) -> Self {
57        self.debug_name = debug_name;
58        self
59    }
60
61    /// The channel configuration of this node.
62    ///
63    /// By default this has a channel configuration with zero input and output
64    /// channels.
65    pub const fn channel_config(mut self, channel_config: ChannelConfig) -> Self {
66        self.channel_config = channel_config;
67        self
68    }
69
70    /// Set to `true` if this node type uses events, `false` otherwise.
71    ///
72    /// Setting to `false` will help the system save some memory by not
73    /// allocating an event buffer for this node.
74    ///
75    /// By default this is set to `false`.
76    pub const fn uses_events(mut self, uses_events: bool) -> Self {
77        self.uses_events = uses_events;
78        self
79    }
80
81    /// Set to `true` if this node wishes to have the Firewheel context call
82    /// [`AudioNode::update`] on every update cycle.
83    ///
84    /// By default this is set to `false`.
85    pub const fn call_update_method(mut self, call_update_method: bool) -> Self {
86        self.call_update_method = call_update_method;
87        self
88    }
89
90    /// Custom `!Send` state that can be stored in the Firewheel context and accessed
91    /// by the user.
92    ///
93    /// The user accesses this state via `FirewheelCtx::node_state` and
94    /// `FirewheelCtx::node_state_mut`.
95    pub fn custom_state<T: 'static>(mut self, custom_state: T) -> Self {
96        self.custom_state = Some(Box::new(custom_state));
97        self
98    }
99}
100
101/// Information about an [`AudioNode`]. Used internally by the Firewheel context.
102#[derive(Debug)]
103pub struct AudioNodeInfoInner {
104    pub debug_name: &'static str,
105    pub channel_config: ChannelConfig,
106    pub uses_events: bool,
107    pub call_update_method: bool,
108    pub custom_state: Option<Box<dyn Any>>,
109}
110
111impl Into<AudioNodeInfoInner> for AudioNodeInfo {
112    fn into(self) -> AudioNodeInfoInner {
113        AudioNodeInfoInner {
114            debug_name: self.debug_name,
115            channel_config: self.channel_config,
116            uses_events: self.uses_events,
117            call_update_method: self.call_update_method,
118            custom_state: self.custom_state,
119        }
120    }
121}
122
123/// A trait representing a node in a Firewheel audio graph.
124///
125/// # Notes about ECS
126///
127/// In order to be friendlier to ECS's (entity component systems), it is encouraged
128/// that any struct deriving this trait be POD (plain ol' data). If you want your
129/// audio node to be usable in the Bevy game engine, also derive
130/// `bevy_ecs::prelude::Component`. (You can hide this derive behind a feature flag
131/// by using `#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]`).
132///
133/// # Audio Node Lifecycle
134///
135/// 1. The user constructs the node as POD or from a custom constructor method for
136/// that node.
137/// 2. The user adds the node to the graph using `FirewheelCtx::add_node`. If the
138/// node has any custom configuration, then the user passes that configuration to this
139/// method as well. In this method, the Firewheel context calls [`AudioNode::info`] to
140/// get information about the node. The node can also store any custom state in the
141/// [`AudioNodeInfo`] struct.
142/// 3. At this point the user may now call `FirewheelCtx::node_state` and
143/// `FirewheelCtx::node_state_mut` to retrieve the node's custom state.
144/// 4. If [`AudioNodeInfo::call_update_method`] was set to `true`, then
145/// [`AudioNode::update`] will be called every time the Firewheel context updates.
146/// The node's custom state is also accessible in this method.
147/// 5. When the Firewheel context is ready for the node to start processing data,
148/// it calls [`AudioNode::construct_processor`] to retrieve the realtime
149/// [`AudioNodeProcessor`] counterpart of the node. This processor counterpart is
150/// then sent to the audio thread.
151/// 6. The Firewheel processor calls [`AudioNodeProcessor::process`] whenever there
152/// is a new block of audio data to process.
153/// 7. (Graceful shutdown)
154///
155///     7a. The Firewheel processor calls [`AudioNodeProcessor::stream_stopped`].
156/// The processor is then sent back to the main thread.
157///
158///     7b. If a new audio stream is started, then the context will call
159/// [`AudioNodeProcessor::new_stream`] on the main thread, and then send the
160/// processor back to the audio thread for processing.
161///
162///     7c. If the Firewheel context is dropped before a new stream is started, then
163/// both the node and the processor counterpart are dropped.
164/// 8. (Audio thread crashes or stops unexpectedly) - The node's processor counterpart
165/// may or may not be dropped. The user may try to create a new audio stream, in which
166/// case [`AudioNode::construct_processor`] might be called again. If a second processor
167/// instance is not able to be created, then the node may panic.
168pub trait AudioNode {
169    /// A type representing this constructor's configuration.
170    ///
171    /// This is intended as a one-time configuration to be used
172    /// when constructing an audio node. When no configuration
173    /// is required, [`EmptyConfig`] should be used.
174    type Configuration: Default;
175
176    /// Get information about this node.
177    ///
178    /// This method is only called once after the node is added to the audio graph.
179    fn info(&self, configuration: &Self::Configuration) -> AudioNodeInfo;
180
181    /// Construct a realtime processor for this node.
182    ///
183    /// * `configuration` - The custom configuration of this node.
184    /// * `cx` - A context for interacting with the Firewheel context. This context
185    /// also includes information about the audio stream.
186    fn construct_processor(
187        &self,
188        configuration: &Self::Configuration,
189        cx: ConstructProcessorContext,
190    ) -> impl AudioNodeProcessor;
191
192    /// If [`AudioNodeInfo::call_update_method`] was set to `true`, then the Firewheel
193    /// context will call this method on every update cycle.
194    ///
195    /// * `configuration` - The custom configuration of this node.
196    /// * `cx` - A context for interacting with the Firewheel context.
197    fn update(&mut self, configuration: &Self::Configuration, cx: UpdateContext) {
198        let _ = configuration;
199        let _ = cx;
200    }
201}
202
203/// A context for [`AudioNode::construct_processor`].
204pub struct ConstructProcessorContext<'a> {
205    /// The ID of this audio node.
206    pub node_id: NodeID,
207    /// Information about the running audio stream.
208    pub stream_info: &'a StreamInfo,
209    custom_state: &'a mut Option<Box<dyn Any>>,
210}
211
212impl<'a> ConstructProcessorContext<'a> {
213    pub fn new(
214        node_id: NodeID,
215        stream_info: &'a StreamInfo,
216        custom_state: &'a mut Option<Box<dyn Any>>,
217    ) -> Self {
218        Self {
219            node_id,
220            stream_info,
221            custom_state,
222        }
223    }
224
225    /// Get an immutable reference to the custom state that was created in
226    /// [`AudioNodeInfo::custom_state`].
227    pub fn custom_state<T: 'static>(&self) -> Option<&T> {
228        self.custom_state
229            .as_ref()
230            .and_then(|s| s.downcast_ref::<T>())
231    }
232
233    /// Get a mutable reference to the custom state that was created in
234    /// [`AudioNodeInfo::custom_state`].
235    pub fn custom_state_mut<T: 'static>(&mut self) -> Option<&mut T> {
236        self.custom_state
237            .as_mut()
238            .and_then(|s| s.downcast_mut::<T>())
239    }
240}
241
242/// A context for [`AudioNode::update`].
243pub struct UpdateContext<'a> {
244    /// The ID of this audio node.
245    pub node_id: NodeID,
246    /// Information about the running audio stream. If no audio stream is running,
247    /// then this will be `None`.
248    pub stream_info: Option<&'a StreamInfo>,
249    custom_state: &'a mut Option<Box<dyn Any>>,
250    event_queue: &'a mut Vec<NodeEvent>,
251}
252
253impl<'a> UpdateContext<'a> {
254    pub fn new(
255        node_id: NodeID,
256        stream_info: Option<&'a StreamInfo>,
257        custom_state: &'a mut Option<Box<dyn Any>>,
258        event_queue: &'a mut Vec<NodeEvent>,
259    ) -> Self {
260        Self {
261            node_id,
262            stream_info,
263            custom_state,
264            event_queue,
265        }
266    }
267
268    /// Queue an event to send to this node's processor counterpart.
269    pub fn queue_event(&mut self, event: NodeEventType) {
270        self.event_queue.push(NodeEvent {
271            node_id: self.node_id,
272            event,
273        });
274    }
275
276    /// Get an immutable reference to the custom state that was created in
277    /// [`AudioNodeInfo::custom_state`].
278    pub fn custom_state<T: 'static>(&self) -> Option<&T> {
279        self.custom_state
280            .as_ref()
281            .and_then(|s| s.downcast_ref::<T>())
282    }
283
284    /// Get a mutable reference to the custom state that was created in
285    /// [`AudioNodeInfo::custom_state`].
286    pub fn custom_state_mut<T: 'static>(&mut self) -> Option<&mut T> {
287        self.custom_state
288            .as_mut()
289            .and_then(|s| s.downcast_mut::<T>())
290    }
291}
292
293/// An empty constructor configuration.
294///
295/// This should be preferred over `()` because it implements
296/// [`Component`][bevy_ecs::prelude::Component], making the
297/// [`AudioNode`] implementor trivially Bevy-compatible.
298#[derive(Debug, Default, Clone, Copy)]
299#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
300pub struct EmptyConfig;
301
302/// A type-erased dyn-compatible [`AudioNode`].
303pub trait DynAudioNode {
304    /// Get information about this node.
305    ///
306    /// This method is only called once after the node is added to the audio graph.
307    fn info(&self) -> AudioNodeInfo;
308
309    /// Construct a realtime processor for this node.
310    ///
311    /// * `cx` - A context for interacting with the Firewheel context. This context
312    /// also includes information about the audio stream.
313    fn construct_processor(&self, cx: ConstructProcessorContext) -> Box<dyn AudioNodeProcessor>;
314
315    /// If [`AudioNodeInfo::call_update_method`] was set to `true`, then the Firewheel
316    /// context will call this method on every update cycle.
317    ///
318    /// * `cx` - A context for interacting with the Firewheel context.
319    fn update(&mut self, cx: UpdateContext) {
320        let _ = cx;
321    }
322}
323
324/// Pairs constructors with their configurations.
325///
326/// This is useful for type-erasing an [`AudioNode`].
327pub struct Constructor<T, C> {
328    constructor: T,
329    configuration: C,
330}
331
332impl<T: AudioNode> Constructor<T, T::Configuration> {
333    pub fn new(constructor: T, configuration: Option<T::Configuration>) -> Self {
334        Self {
335            constructor,
336            configuration: configuration.unwrap_or_default(),
337        }
338    }
339}
340
341impl<T: AudioNode> DynAudioNode for Constructor<T, T::Configuration> {
342    fn info(&self) -> AudioNodeInfo {
343        self.constructor.info(&self.configuration)
344    }
345
346    fn construct_processor(&self, cx: ConstructProcessorContext) -> Box<dyn AudioNodeProcessor> {
347        Box::new(
348            self.constructor
349                .construct_processor(&self.configuration, cx),
350        )
351    }
352
353    fn update(&mut self, cx: UpdateContext) {
354        self.constructor.update(&self.configuration, cx);
355    }
356}
357
358/// The trait describing the realtime processor counterpart to an
359/// audio node.
360pub trait AudioNodeProcessor: 'static + Send {
361    /// Process the given block of audio. Only process data in the
362    /// buffers up to `samples`.
363    ///
364    /// The node *MUST* either return `ProcessStatus::ClearAllOutputs`
365    /// or fill all output buffers with data.
366    ///
367    /// If any output buffers contain all zeros up to `samples` (silent),
368    /// then mark that buffer as silent in [`ProcInfo::out_silence_mask`].
369    ///
370    /// * `buffers` - The buffers of data to process.
371    /// * `proc_info` - Additional information about the process.
372    /// * `events` - A list of events for this node to process.
373    fn process(
374        &mut self,
375        buffers: ProcBuffers,
376        proc_info: &ProcInfo,
377        events: NodeEventList,
378    ) -> ProcessStatus;
379
380    /// Called when the audio stream has been stopped.
381    fn stream_stopped(&mut self) {}
382
383    /// Called when a new audio stream has been started after a previous
384    /// call to [`AudioNodeProcessor::stream_stopped`].
385    ///
386    /// Note, this method gets called on the main thread, not the audio
387    /// thread. So it is safe to allocate/deallocate here.
388    fn new_stream(&mut self, stream_info: &StreamInfo) {
389        let _ = stream_info;
390    }
391}
392
393pub const NUM_SCRATCH_BUFFERS: usize = 8;
394
395/// The buffers used in [`AudioNodeProcessor::process`].
396pub struct ProcBuffers<'a, 'b, 'c, 'd> {
397    /// The audio input buffers.
398    ///
399    /// The number of channels will always equal the [`ChannelConfig::num_inputs`]
400    /// value that was returned in [`AudioNode::info`].
401    ///
402    /// Each channel slice will have a length of [`ProcInfo::frames`].
403    pub inputs: &'a [&'b [f32]],
404
405    /// The audio input buffers.
406    ///
407    /// The number of channels will always equal the [`ChannelConfig::num_outputs`]
408    /// value that was returned in [`AudioNode::info`].
409    ///
410    /// Each channel slice will have a length of [`ProcInfo::frames`].
411    ///
412    /// These buffers may contain junk data.
413    pub outputs: &'a mut [&'b mut [f32]],
414
415    /// A list of extra scratch buffers that can be used for processing.
416    /// This removes the need for nodes to allocate their own scratch buffers.
417    /// Each buffer has a length of [`StreamInfo::max_block_frames`]. These
418    /// buffers are shared across all nodes, so assume that they contain junk
419    /// data.
420    pub scratch_buffers: &'c mut [&'d mut [f32]; NUM_SCRATCH_BUFFERS],
421}
422
423/// Additional information for processing audio
424pub struct ProcInfo<'a> {
425    /// The number of samples (in a single channel of audio) in this
426    /// processing block.
427    ///
428    /// Not to be confused with video frames.
429    pub frames: usize,
430
431    /// An optional optimization hint on which input channels contain
432    /// all zeros (silence). The first bit (`0b1`) is the first channel,
433    /// the second bit is the second channel, and so on.
434    pub in_silence_mask: SilenceMask,
435
436    /// An optional optimization hint on which output channels contain
437    /// all zeros (silence). The first bit (`0b1`) is the first channel,
438    /// the second bit is the second channel, and so on.
439    pub out_silence_mask: SilenceMask,
440
441    /// The current interval of time of the internal clock in units of
442    /// seconds. The start of the range is the instant of time at the
443    /// first sample in the block (inclusive), and the end of the range
444    /// is the instant of time at the end of the block (exclusive).
445    ///
446    /// This uses the clock from the OS's audio API so it should be quite
447    /// accurate, and it correctly accounts for any output underflows that
448    /// may occur.
449    pub clock_seconds: Range<ClockSeconds>,
450
451    /// The total number of samples (in a single channel of audio) that
452    /// have been processed since the start of the audio stream.
453    ///
454    /// This value can be used for more accurate timing than
455    /// [`ProcInfo::clock_seconds`], but note it does *NOT* account for any
456    /// output underflows that may occur.
457    pub clock_samples: ClockSamples,
458
459    /// Information about the musical transport.
460    ///
461    /// This will be `None` if no musical transport is currently active,
462    /// or if the current transport is currently paused.
463    pub transport_info: Option<TransportInfo<'a>>,
464
465    /// Flags indicating the current status of the audio stream
466    pub stream_status: StreamStatus,
467
468    /// A buffer of values that linearly ramp up/down between `0.0` and `1.0`
469    /// which can be used to implement efficient declicking when
470    /// pausing/resuming/stopping.
471    pub declick_values: &'a DeclickValues,
472}
473
474#[derive(Debug, Clone, PartialEq)]
475pub struct TransportInfo<'a> {
476    /// The current transport.
477    pub transport: &'a MusicalTransport,
478
479    /// The current interval of time of the internal clock in units of
480    /// musical time. The start of the range is the instant of time at the
481    /// first sample in the block (inclusive), and the end of the range
482    /// is the instant of time at the end of the block (exclusive).
483    ///
484    /// This will be `None` if no musical clock is currently present.
485    pub musical_clock: Range<MusicalTime>,
486
487    /// Whether or not the transport is currently paused.
488    pub paused: bool,
489}
490
491bitflags::bitflags! {
492    /// Flags indicating the current status of the audio stream
493    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
494    pub struct StreamStatus: u32 {
495        /// Some input data was discarded because of an overflow condition
496        /// at the audio driver.
497        const INPUT_OVERFLOW = 0b01;
498
499        /// The output buffer ran low, likely producing a break in the
500        /// output sound.
501        const OUTPUT_UNDERFLOW = 0b10;
502    }
503}
504
505/// The status of processing buffers in an audio node.
506#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
507pub enum ProcessStatus {
508    /// No output buffers were modified. If this is returned, then
509    /// the engine will automatically clear all output buffers
510    /// for you as efficiently as possible.
511    #[default]
512    ClearAllOutputs,
513    /// No output buffers were modified. If this is returned, then
514    /// the engine will automatically copy the input buffers to
515    /// their corresponding output buffers for you as efficiently
516    /// as possible.
517    Bypass,
518    /// All output buffers were filled with data.
519    OutputsModified { out_silence_mask: SilenceMask },
520}
521
522impl ProcessStatus {
523    /// All output buffers were filled with non-silence.
524    pub const fn outputs_not_silent() -> Self {
525        Self::OutputsModified {
526            out_silence_mask: SilenceMask::NONE_SILENT,
527        }
528    }
529
530    /// All output buffers were filled with data.
531    pub const fn outputs_modified(out_silence_mask: SilenceMask) -> Self {
532        Self::OutputsModified { out_silence_mask }
533    }
534}