1mod wav;
26
27use crate::registry::ServiceRegistry;
28use cranpose_core::compositionLocalOfWithPolicy;
29use cranpose_core::CompositionLocal;
30use cranpose_core::CompositionLocalProvider;
31use cranpose_macros::composable;
32use parking_lot::Mutex;
33use std::cell::RefCell;
34use std::fmt;
35use std::ops::Index;
36use std::rc::Rc;
37use std::sync::{Arc, OnceLock};
38
39#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
44pub struct SoundId(u32);
45
46impl SoundId {
47 pub const NONE: SoundId = SoundId(0);
49
50 pub fn from_raw(raw: u32) -> Self {
52 SoundId(raw)
53 }
54
55 pub fn raw(self) -> u32 {
57 self.0
58 }
59
60 pub fn is_valid(self) -> bool {
62 self.0 != 0
63 }
64}
65
66#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
69pub struct VoiceId(u64);
70
71impl VoiceId {
72 pub const NONE: VoiceId = VoiceId(0);
74
75 pub fn from_raw(raw: u64) -> Self {
77 VoiceId(raw)
78 }
79
80 pub fn raw(self) -> u64 {
82 self.0
83 }
84
85 pub fn is_valid(self) -> bool {
87 self.0 != 0
88 }
89}
90
91#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
94pub enum AudioBus {
95 #[default]
97 Effects,
98 Music,
100}
101
102impl AudioBus {
103 pub const ALL: [AudioBus; 2] = [AudioBus::Effects, AudioBus::Music];
105
106 pub fn index(self) -> usize {
108 match self {
109 AudioBus::Effects => 0,
110 AudioBus::Music => 1,
111 }
112 }
113
114 pub fn from_index(index: usize) -> Option<AudioBus> {
116 AudioBus::ALL.get(index).copied()
117 }
118}
119
120#[derive(Clone, Copy, PartialEq, Debug)]
125pub struct PlaybackParams {
126 pub volume: f32,
128 pub rate: f32,
131 pub pan: f32,
133 pub bus: AudioBus,
135}
136
137impl PlaybackParams {
138 pub const DEFAULT: PlaybackParams = PlaybackParams {
140 volume: 1.0,
141 rate: 1.0,
142 pan: 0.0,
143 bus: AudioBus::Effects,
144 };
145
146 pub const MIN_RATE: f32 = 0.05;
148 pub const MAX_RATE: f32 = 8.0;
150 pub const MAX_VOLUME: f32 = 4.0;
152
153 pub const fn new() -> PlaybackParams {
155 PlaybackParams::DEFAULT
156 }
157
158 pub fn volume(mut self, volume: f32) -> PlaybackParams {
160 self.volume = volume;
161 self
162 }
163
164 pub fn rate(mut self, rate: f32) -> PlaybackParams {
166 self.rate = rate;
167 self
168 }
169
170 pub fn pan(mut self, pan: f32) -> PlaybackParams {
172 self.pan = pan;
173 self
174 }
175
176 pub fn bus(mut self, bus: AudioBus) -> PlaybackParams {
178 self.bus = bus;
179 self
180 }
181
182 pub fn pitch_semitones(self, semitones: f32) -> PlaybackParams {
187 let semitones = if semitones.is_finite() {
188 semitones
189 } else {
190 0.0
191 };
192 self.rate(2.0f32.powf(semitones / 12.0))
193 }
194
195 pub fn sanitized(self) -> PlaybackParams {
200 fn finite(value: f32, fallback: f32) -> f32 {
201 if value.is_finite() {
202 value
203 } else {
204 fallback
205 }
206 }
207 PlaybackParams {
208 volume: finite(self.volume, 1.0).clamp(0.0, PlaybackParams::MAX_VOLUME),
209 rate: finite(self.rate, 1.0).clamp(PlaybackParams::MIN_RATE, PlaybackParams::MAX_RATE),
210 pan: finite(self.pan, 0.0).clamp(-1.0, 1.0),
211 bus: self.bus,
212 }
213 }
214
215 pub fn gains(self) -> (f32, f32) {
219 let params = self.sanitized();
220 let angle = (params.pan + 1.0) * std::f32::consts::FRAC_PI_4;
222 (params.volume * angle.cos(), params.volume * angle.sin())
223 }
224}
225
226impl Default for PlaybackParams {
227 fn default() -> PlaybackParams {
228 PlaybackParams::DEFAULT
229 }
230}
231
232#[derive(Clone)]
238pub struct AudioClip {
239 samples: Arc<[f32]>,
240 channels: u16,
241 sample_rate: u32,
242}
243
244impl AudioClip {
245 pub const MAX_FRAMES: usize = 1 << 26;
248
249 pub fn from_samples(
251 samples: Vec<f32>,
252 channels: u16,
253 sample_rate: u32,
254 ) -> Result<AudioClip, AudioError> {
255 if channels == 0 || channels > 2 {
256 return Err(AudioError::UnsupportedFormat(format!(
257 "clips must be mono or stereo, got {channels} channels"
258 )));
259 }
260 if sample_rate == 0 {
261 return Err(AudioError::UnsupportedFormat(
262 "clips must declare a non-zero sample rate".to_string(),
263 ));
264 }
265 if samples.is_empty() {
266 return Err(AudioError::Decode("clip holds no samples".to_string()));
267 }
268 if !samples.len().is_multiple_of(usize::from(channels)) {
269 return Err(AudioError::Decode(format!(
270 "clip holds {} samples, which is not a whole number of {channels}-channel frames",
271 samples.len()
272 )));
273 }
274 if samples.len() / usize::from(channels) > AudioClip::MAX_FRAMES {
275 return Err(AudioError::Decode(
276 "clip exceeds the maximum in-memory length".to_string(),
277 ));
278 }
279 Ok(AudioClip {
280 samples: samples.into(),
281 channels,
282 sample_rate,
283 })
284 }
285
286 pub fn decode(bytes: &[u8]) -> Result<AudioClip, AudioError> {
292 if wav::is_wav(bytes) {
293 wav::decode(bytes)
294 } else {
295 Err(AudioError::UnsupportedFormat(
296 "only RIFF/WAVE clips are decoded by the framework".to_string(),
297 ))
298 }
299 }
300
301 pub fn samples(&self) -> &[f32] {
303 &self.samples
304 }
305
306 pub fn shared_samples(&self) -> Arc<[f32]> {
308 Arc::clone(&self.samples)
309 }
310
311 pub fn channels(&self) -> u16 {
313 self.channels
314 }
315
316 pub fn sample_rate(&self) -> u32 {
318 self.sample_rate
319 }
320
321 pub fn frames(&self) -> usize {
323 self.samples.len() / usize::from(self.channels)
324 }
325
326 pub fn duration_secs(&self) -> f32 {
328 self.frames() as f32 / self.sample_rate as f32
329 }
330}
331
332impl fmt::Debug for AudioClip {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 f.debug_struct("AudioClip")
335 .field("frames", &self.frames())
336 .field("channels", &self.channels)
337 .field("sample_rate", &self.sample_rate)
338 .finish()
339 }
340}
341
342#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
344pub enum AudioError {
345 #[error("no audio backend is installed")]
347 Unsupported,
348 #[error("unsupported audio format: {0}")]
350 UnsupportedFormat(String),
351 #[error("failed to decode audio: {0}")]
353 Decode(String),
354 #[error("the audio engine already holds its maximum of {capacity} clips")]
356 ClipTableFull {
357 capacity: usize,
359 },
360 #[error("audio backend failure: {0}")]
362 Backend(String),
363}
364
365pub trait AudioPlayer: Send + Sync {
373 fn load_clip(&self, clip: AudioClip) -> Result<SoundId, AudioError>;
375
376 fn load(&self, bytes: &[u8]) -> Result<SoundId, AudioError> {
383 self.load_clip(AudioClip::decode(bytes)?)
384 }
385
386 fn unload(&self, _id: SoundId) {}
388
389 fn play(&self, id: SoundId, params: PlaybackParams);
392
393 fn play_loop(&self, id: SoundId, params: PlaybackParams) -> VoiceId;
395
396 fn stop(&self, id: SoundId);
398
399 fn stop_voice(&self, voice: VoiceId);
401
402 fn stop_all(&self) {}
404
405 fn set_voice_params(&self, _voice: VoiceId, _params: PlaybackParams) {}
408
409 fn set_master_volume(&self, volume: f32);
411
412 fn master_volume(&self) -> f32 {
414 1.0
415 }
416
417 fn set_bus_volume(&self, _bus: AudioBus, _volume: f32) {}
419
420 fn bus_volume(&self, _bus: AudioBus) -> f32 {
422 1.0
423 }
424
425 fn set_bus_enabled(&self, _bus: AudioBus, _enabled: bool) {}
428
429 fn bus_enabled(&self, _bus: AudioBus) -> bool {
431 true
432 }
433
434 fn suspend(&self) {}
437
438 fn resume(&self) {}
440
441 fn is_available(&self) -> bool {
444 false
445 }
446}
447
448pub type AudioPlayerRef = Arc<dyn AudioPlayer>;
450
451#[derive(Default)]
457pub struct NoopAudioPlayer {
458 next_sound: Mutex<u32>,
459 next_voice: Mutex<u64>,
460 master: Mutex<f32>,
461 bus_volumes: Mutex<[f32; 2]>,
462 bus_enabled: Mutex<[bool; 2]>,
463}
464
465impl NoopAudioPlayer {
466 pub fn new() -> NoopAudioPlayer {
468 NoopAudioPlayer {
469 next_sound: Mutex::new(0),
470 next_voice: Mutex::new(0),
471 master: Mutex::new(1.0),
472 bus_volumes: Mutex::new([1.0, 1.0]),
473 bus_enabled: Mutex::new([true, true]),
474 }
475 }
476}
477
478impl AudioPlayer for NoopAudioPlayer {
479 fn load_clip(&self, _clip: AudioClip) -> Result<SoundId, AudioError> {
480 let mut next = self.next_sound.lock();
481 *next = next.saturating_add(1);
482 Ok(SoundId::from_raw(*next))
483 }
484
485 fn play(&self, _id: SoundId, _params: PlaybackParams) {}
486
487 fn play_loop(&self, id: SoundId, _params: PlaybackParams) -> VoiceId {
488 if !id.is_valid() {
489 return VoiceId::NONE;
490 }
491 let mut next = self.next_voice.lock();
492 *next = next.saturating_add(1);
493 VoiceId::from_raw(*next)
494 }
495
496 fn stop(&self, _id: SoundId) {}
497
498 fn stop_voice(&self, _voice: VoiceId) {}
499
500 fn set_master_volume(&self, volume: f32) {
501 *self.master.lock() = if volume.is_finite() {
502 volume.clamp(0.0, 1.0)
503 } else {
504 1.0
505 };
506 }
507
508 fn master_volume(&self) -> f32 {
509 *self.master.lock()
510 }
511
512 fn set_bus_volume(&self, bus: AudioBus, volume: f32) {
513 let mut volumes = self.bus_volumes.lock();
514 volumes[bus.index()] = if volume.is_finite() {
515 volume.clamp(0.0, 1.0)
516 } else {
517 1.0
518 };
519 }
520
521 fn bus_volume(&self, bus: AudioBus) -> f32 {
522 self.bus_volumes.lock()[bus.index()]
523 }
524
525 fn set_bus_enabled(&self, bus: AudioBus, enabled: bool) {
526 self.bus_enabled.lock()[bus.index()] = enabled;
527 }
528
529 fn bus_enabled(&self, bus: AudioBus) -> bool {
530 self.bus_enabled.lock()[bus.index()]
531 }
532}
533
534static PLATFORM_AUDIO: ServiceRegistry<dyn AudioPlayer> = ServiceRegistry::new();
535static NOOP_AUDIO: OnceLock<AudioPlayerRef> = OnceLock::new();
536static DEFAULT_AUDIO: OnceLock<AudioPlayerRef> = OnceLock::new();
537
538struct PlatformAudioPlayer;
539
540fn registered_audio() -> AudioPlayerRef {
541 PLATFORM_AUDIO.get_or_warn("audio").unwrap_or_else(|| {
542 NOOP_AUDIO
543 .get_or_init(|| Arc::new(NoopAudioPlayer::new()))
544 .clone()
545 })
546}
547
548impl AudioPlayer for PlatformAudioPlayer {
549 fn load_clip(&self, clip: AudioClip) -> Result<SoundId, AudioError> {
550 registered_audio().load_clip(clip)
551 }
552
553 fn unload(&self, id: SoundId) {
554 registered_audio().unload(id);
555 }
556
557 fn play(&self, id: SoundId, params: PlaybackParams) {
558 registered_audio().play(id, params);
559 }
560
561 fn play_loop(&self, id: SoundId, params: PlaybackParams) -> VoiceId {
562 registered_audio().play_loop(id, params)
563 }
564
565 fn stop(&self, id: SoundId) {
566 registered_audio().stop(id);
567 }
568
569 fn stop_voice(&self, voice: VoiceId) {
570 registered_audio().stop_voice(voice);
571 }
572
573 fn stop_all(&self) {
574 registered_audio().stop_all();
575 }
576
577 fn set_voice_params(&self, voice: VoiceId, params: PlaybackParams) {
578 registered_audio().set_voice_params(voice, params);
579 }
580
581 fn set_master_volume(&self, volume: f32) {
582 registered_audio().set_master_volume(volume);
583 }
584
585 fn master_volume(&self) -> f32 {
586 registered_audio().master_volume()
587 }
588
589 fn set_bus_volume(&self, bus: AudioBus, volume: f32) {
590 registered_audio().set_bus_volume(bus, volume);
591 }
592
593 fn bus_volume(&self, bus: AudioBus) -> f32 {
594 registered_audio().bus_volume(bus)
595 }
596
597 fn set_bus_enabled(&self, bus: AudioBus, enabled: bool) {
598 registered_audio().set_bus_enabled(bus, enabled);
599 }
600
601 fn bus_enabled(&self, bus: AudioBus) -> bool {
602 registered_audio().bus_enabled(bus)
603 }
604
605 fn suspend(&self) {
606 registered_audio().suspend();
607 }
608
609 fn resume(&self) {
610 registered_audio().resume();
611 }
612
613 fn is_available(&self) -> bool {
614 registered_audio().is_available()
615 }
616}
617
618pub fn set_platform_audio(player: AudioPlayerRef) {
620 PLATFORM_AUDIO.set(player);
621}
622
623pub fn clear_platform_audio() {
625 PLATFORM_AUDIO.clear();
626}
627
628pub fn default_audio() -> AudioPlayerRef {
630 DEFAULT_AUDIO
631 .get_or_init(|| Arc::new(PlatformAudioPlayer))
632 .clone()
633}
634
635pub fn local_audio() -> CompositionLocal<AudioPlayerRef> {
637 thread_local! {
638 static LOCAL_AUDIO: RefCell<Option<CompositionLocal<AudioPlayerRef>>> = const { RefCell::new(None) };
639 }
640
641 LOCAL_AUDIO.with(|cell| {
642 let mut local = cell.borrow_mut();
643 local
644 .get_or_insert_with(|| compositionLocalOfWithPolicy(default_audio, Arc::ptr_eq))
645 .clone()
646 })
647}
648
649#[allow(non_snake_case)]
651#[composable]
652pub fn ProvideAudio(content: impl FnOnce()) {
653 let player = cranpose_core::remember(default_audio).with(|state| state.clone());
654 let local = local_audio();
655 CompositionLocalProvider(vec![local.provides(player)], move || {
656 content();
657 });
658}
659
660#[derive(Clone, Copy, Debug)]
663pub struct SoundSpec<'a> {
664 pub name: &'static str,
666 pub bytes: &'a [u8],
668 pub base_volume: f32,
671 pub bus: AudioBus,
673}
674
675impl<'a> SoundSpec<'a> {
676 pub fn new(name: &'static str, bytes: &'a [u8]) -> SoundSpec<'a> {
678 SoundSpec {
679 name,
680 bytes,
681 base_volume: 1.0,
682 bus: AudioBus::Effects,
683 }
684 }
685
686 pub fn volume(mut self, base_volume: f32) -> SoundSpec<'a> {
688 self.base_volume = base_volume;
689 self
690 }
691
692 pub fn bus(mut self, bus: AudioBus) -> SoundSpec<'a> {
694 self.bus = bus;
695 self
696 }
697}
698
699#[derive(Clone, Copy, Debug)]
701pub struct SoundBankEntry {
702 pub name: &'static str,
704 pub id: SoundId,
706 pub base_volume: f32,
708 pub bus: AudioBus,
710}
711
712#[derive(Clone, Debug)]
714pub struct SoundBankFailure {
715 pub name: &'static str,
717 pub error: AudioError,
719}
720
721struct SoundBankInner {
722 player: AudioPlayerRef,
723 entries: Vec<SoundBankEntry>,
724 failures: Vec<SoundBankFailure>,
725}
726
727impl Drop for SoundBankInner {
728 fn drop(&mut self) {
729 for entry in &self.entries {
730 self.player.unload(entry.id);
731 }
732 }
733}
734
735#[derive(Clone)]
742pub struct SoundBank {
743 inner: Rc<SoundBankInner>,
744}
745
746impl SoundBank {
747 pub fn load(player: AudioPlayerRef, specs: &[SoundSpec<'_>]) -> SoundBank {
750 let mut entries = Vec::with_capacity(specs.len());
751 let mut failures = Vec::new();
752 for spec in specs {
753 match player.load(spec.bytes) {
754 Ok(id) => entries.push(SoundBankEntry {
755 name: spec.name,
756 id,
757 base_volume: spec.base_volume,
758 bus: spec.bus,
759 }),
760 Err(error) => {
761 entries.push(SoundBankEntry {
765 name: spec.name,
766 id: SoundId::NONE,
767 base_volume: spec.base_volume,
768 bus: spec.bus,
769 });
770 failures.push(SoundBankFailure {
771 name: spec.name,
772 error,
773 });
774 }
775 }
776 }
777 SoundBank {
778 inner: Rc::new(SoundBankInner {
779 player,
780 entries,
781 failures,
782 }),
783 }
784 }
785
786 pub fn len(&self) -> usize {
788 self.inner.entries.len()
789 }
790
791 pub fn is_empty(&self) -> bool {
793 self.inner.entries.is_empty()
794 }
795
796 pub fn entries(&self) -> &[SoundBankEntry] {
798 &self.inner.entries
799 }
800
801 pub fn failures(&self) -> &[SoundBankFailure] {
803 &self.inner.failures
804 }
805
806 pub fn player(&self) -> AudioPlayerRef {
808 Arc::clone(&self.inner.player)
809 }
810
811 pub fn id(&self, index: usize) -> SoundId {
813 self.inner
814 .entries
815 .get(index)
816 .map(|entry| entry.id)
817 .unwrap_or(SoundId::NONE)
818 }
819
820 pub fn find(&self, name: &str) -> Option<SoundId> {
822 self.inner
823 .entries
824 .iter()
825 .find(|entry| entry.name == name)
826 .map(|entry| entry.id)
827 }
828
829 pub fn play(&self, index: usize) {
831 self.play_with(index, PlaybackParams::DEFAULT);
832 }
833
834 pub fn play_with(&self, index: usize, params: PlaybackParams) {
837 let Some(entry) = self.inner.entries.get(index) else {
838 return;
839 };
840 if !entry.id.is_valid() {
841 return;
842 }
843 self.inner.player.play(entry.id, entry.apply(params));
844 }
845
846 pub fn play_named(&self, name: &str, params: PlaybackParams) {
848 if let Some(index) = self.inner.entries.iter().position(|e| e.name == name) {
849 self.play_with(index, params);
850 }
851 }
852
853 pub fn play_loop(&self, index: usize, params: PlaybackParams) -> VoiceId {
855 let Some(entry) = self.inner.entries.get(index) else {
856 return VoiceId::NONE;
857 };
858 if !entry.id.is_valid() {
859 return VoiceId::NONE;
860 }
861 self.inner.player.play_loop(entry.id, entry.apply(params))
862 }
863
864 pub fn stop(&self, index: usize) {
866 let id = self.id(index);
867 if id.is_valid() {
868 self.inner.player.stop(id);
869 }
870 }
871}
872
873impl SoundBankEntry {
874 fn apply(&self, params: PlaybackParams) -> PlaybackParams {
875 PlaybackParams {
876 volume: params.volume * self.base_volume,
877 rate: params.rate,
878 pan: params.pan,
879 bus: self.bus,
880 }
881 }
882}
883
884impl fmt::Debug for SoundBank {
885 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
886 f.debug_struct("SoundBank")
887 .field("entries", &self.inner.entries.len())
888 .field("failures", &self.inner.failures.len())
889 .finish()
890 }
891}
892
893impl Index<usize> for SoundBank {
894 type Output = SoundId;
895
896 fn index(&self, index: usize) -> &SoundId {
897 static NONE: SoundId = SoundId::NONE;
898 self.inner
899 .entries
900 .get(index)
901 .map(|entry| &entry.id)
902 .unwrap_or(&NONE)
903 }
904}
905
906#[allow(non_snake_case)]
912#[composable(no_skip)]
913pub fn rememberSoundBank(specs: &[SoundSpec<'_>]) -> SoundBank {
914 let key = sound_bank_key(specs);
915 let player = local_audio().current();
916 cranpose_core::rememberKeyed((key, Arc::as_ptr(&player) as *const () as usize), |_| {
917 SoundBank::load(Arc::clone(&player), specs)
918 })
919}
920
921fn sound_bank_key(specs: &[SoundSpec<'_>]) -> (usize, u64) {
924 let mut hash = 0xcbf2_9ce4_8422_2325u64;
925 for spec in specs {
926 for byte in spec.name.as_bytes() {
927 hash ^= u64::from(*byte);
928 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
929 }
930 hash ^= spec.bytes.len() as u64;
931 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
932 }
933 (specs.len(), hash)
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939 use crate::run_test_composition;
940 use parking_lot::Mutex;
941 use std::cell::Cell;
942
943 fn tiny_wav() -> Vec<u8> {
945 let data = 0i16.to_le_bytes();
946 let mut out = Vec::new();
947 out.extend_from_slice(b"RIFF");
948 out.extend_from_slice(&(36u32 + data.len() as u32).to_le_bytes());
949 out.extend_from_slice(b"WAVE");
950 out.extend_from_slice(b"fmt ");
951 out.extend_from_slice(&16u32.to_le_bytes());
952 out.extend_from_slice(&1u16.to_le_bytes());
953 out.extend_from_slice(&1u16.to_le_bytes());
954 out.extend_from_slice(&8000u32.to_le_bytes());
955 out.extend_from_slice(&16000u32.to_le_bytes());
956 out.extend_from_slice(&2u16.to_le_bytes());
957 out.extend_from_slice(&16u16.to_le_bytes());
958 out.extend_from_slice(b"data");
959 out.extend_from_slice(&(data.len() as u32).to_le_bytes());
960 out.extend_from_slice(&data);
961 out
962 }
963
964 #[derive(Default)]
965 struct RecordingPlayer {
966 played: Mutex<Vec<(SoundId, PlaybackParams)>>,
967 unloaded: Mutex<Vec<SoundId>>,
968 next: Mutex<u32>,
969 }
970
971 impl AudioPlayer for RecordingPlayer {
972 fn load_clip(&self, _clip: AudioClip) -> Result<SoundId, AudioError> {
973 let mut next = self.next.lock();
974 *next += 1;
975 Ok(SoundId::from_raw(*next))
976 }
977 fn play(&self, id: SoundId, params: PlaybackParams) {
978 self.played.lock().push((id, params));
979 }
980 fn play_loop(&self, _id: SoundId, _params: PlaybackParams) -> VoiceId {
981 VoiceId::from_raw(7)
982 }
983 fn stop(&self, _id: SoundId) {}
984 fn stop_voice(&self, _voice: VoiceId) {}
985 fn set_master_volume(&self, _volume: f32) {}
986 fn unload(&self, id: SoundId) {
987 self.unloaded.lock().push(id);
988 }
989 fn is_available(&self) -> bool {
990 true
991 }
992 }
993
994 #[test]
995 fn playback_params_default_is_neutral() {
996 let params = PlaybackParams::default();
997 assert_eq!(params.volume, 1.0);
998 assert_eq!(params.rate, 1.0);
999 assert_eq!(params.pan, 0.0);
1000 assert_eq!(params.bus, AudioBus::Effects);
1001 assert_eq!(params, PlaybackParams::new());
1002 assert_eq!(params, PlaybackParams::DEFAULT);
1003 }
1004
1005 #[test]
1006 fn playback_params_sanitizes_out_of_range_and_nan() {
1007 let wild = PlaybackParams {
1008 volume: f32::NAN,
1009 rate: 1_000.0,
1010 pan: -9.0,
1011 bus: AudioBus::Music,
1012 }
1013 .sanitized();
1014 assert_eq!(wild.volume, 1.0);
1015 assert_eq!(wild.rate, PlaybackParams::MAX_RATE);
1016 assert_eq!(wild.pan, -1.0);
1017 assert_eq!(wild.bus, AudioBus::Music);
1018
1019 let slow = PlaybackParams::new().rate(0.0).sanitized();
1020 assert_eq!(slow.rate, PlaybackParams::MIN_RATE);
1021 }
1022
1023 #[test]
1024 fn pitch_semitones_maps_octaves_to_rate() {
1025 let up = PlaybackParams::new().pitch_semitones(12.0);
1026 assert!((up.rate - 2.0).abs() < 1e-5);
1027 let down = PlaybackParams::new().pitch_semitones(-12.0);
1028 assert!((down.rate - 0.5).abs() < 1e-5);
1029 let broken = PlaybackParams::new().pitch_semitones(f32::NAN);
1030 assert_eq!(broken.rate, 1.0);
1031 }
1032
1033 #[test]
1034 fn pan_gains_are_constant_power() {
1035 let (left, right) = PlaybackParams::new().gains();
1036 assert!((left - right).abs() < 1e-6);
1037 assert!((left * left + right * right - 1.0).abs() < 1e-5);
1038
1039 let (left, right) = PlaybackParams::new().pan(-1.0).gains();
1040 assert!((left - 1.0).abs() < 1e-5);
1041 assert!(right.abs() < 1e-5);
1042
1043 let (left, right) = PlaybackParams::new().pan(1.0).gains();
1044 assert!(left.abs() < 1e-5);
1045 assert!((right - 1.0).abs() < 1e-5);
1046 }
1047
1048 #[test]
1049 fn audio_bus_indices_round_trip() {
1050 for bus in AudioBus::ALL {
1051 assert_eq!(AudioBus::from_index(bus.index()), Some(bus));
1052 }
1053 assert_eq!(AudioBus::from_index(2), None);
1054 assert_eq!(AudioBus::default(), AudioBus::Effects);
1055 }
1056
1057 #[test]
1058 fn noop_player_hands_out_handles_and_keeps_settings() {
1059 let _guard = crate::registry::test_service_guard();
1060 clear_platform_audio();
1061 let player = default_audio();
1062 assert!(!player.is_available());
1063
1064 let id = player.load(&tiny_wav()).expect("no-op load succeeds");
1065 assert!(id.is_valid());
1066 let second = player.load(&tiny_wav()).expect("no-op load succeeds");
1067 assert_ne!(id, second);
1068
1069 player.play(id, PlaybackParams::new());
1071 let voice = player.play_loop(id, PlaybackParams::new());
1072 assert!(voice.is_valid());
1073 player.stop_voice(voice);
1074 player.stop(id);
1075 player.stop_all();
1076 player.set_voice_params(voice, PlaybackParams::new());
1077 player.unload(id);
1078 player.suspend();
1079 player.resume();
1080
1081 player.set_master_volume(0.25);
1082 assert_eq!(player.master_volume(), 0.25);
1083 player.set_master_volume(f32::NAN);
1084 assert_eq!(player.master_volume(), 1.0);
1085 player.set_bus_enabled(AudioBus::Music, false);
1086 assert!(!player.bus_enabled(AudioBus::Music));
1087 assert!(player.bus_enabled(AudioBus::Effects));
1088 player.set_bus_volume(AudioBus::Effects, 0.5);
1089 assert_eq!(player.bus_volume(AudioBus::Effects), 0.5);
1090 }
1091
1092 #[test]
1093 fn noop_player_rejects_invalid_loop_handle() {
1094 let _guard = crate::registry::test_service_guard();
1095 let player = NoopAudioPlayer::new();
1096 assert_eq!(
1097 player.play_loop(SoundId::NONE, PlaybackParams::new()),
1098 VoiceId::NONE
1099 );
1100 }
1101
1102 #[test]
1103 fn registered_player_replaces_the_default() {
1104 let _guard = crate::registry::test_service_guard();
1105 clear_platform_audio();
1106 assert!(!default_audio().is_available());
1107 let player: AudioPlayerRef = Arc::new(RecordingPlayer::default());
1108 set_platform_audio(player);
1109 assert!(default_audio().is_available());
1110 clear_platform_audio();
1111 assert!(!default_audio().is_available());
1112 }
1113
1114 #[test]
1115 fn audio_clip_validates_shape() {
1116 assert!(matches!(
1117 AudioClip::from_samples(vec![0.0], 0, 44_100),
1118 Err(AudioError::UnsupportedFormat(_))
1119 ));
1120 assert!(matches!(
1121 AudioClip::from_samples(vec![0.0], 3, 44_100),
1122 Err(AudioError::UnsupportedFormat(_))
1123 ));
1124 assert!(matches!(
1125 AudioClip::from_samples(vec![0.0], 1, 0),
1126 Err(AudioError::UnsupportedFormat(_))
1127 ));
1128 assert!(matches!(
1129 AudioClip::from_samples(Vec::new(), 1, 44_100),
1130 Err(AudioError::Decode(_))
1131 ));
1132 assert!(matches!(
1133 AudioClip::from_samples(vec![0.0, 0.0, 0.0], 2, 44_100),
1134 Err(AudioError::Decode(_))
1135 ));
1136
1137 let clip = AudioClip::from_samples(vec![0.0, 0.5], 2, 44_100).expect("valid clip");
1138 assert_eq!(clip.frames(), 1);
1139 assert_eq!(clip.channels(), 2);
1140 assert!(clip.duration_secs() > 0.0);
1141 assert_eq!(clip.shared_samples().len(), 2);
1142 assert!(format!("{clip:?}").contains("AudioClip"));
1143 }
1144
1145 #[test]
1146 fn audio_clip_decode_rejects_unknown_container() {
1147 assert!(matches!(
1148 AudioClip::decode(b"OggS not really"),
1149 Err(AudioError::UnsupportedFormat(_))
1150 ));
1151 }
1152
1153 #[test]
1154 fn sound_bank_loads_applies_base_volume_and_unloads_on_drop() {
1155 let player = Arc::new(RecordingPlayer::default());
1156 let wav = tiny_wav();
1157 let specs = [
1158 SoundSpec::new("hit", &wav).volume(0.5),
1159 SoundSpec::new("music", &wav).bus(AudioBus::Music),
1160 SoundSpec::new("broken", b"not audio"),
1161 ];
1162 let player_ref: AudioPlayerRef = player.clone();
1163 let bank = SoundBank::load(player_ref, &specs);
1164
1165 assert_eq!(bank.len(), 3);
1166 assert!(!bank.is_empty());
1167 assert_eq!(bank.failures().len(), 1);
1168 assert_eq!(bank.failures()[0].name, "broken");
1169 assert!(!bank.id(2).is_valid());
1170 assert_eq!(bank.find("music"), Some(bank.id(1)));
1171 assert_eq!(bank.find("absent"), None);
1172 assert_eq!(bank[0], bank.id(0));
1173 assert_eq!(bank[99], SoundId::NONE);
1174 assert!(format!("{bank:?}").contains("SoundBank"));
1175
1176 bank.play(0);
1177 bank.play_with(1, PlaybackParams::new().volume(0.5));
1178 bank.play_named("hit", PlaybackParams::new().pan(1.0));
1179 bank.play_with(2, PlaybackParams::new());
1180 bank.play_named("absent", PlaybackParams::new());
1181 assert_eq!(bank.play_loop(2, PlaybackParams::new()), VoiceId::NONE);
1182 assert!(bank.play_loop(0, PlaybackParams::new()).is_valid());
1183 assert_eq!(bank.play_loop(99, PlaybackParams::new()), VoiceId::NONE);
1184 bank.stop(0);
1185 bank.stop(2);
1186
1187 let played = player.played.lock().clone();
1188 assert_eq!(played.len(), 3);
1189 assert!((played[0].1.volume - 0.5).abs() < 1e-6);
1190 assert_eq!(played[0].1.bus, AudioBus::Effects);
1191 assert!((played[1].1.volume - 0.5).abs() < 1e-6);
1192 assert_eq!(played[1].1.bus, AudioBus::Music);
1193 assert!((played[2].1.pan - 1.0).abs() < 1e-6);
1194
1195 drop(bank);
1196 assert_eq!(player.unloaded.lock().len(), 3);
1197 }
1198
1199 #[test]
1200 fn sound_bank_key_tracks_names_and_lengths() {
1201 let a = [1u8, 2, 3];
1202 let b = [1u8, 2, 3, 4];
1203 assert_eq!(
1204 sound_bank_key(&[SoundSpec::new("x", &a)]),
1205 sound_bank_key(&[SoundSpec::new("x", &a)])
1206 );
1207 assert_ne!(
1208 sound_bank_key(&[SoundSpec::new("x", &a)]),
1209 sound_bank_key(&[SoundSpec::new("y", &a)])
1210 );
1211 assert_ne!(
1212 sound_bank_key(&[SoundSpec::new("x", &a)]),
1213 sound_bank_key(&[SoundSpec::new("x", &b)])
1214 );
1215 assert_ne!(
1216 sound_bank_key(&[SoundSpec::new("x", &a)]),
1217 sound_bank_key(&[SoundSpec::new("x", &a), SoundSpec::new("x", &a)])
1218 );
1219 }
1220
1221 #[test]
1222 fn provide_audio_publishes_the_platform_player() {
1223 let _guard = crate::registry::test_service_guard();
1224 clear_platform_audio();
1225 let player: AudioPlayerRef = Arc::new(RecordingPlayer::default());
1226 set_platform_audio(player);
1227
1228 let captured = Rc::new(RefCell::new(None));
1229 {
1230 let captured = Rc::clone(&captured);
1231 run_test_composition(move || {
1232 let captured = Rc::clone(&captured);
1233 ProvideAudio(move || {
1234 *captured.borrow_mut() = Some(local_audio().current().is_available());
1235 });
1236 });
1237 }
1238
1239 assert_eq!(*captured.borrow(), Some(true));
1240 clear_platform_audio();
1241 }
1242
1243 #[test]
1244 fn local_audio_defaults_to_the_noop_player() {
1245 let _guard = crate::registry::test_service_guard();
1246 clear_platform_audio();
1247 let captured = Rc::new(RefCell::new(None));
1248 {
1249 let captured = Rc::clone(&captured);
1250 run_test_composition(move || {
1251 let captured = Rc::clone(&captured);
1252 ProvideAudio(move || {
1253 *captured.borrow_mut() = Some(local_audio().current().is_available());
1254 });
1255 });
1256 }
1257 assert_eq!(*captured.borrow(), Some(false));
1258 }
1259
1260 #[test]
1261 fn remember_sound_bank_loads_once_across_recompositions() {
1262 let _guard = crate::registry::test_service_guard();
1263 clear_platform_audio();
1264 let player = Arc::new(RecordingPlayer::default());
1265 let player_ref: AudioPlayerRef = player.clone();
1266 set_platform_audio(player_ref);
1267
1268 let wav = tiny_wav();
1269 let bank_len = Rc::new(Cell::new(0usize));
1270 let bank_len_build = Rc::clone(&bank_len);
1271 let mut build = move || {
1272 let specs = [SoundSpec::new("a", &wav), SoundSpec::new("b", &wav)];
1273 let bank = rememberSoundBank(&specs);
1274 bank_len_build.set(bank.len());
1275 };
1276
1277 let key = cranpose_core::location_key(file!(), line!(), column!());
1278 let mut composition = cranpose_core::Composition::new(cranpose_core::MemoryApplier::new());
1279 composition.render(key, &mut build).expect("first render");
1280 composition.render(key, &mut build).expect("second render");
1281
1282 assert_eq!(bank_len.get(), 2);
1283 assert_eq!(
1284 *player.next.lock(),
1285 2,
1286 "the bank decodes once across renders"
1287 );
1288 clear_platform_audio();
1289 }
1290}