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: Self = Self(0);
50
51 pub fn from_raw(raw: u32) -> Self {
53 Self(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: Self = Self(0);
75
76 pub fn from_raw(raw: u64) -> Self {
78 Self(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: [Self; 2] = [Self::Effects, Self::Music];
106
107 pub fn index(self) -> usize {
109 match self {
110 Self::Effects => 0,
111 Self::Music => 1,
112 }
113 }
114
115 pub fn from_index(index: usize) -> Option<Self> {
117 Self::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: Self = Self {
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() -> Self {
156 Self::DEFAULT
157 }
158
159 pub fn volume(mut self, volume: f32) -> Self {
161 self.volume = volume;
162 self
163 }
164
165 pub fn rate(mut self, rate: f32) -> Self {
167 self.rate = rate;
168 self
169 }
170
171 pub fn pan(mut self, pan: f32) -> Self {
173 self.pan = pan;
174 self
175 }
176
177 pub fn bus(mut self, bus: AudioBus) -> Self {
179 self.bus = bus;
180 self
181 }
182
183 pub fn pitch_semitones(self, semitones: f32) -> Self {
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) -> Self {
201 fn finite(value: f32, fallback: f32) -> f32 {
202 if value.is_finite() { value } else { fallback }
203 }
204 Self {
205 volume: finite(self.volume, 1.0).clamp(0.0, Self::MAX_VOLUME),
206 rate: finite(self.rate, 1.0).clamp(Self::MIN_RATE, Self::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() -> Self {
224 Self::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<Self, 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) > Self::MAX_FRAMES {
271 return Err(AudioError::Decode(
272 "clip exceeds the maximum in-memory length".to_string(),
273 ));
274 }
275 Ok(Self {
276 samples: samples.into(),
277 channels,
278 sample_rate,
279 })
280 }
281
282 pub fn decode(bytes: &[u8]) -> Result<Self, 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() -> Self {
464 Self {
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
538 .get_or_warn("audio")
539 .unwrap_or_else(|| Arc::clone(NOOP_AUDIO.get_or_init(|| Arc::new(NoopAudioPlayer::new()))))
540}
541
542impl AudioPlayer for PlatformAudioPlayer {
543 fn load_clip(&self, clip: AudioClip) -> Result<SoundId, AudioError> {
544 registered_audio().load_clip(clip)
545 }
546
547 fn unload(&self, id: SoundId) {
548 registered_audio().unload(id);
549 }
550
551 fn play(&self, id: SoundId, params: PlaybackParams) {
552 registered_audio().play(id, params);
553 }
554
555 fn play_loop(&self, id: SoundId, params: PlaybackParams) -> VoiceId {
556 registered_audio().play_loop(id, params)
557 }
558
559 fn stop(&self, id: SoundId) {
560 registered_audio().stop(id);
561 }
562
563 fn stop_voice(&self, voice: VoiceId) {
564 registered_audio().stop_voice(voice);
565 }
566
567 fn stop_all(&self) {
568 registered_audio().stop_all();
569 }
570
571 fn set_voice_params(&self, voice: VoiceId, params: PlaybackParams) {
572 registered_audio().set_voice_params(voice, params);
573 }
574
575 fn set_master_volume(&self, volume: f32) {
576 registered_audio().set_master_volume(volume);
577 }
578
579 fn master_volume(&self) -> f32 {
580 registered_audio().master_volume()
581 }
582
583 fn set_bus_volume(&self, bus: AudioBus, volume: f32) {
584 registered_audio().set_bus_volume(bus, volume);
585 }
586
587 fn bus_volume(&self, bus: AudioBus) -> f32 {
588 registered_audio().bus_volume(bus)
589 }
590
591 fn set_bus_enabled(&self, bus: AudioBus, enabled: bool) {
592 registered_audio().set_bus_enabled(bus, enabled);
593 }
594
595 fn bus_enabled(&self, bus: AudioBus) -> bool {
596 registered_audio().bus_enabled(bus)
597 }
598
599 fn suspend(&self) {
600 registered_audio().suspend();
601 }
602
603 fn resume(&self) {
604 registered_audio().resume();
605 }
606
607 fn is_available(&self) -> bool {
608 registered_audio().is_available()
609 }
610}
611
612pub fn set_platform_audio(player: AudioPlayerRef) {
614 PLATFORM_AUDIO.set(player);
615}
616
617pub fn clear_platform_audio() {
619 PLATFORM_AUDIO.clear();
620}
621
622pub fn default_audio() -> AudioPlayerRef {
624 Arc::clone(DEFAULT_AUDIO.get_or_init(|| Arc::new(PlatformAudioPlayer)))
625}
626
627pub fn local_audio() -> CompositionLocal<AudioPlayerRef> {
629 crate::composition_locals::cached_local(
630 |locals| &locals.audio,
631 || compositionLocalOfWithPolicy(default_audio, Arc::ptr_eq),
632 )
633}
634
635#[composable]
637pub fn ProvideAudio(content: impl FnOnce()) {
638 let player = cranpose_core::remember(default_audio).with(|state| Arc::clone(state));
639 let local = local_audio();
640 CompositionLocalProvider([local.provides(player)], move || {
641 content();
642 });
643}
644
645#[derive(Clone, Copy, Debug)]
648pub struct SoundSpec<'a> {
649 pub name: &'static str,
651 pub bytes: &'a [u8],
653 pub base_volume: f32,
656 pub bus: AudioBus,
658}
659
660impl<'a> SoundSpec<'a> {
661 pub fn new(name: &'static str, bytes: &'a [u8]) -> Self {
663 SoundSpec {
664 name,
665 bytes,
666 base_volume: 1.0,
667 bus: AudioBus::Effects,
668 }
669 }
670
671 pub fn volume(mut self, base_volume: f32) -> Self {
673 self.base_volume = base_volume;
674 self
675 }
676
677 pub fn bus(mut self, bus: AudioBus) -> Self {
679 self.bus = bus;
680 self
681 }
682}
683
684#[derive(Clone, Copy, Debug)]
686pub struct SoundBankEntry {
687 pub name: &'static str,
689 pub id: SoundId,
691 pub base_volume: f32,
693 pub bus: AudioBus,
695}
696
697#[derive(Clone, Debug)]
699pub struct SoundBankFailure {
700 pub name: &'static str,
702 pub error: AudioError,
704}
705
706struct SoundBankInner {
707 player: AudioPlayerRef,
708 entries: Vec<SoundBankEntry>,
709 failures: Vec<SoundBankFailure>,
710}
711
712impl Drop for SoundBankInner {
713 fn drop(&mut self) {
714 for entry in &self.entries {
715 self.player.unload(entry.id);
716 }
717 }
718}
719
720#[derive(Clone)]
727pub struct SoundBank {
728 inner: Rc<SoundBankInner>,
729}
730
731impl SoundBank {
732 pub fn load(player: AudioPlayerRef, specs: &[SoundSpec<'_>]) -> Self {
735 let mut entries = Vec::with_capacity(specs.len());
736 let mut failures = Vec::new();
737 for spec in specs {
738 match player.load(spec.bytes) {
739 Ok(id) => entries.push(SoundBankEntry {
740 name: spec.name,
741 id,
742 base_volume: spec.base_volume,
743 bus: spec.bus,
744 }),
745 Err(error) => {
746 entries.push(SoundBankEntry {
747 name: spec.name,
748 id: SoundId::NONE,
749 base_volume: spec.base_volume,
750 bus: spec.bus,
751 });
752 failures.push(SoundBankFailure {
753 name: spec.name,
754 error,
755 });
756 }
757 }
758 }
759 Self {
760 inner: Rc::new(SoundBankInner {
761 player,
762 entries,
763 failures,
764 }),
765 }
766 }
767
768 pub fn len(&self) -> usize {
770 self.inner.entries.len()
771 }
772
773 pub fn is_empty(&self) -> bool {
775 self.inner.entries.is_empty()
776 }
777
778 pub fn entries(&self) -> &[SoundBankEntry] {
780 &self.inner.entries
781 }
782
783 pub fn failures(&self) -> &[SoundBankFailure] {
785 &self.inner.failures
786 }
787
788 pub fn player(&self) -> AudioPlayerRef {
790 Arc::clone(&self.inner.player)
791 }
792
793 pub fn id(&self, index: usize) -> SoundId {
795 self.inner
796 .entries
797 .get(index)
798 .map_or(SoundId::NONE, |entry| entry.id)
799 }
800
801 pub fn find(&self, name: &str) -> Option<SoundId> {
803 self.inner
804 .entries
805 .iter()
806 .find(|entry| entry.name == name)
807 .map(|entry| entry.id)
808 }
809
810 pub fn play(&self, index: usize) {
812 self.play_with(index, PlaybackParams::DEFAULT);
813 }
814
815 pub fn play_with(&self, index: usize, params: PlaybackParams) {
818 let Some(entry) = self.inner.entries.get(index) else {
819 return;
820 };
821 if !entry.id.is_valid() {
822 return;
823 }
824 self.inner.player.play(entry.id, entry.apply(params));
825 }
826
827 pub fn play_named(&self, name: &str, params: PlaybackParams) {
829 if let Some(index) = self.inner.entries.iter().position(|e| e.name == name) {
830 self.play_with(index, params);
831 }
832 }
833
834 pub fn play_loop(&self, index: usize, params: PlaybackParams) -> VoiceId {
836 let Some(entry) = self.inner.entries.get(index) else {
837 return VoiceId::NONE;
838 };
839 if !entry.id.is_valid() {
840 return VoiceId::NONE;
841 }
842 self.inner.player.play_loop(entry.id, entry.apply(params))
843 }
844
845 pub fn stop(&self, index: usize) {
847 let id = self.id(index);
848 if id.is_valid() {
849 self.inner.player.stop(id);
850 }
851 }
852}
853
854impl SoundBankEntry {
855 fn apply(&self, params: PlaybackParams) -> PlaybackParams {
856 PlaybackParams {
857 volume: params.volume * self.base_volume,
858 rate: params.rate,
859 pan: params.pan,
860 bus: self.bus,
861 }
862 }
863}
864
865impl fmt::Debug for SoundBank {
866 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
867 f.debug_struct("SoundBank")
868 .field("entries", &self.inner.entries.len())
869 .field("failures", &self.inner.failures.len())
870 .finish()
871 }
872}
873
874impl Index<usize> for SoundBank {
875 type Output = SoundId;
876
877 fn index(&self, index: usize) -> &SoundId {
878 static NONE: SoundId = SoundId::NONE;
879 self.inner
880 .entries
881 .get(index)
882 .map_or(&NONE, |entry| &entry.id)
883 }
884}
885
886#[composable(no_skip)]
892#[track_caller]
893pub fn rememberSoundBank(specs: &[SoundSpec<'_>]) -> SoundBank {
894 let key = sound_bank_key(specs);
895 let player = local_audio().current();
896 cranpose_core::rememberKeyed((key, Arc::as_ptr(&player) as *const () as usize), |_| {
897 SoundBank::load(Arc::clone(&player), specs)
898 })
899}
900
901fn sound_bank_key(specs: &[SoundSpec<'_>]) -> (usize, u64) {
902 let mut hash = 0xcbf2_9ce4_8422_2325u64;
903 for spec in specs {
904 for byte in spec.name.as_bytes() {
905 hash ^= u64::from(*byte);
906 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
907 }
908 hash ^= spec.bytes.len() as u64;
909 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
910 }
911 (specs.len(), hash)
912}
913
914#[cfg(test)]
915#[path = "tests/audio_tests.rs"]
916mod tests;