use crate::{
context::AudioStreamConfig,
edge::{AudioGraphInput, AudioGraphOutput, PendingConnections},
node::FirewheelNode,
};
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;
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 initialize_stream = {
let config = self.firewheel_config;
move |mut commands: Commands,
server: Res<AssetServer>,
stream_config: Res<AudioStreamConfig<B>>| {
crate::context::initialize_context::<B>(
config,
stream_config.0.clone(),
&mut commands,
&server,
)
}
};
app.preregister_asset_loader::<crate::sample::SampleLoader>(
crate::sample::SampleLoader::extensions(),
)
.add_systems(
PreStartup,
(insert_io, set_up_graph)
.chain()
.in_set(SeedlingStartupSystems::GraphSetup),
)
.add_systems(
PostStartup,
(initialize_stream, connect_io)
.chain()
.in_set(SeedlingStartupSystems::StreamInitialization),
)
.add_systems(
Last,
add_default_transforms.before(crate::SeedlingSystems::Acquire),
)
.add_observer(fetch_io::<B>)
.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<B: AudioBackend>(
_: Trigger<FetchAudioIoEvent>,
existing_inputs: Query<(Entity, &InputDeviceInfo)>,
existing_outputs: Query<(Entity, &OutputDeviceInfo)>,
mut commands: Commands,
) {
let new_inputs = B::available_input_devices()
.into_iter()
.map(|input| InputDeviceInfo {
name: input.name,
num_channels: input.num_channels,
is_default: input.is_default,
})
.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.name == new_input.name);
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.name);
commands.spawn((new_input.clone(), Name::new("Audio Input Device")));
}
}
}
for (entity, old_input) in old_inputs {
if !new_inputs.iter().any(|i| i.name == old_input.name) {
debug!("Audio input \"{}\" no longer available.", old_input.name);
commands.entity(entity).despawn();
}
}
let new_outputs = B::available_output_devices()
.into_iter()
.map(|output| OutputDeviceInfo {
name: output.name,
num_channels: output.num_channels,
is_default: output.is_default,
})
.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.name == new_output.name);
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.name);
commands.spawn((new_output.clone(), Name::new("Audio Output Device")));
}
}
}
for (entity, old_output) in old_outputs {
if !new_outputs.iter().any(|i| i.name == old_output.name) {
debug!("Audio output \"{}\" no longer available.", old_output.name);
commands.entity(entity).despawn();
}
}
}
#[derive(Event, Debug)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct RestartAudioEvent;
fn restart_audio(
_: Trigger<RestartAudioEvent>,
inputs: Query<&InputDeviceInfo>,
outputs: Query<&OutputDeviceInfo>,
mut config: ResMut<AudioStreamConfig>,
) {
if let Some(input) = &mut config.0.input {
if let Some(input_name) = &input.device_name {
if !inputs.iter().any(|i| &i.name == input_name) {
let new_input_name = inputs
.iter()
.find(|i| i.is_default)
.map(|input| input.name.clone());
input.device_name = new_input_name;
}
}
}
if let Some(output_name) = &config.0.output.device_name {
if !outputs.iter().any(|i| &i.name == output_name) {
let new_output_name = outputs
.iter()
.find(|o| o.is_default)
.map(|output| output.name.clone());
config.0.output.device_name = new_output_name;
}
}
config.set_changed();
}
#[derive(Component, Debug, PartialEq, Clone)]
#[component(immutable)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct InputDeviceInfo {
pub name: String,
pub num_channels: u16,
pub is_default: bool,
}
#[derive(Component, Debug, PartialEq, Clone)]
#[component(immutable)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct OutputDeviceInfo {
pub name: String,
pub num_channels: u16,
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 SfxBus;
#[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()));
commands.trigger(FetchAudioIoEvent);
}
fn connect_io(
input: Query<Entity, With<AudioGraphInput>>,
output: Query<Entity, With<AudioGraphOutput>>,
mut commands: Commands,
mut context: ResMut<crate::prelude::AudioContext>,
) -> Result {
context.with(|ctx| {
commands.entity(input.single()?).insert((
FirewheelNode(ctx.graph_in_node_id()),
Name::new("Audio Input Node"),
));
commands.entity(output.single()?).insert((
FirewheelNode(ctx.graph_out_node_id()),
Name::new("Audio Output Node"),
));
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((SfxBus, VolumeNode::default(), Name::new("SFX Bus")));
commands
.spawn((
crate::pool::dynamic::DynamicBus,
VolumeNode::default(),
Name::new("Dynamic Bus"),
))
.connect(SfxBus);
commands
.spawn((
SamplerPool(DefaultPool),
Name::new("Default Sampler Pool"),
sample_effects![VolumeNode::default()],
))
.connect(SfxBus);
commands
.spawn((
SamplerPool(SpatialPool),
Name::new("Spatial Sampler Pool"),
sample_effects![VolumeNode::default(), SpatialBasicNode::default()],
))
.connect(SfxBus);
commands.spawn((
SamplerPool(MusicPool),
Name::new("Music Sampler Pool"),
sample_effects![VolumeNode::default()],
));
}
GraphConfiguration::Minimal => {
commands
.spawn((MainBus, VolumeNode::default()))
.connect(AudioGraphOutput);
commands.spawn((crate::pool::dynamic::DynamicBus, VolumeNode::default()));
commands.spawn((
SamplerPool(DefaultPool),
sample_effects![VolumeNode::default()],
));
}
GraphConfiguration::Empty => {}
}
commands.remove_resource::<ConfigResource>();
}