use bevy_ecs::prelude::{Resource, World};
use super::{EventQueue, SoundEvent};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BeatClock {
pub bpm: u32,
pub start_tick: u64,
pub ticks_per_minute: u64,
}
impl BeatClock {
#[must_use]
pub fn new(
bpm: u32,
start_tick: u64,
fixed_delta: std::time::Duration,
) -> Self {
Self {
bpm: bpm.max(1),
start_tick,
ticks_per_minute: (60.0 / fixed_delta.as_secs_f64()).round() as u64,
}
}
#[must_use]
pub fn tick_of(&self, beat: u64) -> u64 {
let bpm = u64::from(self.bpm);
self.start_tick + (beat * self.ticks_per_minute).div_ceil(bpm)
}
#[must_use]
pub fn beat_at(&self, tick: u64) -> Option<u64> {
let since = tick.checked_sub(self.start_tick)?;
Some(since * u64::from(self.bpm) / self.ticks_per_minute)
}
#[must_use]
pub fn is_beat(&self, tick: u64) -> bool {
self.beat_at(tick)
.is_some_and(|beat| self.tick_of(beat) == tick)
}
#[must_use]
pub fn ticks_to_next(&self, tick: u64) -> u64 {
let next = self.beat_at(tick).map_or(0, |beat| beat + 1);
self.tick_of(next) - tick
}
#[must_use]
pub fn phase(&self, tick: u64) -> f64 {
let Some(since) = tick.checked_sub(self.start_tick) else {
return 0.0;
};
let scaled = since * u64::from(self.bpm);
(scaled % self.ticks_per_minute) as f64 / self.ticks_per_minute as f64
}
#[must_use]
pub fn beats_between(&self, after: u64, upto: u64) -> std::ops::Range<u64> {
let first = self.beat_at(after).map_or(0, |beat| beat + 1);
let end = self.beat_at(upto).map_or(0, |beat| beat + 1);
first..end.max(first)
}
}
pub const QUEUE_LIMIT: usize = 1024;
#[derive(Clone, Debug, PartialEq)]
pub struct Sound {
pub volume: f32,
pub pan: f32,
pub bus: String,
pub looped: bool,
pub at_tick: Option<u64>,
pub position: Option<[f32; 3]>,
}
impl Default for Sound {
fn default() -> Self {
Self {
volume: 1.0,
pan: 0.0,
bus: String::new(),
looped: false,
at_tick: None,
position: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SoundId(pub u64);
#[derive(Clone, Debug, PartialEq)]
pub enum AudioCommand {
Play {
id: SoundId,
clip: String,
volume: f32,
looped: bool,
pan: f32,
bus: String,
tick: u64,
},
Stop(SoundId),
SetVolume {
id: SoundId,
volume: f32,
fade: f32,
},
SetBusVolume {
bus: String,
volume: f32,
fade: f32,
},
StopAll,
SetMasterVolume(f32),
}
#[derive(Resource, Clone, Debug, Default)]
pub struct AudioQueue {
commands: Vec<AudioCommand>,
next_id: u64,
requested: u64,
clips: std::collections::BTreeMap<String, u64>,
}
impl AudioQueue {
pub fn play(&mut self, clip: &str, sound: &Sound, tick: u64) -> SoundId {
let id = SoundId(self.next_id);
self.next_id += 1;
self.requested += 1;
*self.clips.entry(clip.to_owned()).or_default() += 1;
self.push(AudioCommand::Play {
id,
clip: clip.to_string(),
volume: sound.volume,
looped: sound.looped,
pan: sound.pan.clamp(-1.0, 1.0),
bus: sound.bus.clone(),
tick: sound.at_tick.unwrap_or(tick),
});
id
}
fn push(&mut self, command: AudioCommand) {
if self.commands.len() >= QUEUE_LIMIT {
self.commands.remove(0);
}
self.commands.push(command);
}
pub fn stop(&mut self, id: SoundId) {
self.push(AudioCommand::Stop(id));
}
pub fn stop_all(&mut self) {
self.push(AudioCommand::StopAll);
}
pub fn set_master_volume(&mut self, volume: f32) {
self.push(AudioCommand::SetMasterVolume(volume));
}
pub fn set_volume(&mut self, id: SoundId, volume: f32, fade: f32) {
self.push(AudioCommand::SetVolume { id, volume, fade });
}
pub fn set_bus_volume(&mut self, bus: &str, volume: f32, fade: f32) {
self.push(AudioCommand::SetBusVolume {
bus: bus.to_owned(),
volume,
fade,
});
}
#[must_use]
pub fn requested(&self) -> u64 {
self.requested
}
#[must_use]
pub fn requested_clips(&self) -> &std::collections::BTreeMap<String, u64> {
&self.clips
}
pub fn drain(&mut self) -> Vec<AudioCommand> {
std::mem::take(&mut self.commands)
}
}
pub fn route_sound_events(world: &mut World) {
let tick = world
.get_resource::<crate::runtime::FrameTime>()
.map_or(0, |time| time.fixed_tick);
let clips: Vec<(String, f32)> = world
.get_resource::<EventQueue<SoundEvent>>()
.map(|events| {
events.iter().map(|e| (e.clip.clone(), e.volume)).collect()
})
.unwrap_or_default();
if let Some(mut queue) = world.get_resource_mut::<AudioQueue>() {
for (clip, volume) in clips {
let sound = Sound {
volume,
..Sound::default()
};
queue.play(&clip, &sound, tick);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn beat_clock_lands_on_whole_ticks() {
let step = std::time::Duration::from_secs_f64(1.0 / 60.0);
let clock = BeatClock::new(128, 2, step);
let ticks: Vec<_> = (0..5).map(|beat| clock.tick_of(beat)).collect();
assert_eq!(ticks, [2, 31, 59, 87, 115]);
assert_eq!(clock.beat_at(1), None);
assert_eq!(clock.beat_at(58), Some(1));
assert!(clock.is_beat(59) && !clock.is_beat(60));
assert_eq!(clock.ticks_to_next(59), 28);
assert_eq!(clock.ticks_to_next(0), 2);
assert_eq!(clock.beats_between(30, 90), 1..4);
assert_eq!(clock.beats_between(0, 1), 0..0);
let even = BeatClock::new(120, 0, step);
assert_eq!(even.tick_of(3), 90);
assert_eq!(even.phase(45), 0.5);
}
#[test]
fn requests_get_distinct_ids_and_drain_once() {
let mut queue = AudioQueue::default();
let a = queue.play("sfx/a.wav", &Sound::default(), 0);
let b = queue.play("sfx/b.wav", &Sound::default(), 0);
queue.stop(a);
assert_ne!(a, b);
assert_eq!(queue.requested(), 2);
assert_eq!(queue.requested_clips()["sfx/b.wav"], 1);
assert_eq!(queue.drain().len(), 3);
assert!(queue.drain().is_empty());
for _ in 0..QUEUE_LIMIT + 10 {
queue.set_master_volume(1.0);
}
assert_eq!(queue.drain().len(), QUEUE_LIMIT);
assert_eq!(queue.requested(), 2);
}
}