#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::type_complexity)]
#![expect(clippy::needless_doctest_main)]
#![warn(missing_debug_implementations)]
#![warn(missing_docs)]
extern crate self as bevy_seedling;
use bevy_app::prelude::*;
use bevy_asset::prelude::AssetApp;
use bevy_ecs::prelude::*;
use context::AudioStreamConfig;
use firewheel::{CpalBackend, backend::AudioBackend};
pub use firewheel;
pub mod configuration;
pub mod context;
pub mod edge;
pub mod error;
pub mod node;
pub mod nodes;
pub mod pool;
pub mod sample;
pub mod spatial;
pub mod time;
pub mod utils;
pub mod prelude {
pub use crate::configuration::{
GraphConfiguration, InputDeviceInfo, MusicPool, OutputDeviceInfo, SeedlingStartupSystems,
SfxBus, SpatialPool,
};
pub use crate::context::AudioContext;
pub use crate::edge::{AudioGraphInput, AudioGraphOutput, Connect, Disconnect, EdgeTarget};
pub use crate::node::{
FirewheelNode, RegisterNode,
events::{AudioEvents, VolumeFade},
label::{MainBus, NodeLabel},
};
#[cfg(feature = "loudness")]
pub use crate::nodes::loudness::{LoudnessConfig, LoudnessNode, LoudnessState};
pub use crate::nodes::{
bpf::{BandPassConfig, BandPassNode},
freeverb::FreeverbNode,
itd::{ItdConfig, ItdNode},
limiter::{LimiterConfig, LimiterNode},
lpf::{LowPassConfig, LowPassNode},
send::{SendConfig, SendNode},
};
pub use crate::pool::{
DefaultPoolSize, PlaybackCompletionEvent, PoolCommands, PoolDespawn, PoolSize, SamplerPool,
dynamic::DynamicBus,
label::{DefaultPool, PoolLabel},
sample_effects::{EffectOf, EffectsQuery, SampleEffects},
};
pub use crate::sample::{
AudioSample, OnComplete, PlaybackSettings, SamplePlayer, SamplePriority,
};
pub use crate::sample_effects;
pub use crate::spatial::{
DefaultSpatialScale, SpatialListener2D, SpatialListener3D, SpatialScale,
};
pub use crate::time::{Audio, AudioTime};
pub use crate::utils::perceptual_volume::PerceptualVolume;
pub use crate::{SeedlingPlugin, SeedlingSystems};
pub use firewheel::{
CpalBackend, FirewheelConfig, Volume,
channel_config::{ChannelCount, NonZeroChannelCount},
clock::{
DurationMusical, DurationSamples, DurationSeconds, InstantMusical, InstantSamples,
InstantSeconds,
},
diff::{Memo, Notify},
nodes::{
StereoToMonoNode,
sampler::{
PlaybackSpeedQuality, PlaybackState, Playhead, RepeatMode, SamplerConfig,
SamplerNode,
},
spatial_basic::SpatialBasicNode,
volume::{VolumeNode, VolumeNodeConfig},
volume_pan::VolumePanNode,
},
};
#[cfg(feature = "stream")]
pub use firewheel::nodes::stream::{
reader::{StreamReaderConfig, StreamReaderNode},
writer::{StreamWriterConfig, StreamWriterNode},
};
#[cfg(feature = "hrtf")]
pub use firewheel_ircam_hrtf::{self as hrtf, HrtfConfig, HrtfNode};
#[cfg(feature = "rand")]
pub use crate::sample::RandomPitch;
}
#[derive(Debug, SystemSet, PartialEq, Eq, Hash, Clone)]
pub enum SeedlingSystems {
Acquire,
Connect,
Pool,
Queue,
Flush,
}
#[derive(Debug)]
pub struct SeedlingPlugin<B: AudioBackend> {
pub config: prelude::FirewheelConfig,
pub stream_config: B::Config,
pub graph_config: configuration::GraphConfiguration,
}
impl Default for SeedlingPlugin<CpalBackend> {
fn default() -> Self {
SeedlingPlugin::<CpalBackend>::new()
}
}
impl<B: AudioBackend> SeedlingPlugin<B>
where
B::Config: Default,
{
pub fn new() -> Self {
Self {
config: prelude::FirewheelConfig::default(),
stream_config: B::Config::default(),
graph_config: prelude::GraphConfiguration::default(),
}
}
}
#[cfg(feature = "web_audio")]
impl SeedlingPlugin<firewheel_web_audio::WebAudioBackend> {
pub fn new_web_audio() -> Self {
Self {
config: prelude::FirewheelConfig::default(),
stream_config: <firewheel_web_audio::WebAudioBackend as AudioBackend>::Config::default(
),
graph_config: prelude::GraphConfiguration::default(),
}
}
}
fn resource_changed_without_insert<R: Resource>(res: Res<R>, mut has_run: Local<bool>) -> bool {
let changed = res.is_changed() && *has_run;
*has_run = true;
changed
}
impl<B: AudioBackend> Plugin for SeedlingPlugin<B>
where
B: 'static,
B::Config: Clone + Send + Sync + 'static,
B::StreamError: Send + Sync + 'static,
{
fn build(&self, app: &mut App) {
use prelude::*;
app.insert_resource(context::AudioStreamConfig::<B>(self.stream_config.clone()))
.insert_resource(configuration::ConfigResource(self.graph_config))
.init_resource::<edge::NodeMap>()
.init_resource::<node::ScheduleDiffing>()
.init_resource::<node::AudioScheduleLookahead>()
.init_resource::<node::PendingRemovals>()
.init_resource::<pool::DefaultPoolSize>()
.init_asset::<sample::AudioSample>()
.register_node::<VolumeNode>()
.register_node::<VolumePanNode>()
.register_node::<SpatialBasicNode>()
.register_simple_node::<StereoToMonoNode>();
app.configure_sets(
Last,
(
SeedlingSystems::Connect.after(SeedlingSystems::Acquire),
SeedlingSystems::Pool.after(SeedlingSystems::Connect),
SeedlingSystems::Queue.after(SeedlingSystems::Pool),
SeedlingSystems::Flush.after(SeedlingSystems::Queue),
),
)
.add_systems(
Last,
(
edge::auto_connect
.before(SeedlingSystems::Connect)
.after(SeedlingSystems::Acquire),
(edge::process_connections, edge::process_disconnections)
.chain()
.in_set(SeedlingSystems::Connect),
node::flush_events.in_set(SeedlingSystems::Flush),
),
)
.add_systems(
PostUpdate,
(context::pre_restart_context, context::restart_context::<B>)
.chain()
.run_if(resource_changed_without_insert::<AudioStreamConfig<B>>),
)
.add_observer(node::label::NodeLabels::on_add_observer)
.add_observer(node::label::NodeLabels::on_replace_observer)
.add_observer(sample::observe_player_insert);
app.add_plugins((
configuration::SeedlingStartup::<B>::new(self.config),
pool::SamplePoolPlugin,
nodes::SeedlingNodesPlugin,
node::events::EventsPlugin,
spatial::SpatialPlugin,
time::TimePlugin,
#[cfg(feature = "rand")]
sample::RandomPlugin,
));
#[cfg(feature = "stream")]
app.register_simple_node::<StreamReaderNode>()
.register_simple_node::<StreamWriterNode>();
#[cfg(all(feature = "reflect", feature = "stream"))]
app.register_type::<StreamReaderNode>()
.register_type::<StreamWriterNode>();
#[cfg(feature = "hrtf")]
app.register_node::<HrtfNode>();
#[cfg(all(feature = "reflect", feature = "hrtf"))]
app.register_type::<HrtfNode>()
.register_type::<HrtfConfig>();
#[cfg(all(feature = "reflect", feature = "rand"))]
app.register_type::<RandomPitch>();
#[cfg(feature = "reflect")]
app.register_type::<FirewheelNode>()
.register_type::<SamplePlayer>()
.register_type::<SamplePriority>()
.register_type::<PlaybackSettings>()
.register_type::<sample::SampleQueueLifetime>()
.register_type::<OnComplete>()
.register_type::<SpatialScale>()
.register_type::<DefaultSpatialScale>()
.register_type::<SpatialListener2D>()
.register_type::<SpatialListener3D>()
.register_type::<InputDeviceInfo>()
.register_type::<OutputDeviceInfo>()
.register_type::<firewheel::node::NodeID>()
.register_type::<node::follower::FollowerOf>()
.register_type::<SendNode>()
.register_type::<LowPassNode>()
.register_type::<LowPassConfig>()
.register_type::<BandPassConfig>()
.register_type::<LimiterNode>()
.register_type::<LimiterConfig>()
.register_type::<ItdNode>()
.register_type::<ItdConfig>()
.register_type::<LimiterConfig>()
.register_type::<FreeverbNode>()
.register_type::<Volume>()
.register_type::<firewheel::dsp::pan_law::PanLaw>()
.register_type::<MainBus>()
.register_type::<PoolSize>()
.register_type::<DefaultPoolSize>()
.register_type::<PlaybackCompletionEvent>()
.register_type::<DefaultPool>()
.register_type::<SamplerPool<DefaultPool>>()
.register_type::<DynamicBus>()
.register_type::<configuration::FetchAudioIoEvent>()
.register_type::<configuration::RestartAudioEvent>()
.register_type::<configuration::SfxBus>()
.register_type::<configuration::GraphConfiguration>()
.register_type::<configuration::MusicPool>()
.register_type::<SamplerPool<configuration::MusicPool>>()
.register_type::<configuration::SpatialPool>()
.register_type::<SamplerPool<configuration::SpatialPool>>()
.register_type::<node::ScheduleDiffing>()
.register_type::<node::AudioScheduleLookahead>()
.register_type::<NonZeroChannelCount>()
.register_type::<SamplerConfig>()
.register_type::<PlaybackState>()
.register_type::<RepeatMode>()
.register_type::<Playhead>()
.register_type::<Notify<f32>>()
.register_type::<Notify<bool>>()
.register_type::<Notify<PlaybackState>>()
.register_type::<InstantMusical>()
.register_type::<InstantSeconds>()
.register_type::<InstantSamples>()
.register_type::<DurationMusical>()
.register_type::<DurationSeconds>()
.register_type::<DurationSamples>()
.register_type::<VolumeNode>()
.register_type::<VolumeNodeConfig>()
.register_type::<VolumePanNode>();
}
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use bevy::{ecs::system::RunSystemOnce, prelude::*};
pub fn prepare_app<F: IntoSystem<(), (), M>, M>(startup: F) -> App {
let mut app = App::new();
app.add_plugins((
MinimalPlugins,
AssetPlugin::default(),
SeedlingPlugin::<crate::utils::profiling::ProfilingBackend> {
graph_config: crate::configuration::GraphConfiguration::Empty,
..SeedlingPlugin::<crate::utils::profiling::ProfilingBackend>::new()
},
TransformPlugin,
))
.add_systems(Startup, startup);
app.finish();
app.cleanup();
app.update();
app
}
pub fn run<F: IntoSystem<(), O, M>, O, M>(app: &mut App, system: F) -> O {
let world = app.world_mut();
world.run_system_once(system).unwrap()
}
}