use crate::{
SeedlingSystems,
context::AudioContext,
edge::{PendingConnections, PendingEdge},
error::SeedlingError,
node::{EffectId, FirewheelNode, RegisterNode},
pool::label::PoolLabelContainer,
prelude::PoolLabel,
sample::{OnComplete, PlaybackSettings, QueuedSample, SamplePlayer},
};
use bevy::{
ecs::{
component::{ComponentId, HookContext},
entity::EntityCloner,
system::QueryLens,
world::DeferredWorld,
},
prelude::*,
};
use core::ops::{Deref, RangeInclusive};
use firewheel::nodes::{
sampler::{SamplerConfig, SamplerNode, SamplerState},
volume::VolumeNode,
};
use queue::SkipTimer;
use sample_effects::{EffectOf, SampleEffects};
pub mod dynamic;
mod entity_set;
pub mod label;
mod queue;
pub mod sample_effects;
pub(crate) struct SamplePoolPlugin;
impl Plugin for SamplePoolPlugin {
fn build(&self, app: &mut App) {
app.register_node::<SamplerNode>()
.add_systems(
Last,
(
(populate_pool, queue::grow_pools)
.chain()
.before(SeedlingSystems::Acquire),
(poll_finished, queue::assign_default, retrieve_state)
.before(SeedlingSystems::Pool)
.after(SeedlingSystems::Connect),
watch_sample_players
.before(SeedlingSystems::Queue)
.after(SeedlingSystems::Pool),
(queue::assign_work, queue::update_followers)
.chain()
.in_set(SeedlingSystems::Pool),
(queue::tick_skipped, queue::mark_skipped)
.chain()
.after(SeedlingSystems::Pool),
),
)
.add_observer(remove_finished)
.add_plugins(dynamic::DynamicPlugin);
}
}
#[derive(Debug, Component)]
#[component(immutable, on_insert = Self::on_insert_hook)]
#[require(PoolMarker)]
pub struct SamplerPool<T: PoolLabel + Component + Clone>(pub T);
impl<T: PoolLabel + Component + Clone> SamplerPool<T> {
fn on_insert_hook(mut world: DeferredWorld, context: HookContext) {
world.commands().queue(move |world: &mut World| {
let id = match world.component_id::<T>() {
Some(id) => id,
None => world.register_component::<T>(),
};
let Some(value) = world.get::<SamplerPool<T>>(context.entity) else {
return;
};
let container = PoolLabelContainer::new(&value.0, id);
world.entity_mut(context.entity).insert(container);
});
}
}
#[derive(Component, Default)]
struct PoolMarker;
#[derive(Debug, Component)]
#[relationship(relationship_target = PoolSamplers)]
struct PoolSamplerOf(pub Entity);
#[derive(Debug, Component)]
#[relationship_target(relationship = PoolSamplerOf, linked_spawn)]
struct PoolSamplers(Vec<Entity>);
#[derive(Component, Clone)]
struct SamplerStateWrapper(SamplerState);
#[derive(Debug, Component)]
#[relationship(relationship_target = Sampler)]
#[component(on_remove = Self::on_remove_hook)]
pub struct SamplerOf(pub Entity);
impl SamplerOf {
fn on_remove_hook(mut world: DeferredWorld, context: HookContext) {
if let Some(mut sampler) = world.get_mut::<SamplerNode>(context.entity) {
sampler.stop();
}
}
}
#[derive(Component)]
#[relationship_target(relationship = SamplerOf)]
#[component(on_remove = Self::on_insert_hook)]
pub struct Sampler {
#[relationship]
sampler: Entity,
state: Option<SamplerState>,
}
impl Sampler {
pub fn sampler(&self) -> Entity {
self.sampler
}
pub fn is_playing(&self) -> bool {
self.state
.as_ref()
.map(|s| !s.stopped())
.unwrap_or_default()
}
pub fn playhead_frames(&self) -> u64 {
self.try_playhead_frames().unwrap()
}
pub fn try_playhead_frames(&self) -> Option<u64> {
self.state.as_ref().map(|s| s.playhead_frames())
}
}
impl core::fmt::Debug for Sampler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SamplerAssignment")
.field("sampler", &self.sampler)
.finish_non_exhaustive()
}
}
impl Sampler {
fn on_insert_hook(mut world: DeferredWorld, context: HookContext) {
let sampler = world.get::<Sampler>(context.entity).unwrap().sampler;
if let Some(state) = world.get::<SamplerStateWrapper>(sampler).cloned() {
world.get_mut::<Sampler>(context.entity).unwrap().state = Some(state.0);
}
}
}
#[derive(Component)]
struct PoolShape(Vec<ComponentId>);
fn fetch_effect_ids(
effects: &[Entity],
lens: &mut QueryLens<&EffectId>,
) -> core::result::Result<Vec<ComponentId>, SeedlingError> {
let query = lens.query();
let mut effect_ids = Vec::new();
effect_ids.reserve_exact(effects.len());
for entity in effects {
let id = query
.get(*entity)
.map_err(|_| SeedlingError::MissingEffect {
empty_entity: *entity,
})?;
effect_ids.push(id.0);
}
Ok(effect_ids)
}
fn retrieve_state(
q: Query<(Entity, &FirewheelNode), (With<SamplerNode>, Without<SamplerStateWrapper>)>,
mut commands: Commands,
mut context: ResMut<AudioContext>,
) {
if q.iter().len() == 0 {
return;
}
context.with(|ctx| {
for (entity, node_id) in q.iter() {
let Some(state) = ctx.node_state::<SamplerState>(node_id.0) else {
continue;
};
commands
.entity(entity)
.insert(SamplerStateWrapper(state.clone()));
}
});
}
fn watch_sample_players(
mut q: Query<(&mut SamplerNode, &SamplerOf)>,
samples: Query<&PlaybackSettings>,
) {
for (mut sampler_node, sample) in q.iter_mut() {
let Ok(settings) = samples.get(sample.0) else {
continue;
};
sampler_node.playhead = settings.playhead.clone();
sampler_node.playback = settings.playback.clone();
sampler_node.speed = settings.speed;
}
}
fn spawn_chain(
bus: Entity,
config: Option<SamplerConfig>,
effects: &[Entity],
commands: &mut Commands,
) -> Entity {
let sampler = commands
.spawn((
SamplerNode::default(),
config.unwrap_or_default(),
PoolSamplerOf(bus),
))
.id();
let effects = effects.to_vec();
commands.queue(move |world: &mut World| -> Result {
let mut cloner = EntityCloner::build(world);
cloner.deny::<EffectOf>();
let mut cloner = cloner.finish();
let mut chain = Vec::new();
chain.reserve_exact(effects.len() + 1);
for effect in effects {
chain.push(cloner.spawn_clone(world, effect));
}
chain.push(bus);
world
.get_entity_mut(sampler)?
.add_children(&chain)
.entry::<PendingConnections>()
.or_default()
.into_mut()
.push(PendingEdge::new(chain[0], None));
for pair in chain.windows(2) {
world
.get_entity_mut(pair[0])?
.entry::<PendingConnections>()
.or_default()
.into_mut()
.push(PendingEdge::new(pair[1], None));
}
Ok(())
});
sampler
}
#[derive(Debug, Clone, Component)]
pub struct PoolSize(pub RangeInclusive<usize>);
#[derive(Debug, Clone, Resource)]
pub struct DefaultPoolSize(pub RangeInclusive<usize>);
impl Default for DefaultPoolSize {
fn default() -> Self {
Self(4..=32)
}
}
fn populate_pool(
q: Query<
(
Entity,
&SamplerConfig,
Option<&PoolSize>,
Option<&SampleEffects>,
Option<&EffectId>,
),
(
With<PoolLabelContainer>,
With<PoolMarker>,
Without<PoolSamplers>,
),
>,
mut effects: Query<&EffectId>,
default_pool_size: Res<DefaultPoolSize>,
mut commands: Commands,
) -> Result {
for (pool, config, size, pool_effects, effect_id) in &q {
if effect_id.is_none() {
commands.entity(pool).insert(VolumeNode::default());
}
let component_ids = fetch_effect_ids(
pool_effects.map(|e| e.deref()).unwrap_or(&[]),
&mut effects.as_query_lens(),
)?;
let size = size
.map(|p| p.0.clone())
.unwrap_or(default_pool_size.0.clone());
commands
.entity(pool)
.insert((PoolShape(component_ids), PoolSize(size.clone())));
let size = (*size.start()).max(1);
let config = config.clone();
for _ in 0..size {
spawn_chain(
pool,
Some(config.clone()),
pool_effects.map(|e| e.deref()).unwrap_or(&[]),
&mut commands,
);
}
}
Ok(())
}
#[derive(Debug, Event)]
pub struct PlaybackCompletionEvent;
fn remove_finished(
trigger: Trigger<PlaybackCompletionEvent>,
samples: Query<(&PlaybackSettings, &PoolLabelContainer)>,
mut commands: Commands,
) -> Result {
let sample_entity = trigger.target();
let (settings, container) = samples.get(sample_entity)?;
match settings.on_complete {
OnComplete::Preserve => {
commands
.entity(sample_entity)
.remove::<(Sampler, QueuedSample, SkipTimer)>();
}
OnComplete::Remove => {
commands
.entity(sample_entity)
.remove_by_id(container.label_id)
.remove_with_requires::<(
SampleEffects,
SamplePlayer,
PoolLabelContainer,
Sampler,
QueuedSample,
SkipTimer,
)>();
}
OnComplete::Despawn => {
commands.entity(sample_entity).despawn();
}
}
Ok(())
}
fn poll_finished(
nodes: Query<(&SamplerNode, &SamplerOf, &SamplerStateWrapper)>,
mut commands: Commands,
) {
for (node, active, state) in nodes.iter() {
let finished = state.0.finished() == node.sequence.id();
if finished {
commands.entity(active.0).trigger(PlaybackCompletionEvent);
}
}
}
#[derive(Debug)]
pub struct PoolDespawn<T>(T);
impl<T: PoolLabel + Component + Clone> PoolDespawn<T> {
pub fn new(label: T) -> Self {
Self(label)
}
}
impl<T: PoolLabel + Component + Clone> Command for PoolDespawn<T> {
fn apply(self, world: &mut World) {
let mut roots = world.query_filtered::<(Entity, &PoolLabelContainer), (
With<SamplerPool<T>>,
With<PoolSamplers>,
With<FirewheelNode>,
)>();
let roots: Vec<_> = roots
.iter(world)
.map(|(root, label)| (root, label.clone()))
.collect();
let mut commands = world.commands();
let interned = self.0.intern();
for (root, label) in roots {
if label.label == interned {
commands.entity(root).despawn();
}
}
}
}
pub trait PoolCommands {
fn despawn_pool<T: PoolLabel + Component + Clone>(&mut self, label: T);
}
impl PoolCommands for Commands<'_, '_> {
fn despawn_pool<T: PoolLabel + Component + Clone>(&mut self, label: T) {
self.queue(PoolDespawn::new(label));
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
prelude::LowPassNode,
sample_effects,
test::{prepare_app, run},
};
use bevy_seedling_macros::PoolLabel;
#[derive(PoolLabel, Clone, Debug, PartialEq, Eq, Hash)]
struct TestPool;
#[test]
fn test_spawn() {
let mut app = prepare_app(|mut commands: Commands| {
commands.spawn((
SamplerPool(TestPool),
sample_effects![LowPassNode::default()],
));
});
run(
&mut app,
|q: Query<&PoolSamplers, With<SamplerPool<TestPool>>>| {
assert_eq!(q.iter().len(), 1);
},
);
}
#[test]
fn test_despawn() {
let mut app = prepare_app(|mut commands: Commands| {
commands.spawn((
SamplerPool(TestPool),
PoolSize(4..=32),
sample_effects![LowPassNode::default()],
));
});
run(&mut app, |pool_nodes: Query<&FirewheelNode>| {
assert_eq!(pool_nodes.iter().count(), 11);
});
run(&mut app, |mut commands: Commands| {
commands.despawn_pool(TestPool);
});
app.update();
run(&mut app, |pool_nodes: Query<&FirewheelNode>| {
assert_eq!(pool_nodes.iter().count(), 2);
});
}
#[test]
fn test_playback_starts() {
let mut app = prepare_app(|mut commands: Commands, server: Res<AssetServer>| {
commands.spawn((
SamplerPool(TestPool),
sample_effects![LowPassNode::default()],
));
commands.spawn((
TestPool,
SamplePlayer::new(server.load("caw.ogg")).looping(),
EmptyComponent,
));
});
loop {
let players = run(
&mut app,
|q: Query<Entity, (With<SamplePlayer>, With<Sampler>)>| q.iter().len(),
);
if players == 1 {
break;
}
app.update();
}
}
#[derive(Component)]
struct EmptyComponent;
#[test]
fn test_remove_in_dynamic() {
let mut app = prepare_app(|mut commands: Commands, server: Res<AssetServer>| {
commands.spawn((
SamplePlayer::new(server.load("sine_440hz_1ms.wav")),
EmptyComponent,
PlaybackSettings {
on_complete: OnComplete::Remove,
..Default::default()
},
sample_effects![LowPassNode::default()],
));
});
loop {
let players = run(
&mut app,
|q: Query<Entity, (With<SamplePlayer>, With<EmptyComponent>)>| q.iter().len(),
);
if players == 0 {
break;
}
app.update();
}
let world = app.world_mut();
let mut q = world.query_filtered::<EntityRef, With<EmptyComponent>>();
let entity = q.single(world).unwrap();
let archetype = entity.archetype();
assert_eq!(archetype.components().count(), 1);
assert!(entity.contains::<EmptyComponent>());
}
#[test]
fn test_remove_in_pool() {
let mut app = prepare_app(|mut commands: Commands, server: Res<AssetServer>| {
commands.spawn((
SamplerPool(TestPool),
sample_effects![LowPassNode::default()],
));
commands.spawn((
TestPool,
SamplePlayer::new(server.load("sine_440hz_1ms.wav")),
EmptyComponent,
PlaybackSettings {
on_complete: OnComplete::Remove,
..Default::default()
},
));
});
loop {
let players = run(
&mut app,
|q: Query<Entity, (With<SamplePlayer>, With<EmptyComponent>)>| q.iter().len(),
);
if players == 0 {
break;
}
app.update();
}
let world = app.world_mut();
let mut q = world.query_filtered::<EntityRef, With<EmptyComponent>>();
let entity = q.single(world).unwrap();
let archetype = entity.archetype();
assert_eq!(archetype.components().count(), 1);
assert!(entity.contains::<EmptyComponent>());
}
}