use crate::context::{AudioContext, SeedlingContextWrapper};
use crate::node::FirewheelNodeInfo;
use crate::node::label::InternedNodeLabel;
use crate::prelude::{FirewheelNode, MainBus, NodeLabel};
use bevy_ecs::prelude::*;
use bevy_log::error_once;
use bevy_platform::collections::HashMap;
use firewheel::node::NodeID;
#[cfg(debug_assertions)]
use core::panic::Location;
#[allow(clippy::module_inception)]
mod connect;
mod disconnect;
pub use connect::*;
pub use disconnect::*;
#[derive(NodeLabel, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct AudioGraphInput;
#[derive(NodeLabel, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct AudioGraphOutput;
#[derive(Component, Default, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub enum ChannelMapping {
#[default]
Speakers,
Discrete,
}
impl ChannelMapping {
pub fn map_channels(&self, outputs: u32, inputs: u32) -> Vec<(u32, u32)> {
let map_min = || (0..outputs.min(inputs)).map(|i| (i, i)).collect();
match self {
ChannelMapping::Discrete => map_min(),
ChannelMapping::Speakers => {
match (outputs, inputs) {
(1, 2) | (1, 4) => {
vec![(0, 0), (0, 1)]
}
(1, 6) => {
vec![(0, 2)]
}
(2, 1) => {
vec![(0, 0), (1, 0)]
}
(2, 4) | (2, 6) => {
vec![(0, 0), (1, 1)]
}
(4, 1) => {
vec![(0, 0), (1, 0), (2, 0), (3, 0)]
}
(4, 2) => {
vec![(0, 0), (1, 1), (2, 0), (3, 1)]
}
(4, 6) => {
vec![(0, 0), (1, 1), (2, 4), (3, 5)]
}
(6, 1) => {
vec![(0, 0), (1, 0), (2, 0), (4, 0), (5, 0)]
}
(6, 2) => {
vec![(0, 0), (2, 0), (4, 0), (1, 1), (2, 1), (5, 1)]
}
(6, 4) => {
vec![(0, 0), (2, 0), (1, 1), (2, 1), (4, 2), (5, 3)]
}
_ => map_min(),
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EdgeTarget {
Label(InternedNodeLabel),
Entity(Entity),
Node(NodeID),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PendingEdge {
pub target: EdgeTarget,
pub ports: Option<Vec<(u32, u32)>>,
#[cfg(debug_assertions)]
pub(crate) origin: &'static Location<'static>,
}
impl PendingEdge {
#[cfg_attr(debug_assertions, track_caller)]
pub fn new(target: impl Into<EdgeTarget>, ports: Option<Vec<(u32, u32)>>) -> Self {
Self {
target: target.into(),
ports,
#[cfg(debug_assertions)]
origin: Location::caller(),
}
}
fn new_with_location(
target: impl Into<EdgeTarget>,
ports: Option<Vec<(u32, u32)>>,
#[cfg(debug_assertions)] location: &'static Location<'static>,
) -> Self {
Self {
target: target.into(),
ports,
#[cfg(debug_assertions)]
origin: location,
}
}
}
impl From<NodeID> for EdgeTarget {
fn from(value: NodeID) -> Self {
Self::Node(value)
}
}
impl<T> From<T> for EdgeTarget
where
T: NodeLabel,
{
fn from(value: T) -> Self {
Self::Label(value.intern())
}
}
impl From<Entity> for EdgeTarget {
fn from(value: Entity) -> Self {
Self::Entity(value)
}
}
#[derive(Default, Debug, Resource)]
pub struct NodeMap(HashMap<InternedNodeLabel, Entity>);
impl core::ops::Deref for NodeMap {
type Target = HashMap<InternedNodeLabel, Entity>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl core::ops::DerefMut for NodeMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub(crate) fn auto_connect(
nodes: Query<(Entity, &FirewheelNode), Without<PendingConnections>>,
mut context: ResMut<AudioContext>,
mut commands: Commands,
) {
if nodes.iter().len() == 0 {
return;
}
context.with(|context| {
for (entity, node) in nodes.iter() {
let Some(info) = context.node_info(node.0) else {
continue;
};
let outputs = info.info.channel_config.num_outputs.get();
if outputs == 0 {
continue;
}
commands.entity(entity).connect(MainBus);
}
});
}
fn lookup_node<'a>(
target_entity: Entity,
connection: &PendingEdge,
targets: &'a Query<(&FirewheelNode, &FirewheelNodeInfo)>,
) -> Option<(&'a FirewheelNode, &'a FirewheelNodeInfo)> {
match targets.get(target_entity) {
Ok(t) => Some(t),
Err(_) => {
#[cfg(debug_assertions)]
{
let location = connection.origin;
error_once!(
"failed to connect to entity `{target_entity:?}` at {location}: no Firewheel node found"
);
}
#[cfg(not(debug_assertions))]
{
let _ = connection;
error_once!(
"failed to connect to entity `{target_entity:?}`: no Firewheel node found"
);
}
None
}
}
}
fn fetch_target(
connection: &PendingEdge,
node_map: &NodeMap,
targets: &Query<(&FirewheelNode, &FirewheelNodeInfo)>,
context: &dyn SeedlingContextWrapper,
) -> Option<(NodeID, FirewheelNodeInfo)> {
match connection.target {
EdgeTarget::Entity(entity) => {
lookup_node(entity, connection, targets).map(|(node, info)| (node.0, *info))
}
EdgeTarget::Label(label) => {
let Some(entity) = node_map.get(&label) else {
#[cfg(debug_assertions)]
{
let location = connection.origin;
error_once!(
"failed to connect to node label `{label:?}` at {location}: no associated Firewheel node found"
);
}
#[cfg(not(debug_assertions))]
error_once!(
"failed to connect to node label `{label:?}`: no associated Firewheel node found"
);
return None;
};
lookup_node(*entity, connection, targets).map(|(node, info)| (node.0, *info))
}
EdgeTarget::Node(dest_node) => {
let Some(info) = context.node_info(dest_node) else {
error_once!(
"failed to connect audio node to target: the target `NodeID` doesn't exist"
);
return None;
};
let info = FirewheelNodeInfo::new(info);
Some((dest_node, info))
}
}
}
#[cfg(test)]
mod test {
use crate::{
prelude::*,
test::{prepare_app, run},
};
use bevy_ecs::prelude::*;
#[derive(Component)]
struct One;
#[test]
fn test_disconnect_then_reconnect() {
let mut app = prepare_app(|mut commands: Commands| {
commands.spawn((VolumeNode::default(), One));
commands
.spawn((VolumeNode::default(), MainBus))
.connect(AudioGraphOutput);
});
app.update();
run(
&mut app,
|mut context: ResMut<AudioContext>,
one: Single<&FirewheelNode, With<One>>,
main: Single<&FirewheelNode, With<MainBus>>| {
let one = one.into_inner();
let main = main.into_inner();
context.with(|context| {
assert_eq!(context.nodes().len(), 4);
let outgoing_edges_one: Vec<_> = context
.edges()
.into_iter()
.filter(|e| e.src_node == one.0)
.collect();
assert_eq!(outgoing_edges_one.len(), 2);
assert!(outgoing_edges_one.iter().all(|e| e.dst_node == main.0));
});
},
);
run(
&mut app,
|one: Single<Entity, With<One>>, mut commands: Commands| {
commands
.entity(*one)
.disconnect(MainBus)
.connect_with(MainBus, &[(0, 0)]);
},
);
app.update();
run(
&mut app,
|mut context: ResMut<AudioContext>,
one: Single<&FirewheelNode, With<One>>,
main: Single<&FirewheelNode, With<MainBus>>| {
let one = one.into_inner();
let main = main.into_inner();
context.with(|context| {
assert_eq!(context.nodes().len(), 4);
let outgoing_edges_one: Vec<_> = context
.edges()
.into_iter()
.filter(|e| e.src_node == one.0)
.collect();
assert_eq!(outgoing_edges_one.len(), 1);
assert!(outgoing_edges_one.iter().all(|e| e.dst_node == main.0));
});
},
);
}
}