use crate::error::SeedlingError;
use crate::pool::sample_effects::EffectOf;
use crate::time::{Audio, AudioTime};
use crate::{
SeedlingSystems,
edge::{ChannelMapping, NodeMap},
prelude::AudioContext,
};
use bevy_app::prelude::*;
use bevy_ecs::component::Components;
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::{
component::{ComponentId, Mutable},
prelude::*,
world::DeferredWorld,
};
use bevy_log::prelude::*;
use bevy_platform::collections::HashSet;
use bevy_time::Time;
use firewheel::channel_config::ChannelConfig;
use firewheel::clock::{DurationSeconds, EventInstant, InstantSeconds};
use firewheel::error::UpdateError;
use firewheel::graph::NodeEntry;
use firewheel::{
diff::{Diff, Patch},
event::{NodeEvent, NodeEventType},
node::{AudioNode, NodeID},
};
use std::any::TypeId;
use std::ops::DerefMut;
pub mod events;
pub mod follower;
pub mod label;
use events::AudioEvents;
use label::NodeLabels;
#[derive(Component)]
pub(crate) struct Baseline<T>(pub(crate) T);
#[derive(Debug, Component, Clone)]
pub struct DiffTimestamp(pub(crate) InstantSeconds);
impl DiffTimestamp {
pub fn new(time: &bevy_time::Time<Audio>) -> Self {
Self(time.context().instant())
}
}
#[derive(Resource, Debug)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct ScheduleDiffing(pub bool);
impl Default for ScheduleDiffing {
fn default() -> Self {
Self(true)
}
}
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
#[component(immutable)]
pub struct FirewheelNodeInfo {
pub channel_config: ChannelConfig,
pub latency_frames: u32,
}
impl FirewheelNodeInfo {
pub(crate) fn new(entry: &NodeEntry) -> Self {
Self {
channel_config: entry.info.channel_config,
latency_frames: entry.info.latency_frames,
}
}
}
#[derive(Resource, Debug)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct AudioScheduleLookahead(pub DurationSeconds);
impl Default for AudioScheduleLookahead {
fn default() -> Self {
Self(DurationSeconds(0.1))
}
}
#[derive(Component, Clone, Copy)]
pub(crate) struct EffectId(pub(crate) ComponentId);
fn apply_patch<T: Patch>(value: &mut T, event: &NodeEventType) -> Result {
let NodeEventType::Param { data, path } = event else {
return Ok(());
};
let patch = T::patch(data, path).map_err(|e| SeedlingError::PatchError {
ty: core::any::type_name::<T>(),
error: e,
})?;
value.apply(patch);
Ok(())
}
fn generate_param_events<T: Diff + Patch + Component<Mutability = Mutable> + Clone>(
mut nodes: Query<(Mut<T>, &mut Baseline<T>, &mut AudioEvents, Has<EffectOf>)>,
time: Res<bevy_time::Time<Audio>>,
) -> Result {
let render_range = time.render_range();
for (mut params, mut baseline, mut events, effect) in nodes.iter_mut() {
if params.is_changed() && !effect {
let starting_len = events.queue.len();
params.diff(&baseline.0, Default::default(), events.deref_mut());
for event in &events.queue[starting_len..] {
apply_patch(&mut baseline.0, event)?;
}
}
events.clear_elapsed_events(render_range.start);
if events.active_within(render_range.start, render_range.end) {
events.value_at(render_range.start, render_range.end, params.as_mut())?;
events.value_at(render_range.start, render_range.end, &mut baseline.0)?;
}
}
Ok(())
}
fn handle_configuration_changes<
T: AudioNode<Configuration: Component + PartialEq + Clone> + Component + Clone,
>(
mut configs: Query<
(
Entity,
&T,
&FirewheelNode,
&T::Configuration,
&mut Baseline<T::Configuration>,
),
Changed<T::Configuration>,
>,
mut context: ResMut<AudioContext>,
mut commands: Commands,
) -> Result {
let changes: Vec<_> = configs.iter_mut().filter(|(.., c, b)| *c != &b.0).collect();
if changes.is_empty() {
return Ok(());
}
context.with(|context| {
for (entity, node, node_id, config, mut baseline) in changes {
let edges = context.edges();
let existing_inputs = edges
.iter()
.filter(|e| e.dst_node == node_id.0)
.map(|e| firewheel::graph::Edge::clone(e))
.collect::<Vec<_>>();
let existing_outputs = edges
.iter()
.filter(|e| e.src_node == node_id.0)
.map(|e| firewheel::graph::Edge::clone(e))
.collect::<Vec<_>>();
let new_node = context.add_node(node.clone(), Some(config.clone()));
let info = FirewheelNodeInfo::new(context.node_info(new_node).unwrap());
commands
.entity(entity)
.insert((FirewheelNode(new_node), info));
for edge in existing_inputs
.into_iter()
.take(info.channel_config.num_inputs.get() as usize)
{
context.connect(
edge.src_node,
new_node,
&[(edge.src_port, edge.dst_port)],
true,
)?;
}
for edge in existing_outputs
.into_iter()
.take(info.channel_config.num_outputs.get() as usize)
{
context.connect(
new_node,
edge.dst_node,
&[(edge.src_port, edge.dst_port)],
true,
)?;
}
baseline.0 = config.clone();
}
Ok(())
})
}
fn acquire_id<T>(
q: Query<
(Entity, &T, Option<&T::Configuration>, Option<&NodeLabels>),
(Without<FirewheelNode>, Without<EffectOf>),
>,
mut context: ResMut<AudioContext>,
mut node_map: ResMut<NodeMap>,
mut commands: Commands,
) where
T: AudioNode<Configuration: Component + Clone> + Component + Clone,
{
if q.iter().len() == 0 {
return;
}
context.with(|context| {
for (entity, container, config, labels) in q.iter() {
let node = context.add_node(container.clone(), config.cloned());
let info = FirewheelNodeInfo::new(context.node_info(node).unwrap());
for label in labels.iter().flat_map(|l| l.iter()) {
node_map.insert(*label, entity);
}
commands.entity(entity).insert((FirewheelNode(node), info));
}
});
}
fn insert_baseline<T: Component + Clone>(
trigger: On<Insert, T>,
q: Query<&T>,
mut commands: Commands,
) -> Result {
let value = q.get(trigger.event_target())?;
commands
.entity(trigger.event_target())
.insert(Baseline(value.clone()));
Ok(())
}
#[derive(Debug, Component)]
pub struct AudioState<T>(pub T);
fn fetch_state<T, S>(
q: Query<(Entity, &FirewheelNode), (Changed<FirewheelNode>, With<T>)>,
mut context: ResMut<AudioContext>,
mut commands: Commands,
) where
T: AudioNode + Component,
S: Clone + Send + Sync + 'static,
{
if q.iter().count() == 0 {
return;
}
context.with(|context| {
for (entity, node) in q.iter() {
match context.node_state::<S>(node.0) {
Some(state) => {
commands.entity(entity).insert(AudioState(state.clone()));
}
None => {
bevy_log::error!(
"Failed to fetch state `{}` for node `{}`",
core::any::type_name::<S>(),
core::any::type_name::<T>(),
);
}
}
}
});
}
#[derive(Resource, Default)]
struct RegisteredNodes(HashSet<TypeId>);
impl RegisteredNodes {
fn insert<T: core::any::Any>(&mut self) -> bool {
self.0.insert(TypeId::of::<T>())
}
}
#[derive(Resource, Default)]
struct RegisteredConfigs(HashSet<TypeId>);
impl RegisteredConfigs {
fn insert<T: core::any::Any>(&mut self) -> bool {
self.0.insert(TypeId::of::<T>())
}
}
#[derive(Resource, Default)]
struct RegisteredState(HashSet<(TypeId, TypeId)>);
impl RegisteredState {
fn insert<T: core::any::Any, U: core::any::Any>(&mut self) -> bool {
self.0.insert((TypeId::of::<T>(), TypeId::of::<U>()))
}
}
pub trait RegisterNode {
fn register_node<T>(&mut self) -> &mut Self
where
T: AudioNode<Configuration: Component + Clone + PartialEq>
+ Diff
+ Patch
+ Component<Mutability = Mutable>
+ Clone;
fn register_simple_node<T>(&mut self) -> &mut Self
where
T: AudioNode<Configuration: Component + Clone + PartialEq> + Component + Clone;
fn register_node_state<T, S>(&mut self) -> &mut Self
where
T: AudioNode + Component,
S: Clone + Send + Sync + 'static;
}
impl RegisterNode for App {
#[cfg_attr(debug_assertions, track_caller)]
fn register_node<T>(&mut self) -> &mut Self
where
T: AudioNode<Configuration: Component + Clone + PartialEq>
+ Diff
+ Patch
+ Component<Mutability = Mutable>
+ Clone,
{
let world = self.world_mut();
let mut nodes = world.get_resource_or_init::<RegisteredNodes>();
if nodes.insert::<T>() {
world.add_observer(observe_node_insertion::<T>);
world.register_required_components::<T, T::Configuration>();
} else {
#[cfg(debug_assertions)]
{
bevy_log::warn!(
"Audio node `{}` was registered more than once at {}",
core::any::type_name::<T>(),
std::panic::Location::caller(),
);
}
#[cfg(not(debug_assertions))]
bevy_log::warn!(
"Audio node `{}` was registered more than once",
core::any::type_name::<T>(),
);
return self;
}
let mut configs = world.get_resource_or_init::<RegisteredConfigs>();
if configs.insert::<T::Configuration>() {
world.add_observer(insert_baseline::<T::Configuration>);
}
self.add_systems(
Last,
(
(acquire_id::<T>, handle_configuration_changes::<T>)
.chain()
.in_set(SeedlingSystems::Acquire),
(follower::param_follower::<T>, generate_param_events::<T>)
.chain()
.in_set(SeedlingSystems::Queue),
),
)
}
#[cfg_attr(debug_assertions, track_caller)]
fn register_simple_node<T>(&mut self) -> &mut Self
where
T: AudioNode<Configuration: Component + Clone + PartialEq> + Component + Clone,
{
let world = self.world_mut();
let mut nodes = world.get_resource_or_init::<RegisteredNodes>();
if nodes.insert::<T>() {
world.add_observer(observe_simple_node_insertion::<T>);
world.register_required_components::<T, T::Configuration>();
} else {
#[cfg(debug_assertions)]
{
bevy_log::warn!(
"Audio node `{}` was registered more than once at {}",
core::any::type_name::<T>(),
std::panic::Location::caller(),
);
}
#[cfg(not(debug_assertions))]
bevy_log::warn!(
"Audio node `{}` was registered more than once",
core::any::type_name::<T>(),
);
return self;
}
let mut configs = world.get_resource_or_init::<RegisteredConfigs>();
if configs.insert::<T::Configuration>() {
world.add_observer(insert_baseline::<T::Configuration>);
}
self.add_systems(
Last,
(acquire_id::<T>, handle_configuration_changes::<T>)
.chain()
.in_set(SeedlingSystems::Acquire),
)
}
#[cfg_attr(debug_assertions, track_caller)]
fn register_node_state<T, S>(&mut self) -> &mut Self
where
T: AudioNode + Component,
S: Clone + Send + Sync + 'static,
{
let world = self.world_mut();
let mut nodes = world.get_resource_or_init::<RegisteredState>();
if !nodes.insert::<T, S>() {
#[cfg(debug_assertions)]
{
bevy_log::warn!(
"State `{}` was registered for node `{}` at {}",
core::any::type_name::<S>(),
core::any::type_name::<T>(),
std::panic::Location::caller(),
);
}
#[cfg(not(debug_assertions))]
bevy_log::warn!(
"State `{}` registered more than once for node `{}`",
core::any::type_name::<S>(),
core::any::type_name::<T>(),
);
return self;
}
self.add_systems(
Last,
fetch_state::<T, S>
.after(SeedlingSystems::Acquire)
.before(SeedlingSystems::Connect),
)
}
}
fn observe_node_insertion<T: Component + Clone>(
trigger: On<Insert, T>,
node: Query<&T>,
components: &Components,
time: Res<Time<Audio>>,
mut commands: Commands,
) -> Result {
let value = node.get(trigger.event_target())?.clone();
commands
.entity(trigger.event_target())
.insert(EffectId(
components
.component_id::<T>()
.expect("`ComponentId` must be available"),
))
.insert_if_new((
Baseline(value),
AudioEvents::new(&time),
));
Ok(())
}
fn observe_simple_node_insertion<T: Component>(
trigger: On<Insert, T>,
components: &Components,
time: Res<Time<Audio>>,
mut commands: Commands,
) -> Result {
commands
.entity(trigger.event_target())
.insert(EffectId(
components
.component_id::<T>()
.expect("`ComponentId` must be available"),
))
.insert_if_new(AudioEvents::new(&time));
Ok(())
}
#[derive(Debug, Clone, Copy, Component)]
#[component(on_replace = Self::on_replace_hook, immutable)]
#[require(ChannelMapping)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct FirewheelNode(pub NodeID);
impl FirewheelNode {
fn on_replace_hook(mut world: DeferredWorld, context: HookContext) {
let Some(node) = world.get::<FirewheelNode>(context.entity).copied() else {
return;
};
let mut removals = world.resource_mut::<PendingRemovals>();
removals.push(node.0);
}
}
#[derive(Debug, Default, Resource)]
pub(crate) struct PendingRemovals(Vec<NodeID>);
impl PendingRemovals {
pub fn push(&mut self, node: NodeID) {
self.0.push(node);
}
}
pub(crate) fn flush_events(
mut nodes: Query<(
Entity,
&FirewheelNode,
&mut AudioEvents,
Option<&DiffTimestamp>,
)>,
mut removals: ResMut<PendingRemovals>,
mut context: ResMut<AudioContext>,
time: Res<bevy_time::Time<Audio>>,
should_schedule: Res<ScheduleDiffing>,
lookahead: Res<AudioScheduleLookahead>,
mut commands: Commands,
) {
context.with(|context| {
for node in removals.0.drain(..) {
if context.remove_node(node).is_err() {
error!("attempted to remove non-existent or invalid node from audio graph");
}
}
let now = time.now();
let range_to_render = InstantSeconds(0.0)..now + lookahead.0;
for (node_entity, node, mut events, timestamp) in nodes.iter_mut() {
for event in events.queue.drain(..) {
let time = should_schedule.0.then(|| match timestamp {
Some(t) => {
commands.entity(node_entity).remove::<DiffTimestamp>();
EventInstant::Seconds(t.0)
}
None => EventInstant::Seconds(now),
});
context.queue_event(NodeEvent {
node_id: node.0,
event,
time,
});
}
for event in &mut events.timeline {
if let Err(e) =
event.render(range_to_render.start, range_to_render.end, |event, time| {
context.queue_event(NodeEvent {
node_id: node.0,
event,
time: Some(EventInstant::Seconds(time)),
})
})
{
bevy_log::error!("failed to apply animation patch: {e:?}");
}
}
}
let result = context.update();
match result {
Err(UpdateError::StreamStoppedUnexpectedly(e)) => {
warn!("Audio stream stopped: {e:?}");
commands.trigger(crate::configuration::FetchAudioIoEvent);
commands.trigger(crate::configuration::RestartAudioEvent);
}
Err(e) => {
error!("graph error: {e:?}");
}
_ => {}
}
});
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
prelude::*,
test::{prepare_app, run},
};
#[derive(Component)]
struct TestMarker;
#[test]
fn test_config_reinsertion() {
let mut app = prepare_app(|mut commands: Commands| {
commands
.spawn(VolumeNode::default())
.chain_node((VolumeNode::default(), TestMarker))
.chain_node(VolumeNode::default());
});
let initial_id = run(
&mut app,
|q: Query<&FirewheelNode, With<TestMarker>>, mut context: ResMut<AudioContext>| {
let node = q.single().unwrap().0;
let total_nodes = context.with(|context| {
let edges = context.edges();
let inputs = edges.iter().filter(|e| e.src_node == node).count();
let outputs = edges.iter().filter(|e| e.dst_node == node).count();
assert_eq!(inputs, 2);
assert_eq!(outputs, 2);
context.nodes().len()
});
assert_eq!(total_nodes, 5);
node
},
);
run(
&mut app,
|mut q: Query<&mut VolumeNodeConfig, With<TestMarker>>| {
let mut config = q.single_mut().unwrap();
config.channels = NonZeroChannelCount::new(3).unwrap();
},
);
app.update();
run(
&mut app,
move |q: Query<&FirewheelNode, With<TestMarker>>, mut context: ResMut<AudioContext>| {
let node = q.single().unwrap().0;
assert_ne!(initial_id, node);
let total_nodes = context.with(|context| {
let edges = context.edges();
let inputs = edges.iter().filter(|e| e.src_node == node).count();
let outputs = edges.iter().filter(|e| e.dst_node == node).count();
assert_eq!(inputs, 2);
assert_eq!(outputs, 2);
context.nodes().len()
});
assert_eq!(total_nodes, 5);
node.0
},
);
}
}