use super::{EdgeTarget, NodeMap, PendingEdge};
use crate::{
context::AudioContext,
edge::ChannelMapping,
node::{FirewheelNode, FirewheelNodeInfo},
};
use bevy_ecs::prelude::*;
use bevy_log::prelude::*;
use core::ops::Deref;
#[cfg(debug_assertions)]
use core::panic::Location;
#[derive(Debug, Default, Component)]
pub struct PendingConnections(Vec<PendingEdge>);
impl PendingConnections {
pub fn push(&mut self, connection: PendingEdge) {
self.0.push(connection)
}
}
pub trait Connect<'a>: Sized {
#[cfg_attr(debug_assertions, track_caller)]
fn connect(self, target: impl Into<EdgeTarget>) -> ConnectCommands<'a>;
#[cfg_attr(debug_assertions, track_caller)]
fn connect_with(
self,
target: impl Into<EdgeTarget>,
ports: &[(u32, u32)],
) -> ConnectCommands<'a>;
#[cfg_attr(debug_assertions, track_caller)]
fn chain_node<B: Bundle>(self, node: B) -> ConnectCommands<'a>;
#[cfg_attr(debug_assertions, track_caller)]
fn chain_node_with<B: Bundle>(self, node: B, ports: &[(u32, u32)]) -> ConnectCommands<'a>;
#[must_use]
fn head(&self) -> Entity;
#[must_use]
fn tail(&self) -> Entity;
}
#[cfg_attr(debug_assertions, track_caller)]
fn connect_with_commands(
target: EdgeTarget,
connections: Option<Vec<(u32, u32)>>,
commands: &mut EntityCommands,
) {
#[cfg(debug_assertions)]
let location = Location::caller();
commands
.entry::<PendingConnections>()
.or_default()
.and_modify(|mut pending| {
pending.push(PendingEdge::new_with_location(
target,
connections,
#[cfg(debug_assertions)]
location,
));
});
}
impl<'a> Connect<'a> for EntityCommands<'a> {
fn connect(mut self, target: impl Into<EdgeTarget>) -> ConnectCommands<'a> {
let target = target.into();
connect_with_commands(target, None, &mut self);
ConnectCommands::new(self)
}
fn connect_with(
mut self,
target: impl Into<EdgeTarget>,
ports: &[(u32, u32)],
) -> ConnectCommands<'a> {
let target = target.into();
let ports = ports.to_vec();
connect_with_commands(target, Some(ports), &mut self);
ConnectCommands::new(self)
}
fn chain_node<B: Bundle>(mut self, node: B) -> ConnectCommands<'a> {
let new_id = self.commands().spawn(node).id();
let mut new_connection = self.connect(new_id);
new_connection.tail = Some(new_id);
new_connection
}
fn chain_node_with<B: Bundle>(mut self, node: B, ports: &[(u32, u32)]) -> ConnectCommands<'a> {
let new_id = self.commands().spawn(node).id();
let mut new_connection = self.connect_with(new_id, ports);
new_connection.tail = Some(new_id);
new_connection
}
#[inline(always)]
fn head(&self) -> Entity {
self.id()
}
#[inline(always)]
fn tail(&self) -> Entity {
self.id()
}
}
impl<'a> Connect<'a> for ConnectCommands<'a> {
#[cfg_attr(debug_assertions, track_caller)]
fn connect(mut self, target: impl Into<EdgeTarget>) -> ConnectCommands<'a> {
let tail = self.tail();
let mut commands = self.commands.commands();
let mut commands = commands.entity(tail);
let target = target.into();
connect_with_commands(target, None, &mut commands);
self
}
#[cfg_attr(debug_assertions, track_caller)]
fn connect_with(
mut self,
target: impl Into<EdgeTarget>,
ports: &[(u32, u32)],
) -> ConnectCommands<'a> {
let tail = self.tail();
let mut commands = self.commands.commands();
let mut commands = commands.entity(tail);
let target = target.into();
let ports = ports.to_vec();
connect_with_commands(target, Some(ports), &mut commands);
self
}
fn chain_node<B: Bundle>(mut self, node: B) -> ConnectCommands<'a> {
let new_id = self.commands.commands().spawn(node).id();
let mut new_connection = self.connect(new_id);
new_connection.tail = Some(new_id);
new_connection
}
fn chain_node_with<B: Bundle>(mut self, node: B, ports: &[(u32, u32)]) -> ConnectCommands<'a> {
let new_id = self.commands.commands().spawn(node).id();
let mut new_connection = self.connect_with(new_id, ports);
new_connection.tail = Some(new_id);
new_connection
}
#[inline(always)]
fn head(&self) -> Entity {
<Self>::head(self)
}
#[inline(always)]
fn tail(&self) -> Entity {
<Self>::tail(self)
}
}
pub struct ConnectCommands<'a> {
commands: EntityCommands<'a>,
head: Entity,
tail: Option<Entity>,
}
impl<'a> ConnectCommands<'a> {
pub(crate) fn new(commands: EntityCommands<'a>) -> Self {
Self {
head: commands.id(),
tail: None,
commands,
}
}
fn head(&self) -> Entity {
self.head
}
fn tail(&self) -> Entity {
self.tail.unwrap_or(self.head)
}
}
impl core::fmt::Debug for ConnectCommands<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConnectCommands")
.field("entity", &self.head)
.finish_non_exhaustive()
}
}
pub(crate) fn process_connections(
mut connections: Query<(
&mut PendingConnections,
&FirewheelNode,
&FirewheelNodeInfo,
&ChannelMapping,
)>,
targets: Query<(&FirewheelNode, &FirewheelNodeInfo)>,
node_map: Res<NodeMap>,
mut context: ResMut<AudioContext>,
) {
let connections = connections
.iter_mut()
.filter(|(pending, ..)| !pending.0.is_empty())
.collect::<Vec<_>>();
if connections.is_empty() {
return;
}
context.with(|context| {
for (mut pending, source_node, source_info, source_mapping) in connections.into_iter() {
pending.0.retain(|connection| {
let Some((target_node, target_info)) =
super::fetch_target(connection, &node_map, &targets, (*context).deref())
else {
return false;
};
let inferred_ports;
let ports = match connection.ports.as_deref() {
Some(ports) => ports,
None => {
let outputs = source_info.channel_config.num_outputs.get();
let inputs = target_info.channel_config.num_inputs.get();
inferred_ports = source_mapping.map_channels(outputs, inputs);
inferred_ports.as_slice()
}
};
if let Err(e) = context.connect(source_node.0, target_node, ports, false) {
error_once!("failed to connect audio node to target: {e}");
}
false
});
}
});
}
#[cfg(test)]
mod test {
use crate::{
context::AudioContext,
edge::AudioGraphOutput,
prelude::MainBus,
test::{prepare_app, run},
};
use super::*;
use bevy::ecs::system::RunSystemOnce;
use firewheel::{
channel_config::NonZeroChannelCount,
nodes::volume::{VolumeNode, VolumeNodeConfig},
};
#[derive(Component)]
struct One;
#[derive(Component)]
struct Two;
#[derive(Component)]
struct Three;
#[test]
fn test_chain() {
let mut app = prepare_app(|mut commands: Commands| {
commands
.spawn((VolumeNode::default(), One))
.chain_node((VolumeNode::default(), Two));
commands
.spawn((VolumeNode::default(), MainBus))
.connect(AudioGraphOutput);
});
app.world_mut()
.run_system_once(
|mut context: ResMut<AudioContext>,
one: Single<&FirewheelNode, With<One>>,
two: Single<&FirewheelNode, With<Two>>,
main: Single<&FirewheelNode, With<MainBus>>| {
let one = one.into_inner();
let two = two.into_inner();
let main = main.into_inner();
context.with(|context| {
assert_eq!(context.nodes().len(), 5);
let outgoing_edges_one: Vec<_> = context
.edges()
.into_iter()
.filter(|e| e.src_node == one.0)
.collect();
let outgoing_edges_two: Vec<_> = context
.edges()
.into_iter()
.filter(|e| e.src_node == two.0)
.collect();
assert_eq!(outgoing_edges_one.len(), 2);
assert_eq!(outgoing_edges_two.len(), 2);
assert!(outgoing_edges_one.iter().all(|e| e.dst_node == two.0));
assert!(outgoing_edges_two.iter().all(|e| e.dst_node == main.0));
});
},
)
.unwrap();
}
#[test]
fn test_fanout() {
let mut app = prepare_app(|mut commands: Commands| {
let a = commands.spawn((VolumeNode::default(), One)).head();
let b = commands.spawn((VolumeNode::default(), Two)).head();
commands
.spawn((VolumeNode::default(), Three))
.connect(a)
.connect(b);
commands
.spawn((VolumeNode::default(), MainBus))
.connect(AudioGraphOutput);
});
app.world_mut()
.run_system_once(
|mut context: ResMut<AudioContext>,
one: Single<&FirewheelNode, With<One>>,
two: Single<&FirewheelNode, With<Two>>,
three: Single<&FirewheelNode, With<Three>>| {
let one = one.into_inner();
let two = two.into_inner();
let three = three.into_inner();
context.with(|context| {
assert_eq!(context.nodes().len(), 6);
let outgoing_edges_three: Vec<_> = context
.edges()
.into_iter()
.filter(|e| e.src_node == three.0)
.collect();
assert_eq!(
outgoing_edges_three
.iter()
.filter(|e| e.dst_node == one.0)
.count(),
2
);
assert_eq!(
outgoing_edges_three
.iter()
.filter(|e| e.dst_node == two.0)
.count(),
2
);
});
},
)
.unwrap();
}
#[test]
fn test_simple_auto_connect() {
let mut app = prepare_app(|mut commands: Commands| {
commands
.spawn((
VolumeNode::default(),
VolumeNodeConfig {
channels: NonZeroChannelCount::new(1).unwrap(),
},
One,
))
.chain_node((
VolumeNode::default(),
VolumeNodeConfig {
channels: NonZeroChannelCount::new(1).unwrap(),
},
Two,
))
.connect(AudioGraphOutput);
});
let connected = run(
&mut app,
|one: Single<&FirewheelNode, With<One>>,
two: Single<&FirewheelNode, With<Two>>,
mut context: ResMut<AudioContext>| {
context.with(|context| {
let edges = context.edges();
for edge in edges {
if edge.src_node == one.0 && edge.dst_node == two.0 {
return true;
}
}
false
})
},
);
assert!(connected);
}
#[test]
fn test_downmix() {
let mut app = prepare_app(|mut commands: Commands| {
commands
.spawn((
VolumeNode::default(),
VolumeNodeConfig {
channels: NonZeroChannelCount::new(2).unwrap(),
},
One,
))
.chain_node((
VolumeNode::default(),
VolumeNodeConfig {
channels: NonZeroChannelCount::new(1).unwrap(),
},
Two,
))
.connect(AudioGraphOutput);
});
let connected = run(
&mut app,
|one: Single<&FirewheelNode, With<One>>,
two: Single<&FirewheelNode, With<Two>>,
mut context: ResMut<AudioContext>| {
context.with(|context| {
let edges = context.edges();
let mut left = false;
let mut right = false;
for edge in edges {
if edge.src_node == one.0
&& edge.dst_node == two.0
&& edge.src_port == 0
&& edge.dst_port == 0
{
left = true;
}
if edge.src_node == one.0
&& edge.dst_node == two.0
&& edge.src_port == 1
&& edge.dst_port == 0
{
right = true;
}
}
left && right
})
},
);
assert!(connected);
}
}