1mod wav;
26
27use std::{
28 fmt,
29 ops::Index,
30 rc::Rc,
31 sync::{Arc, OnceLock},
32};
33
34use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOfWithPolicy};
35use cranpose_macros::composable;
36use parking_lot::Mutex;
37
38use crate::registry::ServiceRegistry;
39
40#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
45pub struct SoundId(u32);
46
47impl SoundId {
48 pub const NONE: SoundId = SoundId(0);
50
51 pub fn from_raw(raw: u32) -> Self {
53 SoundId(raw)
54 }
55
56 pub fn raw(self) -> u32 {
58 self.0
59 }
60
61 pub fn is_valid(self) -> bool {
63 self.0 != 0
64 }
65}
66
67#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
70pub struct VoiceId(u64);
71
72impl VoiceId {
73 pub const NONE: VoiceId = VoiceId(0);
75
76 pub fn from_raw(raw: u64) -> Self {
78 VoiceId(raw)
79 }
80
81 pub fn raw(self) -> u64 {
83 self.0
84 }
85
86 pub fn is_valid(self) -> bool {
88 self.0 != 0
89 }
90}
91
92#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
95pub enum AudioBus {
96 #[default]
98 Effects,
99 Music,
101}
102
103impl AudioBus {
104 pub const ALL: [AudioBus; 2] = [AudioBus::Effects, AudioBus::Music];
106
107 pub fn index(self) -> usize {
109 match self {
110 AudioBus::Effects => 0,
111 AudioBus::Music => 1,
112 }
113 }
114
115 pub fn from_index(index: usize) -> Option<AudioBus> {
117 AudioBus::ALL.get(index).copied()
118 }
119}
120
121#[derive(Clone, Copy, PartialEq, Debug)]
126pub struct PlaybackParams {
127 pub volume: f32,
129 pub rate: f32,
132 pub pan: f32,
134 pub bus: AudioBus,
136}
137
138impl PlaybackParams {
139 pub const DEFAULT: PlaybackParams = PlaybackParams {
141 volume: 1.0,
142 rate: 1.0,
143 pan: 0.0,
144 bus: AudioBus::Effects,
145 };
146
147 pub const MIN_RATE: f32 = 0.05;
149 pub const MAX_RATE: f32 = 8.0;
151 pub const MAX_VOLUME: f32 = 4.0;
153
154 pub const fn new() -> PlaybackParams {
156 PlaybackParams::DEFAULT
157 }
158
159 pub fn volume(mut self, volume: f32) -> PlaybackParams {
161 self.volume = volume;
162 self
163 }
164
165 pub fn rate(mut self, rate: f32) -> PlaybackParams {
167 self.rate = rate;
168 self
169 }
170
171 pub fn pan(mut self, pan: f32) -> PlaybackParams {
173 self.pan = pan;
174 self
175 }
176
177 pub fn bus(mut self, bus: AudioBus) -> PlaybackParams {
179 self.bus = bus;
180 self
181 }
182
183 pub fn pitch_semitones(self, semitones: f32) -> PlaybackParams {
188 let semitones = if semitones.is_finite() {
189 semitones
190 } else {
191 0.0
192 };
193 self.rate(2.0f32.powf(semitones / 12.0))
194 }
195
196 pub fn sanitized(self) -> PlaybackParams {
201 fn finite(value: f32, fallback: f32) -> f32 {
202 if value.is_finite() { value } else { fallback }
203 }
204 PlaybackParams {
205 volume: finite(self.volume, 1.0).clamp(0.0, PlaybackParams::MAX_VOLUME),
206 rate: finite(self.rate, 1.0).clamp(PlaybackParams::MIN_RATE, PlaybackParams::MAX_RATE),
207 pan: finite(self.pan, 0.0).clamp(-1.0, 1.0),
208 bus: self.bus,
209 }
210 }
211
212 pub fn gains(self) -> (f32, f32) {
216 let params = self.sanitized();
217 let angle = (params.pan + 1.0) * std::f32::consts::FRAC_PI_4;
218 (params.volume * angle.cos(), params.volume * angle.sin())
219 }
220}
221
222impl Default for PlaybackParams {
223 fn default() -> PlaybackParams {
224 PlaybackParams::DEFAULT
225 }
226}
227
228#[derive(Clone)]
234pub struct AudioClip {
235 samples: Arc<[f32]>,
236 channels: u16,
237 sample_rate: u32,
238}
239
240impl AudioClip {
241 pub const MAX_FRAMES: usize = 1 << 26;
244
245 pub fn from_samples(
247 samples: Vec<f32>,
248 channels: u16,
249 sample_rate: u32,
250 ) -> Result<AudioClip, AudioError> {
251 if channels == 0 || channels > 2 {
252 return Err(AudioError::UnsupportedFormat(format!(
253 "clips must be mono or stereo, got {channels} channels"
254 )));
255 }
256 if sample_rate == 0 {
257 return Err(AudioError::UnsupportedFormat(
258 "clips must declare a non-zero sample rate".to_string(),
259 ));
260 }
261 if samples.is_empty() {
262 return Err(AudioError::Decode("clip holds no samples".to_string()));
263 }
264 if !samples.len().is_multiple_of(usize::from(channels)) {
265 return Err(AudioError::Decode(format!(
266 "clip holds {} samples, which is not a whole number of {channels}-channel frames",
267 samples.len()
268 )));
269 }
270 if samples.len() / usize::from(channels) > AudioClip::MAX_FRAMES {
271 return Err(AudioError::Decode(
272 "clip exceeds the maximum in-memory length".to_string(),
273 ));
274 }
275 Ok(AudioClip {
276 samples: samples.into(),
277 channels,
278 sample_rate,
279 })
280 }
281
282 pub fn decode(bytes: &[u8]) -> Result<AudioClip, AudioError> {
288 if wav::is_wav(bytes) {
289 wav::decode(bytes)
290 } else {
291 Err(AudioError::UnsupportedFormat(
292 "only RIFF/WAVE clips are decoded by the framework".to_string(),
293 ))
294 }
295 }
296
297 pub fn samples(&self) -> &[f32] {
299 &self.samples
300 }
301
302 pub fn shared_samples(&self) -> Arc<[f32]> {
304 Arc::clone(&self.samples)
305 }
306
307 pub fn channels(&self) -> u16 {
309 self.channels
310 }
311
312 pub fn sample_rate(&self) -> u32 {
314 self.sample_rate
315 }
316
317 pub fn frames(&self) -> usize {
319 self.samples.len() / usize::from(self.channels)
320 }
321
322 pub fn duration_secs(&self) -> f32 {
324 self.frames() as f32 / self.sample_rate as f32
325 }
326}
327
328impl fmt::Debug for AudioClip {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 f.debug_struct("AudioClip")
331 .field("frames", &self.frames())
332 .field("channels", &self.channels)
333 .field("sample_rate", &self.sample_rate)
334 .finish()
335 }
336}
337
338#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
340pub enum AudioError {
341 #[error("no audio backend is installed")]
343 Unsupported,
344 #[error("unsupported audio format: {0}")]
346 UnsupportedFormat(String),
347 #[error("failed to decode audio: {0}")]
349 Decode(String),
350 #[error("the audio engine already holds its maximum of {capacity} clips")]
352 ClipTableFull {
353 capacity: usize,
355 },
356 #[error("audio backend failure: {0}")]
358 Backend(String),
359}
360
361pub trait AudioPlayer: Send + Sync {
369 fn load_clip(&self, clip: AudioClip) -> Result<SoundId, AudioError>;
371
372 fn load(&self, bytes: &[u8]) -> Result<SoundId, AudioError> {
379 self.load_clip(AudioClip::decode(bytes)?)
380 }
381
382 fn unload(&self, _id: SoundId) {}
384
385 fn play(&self, id: SoundId, params: PlaybackParams);
388
389 fn play_loop(&self, id: SoundId, params: PlaybackParams) -> VoiceId;
391
392 fn stop(&self, id: SoundId);
394
395 fn stop_voice(&self, voice: VoiceId);
397
398 fn stop_all(&self) {}
400
401 fn set_voice_params(&self, _voice: VoiceId, _params: PlaybackParams) {}
404
405 fn set_master_volume(&self, volume: f32);
407
408 fn master_volume(&self) -> f32 {
410 1.0
411 }
412
413 fn set_bus_volume(&self, _bus: AudioBus, _volume: f32) {}
415
416 fn bus_volume(&self, _bus: AudioBus) -> f32 {
418 1.0
419 }
420
421 fn set_bus_enabled(&self, _bus: AudioBus, _enabled: bool) {}
424
425 fn bus_enabled(&self, _bus: AudioBus) -> bool {
427 true
428 }
429
430 fn suspend(&self) {}
433
434 fn resume(&self) {}
436
437 fn is_available(&self) -> bool {
440 false
441 }
442}
443
444pub type AudioPlayerRef = Arc<dyn AudioPlayer>;
446
447#[derive(Default)]
453pub struct NoopAudioPlayer {
454 next_sound: Mutex<u32>,
455 next_voice: Mutex<u64>,
456 master: Mutex<f32>,
457 bus_volumes: Mutex<[f32; 2]>,
458 bus_enabled: Mutex<[bool; 2]>,
459}
460
461impl NoopAudioPlayer {
462 pub fn new() -> NoopAudioPlayer {
464 NoopAudioPlayer {
465 next_sound: Mutex::new(0),
466 next_voice: Mutex::new(0),
467 master: Mutex::new(1.0),
468 bus_volumes: Mutex::new([1.0, 1.0]),
469 bus_enabled: Mutex::new([true, true]),
470 }
471 }
472}
473
474impl AudioPlayer for NoopAudioPlayer {
475 fn load_clip(&self, _clip: AudioClip) -> Result<SoundId, AudioError> {
476 let mut next = self.next_sound.lock();
477 *next = next.saturating_add(1);
478 Ok(SoundId::from_raw(*next))
479 }
480
481 fn play(&self, _id: SoundId, _params: PlaybackParams) {}
482
483 fn play_loop(&self, id: SoundId, _params: PlaybackParams) -> VoiceId {
484 if !id.is_valid() {
485 return VoiceId::NONE;
486 }
487 let mut next = self.next_voice.lock();
488 *next = next.saturating_add(1);
489 VoiceId::from_raw(*next)
490 }
491
492 fn stop(&self, _id: SoundId) {}
493
494 fn stop_voice(&self, _voice: VoiceId) {}
495
496 fn set_master_volume(&self, volume: f32) {
497 *self.master.lock() = if volume.is_finite() {
498 volume.clamp(0.0, 1.0)
499 } else {
500 1.0
501 };
502 }
503
504 fn master_volume(&self) -> f32 {
505 *self.master.lock()
506 }
507
508 fn set_bus_volume(&self, bus: AudioBus, volume: f32) {
509 let mut volumes = self.bus_volumes.lock();
510 volumes[bus.index()] = if volume.is_finite() {
511 volume.clamp(0.0, 1.0)
512 } else {
513 1.0
514 };
515 }
516
517 fn bus_volume(&self, bus: AudioBus) -> f32 {
518 self.bus_volumes.lock()[bus.index()]
519 }
520
521 fn set_bus_enabled(&self, bus: AudioBus, enabled: bool) {
522 self.bus_enabled.lock()[bus.index()] = enabled;
523 }
524
525 fn bus_enabled(&self, bus: AudioBus) -> bool {
526 self.bus_enabled.lock()[bus.index()]
527 }
528}
529
530static PLATFORM_AUDIO: ServiceRegistry<dyn AudioPlayer> = ServiceRegistry::new();
531static NOOP_AUDIO: OnceLock<AudioPlayerRef> = OnceLock::new();
532static DEFAULT_AUDIO: OnceLock<AudioPlayerRef> = OnceLock::new();
533
534struct PlatformAudioPlayer;
535
536fn registered_audio() -> AudioPlayerRef {
537 PLATFORM_AUDIO.get_or_warn("audio").unwrap_or_else(|| {
538 NOOP_AUDIO
539 .get_or_init(|| Arc::new(NoopAudioPlayer::new()))
540 .clone()
541 })
542}
543
544impl AudioPlayer for PlatformAudioPlayer {
545 fn load_clip(&self, clip: AudioClip) -> Result<SoundId, AudioError> {
546 registered_audio().load_clip(clip)
547 }
548
549 fn unload(&self, id: SoundId) {
550 registered_audio().unload(id);
551 }
552
553 fn play(&self, id: SoundId, params: PlaybackParams) {
554 registered_audio().play(id, params);
555 }
556
557 fn play_loop(&self, id: SoundId, params: PlaybackParams) -> VoiceId {
558 registered_audio().play_loop(id, params)
559 }
560
561 fn stop(&self, id: SoundId) {
562 registered_audio().stop(id);
563 }
564
565 fn stop_voice(&self, voice: VoiceId) {
566 registered_audio().stop_voice(voice);
567 }
568
569 fn stop_all(&self) {
570 registered_audio().stop_all();
571 }
572
573 fn set_voice_params(&self, voice: VoiceId, params: PlaybackParams) {
574 registered_audio().set_voice_params(voice, params);
575 }
576
577 fn set_master_volume(&self, volume: f32) {
578 registered_audio().set_master_volume(volume);
579 }
580
581 fn master_volume(&self) -> f32 {
582 registered_audio().master_volume()
583 }
584
585 fn set_bus_volume(&self, bus: AudioBus, volume: f32) {
586 registered_audio().set_bus_volume(bus, volume);
587 }
588
589 fn bus_volume(&self, bus: AudioBus) -> f32 {
590 registered_audio().bus_volume(bus)
591 }
592
593 fn set_bus_enabled(&self, bus: AudioBus, enabled: bool) {
594 registered_audio().set_bus_enabled(bus, enabled);
595 }
596
597 fn bus_enabled(&self, bus: AudioBus) -> bool {
598 registered_audio().bus_enabled(bus)
599 }
600
601 fn suspend(&self) {
602 registered_audio().suspend();
603 }
604
605 fn resume(&self) {
606 registered_audio().resume();
607 }
608
609 fn is_available(&self) -> bool {
610 registered_audio().is_available()
611 }
612}
613
614pub fn set_platform_audio(player: AudioPlayerRef) {
616 PLATFORM_AUDIO.set(player);
617}
618
619pub fn clear_platform_audio() {
621 PLATFORM_AUDIO.clear();
622}
623
624pub fn default_audio() -> AudioPlayerRef {
626 DEFAULT_AUDIO
627 .get_or_init(|| Arc::new(PlatformAudioPlayer))
628 .clone()
629}
630
631pub fn local_audio() -> CompositionLocal<AudioPlayerRef> {
633 crate::composition_locals::cached_local(
634 |locals| &locals.audio,
635 || compositionLocalOfWithPolicy(default_audio, Arc::ptr_eq),
636 )
637}
638
639#[composable]
641pub fn ProvideAudio(content: impl FnOnce()) {
642 let player = cranpose_core::remember(default_audio).with(|state| state.clone());
643 let local = local_audio();
644 CompositionLocalProvider([local.provides(player)], move || {
645 content();
646 });
647}
648
649#[derive(Clone, Copy, Debug)]
652pub struct SoundSpec<'a> {
653 pub name: &'static str,
655 pub bytes: &'a [u8],
657 pub base_volume: f32,
660 pub bus: AudioBus,
662}
663
664impl<'a> SoundSpec<'a> {
665 pub fn new(name: &'static str, bytes: &'a [u8]) -> SoundSpec<'a> {
667 SoundSpec {
668 name,
669 bytes,
670 base_volume: 1.0,
671 bus: AudioBus::Effects,
672 }
673 }
674
675 pub fn volume(mut self, base_volume: f32) -> SoundSpec<'a> {
677 self.base_volume = base_volume;
678 self
679 }
680
681 pub fn bus(mut self, bus: AudioBus) -> SoundSpec<'a> {
683 self.bus = bus;
684 self
685 }
686}
687
688#[derive(Clone, Copy, Debug)]
690pub struct SoundBankEntry {
691 pub name: &'static str,
693 pub id: SoundId,
695 pub base_volume: f32,
697 pub bus: AudioBus,
699}
700
701#[derive(Clone, Debug)]
703pub struct SoundBankFailure {
704 pub name: &'static str,
706 pub error: AudioError,
708}
709
710struct SoundBankInner {
711 player: AudioPlayerRef,
712 entries: Vec<SoundBankEntry>,
713 failures: Vec<SoundBankFailure>,
714}
715
716impl Drop for SoundBankInner {
717 fn drop(&mut self) {
718 for entry in &self.entries {
719 self.player.unload(entry.id);
720 }
721 }
722}
723
724#[derive(Clone)]
731pub struct SoundBank {
732 inner: Rc<SoundBankInner>,
733}
734
735impl SoundBank {
736 pub fn load(player: AudioPlayerRef, specs: &[SoundSpec<'_>]) -> SoundBank {
739 let mut entries = Vec::with_capacity(specs.len());
740 let mut failures = Vec::new();
741 for spec in specs {
742 match player.load(spec.bytes) {
743 Ok(id) => entries.push(SoundBankEntry {
744 name: spec.name,
745 id,
746 base_volume: spec.base_volume,
747 bus: spec.bus,
748 }),
749 Err(error) => {
750 entries.push(SoundBankEntry {
751 name: spec.name,
752 id: SoundId::NONE,
753 base_volume: spec.base_volume,
754 bus: spec.bus,
755 });
756 failures.push(SoundBankFailure {
757 name: spec.name,
758 error,
759 });
760 }
761 }
762 }
763 SoundBank {
764 inner: Rc::new(SoundBankInner {
765 player,
766 entries,
767 failures,
768 }),
769 }
770 }
771
772 pub fn len(&self) -> usize {
774 self.inner.entries.len()
775 }
776
777 pub fn is_empty(&self) -> bool {
779 self.inner.entries.is_empty()
780 }
781
782 pub fn entries(&self) -> &[SoundBankEntry] {
784 &self.inner.entries
785 }
786
787 pub fn failures(&self) -> &[SoundBankFailure] {
789 &self.inner.failures
790 }
791
792 pub fn player(&self) -> AudioPlayerRef {
794 Arc::clone(&self.inner.player)
795 }
796
797 pub fn id(&self, index: usize) -> SoundId {
799 self.inner
800 .entries
801 .get(index)
802 .map_or(SoundId::NONE, |entry| entry.id)
803 }
804
805 pub fn find(&self, name: &str) -> Option<SoundId> {
807 self.inner
808 .entries
809 .iter()
810 .find(|entry| entry.name == name)
811 .map(|entry| entry.id)
812 }
813
814 pub fn play(&self, index: usize) {
816 self.play_with(index, PlaybackParams::DEFAULT);
817 }
818
819 pub fn play_with(&self, index: usize, params: PlaybackParams) {
822 let Some(entry) = self.inner.entries.get(index) else {
823 return;
824 };
825 if !entry.id.is_valid() {
826 return;
827 }
828 self.inner.player.play(entry.id, entry.apply(params));
829 }
830
831 pub fn play_named(&self, name: &str, params: PlaybackParams) {
833 if let Some(index) = self.inner.entries.iter().position(|e| e.name == name) {
834 self.play_with(index, params);
835 }
836 }
837
838 pub fn play_loop(&self, index: usize, params: PlaybackParams) -> VoiceId {
840 let Some(entry) = self.inner.entries.get(index) else {
841 return VoiceId::NONE;
842 };
843 if !entry.id.is_valid() {
844 return VoiceId::NONE;
845 }
846 self.inner.player.play_loop(entry.id, entry.apply(params))
847 }
848
849 pub fn stop(&self, index: usize) {
851 let id = self.id(index);
852 if id.is_valid() {
853 self.inner.player.stop(id);
854 }
855 }
856}
857
858impl SoundBankEntry {
859 fn apply(&self, params: PlaybackParams) -> PlaybackParams {
860 PlaybackParams {
861 volume: params.volume * self.base_volume,
862 rate: params.rate,
863 pan: params.pan,
864 bus: self.bus,
865 }
866 }
867}
868
869impl fmt::Debug for SoundBank {
870 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
871 f.debug_struct("SoundBank")
872 .field("entries", &self.inner.entries.len())
873 .field("failures", &self.inner.failures.len())
874 .finish()
875 }
876}
877
878impl Index<usize> for SoundBank {
879 type Output = SoundId;
880
881 fn index(&self, index: usize) -> &SoundId {
882 static NONE: SoundId = SoundId::NONE;
883 self.inner
884 .entries
885 .get(index)
886 .map_or(&NONE, |entry| &entry.id)
887 }
888}
889
890#[composable(no_skip)]
896#[track_caller]
897pub fn rememberSoundBank(specs: &[SoundSpec<'_>]) -> SoundBank {
898 let key = sound_bank_key(specs);
899 let player = local_audio().current();
900 cranpose_core::rememberKeyed((key, Arc::as_ptr(&player) as *const () as usize), |_| {
901 SoundBank::load(Arc::clone(&player), specs)
902 })
903}
904
905fn sound_bank_key(specs: &[SoundSpec<'_>]) -> (usize, u64) {
906 let mut hash = 0xcbf2_9ce4_8422_2325u64;
907 for spec in specs {
908 for byte in spec.name.as_bytes() {
909 hash ^= u64::from(*byte);
910 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
911 }
912 hash ^= spec.bytes.len() as u64;
913 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
914 }
915 (specs.len(), hash)
916}
917
918#[cfg(test)]
919#[path = "tests/audio_tests.rs"]
920mod tests;