1use rustc_hash::FxHashMap;
30
31use crate::input::UiEvent;
32use crate::key::Key;
33use crate::resources::SoundId;
34use crate::tree::OriginId;
35use crate::value::Value;
36use crate::window::WindowId;
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
42pub struct PlaybackId(pub u64);
43
44#[derive(Clone, Debug, PartialEq)]
46pub struct PlayOptions {
47 pub volume: f32,
49 pub looped: bool,
50 pub fade_in_ms: f32,
52 pub tag: Option<Value>,
54}
55
56impl Default for PlayOptions {
57 fn default() -> Self {
58 Self {
59 volume: 1.0,
60 looped: false,
61 fade_in_ms: 0.0,
62 tag: None,
63 }
64 }
65}
66
67impl PlayOptions {
68 pub fn volume(mut self, v: f32) -> Self {
69 self.volume = v;
70 self
71 }
72
73 pub fn looped(mut self) -> Self {
74 self.looped = true;
75 self
76 }
77
78 pub fn fade_in(mut self, ms: f32) -> Self {
79 self.fade_in_ms = ms;
80 self
81 }
82
83 pub fn tag(mut self, tag: impl Into<Value>) -> Self {
85 self.tag = Some(tag.into());
86 self
87 }
88}
89
90#[derive(Clone, Debug, PartialEq)]
92pub struct AudioSpec {
93 pub src: SoundId,
94 pub volume: f32,
96 pub looped: bool,
97 pub paused: bool,
99 pub finish: bool,
102 pub tag: Option<Value>,
104}
105
106impl AudioSpec {
107 pub fn new(src: SoundId) -> Self {
108 Self {
109 src,
110 volume: 1.0,
111 looped: false,
112 paused: false,
113 finish: false,
114 tag: None,
115 }
116 }
117
118 pub fn volume(mut self, v: f32) -> Self {
119 self.volume = v;
120 self
121 }
122
123 pub fn looped(mut self) -> Self {
124 self.looped = true;
125 self
126 }
127
128 pub fn paused(mut self, paused: bool) -> Self {
129 self.paused = paused;
130 self
131 }
132
133 pub fn finish(mut self) -> Self {
161 self.finish = true;
162 self
163 }
164
165 pub fn tag(mut self, tag: impl Into<Value>) -> Self {
166 self.tag = Some(tag.into());
167 self
168 }
169}
170
171#[derive(Clone, Debug, PartialEq)]
174pub enum AudioCommand {
175 Play {
176 playback: PlaybackId,
177 sound: SoundId,
178 volume: f32,
179 looped: bool,
180 fade_in_ms: f32,
181 },
182 Stop {
183 playback: PlaybackId,
184 fade_ms: f32,
185 },
186 SetVolume {
187 playback: PlaybackId,
188 volume: f32,
189 tween_ms: f32,
190 },
191 Pause {
192 playback: PlaybackId,
193 fade_ms: f32,
194 },
195 Resume {
196 playback: PlaybackId,
197 fade_ms: f32,
198 },
199 MasterVolume {
200 volume: f32,
201 tween_ms: f32,
202 },
203 Unload {
205 sound: SoundId,
206 },
207}
208
209impl AudioCommand {
210 pub fn kind_name(&self) -> &'static str {
213 match self {
214 AudioCommand::Play { .. } => "play",
215 AudioCommand::Stop { .. } => "stop",
216 AudioCommand::SetVolume { .. } => "setVolume",
217 AudioCommand::Pause { .. } => "pause",
218 AudioCommand::Resume { .. } => "resume",
219 AudioCommand::MasterVolume { .. } => "masterVolume",
220 AudioCommand::Unload { .. } => "unload",
221 }
222 }
223
224 pub fn to_value(&self, h: crate::value::Handles) -> Value {
229 let pb = |p: PlaybackId| Value::Int(p.0 as i64);
230 let mut out = vec![("kind".to_string(), Value::str(self.kind_name()))];
231 let mut push = |k: &str, v: Value| out.push((k.to_string(), v));
232 match *self {
233 AudioCommand::Play {
234 playback,
235 sound,
236 volume,
237 looped,
238 fade_in_ms,
239 } => {
240 push("playback", pb(playback));
241 push("sound", (h.id)(sound.to_ffi()));
242 push("volume", Value::float(volume));
243 push("loop", Value::Bool(looped));
244 push("fade_in", Value::float(fade_in_ms));
245 }
246 AudioCommand::Stop { playback, fade_ms }
247 | AudioCommand::Pause { playback, fade_ms }
248 | AudioCommand::Resume { playback, fade_ms } => {
249 push("playback", pb(playback));
250 push("fade", Value::float(fade_ms));
251 }
252 AudioCommand::SetVolume {
253 playback,
254 volume,
255 tween_ms,
256 } => {
257 push("playback", pb(playback));
258 push("volume", Value::float(volume));
259 push("tween", Value::float(tween_ms));
260 }
261 AudioCommand::MasterVolume { volume, tween_ms } => {
262 push("volume", Value::float(volume));
263 push("tween", Value::float(tween_ms));
264 }
265 AudioCommand::Unload { sound } => push("sound", (h.id)(sound.to_ffi())),
266 }
267 Value::Map(out)
268 }
269}
270
271struct Tagged {
273 origin: OriginId,
274 window: WindowId,
278 key: Key,
279 tag: Value,
280}
281
282struct Mounted {
284 playback: PlaybackId,
285 spec: AudioSpec,
286}
287
288type Mount = (WindowId, Key);
293
294#[derive(Clone, Copy, Debug, PartialEq, Eq)]
298pub enum Why {
299 Removed,
301 Restarted,
303}
304
305impl Why {
306 pub(crate) fn verb(self) -> &'static str {
308 match self {
309 Why::Removed => "removed",
310 Why::Restarted => "restarted",
311 }
312 }
313}
314
315const MAX_STOPPED: usize = 256;
320
321#[derive(Default)]
329pub struct AudioStore {
330 next: u64,
331 commands: Vec<AudioCommand>,
332 tagged: FxHashMap<PlaybackId, Tagged>,
333 mounted: FxHashMap<Mount, Mounted>,
334 declared: Vec<(Mount, OriginId, AudioSpec)>,
337 stopped: FxHashMap<PlaybackId, (Key, Why)>,
344}
345
346impl AudioStore {
347 fn alloc(&mut self) -> PlaybackId {
348 self.next += 1;
349 PlaybackId(self.next)
350 }
351
352 pub(crate) fn play(
355 &mut self,
356 origin: OriginId,
357 window: WindowId,
358 key: Key,
359 sound: SoundId,
360 opts: PlayOptions,
361 ) -> PlaybackId {
362 let playback = self.alloc();
363 self.commands.push(AudioCommand::Play {
364 playback,
365 sound,
366 volume: opts.volume,
367 looped: opts.looped,
368 fade_in_ms: opts.fade_in_ms,
369 });
370 if let Some(tag) = opts.tag {
371 self.tagged.insert(
372 playback,
373 Tagged {
374 origin,
375 window,
376 key,
377 tag,
378 },
379 );
380 }
381 playback
382 }
383
384 pub(crate) fn stop(&mut self, playback: PlaybackId, fade_ms: f32) {
386 self.tagged.remove(&playback);
387 self.commands.push(AudioCommand::Stop { playback, fade_ms });
388 }
389
390 fn record_stop(&mut self, playback: PlaybackId, key: Key, why: Why) {
394 if self.stopped.len() >= MAX_STOPPED
395 && let Some(oldest) = self.stopped.keys().min().copied()
396 {
397 self.stopped.remove(&oldest);
398 }
399 self.stopped.insert(playback, (key, why));
400 }
401
402 pub(crate) fn truncated(&mut self, playback: PlaybackId) -> Option<(Key, Why)> {
408 self.stopped.remove(&playback)
409 }
410
411 pub(crate) fn set_volume(&mut self, playback: PlaybackId, volume: f32, tween_ms: f32) {
412 self.commands.push(AudioCommand::SetVolume {
413 playback,
414 volume,
415 tween_ms,
416 });
417 }
418
419 pub(crate) fn pause(&mut self, playback: PlaybackId, fade_ms: f32) {
420 self.commands
421 .push(AudioCommand::Pause { playback, fade_ms });
422 }
423
424 pub(crate) fn resume(&mut self, playback: PlaybackId, fade_ms: f32) {
425 self.commands
426 .push(AudioCommand::Resume { playback, fade_ms });
427 }
428
429 pub(crate) fn master_volume(&mut self, volume: f32, tween_ms: f32) {
430 self.commands
431 .push(AudioCommand::MasterVolume { volume, tween_ms });
432 }
433
434 pub(crate) fn unload(&mut self, sound: SoundId) {
435 self.commands.push(AudioCommand::Unload { sound });
436 }
437
438 pub fn take_commands(&mut self) -> Vec<AudioCommand> {
441 std::mem::take(&mut self.commands)
442 }
443
444 pub fn pending(&self) -> &[AudioCommand] {
446 &self.commands
447 }
448
449 pub fn playback_of(&self, window: WindowId, key: Key) -> Option<PlaybackId> {
451 self.mounted.get(&(window, key)).map(|m| m.playback)
452 }
453
454 #[cfg(feature = "conformance")]
458 pub(crate) fn any_mounted(&self, window: WindowId) -> bool {
459 self.mounted.keys().any(|(w, _)| *w == window)
460 }
461
462 pub(crate) fn declare(
465 &mut self,
466 window: WindowId,
467 key: Key,
468 origin: OriginId,
469 spec: AudioSpec,
470 ) {
471 self.declared.push(((window, key), origin, spec));
472 }
473
474 pub(crate) fn ended(&mut self, playback: PlaybackId) -> Option<UiEvent> {
477 self.stopped.remove(&playback);
480 let t = self.tagged.remove(&playback)?;
481 Some(UiEvent {
482 origin: t.origin,
483 window: t.window,
484 key: t.key,
485 payload: Value::map([
486 ("kind", Value::str("sound")),
487 ("phase", Value::str("ended")),
488 ("playback", Value::Int(playback.0 as i64)),
489 ("tag", t.tag),
490 ]),
491 slot: None,
492 })
493 }
494
495 pub(crate) fn refused(
507 &mut self,
508 playback: PlaybackId,
509 ) -> (Option<UiEvent>, crate::diag::Warning) {
510 let warning = crate::diag::playback_refused(self.key_of(playback), playback);
511 let event = self.tagged.remove(&playback).map(|t| UiEvent {
512 origin: t.origin,
513 window: t.window,
514 key: t.key,
515 payload: Value::map([
516 ("kind", Value::str("sound")),
517 ("phase", Value::str("refused")),
518 ("playback", Value::Int(playback.0 as i64)),
519 ("tag", t.tag),
520 ]),
521 slot: None,
522 });
523 (event, warning)
524 }
525
526 fn key_of(&self, playback: PlaybackId) -> Key {
531 if let Some(t) = self.tagged.get(&playback) {
532 return t.key;
533 }
534 self.mounted
535 .iter()
536 .find(|(_, m)| m.playback == playback)
537 .map(|((_, k), _)| *k)
538 .unwrap_or(Key::ROOT)
539 }
540
541 pub(crate) fn reconcile(&mut self, window: WindowId) {
552 let (declared, others): (Vec<_>, Vec<_>) = std::mem::take(&mut self.declared)
556 .into_iter()
557 .partition(|(mount, _, _)| mount.0 == window);
558 self.declared = others;
559 let mut seen: Vec<Key> = Vec::with_capacity(declared.len());
560 for (mount, origin, spec) in declared {
561 let key = mount.1;
562 if seen.contains(&key) {
563 continue;
564 }
565 seen.push(key);
566 let restart = match self.mounted.get(&mount) {
567 None => true,
568 Some(m) => m.spec.src != spec.src || m.spec.looped != spec.looped,
569 };
570 if restart {
571 if let Some(old) = self.mounted.remove(&mount) {
572 self.stop(old.playback, 0.0);
573 if !old.spec.looped && !old.spec.finish {
577 self.record_stop(old.playback, key, Why::Restarted);
578 }
579 }
580 let opts = PlayOptions {
581 volume: spec.volume,
582 looped: spec.looped,
583 fade_in_ms: 0.0,
584 tag: spec.tag.clone(),
585 };
586 let playback = self.play(origin, window, key, spec.src, opts);
587 if spec.paused {
588 self.pause(playback, 0.0);
589 }
590 self.mounted.insert(mount, Mounted { playback, spec });
591 continue;
592 }
593 let m = self.mounted.get_mut(&mount).expect("mounted");
594 let playback = m.playback;
595 if m.spec.volume != spec.volume {
596 self.commands.push(AudioCommand::SetVolume {
597 playback,
598 volume: spec.volume,
599 tween_ms: 0.0,
600 });
601 }
602 if m.spec.paused != spec.paused {
603 self.commands.push(if spec.paused {
604 AudioCommand::Pause {
605 playback,
606 fade_ms: 0.0,
607 }
608 } else {
609 AudioCommand::Resume {
610 playback,
611 fade_ms: 0.0,
612 }
613 });
614 }
615 if m.spec.tag != spec.tag {
616 match (&spec.tag, self.tagged.get_mut(&playback)) {
617 (Some(tag), Some(t)) => t.tag = tag.clone(),
618 (Some(tag), None) => {
619 self.tagged.insert(
620 playback,
621 Tagged {
622 origin,
623 window,
624 key,
625 tag: tag.clone(),
626 },
627 );
628 }
629 (None, _) => {
630 self.tagged.remove(&playback);
631 }
632 }
633 }
634 m.spec = spec;
635 }
636 let gone: Vec<(Key, PlaybackId, bool, bool)> = self
641 .mounted
642 .iter()
643 .filter(|((w, k), _)| *w == window && !seen.contains(k))
644 .map(|((_, k), m)| (*k, m.playback, m.spec.finish, m.spec.looped))
645 .collect();
646 for (key, playback, finish, looped) in gone {
647 self.mounted.remove(&(window, key));
648 if finish && !looped {
649 continue;
650 }
651 self.stop(playback, 0.0);
652 if !looped && !finish {
656 self.record_stop(playback, key, Why::Removed);
657 }
658 }
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665 use crate::resources::{Resources, SessionId};
666
667 fn sound() -> SoundId {
668 Resources::new(SessionId::next()).add_sound(vec![0; 4])
669 }
670
671 #[test]
672 fn play_allocates_ids_and_only_tagged_playbacks_report_ended() {
673 let mut a = AudioStore::default();
674 let s = sound();
675 let quiet = a.play(
676 OriginId::HOST,
677 WindowId::MAIN,
678 Key::ROOT,
679 s,
680 PlayOptions::default(),
681 );
682 let loud = a.play(
683 OriginId::HOST,
684 WindowId::MAIN,
685 Key::ROOT,
686 s,
687 PlayOptions::default().tag(Value::str("t")),
688 );
689 assert_ne!(quiet, loud);
690 assert_eq!(a.take_commands().len(), 2);
691 assert!(a.ended(quiet).is_none());
692 let ev = a.ended(loud).expect("tagged playback reports ended");
693 assert_eq!(ev.kind(), Some("sound"));
694 assert_eq!(ev.payload.get_str("tag"), Some("t"));
695 assert_eq!(ev.payload.get_int("playback"), Some(loud.0 as i64));
696 assert!(a.ended(loud).is_none());
698 }
699
700 #[test]
701 fn stop_cancels_the_ended_event() {
702 let mut a = AudioStore::default();
703 let s = sound();
704 let p = a.play(
705 OriginId::HOST,
706 WindowId::MAIN,
707 Key::ROOT,
708 s,
709 PlayOptions::default().tag(Value::Null),
710 );
711 a.stop(p, 0.0);
712 assert!(a.ended(p).is_none());
713 }
714
715 #[test]
719 fn a_removed_node_that_asked_to_finish_is_not_stopped() {
720 let mut a = AudioStore::default();
721 let s = sound();
722 let k = Key::ROOT.str("chime");
723 a.declare(
724 WindowId::MAIN,
725 k,
726 OriginId::HOST,
727 AudioSpec::new(s).finish(),
728 );
729 a.reconcile(WindowId::MAIN);
730 assert!(matches!(
731 a.take_commands().as_slice(),
732 [AudioCommand::Play { .. }]
733 ));
734
735 a.reconcile(WindowId::MAIN);
738 assert_eq!(a.take_commands(), vec![]);
739 assert!(a.playback_of(WindowId::MAIN, k).is_none());
740 }
741
742 #[test]
745 fn a_removed_loop_stops_even_when_it_asked_to_finish() {
746 let mut a = AudioStore::default();
747 let s = sound();
748 let k = Key::ROOT.str("bed");
749 a.declare(
750 WindowId::MAIN,
751 k,
752 OriginId::HOST,
753 AudioSpec::new(s).looped().finish(),
754 );
755 a.reconcile(WindowId::MAIN);
756 let p = a.playback_of(WindowId::MAIN, k).unwrap();
757 a.take_commands();
758
759 a.reconcile(WindowId::MAIN);
760 assert_eq!(
761 a.take_commands(),
762 vec![AudioCommand::Stop {
763 playback: p,
764 fade_ms: 0.0
765 }]
766 );
767 }
768
769 #[test]
773 fn a_released_playback_still_reports_ended() {
774 let mut a = AudioStore::default();
775 let s = sound();
776 let k = Key::ROOT.str("chime");
777 let spec = {
778 let mut spec = AudioSpec::new(s).finish();
779 spec.tag = Some(Value::str("chime"));
780 spec
781 };
782 a.declare(WindowId::MAIN, k, OriginId::HOST, spec);
783 a.reconcile(WindowId::MAIN);
784 let p = a.playback_of(WindowId::MAIN, k).unwrap();
785 a.take_commands();
786
787 a.reconcile(WindowId::MAIN);
788 assert_eq!(a.take_commands(), vec![]);
789 let ev = a.ended(p).expect("a released playback still reports ended");
790 assert_eq!(ev.key, k);
791 assert_eq!(ev.payload.get_str("tag"), Some("chime"));
792 }
793
794 #[test]
799 fn a_refused_tagged_playback_reports_refused_and_warns() {
800 let mut a = AudioStore::default();
801 let s = sound();
802 let k = Key::ROOT.str("chime");
803 let p = a.play(
804 OriginId::HOST,
805 WindowId::MAIN,
806 k,
807 s,
808 PlayOptions::default().tag(Value::str("chime")),
809 );
810 let (event, warning) = a.refused(p);
811 let ev = event.expect("a tagged playback hears the refusal");
812 assert_eq!(ev.key, k);
813 assert_eq!(ev.kind(), Some("sound"));
814 assert_eq!(
815 ev.payload.get_str("phase"),
816 Some("refused"),
817 "told apart from the `ended` that will never come"
818 );
819 assert_eq!(ev.payload.get_str("tag"), Some("chime"));
820 assert_eq!(ev.payload.get_int("playback"), Some(p.0 as i64));
821 assert_eq!(warning.code, crate::diag::PLAYBACK_REFUSED);
822 assert_eq!(warning.key, k);
823
824 assert!(a.refused(p).0.is_none());
826 assert!(a.ended(p).is_none(), "and it can never end afterwards");
827 }
828
829 #[test]
832 fn a_refused_untagged_playback_is_the_warning_alone() {
833 let mut a = AudioStore::default();
834 let s = sound();
835 let p = a.play(
836 OriginId::HOST,
837 WindowId::MAIN,
838 Key::ROOT,
839 s,
840 PlayOptions::default(),
841 );
842 let (event, warning) = a.refused(p);
843 assert!(event.is_none(), "nothing asked to hear about this one");
844 assert_eq!(warning.code, crate::diag::PLAYBACK_REFUSED);
845 assert_eq!(warning.key, Key::ROOT);
846 }
847
848 #[test]
852 fn a_refused_released_playback_still_reports() {
853 let mut a = AudioStore::default();
854 let s = sound();
855 let k = Key::ROOT.str("chime");
856 let spec = {
857 let mut spec = AudioSpec::new(s).finish();
858 spec.tag = Some(Value::str("chime"));
859 spec
860 };
861 a.declare(WindowId::MAIN, k, OriginId::HOST, spec);
862 a.reconcile(WindowId::MAIN);
863 let p = a.playback_of(WindowId::MAIN, k).unwrap();
864 a.take_commands();
865
866 a.reconcile(WindowId::MAIN);
868 assert!(a.playback_of(WindowId::MAIN, k).is_none());
869
870 let (event, warning) = a.refused(p);
871 let ev = event.expect("a released playback still hears the refusal");
872 assert_eq!(ev.key, k);
873 assert_eq!(ev.payload.get_str("phase"), Some("refused"));
874 assert_eq!(warning.key, k);
875 }
876
877 #[test]
880 fn an_untagged_audio_node_warns_on_its_own_key() {
881 let mut a = AudioStore::default();
882 let s = sound();
883 let k = Key::ROOT.str("bed");
884 a.declare(
885 WindowId::MAIN,
886 k,
887 OriginId::HOST,
888 AudioSpec::new(s).looped(),
889 );
890 a.reconcile(WindowId::MAIN);
891 let p = a.playback_of(WindowId::MAIN, k).unwrap();
892
893 let (event, warning) = a.refused(p);
894 assert!(event.is_none());
895 assert_eq!(warning.key, k);
896 }
897
898 #[test]
899 fn audio_nodes_reconcile_by_key() {
900 let mut a = AudioStore::default();
901 let s = sound();
902 let k = Key::ROOT.str("music");
903 a.declare(
904 WindowId::MAIN,
905 k,
906 OriginId::HOST,
907 AudioSpec::new(s).looped(),
908 );
909 a.reconcile(WindowId::MAIN);
910 let cmds = a.take_commands();
911 assert!(matches!(
912 cmds.as_slice(),
913 [AudioCommand::Play { looped: true, .. }]
914 ));
915 let p = a.playback_of(WindowId::MAIN, k).unwrap();
916
917 a.declare(
919 WindowId::MAIN,
920 k,
921 OriginId::HOST,
922 AudioSpec::new(s).looped(),
923 );
924 a.reconcile(WindowId::MAIN);
925 assert!(a.take_commands().is_empty());
926
927 a.declare(
929 WindowId::MAIN,
930 k,
931 OriginId::HOST,
932 AudioSpec::new(s).looped().volume(0.5).paused(true),
933 );
934 a.reconcile(WindowId::MAIN);
935 let cmds = a.take_commands();
936 assert_eq!(
937 cmds,
938 vec![
939 AudioCommand::SetVolume {
940 playback: p,
941 volume: 0.5,
942 tween_ms: 0.0
943 },
944 AudioCommand::Pause {
945 playback: p,
946 fade_ms: 0.0
947 }
948 ]
949 );
950
951 a.reconcile(WindowId::MAIN);
953 assert_eq!(
954 a.take_commands(),
955 vec![AudioCommand::Stop {
956 playback: p,
957 fade_ms: 0.0
958 }]
959 );
960 assert!(a.playback_of(WindowId::MAIN, k).is_none());
961 }
962
963 #[test]
964 fn changing_src_restarts() {
965 let mut a = AudioStore::default();
966 let mut r = Resources::new(SessionId::next());
967 let (s1, s2) = (r.add_sound(vec![0; 4]), r.add_sound(vec![1; 4]));
968 let k = Key::ROOT.str("fx");
969 a.declare(WindowId::MAIN, k, OriginId::HOST, AudioSpec::new(s1));
970 a.reconcile(WindowId::MAIN);
971 let p1 = a.playback_of(WindowId::MAIN, k).unwrap();
972 a.take_commands();
973 a.declare(WindowId::MAIN, k, OriginId::HOST, AudioSpec::new(s2));
974 a.reconcile(WindowId::MAIN);
975 let cmds = a.take_commands();
976 assert!(matches!(
977 cmds.as_slice(),
978 [
979 AudioCommand::Stop { playback, .. },
980 AudioCommand::Play { sound, .. }
981 ] if *playback == p1 && *sound == s2
982 ));
983 }
984}