use crate::{
prelude::{AudioEvents, Volume},
time::Audio,
};
use bevy_asset::Handle;
use bevy_ecs::prelude::*;
use bevy_math::FloatExt;
use firewheel::{
clock::{DurationSeconds, InstantSeconds},
diff::Notify,
nodes::sampler::{PlayFrom, RepeatMode},
};
use std::time::Duration;
mod assets;
pub use assets::{AudioSample, SampleLoader, SampleLoaderError};
#[derive(Debug, Component, Clone)]
#[component(immutable)]
#[require(PlaybackSettings, SamplePriority, SampleQueueLifetime, QueuedSample)]
#[cfg_attr(feature = "entity_names", require(Name::new("SamplePlayer")))]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct SamplePlayer {
pub sample: Handle<AudioSample>,
pub repeat_mode: RepeatMode,
pub volume: Volume,
}
impl Default for SamplePlayer {
fn default() -> Self {
Self {
sample: Default::default(),
repeat_mode: RepeatMode::PlayOnce,
volume: Volume::UNITY_GAIN,
}
}
}
impl SamplePlayer {
pub fn new(handle: Handle<AudioSample>) -> Self {
Self {
sample: handle,
..Default::default()
}
}
pub fn looping(self) -> Self {
Self {
repeat_mode: RepeatMode::RepeatEndlessly,
..self
}
}
pub fn with_volume(self, volume: Volume) -> Self {
Self { volume, ..self }
}
}
pub(super) fn observe_player_insert(
player: On<Insert, SamplePlayer>,
time: Res<bevy_time::Time<Audio>>,
mut commands: Commands,
) {
commands
.entity(player.event_target())
.remove::<crate::pool::Sampler>()
.insert_if_new(AudioEvents::new(&time));
}
#[derive(Debug, Default, Component, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[component(immutable)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct SamplePriority(pub i32);
#[derive(Debug, Component, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[component(immutable)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct SampleQueueLifetime(pub Duration);
impl Default for SampleQueueLifetime {
fn default() -> Self {
Self(Duration::from_millis(100))
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub enum OnComplete {
Preserve,
Remove,
#[default]
Despawn,
}
#[derive(Component, Debug, Clone)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct PlaybackSettings {
pub play: Notify<bool>,
pub play_from: PlayFrom,
pub speed: f64,
pub on_complete: OnComplete,
}
impl PlaybackSettings {
pub fn with_playback(self, play: bool) -> Self {
Self {
play: Notify::new(play),
..self
}
}
pub fn with_play_from(self, play_from: PlayFrom) -> Self {
Self { play_from, ..self }
}
pub fn with_speed(self, speed: f64) -> Self {
Self { speed, ..self }
}
pub fn with_on_complete(self, on_complete: OnComplete) -> Self {
Self {
on_complete,
..self
}
}
pub fn preserve(self) -> Self {
Self {
on_complete: OnComplete::Preserve,
..self
}
}
pub fn remove(self) -> Self {
Self {
on_complete: OnComplete::Remove,
..self
}
}
pub fn despawn(self) -> Self {
Self {
on_complete: OnComplete::Despawn,
..self
}
}
pub fn play_at(
&self,
play_from: Option<PlayFrom>,
time: InstantSeconds,
events: &mut AudioEvents,
) {
events.schedule(time, self, |settings| {
*settings.play = true;
if let Some(play_from) = play_from {
settings.play_from = play_from;
}
});
}
pub fn pause_at(&self, time: InstantSeconds, events: &mut AudioEvents) {
events.schedule(time, self, |settings| {
*settings.play = false;
});
}
pub fn speed_to(&self, speed: f64, duration: DurationSeconds, events: &mut AudioEvents) {
self.speed_at(speed, events.now(), events.now() + duration, events)
}
pub fn speed_at(
&self,
speed: f64,
start: InstantSeconds,
end: InstantSeconds,
events: &mut AudioEvents,
) {
let start_value = events.get_value_at(start, self);
let mut end_value = start_value.clone();
end_value.speed = speed;
let pitch_span = (end_value.speed - start_value.speed).abs();
let total_events = (pitch_span / 0.001).max(1.0) as usize;
let total_events =
crate::node::events::max_event_rate(end.0 - start.0, 0.001).min(total_events);
events.schedule_tween(
start,
end,
start_value,
end_value,
total_events,
|a, b, t| {
let mut output = a.clone();
output.speed = a.speed.lerp(b.speed, t as f64);
output
},
);
}
pub fn play(&mut self) {
*self.play = true;
}
pub fn pause(&mut self) {
*self.play = false;
}
}
impl Default for PlaybackSettings {
fn default() -> Self {
Self {
play: Notify::new(true),
play_from: PlayFrom::Resume,
speed: 1.0,
on_complete: OnComplete::Despawn,
}
}
}
impl firewheel::diff::Diff for PlaybackSettings {
fn diff<E: firewheel::diff::EventQueue>(
&self,
baseline: &Self,
path: firewheel::diff::PathBuilder,
event_queue: &mut E,
) {
self.play.diff(&baseline.play, path.with(2), event_queue);
self.play_from
.diff(&baseline.play_from, path.with(3), event_queue);
self.speed.diff(&baseline.speed, path.with(5), event_queue);
}
}
impl firewheel::diff::Patch for PlaybackSettings {
type Patch = firewheel::nodes::sampler::SamplerNodePatch;
fn patch(
data: &firewheel::event::ParamData,
path: &[u32],
) -> std::result::Result<Self::Patch, firewheel::diff::PatchError> {
firewheel::nodes::sampler::SamplerNode::patch(data, path)
}
fn apply(&mut self, patch: Self::Patch) {
match patch {
firewheel::nodes::sampler::SamplerNodePatch::Play(p) => self.play = p,
firewheel::nodes::sampler::SamplerNodePatch::PlayFrom(p) => self.play_from = p,
firewheel::nodes::sampler::SamplerNodePatch::Speed(s) => self.speed = s,
_ => {}
}
}
}
#[derive(Debug, Component, Default)]
#[component(storage = "SparseSet")]
pub struct QueuedSample;
#[cfg(feature = "rand")]
pub use random::{PitchRngSource, RandomPitch};
#[cfg(feature = "rand")]
pub(crate) use random::RandomPlugin;
#[cfg(feature = "rand")]
mod random {
use crate::SeedlingSystems;
use super::PlaybackSettings;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use rand::{SeedableRng, rngs::SmallRng};
pub struct RandomPlugin;
impl Plugin for RandomPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(PitchRngSource::new(SmallRng::from_os_rng()))
.add_systems(Last, RandomPitch::apply.before(SeedlingSystems::Acquire));
}
}
trait PitchRng {
fn gen_pitch(&mut self, range: std::ops::Range<f64>) -> f64;
}
struct RandRng<T>(T);
impl<T: rand::Rng> PitchRng for RandRng<T> {
fn gen_pitch(&mut self, range: std::ops::Range<f64>) -> f64 {
self.0.random_range(range)
}
}
#[derive(Resource)]
pub struct PitchRngSource(Box<dyn PitchRng + Send + Sync>);
impl core::fmt::Debug for PitchRngSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("PitchRngSource").finish_non_exhaustive()
}
}
impl PitchRngSource {
pub fn new<T: rand::Rng + Send + Sync + 'static>(rng: T) -> Self {
Self(Box::new(RandRng(rng)))
}
}
#[derive(Debug, Component, Default, Clone)]
#[require(PlaybackSettings)]
#[component(immutable)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct RandomPitch(pub core::ops::Range<f64>);
impl RandomPitch {
pub fn new(deviation: f64) -> Self {
let minimum = (1.0 - deviation).clamp(0.0, f64::MAX);
let maximum = (1.0 + deviation).clamp(0.0, f64::MAX);
Self(minimum..maximum)
}
fn apply(
mut samples: Query<(Entity, &mut PlaybackSettings, &Self)>,
mut commands: Commands,
mut rng: ResMut<PitchRngSource>,
) {
for (entity, mut settings, range) in samples.iter_mut() {
let speed = if range.0.is_empty() {
range.0.start
} else {
rng.0.gen_pitch(range.0.clone())
};
settings.speed = speed;
commands.entity(entity).remove::<Self>();
}
}
}
}
#[cfg(test)]
mod test {
use crate::pool::Sampler;
use crate::prelude::*;
use crate::test::{prepare_app, run};
use bevy::prelude::*;
#[test]
fn test_reinsertion() {
let mut app = prepare_app(|mut commands: Commands| {
commands.spawn((SamplerPool(DefaultPool), PoolSize(1..=1)));
commands
.spawn((VolumeNode::default(), MainBus))
.connect(AudioGraphOutput);
});
run(
&mut app,
|mut commands: Commands, server: Res<AssetServer>| {
commands.spawn(SamplePlayer::new(server.load("caw.ogg")));
},
);
loop {
let world = app.world_mut();
let mut q = world.query_filtered::<Entity, With<Sampler>>();
if q.iter(world).len() != 0 {
break;
}
app.update();
}
run(
&mut app,
|target: Single<Entity, With<Sampler>>,
mut commands: Commands,
server: Res<AssetServer>| {
commands
.entity(*target)
.insert(SamplePlayer::new(server.load("caw.ogg")));
},
);
app.update();
}
}