1mod wav;
33
34use cranpose_core::compositionLocalOfWithPolicy;
35use cranpose_core::CompositionLocal;
36use cranpose_core::CompositionLocalProvider;
37use cranpose_macros::composable;
38use std::cell::Cell;
39use std::cell::RefCell;
40use std::fmt;
41use std::ops::Index;
42use std::rc::Rc;
43use std::sync::Arc;
44
45#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
50pub struct SoundId(u32);
51
52impl SoundId {
53 pub const NONE: SoundId = SoundId(0);
55
56 pub fn from_raw(raw: u32) -> Self {
58 SoundId(raw)
59 }
60
61 pub fn raw(self) -> u32 {
63 self.0
64 }
65
66 pub fn is_valid(self) -> bool {
68 self.0 != 0
69 }
70}
71
72#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
75pub struct VoiceId(u64);
76
77impl VoiceId {
78 pub const NONE: VoiceId = VoiceId(0);
80
81 pub fn from_raw(raw: u64) -> Self {
83 VoiceId(raw)
84 }
85
86 pub fn raw(self) -> u64 {
88 self.0
89 }
90
91 pub fn is_valid(self) -> bool {
93 self.0 != 0
94 }
95}
96
97#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
100pub enum AudioBus {
101 #[default]
103 Effects,
104 Music,
106}
107
108impl AudioBus {
109 pub const ALL: [AudioBus; 2] = [AudioBus::Effects, AudioBus::Music];
111
112 pub fn index(self) -> usize {
114 match self {
115 AudioBus::Effects => 0,
116 AudioBus::Music => 1,
117 }
118 }
119
120 pub fn from_index(index: usize) -> Option<AudioBus> {
122 AudioBus::ALL.get(index).copied()
123 }
124}
125
126#[derive(Clone, Copy, PartialEq, Debug)]
131pub struct PlaybackParams {
132 pub volume: f32,
134 pub rate: f32,
137 pub pan: f32,
139 pub bus: AudioBus,
141}
142
143impl PlaybackParams {
144 pub const DEFAULT: PlaybackParams = PlaybackParams {
146 volume: 1.0,
147 rate: 1.0,
148 pan: 0.0,
149 bus: AudioBus::Effects,
150 };
151
152 pub const MIN_RATE: f32 = 0.05;
154 pub const MAX_RATE: f32 = 8.0;
156 pub const MAX_VOLUME: f32 = 4.0;
158
159 pub const fn new() -> PlaybackParams {
161 PlaybackParams::DEFAULT
162 }
163
164 pub fn volume(mut self, volume: f32) -> PlaybackParams {
166 self.volume = volume;
167 self
168 }
169
170 pub fn rate(mut self, rate: f32) -> PlaybackParams {
172 self.rate = rate;
173 self
174 }
175
176 pub fn pan(mut self, pan: f32) -> PlaybackParams {
178 self.pan = pan;
179 self
180 }
181
182 pub fn bus(mut self, bus: AudioBus) -> PlaybackParams {
184 self.bus = bus;
185 self
186 }
187
188 pub fn pitch_semitones(self, semitones: f32) -> PlaybackParams {
193 let semitones = if semitones.is_finite() {
194 semitones
195 } else {
196 0.0
197 };
198 self.rate(2.0f32.powf(semitones / 12.0))
199 }
200
201 pub fn sanitized(self) -> PlaybackParams {
206 fn finite(value: f32, fallback: f32) -> f32 {
207 if value.is_finite() {
208 value
209 } else {
210 fallback
211 }
212 }
213 PlaybackParams {
214 volume: finite(self.volume, 1.0).clamp(0.0, PlaybackParams::MAX_VOLUME),
215 rate: finite(self.rate, 1.0).clamp(PlaybackParams::MIN_RATE, PlaybackParams::MAX_RATE),
216 pan: finite(self.pan, 0.0).clamp(-1.0, 1.0),
217 bus: self.bus,
218 }
219 }
220
221 pub fn gains(self) -> (f32, f32) {
225 let params = self.sanitized();
226 let angle = (params.pan + 1.0) * std::f32::consts::FRAC_PI_4;
228 (params.volume * angle.cos(), params.volume * angle.sin())
229 }
230}
231
232impl Default for PlaybackParams {
233 fn default() -> PlaybackParams {
234 PlaybackParams::DEFAULT
235 }
236}
237
238#[derive(Clone)]
244pub struct AudioClip {
245 samples: Arc<[f32]>,
246 channels: u16,
247 sample_rate: u32,
248}
249
250impl AudioClip {
251 pub const MAX_FRAMES: usize = 1 << 26;
254
255 pub fn from_samples(
257 samples: Vec<f32>,
258 channels: u16,
259 sample_rate: u32,
260 ) -> Result<AudioClip, AudioError> {
261 if channels == 0 || channels > 2 {
262 return Err(AudioError::UnsupportedFormat(format!(
263 "clips must be mono or stereo, got {channels} channels"
264 )));
265 }
266 if sample_rate == 0 {
267 return Err(AudioError::UnsupportedFormat(
268 "clips must declare a non-zero sample rate".to_string(),
269 ));
270 }
271 if samples.is_empty() {
272 return Err(AudioError::Decode("clip holds no samples".to_string()));
273 }
274 if !samples.len().is_multiple_of(usize::from(channels)) {
275 return Err(AudioError::Decode(format!(
276 "clip holds {} samples, which is not a whole number of {channels}-channel frames",
277 samples.len()
278 )));
279 }
280 if samples.len() / usize::from(channels) > AudioClip::MAX_FRAMES {
281 return Err(AudioError::Decode(
282 "clip exceeds the maximum in-memory length".to_string(),
283 ));
284 }
285 Ok(AudioClip {
286 samples: samples.into(),
287 channels,
288 sample_rate,
289 })
290 }
291
292 pub fn decode(bytes: &[u8]) -> Result<AudioClip, AudioError> {
298 if wav::is_wav(bytes) {
299 wav::decode(bytes)
300 } else {
301 Err(AudioError::UnsupportedFormat(
302 "only RIFF/WAVE clips are decoded by the framework".to_string(),
303 ))
304 }
305 }
306
307 pub fn samples(&self) -> &[f32] {
309 &self.samples
310 }
311
312 pub fn shared_samples(&self) -> Arc<[f32]> {
314 Arc::clone(&self.samples)
315 }
316
317 pub fn channels(&self) -> u16 {
319 self.channels
320 }
321
322 pub fn sample_rate(&self) -> u32 {
324 self.sample_rate
325 }
326
327 pub fn frames(&self) -> usize {
329 self.samples.len() / usize::from(self.channels)
330 }
331
332 pub fn duration_secs(&self) -> f32 {
334 self.frames() as f32 / self.sample_rate as f32
335 }
336}
337
338impl fmt::Debug for AudioClip {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 f.debug_struct("AudioClip")
341 .field("frames", &self.frames())
342 .field("channels", &self.channels)
343 .field("sample_rate", &self.sample_rate)
344 .finish()
345 }
346}
347
348#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
350pub enum AudioError {
351 #[error("no audio backend is installed")]
353 Unsupported,
354 #[error("unsupported audio format: {0}")]
356 UnsupportedFormat(String),
357 #[error("failed to decode audio: {0}")]
359 Decode(String),
360 #[error("the audio engine already holds its maximum of {capacity} clips")]
362 ClipTableFull {
363 capacity: usize,
365 },
366 #[error("audio backend failure: {0}")]
368 Backend(String),
369}
370
371pub trait AudioPlayer {
379 fn load_clip(&self, clip: AudioClip) -> Result<SoundId, AudioError>;
381
382 fn load(&self, bytes: &[u8]) -> Result<SoundId, AudioError> {
389 self.load_clip(AudioClip::decode(bytes)?)
390 }
391
392 fn unload(&self, _id: SoundId) {}
394
395 fn play(&self, id: SoundId, params: PlaybackParams);
398
399 fn play_loop(&self, id: SoundId, params: PlaybackParams) -> VoiceId;
401
402 fn stop(&self, id: SoundId);
404
405 fn stop_voice(&self, voice: VoiceId);
407
408 fn stop_all(&self) {}
410
411 fn set_voice_params(&self, _voice: VoiceId, _params: PlaybackParams) {}
414
415 fn set_master_volume(&self, volume: f32);
417
418 fn master_volume(&self) -> f32 {
420 1.0
421 }
422
423 fn set_bus_volume(&self, _bus: AudioBus, _volume: f32) {}
425
426 fn bus_volume(&self, _bus: AudioBus) -> f32 {
428 1.0
429 }
430
431 fn set_bus_enabled(&self, _bus: AudioBus, _enabled: bool) {}
434
435 fn bus_enabled(&self, _bus: AudioBus) -> bool {
437 true
438 }
439
440 fn suspend(&self) {}
443
444 fn resume(&self) {}
446
447 fn is_available(&self) -> bool {
450 false
451 }
452}
453
454pub type AudioPlayerRef = Rc<dyn AudioPlayer>;
456
457#[derive(Default)]
463pub struct NoopAudioPlayer {
464 next_sound: Cell<u32>,
465 next_voice: Cell<u64>,
466 master: Cell<f32>,
467 bus_volumes: Cell<[f32; 2]>,
468 bus_enabled: Cell<[bool; 2]>,
469}
470
471impl NoopAudioPlayer {
472 pub fn new() -> NoopAudioPlayer {
474 NoopAudioPlayer {
475 next_sound: Cell::new(0),
476 next_voice: Cell::new(0),
477 master: Cell::new(1.0),
478 bus_volumes: Cell::new([1.0, 1.0]),
479 bus_enabled: Cell::new([true, true]),
480 }
481 }
482}
483
484impl AudioPlayer for NoopAudioPlayer {
485 fn load_clip(&self, _clip: AudioClip) -> Result<SoundId, AudioError> {
486 let next = self.next_sound.get().saturating_add(1);
487 self.next_sound.set(next);
488 Ok(SoundId::from_raw(next))
489 }
490
491 fn play(&self, _id: SoundId, _params: PlaybackParams) {}
492
493 fn play_loop(&self, id: SoundId, _params: PlaybackParams) -> VoiceId {
494 if !id.is_valid() {
495 return VoiceId::NONE;
496 }
497 let next = self.next_voice.get().saturating_add(1);
498 self.next_voice.set(next);
499 VoiceId::from_raw(next)
500 }
501
502 fn stop(&self, _id: SoundId) {}
503
504 fn stop_voice(&self, _voice: VoiceId) {}
505
506 fn set_master_volume(&self, volume: f32) {
507 self.master.set(if volume.is_finite() {
508 volume.clamp(0.0, 1.0)
509 } else {
510 1.0
511 });
512 }
513
514 fn master_volume(&self) -> f32 {
515 self.master.get()
516 }
517
518 fn set_bus_volume(&self, bus: AudioBus, volume: f32) {
519 let mut volumes = self.bus_volumes.get();
520 volumes[bus.index()] = if volume.is_finite() {
521 volume.clamp(0.0, 1.0)
522 } else {
523 1.0
524 };
525 self.bus_volumes.set(volumes);
526 }
527
528 fn bus_volume(&self, bus: AudioBus) -> f32 {
529 self.bus_volumes.get()[bus.index()]
530 }
531
532 fn set_bus_enabled(&self, bus: AudioBus, enabled: bool) {
533 let mut flags = self.bus_enabled.get();
534 flags[bus.index()] = enabled;
535 self.bus_enabled.set(flags);
536 }
537
538 fn bus_enabled(&self, bus: AudioBus) -> bool {
539 self.bus_enabled.get()[bus.index()]
540 }
541}
542
543thread_local! {
544 static PLATFORM_AUDIO: RefCell<Option<AudioPlayerRef>> = const { RefCell::new(None) };
545}
546
547pub fn set_platform_audio(player: AudioPlayerRef) {
549 PLATFORM_AUDIO.with(|cell| *cell.borrow_mut() = Some(player));
550}
551
552pub fn clear_platform_audio() {
554 PLATFORM_AUDIO.with(|cell| *cell.borrow_mut() = None);
555}
556
557pub fn default_audio() -> AudioPlayerRef {
559 PLATFORM_AUDIO
560 .with(|cell| cell.borrow().clone())
561 .unwrap_or_else(|| Rc::new(NoopAudioPlayer::new()))
562}
563
564pub fn local_audio() -> CompositionLocal<AudioPlayerRef> {
566 thread_local! {
567 static LOCAL_AUDIO: RefCell<Option<CompositionLocal<AudioPlayerRef>>> = const { RefCell::new(None) };
568 }
569
570 LOCAL_AUDIO.with(|cell| {
571 let mut local = cell.borrow_mut();
572 local
573 .get_or_insert_with(|| compositionLocalOfWithPolicy(default_audio, Rc::ptr_eq))
574 .clone()
575 })
576}
577
578#[allow(non_snake_case)]
580#[composable]
581pub fn ProvideAudio(content: impl FnOnce()) {
582 let player = cranpose_core::remember(default_audio).with(|state| state.clone());
583 let local = local_audio();
584 CompositionLocalProvider(vec![local.provides(player)], move || {
585 content();
586 });
587}
588
589#[derive(Clone, Copy, Debug)]
592pub struct SoundSpec<'a> {
593 pub name: &'static str,
595 pub bytes: &'a [u8],
597 pub base_volume: f32,
600 pub bus: AudioBus,
602}
603
604impl<'a> SoundSpec<'a> {
605 pub fn new(name: &'static str, bytes: &'a [u8]) -> SoundSpec<'a> {
607 SoundSpec {
608 name,
609 bytes,
610 base_volume: 1.0,
611 bus: AudioBus::Effects,
612 }
613 }
614
615 pub fn volume(mut self, base_volume: f32) -> SoundSpec<'a> {
617 self.base_volume = base_volume;
618 self
619 }
620
621 pub fn bus(mut self, bus: AudioBus) -> SoundSpec<'a> {
623 self.bus = bus;
624 self
625 }
626}
627
628#[derive(Clone, Copy, Debug)]
630pub struct SoundBankEntry {
631 pub name: &'static str,
633 pub id: SoundId,
635 pub base_volume: f32,
637 pub bus: AudioBus,
639}
640
641#[derive(Clone, Debug)]
643pub struct SoundBankFailure {
644 pub name: &'static str,
646 pub error: AudioError,
648}
649
650struct SoundBankInner {
651 player: AudioPlayerRef,
652 entries: Vec<SoundBankEntry>,
653 failures: Vec<SoundBankFailure>,
654}
655
656impl Drop for SoundBankInner {
657 fn drop(&mut self) {
658 for entry in &self.entries {
659 self.player.unload(entry.id);
660 }
661 }
662}
663
664#[derive(Clone)]
671pub struct SoundBank {
672 inner: Rc<SoundBankInner>,
673}
674
675impl SoundBank {
676 pub fn load(player: AudioPlayerRef, specs: &[SoundSpec<'_>]) -> SoundBank {
679 let mut entries = Vec::with_capacity(specs.len());
680 let mut failures = Vec::new();
681 for spec in specs {
682 match player.load(spec.bytes) {
683 Ok(id) => entries.push(SoundBankEntry {
684 name: spec.name,
685 id,
686 base_volume: spec.base_volume,
687 bus: spec.bus,
688 }),
689 Err(error) => {
690 entries.push(SoundBankEntry {
694 name: spec.name,
695 id: SoundId::NONE,
696 base_volume: spec.base_volume,
697 bus: spec.bus,
698 });
699 failures.push(SoundBankFailure {
700 name: spec.name,
701 error,
702 });
703 }
704 }
705 }
706 SoundBank {
707 inner: Rc::new(SoundBankInner {
708 player,
709 entries,
710 failures,
711 }),
712 }
713 }
714
715 pub fn len(&self) -> usize {
717 self.inner.entries.len()
718 }
719
720 pub fn is_empty(&self) -> bool {
722 self.inner.entries.is_empty()
723 }
724
725 pub fn entries(&self) -> &[SoundBankEntry] {
727 &self.inner.entries
728 }
729
730 pub fn failures(&self) -> &[SoundBankFailure] {
732 &self.inner.failures
733 }
734
735 pub fn player(&self) -> AudioPlayerRef {
737 Rc::clone(&self.inner.player)
738 }
739
740 pub fn id(&self, index: usize) -> SoundId {
742 self.inner
743 .entries
744 .get(index)
745 .map(|entry| entry.id)
746 .unwrap_or(SoundId::NONE)
747 }
748
749 pub fn find(&self, name: &str) -> Option<SoundId> {
751 self.inner
752 .entries
753 .iter()
754 .find(|entry| entry.name == name)
755 .map(|entry| entry.id)
756 }
757
758 pub fn play(&self, index: usize) {
760 self.play_with(index, PlaybackParams::DEFAULT);
761 }
762
763 pub fn play_with(&self, index: usize, params: PlaybackParams) {
766 let Some(entry) = self.inner.entries.get(index) else {
767 return;
768 };
769 if !entry.id.is_valid() {
770 return;
771 }
772 self.inner.player.play(entry.id, entry.apply(params));
773 }
774
775 pub fn play_named(&self, name: &str, params: PlaybackParams) {
777 if let Some(index) = self.inner.entries.iter().position(|e| e.name == name) {
778 self.play_with(index, params);
779 }
780 }
781
782 pub fn play_loop(&self, index: usize, params: PlaybackParams) -> VoiceId {
784 let Some(entry) = self.inner.entries.get(index) else {
785 return VoiceId::NONE;
786 };
787 if !entry.id.is_valid() {
788 return VoiceId::NONE;
789 }
790 self.inner.player.play_loop(entry.id, entry.apply(params))
791 }
792
793 pub fn stop(&self, index: usize) {
795 let id = self.id(index);
796 if id.is_valid() {
797 self.inner.player.stop(id);
798 }
799 }
800}
801
802impl SoundBankEntry {
803 fn apply(&self, params: PlaybackParams) -> PlaybackParams {
804 PlaybackParams {
805 volume: params.volume * self.base_volume,
806 rate: params.rate,
807 pan: params.pan,
808 bus: self.bus,
809 }
810 }
811}
812
813impl fmt::Debug for SoundBank {
814 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
815 f.debug_struct("SoundBank")
816 .field("entries", &self.inner.entries.len())
817 .field("failures", &self.inner.failures.len())
818 .finish()
819 }
820}
821
822impl Index<usize> for SoundBank {
823 type Output = SoundId;
824
825 fn index(&self, index: usize) -> &SoundId {
826 static NONE: SoundId = SoundId::NONE;
827 self.inner
828 .entries
829 .get(index)
830 .map(|entry| &entry.id)
831 .unwrap_or(&NONE)
832 }
833}
834
835#[allow(non_snake_case)]
841#[composable(no_skip)]
842pub fn rememberSoundBank(specs: &[SoundSpec<'_>]) -> SoundBank {
843 let key = sound_bank_key(specs);
844 let player = local_audio().current();
845 cranpose_core::remember_keyed((key, Rc::as_ptr(&player) as *const () as usize), |_| {
846 SoundBank::load(Rc::clone(&player), specs)
847 })
848}
849
850fn sound_bank_key(specs: &[SoundSpec<'_>]) -> (usize, u64) {
853 let mut hash = 0xcbf2_9ce4_8422_2325u64;
854 for spec in specs {
855 for byte in spec.name.as_bytes() {
856 hash ^= u64::from(*byte);
857 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
858 }
859 hash ^= spec.bytes.len() as u64;
860 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
861 }
862 (specs.len(), hash)
863}
864
865#[cfg(test)]
866mod tests {
867 use super::*;
868 use crate::run_test_composition;
869
870 fn tiny_wav() -> Vec<u8> {
872 let data = 0i16.to_le_bytes();
873 let mut out = Vec::new();
874 out.extend_from_slice(b"RIFF");
875 out.extend_from_slice(&(36u32 + data.len() as u32).to_le_bytes());
876 out.extend_from_slice(b"WAVE");
877 out.extend_from_slice(b"fmt ");
878 out.extend_from_slice(&16u32.to_le_bytes());
879 out.extend_from_slice(&1u16.to_le_bytes());
880 out.extend_from_slice(&1u16.to_le_bytes());
881 out.extend_from_slice(&8000u32.to_le_bytes());
882 out.extend_from_slice(&16000u32.to_le_bytes());
883 out.extend_from_slice(&2u16.to_le_bytes());
884 out.extend_from_slice(&16u16.to_le_bytes());
885 out.extend_from_slice(b"data");
886 out.extend_from_slice(&(data.len() as u32).to_le_bytes());
887 out.extend_from_slice(&data);
888 out
889 }
890
891 #[derive(Default)]
892 struct RecordingPlayer {
893 played: RefCell<Vec<(SoundId, PlaybackParams)>>,
894 unloaded: RefCell<Vec<SoundId>>,
895 next: Cell<u32>,
896 }
897
898 impl AudioPlayer for RecordingPlayer {
899 fn load_clip(&self, _clip: AudioClip) -> Result<SoundId, AudioError> {
900 let next = self.next.get() + 1;
901 self.next.set(next);
902 Ok(SoundId::from_raw(next))
903 }
904 fn play(&self, id: SoundId, params: PlaybackParams) {
905 self.played.borrow_mut().push((id, params));
906 }
907 fn play_loop(&self, _id: SoundId, _params: PlaybackParams) -> VoiceId {
908 VoiceId::from_raw(7)
909 }
910 fn stop(&self, _id: SoundId) {}
911 fn stop_voice(&self, _voice: VoiceId) {}
912 fn set_master_volume(&self, _volume: f32) {}
913 fn unload(&self, id: SoundId) {
914 self.unloaded.borrow_mut().push(id);
915 }
916 fn is_available(&self) -> bool {
917 true
918 }
919 }
920
921 #[test]
922 fn playback_params_default_is_neutral() {
923 let params = PlaybackParams::default();
924 assert_eq!(params.volume, 1.0);
925 assert_eq!(params.rate, 1.0);
926 assert_eq!(params.pan, 0.0);
927 assert_eq!(params.bus, AudioBus::Effects);
928 assert_eq!(params, PlaybackParams::new());
929 assert_eq!(params, PlaybackParams::DEFAULT);
930 }
931
932 #[test]
933 fn playback_params_sanitizes_out_of_range_and_nan() {
934 let wild = PlaybackParams {
935 volume: f32::NAN,
936 rate: 1_000.0,
937 pan: -9.0,
938 bus: AudioBus::Music,
939 }
940 .sanitized();
941 assert_eq!(wild.volume, 1.0);
942 assert_eq!(wild.rate, PlaybackParams::MAX_RATE);
943 assert_eq!(wild.pan, -1.0);
944 assert_eq!(wild.bus, AudioBus::Music);
945
946 let slow = PlaybackParams::new().rate(0.0).sanitized();
947 assert_eq!(slow.rate, PlaybackParams::MIN_RATE);
948 }
949
950 #[test]
951 fn pitch_semitones_maps_octaves_to_rate() {
952 let up = PlaybackParams::new().pitch_semitones(12.0);
953 assert!((up.rate - 2.0).abs() < 1e-5);
954 let down = PlaybackParams::new().pitch_semitones(-12.0);
955 assert!((down.rate - 0.5).abs() < 1e-5);
956 let broken = PlaybackParams::new().pitch_semitones(f32::NAN);
957 assert_eq!(broken.rate, 1.0);
958 }
959
960 #[test]
961 fn pan_gains_are_constant_power() {
962 let (left, right) = PlaybackParams::new().gains();
963 assert!((left - right).abs() < 1e-6);
964 assert!((left * left + right * right - 1.0).abs() < 1e-5);
965
966 let (left, right) = PlaybackParams::new().pan(-1.0).gains();
967 assert!((left - 1.0).abs() < 1e-5);
968 assert!(right.abs() < 1e-5);
969
970 let (left, right) = PlaybackParams::new().pan(1.0).gains();
971 assert!(left.abs() < 1e-5);
972 assert!((right - 1.0).abs() < 1e-5);
973 }
974
975 #[test]
976 fn audio_bus_indices_round_trip() {
977 for bus in AudioBus::ALL {
978 assert_eq!(AudioBus::from_index(bus.index()), Some(bus));
979 }
980 assert_eq!(AudioBus::from_index(2), None);
981 assert_eq!(AudioBus::default(), AudioBus::Effects);
982 }
983
984 #[test]
985 fn noop_player_hands_out_handles_and_keeps_settings() {
986 clear_platform_audio();
987 let player = default_audio();
988 assert!(!player.is_available());
989
990 let id = player.load(&tiny_wav()).expect("no-op load succeeds");
991 assert!(id.is_valid());
992 let second = player.load(&tiny_wav()).expect("no-op load succeeds");
993 assert_ne!(id, second);
994
995 player.play(id, PlaybackParams::new());
997 let voice = player.play_loop(id, PlaybackParams::new());
998 assert!(voice.is_valid());
999 player.stop_voice(voice);
1000 player.stop(id);
1001 player.stop_all();
1002 player.set_voice_params(voice, PlaybackParams::new());
1003 player.unload(id);
1004 player.suspend();
1005 player.resume();
1006
1007 player.set_master_volume(0.25);
1008 assert_eq!(player.master_volume(), 0.25);
1009 player.set_master_volume(f32::NAN);
1010 assert_eq!(player.master_volume(), 1.0);
1011 player.set_bus_enabled(AudioBus::Music, false);
1012 assert!(!player.bus_enabled(AudioBus::Music));
1013 assert!(player.bus_enabled(AudioBus::Effects));
1014 player.set_bus_volume(AudioBus::Effects, 0.5);
1015 assert_eq!(player.bus_volume(AudioBus::Effects), 0.5);
1016 }
1017
1018 #[test]
1019 fn noop_player_rejects_invalid_loop_handle() {
1020 let player = NoopAudioPlayer::new();
1021 assert_eq!(
1022 player.play_loop(SoundId::NONE, PlaybackParams::new()),
1023 VoiceId::NONE
1024 );
1025 }
1026
1027 #[test]
1028 fn registered_player_replaces_the_default() {
1029 clear_platform_audio();
1030 assert!(!default_audio().is_available());
1031 let player: AudioPlayerRef = Rc::new(RecordingPlayer::default());
1032 set_platform_audio(player);
1033 assert!(default_audio().is_available());
1034 clear_platform_audio();
1035 assert!(!default_audio().is_available());
1036 }
1037
1038 #[test]
1039 fn audio_clip_validates_shape() {
1040 assert!(matches!(
1041 AudioClip::from_samples(vec![0.0], 0, 44_100),
1042 Err(AudioError::UnsupportedFormat(_))
1043 ));
1044 assert!(matches!(
1045 AudioClip::from_samples(vec![0.0], 3, 44_100),
1046 Err(AudioError::UnsupportedFormat(_))
1047 ));
1048 assert!(matches!(
1049 AudioClip::from_samples(vec![0.0], 1, 0),
1050 Err(AudioError::UnsupportedFormat(_))
1051 ));
1052 assert!(matches!(
1053 AudioClip::from_samples(Vec::new(), 1, 44_100),
1054 Err(AudioError::Decode(_))
1055 ));
1056 assert!(matches!(
1057 AudioClip::from_samples(vec![0.0, 0.0, 0.0], 2, 44_100),
1058 Err(AudioError::Decode(_))
1059 ));
1060
1061 let clip = AudioClip::from_samples(vec![0.0, 0.5], 2, 44_100).expect("valid clip");
1062 assert_eq!(clip.frames(), 1);
1063 assert_eq!(clip.channels(), 2);
1064 assert!(clip.duration_secs() > 0.0);
1065 assert_eq!(clip.shared_samples().len(), 2);
1066 assert!(format!("{clip:?}").contains("AudioClip"));
1067 }
1068
1069 #[test]
1070 fn audio_clip_decode_rejects_unknown_container() {
1071 assert!(matches!(
1072 AudioClip::decode(b"OggS not really"),
1073 Err(AudioError::UnsupportedFormat(_))
1074 ));
1075 }
1076
1077 #[test]
1078 fn sound_bank_loads_applies_base_volume_and_unloads_on_drop() {
1079 let player = Rc::new(RecordingPlayer::default());
1080 let wav = tiny_wav();
1081 let specs = [
1082 SoundSpec::new("hit", &wav).volume(0.5),
1083 SoundSpec::new("music", &wav).bus(AudioBus::Music),
1084 SoundSpec::new("broken", b"not audio"),
1085 ];
1086 let player_ref: AudioPlayerRef = player.clone();
1087 let bank = SoundBank::load(player_ref, &specs);
1088
1089 assert_eq!(bank.len(), 3);
1090 assert!(!bank.is_empty());
1091 assert_eq!(bank.failures().len(), 1);
1092 assert_eq!(bank.failures()[0].name, "broken");
1093 assert!(!bank.id(2).is_valid());
1094 assert_eq!(bank.find("music"), Some(bank.id(1)));
1095 assert_eq!(bank.find("absent"), None);
1096 assert_eq!(bank[0], bank.id(0));
1097 assert_eq!(bank[99], SoundId::NONE);
1098 assert!(format!("{bank:?}").contains("SoundBank"));
1099
1100 bank.play(0);
1101 bank.play_with(1, PlaybackParams::new().volume(0.5));
1102 bank.play_named("hit", PlaybackParams::new().pan(1.0));
1103 bank.play_with(2, PlaybackParams::new());
1104 bank.play_named("absent", PlaybackParams::new());
1105 assert_eq!(bank.play_loop(2, PlaybackParams::new()), VoiceId::NONE);
1106 assert!(bank.play_loop(0, PlaybackParams::new()).is_valid());
1107 assert_eq!(bank.play_loop(99, PlaybackParams::new()), VoiceId::NONE);
1108 bank.stop(0);
1109 bank.stop(2);
1110
1111 let played = player.played.borrow().clone();
1112 assert_eq!(played.len(), 3);
1113 assert!((played[0].1.volume - 0.5).abs() < 1e-6);
1114 assert_eq!(played[0].1.bus, AudioBus::Effects);
1115 assert!((played[1].1.volume - 0.5).abs() < 1e-6);
1116 assert_eq!(played[1].1.bus, AudioBus::Music);
1117 assert!((played[2].1.pan - 1.0).abs() < 1e-6);
1118
1119 drop(bank);
1120 assert_eq!(player.unloaded.borrow().len(), 3);
1121 }
1122
1123 #[test]
1124 fn sound_bank_key_tracks_names_and_lengths() {
1125 let a = [1u8, 2, 3];
1126 let b = [1u8, 2, 3, 4];
1127 assert_eq!(
1128 sound_bank_key(&[SoundSpec::new("x", &a)]),
1129 sound_bank_key(&[SoundSpec::new("x", &a)])
1130 );
1131 assert_ne!(
1132 sound_bank_key(&[SoundSpec::new("x", &a)]),
1133 sound_bank_key(&[SoundSpec::new("y", &a)])
1134 );
1135 assert_ne!(
1136 sound_bank_key(&[SoundSpec::new("x", &a)]),
1137 sound_bank_key(&[SoundSpec::new("x", &b)])
1138 );
1139 assert_ne!(
1140 sound_bank_key(&[SoundSpec::new("x", &a)]),
1141 sound_bank_key(&[SoundSpec::new("x", &a), SoundSpec::new("x", &a)])
1142 );
1143 }
1144
1145 #[test]
1146 fn provide_audio_publishes_the_platform_player() {
1147 clear_platform_audio();
1148 let player: AudioPlayerRef = Rc::new(RecordingPlayer::default());
1149 set_platform_audio(player);
1150
1151 let captured = Rc::new(RefCell::new(None));
1152 {
1153 let captured = Rc::clone(&captured);
1154 run_test_composition(move || {
1155 let captured = Rc::clone(&captured);
1156 ProvideAudio(move || {
1157 *captured.borrow_mut() = Some(local_audio().current().is_available());
1158 });
1159 });
1160 }
1161
1162 assert_eq!(*captured.borrow(), Some(true));
1163 clear_platform_audio();
1164 }
1165
1166 #[test]
1167 fn local_audio_defaults_to_the_noop_player() {
1168 clear_platform_audio();
1169 let captured = Rc::new(RefCell::new(None));
1170 {
1171 let captured = Rc::clone(&captured);
1172 run_test_composition(move || {
1173 let captured = Rc::clone(&captured);
1174 ProvideAudio(move || {
1175 *captured.borrow_mut() = Some(local_audio().current().is_available());
1176 });
1177 });
1178 }
1179 assert_eq!(*captured.borrow(), Some(false));
1180 }
1181
1182 #[test]
1183 fn remember_sound_bank_loads_once_across_recompositions() {
1184 clear_platform_audio();
1185 let player = Rc::new(RecordingPlayer::default());
1186 let player_ref: AudioPlayerRef = player.clone();
1187 set_platform_audio(player_ref);
1188
1189 let wav = tiny_wav();
1190 let bank_len = Rc::new(Cell::new(0usize));
1191 let bank_len_build = Rc::clone(&bank_len);
1192 let mut build = move || {
1193 let specs = [SoundSpec::new("a", &wav), SoundSpec::new("b", &wav)];
1194 let bank = rememberSoundBank(&specs);
1195 bank_len_build.set(bank.len());
1196 };
1197
1198 let key = cranpose_core::location_key(file!(), line!(), column!());
1199 let mut composition = cranpose_core::Composition::new(cranpose_core::MemoryApplier::new());
1200 composition.render(key, &mut build).expect("first render");
1201 composition.render(key, &mut build).expect("second render");
1202
1203 assert_eq!(bank_len.get(), 2);
1204 assert_eq!(player.next.get(), 2, "the bank decodes once across renders");
1205 clear_platform_audio();
1206 }
1207}