use core::time::Duration;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
pub use build::{NoSounds, SoundCue, Sounds};
pub use data::SoundData;
pub use mixer::MAX_VOICES;
pub(crate) use data::{Channels, ClipFrame, Encoded, SampleRate};
pub(crate) use output::{MixRate, Output};
pub(crate) use resample::Resampler;
use build::Knobs;
use data::{Body, Clip, Source};
use mixer::{Command, Declared, LiveKnobs, SoundId, Sustained, Voicing, Window, levels};
use crate::assets::ogg;
use crate::{Assets, View};
pub(crate) struct Audio<S: Sounds> {
bank: Bank<S>,
output: Output,
declared: Declared,
submitted: Vec<Submission>,
commands: Vec<Command>,
listener: Option<View>,
volume: f32,
}
impl<S: Sounds> Audio<S> {
pub(crate) fn new(assets: Rc<Assets>, output: Output) -> Self {
Self {
bank: Bank {
assets,
rate: output.rate(),
ids: HashMap::new(),
clips: Vec::new(),
},
output,
declared: Declared::default(),
submitted: Vec::new(),
commands: Vec::new(),
listener: None,
volume: 1.0,
}
}
pub(crate) fn build_catalog(&mut self) {
for sound in S::catalog() {
self.bank.id_of(&sound);
}
}
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 unlock(&mut self) {
self.output.unlock();
}
pub(crate) fn unlocked(&self) -> bool {
self.output.unlocked()
}
pub(crate) fn duration(&mut self, sound: &S) -> Duration {
let id = self.bank.id_of(sound);
let clip = &self.bank.clips[id.0 as usize];
clip.rate.duration_of(clip.frames)
}
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) {
let listener = self.listener.take().unwrap_or(camera);
self.commands.push(Command::Volume(self.volume));
self.volume = 1.0;
for submission in self.submitted.drain(..) {
let clip = &self.bank.clips[submission.sound.0 as usize];
let Some(window) = Window::of(clip, &submission.knobs, submission.sustained.is_some())
else {
continue;
};
let voicing = Voicing {
sound: submission.sound,
clip: Arc::clone(clip),
window,
live: LiveKnobs {
levels: levels(&submission.knobs, &listener),
pitch: submission.knobs.pitch,
fade: submission.knobs.fade,
glide: submission.knobs.glide,
},
};
match submission.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 submit(&mut self, cue: SoundCue<S>, sustained: bool) {
let (sound, knobs) = cue.split();
let sound = self.bank.id_of(&sound);
self.submitted.push(Submission {
sound,
sustained: Sustained::of(sound, &knobs, sustained),
knobs,
});
}
}
struct Submission {
sound: SoundId,
knobs: Knobs,
sustained: Option<Sustained>,
}
struct Bank<S: Sounds> {
assets: Rc<Assets>,
rate: Option<MixRate>,
ids: HashMap<S, SoundId>,
clips: Vec<Arc<Clip>>,
}
impl<S: Sounds> Bank<S> {
fn id_of(&mut self, sound: &S) -> SoundId {
if let Some(&id) = self.ids.get(sound) {
return id;
}
let id = SoundId(self.clips.len() as u32);
self.clips
.push(Arc::new(resolve(&sound.build(&self.assets), self.rate)));
self.ids.insert(sound.clone(), id);
id
}
}
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;
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]),
}
}
}
fn bank() -> Bank<Cry> {
Bank {
assets: Rc::new(Assets::default()),
rate: None,
ids: HashMap::new(),
clips: Vec::new(),
}
}
#[test]
fn a_value_builds_once_and_answers_with_the_same_sound() {
let mut bank = bank();
let first = bank.id_of(&Cry::Miss);
assert_eq!(bank.id_of(&Cry::Miss), first, "and never builds again");
assert_ne!(bank.id_of(&Cry::Hit), first);
assert_eq!(bank.clips.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 audio = Audio::<Music>::new(Rc::new(assets), Output::silent());
audio.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!(audio.duration(&Music::Whole), decoded);
assert_eq!(
audio.duration(&Music::Streamed),
decoded,
"however the clip is decoded"
);
assert_eq!(
audio.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 audio = Audio::<Music>::new(Rc::new(assets), Output::silent());
audio.build_catalog();
let durations = audio.durations();
assert_eq!(durations.len(), Music::catalog().len());
for value in Music::catalog() {
assert_eq!(durations.get(&value), Some(&audio.duration(&value)));
}
}
#[test]
fn the_desktop_allows_sound_from_boot_with_no_device_at_all() {
let audio = Audio::<Cry>::new(Rc::new(Assets::default()), Output::silent());
assert!(audio.unlocked());
}
#[test]
fn the_catalog_run_names_every_asset_it_could_not_find() {
let assets = Rc::new(Assets::default());
let mut audio = Audio::<Cry>::new(Rc::clone(&assets), Output::silent());
audio.build_catalog();
let error = assets.unresolved().expect("the pull missed");
assert_eq!(
error.to_string(),
"the game's assets did not resolve: no asset is named `hit`"
);
}
}