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