use super::{Ctx, Playing, Sound, SoundParams};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(transparent)] pub struct AudioBus(pub(super) usize);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AudioEffects {
pub low_pass_hz: f32,
pub reverb: f32,
}
impl Default for AudioEffects {
fn default() -> Self {
Self {
low_pass_hz: 20_000.0,
reverb: 0.0,
}
}
}
impl AudioEffects {
pub(super) fn validated(self) -> Self {
Self {
low_pass_hz: finite(self.low_pass_hz, 20_000.0).clamp(20.0, 20_000.0),
reverb: finite(self.reverb, 0.0).clamp(0.0, 1.0),
}
}
}
pub(super) fn finite(value: f32, fallback: f32) -> f32 {
if value.is_finite() { value } else { fallback }
}
#[cfg(feature = "audio")]
pub(super) fn volume_db(value: f32) -> kira::Decibels {
let value = finite(value, 0.0).max(0.0);
if value <= 0.001 {
kira::Decibels::SILENCE
} else {
kira::Decibels(20.0 * value.log10())
}
}
pub(super) struct BusState {
name: String,
volume: f32,
muted: bool,
#[cfg(feature = "audio")]
pub(super) track: Option<kira::track::TrackHandle>,
}
impl BusState {
#[cfg(feature = "audio")]
pub(super) fn gain(&self) -> f32 {
if self.muted { 0.0 } else { self.volume }
}
}
#[derive(Default)]
pub(super) struct BusRegistry {
pub(super) entries: Vec<BusState>,
}
impl BusRegistry {
pub(super) fn ensure_default(&mut self) {
if self.entries.is_empty() {
self.insert("sfx");
}
}
fn insert(&mut self, name: &str) -> AudioBus {
let id = AudioBus(self.entries.len());
self.entries.push(BusState {
name: name.into(),
volume: 1.0,
muted: false,
#[cfg(feature = "audio")]
track: None,
});
id
}
fn named(&mut self, name: &str) -> AudioBus {
self.ensure_default();
if let Some(index) = self.entries.iter().position(|bus| bus.name == name) {
AudioBus(index)
} else {
self.insert(name)
}
}
}
impl Ctx {
pub fn audio_bus(&mut self, name: &str) -> AudioBus {
self.audio.buses.named(name)
}
pub fn set_audio_bus_volume(&mut self, bus: AudioBus, volume: f32) {
self.audio.buses.ensure_default();
if let Some(state) = self.audio.buses.entries.get_mut(bus.0) {
state.volume = finite(volume, 0.0).max(0.0);
}
self.audio.bus_changed(bus);
}
pub fn mute_audio_bus(&mut self, bus: AudioBus, muted: bool) {
self.audio.buses.ensure_default();
if let Some(state) = self.audio.buses.entries.get_mut(bus.0) {
state.muted = muted;
}
self.audio.bus_changed(bus);
}
pub fn play_sound_on(
&mut self,
sound: &Sound,
params: SoundParams,
bus: AudioBus,
effects: AudioEffects,
) -> Playing {
if self.headless {
return Playing(0);
}
Playing(self.audio.play_on(sound.id, params, bus, effects.validated()))
}
pub fn set_sound_effects(&mut self, playing: Playing, effects: AudioEffects) {
self.audio.effects(playing.0, effects.validated());
}
pub fn load_stream(&mut self, path: &str) -> Sound {
let file = crate::assets::resolve(path).unwrap_or_else(|e| panic!("ponko: {e}"));
let (id, seconds) = self.audio.stream(file).unwrap_or_else(|e| panic!("ponko: {path}: {e}"));
Sound { id, seconds }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audio_effects_validate_nonfinite_and_out_of_range_parameters() {
assert_eq!(
AudioEffects {
low_pass_hz: f32::NAN,
reverb: f32::INFINITY
}
.validated(),
AudioEffects::default()
);
assert_eq!(
AudioEffects {
low_pass_hz: -100.0,
reverb: 2.0
}
.validated(),
AudioEffects {
low_pass_hz: 20.0,
reverb: 1.0
}
);
}
#[test]
fn audio_bus_lookup_reuses_names_and_reserves_default() {
let mut buses = BusRegistry::default();
let music = buses.named("music");
assert_ne!(music, AudioBus::default());
assert_eq!(music, buses.named("music"));
assert_eq!(buses.named("sfx"), AudioBus::default());
assert_eq!(buses.entries.len(), 2);
}
#[test]
fn audio_headless_buses_and_effects_never_open_device() {
crate::Game::new("audio", 1, 1).run_headless(0, |ctx| {
let music = ctx.audio_bus("music");
ctx.set_audio_bus_volume(music, 0.4);
ctx.mute_audio_bus(music, true);
ctx.mute_audio_bus(music, false);
assert_eq!(ctx.audio.buses.entries[music.0].volume, 0.4);
assert!(!ctx.audio.buses.entries[music.0].muted);
let sound = ctx.create_sound(100, &[0.0; 100]);
let voice = ctx.play_sound_on(&sound, SoundParams::default(), music, AudioEffects::default());
ctx.set_sound_effects(
voice,
AudioEffects {
low_pass_hz: 600.0,
reverb: 0.5,
},
);
ctx.stop_sound(voice);
});
}
#[cfg(feature = "audio")]
#[test]
fn audio_volume_mapping_is_linear_gain_to_decibels() {
assert_eq!(volume_db(1.0), kira::Decibels(0.0));
assert!((volume_db(0.5).0 + 6.0206).abs() < 0.001);
assert_eq!(volume_db(f32::NAN), kira::Decibels::SILENCE);
}
}