Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 139 additions & 19 deletions crates/firewheel-core/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ impl Default for NodeID {
///
/// This struct enforces the use of the builder pattern for future-proofness, as
/// it is likely that more fields will be added in the future.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug)]
pub struct AudioNodeInfo {
debug_name: &'static str,
channel_config: ChannelConfig,
uses_events: bool,
call_update_method: bool,
custom_state: Option<Box<dyn Any>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super nitpicky, so take it for what it's worth, but could we possibly separate out the state from the info struct?

The reason I ask is that technically, a user could provide the state in both AudioNode::info and AudioNode::processor, although it seems like it's supposed to be constructed in the latter (correct me if I'm wrong).

@BillyDM BillyDM Mar 14, 2025

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same Option<Box<dyn Any>> object that is created in the AudioNodeInfo struct is what is passed to AudioNode::processor and AudioNode::update. The node is free to mutate these at any point, but generally nodes should construct it in AudioNodeInfo so that the state is immediately available to the user in FirewheelCtx::node_state.

}

impl AudioNodeInfo {
Expand All @@ -47,6 +48,7 @@ impl AudioNodeInfo {
},
uses_events: false,
call_update_method: false,
custom_state: None,
}
}

Expand Down Expand Up @@ -84,15 +86,26 @@ impl AudioNodeInfo {
self.call_update_method = call_update_method;
self
}

/// Custom `!Send` state that can be stored in the Firewheel context and accessed
/// by the user.
///
/// The user accesses this state via `FirewheelCtx::node_state` and
/// `FirewheelCtx::node_state_mut`.
pub fn custom_state<T: 'static>(mut self, custom_state: T) -> Self {
self.custom_state = Some(Box::new(custom_state));
self
}
}

/// Information about an [`AudioNode`]. Used internally by the Firewheel context.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug)]
pub struct AudioNodeInfoInner {
pub debug_name: &'static str,
pub channel_config: ChannelConfig,
pub uses_events: bool,
pub call_update_method: bool,
pub custom_state: Option<Box<dyn Any>>,
}

impl Into<AudioNodeInfoInner> for AudioNodeInfo {
Expand All @@ -102,15 +115,61 @@ impl Into<AudioNodeInfoInner> for AudioNodeInfo {
channel_config: self.channel_config,
uses_events: self.uses_events,
call_update_method: self.call_update_method,
custom_state: self.custom_state,
}
}
}

/// A trait representing a node in a Firewheel audio graph.
///
/// # Notes about ECS
///
/// In order to be friendlier to ECS's (entity component systems), it is encouraged
/// that any struct deriving this trait be POD (plain ol' data). If you want your
/// audio node to be usable in the Bevy game engine, also derive
/// `bevy_ecs::prelude::Component`. (You can hide this derive behind a feature flag
/// by using `#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]`).
///
/// # Audio Node Lifecycle
///
/// 1. The user constructs the node as POD or from a custom constructor method for
/// that node.
/// 2. The user adds the node to the graph using `FirewheelCtx::add_node`. If the
/// node has any custom configuration, then the user passes that configuration to this
/// method as well. In this method, the Firewheel context calls [`AudioNode::info`] to
/// get information about the node. The node can also store any custom state in the
/// [`AudioNodeInfo`] struct.
/// 3. At this point the user may now call `FirewheelCtx::node_state` and
/// `FirewheelCtx::node_state_mut` to retrieve the node's custom state.
/// 4. If [`AudioNodeInfo::call_update_method`] was set to `true`, then
/// [`AudioNode::update`] will be called every time the Firewheel context updates.
/// The node's custom state is also accessible in this method.
/// 5. When the Firewheel context is ready for the node to start processing data,
/// it calls [`AudioNode::construct_processor`] to retrieve the realtime
/// [`AudioNodeProcessor`] counterpart of the node. This processor counterpart is
/// then sent to the audio thread.
/// 6. The Firewheel processor calls [`AudioNodeProcessor::process`] whenever there
/// is a new block of audio data to process.
/// 7. (Graceful shutdown)
///
/// 7a. The Firewheel processor calls [`AudioNodeProcessor::stream_stopped`].
/// The processor is then sent back to the main thread.
///
/// 7b. If a new audio stream is started, then the context will call
/// [`AudioNodeProcessor::new_stream`] on the main thread, and then send the
/// processor back to the audio thread for processing.
///
/// 7c. If the Firewheel context is dropped before a new stream is started, then
/// both the node and the processor counterpart are dropped.
/// 8. (Audio thread crashes or stops unexpectedly) - The node's processor counterpart
/// may or may not be dropped. The user may try to create a new audio stream, in which
/// case [`AudioNode::construct_processor`] might be called again. If a second processor
/// instance is not able to be created, then the node may panic.
pub trait AudioNode {
/// A type representing this constructor's configuration.
///
/// This is intended as a one-time configuration to be used
/// when constructing an audio processor. When no configuration
/// when constructing an audio node. When no configuration
/// is required, [`EmptyConfig`] should be used.
type Configuration: Default;

Expand All @@ -119,17 +178,20 @@ pub trait AudioNode {
/// This method is only called once after the node is added to the audio graph.
fn info(&self, configuration: &Self::Configuration) -> AudioNodeInfo;

/// Construct a processor for this node.
fn processor(
/// Construct a realtime processor for this node.
///
/// * `configuration` - The custom configuration of this node.
/// * `cx` - A context for interacting with the Firewheel context. This context
/// also includes information about the audio stream.
fn construct_processor(
&self,
configuration: &Self::Configuration,
stream_info: &StreamInfo,
cx: ConstructProcessorContext,
) -> impl AudioNodeProcessor;

/// If [`AudioNodeInfo::call_update_method`] was set to `true`, then the Firewheel
/// context will call this method on every update cycle.
///
/// * `id` - The ID of this node.
/// * `configuration` - The custom configuration of this node.
/// * `cx` - A context for interacting with the Firewheel context.
fn update(&mut self, configuration: &Self::Configuration, cx: UpdateContext) {
Expand All @@ -138,30 +200,67 @@ pub trait AudioNode {
}
}

/// A context for [`AudioNode::construct_processor`].
pub struct ConstructProcessorContext<'a> {
/// The ID of this audio node.
pub node_id: NodeID,
/// Information about the running audio stream.
pub stream_info: &'a StreamInfo,
custom_state: &'a mut Option<Box<dyn Any>>,
}

impl<'a> ConstructProcessorContext<'a> {
pub fn new(
node_id: NodeID,
stream_info: &'a StreamInfo,
custom_state: &'a mut Option<Box<dyn Any>>,
) -> Self {
Self {
node_id,
stream_info,
custom_state,
}
}

/// Get an immutable reference to the custom state that was created in
/// [`AudioNodeInfo::custom_state`].
pub fn custom_state<T: 'static>(&self) -> Option<&T> {
self.custom_state
.as_ref()
.and_then(|s| s.downcast_ref::<T>())
}

/// Get a mutable reference to the custom state that was created in
/// [`AudioNodeInfo::custom_state`].
pub fn custom_state_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.custom_state
.as_mut()
.and_then(|s| s.downcast_mut::<T>())
}
}

/// A context for [`AudioNode::update`].
pub struct UpdateContext<'a> {
/// The ID of this audio node.
pub node_id: NodeID,
/// Information about the running audio stream. If no audio stream is running,
/// then this will be `None`.
pub stream_info: Option<&'a StreamInfo>,
/// Custom `!Send` data that can be stored in the Firewheel
/// context.
pub custom_data: &'a mut Option<Box<dyn Any>>,
custom_state: &'a mut Option<Box<dyn Any>>,
event_queue: &'a mut Vec<NodeEvent>,
}

impl<'a> UpdateContext<'a> {
pub fn new(
node_id: NodeID,
stream_info: Option<&'a StreamInfo>,
custom_data: &'a mut Option<Box<dyn Any>>,
custom_state: &'a mut Option<Box<dyn Any>>,
event_queue: &'a mut Vec<NodeEvent>,
) -> Self {
Self {
node_id,
stream_info,
custom_data,
custom_state,
event_queue,
}
}
Expand All @@ -173,6 +272,22 @@ impl<'a> UpdateContext<'a> {
event,
});
}

/// Get an immutable reference to the custom state that was created in
/// [`AudioNodeInfo::custom_state`].
pub fn custom_state<T: 'static>(&self) -> Option<&T> {
self.custom_state
.as_ref()
.and_then(|s| s.downcast_ref::<T>())
}

/// Get a mutable reference to the custom state that was created in
/// [`AudioNodeInfo::custom_state`].
pub fn custom_state_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.custom_state
.as_mut()
.and_then(|s| s.downcast_mut::<T>())
}
}

/// An empty constructor configuration.
Expand All @@ -184,21 +299,23 @@ impl<'a> UpdateContext<'a> {
#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
pub struct EmptyConfig;

/// A dyn-compatible [`AudioNode`].
/// A type-erased dyn-compatible [`AudioNode`].
pub trait DynAudioNode {
/// Get information about this node.
///
/// This method is only called once after the node is added to the audio graph.
fn info(&self) -> AudioNodeInfo;

/// Construct a processor for this node.
fn processor(&self, stream_info: &StreamInfo) -> Box<dyn AudioNodeProcessor>;
/// Construct a realtime processor for this node.
///
/// * `cx` - A context for interacting with the Firewheel context. This context
/// also includes information about the audio stream.
fn construct_processor(&self, cx: ConstructProcessorContext) -> Box<dyn AudioNodeProcessor>;

/// If [`AudioNodeInfo::call_update_method`] was set to `true`, then the Firewheel
/// context will call this method on every update cycle.
///
/// * `id` - The ID of this node.
/// * `configuration` - The custom configuration of this node.
/// * `cx` - A context for interacting with the Firewheel context.
fn update(&mut self, cx: UpdateContext) {
let _ = cx;
}
Expand Down Expand Up @@ -226,8 +343,11 @@ impl<T: AudioNode> DynAudioNode for Constructor<T, T::Configuration> {
self.constructor.info(&self.configuration)
}

fn processor(&self, stream_info: &StreamInfo) -> Box<dyn AudioNodeProcessor> {
Box::new(self.constructor.processor(&self.configuration, stream_info))
fn construct_processor(&self, cx: ConstructProcessorContext) -> Box<dyn AudioNodeProcessor> {
Box::new(
self.constructor
.construct_processor(&self.configuration, cx),
)
}

fn update(&mut self, cx: UpdateContext) {
Expand Down
11 changes: 7 additions & 4 deletions crates/firewheel-core/src/node/dummy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::{channel_config::ChannelConfig, event::NodeEventList, StreamInfo};
use crate::{channel_config::ChannelConfig, event::NodeEventList};

use super::{AudioNode, AudioNodeInfo, AudioNodeProcessor, ProcBuffers, ProcInfo, ProcessStatus};
use super::{
AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, ProcBuffers, ProcInfo,
ProcessStatus,
};

/// A "dummy" [`AudioNode`], a node which does nothing.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
Expand All @@ -22,10 +25,10 @@ impl AudioNode for DummyNode {
.uses_events(false)
}

fn processor(
fn construct_processor(
&self,
_config: &Self::Configuration,
_stream_info: &StreamInfo,
_cx: ConstructProcessorContext,
) -> impl AudioNodeProcessor {
DummyProcessor
}
Expand Down
10 changes: 10 additions & 0 deletions crates/firewheel-graph/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,16 @@ impl<B: AudioBackend> FirewheelCtx<B> {
self.graph.node_info(id)
}

/// Get an immutable reference to the custom state of a node.
pub fn node_state<T: 'static>(&self, id: NodeID) -> Option<&T> {
self.graph.node_state(id)
}

/// Get a mutable reference to the custom state of a node.
pub fn node_state_mut<T: 'static>(&mut self, id: NodeID) -> Option<&mut T> {
self.graph.node_state_mut(id)
}

/// Get a list of all the existing nodes in the graph.
pub fn nodes<'a>(&'a self) -> impl Iterator<Item = &'a NodeEntry> {
self.graph.nodes()
Expand Down
Loading