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;
219 (params.volume * angle.cos(), params.volume * angle.sin())
220 }
221}
222
223impl Default for PlaybackParams {
224 fn default() -> PlaybackParams {
225 PlaybackParams::DEFAULT
226 }
227}
228
229#[derive(Clone)]
235pub struct AudioClip {
236 samples: Arc<[f32]>,
237 channels: u16,
238 sample_rate: u32,
239}
240
241impl AudioClip {
242 pub const MAX_FRAMES: usize = 1 << 26;
245
246 pub fn from_samples(
248 samples: Vec<f32>,
249 channels: u16,
250 sample_rate: u32,
251 ) -> Result<AudioClip, AudioError> {
252 if channels == 0 || channels > 2 {
253 return Err(AudioError::UnsupportedFormat(format!(
254 "clips must be mono or stereo, got {channels} channels"
255 )));
256 }
257 if sample_rate == 0 {
258 return Err(AudioError::UnsupportedFormat(
259 "clips must declare a non-zero sample rate".to_string(),
260 ));
261 }
262 if samples.is_empty() {
263 return Err(AudioError::Decode("clip holds no samples".to_string()));
264 }
265 if !samples.len().is_multiple_of(usize::from(channels)) {
266 return Err(AudioError::Decode(format!(
267 "clip holds {} samples, which is not a whole number of {channels}-channel frames",
268 samples.len()
269 )));
270 }
271 if samples.len() / usize::from(channels) > AudioClip::MAX_FRAMES {
272 return Err(AudioError::Decode(
273 "clip exceeds the maximum in-memory length".to_string(),
274 ));
275 }
276 Ok(AudioClip {
277 samples: samples.into(),
278 channels,
279 sample_rate,
280 })
281 }
282
283 pub fn decode(bytes: &[u8]) -> Result<AudioClip, AudioError> {
289 if wav::is_wav(bytes) {
290 wav::decode(bytes)
291 } else {
292 Err(AudioError::UnsupportedFormat(
293 "only RIFF/WAVE clips are decoded by the framework".to_string(),
294 ))
295 }
296 }
297
298 pub fn samples(&self) -> &[f32] {
300 &self.samples
301 }
302
303 pub fn shared_samples(&self) -> Arc<[f32]> {
305 Arc::clone(&self.samples)
306 }
307
308 pub fn channels(&self) -> u16 {
310 self.channels
311 }
312
313 pub fn sample_rate(&self) -> u32 {
315 self.sample_rate
316 }
317
318 pub fn frames(&self) -> usize {
320 self.samples.len() / usize::from(self.channels)
321 }
322
323 pub fn duration_secs(&self) -> f32 {
325 self.frames() as f32 / self.sample_rate as f32
326 }
327}
328
329impl fmt::Debug for AudioClip {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 f.debug_struct("AudioClip")
332 .field("frames", &self.frames())
333 .field("channels", &self.channels)
334 .field("sample_rate", &self.sample_rate)
335 .finish()
336 }
337}
338
339#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
341pub enum AudioError {
342 #[error("no audio backend is installed")]
344 Unsupported,
345 #[error("unsupported audio format: {0}")]
347 UnsupportedFormat(String),
348 #[error("failed to decode audio: {0}")]
350 Decode(String),
351 #[error("the audio engine already holds its maximum of {capacity} clips")]
353 ClipTableFull {
354 capacity: usize,
356 },
357 #[error("audio backend failure: {0}")]
359 Backend(String),
360}
361
362pub trait AudioPlayer: Send + Sync {
370 fn load_clip(&self, clip: AudioClip) -> Result<SoundId, AudioError>;
372
373 fn load(&self, bytes: &[u8]) -> Result<SoundId, AudioError> {
380 self.load_clip(AudioClip::decode(bytes)?)
381 }
382
383 fn unload(&self, _id: SoundId) {}
385
386 fn play(&self, id: SoundId, params: PlaybackParams);
389
390 fn play_loop(&self, id: SoundId, params: PlaybackParams) -> VoiceId;
392
393 fn stop(&self, id: SoundId);
395
396 fn stop_voice(&self, voice: VoiceId);
398
399 fn stop_all(&self) {}
401
402 fn set_voice_params(&self, _voice: VoiceId, _params: PlaybackParams) {}
405
406 fn set_master_volume(&self, volume: f32);
408
409 fn master_volume(&self) -> f32 {
411 1.0
412 }
413
414 fn set_bus_volume(&self, _bus: AudioBus, _volume: f32) {}
416
417 fn bus_volume(&self, _bus: AudioBus) -> f32 {
419 1.0
420 }
421
422 fn set_bus_enabled(&self, _bus: AudioBus, _enabled: bool) {}
425
426 fn bus_enabled(&self, _bus: AudioBus) -> bool {
428 true
429 }
430
431 fn suspend(&self) {}
434
435 fn resume(&self) {}
437
438 fn is_available(&self) -> bool {
441 false
442 }
443}
444
445pub type AudioPlayerRef = Arc<dyn AudioPlayer>;
447
448#[derive(Default)]
454pub struct NoopAudioPlayer {
455 next_sound: Mutex<u32>,
456 next_voice: Mutex<u64>,
457 master: Mutex<f32>,
458 bus_volumes: Mutex<[f32; 2]>,
459 bus_enabled: Mutex<[bool; 2]>,
460}
461
462impl NoopAudioPlayer {
463 pub fn new() -> NoopAudioPlayer {
465 NoopAudioPlayer {
466 next_sound: Mutex::new(0),
467 next_voice: Mutex::new(0),
468 master: Mutex::new(1.0),
469 bus_volumes: Mutex::new([1.0, 1.0]),
470 bus_enabled: Mutex::new([true, true]),
471 }
472 }
473}
474
475impl AudioPlayer for NoopAudioPlayer {
476 fn load_clip(&self, _clip: AudioClip) -> Result<SoundId, AudioError> {
477 let mut next = self.next_sound.lock();
478 *next = next.saturating_add(1);
479 Ok(SoundId::from_raw(*next))
480 }
481
482 fn play(&self, _id: SoundId, _params: PlaybackParams) {}
483
484 fn play_loop(&self, id: SoundId, _params: PlaybackParams) -> VoiceId {
485 if !id.is_valid() {
486 return VoiceId::NONE;
487 }
488 let mut next = self.next_voice.lock();
489 *next = next.saturating_add(1);
490 VoiceId::from_raw(*next)
491 }
492
493 fn stop(&self, _id: SoundId) {}
494
495 fn stop_voice(&self, _voice: VoiceId) {}
496
497 fn set_master_volume(&self, volume: f32) {
498 *self.master.lock() = if volume.is_finite() {
499 volume.clamp(0.0, 1.0)
500 } else {
501 1.0
502 };
503 }
504
505 fn master_volume(&self) -> f32 {
506 *self.master.lock()
507 }
508
509 fn set_bus_volume(&self, bus: AudioBus, volume: f32) {
510 let mut volumes = self.bus_volumes.lock();
511 volumes[bus.index()] = if volume.is_finite() {
512 volume.clamp(0.0, 1.0)
513 } else {
514 1.0
515 };
516 }
517
518 fn bus_volume(&self, bus: AudioBus) -> f32 {
519 self.bus_volumes.lock()[bus.index()]
520 }
521
522 fn set_bus_enabled(&self, bus: AudioBus, enabled: bool) {
523 self.bus_enabled.lock()[bus.index()] = enabled;
524 }
525
526 fn bus_enabled(&self, bus: AudioBus) -> bool {
527 self.bus_enabled.lock()[bus.index()]
528 }
529}
530
531static PLATFORM_AUDIO: ServiceRegistry<dyn AudioPlayer> = ServiceRegistry::new();
532static NOOP_AUDIO: OnceLock<AudioPlayerRef> = OnceLock::new();
533static DEFAULT_AUDIO: OnceLock<AudioPlayerRef> = OnceLock::new();
534
535struct PlatformAudioPlayer;
536
537fn registered_audio() -> AudioPlayerRef {
538 PLATFORM_AUDIO.get_or_warn("audio").unwrap_or_else(|| {
539 NOOP_AUDIO
540 .get_or_init(|| Arc::new(NoopAudioPlayer::new()))
541 .clone()
542 })
543}
544
545impl AudioPlayer for PlatformAudioPlayer {
546 fn load_clip(&self, clip: AudioClip) -> Result<SoundId, AudioError> {
547 registered_audio().load_clip(clip)
548 }
549
550 fn unload(&self, id: SoundId) {
551 registered_audio().unload(id);
552 }
553
554 fn play(&self, id: SoundId, params: PlaybackParams) {
555 registered_audio().play(id, params);
556 }
557
558 fn play_loop(&self, id: SoundId, params: PlaybackParams) -> VoiceId {
559 registered_audio().play_loop(id, params)
560 }
561
562 fn stop(&self, id: SoundId) {
563 registered_audio().stop(id);
564 }
565
566 fn stop_voice(&self, voice: VoiceId) {
567 registered_audio().stop_voice(voice);
568 }
569
570 fn stop_all(&self) {
571 registered_audio().stop_all();
572 }
573
574 fn set_voice_params(&self, voice: VoiceId, params: PlaybackParams) {
575 registered_audio().set_voice_params(voice, params);
576 }
577
578 fn set_master_volume(&self, volume: f32) {
579 registered_audio().set_master_volume(volume);
580 }
581
582 fn master_volume(&self) -> f32 {
583 registered_audio().master_volume()
584 }
585
586 fn set_bus_volume(&self, bus: AudioBus, volume: f32) {
587 registered_audio().set_bus_volume(bus, volume);
588 }
589
590 fn bus_volume(&self, bus: AudioBus) -> f32 {
591 registered_audio().bus_volume(bus)
592 }
593
594 fn set_bus_enabled(&self, bus: AudioBus, enabled: bool) {
595 registered_audio().set_bus_enabled(bus, enabled);
596 }
597
598 fn bus_enabled(&self, bus: AudioBus) -> bool {
599 registered_audio().bus_enabled(bus)
600 }
601
602 fn suspend(&self) {
603 registered_audio().suspend();
604 }
605
606 fn resume(&self) {
607 registered_audio().resume();
608 }
609
610 fn is_available(&self) -> bool {
611 registered_audio().is_available()
612 }
613}
614
615pub fn set_platform_audio(player: AudioPlayerRef) {
617 PLATFORM_AUDIO.set(player);
618}
619
620pub fn clear_platform_audio() {
622 PLATFORM_AUDIO.clear();
623}
624
625pub fn default_audio() -> AudioPlayerRef {
627 DEFAULT_AUDIO
628 .get_or_init(|| Arc::new(PlatformAudioPlayer))
629 .clone()
630}
631
632pub fn local_audio() -> CompositionLocal<AudioPlayerRef> {
634 thread_local! {
635 static LOCAL_AUDIO: RefCell<Option<CompositionLocal<AudioPlayerRef>>> = const { RefCell::new(None) };
636 }
637
638 LOCAL_AUDIO.with(|cell| {
639 let mut local = cell.borrow_mut();
640 local
641 .get_or_insert_with(|| compositionLocalOfWithPolicy(default_audio, Arc::ptr_eq))
642 .clone()
643 })
644}
645
646#[allow(non_snake_case)]
648#[composable]
649pub fn ProvideAudio(content: impl FnOnce()) {
650 let player = cranpose_core::remember(default_audio).with(|state| state.clone());
651 let local = local_audio();
652 CompositionLocalProvider(vec![local.provides(player)], move || {
653 content();
654 });
655}
656
657#[derive(Clone, Copy, Debug)]
660pub struct SoundSpec<'a> {
661 pub name: &'static str,
663 pub bytes: &'a [u8],
665 pub base_volume: f32,
668 pub bus: AudioBus,
670}
671
672impl<'a> SoundSpec<'a> {
673 pub fn new(name: &'static str, bytes: &'a [u8]) -> SoundSpec<'a> {
675 SoundSpec {
676 name,
677 bytes,
678 base_volume: 1.0,
679 bus: AudioBus::Effects,
680 }
681 }
682
683 pub fn volume(mut self, base_volume: f32) -> SoundSpec<'a> {
685 self.base_volume = base_volume;
686 self
687 }
688
689 pub fn bus(mut self, bus: AudioBus) -> SoundSpec<'a> {
691 self.bus = bus;
692 self
693 }
694}
695
696#[derive(Clone, Copy, Debug)]
698pub struct SoundBankEntry {
699 pub name: &'static str,
701 pub id: SoundId,
703 pub base_volume: f32,
705 pub bus: AudioBus,
707}
708
709#[derive(Clone, Debug)]
711pub struct SoundBankFailure {
712 pub name: &'static str,
714 pub error: AudioError,
716}
717
718struct SoundBankInner {
719 player: AudioPlayerRef,
720 entries: Vec<SoundBankEntry>,
721 failures: Vec<SoundBankFailure>,
722}
723
724impl Drop for SoundBankInner {
725 fn drop(&mut self) {
726 for entry in &self.entries {
727 self.player.unload(entry.id);
728 }
729 }
730}
731
732#[derive(Clone)]
739pub struct SoundBank {
740 inner: Rc<SoundBankInner>,
741}
742
743impl SoundBank {
744 pub fn load(player: AudioPlayerRef, specs: &[SoundSpec<'_>]) -> SoundBank {
747 let mut entries = Vec::with_capacity(specs.len());
748 let mut failures = Vec::new();
749 for spec in specs {
750 match player.load(spec.bytes) {
751 Ok(id) => entries.push(SoundBankEntry {
752 name: spec.name,
753 id,
754 base_volume: spec.base_volume,
755 bus: spec.bus,
756 }),
757 Err(error) => {
758 entries.push(SoundBankEntry {
759 name: spec.name,
760 id: SoundId::NONE,
761 base_volume: spec.base_volume,
762 bus: spec.bus,
763 });
764 failures.push(SoundBankFailure {
765 name: spec.name,
766 error,
767 });
768 }
769 }
770 }
771 SoundBank {
772 inner: Rc::new(SoundBankInner {
773 player,
774 entries,
775 failures,
776 }),
777 }
778 }
779
780 pub fn len(&self) -> usize {
782 self.inner.entries.len()
783 }
784
785 pub fn is_empty(&self) -> bool {
787 self.inner.entries.is_empty()
788 }
789
790 pub fn entries(&self) -> &[SoundBankEntry] {
792 &self.inner.entries
793 }
794
795 pub fn failures(&self) -> &[SoundBankFailure] {
797 &self.inner.failures
798 }
799
800 pub fn player(&self) -> AudioPlayerRef {
802 Arc::clone(&self.inner.player)
803 }
804
805 pub fn id(&self, index: usize) -> SoundId {
807 self.inner
808 .entries
809 .get(index)
810 .map(|entry| entry.id)
811 .unwrap_or(SoundId::NONE)
812 }
813
814 pub fn find(&self, name: &str) -> Option<SoundId> {
816 self.inner
817 .entries
818 .iter()
819 .find(|entry| entry.name == name)
820 .map(|entry| entry.id)
821 }
822
823 pub fn play(&self, index: usize) {
825 self.play_with(index, PlaybackParams::DEFAULT);
826 }
827
828 pub fn play_with(&self, index: usize, params: PlaybackParams) {
831 let Some(entry) = self.inner.entries.get(index) else {
832 return;
833 };
834 if !entry.id.is_valid() {
835 return;
836 }
837 self.inner.player.play(entry.id, entry.apply(params));
838 }
839
840 pub fn play_named(&self, name: &str, params: PlaybackParams) {
842 if let Some(index) = self.inner.entries.iter().position(|e| e.name == name) {
843 self.play_with(index, params);
844 }
845 }
846
847 pub fn play_loop(&self, index: usize, params: PlaybackParams) -> VoiceId {
849 let Some(entry) = self.inner.entries.get(index) else {
850 return VoiceId::NONE;
851 };
852 if !entry.id.is_valid() {
853 return VoiceId::NONE;
854 }
855 self.inner.player.play_loop(entry.id, entry.apply(params))
856 }
857
858 pub fn stop(&self, index: usize) {
860 let id = self.id(index);
861 if id.is_valid() {
862 self.inner.player.stop(id);
863 }
864 }
865}
866
867impl SoundBankEntry {
868 fn apply(&self, params: PlaybackParams) -> PlaybackParams {
869 PlaybackParams {
870 volume: params.volume * self.base_volume,
871 rate: params.rate,
872 pan: params.pan,
873 bus: self.bus,
874 }
875 }
876}
877
878impl fmt::Debug for SoundBank {
879 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
880 f.debug_struct("SoundBank")
881 .field("entries", &self.inner.entries.len())
882 .field("failures", &self.inner.failures.len())
883 .finish()
884 }
885}
886
887impl Index<usize> for SoundBank {
888 type Output = SoundId;
889
890 fn index(&self, index: usize) -> &SoundId {
891 static NONE: SoundId = SoundId::NONE;
892 self.inner
893 .entries
894 .get(index)
895 .map(|entry| &entry.id)
896 .unwrap_or(&NONE)
897 }
898}
899
900#[allow(non_snake_case)]
906#[composable(no_skip)]
907#[track_caller]
908pub fn rememberSoundBank(specs: &[SoundSpec<'_>]) -> SoundBank {
909 let key = sound_bank_key(specs);
910 let player = local_audio().current();
911 cranpose_core::rememberKeyed((key, Arc::as_ptr(&player) as *const () as usize), |_| {
912 SoundBank::load(Arc::clone(&player), specs)
913 })
914}
915
916fn sound_bank_key(specs: &[SoundSpec<'_>]) -> (usize, u64) {
917 let mut hash = 0xcbf2_9ce4_8422_2325u64;
918 for spec in specs {
919 for byte in spec.name.as_bytes() {
920 hash ^= u64::from(*byte);
921 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
922 }
923 hash ^= spec.bytes.len() as u64;
924 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
925 }
926 (specs.len(), hash)
927}
928
929#[cfg(test)]
930mod tests {
931 use std::cell::Cell;
932
933 use parking_lot::Mutex;
934
935 use super::*;
936 use crate::run_test_composition;
937
938 fn tiny_wav() -> Vec<u8> {
939 let data = 0i16.to_le_bytes();
940 let mut out = Vec::new();
941 out.extend_from_slice(b"RIFF");
942 out.extend_from_slice(&(36u32 + data.len() as u32).to_le_bytes());
943 out.extend_from_slice(b"WAVE");
944 out.extend_from_slice(b"fmt ");
945 out.extend_from_slice(&16u32.to_le_bytes());
946 out.extend_from_slice(&1u16.to_le_bytes());
947 out.extend_from_slice(&1u16.to_le_bytes());
948 out.extend_from_slice(&8000u32.to_le_bytes());
949 out.extend_from_slice(&16000u32.to_le_bytes());
950 out.extend_from_slice(&2u16.to_le_bytes());
951 out.extend_from_slice(&16u16.to_le_bytes());
952 out.extend_from_slice(b"data");
953 out.extend_from_slice(&(data.len() as u32).to_le_bytes());
954 out.extend_from_slice(&data);
955 out
956 }
957
958 #[derive(Default)]
959 struct RecordingPlayer {
960 played: Mutex<Vec<(SoundId, PlaybackParams)>>,
961 unloaded: Mutex<Vec<SoundId>>,
962 next: Mutex<u32>,
963 }
964
965 impl AudioPlayer for RecordingPlayer {
966 fn load_clip(&self, _clip: AudioClip) -> Result<SoundId, AudioError> {
967 let mut next = self.next.lock();
968 *next += 1;
969 Ok(SoundId::from_raw(*next))
970 }
971 fn play(&self, id: SoundId, params: PlaybackParams) {
972 self.played.lock().push((id, params));
973 }
974 fn play_loop(&self, _id: SoundId, _params: PlaybackParams) -> VoiceId {
975 VoiceId::from_raw(7)
976 }
977 fn stop(&self, _id: SoundId) {}
978 fn stop_voice(&self, _voice: VoiceId) {}
979 fn set_master_volume(&self, _volume: f32) {}
980 fn unload(&self, id: SoundId) {
981 self.unloaded.lock().push(id);
982 }
983 fn is_available(&self) -> bool {
984 true
985 }
986 }
987
988 #[test]
989 fn playback_params_default_is_neutral() {
990 let params = PlaybackParams::default();
991 assert_eq!(params.volume, 1.0);
992 assert_eq!(params.rate, 1.0);
993 assert_eq!(params.pan, 0.0);
994 assert_eq!(params.bus, AudioBus::Effects);
995 assert_eq!(params, PlaybackParams::new());
996 assert_eq!(params, PlaybackParams::DEFAULT);
997 }
998
999 #[test]
1000 fn playback_params_sanitizes_out_of_range_and_nan() {
1001 let wild = PlaybackParams {
1002 volume: f32::NAN,
1003 rate: 1_000.0,
1004 pan: -9.0,
1005 bus: AudioBus::Music,
1006 }
1007 .sanitized();
1008 assert_eq!(wild.volume, 1.0);
1009 assert_eq!(wild.rate, PlaybackParams::MAX_RATE);
1010 assert_eq!(wild.pan, -1.0);
1011 assert_eq!(wild.bus, AudioBus::Music);
1012
1013 let slow = PlaybackParams::new().rate(0.0).sanitized();
1014 assert_eq!(slow.rate, PlaybackParams::MIN_RATE);
1015 }
1016
1017 #[test]
1018 fn pitch_semitones_maps_octaves_to_rate() {
1019 let up = PlaybackParams::new().pitch_semitones(12.0);
1020 assert!((up.rate - 2.0).abs() < 1e-5);
1021 let down = PlaybackParams::new().pitch_semitones(-12.0);
1022 assert!((down.rate - 0.5).abs() < 1e-5);
1023 let broken = PlaybackParams::new().pitch_semitones(f32::NAN);
1024 assert_eq!(broken.rate, 1.0);
1025 }
1026
1027 #[test]
1028 fn pan_gains_are_constant_power() {
1029 let (left, right) = PlaybackParams::new().gains();
1030 assert!((left - right).abs() < 1e-6);
1031 assert!((left * left + right * right - 1.0).abs() < 1e-5);
1032
1033 let (left, right) = PlaybackParams::new().pan(-1.0).gains();
1034 assert!((left - 1.0).abs() < 1e-5);
1035 assert!(right.abs() < 1e-5);
1036
1037 let (left, right) = PlaybackParams::new().pan(1.0).gains();
1038 assert!(left.abs() < 1e-5);
1039 assert!((right - 1.0).abs() < 1e-5);
1040 }
1041
1042 #[test]
1043 fn audio_bus_indices_round_trip() {
1044 for bus in AudioBus::ALL {
1045 assert_eq!(AudioBus::from_index(bus.index()), Some(bus));
1046 }
1047 assert_eq!(AudioBus::from_index(2), None);
1048 assert_eq!(AudioBus::default(), AudioBus::Effects);
1049 }
1050
1051 #[test]
1052 fn noop_player_hands_out_handles_and_keeps_settings() {
1053 let _guard = crate::registry::test_service_guard();
1054 clear_platform_audio();
1055 let player = default_audio();
1056 assert!(!player.is_available());
1057
1058 let id = player.load(&tiny_wav()).expect("no-op load succeeds");
1059 assert!(id.is_valid());
1060 let second = player.load(&tiny_wav()).expect("no-op load succeeds");
1061 assert_ne!(id, second);
1062
1063 player.play(id, PlaybackParams::new());
1064 let voice = player.play_loop(id, PlaybackParams::new());
1065 assert!(voice.is_valid());
1066 player.stop_voice(voice);
1067 player.stop(id);
1068 player.stop_all();
1069 player.set_voice_params(voice, PlaybackParams::new());
1070 player.unload(id);
1071 player.suspend();
1072 player.resume();
1073
1074 player.set_master_volume(0.25);
1075 assert_eq!(player.master_volume(), 0.25);
1076 player.set_master_volume(f32::NAN);
1077 assert_eq!(player.master_volume(), 1.0);
1078 player.set_bus_enabled(AudioBus::Music, false);
1079 assert!(!player.bus_enabled(AudioBus::Music));
1080 assert!(player.bus_enabled(AudioBus::Effects));
1081 player.set_bus_volume(AudioBus::Effects, 0.5);
1082 assert_eq!(player.bus_volume(AudioBus::Effects), 0.5);
1083 }
1084
1085 #[test]
1086 fn noop_player_rejects_invalid_loop_handle() {
1087 let _guard = crate::registry::test_service_guard();
1088 let player = NoopAudioPlayer::new();
1089 assert_eq!(
1090 player.play_loop(SoundId::NONE, PlaybackParams::new()),
1091 VoiceId::NONE
1092 );
1093 }
1094
1095 #[test]
1096 fn registered_player_replaces_the_default() {
1097 let _guard = crate::registry::test_service_guard();
1098 clear_platform_audio();
1099 assert!(!default_audio().is_available());
1100 let player: AudioPlayerRef = Arc::new(RecordingPlayer::default());
1101 set_platform_audio(player);
1102 assert!(default_audio().is_available());
1103 clear_platform_audio();
1104 assert!(!default_audio().is_available());
1105 }
1106
1107 #[test]
1108 fn audio_clip_validates_shape() {
1109 assert!(matches!(
1110 AudioClip::from_samples(vec![0.0], 0, 44_100),
1111 Err(AudioError::UnsupportedFormat(_))
1112 ));
1113 assert!(matches!(
1114 AudioClip::from_samples(vec![0.0], 3, 44_100),
1115 Err(AudioError::UnsupportedFormat(_))
1116 ));
1117 assert!(matches!(
1118 AudioClip::from_samples(vec![0.0], 1, 0),
1119 Err(AudioError::UnsupportedFormat(_))
1120 ));
1121 assert!(matches!(
1122 AudioClip::from_samples(Vec::new(), 1, 44_100),
1123 Err(AudioError::Decode(_))
1124 ));
1125 assert!(matches!(
1126 AudioClip::from_samples(vec![0.0, 0.0, 0.0], 2, 44_100),
1127 Err(AudioError::Decode(_))
1128 ));
1129
1130 let clip = AudioClip::from_samples(vec![0.0, 0.5], 2, 44_100).expect("valid clip");
1131 assert_eq!(clip.frames(), 1);
1132 assert_eq!(clip.channels(), 2);
1133 assert!(clip.duration_secs() > 0.0);
1134 assert_eq!(clip.shared_samples().len(), 2);
1135 assert!(format!("{clip:?}").contains("AudioClip"));
1136 }
1137
1138 #[test]
1139 fn audio_clip_decode_rejects_unknown_container() {
1140 assert!(matches!(
1141 AudioClip::decode(b"OggS not really"),
1142 Err(AudioError::UnsupportedFormat(_))
1143 ));
1144 }
1145
1146 #[test]
1147 fn sound_bank_loads_applies_base_volume_and_unloads_on_drop() {
1148 let player = Arc::new(RecordingPlayer::default());
1149 let wav = tiny_wav();
1150 let specs = [
1151 SoundSpec::new("hit", &wav).volume(0.5),
1152 SoundSpec::new("music", &wav).bus(AudioBus::Music),
1153 SoundSpec::new("broken", b"not audio"),
1154 ];
1155 let player_ref: AudioPlayerRef = player.clone();
1156 let bank = SoundBank::load(player_ref, &specs);
1157
1158 assert_eq!(bank.len(), 3);
1159 assert!(!bank.is_empty());
1160 assert_eq!(bank.failures().len(), 1);
1161 assert_eq!(bank.failures()[0].name, "broken");
1162 assert!(!bank.id(2).is_valid());
1163 assert_eq!(bank.find("music"), Some(bank.id(1)));
1164 assert_eq!(bank.find("absent"), None);
1165 assert_eq!(bank[0], bank.id(0));
1166 assert_eq!(bank[99], SoundId::NONE);
1167 assert!(format!("{bank:?}").contains("SoundBank"));
1168
1169 bank.play(0);
1170 bank.play_with(1, PlaybackParams::new().volume(0.5));
1171 bank.play_named("hit", PlaybackParams::new().pan(1.0));
1172 bank.play_with(2, PlaybackParams::new());
1173 bank.play_named("absent", PlaybackParams::new());
1174 assert_eq!(bank.play_loop(2, PlaybackParams::new()), VoiceId::NONE);
1175 assert!(bank.play_loop(0, PlaybackParams::new()).is_valid());
1176 assert_eq!(bank.play_loop(99, PlaybackParams::new()), VoiceId::NONE);
1177 bank.stop(0);
1178 bank.stop(2);
1179
1180 let played = player.played.lock().clone();
1181 assert_eq!(played.len(), 3);
1182 assert!((played[0].1.volume - 0.5).abs() < 1e-6);
1183 assert_eq!(played[0].1.bus, AudioBus::Effects);
1184 assert!((played[1].1.volume - 0.5).abs() < 1e-6);
1185 assert_eq!(played[1].1.bus, AudioBus::Music);
1186 assert!((played[2].1.pan - 1.0).abs() < 1e-6);
1187
1188 drop(bank);
1189 assert_eq!(player.unloaded.lock().len(), 3);
1190 }
1191
1192 #[test]
1193 fn sound_bank_key_tracks_names_and_lengths() {
1194 let a = [1u8, 2, 3];
1195 let b = [1u8, 2, 3, 4];
1196 assert_eq!(
1197 sound_bank_key(&[SoundSpec::new("x", &a)]),
1198 sound_bank_key(&[SoundSpec::new("x", &a)])
1199 );
1200 assert_ne!(
1201 sound_bank_key(&[SoundSpec::new("x", &a)]),
1202 sound_bank_key(&[SoundSpec::new("y", &a)])
1203 );
1204 assert_ne!(
1205 sound_bank_key(&[SoundSpec::new("x", &a)]),
1206 sound_bank_key(&[SoundSpec::new("x", &b)])
1207 );
1208 assert_ne!(
1209 sound_bank_key(&[SoundSpec::new("x", &a)]),
1210 sound_bank_key(&[SoundSpec::new("x", &a), SoundSpec::new("x", &a)])
1211 );
1212 }
1213
1214 #[test]
1215 fn provide_audio_publishes_the_platform_player() {
1216 let _guard = crate::registry::test_service_guard();
1217 clear_platform_audio();
1218 let player: AudioPlayerRef = Arc::new(RecordingPlayer::default());
1219 set_platform_audio(player);
1220
1221 let captured = Rc::new(RefCell::new(None));
1222 {
1223 let captured = Rc::clone(&captured);
1224 run_test_composition(move || {
1225 let captured = Rc::clone(&captured);
1226 ProvideAudio(move || {
1227 *captured.borrow_mut() = Some(local_audio().current().is_available());
1228 });
1229 });
1230 }
1231
1232 assert_eq!(*captured.borrow(), Some(true));
1233 clear_platform_audio();
1234 }
1235
1236 #[test]
1237 fn local_audio_defaults_to_the_noop_player() {
1238 let _guard = crate::registry::test_service_guard();
1239 clear_platform_audio();
1240 let captured = Rc::new(RefCell::new(None));
1241 {
1242 let captured = Rc::clone(&captured);
1243 run_test_composition(move || {
1244 let captured = Rc::clone(&captured);
1245 ProvideAudio(move || {
1246 *captured.borrow_mut() = Some(local_audio().current().is_available());
1247 });
1248 });
1249 }
1250 assert_eq!(*captured.borrow(), Some(false));
1251 }
1252
1253 #[test]
1254 fn remember_sound_bank_loads_once_across_recompositions() {
1255 let _guard = crate::registry::test_service_guard();
1256 clear_platform_audio();
1257 let player = Arc::new(RecordingPlayer::default());
1258 let player_ref: AudioPlayerRef = player.clone();
1259 set_platform_audio(player_ref);
1260
1261 let wav = tiny_wav();
1262 let bank_len = Rc::new(Cell::new(0usize));
1263 let bank_len_build = Rc::clone(&bank_len);
1264 let mut build = move || {
1265 let specs = [SoundSpec::new("a", &wav), SoundSpec::new("b", &wav)];
1266 let bank = rememberSoundBank(&specs);
1267 bank_len_build.set(bank.len());
1268 };
1269
1270 let key = cranpose_core::location_key(file!(), line!(), column!());
1271 let mut composition = cranpose_core::Composition::new(cranpose_core::MemoryApplier::new());
1272 composition.render(key, &mut build).expect("first render");
1273 composition.render(key, &mut build).expect("second render");
1274
1275 assert_eq!(bank_len.get(), 2);
1276 assert_eq!(
1277 *player.next.lock(),
1278 2,
1279 "the bank decodes once across renders"
1280 );
1281 clear_platform_audio();
1282 }
1283}