1use crate::prelude::*;
38use shuyi_assets::audio::AudioManifest;
39pub use shuyi_assets::audio::MusicSwitch;
40use shuyi_assets::dialogue::LineId;
41#[cfg(doc)]
42use shuyi_assets::voice::ClipVariant;
43
44#[cfg(doc)]
45use crate::GameData;
46use crate::input::frame_seconds;
47use alloc::collections::{BTreeMap, BTreeSet};
48
49mod added;
50mod handles;
51mod samples;
52
53use added::Added;
54pub use added::{ADDED_BYTES_MAX, ADDED_SECONDS_MAX, AddSoundError};
55use handles::Handle;
56pub use samples::{Samples, SamplesError, SharedSamples};
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum MenuSound {
62 Move,
64 Select,
66 Cancel,
68 Denied,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct AudioConfig {
79 pub move_cue: &'static str,
81 pub select_cue: &'static str,
83 pub cancel_cue: &'static str,
85 pub denied_cue: &'static str,
87 pub cursor_cue: &'static str,
89}
90
91impl AudioConfig {
92 pub const DEFAULT: Self = Self {
94 move_cue: "ui_move",
95 select_cue: "ui_select",
96 cancel_cue: "ui_cancel",
97 denied_cue: "ui_cancel",
98 cursor_cue: "ui_cursor",
99 };
100
101 pub const fn cue(&self, sound: MenuSound) -> &'static str {
103 match sound {
104 MenuSound::Move => self.move_cue,
105 MenuSound::Select => self.select_cue,
106 MenuSound::Cancel => self.cancel_cue,
107 MenuSound::Denied => self.denied_cue,
108 }
109 }
110}
111
112impl Default for AudioConfig {
113 fn default() -> Self {
114 Self::DEFAULT
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
122pub struct SoundId(u32);
123
124impl SoundId {
125 pub const fn get(self) -> u32 {
127 self.0
128 }
129}
130
131#[derive(Debug, Clone, PartialEq)]
134#[non_exhaustive]
135pub enum AudioRequest<V> {
136 PlaySound {
138 cue: String,
140 volume: f32,
142 },
143 StartSound {
147 id: SoundId,
149 cue: String,
151 volume: f32,
153 },
154 StopSound {
156 id: SoundId,
158 },
159 SoundVolume {
161 id: SoundId,
163 volume: f32,
165 },
166 PauseSounds,
170 ResumeSounds,
172 #[non_exhaustive]
175 PlayMusic {
176 cue: String,
178 switch: Option<MusicSwitch>,
181 },
182 StopMusic,
185 #[non_exhaustive]
190 StopMusicWith {
191 fade_ms: u32,
193 },
194 PlayVoice {
197 line: LineId,
199 variant: V,
201 },
202 StopVoice,
204 PreloadVoices {
208 lines: Vec<(LineId, V)>,
210 },
211 #[non_exhaustive]
217 AddSound {
218 cue: String,
220 samples: SharedSamples,
222 },
223 RemoveSound {
226 cue: String,
228 },
229}
230
231impl<V> AudioRequest<V> {
232 pub fn play_music(cue: impl Into<String>, switch: Option<MusicSwitch>) -> Self {
235 Self::PlayMusic {
236 cue: cue.into(),
237 switch,
238 }
239 }
240
241 pub fn stop_music_with(fade_ms: u32) -> Self {
244 Self::StopMusicWith { fade_ms }
245 }
246
247 pub fn cue(&self) -> Option<&str> {
250 match self {
251 Self::PlaySound { cue, .. }
252 | Self::StartSound { cue, .. }
253 | Self::PlayMusic { cue, .. } => Some(cue),
254 Self::AddSound { .. }
255 | Self::RemoveSound { .. }
256 | Self::StopSound { .. }
257 | Self::SoundVolume { .. }
258 | Self::PauseSounds
259 | Self::ResumeSounds
260 | Self::StopMusic
261 | Self::StopMusicWith { .. }
262 | Self::PlayVoice { .. }
263 | Self::StopVoice
264 | Self::PreloadVoices { .. } => None,
265 }
266 }
267}
268
269#[derive(Debug, Clone, PartialEq)]
273pub struct AudioQueue<V> {
274 requests: Vec<AudioRequest<V>>,
275 config: AudioConfig,
276 last_id: u32,
278 handles: BTreeMap<SoundId, Handle>,
280 owner: usize,
283 pauses: BTreeSet<usize>,
286 added: Added,
288}
289
290impl<V> Default for AudioQueue<V> {
291 fn default() -> Self {
292 Self::new(AudioConfig::DEFAULT)
293 }
294}
295
296impl<V> AudioQueue<V> {
297 pub fn new(config: AudioConfig) -> Self {
299 Self {
300 requests: Vec::new(),
301 config,
302 last_id: 0,
303 handles: BTreeMap::new(),
304 owner: 0,
305 pauses: BTreeSet::new(),
306 added: Added::default(),
307 }
308 }
309
310 pub fn config(&self) -> AudioConfig {
312 self.config
313 }
314
315 pub fn menu(&mut self, sound: MenuSound) {
317 self.play_sound(self.config.cue(sound));
318 }
319
320 pub fn cursor_tick(&mut self) {
322 self.play_sound(self.config.cursor_cue);
323 }
324
325 pub fn play_sound(&mut self, cue: &str) {
327 self.play_sound_at(cue, 1.0);
328 }
329
330 pub fn play_sound_at(&mut self, cue: &str, volume: f32) {
334 if self.added.silences(cue) {
335 return;
336 }
337 self.requests.push(AudioRequest::PlaySound {
338 cue: cue.to_owned(),
339 volume: clamp_volume(volume),
340 });
341 }
342
343 pub fn play_music(&mut self, cue: &str) {
346 self.requests.push(AudioRequest::play_music(cue, None));
347 }
348
349 pub fn play_music_with(&mut self, cue: &str, switch: MusicSwitch) {
353 self.requests
354 .push(AudioRequest::play_music(cue, Some(switch)));
355 }
356
357 pub fn stop_music(&mut self) {
359 self.requests.push(AudioRequest::StopMusic);
360 }
361
362 pub fn stop_music_with(&mut self, fade_ms: u32) {
367 self.requests.push(AudioRequest::stop_music_with(fade_ms));
368 }
369
370 pub fn play_voice(&mut self, line: &LineId, variant: V) {
374 self.requests.push(AudioRequest::PlayVoice {
375 line: line.clone(),
376 variant,
377 });
378 }
379
380 pub fn stop_voice(&mut self) {
382 self.requests.push(AudioRequest::StopVoice);
383 }
384
385 pub fn preload_voices(&mut self, lines: Vec<(LineId, V)>) {
388 self.requests.push(AudioRequest::PreloadVoices { lines });
389 }
390
391 pub fn pending(&self) -> &[AudioRequest<V>] {
393 &self.requests
394 }
395
396 pub fn take(&mut self) -> Vec<AudioRequest<V>> {
398 core::mem::take(&mut self.requests)
399 }
400}
401
402fn clamp_volume(volume: f32) -> f32 {
404 if volume.is_nan() {
405 0.0
406 } else {
407 volume.clamp(0.0, 1.0)
408 }
409}
410
411pub fn pick_from_pool<'a>(manifest: &'a AudioManifest, pool: &str, seed: u64) -> Option<&'a str> {
416 let cues = manifest.pools.get(pool)?;
417 let len = u64::try_from(cues.len()).ok().filter(|&n| n > 0)?;
418 let index = usize::try_from(splitmix64(seed) % len).ok()?;
419 cues.get(index).map(String::as_str)
420}
421
422pub fn splitmix64(seed: u64) -> u64 {
425 let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
426 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
427 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
428 z ^ (z >> 31)
429}
430
431#[derive(Debug, Clone, PartialEq)]
437pub struct MusicClock {
438 pub cue: String,
440 pub position: f32,
443 pub length: f32,
445}
446
447impl MusicClock {
448 pub fn from_elapsed(manifest: &AudioManifest, cue: &str, elapsed: f64) -> Option<Self> {
452 let track = manifest.music.get(cue)?;
453 let length = f64::from(track.length_ms) / 1000.0;
454 if !elapsed.is_finite() || track.length_ms == 0 {
455 return None;
456 }
457 let elapsed = elapsed.max(0.0);
458 let position = if track.looped {
459 elapsed % length
460 } else {
461 elapsed
462 };
463 #[allow(clippy::cast_possible_truncation)]
465 let (position, length) = (position as f32, length as f32);
466 let position = if position < length {
467 position
468 } else if track.looped {
469 0.0
471 } else {
472 return None;
473 };
474 Some(Self {
475 cue: cue.to_owned(),
476 position,
477 length,
478 })
479 }
480}
481
482#[derive(Debug, Clone, PartialEq, serde::Serialize)]
485pub enum MusicCommand {
486 Load {
488 cue: String,
490 },
491 #[non_exhaustive]
495 Start {
496 cue: String,
498 gain: f32,
502 },
503 Gain {
505 cue: String,
507 gain: f32,
509 },
510 Stop {
512 cue: String,
514 },
515}
516
517impl MusicCommand {
518 pub fn start(cue: impl Into<String>, gain: f32) -> Self {
520 Self::Start {
521 cue: cue.into(),
522 gain,
523 }
524 }
525}
526
527#[derive(Debug, Clone, PartialEq)]
536pub struct MusicState {
537 default: MusicSwitch,
539 current: Option<Track>,
541 next: Option<Track>,
545}
546
547#[derive(Debug, Clone, PartialEq)]
549struct Track {
550 cue: String,
551 started: bool,
553 heard: bool,
555 waited: f32,
558 ramp: Ramp,
561}
562
563#[derive(Debug, Clone, PartialEq)]
566struct Ramp {
567 from: f32,
569 rising: bool,
571 secs: f32,
573 elapsed: f32,
575}
576
577impl Ramp {
578 fn at(gain: f32) -> Self {
580 Self {
581 from: gain,
582 rising: gain >= 1.0,
583 secs: 0.0,
584 elapsed: 0.0,
585 }
586 }
587
588 fn to(&self) -> f32 {
589 if self.rising { 1.0 } else { 0.0 }
590 }
591
592 fn done(&self) -> bool {
594 self.elapsed >= self.secs * (self.from - self.to()).abs()
595 }
596
597 fn gain(&self) -> f32 {
598 if self.done() {
599 return self.to();
600 }
601 let moved = self.elapsed / self.secs;
603 let gain = if self.rising {
604 self.from + moved
605 } else {
606 self.from - moved
607 };
608 gain.clamp(0.0, 1.0)
609 }
610}
611
612impl Track {
613 fn new(cue: &str, switch: MusicSwitch) -> Self {
616 let from = if switch.in_ms == 0 { 1.0 } else { 0.0 };
617 Self {
618 cue: cue.into(),
619 started: false,
620 heard: false,
621 waited: 0.0,
622 ramp: Ramp {
623 from,
624 rising: true,
625 secs: secs(switch.in_ms),
626 elapsed: 0.0,
627 },
628 }
629 }
630
631 fn start(&mut self, out: &mut Vec<MusicCommand>) {
632 self.started = true;
633 out.push(MusicCommand::start(self.cue.clone(), self.ramp.gain()));
634 }
635
636 fn aim(&mut self, rising: bool, ms: u32) {
639 let secs = secs(ms);
640 if self.ramp.rising != rising || self.ramp.secs.to_bits() != secs.to_bits() {
641 self.ramp = Ramp {
642 from: self.ramp.gain(),
643 rising,
644 secs,
645 elapsed: 0.0,
646 };
647 }
648 }
649
650 fn sounds(&self) -> bool {
652 self.heard || self.waited >= MusicState::LOAD_WAIT_SECS
653 }
654
655 fn coming_in_unheard(&self) -> bool {
658 self.started && self.ramp.rising && !self.sounds()
659 }
660
661 fn wait(&mut self, dt: f32) {
663 if self.started && !self.heard {
664 self.waited = (self.waited + dt).min(MusicState::LOAD_WAIT_SECS);
665 }
666 }
667
668 fn step(&mut self, dt: f32, out: &mut Vec<MusicCommand>) -> bool {
672 if !self.started {
673 return false;
674 }
675 let rising = self.ramp.rising;
676 if rising && (self.ramp.done() || !self.sounds()) {
677 return false;
678 }
679 self.ramp.elapsed += dt;
680 if !rising && self.ramp.done() {
681 out.push(MusicCommand::Stop {
682 cue: self.cue.clone(),
683 });
684 return true;
685 }
686 out.push(MusicCommand::Gain {
687 cue: self.cue.clone(),
688 gain: self.ramp.gain(),
689 });
690 false
691 }
692}
693
694fn secs(ms: u32) -> f32 {
696 #[allow(clippy::cast_precision_loss)]
698 let ms = ms as f32;
699 ms / 1000.0
700}
701
702impl MusicState {
703 pub const LOAD_WAIT_SECS: f32 = 3.0;
707
708 pub fn new(fade_secs: f32) -> Self {
712 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
714 let ms = (fade_secs.max(0.0) * 1000.0).round() as u32;
715 Self::with_switch(MusicSwitch::after_fade(ms))
716 }
717
718 pub fn with_switch(default: MusicSwitch) -> Self {
721 Self {
722 default,
723 current: None,
724 next: None,
725 }
726 }
727
728 pub fn current(&self) -> Option<&str> {
730 self.current.as_ref().map(|t| t.cue.as_str())
731 }
732
733 pub fn target(&self) -> Option<&str> {
736 [&self.next, &self.current]
737 .into_iter()
738 .flatten()
739 .find(|t| t.ramp.rising)
740 .map(|t| t.cue.as_str())
741 }
742
743 pub fn heard(&mut self, clock: Option<&MusicClock>) {
749 let Some(clock) = clock else {
750 return;
751 };
752 for track in [&mut self.current, &mut self.next].into_iter().flatten() {
753 if track.started && track.cue == clock.cue {
754 track.heard = true;
755 }
756 }
757 }
758
759 pub fn request<V>(&mut self, request: &AudioRequest<V>, out: &mut Vec<MusicCommand>) {
762 match request {
763 AudioRequest::PlaySound { .. }
764 | AudioRequest::StartSound { .. }
765 | AudioRequest::StopSound { .. }
766 | AudioRequest::SoundVolume { .. }
767 | AudioRequest::PauseSounds
768 | AudioRequest::ResumeSounds
769 | AudioRequest::PlayVoice { .. }
770 | AudioRequest::StopVoice
771 | AudioRequest::PreloadVoices { .. }
772 | AudioRequest::AddSound { .. }
773 | AudioRequest::RemoveSound { .. } => {}
774 AudioRequest::PlayMusic { cue, switch } => {
775 self.play(cue, switch.unwrap_or(self.default), out);
776 }
777 AudioRequest::StopMusic => self.stop(self.default.out_ms, out),
778 AudioRequest::StopMusicWith { fade_ms } => self.stop(*fade_ms, out),
779 }
780 }
781
782 fn play(&mut self, cue: &str, switch: MusicSwitch, out: &mut Vec<MusicCommand>) {
783 if self.current.is_none() {
784 let mut track = Track::new(cue, switch);
785 out.push(MusicCommand::Load { cue: cue.into() });
786 track.start(out);
787 self.current = Some(track);
788 return;
789 }
790 if self.target() == Some(cue) {
791 return;
792 }
793 self.drop_waiting(out);
794 if let Some(mut back) = self.take(cue) {
795 if let Some(other) = &mut self.current {
798 other.aim(false, switch.out_ms);
799 }
800 if switch.in_ms == 0 {
801 back.ramp = Ramp::at(1.0);
802 out.push(MusicCommand::Gain {
803 cue: cue.into(),
804 gain: 1.0,
805 });
806 } else {
807 back.aim(true, switch.in_ms);
808 }
809 self.push(back);
810 return;
811 }
812 if self.next.is_some() {
813 if let Some(older) = self.current.take() {
815 out.push(MusicCommand::Stop { cue: older.cue });
816 }
817 self.current = self.next.take();
818 }
819 if let Some(old) = &mut self.current {
820 old.aim(false, switch.out_ms);
821 }
822 out.push(MusicCommand::Load { cue: cue.into() });
823 let mut track = Track::new(cue, switch);
824 if switch.overlap {
825 track.start(out);
826 }
827 self.next = Some(track);
828 }
829
830 fn drop_waiting(&mut self, out: &mut Vec<MusicCommand>) {
832 if let Some(next) = self.next.take_if(|t| !t.started) {
833 out.push(MusicCommand::Stop { cue: next.cue });
834 }
835 }
836
837 fn take(&mut self, cue: &str) -> Option<Track> {
839 if let Some(next) = self.next.take_if(|t| t.cue == cue) {
840 return Some(next);
841 }
842 let current = self.current.take_if(|t| t.cue == cue)?;
843 self.current = self.next.take();
844 Some(current)
845 }
846
847 fn push(&mut self, track: Track) {
849 if self.current.is_none() {
850 self.current = Some(track);
851 } else {
852 self.next = Some(track);
853 }
854 }
855
856 fn stop(&mut self, fade_ms: u32, out: &mut Vec<MusicCommand>) {
859 self.drop_waiting(out);
860 for track in [&mut self.current, &mut self.next].into_iter().flatten() {
861 track.aim(false, fade_ms);
862 }
863 }
864
865 pub fn update(&mut self, dt: f32, out: &mut Vec<MusicCommand>) {
868 let dt = frame_seconds(dt);
869 for track in [&mut self.current, &mut self.next].into_iter().flatten() {
870 track.wait(dt);
871 }
872 let waiting = self.next.as_ref().is_some_and(Track::coming_in_unheard);
875 if let Some(current) = &mut self.current {
876 let held = waiting && !current.ramp.rising;
877 if !held && current.step(dt, out) {
878 self.current = None;
879 }
880 }
881 if let Some(next) = &mut self.next
882 && next.step(dt, out)
883 {
884 self.next = None;
885 }
886 if self.current.is_none() {
887 self.current = self.next.take();
888 if let Some(track) = self.current.as_mut().filter(|t| !t.started) {
889 track.start(out);
890 }
891 }
892 }
893}
894
895#[cfg(test)]
896mod tests {
897 use super::*;
898 use crate::toy::Voice;
899 use shuyi_assets::audio::{CreditRef, MusicCue};
900
901 type AudioRequest = super::AudioRequest<Voice>;
902 type AudioQueue = super::AudioQueue<Voice>;
903
904 fn play(cue: &str) -> AudioRequest {
905 AudioRequest::PlayMusic {
906 cue: cue.into(),
907 switch: None,
908 }
909 }
910
911 fn load(cue: &str) -> MusicCommand {
912 MusicCommand::Load { cue: cue.into() }
913 }
914
915 fn start(cue: &str) -> MusicCommand {
916 MusicCommand::Start {
917 cue: cue.into(),
918 gain: 1.0,
919 }
920 }
921
922 fn stop(cue: &str) -> MusicCommand {
923 MusicCommand::Stop { cue: cue.into() }
924 }
925
926 fn gain(cue: &str, gain: f32) -> MusicCommand {
927 MusicCommand::Gain {
928 cue: cue.into(),
929 gain,
930 }
931 }
932
933 fn frame(m: &mut MusicState, requests: &[AudioRequest], dt: f32) -> Vec<MusicCommand> {
935 let mut out = Vec::new();
936 for r in requests {
937 m.request(r, &mut out);
938 }
939 m.update(dt, &mut out);
940 out
941 }
942
943 fn playing(cue: &str) -> MusicState {
945 let mut m = MusicState::new(0.5);
946 frame(&mut m, &[play(cue)], 0.0);
947 m
948 }
949
950 #[test]
951 fn the_first_track_starts_at_once() {
952 let mut m = MusicState::new(0.5);
953 assert_eq!(m.current(), None);
954 assert_eq!(
955 frame(&mut m, &[play("title")], 0.1),
956 [load("title"), start("title")]
957 );
958 assert_eq!(m.current(), Some("title"));
959 assert_eq!(m.target(), Some("title"));
960 assert_eq!(frame(&mut m, &[], 0.1), []);
961 }
962
963 #[test]
964 fn the_same_cue_does_not_restart() {
965 let mut m = playing("title");
966 assert_eq!(frame(&mut m, &[play("title"), play("title")], 0.1), []);
967 assert_eq!(m.current(), Some("title"));
968 }
969
970 #[test]
971 fn a_switch_fades_out_then_starts_the_new_track() {
972 let mut m = playing("title");
973 assert_eq!(
974 frame(&mut m, &[play("battle")], 0.0),
975 [load("battle"), gain("title", 1.0)]
976 );
977 assert_eq!(m.current(), Some("title"));
978 assert_eq!(m.target(), Some("battle"));
979 assert_eq!(frame(&mut m, &[], 0.25), [gain("title", 0.5)]);
980 assert_eq!(frame(&mut m, &[], 0.125), [gain("title", 0.25)]);
981 assert_eq!(frame(&mut m, &[play("battle")], 0.0), [gain("title", 0.25)]);
983 assert_eq!(frame(&mut m, &[], 0.125), [stop("title"), start("battle")]);
984 assert_eq!(m.current(), Some("battle"));
985 assert_eq!(frame(&mut m, &[], 0.1), []);
986 }
987
988 #[test]
989 fn stop_music_fades_out_to_silence() {
990 let mut m = playing("title");
991 let stop_req = AudioRequest::StopMusic;
992 assert_eq!(
993 frame(&mut m, core::slice::from_ref(&stop_req), 0.25),
994 [gain("title", 0.5)]
995 );
996 assert_eq!(m.target(), None);
997 assert_eq!(
998 frame(&mut m, core::slice::from_ref(&stop_req), 0.0),
999 [gain("title", 0.5)]
1000 );
1001 assert_eq!(frame(&mut m, &[], 0.3), [stop("title")]);
1002 assert_eq!(m.current(), None);
1003 assert_eq!(frame(&mut m, &[stop_req], 1.0), []);
1005 }
1006
1007 #[test]
1008 fn a_new_request_during_a_fade_wins() {
1009 let mut m = playing("title");
1010 frame(&mut m, &[play("battle")], 0.25);
1011 assert_eq!(
1013 frame(&mut m, &[play("boss")], 0.0),
1014 [stop("battle"), load("boss"), gain("title", 0.5)]
1015 );
1016 assert_eq!(frame(&mut m, &[], 0.25), [stop("title"), start("boss")]);
1017 assert_eq!(m.current(), Some("boss"));
1018 frame(&mut m, &[play("town")], 0.25);
1020 assert_eq!(
1021 frame(&mut m, &[AudioRequest::StopMusic], 0.0),
1022 [stop("town"), gain("boss", 0.5)]
1023 );
1024 assert_eq!(frame(&mut m, &[], 0.25), [stop("boss")]);
1025 assert_eq!(m.current(), None);
1026 let mut m = playing("title");
1028 frame(&mut m, &[AudioRequest::StopMusic], 0.25);
1029 assert_eq!(
1030 frame(&mut m, &[play("battle")], 0.25),
1031 [load("battle"), stop("title"), start("battle")]
1032 );
1033 }
1034
1035 #[test]
1036 fn asking_for_the_fading_track_keeps_it() {
1037 let mut m = playing("title");
1038 frame(&mut m, &[play("battle")], 0.25);
1039 assert_eq!(
1040 frame(&mut m, &[play("title")], 1.0),
1041 [stop("battle"), gain("title", 1.0)]
1042 );
1043 assert_eq!(m.current(), Some("title"));
1044 assert_eq!(m.target(), Some("title"));
1045 let mut m = playing("title");
1046 frame(&mut m, &[AudioRequest::StopMusic], 0.25);
1047 assert_eq!(frame(&mut m, &[play("title")], 1.0), [gain("title", 1.0)]);
1048 assert_eq!(m.current(), Some("title"));
1049 }
1050
1051 #[test]
1052 fn a_zero_fade_switches_in_the_same_frame() {
1053 for secs in [0.0, -1.0, f32::NAN] {
1054 let mut m = MusicState::new(secs);
1055 frame(&mut m, &[play("title")], 0.0);
1056 assert_eq!(
1057 frame(&mut m, &[play("battle")], 0.0),
1058 [load("battle"), stop("title"), start("battle")]
1059 );
1060 }
1061 }
1062
1063 #[test]
1064 fn bad_frame_times_do_not_advance_a_fade() {
1065 let mut m = playing("title");
1066 frame(&mut m, &[play("battle")], 0.25);
1067 for dt in [-1.0, f32::NAN, f32::INFINITY] {
1068 assert_eq!(frame(&mut m, &[], dt), [gain("title", 0.5)]);
1069 }
1070 }
1071
1072 fn play_with(cue: &str, switch: MusicSwitch) -> AudioRequest {
1073 AudioRequest::play_music(cue, Some(switch))
1074 }
1075
1076 fn start_at(cue: &str, gain: f32) -> MusicCommand {
1077 MusicCommand::start(cue, gain)
1078 }
1079
1080 fn clock(cue: &str) -> MusicClock {
1082 MusicClock {
1083 cue: cue.into(),
1084 position: 0.0,
1085 length: 10.0,
1086 }
1087 }
1088
1089 fn sounding(cue: &str) -> MusicState {
1091 let mut m = playing(cue);
1092 m.heard(Some(&clock(cue)));
1093 m
1094 }
1095
1096 fn crossfading(secs: f32) -> MusicState {
1099 let mut m = sounding("title");
1100 frame(
1101 &mut m,
1102 &[play_with("battle", MusicSwitch::crossfade(1000))],
1103 0.0,
1104 );
1105 m.heard(Some(&clock("battle")));
1106 frame(&mut m, &[], secs);
1107 m
1108 }
1109
1110 #[test]
1111 fn a_crossfade_starts_the_new_track_at_once_and_fades_both_once_it_sounds() {
1112 let mut m = sounding("title");
1113 let crossfade = play_with("battle", MusicSwitch::crossfade(1000));
1114 assert_eq!(
1115 frame(&mut m, &[crossfade], 0.0),
1116 [load("battle"), start_at("battle", 0.0)]
1117 );
1118 assert_eq!((m.current(), m.target()), (Some("title"), Some("battle")));
1119 assert_eq!(frame(&mut m, &[], 0.25), []);
1121 m.heard(Some(&clock("title")));
1122 assert_eq!(frame(&mut m, &[], 0.25), []);
1123 m.heard(Some(&clock("battle")));
1124 assert_eq!(
1125 frame(&mut m, &[], 0.5),
1126 [gain("title", 0.5), gain("battle", 0.5)]
1127 );
1128 assert_eq!(
1129 frame(&mut m, &[], 0.5),
1130 [stop("title"), gain("battle", 1.0)]
1131 );
1132 assert_eq!((m.current(), m.target()), (Some("battle"), Some("battle")));
1133 assert_eq!(frame(&mut m, &[], 0.5), []);
1134 }
1135
1136 #[test]
1137 fn a_fade_in_starts_at_silence_and_rises_once_the_track_sounds() {
1138 let mut m = MusicState::with_switch(MusicSwitch::new(500, 1000, false));
1139 assert_eq!(
1140 frame(&mut m, &[play("title")], 0.5),
1141 [load("title"), start_at("title", 0.0)]
1142 );
1143 m.heard(None);
1145 m.heard(Some(&clock("battle")));
1146 assert_eq!(frame(&mut m, &[], 0.5), []);
1147 m.heard(Some(&clock("title")));
1148 assert_eq!(frame(&mut m, &[], 0.5), [gain("title", 0.5)]);
1149 m.heard(None);
1151 assert_eq!(frame(&mut m, &[], 0.5), [gain("title", 1.0)]);
1152 assert_eq!(frame(&mut m, &[], 0.5), []);
1153 }
1154
1155 #[test]
1156 fn without_overlap_the_new_track_fades_in_after_the_old_one_stops() {
1157 let mut m = sounding("title");
1158 let switch = play_with("battle", MusicSwitch::new(500, 1000, false));
1159 assert_eq!(
1160 frame(&mut m, core::slice::from_ref(&switch), 0.25),
1161 [load("battle"), gain("title", 0.5)]
1162 );
1163 m.heard(Some(&clock("battle")));
1165 assert_eq!(
1166 frame(&mut m, &[], 0.25),
1167 [stop("title"), start_at("battle", 0.0)]
1168 );
1169 assert_eq!(frame(&mut m, &[], 0.5), []);
1170 m.heard(Some(&clock("battle")));
1171 assert_eq!(frame(&mut m, &[], 0.25), [gain("battle", 0.25)]);
1172 let mut m = sounding("title");
1175 let switch = play_with("battle", MusicSwitch::new(3000, 1000, false));
1176 frame(&mut m, &[switch], 0.0);
1177 assert_eq!(frame(&mut m, &[], 1.5), [gain("title", 0.5)]);
1178 assert_eq!(
1179 frame(&mut m, &[], 1.5),
1180 [stop("title"), start_at("battle", 0.0)]
1181 );
1182 assert_eq!(frame(&mut m, &[], 1.0), []);
1183 }
1184
1185 #[test]
1186 fn a_track_that_never_sounds_is_faded_in_after_the_load_wait() {
1187 let mut m = sounding("title");
1188 frame(
1189 &mut m,
1190 &[play_with("battle", MusicSwitch::crossfade(1000))],
1191 0.0,
1192 );
1193 assert_eq!(frame(&mut m, &[], 2.5), []);
1194 assert_eq!(MusicState::LOAD_WAIT_SECS, 3.0);
1195 assert_eq!(
1196 frame(&mut m, &[], 0.5),
1197 [gain("title", 0.5), gain("battle", 0.5)]
1198 );
1199 assert_eq!(
1200 frame(&mut m, &[], 0.5),
1201 [stop("title"), gain("battle", 1.0)]
1202 );
1203 }
1204
1205 #[test]
1206 fn a_switch_during_a_crossfade_stops_the_older_track_and_fades_out_the_newer() {
1207 let mut m = crossfading(0.5);
1208 assert_eq!(
1209 frame(
1210 &mut m,
1211 &[play_with("boss", MusicSwitch::crossfade(1000))],
1212 0.0
1213 ),
1214 [stop("title"), load("boss"), start_at("boss", 0.0)]
1215 );
1216 assert_eq!((m.current(), m.target()), (Some("battle"), Some("boss")));
1217 m.heard(Some(&clock("boss")));
1218 assert_eq!(
1220 frame(&mut m, &[], 0.25),
1221 [gain("battle", 0.25), gain("boss", 0.25)]
1222 );
1223 assert_eq!(
1224 frame(&mut m, &[], 0.25),
1225 [stop("battle"), gain("boss", 0.5)]
1226 );
1227 let mut m = crossfading(0.5);
1230 assert_eq!(
1231 frame(&mut m, &[play("boss")], 0.125),
1232 [stop("title"), load("boss"), gain("battle", 0.25)]
1233 );
1234 assert_eq!(frame(&mut m, &[], 0.125), [stop("battle"), start("boss")]);
1235 }
1236
1237 #[test]
1238 fn asking_again_for_the_track_fading_out_in_a_crossfade_brings_it_back() {
1239 let mut m = crossfading(0.25);
1240 let back = play_with("title", MusicSwitch::crossfade(1000));
1241 assert_eq!(
1242 frame(&mut m, &[back], 0.0),
1243 [gain("battle", 0.25), gain("title", 0.75)]
1244 );
1245 assert_eq!((m.current(), m.target()), (Some("battle"), Some("title")));
1246 assert_eq!(
1247 frame(&mut m, &[], 0.125),
1248 [gain("battle", 0.125), gain("title", 0.875)]
1249 );
1250 assert_eq!(
1251 frame(&mut m, &[], 0.125),
1252 [stop("battle"), gain("title", 1.0)]
1253 );
1254 assert_eq!((m.current(), m.target()), (Some("title"), Some("title")));
1255 assert_eq!(frame(&mut m, &[], 0.125), []);
1257 let mut m = crossfading(0.25);
1259 let back = play_with("title", MusicSwitch::new(500, 0, true));
1260 assert_eq!(
1261 frame(&mut m, &[back], 0.0),
1262 [gain("title", 1.0), gain("battle", 0.25)]
1263 );
1264 }
1265
1266 #[test]
1267 fn stop_music_during_a_crossfade_fades_both_tracks_from_their_gains() {
1268 let mut m = crossfading(0.25);
1269 assert_eq!(
1270 frame(&mut m, &[AudioRequest::StopMusic], 0.125),
1271 [gain("title", 0.5), stop("battle")]
1272 );
1273 assert_eq!((m.current(), m.target()), (Some("title"), None));
1274 assert_eq!(frame(&mut m, &[], 0.25), [stop("title")]);
1275 assert_eq!(m.current(), None);
1276 }
1277
1278 fn stop_with(fade_ms: u32) -> AudioRequest {
1279 AudioRequest::stop_music_with(fade_ms)
1280 }
1281
1282 #[test]
1283 fn a_stop_with_its_own_fade_takes_that_long_from_full_gain() {
1284 let mut m = playing("title");
1286 assert_eq!(
1287 frame(&mut m, &[stop_with(3000)], 1.0),
1288 [gain("title", 1.0 - 1.0 / 3.0)]
1289 );
1290 assert_eq!(m.target(), None);
1291 assert_eq!(
1293 frame(&mut m, &[stop_with(3000), stop_with(3000)], 1.0),
1294 [gain("title", 1.0 - 2.0 / 3.0)]
1295 );
1296 assert_eq!(frame(&mut m, &[], 1.0), [stop("title")]);
1297 assert_eq!(m.current(), None);
1298 assert_eq!(frame(&mut m, &[], 1.0), []);
1299 let mut q = AudioQueue::default();
1301 q.play_music("battle");
1302 q.stop_music();
1303 let requests = q.take();
1304 assert_eq!(requests, [play("battle"), AudioRequest::StopMusic]);
1305 assert_eq!(
1306 frame(&mut m, &requests, 0.25),
1307 [load("battle"), start("battle"), gain("battle", 0.5)]
1308 );
1309 assert_eq!(frame(&mut m, &[], 0.25), [stop("battle")]);
1310 }
1311
1312 #[test]
1313 fn a_stop_with_no_fade_stops_at_once_and_drops_the_waiting_track() {
1314 let mut m = playing("title");
1315 assert_eq!(frame(&mut m, &[stop_with(0)], 0.0), [stop("title")]);
1316 assert_eq!(m.current(), None);
1317 let mut m = playing("title");
1319 frame(&mut m, &[play("battle")], 0.25);
1320 assert_eq!(
1321 frame(&mut m, &[stop_with(0)], 0.0),
1322 [stop("battle"), stop("title")]
1323 );
1324 assert_eq!((m.current(), m.target()), (None, None));
1325 assert_eq!(frame(&mut m, &[], 1.0), []);
1326 }
1327
1328 #[test]
1329 fn a_stop_with_its_own_fade_of_silence_does_nothing() {
1330 let mut m = MusicState::new(0.5);
1331 assert_eq!(frame(&mut m, &[stop_with(3000), stop_with(0)], 1.0), []);
1332 assert_eq!(frame(&mut m, &[], 5.0), []);
1333 assert_eq!(m.current(), None);
1334 }
1335
1336 #[test]
1337 fn a_stop_with_its_own_fade_during_a_switch_fades_from_where_the_gain_is() {
1338 let mut m = playing("title");
1341 frame(&mut m, &[play("battle")], 0.25);
1342 assert_eq!(
1343 frame(&mut m, &[stop_with(2000)], 0.5),
1344 [stop("battle"), gain("title", 0.25)]
1345 );
1346 assert_eq!(frame(&mut m, &[], 0.5), [stop("title")]);
1347 assert_eq!(m.current(), None);
1348 }
1349
1350 #[test]
1351 fn a_stop_with_its_own_fade_during_a_crossfade_fades_both_tracks() {
1352 let mut m = crossfading(0.25);
1355 assert_eq!(
1356 frame(&mut m, &[stop_with(2000)], 0.5),
1357 [gain("title", 0.5), stop("battle")]
1358 );
1359 assert_eq!((m.current(), m.target()), (Some("title"), None));
1360 assert_eq!(frame(&mut m, &[], 0.5), [gain("title", 0.25)]);
1361 assert_eq!(frame(&mut m, &[], 0.5), [stop("title")]);
1362 assert_eq!(m.current(), None);
1363 }
1364
1365 #[test]
1366 fn the_default_switch_is_the_one_a_request_without_one_gets() {
1367 let mut m = MusicState::with_switch(MusicSwitch::crossfade(1000));
1368 frame(&mut m, &[play("title")], 0.0);
1369 m.heard(Some(&clock("title")));
1370 assert_eq!(frame(&mut m, &[], 1.0), [gain("title", 1.0)]);
1371 assert_eq!(
1372 frame(&mut m, &[play("battle")], 0.0),
1373 [load("battle"), start_at("battle", 0.0)]
1374 );
1375 let mut m = MusicState::with_switch(MusicSwitch::crossfade(1000));
1377 frame(&mut m, &[play("title")], 0.0);
1378 assert_eq!(
1379 frame(
1380 &mut m,
1381 &[play_with("battle", MusicSwitch::after_fade(0))],
1382 0.0
1383 ),
1384 [load("battle"), stop("title"), start("battle")]
1385 );
1386 assert_eq!(
1387 MusicState::new(0.5),
1388 MusicState::with_switch(MusicSwitch::after_fade(500))
1389 );
1390 }
1391
1392 #[test]
1393 fn sound_requests_leave_the_music_alone() {
1394 let mut m = playing("title");
1395 let sound = AudioRequest::PlaySound {
1396 cue: "click".into(),
1397 volume: 1.0,
1398 };
1399 assert_eq!(frame(&mut m, &[sound], 0.1), []);
1400 assert_eq!(m.current(), Some("title"));
1401 }
1402
1403 #[test]
1404 fn voice_requests_leave_the_music_alone() {
1405 let mut m = playing("title");
1406 let line = LineId::new("test_1a2b3c4d");
1407 let mut q = AudioQueue::default();
1408 q.play_voice(&line, Voice::High);
1409 q.stop_voice();
1410 q.preload_voices(vec![(line.clone(), Voice::Only)]);
1411 let expected = [
1412 AudioRequest::PlayVoice {
1413 line: line.clone(),
1414 variant: Voice::High,
1415 },
1416 AudioRequest::StopVoice,
1417 AudioRequest::PreloadVoices {
1418 lines: vec![(line, Voice::Only)],
1419 },
1420 ];
1421 assert_eq!(q.pending(), expected);
1422 assert!(expected.iter().all(|r| r.cue().is_none()));
1423 assert_eq!(frame(&mut m, &expected, 0.1), []);
1424 assert_eq!(m.current(), Some("title"));
1425 }
1426
1427 #[test]
1428 fn the_queue_records_requests_in_order() {
1429 let mut q = AudioQueue::default();
1430 q.play_sound("a");
1431 q.play_sound_at("b", 0.6);
1432 q.play_sound_at("c", 2.0);
1433 q.play_sound_at("d", -1.0);
1434 q.play_sound_at("e", f32::NAN);
1435 q.play_music("m");
1436 q.stop_music();
1437 q.stop_music_with(3000);
1438 let sound = |cue: &str, volume| AudioRequest::PlaySound {
1439 cue: cue.into(),
1440 volume,
1441 };
1442 let expected = [
1443 sound("a", 1.0),
1444 sound("b", 0.6),
1445 sound("c", 1.0),
1446 sound("d", 0.0),
1447 sound("e", 0.0),
1448 play("m"),
1449 AudioRequest::StopMusic,
1450 AudioRequest::stop_music_with(3000),
1451 ];
1452 assert_eq!(q.pending(), expected);
1453 assert_eq!(q.take(), expected);
1454 let found = q.pending();
1455 assert!(found.is_empty(), "{found:?}");
1456 assert_eq!(
1457 expected.map(|r| r.cue().map(str::to_owned))[4..],
1458 [Some("e".to_owned()), Some("m".to_owned()), None, None]
1459 );
1460 }
1461
1462 #[test]
1465 fn menu_sounds_and_the_cursor_play_the_configs_cues() {
1466 let cues = |q: &mut AudioQueue| {
1467 for sound in [
1468 MenuSound::Move,
1469 MenuSound::Select,
1470 MenuSound::Cancel,
1471 MenuSound::Denied,
1472 ] {
1473 q.menu(sound);
1474 }
1475 q.cursor_tick();
1476 let taken = q.take();
1477 taken
1478 .iter()
1479 .map(|r| r.cue().unwrap_or_default().to_owned())
1480 .collect::<Vec<_>>()
1481 };
1482 let mut q = AudioQueue::default();
1483 assert_eq!(q.config(), AudioConfig::default());
1484 assert_eq!(
1485 cues(&mut q),
1486 [
1487 "ui_move",
1488 "ui_select",
1489 "ui_cancel",
1490 "ui_cancel",
1491 "ui_cursor"
1492 ]
1493 );
1494 let config = AudioConfig {
1495 move_cue: "tick",
1496 select_cue: "ding",
1497 cancel_cue: "dong",
1498 denied_cue: "buzz",
1499 cursor_cue: "step",
1500 };
1501 let mut q = AudioQueue::new(config);
1502 assert_eq!(q.config(), config);
1503 assert_eq!(cues(&mut q), ["tick", "ding", "dong", "buzz", "step"]);
1504 }
1505
1506 fn manifest() -> AudioManifest {
1509 let mut m = AudioManifest::default();
1510 let tracks = ["drums", "horns", "pipes"];
1511 for cue in tracks {
1512 m.music.insert(cue.into(), track(cue, true, 10_000));
1513 }
1514 m.pools
1515 .insert("fights".into(), tracks.map(str::to_owned).to_vec());
1516 m.pools.insert("quiet".into(), Vec::new());
1517 m
1518 }
1519
1520 fn track(cue: &str, looped: bool, length_ms: u32) -> MusicCue {
1522 MusicCue {
1523 file: format!("{cue}.ogg"),
1524 volume: 100,
1525 looped,
1526 length_ms,
1527 credit: CreditRef::Own,
1528 }
1529 }
1530
1531 #[test]
1532 fn a_pool_pick_is_a_cue_of_that_pool_and_fixed_by_the_seed() {
1533 let m = manifest();
1534 let pool = &m.pools["fights"];
1535 for seed in [0, 1, 2, u64::MAX] {
1536 let cue = pick_from_pool(&m, "fights", seed).unwrap();
1537 assert!(pool.iter().any(|c| c == cue), "{cue}");
1538 assert_eq!(pick_from_pool(&m, "fights", seed), Some(cue));
1539 }
1540 let picks: alloc::collections::BTreeSet<&str> = (0..8)
1542 .filter_map(|seed| pick_from_pool(&m, "fights", seed))
1543 .collect();
1544 assert!(picks.len() > 1, "{picks:?}");
1545 }
1546
1547 #[test]
1549 fn splitmix64_matches_the_reference() {
1550 assert_eq!(splitmix64(0), 0xE220_A839_7B1D_CDAF);
1551 assert_eq!(splitmix64(0x9E37_79B9_7F4A_7C15), 0x6E78_9E6A_A1B9_65F4);
1552 }
1553
1554 #[test]
1555 fn an_unknown_or_empty_pool_picks_nothing() {
1556 let m = manifest();
1557 assert_eq!(pick_from_pool(&m, "no_such_pool", 1), None);
1558 assert_eq!(pick_from_pool(&m, "quiet", 1), None);
1559 }
1560
1561 fn one_track(looped: bool) -> AudioManifest {
1563 let mut m = AudioManifest::default();
1564 m.music.insert("theme".into(), track("theme", looped, 2500));
1565 m
1566 }
1567
1568 fn position(looped: bool, elapsed: f64) -> Option<f32> {
1570 let clock = MusicClock::from_elapsed(&one_track(looped), "theme", elapsed)?;
1571 assert_eq!(clock.cue, "theme");
1572 assert!((clock.length - 2.5).abs() < f32::EPSILON, "{clock:?}");
1573 Some(clock.position)
1574 }
1575
1576 fn close(position: Option<f32>, expected: f32) -> bool {
1577 position.is_some_and(|p| (p - expected).abs() < 1e-5)
1578 }
1579
1580 #[test]
1581 fn the_clock_of_a_looped_track_wraps_at_its_length() {
1582 assert_eq!(position(true, 0.0), Some(0.0));
1583 assert!(close(position(true, 1.25), 1.25));
1584 assert!(close(position(true, 2.499), 2.499));
1585 assert_eq!(position(true, 2.5), Some(0.0));
1586 assert!(close(position(true, 3.0), 0.5));
1587 assert!(close(position(true, 2.5 * 1000.0 + 2.0), 2.0));
1588 assert_eq!(position(true, 2.5 - 1e-12), Some(0.0));
1591 }
1592
1593 #[test]
1594 fn the_clock_of_a_track_played_once_ends_with_it() {
1595 assert_eq!(position(false, 0.0), Some(0.0));
1596 assert!(close(position(false, 2.499), 2.499));
1597 assert_eq!(position(false, 2.5 - 1e-12), None);
1598 assert_eq!(position(false, 2.5), None);
1599 assert_eq!(position(false, 3.0), None);
1600 }
1601
1602 #[test]
1603 fn the_clock_needs_a_known_cue_a_length_and_a_real_time() {
1604 let m = one_track(true);
1605 assert_eq!(MusicClock::from_elapsed(&m, "nope", 1.0), None);
1606 assert_eq!(position(true, -3.0), Some(0.0));
1608 assert_eq!(position(false, -3.0), Some(0.0));
1609 for looped in [true, false] {
1610 for elapsed in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1611 assert_eq!(position(looped, elapsed), None, "{looped} {elapsed}");
1612 }
1613 let mut m = one_track(looped);
1616 if let Some(t) = m.music.get_mut("theme") {
1617 t.length_ms = 0;
1618 }
1619 assert_eq!(MusicClock::from_elapsed(&m, "theme", 0.0), None);
1620 assert_eq!(MusicClock::from_elapsed(&m, "theme", 1.0), None);
1621 }
1622 }
1623
1624 proptest::proptest! {
1625 #[test]
1628 fn a_looped_position_is_always_inside_the_track(
1629 length_ms in 1u32..=600_000,
1630 elapsed in 0.0f64..1.0e7,
1631 ) {
1632 let mut m = one_track(true);
1633 if let Some(t) = m.music.get_mut("theme") {
1634 t.length_ms = length_ms;
1635 }
1636 let clock = MusicClock::from_elapsed(&m, "theme", elapsed);
1637 let clock = clock.unwrap();
1638 proptest::prop_assert!(
1639 (0.0..clock.length).contains(&clock.position),
1640 "{clock:?}"
1641 );
1642 }
1643 }
1644
1645 #[derive(Debug, Clone)]
1647 enum Step {
1648 Play(usize, Option<MusicSwitch>),
1649 Stop,
1650 StopWith(u32),
1651 Heard(Option<usize>),
1652 Frame(f32),
1653 }
1654
1655 fn step() -> impl proptest::strategy::Strategy<Value = Step> {
1656 use proptest::prelude::*;
1657 let switch = (0u32..1500, 0u32..1500, any::<bool>())
1658 .prop_map(|(out_ms, in_ms, overlap)| MusicSwitch::new(out_ms, in_ms, overlap));
1659 prop_oneof![
1660 (0usize..4, proptest::option::of(switch)).prop_map(|(c, s)| Step::Play(c, s)),
1661 Just(Step::Stop),
1662 (0u32..4000).prop_map(Step::StopWith),
1663 proptest::option::of(0usize..4).prop_map(Step::Heard),
1664 (0.0f32..1.0).prop_map(Step::Frame),
1665 ]
1666 }
1667
1668 proptest::proptest! {
1669 #[test]
1674 fn at_most_two_tracks_load_and_every_gain_is_a_gain(
1675 default in (0u32..1500, 0u32..1500, proptest::prelude::any::<bool>()),
1676 steps in proptest::collection::vec(step(), 0..80),
1677 ) {
1678 const CUES: [&str; 4] = ["a", "b", "c", "d"];
1679 let mut m = MusicState::with_switch(MusicSwitch::new(default.0, default.1, default.2));
1680 let mut loaded: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
1681 let mut out = Vec::new();
1682 for step in steps {
1683 match step {
1684 Step::Play(c, switch) => m.request(&AudioRequest::play_music(CUES[c], switch), &mut out),
1685 Step::Stop => m.request(&AudioRequest::StopMusic, &mut out),
1686 Step::StopWith(ms) => m.request(&AudioRequest::stop_music_with(ms), &mut out),
1687 Step::Heard(c) => m.heard(c.map(|c| clock(CUES[c])).as_ref()),
1688 Step::Frame(dt) => m.update(dt, &mut out),
1689 }
1690 for command in out.drain(..) {
1691 match &command {
1692 MusicCommand::Load { cue } => {
1693 proptest::prop_assert!(loaded.insert(cue.clone()), "{command:?}");
1694 }
1695 MusicCommand::Start { cue, gain } | MusicCommand::Gain { cue, gain } => {
1696 proptest::prop_assert!(loaded.contains(cue), "{command:?}");
1697 proptest::prop_assert!((0.0..=1.0).contains(gain), "{command:?}");
1698 }
1699 MusicCommand::Stop { cue } => {
1700 proptest::prop_assert!(loaded.remove(cue), "{command:?}");
1701 }
1702 }
1703 proptest::prop_assert!(loaded.len() <= 2, "{loaded:?}");
1704 }
1705 let current = m.current().map(str::to_owned);
1706 proptest::prop_assert!(current.is_none_or(|c| loaded.contains(&c)));
1707 }
1708 }
1709 }
1710
1711 proptest::proptest! {
1712 #[test]
1714 fn every_track_in_a_pool_can_be_picked(start in proptest::prelude::any::<u64>()) {
1715 let m = manifest();
1716 let pool = &m.pools["fights"];
1717 let picked: alloc::collections::BTreeSet<&str> = (0..200)
1718 .filter_map(|i| pick_from_pool(&m, "fights", start.wrapping_add(i)))
1719 .collect();
1720 proptest::prop_assert_eq!(picked.len(), pool.len());
1721 }
1722 }
1723}