use crate::{
context::{AudioContext, AudioStreamConfig, StreamRestartEvent, StreamStartEvent},
edge::{AudioGraphInput, AudioGraphOutput, PendingConnections},
node::{FirewheelNode, FirewheelNodeInfo},
};
use bevy_app::prelude::*;
use bevy_asset::prelude::*;
use bevy_ecs::prelude::*;
use bevy_log::prelude::*;
use bevy_seedling_macros::{NodeLabel, PoolLabel};
use bevy_transform::prelude::Transform;
use core::marker::PhantomData;
use firewheel::backend::AudioBackend;
use std::fmt::Debug;
pub(crate) struct SeedlingStartup<B: AudioBackend> {
firewheel_config: crate::prelude::FirewheelConfig,
_backend: PhantomData<fn() -> B>,
}
impl<B: AudioBackend> SeedlingStartup<B> {
pub fn new(firewheel_config: crate::prelude::FirewheelConfig) -> Self {
Self {
firewheel_config,
_backend: PhantomData,
}
}
}
impl<B: AudioBackend> Plugin for SeedlingStartup<B>
where
B: 'static,
B::Config: Clone + Send + Sync + 'static,
B::StreamError: Send + Sync + 'static,
{
fn build(&self, app: &mut App) {
let construct_context = {
let config = self.firewheel_config;
move |mut commands: Commands| {
crate::context::initialize_context::<B>(config, &mut commands)
}
};
app.preregister_asset_loader::<crate::sample::SampleLoader>(
crate::sample::SampleLoader::extensions(),
)
.add_systems(
PreStartup,
(construct_context, insert_io, set_up_graph)
.chain()
.in_set(SeedlingStartupSystems::GraphSetup),
)
.add_systems(
PostStartup,
(crate::context::start_stream::<B>, |world: &mut World| {
world.trigger(FetchAudioIoEvent)
})
.chain()
.in_set(SeedlingStartupSystems::StreamInitialization),
)
.add_systems(
Last,
add_default_transforms.before(crate::SeedlingSystems::Acquire),
)
.add_observer(fetch_io)
.add_observer(connect_io::<StreamStartEvent>)
.add_observer(connect_io::<StreamRestartEvent>)
.add_observer(restart_audio);
}
}
#[derive(Debug, SystemSet, PartialEq, Eq, Hash, Clone)]
pub enum SeedlingStartupSystems {
GraphSetup,
StreamInitialization,
}
#[derive(Event, Debug)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct FetchAudioIoEvent;
fn fetch_io(
_: On<FetchAudioIoEvent>,
existing_inputs: Query<(Entity, &InputDeviceInfo)>,
existing_outputs: Query<(Entity, &OutputDeviceInfo)>,
mut context: ResMut<AudioContext>,
mut commands: Commands,
) {
context.with(|context| {
let mut is_default = true;
let new_inputs = context
.input_devices_simple()
.into_iter()
.map(|input| {
let value = InputDeviceInfo {
id: input.id,
name: input.name,
is_default,
};
is_default = false;
value
})
.collect::<Vec<_>>();
let old_inputs: Vec<_> = existing_inputs.iter().collect();
for new_input in &new_inputs {
let matching = old_inputs.iter().find(|e| e.1.id == new_input.id);
match matching {
Some((entity, old_value)) => {
if old_value != &new_input {
commands.entity(*entity).insert(new_input.clone());
}
}
None => {
debug!("Found audio input \"{:?}\"", new_input.id);
commands.spawn((new_input.clone(), Name::new("Audio Input Device")));
}
}
}
for (entity, old_input) in old_inputs {
if !new_inputs.iter().any(|i| i.id == old_input.id) {
debug!("Audio input \"{:?}\" no longer available.", old_input.id);
commands.entity(entity).despawn();
}
}
let mut is_default = true;
let new_outputs = context
.output_devices_simple()
.into_iter()
.map(|output| {
let value = OutputDeviceInfo {
id: output.id,
name: output.name,
is_default,
};
is_default = false;
value
})
.collect::<Vec<_>>();
let old_outputs: Vec<_> = existing_outputs.iter().collect();
for new_output in &new_outputs {
let matching = old_outputs.iter().find(|e| e.1.id == new_output.id);
match matching {
Some((entity, old_value)) => {
if old_value != &new_output {
commands.entity(*entity).insert(new_output.clone());
}
}
None => {
debug!("Found audio output \"{:?}\"", new_output.id);
commands.spawn((new_output.clone(), Name::new("Audio Output Device")));
}
}
}
for (entity, old_output) in old_outputs {
if !new_outputs.iter().any(|i| i.id == old_output.id) {
debug!("Audio output \"{:?}\" no longer available.", old_output.id);
commands.entity(entity).despawn();
}
}
});
}
#[derive(Event, Debug)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct RestartAudioEvent;
fn restart_audio(
_: On<RestartAudioEvent>,
inputs: Query<&InputDeviceInfo>,
outputs: Query<&OutputDeviceInfo>,
mut config: ResMut<AudioStreamConfig>,
) -> Result {
if let Some(input) = &mut config.0.input {
if let Some(input_id) = &input.device_id {
if !inputs.iter().any(|i| i.id == input_id.to_string()) {
let new_input_id = inputs
.iter()
.find(|i| i.is_default)
.map(|input| input.id.clone());
input.device_id = new_input_id.and_then(|id| id.parse().ok());
}
}
}
if let Some(output_id) = &config.0.output.device_id {
if !outputs.iter().any(|i| i.id == output_id.to_string()) {
let new_output_name = outputs
.iter()
.find(|o| o.is_default)
.map(|output| output.id.clone());
config.0.output.device_id = new_output_name.and_then(|id| id.parse().ok());
}
}
config.set_changed();
Ok(())
}
#[derive(Component, Debug, PartialEq, Clone)]
#[component(immutable)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct InputDeviceInfo {
pub id: String,
pub name: String,
pub is_default: bool,
}
#[derive(Component, Debug, PartialEq, Clone)]
#[component(immutable)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct OutputDeviceInfo {
pub id: String,
pub name: String,
pub is_default: bool,
}
#[derive(PoolLabel, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct SpatialPool;
fn add_default_transforms(
q: Query<
Entity,
(
With<crate::prelude::SamplePlayer>,
With<SpatialPool>,
Without<Transform>,
),
>,
mut commands: Commands,
) {
for entity in &q {
commands.entity(entity).insert(Transform::default());
}
}
#[derive(PoolLabel, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct MusicPool;
#[derive(NodeLabel, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct SoundEffectsBus;
#[derive(Debug, Default, Clone, Copy)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub enum GraphConfiguration {
#[default]
Game,
Minimal,
Empty,
}
#[derive(Resource)]
pub(crate) struct ConfigResource(pub GraphConfiguration);
fn insert_io(mut commands: Commands) {
commands.spawn((AudioGraphInput, PendingConnections::default()));
commands.spawn((AudioGraphOutput, PendingConnections::default()));
}
fn connect_io<E: Event>(
_: On<E>,
input: Query<Entity, With<AudioGraphInput>>,
output: Query<Entity, With<AudioGraphOutput>>,
mut commands: Commands,
mut context: ResMut<crate::prelude::AudioContext>,
) -> Result {
context.with(|ctx| {
let node_id = ctx.graph_in_node_id();
let info = FirewheelNodeInfo::new(ctx.node_info(node_id).unwrap());
commands
.entity(input.single()?)
.insert((info, Name::new("Audio Input Node")))
.insert_if_new(FirewheelNode(node_id));
let node_id = ctx.graph_out_node_id();
let info = FirewheelNodeInfo::new(ctx.node_info(node_id).unwrap());
commands
.entity(output.single()?)
.insert((info, Name::new("Audio Output Node")))
.insert_if_new(FirewheelNode(node_id));
Ok(())
})
}
fn set_up_graph(mut commands: Commands, config: Res<ConfigResource>) {
use crate::prelude::*;
match config.0 {
GraphConfiguration::Game => {
commands
.spawn((MainBus, VolumeNode::default(), Name::new("Main Bus")))
.chain_node(LimiterNode::new(0.003, 0.15))
.connect(AudioGraphOutput);
commands.spawn((
SoundEffectsBus,
VolumeNode::default(),
Name::new("Sound Effects Bus"),
));
commands
.spawn((
crate::pool::dynamic::DynamicBus,
VolumeNode::default(),
Name::new("Dynamic Bus"),
))
.connect(SoundEffectsBus);
commands
.spawn((
SamplerPool(DefaultPool),
Name::new("Default Sampler Pool"),
sample_effects![VolumeNode::default()],
))
.connect(SoundEffectsBus);
commands
.spawn((
SamplerPool(SpatialPool),
Name::new("Spatial Sampler Pool"),
sample_effects![VolumeNode::default(), SpatialBasicNode::default()],
))
.connect(SoundEffectsBus);
commands.spawn((
SamplerPool(MusicPool),
Name::new("Music Sampler Pool"),
sample_effects![VolumeNode::default()],
));
}
GraphConfiguration::Minimal => {
commands
.spawn((MainBus, VolumeNode::default(), Name::new("Main Bus")))
.connect(AudioGraphOutput);
commands.spawn((
crate::pool::dynamic::DynamicBus,
VolumeNode::default(),
Name::new("Dynamic Bus"),
));
commands.spawn((
SamplerPool(DefaultPool),
Name::new("Default Sampler Pool"),
sample_effects![VolumeNode::default()],
));
}
GraphConfiguration::Empty => {}
}
commands.remove_resource::<ConfigResource>();
}