mod wav;
use cranpose_core::compositionLocalOfWithPolicy;
use cranpose_core::CompositionLocal;
use cranpose_core::CompositionLocalProvider;
use cranpose_macros::composable;
use std::cell::Cell;
use std::cell::RefCell;
use std::fmt;
use std::ops::Index;
use std::rc::Rc;
use std::sync::Arc;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct SoundId(u32);
impl SoundId {
pub const NONE: SoundId = SoundId(0);
pub fn from_raw(raw: u32) -> Self {
SoundId(raw)
}
pub fn raw(self) -> u32 {
self.0
}
pub fn is_valid(self) -> bool {
self.0 != 0
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct VoiceId(u64);
impl VoiceId {
pub const NONE: VoiceId = VoiceId(0);
pub fn from_raw(raw: u64) -> Self {
VoiceId(raw)
}
pub fn raw(self) -> u64 {
self.0
}
pub fn is_valid(self) -> bool {
self.0 != 0
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub enum AudioBus {
#[default]
Effects,
Music,
}
impl AudioBus {
pub const ALL: [AudioBus; 2] = [AudioBus::Effects, AudioBus::Music];
pub fn index(self) -> usize {
match self {
AudioBus::Effects => 0,
AudioBus::Music => 1,
}
}
pub fn from_index(index: usize) -> Option<AudioBus> {
AudioBus::ALL.get(index).copied()
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct PlaybackParams {
pub volume: f32,
pub rate: f32,
pub pan: f32,
pub bus: AudioBus,
}
impl PlaybackParams {
pub const DEFAULT: PlaybackParams = PlaybackParams {
volume: 1.0,
rate: 1.0,
pan: 0.0,
bus: AudioBus::Effects,
};
pub const MIN_RATE: f32 = 0.05;
pub const MAX_RATE: f32 = 8.0;
pub const MAX_VOLUME: f32 = 4.0;
pub const fn new() -> PlaybackParams {
PlaybackParams::DEFAULT
}
pub fn volume(mut self, volume: f32) -> PlaybackParams {
self.volume = volume;
self
}
pub fn rate(mut self, rate: f32) -> PlaybackParams {
self.rate = rate;
self
}
pub fn pan(mut self, pan: f32) -> PlaybackParams {
self.pan = pan;
self
}
pub fn bus(mut self, bus: AudioBus) -> PlaybackParams {
self.bus = bus;
self
}
pub fn pitch_semitones(self, semitones: f32) -> PlaybackParams {
let semitones = if semitones.is_finite() {
semitones
} else {
0.0
};
self.rate(2.0f32.powf(semitones / 12.0))
}
pub fn sanitized(self) -> PlaybackParams {
fn finite(value: f32, fallback: f32) -> f32 {
if value.is_finite() {
value
} else {
fallback
}
}
PlaybackParams {
volume: finite(self.volume, 1.0).clamp(0.0, PlaybackParams::MAX_VOLUME),
rate: finite(self.rate, 1.0).clamp(PlaybackParams::MIN_RATE, PlaybackParams::MAX_RATE),
pan: finite(self.pan, 0.0).clamp(-1.0, 1.0),
bus: self.bus,
}
}
pub fn gains(self) -> (f32, f32) {
let params = self.sanitized();
let angle = (params.pan + 1.0) * std::f32::consts::FRAC_PI_4;
(params.volume * angle.cos(), params.volume * angle.sin())
}
}
impl Default for PlaybackParams {
fn default() -> PlaybackParams {
PlaybackParams::DEFAULT
}
}
#[derive(Clone)]
pub struct AudioClip {
samples: Arc<[f32]>,
channels: u16,
sample_rate: u32,
}
impl AudioClip {
pub const MAX_FRAMES: usize = 1 << 26;
pub fn from_samples(
samples: Vec<f32>,
channels: u16,
sample_rate: u32,
) -> Result<AudioClip, AudioError> {
if channels == 0 || channels > 2 {
return Err(AudioError::UnsupportedFormat(format!(
"clips must be mono or stereo, got {channels} channels"
)));
}
if sample_rate == 0 {
return Err(AudioError::UnsupportedFormat(
"clips must declare a non-zero sample rate".to_string(),
));
}
if samples.is_empty() {
return Err(AudioError::Decode("clip holds no samples".to_string()));
}
if !samples.len().is_multiple_of(usize::from(channels)) {
return Err(AudioError::Decode(format!(
"clip holds {} samples, which is not a whole number of {channels}-channel frames",
samples.len()
)));
}
if samples.len() / usize::from(channels) > AudioClip::MAX_FRAMES {
return Err(AudioError::Decode(
"clip exceeds the maximum in-memory length".to_string(),
));
}
Ok(AudioClip {
samples: samples.into(),
channels,
sample_rate,
})
}
pub fn decode(bytes: &[u8]) -> Result<AudioClip, AudioError> {
if wav::is_wav(bytes) {
wav::decode(bytes)
} else {
Err(AudioError::UnsupportedFormat(
"only RIFF/WAVE clips are decoded by the framework".to_string(),
))
}
}
pub fn samples(&self) -> &[f32] {
&self.samples
}
pub fn shared_samples(&self) -> Arc<[f32]> {
Arc::clone(&self.samples)
}
pub fn channels(&self) -> u16 {
self.channels
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn frames(&self) -> usize {
self.samples.len() / usize::from(self.channels)
}
pub fn duration_secs(&self) -> f32 {
self.frames() as f32 / self.sample_rate as f32
}
}
impl fmt::Debug for AudioClip {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AudioClip")
.field("frames", &self.frames())
.field("channels", &self.channels)
.field("sample_rate", &self.sample_rate)
.finish()
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum AudioError {
#[error("no audio backend is installed")]
Unsupported,
#[error("unsupported audio format: {0}")]
UnsupportedFormat(String),
#[error("failed to decode audio: {0}")]
Decode(String),
#[error("the audio engine already holds its maximum of {capacity} clips")]
ClipTableFull {
capacity: usize,
},
#[error("audio backend failure: {0}")]
Backend(String),
}
pub trait AudioPlayer {
fn load_clip(&self, clip: AudioClip) -> Result<SoundId, AudioError>;
fn load(&self, bytes: &[u8]) -> Result<SoundId, AudioError> {
self.load_clip(AudioClip::decode(bytes)?)
}
fn unload(&self, _id: SoundId) {}
fn play(&self, id: SoundId, params: PlaybackParams);
fn play_loop(&self, id: SoundId, params: PlaybackParams) -> VoiceId;
fn stop(&self, id: SoundId);
fn stop_voice(&self, voice: VoiceId);
fn stop_all(&self) {}
fn set_voice_params(&self, _voice: VoiceId, _params: PlaybackParams) {}
fn set_master_volume(&self, volume: f32);
fn master_volume(&self) -> f32 {
1.0
}
fn set_bus_volume(&self, _bus: AudioBus, _volume: f32) {}
fn bus_volume(&self, _bus: AudioBus) -> f32 {
1.0
}
fn set_bus_enabled(&self, _bus: AudioBus, _enabled: bool) {}
fn bus_enabled(&self, _bus: AudioBus) -> bool {
true
}
fn suspend(&self) {}
fn resume(&self) {}
fn is_available(&self) -> bool {
false
}
}
pub type AudioPlayerRef = Rc<dyn AudioPlayer>;
#[derive(Default)]
pub struct NoopAudioPlayer {
next_sound: Cell<u32>,
next_voice: Cell<u64>,
master: Cell<f32>,
bus_volumes: Cell<[f32; 2]>,
bus_enabled: Cell<[bool; 2]>,
}
impl NoopAudioPlayer {
pub fn new() -> NoopAudioPlayer {
NoopAudioPlayer {
next_sound: Cell::new(0),
next_voice: Cell::new(0),
master: Cell::new(1.0),
bus_volumes: Cell::new([1.0, 1.0]),
bus_enabled: Cell::new([true, true]),
}
}
}
impl AudioPlayer for NoopAudioPlayer {
fn load_clip(&self, _clip: AudioClip) -> Result<SoundId, AudioError> {
let next = self.next_sound.get().saturating_add(1);
self.next_sound.set(next);
Ok(SoundId::from_raw(next))
}
fn play(&self, _id: SoundId, _params: PlaybackParams) {}
fn play_loop(&self, id: SoundId, _params: PlaybackParams) -> VoiceId {
if !id.is_valid() {
return VoiceId::NONE;
}
let next = self.next_voice.get().saturating_add(1);
self.next_voice.set(next);
VoiceId::from_raw(next)
}
fn stop(&self, _id: SoundId) {}
fn stop_voice(&self, _voice: VoiceId) {}
fn set_master_volume(&self, volume: f32) {
self.master.set(if volume.is_finite() {
volume.clamp(0.0, 1.0)
} else {
1.0
});
}
fn master_volume(&self) -> f32 {
self.master.get()
}
fn set_bus_volume(&self, bus: AudioBus, volume: f32) {
let mut volumes = self.bus_volumes.get();
volumes[bus.index()] = if volume.is_finite() {
volume.clamp(0.0, 1.0)
} else {
1.0
};
self.bus_volumes.set(volumes);
}
fn bus_volume(&self, bus: AudioBus) -> f32 {
self.bus_volumes.get()[bus.index()]
}
fn set_bus_enabled(&self, bus: AudioBus, enabled: bool) {
let mut flags = self.bus_enabled.get();
flags[bus.index()] = enabled;
self.bus_enabled.set(flags);
}
fn bus_enabled(&self, bus: AudioBus) -> bool {
self.bus_enabled.get()[bus.index()]
}
}
thread_local! {
static PLATFORM_AUDIO: RefCell<Option<AudioPlayerRef>> = const { RefCell::new(None) };
}
pub fn set_platform_audio(player: AudioPlayerRef) {
PLATFORM_AUDIO.with(|cell| *cell.borrow_mut() = Some(player));
}
pub fn clear_platform_audio() {
PLATFORM_AUDIO.with(|cell| *cell.borrow_mut() = None);
}
pub fn default_audio() -> AudioPlayerRef {
PLATFORM_AUDIO
.with(|cell| cell.borrow().clone())
.unwrap_or_else(|| Rc::new(NoopAudioPlayer::new()))
}
pub fn local_audio() -> CompositionLocal<AudioPlayerRef> {
thread_local! {
static LOCAL_AUDIO: RefCell<Option<CompositionLocal<AudioPlayerRef>>> = const { RefCell::new(None) };
}
LOCAL_AUDIO.with(|cell| {
let mut local = cell.borrow_mut();
local
.get_or_insert_with(|| compositionLocalOfWithPolicy(default_audio, Rc::ptr_eq))
.clone()
})
}
#[allow(non_snake_case)]
#[composable]
pub fn ProvideAudio(content: impl FnOnce()) {
let player = cranpose_core::remember(default_audio).with(|state| state.clone());
let local = local_audio();
CompositionLocalProvider(vec![local.provides(player)], move || {
content();
});
}
#[derive(Clone, Copy, Debug)]
pub struct SoundSpec<'a> {
pub name: &'static str,
pub bytes: &'a [u8],
pub base_volume: f32,
pub bus: AudioBus,
}
impl<'a> SoundSpec<'a> {
pub fn new(name: &'static str, bytes: &'a [u8]) -> SoundSpec<'a> {
SoundSpec {
name,
bytes,
base_volume: 1.0,
bus: AudioBus::Effects,
}
}
pub fn volume(mut self, base_volume: f32) -> SoundSpec<'a> {
self.base_volume = base_volume;
self
}
pub fn bus(mut self, bus: AudioBus) -> SoundSpec<'a> {
self.bus = bus;
self
}
}
#[derive(Clone, Copy, Debug)]
pub struct SoundBankEntry {
pub name: &'static str,
pub id: SoundId,
pub base_volume: f32,
pub bus: AudioBus,
}
#[derive(Clone, Debug)]
pub struct SoundBankFailure {
pub name: &'static str,
pub error: AudioError,
}
struct SoundBankInner {
player: AudioPlayerRef,
entries: Vec<SoundBankEntry>,
failures: Vec<SoundBankFailure>,
}
impl Drop for SoundBankInner {
fn drop(&mut self) {
for entry in &self.entries {
self.player.unload(entry.id);
}
}
}
#[derive(Clone)]
pub struct SoundBank {
inner: Rc<SoundBankInner>,
}
impl SoundBank {
pub fn load(player: AudioPlayerRef, specs: &[SoundSpec<'_>]) -> SoundBank {
let mut entries = Vec::with_capacity(specs.len());
let mut failures = Vec::new();
for spec in specs {
match player.load(spec.bytes) {
Ok(id) => entries.push(SoundBankEntry {
name: spec.name,
id,
base_volume: spec.base_volume,
bus: spec.bus,
}),
Err(error) => {
entries.push(SoundBankEntry {
name: spec.name,
id: SoundId::NONE,
base_volume: spec.base_volume,
bus: spec.bus,
});
failures.push(SoundBankFailure {
name: spec.name,
error,
});
}
}
}
SoundBank {
inner: Rc::new(SoundBankInner {
player,
entries,
failures,
}),
}
}
pub fn len(&self) -> usize {
self.inner.entries.len()
}
pub fn is_empty(&self) -> bool {
self.inner.entries.is_empty()
}
pub fn entries(&self) -> &[SoundBankEntry] {
&self.inner.entries
}
pub fn failures(&self) -> &[SoundBankFailure] {
&self.inner.failures
}
pub fn player(&self) -> AudioPlayerRef {
Rc::clone(&self.inner.player)
}
pub fn id(&self, index: usize) -> SoundId {
self.inner
.entries
.get(index)
.map(|entry| entry.id)
.unwrap_or(SoundId::NONE)
}
pub fn find(&self, name: &str) -> Option<SoundId> {
self.inner
.entries
.iter()
.find(|entry| entry.name == name)
.map(|entry| entry.id)
}
pub fn play(&self, index: usize) {
self.play_with(index, PlaybackParams::DEFAULT);
}
pub fn play_with(&self, index: usize, params: PlaybackParams) {
let Some(entry) = self.inner.entries.get(index) else {
return;
};
if !entry.id.is_valid() {
return;
}
self.inner.player.play(entry.id, entry.apply(params));
}
pub fn play_named(&self, name: &str, params: PlaybackParams) {
if let Some(index) = self.inner.entries.iter().position(|e| e.name == name) {
self.play_with(index, params);
}
}
pub fn play_loop(&self, index: usize, params: PlaybackParams) -> VoiceId {
let Some(entry) = self.inner.entries.get(index) else {
return VoiceId::NONE;
};
if !entry.id.is_valid() {
return VoiceId::NONE;
}
self.inner.player.play_loop(entry.id, entry.apply(params))
}
pub fn stop(&self, index: usize) {
let id = self.id(index);
if id.is_valid() {
self.inner.player.stop(id);
}
}
}
impl SoundBankEntry {
fn apply(&self, params: PlaybackParams) -> PlaybackParams {
PlaybackParams {
volume: params.volume * self.base_volume,
rate: params.rate,
pan: params.pan,
bus: self.bus,
}
}
}
impl fmt::Debug for SoundBank {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SoundBank")
.field("entries", &self.inner.entries.len())
.field("failures", &self.inner.failures.len())
.finish()
}
}
impl Index<usize> for SoundBank {
type Output = SoundId;
fn index(&self, index: usize) -> &SoundId {
static NONE: SoundId = SoundId::NONE;
self.inner
.entries
.get(index)
.map(|entry| &entry.id)
.unwrap_or(&NONE)
}
}
#[allow(non_snake_case)]
#[composable(no_skip)]
pub fn rememberSoundBank(specs: &[SoundSpec<'_>]) -> SoundBank {
let key = sound_bank_key(specs);
let player = local_audio().current();
cranpose_core::remember_keyed((key, Rc::as_ptr(&player) as *const () as usize), |_| {
SoundBank::load(Rc::clone(&player), specs)
})
}
fn sound_bank_key(specs: &[SoundSpec<'_>]) -> (usize, u64) {
let mut hash = 0xcbf2_9ce4_8422_2325u64;
for spec in specs {
for byte in spec.name.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash ^= spec.bytes.len() as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
(specs.len(), hash)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::run_test_composition;
fn tiny_wav() -> Vec<u8> {
let data = 0i16.to_le_bytes();
let mut out = Vec::new();
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(36u32 + data.len() as u32).to_le_bytes());
out.extend_from_slice(b"WAVE");
out.extend_from_slice(b"fmt ");
out.extend_from_slice(&16u32.to_le_bytes());
out.extend_from_slice(&1u16.to_le_bytes());
out.extend_from_slice(&1u16.to_le_bytes());
out.extend_from_slice(&8000u32.to_le_bytes());
out.extend_from_slice(&16000u32.to_le_bytes());
out.extend_from_slice(&2u16.to_le_bytes());
out.extend_from_slice(&16u16.to_le_bytes());
out.extend_from_slice(b"data");
out.extend_from_slice(&(data.len() as u32).to_le_bytes());
out.extend_from_slice(&data);
out
}
#[derive(Default)]
struct RecordingPlayer {
played: RefCell<Vec<(SoundId, PlaybackParams)>>,
unloaded: RefCell<Vec<SoundId>>,
next: Cell<u32>,
}
impl AudioPlayer for RecordingPlayer {
fn load_clip(&self, _clip: AudioClip) -> Result<SoundId, AudioError> {
let next = self.next.get() + 1;
self.next.set(next);
Ok(SoundId::from_raw(next))
}
fn play(&self, id: SoundId, params: PlaybackParams) {
self.played.borrow_mut().push((id, params));
}
fn play_loop(&self, _id: SoundId, _params: PlaybackParams) -> VoiceId {
VoiceId::from_raw(7)
}
fn stop(&self, _id: SoundId) {}
fn stop_voice(&self, _voice: VoiceId) {}
fn set_master_volume(&self, _volume: f32) {}
fn unload(&self, id: SoundId) {
self.unloaded.borrow_mut().push(id);
}
fn is_available(&self) -> bool {
true
}
}
#[test]
fn playback_params_default_is_neutral() {
let params = PlaybackParams::default();
assert_eq!(params.volume, 1.0);
assert_eq!(params.rate, 1.0);
assert_eq!(params.pan, 0.0);
assert_eq!(params.bus, AudioBus::Effects);
assert_eq!(params, PlaybackParams::new());
assert_eq!(params, PlaybackParams::DEFAULT);
}
#[test]
fn playback_params_sanitizes_out_of_range_and_nan() {
let wild = PlaybackParams {
volume: f32::NAN,
rate: 1_000.0,
pan: -9.0,
bus: AudioBus::Music,
}
.sanitized();
assert_eq!(wild.volume, 1.0);
assert_eq!(wild.rate, PlaybackParams::MAX_RATE);
assert_eq!(wild.pan, -1.0);
assert_eq!(wild.bus, AudioBus::Music);
let slow = PlaybackParams::new().rate(0.0).sanitized();
assert_eq!(slow.rate, PlaybackParams::MIN_RATE);
}
#[test]
fn pitch_semitones_maps_octaves_to_rate() {
let up = PlaybackParams::new().pitch_semitones(12.0);
assert!((up.rate - 2.0).abs() < 1e-5);
let down = PlaybackParams::new().pitch_semitones(-12.0);
assert!((down.rate - 0.5).abs() < 1e-5);
let broken = PlaybackParams::new().pitch_semitones(f32::NAN);
assert_eq!(broken.rate, 1.0);
}
#[test]
fn pan_gains_are_constant_power() {
let (left, right) = PlaybackParams::new().gains();
assert!((left - right).abs() < 1e-6);
assert!((left * left + right * right - 1.0).abs() < 1e-5);
let (left, right) = PlaybackParams::new().pan(-1.0).gains();
assert!((left - 1.0).abs() < 1e-5);
assert!(right.abs() < 1e-5);
let (left, right) = PlaybackParams::new().pan(1.0).gains();
assert!(left.abs() < 1e-5);
assert!((right - 1.0).abs() < 1e-5);
}
#[test]
fn audio_bus_indices_round_trip() {
for bus in AudioBus::ALL {
assert_eq!(AudioBus::from_index(bus.index()), Some(bus));
}
assert_eq!(AudioBus::from_index(2), None);
assert_eq!(AudioBus::default(), AudioBus::Effects);
}
#[test]
fn noop_player_hands_out_handles_and_keeps_settings() {
clear_platform_audio();
let player = default_audio();
assert!(!player.is_available());
let id = player.load(&tiny_wav()).expect("no-op load succeeds");
assert!(id.is_valid());
let second = player.load(&tiny_wav()).expect("no-op load succeeds");
assert_ne!(id, second);
player.play(id, PlaybackParams::new());
let voice = player.play_loop(id, PlaybackParams::new());
assert!(voice.is_valid());
player.stop_voice(voice);
player.stop(id);
player.stop_all();
player.set_voice_params(voice, PlaybackParams::new());
player.unload(id);
player.suspend();
player.resume();
player.set_master_volume(0.25);
assert_eq!(player.master_volume(), 0.25);
player.set_master_volume(f32::NAN);
assert_eq!(player.master_volume(), 1.0);
player.set_bus_enabled(AudioBus::Music, false);
assert!(!player.bus_enabled(AudioBus::Music));
assert!(player.bus_enabled(AudioBus::Effects));
player.set_bus_volume(AudioBus::Effects, 0.5);
assert_eq!(player.bus_volume(AudioBus::Effects), 0.5);
}
#[test]
fn noop_player_rejects_invalid_loop_handle() {
let player = NoopAudioPlayer::new();
assert_eq!(
player.play_loop(SoundId::NONE, PlaybackParams::new()),
VoiceId::NONE
);
}
#[test]
fn registered_player_replaces_the_default() {
clear_platform_audio();
assert!(!default_audio().is_available());
let player: AudioPlayerRef = Rc::new(RecordingPlayer::default());
set_platform_audio(player);
assert!(default_audio().is_available());
clear_platform_audio();
assert!(!default_audio().is_available());
}
#[test]
fn audio_clip_validates_shape() {
assert!(matches!(
AudioClip::from_samples(vec![0.0], 0, 44_100),
Err(AudioError::UnsupportedFormat(_))
));
assert!(matches!(
AudioClip::from_samples(vec![0.0], 3, 44_100),
Err(AudioError::UnsupportedFormat(_))
));
assert!(matches!(
AudioClip::from_samples(vec![0.0], 1, 0),
Err(AudioError::UnsupportedFormat(_))
));
assert!(matches!(
AudioClip::from_samples(Vec::new(), 1, 44_100),
Err(AudioError::Decode(_))
));
assert!(matches!(
AudioClip::from_samples(vec![0.0, 0.0, 0.0], 2, 44_100),
Err(AudioError::Decode(_))
));
let clip = AudioClip::from_samples(vec![0.0, 0.5], 2, 44_100).expect("valid clip");
assert_eq!(clip.frames(), 1);
assert_eq!(clip.channels(), 2);
assert!(clip.duration_secs() > 0.0);
assert_eq!(clip.shared_samples().len(), 2);
assert!(format!("{clip:?}").contains("AudioClip"));
}
#[test]
fn audio_clip_decode_rejects_unknown_container() {
assert!(matches!(
AudioClip::decode(b"OggS not really"),
Err(AudioError::UnsupportedFormat(_))
));
}
#[test]
fn sound_bank_loads_applies_base_volume_and_unloads_on_drop() {
let player = Rc::new(RecordingPlayer::default());
let wav = tiny_wav();
let specs = [
SoundSpec::new("hit", &wav).volume(0.5),
SoundSpec::new("music", &wav).bus(AudioBus::Music),
SoundSpec::new("broken", b"not audio"),
];
let player_ref: AudioPlayerRef = player.clone();
let bank = SoundBank::load(player_ref, &specs);
assert_eq!(bank.len(), 3);
assert!(!bank.is_empty());
assert_eq!(bank.failures().len(), 1);
assert_eq!(bank.failures()[0].name, "broken");
assert!(!bank.id(2).is_valid());
assert_eq!(bank.find("music"), Some(bank.id(1)));
assert_eq!(bank.find("absent"), None);
assert_eq!(bank[0], bank.id(0));
assert_eq!(bank[99], SoundId::NONE);
assert!(format!("{bank:?}").contains("SoundBank"));
bank.play(0);
bank.play_with(1, PlaybackParams::new().volume(0.5));
bank.play_named("hit", PlaybackParams::new().pan(1.0));
bank.play_with(2, PlaybackParams::new());
bank.play_named("absent", PlaybackParams::new());
assert_eq!(bank.play_loop(2, PlaybackParams::new()), VoiceId::NONE);
assert!(bank.play_loop(0, PlaybackParams::new()).is_valid());
assert_eq!(bank.play_loop(99, PlaybackParams::new()), VoiceId::NONE);
bank.stop(0);
bank.stop(2);
let played = player.played.borrow().clone();
assert_eq!(played.len(), 3);
assert!((played[0].1.volume - 0.5).abs() < 1e-6);
assert_eq!(played[0].1.bus, AudioBus::Effects);
assert!((played[1].1.volume - 0.5).abs() < 1e-6);
assert_eq!(played[1].1.bus, AudioBus::Music);
assert!((played[2].1.pan - 1.0).abs() < 1e-6);
drop(bank);
assert_eq!(player.unloaded.borrow().len(), 3);
}
#[test]
fn sound_bank_key_tracks_names_and_lengths() {
let a = [1u8, 2, 3];
let b = [1u8, 2, 3, 4];
assert_eq!(
sound_bank_key(&[SoundSpec::new("x", &a)]),
sound_bank_key(&[SoundSpec::new("x", &a)])
);
assert_ne!(
sound_bank_key(&[SoundSpec::new("x", &a)]),
sound_bank_key(&[SoundSpec::new("y", &a)])
);
assert_ne!(
sound_bank_key(&[SoundSpec::new("x", &a)]),
sound_bank_key(&[SoundSpec::new("x", &b)])
);
assert_ne!(
sound_bank_key(&[SoundSpec::new("x", &a)]),
sound_bank_key(&[SoundSpec::new("x", &a), SoundSpec::new("x", &a)])
);
}
#[test]
fn provide_audio_publishes_the_platform_player() {
clear_platform_audio();
let player: AudioPlayerRef = Rc::new(RecordingPlayer::default());
set_platform_audio(player);
let captured = Rc::new(RefCell::new(None));
{
let captured = Rc::clone(&captured);
run_test_composition(move || {
let captured = Rc::clone(&captured);
ProvideAudio(move || {
*captured.borrow_mut() = Some(local_audio().current().is_available());
});
});
}
assert_eq!(*captured.borrow(), Some(true));
clear_platform_audio();
}
#[test]
fn local_audio_defaults_to_the_noop_player() {
clear_platform_audio();
let captured = Rc::new(RefCell::new(None));
{
let captured = Rc::clone(&captured);
run_test_composition(move || {
let captured = Rc::clone(&captured);
ProvideAudio(move || {
*captured.borrow_mut() = Some(local_audio().current().is_available());
});
});
}
assert_eq!(*captured.borrow(), Some(false));
}
#[test]
fn remember_sound_bank_loads_once_across_recompositions() {
clear_platform_audio();
let player = Rc::new(RecordingPlayer::default());
let player_ref: AudioPlayerRef = player.clone();
set_platform_audio(player_ref);
let wav = tiny_wav();
let bank_len = Rc::new(Cell::new(0usize));
let bank_len_build = Rc::clone(&bank_len);
let mut build = move || {
let specs = [SoundSpec::new("a", &wav), SoundSpec::new("b", &wav)];
let bank = rememberSoundBank(&specs);
bank_len_build.set(bank.len());
};
let key = cranpose_core::location_key(file!(), line!(), column!());
let mut composition = cranpose_core::Composition::new(cranpose_core::MemoryApplier::new());
composition.render(key, &mut build).expect("first render");
composition.render(key, &mut build).expect("second render");
assert_eq!(bank_len.get(), 2);
assert_eq!(player.next.get(), 2, "the bank decodes once across renders");
clear_platform_audio();
}
}