use core::time::Duration;
use std::collections::HashMap;
use std::sync::Arc;
pub use build::{NoSounds, SoundCue, Sounds};
pub use data::SoundData;
pub use mixer::MAX_VOICES;
pub(crate) use build::Knobs;
pub(crate) use data::{Channels, Clip, ClipFrame, Encoded, SampleRate};
pub(crate) use output::{MixRate, Output};
pub(crate) use resample::Resampler;
use data::{Body, Source};
use mixer::{Command, Declared, LiveKnobs, SoundId, Sustained, Voicing, Window, levels};
use crate::assets::{Unresolved, ogg};
use crate::{Assets, View};
pub(crate) struct Sounding<S: Sounds> {
assets: Arc<Assets>,
rate: Option<MixRate>,
ids: HashMap<S, SoundId>,
durations: Vec<Duration>,
unhanded: Vec<(SoundId, Arc<Clip>)>,
submitted: Vec<SoundCommand>,
listener: Option<View>,
volume: f32,
}
impl<S: Sounds> Sounding<S> {
pub(crate) fn new(assets: Arc<Assets>, rate: Option<MixRate>) -> Self {
Self {
assets,
rate,
ids: HashMap::new(),
durations: Vec::new(),
unhanded: Vec::new(),
submitted: Vec::new(),
listener: None,
volume: 1.0,
}
}
pub(crate) fn build_catalog(&mut self) -> Unresolved {
let mut unresolved = Unresolved::default();
for sound in S::catalog() {
unresolved.record(self.built(&sound).1);
}
unresolved
}
pub(crate) fn play(&mut self, cue: SoundCue<S>) {
self.submit(cue, false);
}
pub(crate) fn sustain(&mut self, cue: SoundCue<S>) {
self.submit(cue, true);
}
pub(crate) fn set_listener(&mut self, view: View) {
self.listener = Some(view);
}
pub(crate) fn set_volume(&mut self, volume: f32) {
self.volume = volume.max(0.0);
}
pub(crate) fn duration(&mut self, sound: &S) -> Duration {
let id = self.id_of(sound);
self.durations[id.0 as usize]
}
pub(crate) fn durations(&mut self) -> HashMap<S, Duration> {
S::catalog()
.into_iter()
.map(|sound| {
let duration = self.duration(&sound);
(sound, duration)
})
.collect()
}
pub(crate) fn flush(&mut self, camera: View) -> Played {
Played {
bank: core::mem::take(&mut self.unhanded),
sounds: core::mem::take(&mut self.submitted),
listener: self.listener.take().unwrap_or(camera),
volume: core::mem::replace(&mut self.volume, 1.0),
}
}
fn id_of(&mut self, sound: &S) -> SoundId {
let (id, unresolved) = self.built(sound);
unresolved.logged();
id
}
fn built(&mut self, sound: &S) -> (SoundId, Unresolved) {
if let Some(&id) = self.ids.get(sound) {
return (id, Unresolved::default());
}
let mut data = sound.build(&self.assets);
let unresolved = data.take_unresolved();
let id = SoundId(self.durations.len() as u32);
let clip = resolve(&data, self.rate);
self.durations.push(clip.rate.duration_of(clip.frames));
self.unhanded.push((id, Arc::new(clip)));
self.ids.insert(sound.clone(), id);
(id, unresolved)
}
fn submit(&mut self, cue: SoundCue<S>, sustained: bool) {
let (sound, knobs) = cue.split();
let sound = self.id_of(&sound);
self.submitted.push(SoundCommand {
sound,
sustained: Sustained::of(sound, &knobs, sustained),
knobs,
});
}
}
pub(crate) struct SoundCommand {
sound: SoundId,
knobs: Knobs,
sustained: Option<Sustained>,
}
pub(crate) struct Played {
bank: Vec<(SoundId, Arc<Clip>)>,
sounds: Vec<SoundCommand>,
listener: View,
volume: f32,
}
pub(crate) struct SoundOutput {
clips: Vec<Arc<Clip>>,
output: Output,
declared: Declared,
commands: Vec<Command>,
}
impl SoundOutput {
pub(crate) fn new(output: Output) -> Self {
Self {
clips: Vec::new(),
output,
declared: Declared::default(),
commands: Vec::new(),
}
}
pub(crate) fn rate(&self) -> Option<MixRate> {
self.output.rate()
}
pub(crate) fn unlock(&mut self) {
self.output.unlock();
}
pub(crate) fn unlocked(&self) -> bool {
self.output.unlocked()
}
pub(crate) fn play(&mut self, played: Played) {
self.clips
.extend(played.bank.into_iter().map(|(_, clip)| clip));
let listener = played.listener;
self.commands.push(Command::Volume(played.volume));
for command in played.sounds {
let Some(clip) = self.clips.get(command.sound.0 as usize) else {
continue;
};
let Some(window) = Window::of(clip, &command.knobs, command.sustained.is_some()) else {
continue;
};
let voicing = Voicing {
sound: command.sound,
clip: Arc::clone(clip),
window,
live: LiveKnobs {
levels: levels(&command.knobs, &listener),
pitch: command.knobs.pitch,
fade: command.knobs.fade,
glide: command.knobs.glide,
},
};
match command.sustained {
Some(sustained) => self.declared.declare(sustained, voicing),
None => self.commands.push(Command::Play(voicing)),
}
}
self.commands.push(Command::Sustained(self.declared.take()));
self.output.frame(self.commands.drain(..));
}
}
fn resolve(data: &SoundData, rate: Option<MixRate>) -> Clip {
let rate = rate.map_or(data.rate(), MixRate::get);
let channels = data.channels();
let body = match data.source() {
Source::Samples(samples) => {
Body::Samples(Resampler::whole(&samples.values, samples.rate, rate, channels).into())
}
Source::Streamed(clip) => Body::Encoded(Arc::clone(clip)),
Source::Resident(clip) => Body::Samples(
ogg::samples(Arc::clone(clip), rate)
.inspect_err(|error| log::debug!("{error}"))
.unwrap_or_default()
.into(),
),
};
let frames = match &body {
Body::Samples(samples) => samples.len() as u64 / channels.count() as u64,
Body::Encoded(clip) => clip.rate().frames_at(clip.frames(), rate),
};
Clip {
rate,
channels,
frames,
body,
}
}
mod build;
mod data;
pub(crate) mod mixer;
mod output;
mod resample;
#[cfg(not(target_arch = "wasm32"))]
mod voices;
#[cfg(any(test, target_arch = "wasm32"))]
mod schedule;
#[cfg(test)]
mod tests {
use super::*;
use crate::Catalog;
#[derive(Clone, Eq, Hash, PartialEq)]
enum Cry {
Hit,
Miss,
}
impl Catalog for Cry {
fn catalog() -> Vec<Self> {
vec![Self::Hit, Self::Miss]
}
}
impl Sounds for Cry {
fn build(&self, assets: &Assets) -> SoundData {
match self {
Self::Hit => assets.sound("hit"),
Self::Miss => SoundData::mono(8, vec![0.0; 8]),
}
}
}
#[derive(Clone, Eq, Hash, PartialEq)]
enum Music {
Whole,
Streamed,
Computed,
}
impl Catalog for Music {
fn catalog() -> Vec<Self> {
vec![Self::Whole, Self::Streamed, Self::Computed]
}
}
impl Sounds for Music {
fn build(&self, assets: &Assets) -> SoundData {
match self {
Self::Whole => assets.sound("theme"),
Self::Streamed => assets.sound("theme").streamed(),
Self::Computed => SoundData::mono(8, vec![0.0; 12]),
}
}
}
#[test]
fn a_value_builds_once_and_answers_with_the_same_sound() {
let mut sounding = Sounding::<Cry>::new(Arc::new(Assets::default()), None);
let first = sounding.id_of(&Cry::Miss);
assert_eq!(sounding.id_of(&Cry::Miss), first, "and never builds again");
assert_ne!(sounding.id_of(&Cry::Hit), first);
assert_eq!(sounding.unhanded.len(), 2);
}
#[test]
fn a_sound_lasts_as_long_as_the_clip_the_catalog_run_decoded() {
let assets = Assets::load([crate::assets::file("audio/theme.ogg", crate::assets::SWEEP)])
.expect("the fixture decodes");
let mut sounding = Sounding::<Music>::new(Arc::new(assets), None);
sounding.build_catalog();
let loaded = Arc::new(ogg::decode(crate::assets::SWEEP).expect("the fixture decodes"));
let frames = ogg::samples(Arc::clone(&loaded), loaded.rate())
.expect("it decodes")
.len() as u64
/ loaded.channels().count() as u64;
let decoded = Duration::from_secs_f64(frames as f64 / f64::from(loaded.rate()));
assert_eq!(sounding.duration(&Music::Whole), decoded);
assert_eq!(
sounding.duration(&Music::Streamed),
decoded,
"however the clip is decoded"
);
assert_eq!(
sounding.duration(&Music::Computed),
Duration::from_millis(1_500),
"and computed samples last as long as they take to play"
);
}
#[test]
fn one_read_reports_the_length_of_every_value_the_vocabulary_catalogs() {
let assets = Assets::load([crate::assets::file("audio/theme.ogg", crate::assets::SWEEP)])
.expect("the fixture decodes");
let mut sounding = Sounding::<Music>::new(Arc::new(assets), None);
assert!(sounding.build_catalog().is_empty(), "every name resolved");
let durations = sounding.durations();
assert_eq!(durations.len(), Music::catalog().len());
for value in Music::catalog() {
assert_eq!(durations.get(&value), Some(&sounding.duration(&value)));
}
}
#[test]
fn the_desktop_allows_sound_from_boot_with_no_device_at_all() {
let output = SoundOutput::new(Output::silent());
assert!(output.unlocked());
}
#[test]
fn the_catalog_run_names_every_asset_it_could_not_find() {
let mut sounding = Sounding::<Cry>::new(Arc::new(Assets::default()), None);
let error = sounding.build_catalog().error().expect("the pull missed");
assert_eq!(
error.to_string(),
"the game's assets did not resolve: no asset is named `hit`"
);
}
}