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