1pub const OUTPUT_RATE: u32 = 22050;
8pub const RATE_22KHZ_FIXED: u32 = 0x56EE_8BA3;
10pub const RATE_11KHZ_FIXED: u32 = 0x2B77_45D1;
12
13const STD_Q_LENGTH: usize = 128;
15const FULL_VOLUME: u16 = 0x0100;
17const FULL_STEREO_VOLUME: u32 = ((FULL_VOLUME as u32) << 16) | FULL_VOLUME as u32;
19const UNITY_RATE_FIXED: u32 = 0x0001_0000;
21pub(crate) const DEBUG_DOUBLE_BUFFER_CAPTURE_LIMIT: usize = OUTPUT_RATE as usize * 60;
23
24pub mod cmd {
26 pub const NULL: u16 = 0;
27 pub const QUIET: u16 = 3;
28 pub const FLUSH: u16 = 4;
29 pub const CALLBACK: u16 = 13;
30 pub const AVAILABLE: u16 = 24;
31 pub const VERSION: u16 = 25;
32 pub const TOTAL_LOAD: u16 = 26;
33 pub const LOAD: u16 = 27;
34 pub const REST: u16 = 43;
41 pub const VOLUME: u16 = 46;
42 pub const SOUND: u16 = 80;
43 pub const BUFFER: u16 = 81;
44 pub const RATE: u16 = 82;
45 pub const GET_RATE: u16 = 85;
46}
47
48#[derive(Clone, Debug)]
51pub struct SndCommand {
52 pub cmd: u16,
53 pub param1: i16,
54 pub param2: u32,
55}
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub(crate) struct StereoSample {
60 pub left: u8,
61 pub right: u8,
62}
63
64impl StereoSample {
65 pub(crate) const SILENCE: Self = Self {
66 left: 0x80,
67 right: 0x80,
68 };
69
70 pub(crate) fn mono(sample: u8) -> Self {
71 Self {
72 left: sample,
73 right: sample,
74 }
75 }
76
77 pub(crate) fn downmix(self) -> u8 {
78 let left = self.left as i32 - 0x80;
79 let right = self.right as i32 - 0x80;
80 ((left + right) / 2 + 0x80).clamp(0, 255) as u8
81 }
82}
83
84#[derive(Clone, Debug)]
86struct PlayingBuffer {
87 samples: Vec<StereoSample>,
89 sample_rate_fixed: u32,
91 position: u64,
93 step: u64,
95}
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub(crate) enum PlaybackKind {
99 Buffer,
100 File,
101}
102
103#[derive(Clone, Debug)]
106pub struct DoubleBufferState {
107 pub header_ptr: u32,
109 pub current_buffer: usize,
111 pub callback_addr: u32,
113 pub chan_ptr: u32,
115 pub sample_rate: u32,
117 pub num_channels: usize,
119 pub sample_size: usize,
121 pub last_buffer_seen: bool,
123 pub waiting_for_callback: bool,
125 pub pending_callback_buffers: [bool; 2],
127}
128
129impl DoubleBufferState {
130 fn buffer_index(index: usize) -> usize {
131 index & 1
132 }
133
134 fn callback_pending_for(&self, index: usize) -> bool {
135 self.pending_callback_buffers[Self::buffer_index(index)]
136 }
137
138 fn arm_callback_for(&mut self, index: usize) -> bool {
139 let index = Self::buffer_index(index);
140 if self.pending_callback_buffers[index] {
141 return false;
142 }
143 self.pending_callback_buffers[index] = true;
144 self.waiting_for_callback = true;
145 true
146 }
147
148 pub(crate) fn complete_callback_for(&mut self, index: usize) {
149 let index = Self::buffer_index(index);
150 self.pending_callback_buffers[index] = false;
151 self.waiting_for_callback = self.pending_callback_buffers.iter().any(|pending| *pending);
152 }
153}
154
155#[derive(Clone, Debug)]
157pub struct PendingDoubleBackCallback {
158 pub callback_addr: u32,
160 pub chan_ptr: u32,
162 pub header_ptr: u32,
164 pub exhausted_buffer_index: usize,
166}
167
168#[derive(Clone, Debug)]
170pub enum PendingSoundCallback {
171 Command {
175 callback_addr: u32,
176 chan_ptr: u32,
177 cmd: SndCommand,
178 },
179 FileCompletion { callback_addr: u32, chan_ptr: u32 },
183}
184
185#[derive(Clone, Debug)]
187pub struct SndChannel {
188 pub guest_ptr: u32,
190 pub allocated: bool,
192 queue: Vec<SndCommand>,
194 q_head: usize,
195 q_tail: usize,
196 playing: Option<PlayingBuffer>,
198 playback_kind: Option<PlaybackKind>,
200 pub callback_addr: u32,
202 volume: u32,
204 rate_fixed: u32,
206 pending_callback_cmds: Vec<SndCommand>,
208 file_completion_addr: u32,
210 file_paused: bool,
212 pub double_buffer: Option<DoubleBufferState>,
214 pub debug_double_buffer_loads: u32,
216 pub debug_double_buffer_non_silent_loads: u32,
219 pub debug_double_buffer_frames_loaded: u64,
221 pub debug_double_buffer_non_silent_frames: u64,
223 pub debug_double_buffer_captured_samples: Vec<u8>,
227 auto_dispose_when_idle: bool,
231}
232
233impl SndChannel {
234 pub(crate) fn set_file_paused(&mut self, paused: bool) {
235 self.file_paused = paused;
236 }
237 pub fn new(guest_ptr: u32, allocated: bool) -> Self {
238 Self {
239 guest_ptr,
240 allocated,
241 queue: Vec::with_capacity(STD_Q_LENGTH),
242 q_head: 0,
243 q_tail: 0,
244 playing: None,
245 playback_kind: None,
246 callback_addr: 0,
247 volume: FULL_STEREO_VOLUME,
248 rate_fixed: UNITY_RATE_FIXED,
249 pending_callback_cmds: Vec::new(),
250 file_completion_addr: 0,
251 file_paused: false,
252 double_buffer: None,
253 debug_double_buffer_loads: 0,
254 debug_double_buffer_non_silent_loads: 0,
255 debug_double_buffer_frames_loaded: 0,
256 debug_double_buffer_non_silent_frames: 0,
257 debug_double_buffer_captured_samples: Vec::new(),
258 auto_dispose_when_idle: false,
259 }
260 }
261
262 pub fn enqueue(&mut self, cmd: SndCommand) -> bool {
264 if self.queue.len() < STD_Q_LENGTH {
265 self.queue.push(cmd);
266 true
267 } else {
268 false
269 }
270 }
271
272 fn dequeue(&mut self) -> Option<SndCommand> {
274 if self.queue.is_empty() {
275 None
276 } else {
277 Some(self.queue.remove(0))
278 }
279 }
280
281 pub fn flush(&mut self) {
283 self.queue.clear();
284 self.q_head = 0;
285 self.q_tail = 0;
286 }
287
288 pub fn quiet(&mut self) {
290 self.playing = None;
291 self.playback_kind = None;
292 self.pending_callback_cmds.clear();
293 self.file_completion_addr = 0;
294 self.file_paused = false;
295 self.rate_fixed = UNITY_RATE_FIXED;
296 self.double_buffer = None;
297 }
298
299 pub(crate) fn play_buffer(
301 &mut self,
302 samples: Vec<u8>,
303 sample_rate_fixed: u32,
304 kind: PlaybackKind,
305 file_completion_addr: u32,
306 ) {
307 let samples = samples.into_iter().map(StereoSample::mono).collect();
308 self.play_stereo_buffer(samples, sample_rate_fixed, kind, file_completion_addr);
309 }
310
311 pub(crate) fn play_stereo_buffer(
313 &mut self,
314 samples: Vec<StereoSample>,
315 sample_rate_fixed: u32,
316 kind: PlaybackKind,
317 file_completion_addr: u32,
318 ) {
319 self.rate_fixed = UNITY_RATE_FIXED;
320 self.playing = Some(PlayingBuffer {
321 samples,
322 sample_rate_fixed,
323 position: 0,
324 step: fixed_div(sample_rate_fixed as u64, (OUTPUT_RATE as u64) << 16),
325 });
326 self.playback_kind = Some(kind);
327 self.file_completion_addr = file_completion_addr;
328 self.file_paused = false;
329 }
330
331 pub fn is_playing(&self) -> bool {
333 self.playing.is_some()
334 }
335
336 pub fn has_active_playback(&self) -> bool {
337 self.playing.is_some() || self.file_paused
338 }
339
340 pub fn queue_callback(&mut self, cmd: SndCommand) {
341 self.pending_callback_cmds.push(cmd);
342 }
343
344 pub fn take_pending_callback_cmds(&mut self) -> Vec<SndCommand> {
345 std::mem::take(&mut self.pending_callback_cmds)
346 }
347
348 pub fn set_volume(&mut self, packed_volume: u32) {
349 self.volume = packed_volume;
350 }
351
352 pub fn set_rate(&mut self, rate_fixed: u32) {
353 self.rate_fixed = rate_fixed;
354 if let Some(ref mut playing) = self.playing {
355 playing.step = playback_step(playing.sample_rate_fixed, rate_fixed);
356 }
357 }
358
359 pub fn current_rate(&self) -> u32 {
360 self.rate_fixed
361 }
362
363 pub fn pause_file_playback_toggle(&mut self) {
364 if self.playback_kind == Some(PlaybackKind::File) {
365 self.file_paused = !self.file_paused;
366 }
367 }
368
369 pub(crate) fn mark_auto_dispose_when_idle(&mut self) {
370 self.auto_dispose_when_idle = true;
371 }
372
373 fn is_ready_for_auto_dispose(&self) -> bool {
374 self.auto_dispose_when_idle
375 && self.playing.is_none()
376 && !self.file_paused
377 && self.double_buffer.is_none()
378 && self.queue.is_empty()
379 && self.pending_callback_cmds.is_empty()
380 }
381}
382
383#[derive(Clone, Debug)]
385pub struct SoundManager {
386 pub channels: Vec<SndChannel>,
387 pub pending_callbacks: Vec<PendingDoubleBackCallback>,
389 pub pending_sound_callbacks: Vec<PendingSoundCallback>,
391 pub debug_cmd_count: u32,
393 pub debug_buffer_cmd_count: u32,
394 pub debug_double_buffer_count: u32,
399 pub debug_samples_mixed: u64,
400 pub debug_unhandled_cmds: Vec<u16>,
401 pub debug_cmd_codes_seen: Vec<u16>,
406 pub debug_file_play_count: u32,
416 sys_beep_volume: u32,
418 default_output_volume: u32,
423}
424
425impl Default for SoundManager {
426 fn default() -> Self {
427 Self::new()
428 }
429}
430
431impl SoundManager {
432 pub fn new() -> Self {
433 Self {
434 channels: Vec::new(),
435 pending_callbacks: Vec::new(),
436 pending_sound_callbacks: Vec::new(),
437 debug_cmd_count: 0,
438 debug_buffer_cmd_count: 0,
439 debug_double_buffer_count: 0,
440 debug_samples_mixed: 0,
441 debug_unhandled_cmds: Vec::new(),
442 debug_cmd_codes_seen: Vec::new(),
443 debug_file_play_count: 0,
444 sys_beep_volume: FULL_STEREO_VOLUME,
445 default_output_volume: FULL_STEREO_VOLUME,
446 }
447 }
448
449 pub fn sys_beep_volume(&self) -> u32 {
450 self.sys_beep_volume
451 }
452
453 pub fn set_sys_beep_volume(&mut self, volume: u32) {
454 self.sys_beep_volume = volume;
455 }
456
457 pub fn default_output_volume(&self) -> u32 {
458 self.default_output_volume
459 }
460
461 pub fn set_default_output_volume(&mut self, volume: u32) {
462 self.default_output_volume = volume;
463 }
464
465 pub fn find_channel_mut(&mut self, guest_ptr: u32) -> Option<&mut SndChannel> {
467 self.channels.iter_mut().find(|c| c.guest_ptr == guest_ptr)
468 }
469
470 pub fn take_channel(&mut self, guest_ptr: u32) -> Option<SndChannel> {
472 self.channels
473 .iter()
474 .position(|c| c.guest_ptr == guest_ptr)
475 .map(|idx| self.channels.remove(idx))
476 }
477
478 pub fn remove_channel(&mut self, guest_ptr: u32) -> bool {
480 self.take_channel(guest_ptr).is_some()
481 }
482
483 pub(crate) fn idle_auto_dispose_channel_ptrs(&self) -> Vec<u32> {
484 self.channels
485 .iter()
486 .filter(|chan| chan.is_ready_for_auto_dispose())
487 .map(|chan| chan.guest_ptr)
488 .collect()
489 }
490
491 pub fn mix_frame(&mut self, num_samples: usize) -> Vec<u8> {
494 self.mix_frame_stereo_frames(num_samples)
495 .into_iter()
496 .map(StereoSample::downmix)
497 .collect()
498 }
499
500 pub fn mix_frame_stereo(&mut self, num_samples: usize) -> Vec<u8> {
504 let frames = self.mix_frame_stereo_frames(num_samples);
505 let mut out = Vec::with_capacity(frames.len() * 2);
506 for frame in frames {
507 out.push(frame.left);
508 out.push(frame.right);
509 }
510 out
511 }
512
513 fn mix_frame_stereo_frames(&mut self, num_samples: usize) -> Vec<StereoSample> {
514 let mut queued_callbacks = Vec::new();
518 for chan in &mut self.channels {
519 if chan.has_active_playback() || chan.double_buffer.is_some() {
520 continue;
521 }
522
523 while let Some(cmd) = chan.dequeue() {
524 match cmd.cmd {
525 cmd::NULL => {}
526 cmd::QUIET => chan.quiet(),
527 cmd::FLUSH => chan.flush(),
528 cmd::CALLBACK => {
529 if chan.callback_addr != 0 {
530 queued_callbacks.push(PendingSoundCallback::Command {
531 callback_addr: chan.callback_addr,
532 chan_ptr: chan.guest_ptr,
533 cmd,
534 });
535 }
536 }
537 cmd::BUFFER | cmd::SOUND => {
538 }
540 _ => {}
541 }
542 }
543 }
544 self.pending_sound_callbacks.extend(queued_callbacks);
545
546 let mut output = vec![StereoSample::SILENCE; num_samples];
548 let mut any_active = false;
549
550 let mut exhausted: Vec<(u32, u32, u32, usize)> = Vec::new(); for chan in &mut self.channels {
554 if chan.playing.is_none() {
559 let mut clear_double_buffer = false;
560 if let Some(ref mut db) = chan.double_buffer {
561 if db.last_buffer_seen {
562 clear_double_buffer = true;
563 } else {
564 any_active = true;
565 if !db.callback_pending_for(db.current_buffer) {
566 db.arm_callback_for(db.current_buffer);
567 exhausted.push((
568 db.callback_addr,
569 db.chan_ptr,
570 db.header_ptr,
571 db.current_buffer,
572 ));
573 }
574 }
575 }
576 if clear_double_buffer {
577 chan.double_buffer = None;
578 }
579 }
580 if chan.file_paused {
581 any_active = true;
582 continue;
583 }
584
585 if let Some(ref mut buf) = chan.playing {
586 any_active = true;
587 for slot in output.iter_mut().take(num_samples) {
588 let Some(source_sample) =
589 resampled_sample(&buf.samples, buf.position, buf.step)
590 else {
591 break;
592 };
593 let sample = apply_volume_stereo(source_sample, chan.volume);
594 let mixed_left = slot.left as i16 + sample.left as i16 - 0x80;
595 let mixed_right = slot.right as i16 + sample.right as i16 - 0x80;
596 slot.left = mixed_left.clamp(0, 255) as u8;
597 slot.right = mixed_right.clamp(0, 255) as u8;
598 buf.position += buf.step;
599 }
600 let final_idx = (buf.position >> 32) as usize;
601 if final_idx >= buf.samples.len() {
602 let playback_kind = chan.playback_kind;
603 let callback_addr = chan.callback_addr;
604 let chan_ptr = chan.guest_ptr;
605 let file_completion_addr = chan.file_completion_addr;
606 let callback_cmds = chan.take_pending_callback_cmds();
607 chan.playing = None;
608 chan.playback_kind = None;
609 chan.file_completion_addr = 0;
610 if let Some(ref mut db) = chan.double_buffer {
613 if !db.last_buffer_seen {
614 let exhausted_idx = db.current_buffer;
615 db.current_buffer ^= 1; if db.arm_callback_for(exhausted_idx) {
617 exhausted.push((
618 db.callback_addr,
619 db.chan_ptr,
620 db.header_ptr,
621 exhausted_idx,
622 ));
623 }
624 }
625 }
626 if callback_addr != 0 {
627 for cmd in callback_cmds {
628 self.pending_sound_callbacks
629 .push(PendingSoundCallback::Command {
630 callback_addr,
631 chan_ptr,
632 cmd,
633 });
634 }
635 }
636 if playback_kind == Some(PlaybackKind::File) && file_completion_addr != 0 {
637 self.pending_sound_callbacks
638 .push(PendingSoundCallback::FileCompletion {
639 callback_addr: file_completion_addr,
640 chan_ptr,
641 });
642 }
643 }
644 }
645 }
646
647 for (callback_addr, chan_ptr, header_ptr, exhausted_buf_idx) in exhausted {
649 self.pending_callbacks.push(PendingDoubleBackCallback {
652 callback_addr,
653 chan_ptr,
654 header_ptr,
655 exhausted_buffer_index: exhausted_buf_idx,
656 });
657 }
658
659 if any_active {
660 self.debug_samples_mixed += output.len() as u64;
661 output
662 } else {
663 Vec::new()
664 }
665 }
666
667 pub fn samples_until_next_exhaustion(&self) -> Option<usize> {
672 self.channels
673 .iter()
674 .filter_map(|chan| {
675 let playing = chan.playing.as_ref()?;
676 if playing.step == 0 {
677 return None;
678 }
679 let end = (playing.samples.len() as u128) << 32;
680 let position = playing.position as u128;
681 if position >= end {
682 return Some(0);
683 }
684 let step = playing.step as u128;
685 let samples = (end - position).div_ceil(step);
686 Some(samples.min(usize::MAX as u128) as usize)
687 })
688 .min()
689 }
690}
691
692fn fixed_div(x: u64, y: u64) -> u64 {
695 if y == 0 {
696 return 0;
697 }
698 let int_part = x / y;
699 let remainder = x - y * int_part;
700 let frac_part = (remainder << 32) / y;
701 (int_part << 32) + frac_part
702}
703
704fn playback_step(sample_rate_fixed: u32, rate_fixed: u32) -> u64 {
705 let base = fixed_div(sample_rate_fixed as u64, (OUTPUT_RATE as u64) << 16);
706 ((base as u128 * rate_fixed as u128) >> 16) as u64
707}
708
709fn resampled_sample(samples: &[StereoSample], position: u64, step: u64) -> Option<StereoSample> {
710 if step < (1u64 << 32) {
714 let sample_idx = (position >> 32) as usize;
715 return samples.get(sample_idx).copied();
716 }
717 interpolated_sample(samples, position)
718}
719
720fn interpolated_sample(samples: &[StereoSample], position: u64) -> Option<StereoSample> {
721 let sample_idx = (position >> 32) as usize;
722 let first = *samples.get(sample_idx)?;
723 let second = samples.get(sample_idx + 1).copied().unwrap_or(first);
724 let frac = (position & 0xFFFF_FFFF) as i64;
725 Some(StereoSample {
726 left: interpolate_u8(first.left, second.left, frac),
727 right: interpolate_u8(first.right, second.right, frac),
728 })
729}
730
731fn interpolate_u8(first: u8, second: u8, frac: i64) -> u8 {
732 let delta = second as i64 - first as i64;
733 let interpolated = first as i64 + (delta * frac) / (1i64 << 32);
734 interpolated.clamp(0, 255) as u8
735}
736
737#[cfg(test)]
738fn apply_volume(sample: u8, packed_volume: u32) -> u8 {
739 let left = (packed_volume & 0xFFFF) as i32;
740 let right = ((packed_volume >> 16) & 0xFFFF) as i32;
741 let average = (left + right) / 2;
742 apply_volume_channel(sample, average)
743}
744
745fn apply_volume_stereo(sample: StereoSample, packed_volume: u32) -> StereoSample {
746 let left_volume = (packed_volume & 0xFFFF) as i32;
747 let right_volume = ((packed_volume >> 16) & 0xFFFF) as i32;
748 StereoSample {
749 left: apply_volume_channel(sample.left, left_volume),
750 right: apply_volume_channel(sample.right, right_volume),
751 }
752}
753
754fn apply_volume_channel(sample: u8, volume: i32) -> u8 {
755 let centered = sample as i32 - 0x80;
756 let scaled = centered * volume / FULL_VOLUME as i32;
757 (scaled + 0x80).clamp(0, 255) as u8
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763
764 #[test]
774 fn sound_manager_new_zero_initialized() {
775 let sm = SoundManager::new();
776 assert!(sm.channels.is_empty(), "channels must start empty");
777 assert!(
778 sm.pending_callbacks.is_empty(),
779 "pending_callbacks must start empty"
780 );
781 assert!(
782 sm.pending_sound_callbacks.is_empty(),
783 "pending_sound_callbacks must start empty"
784 );
785 assert_eq!(sm.debug_cmd_count, 0, "debug_cmd_count must start at 0");
786 assert_eq!(
787 sm.debug_buffer_cmd_count, 0,
788 "debug_buffer_cmd_count must start at 0"
789 );
790 assert_eq!(
791 sm.debug_double_buffer_count, 0,
792 "debug_double_buffer_count must start at 0"
793 );
794 assert_eq!(
795 sm.debug_samples_mixed, 0,
796 "debug_samples_mixed must start at 0"
797 );
798 assert!(
799 sm.debug_unhandled_cmds.is_empty(),
800 "debug_unhandled_cmds must start empty"
801 );
802 assert!(
803 sm.debug_cmd_codes_seen.is_empty(),
804 "debug_cmd_codes_seen must start empty"
805 );
806 assert_eq!(
807 sm.debug_file_play_count, 0,
808 "debug_file_play_count must start at 0"
809 );
810 assert_eq!(
811 sm.sys_beep_volume(),
812 FULL_STEREO_VOLUME,
813 "system beep volume starts at full L+R"
814 );
815 assert_eq!(
816 sm.default_output_volume(),
817 FULL_STEREO_VOLUME,
818 "default output volume starts at full L+R"
819 );
820 }
821
822 #[test]
834 fn sound_module_constants_match_mac_sound_manager() {
835 assert_eq!(OUTPUT_RATE, 22050, "OUTPUT_RATE = 22050 Hz");
836 assert_eq!(
837 FULL_VOLUME, 0x0100,
838 "FULL_VOLUME = 256 (8-bit volume range)"
839 );
840 assert_eq!(
841 UNITY_RATE_FIXED, 0x0001_0000,
842 "UNITY_RATE_FIXED = 1.0 as 16.16 Fixed"
843 );
844 assert_eq!(STD_Q_LENGTH, 128, "STD_Q_LENGTH = 128 queue slots");
845 }
846
847 #[test]
858 fn sound_cmd_constants_match_ism_sound_1994() {
859 assert_eq!(cmd::NULL, 0, "nullCmd per IM:Sound 2-126");
860 assert_eq!(cmd::QUIET, 3, "quietCmd per IM:Sound 2-126");
861 assert_eq!(cmd::FLUSH, 4, "flushCmd per IM:Sound 2-126");
862 assert_eq!(cmd::CALLBACK, 13, "callBackCmd per IM:Sound 2-126");
863 assert_eq!(cmd::AVAILABLE, 24, "availableCmd per IM:Sound 2-92");
864 assert_eq!(cmd::VERSION, 25, "versionCmd per IM:Sound 2-92");
865 assert_eq!(cmd::TOTAL_LOAD, 26, "totalLoadCmd per IM:Sound 2-92");
866 assert_eq!(cmd::LOAD, 27, "loadCmd per IM:Sound 2-92");
867 assert_eq!(cmd::REST, 43, "restCmd per IM:Sound 2-95");
868 assert_eq!(cmd::VOLUME, 46, "volumeCmd per IM:Sound 2-126");
869 assert_eq!(cmd::SOUND, 80, "soundCmd per IM:Sound 2-126");
870 assert_eq!(cmd::BUFFER, 81, "bufferCmd per IM:Sound 2-126");
871 assert_eq!(cmd::RATE, 82, "rateCmd per IM:Sound 2-126");
872 assert_eq!(cmd::GET_RATE, 85, "getRateCmd per IM:Sound 2-126");
873 }
874
875 #[test]
880 fn mix_frame_defers_queued_quiet_until_playback_finishes() {
881 let mut sm = SoundManager::new();
882 let mut chan = SndChannel::new(0x1234_0000, true);
883 chan.play_buffer(vec![0x80; 128], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
884 assert!(chan.is_playing(), "channel active pre-queue");
885
886 chan.enqueue(SndCommand {
887 cmd: cmd::QUIET,
888 param1: 0,
889 param2: 0,
890 });
891 sm.channels.push(chan);
892
893 let output = sm.mix_frame(64);
894 assert_eq!(
895 output.len(),
896 64,
897 "active buffer must mix before queued QUIET"
898 );
899 assert_eq!(sm.debug_samples_mixed, 64);
900 assert_eq!(sm.channels[0].queue.len(), 1);
901 assert!(sm.channels[0].is_playing());
902
903 sm.mix_frame(64);
904 assert_eq!(
905 sm.channels[0].queue.len(),
906 1,
907 "command remains queued until the next idle drain"
908 );
909 assert!(!sm.channels[0].is_playing());
910
911 let output = sm.mix_frame(64);
912 assert!(
913 output.is_empty(),
914 "idle queued QUIET drains with no playback"
915 );
916 assert!(sm.channels[0].queue.is_empty());
917 }
918
919 #[test]
924 fn mix_frame_advances_samples_mixed_for_active_channel() {
925 let mut sm = SoundManager::new();
926 let mut chan = SndChannel::new(0x1234_0000, true);
927 chan.play_buffer(vec![0x80; 128], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
929 sm.channels.push(chan);
930
931 let pre = sm.debug_samples_mixed;
932 let output = sm.mix_frame(64);
933 assert_eq!(
934 output.len(),
935 64,
936 "mix_frame(64) with active channel must produce 64 bytes"
937 );
938 assert_eq!(
939 sm.debug_samples_mixed,
940 pre + 64,
941 "debug_samples_mixed must advance by output.len()"
942 );
943 }
944
945 #[test]
953 fn mix_frame_returns_empty_when_no_active_channels() {
954 let mut sm = SoundManager::new();
955 let output = sm.mix_frame(256);
956 assert!(
957 output.is_empty(),
958 "mix_frame with no channels must return empty Vec (got len {})",
959 output.len()
960 );
961 assert_eq!(sm.debug_samples_mixed, 0);
962
963 sm.channels.push(SndChannel::new(0x1234_0000, true));
965 let output = sm.mix_frame(256);
966 assert!(
967 output.is_empty(),
968 "mix_frame with idle channels must return empty Vec (got len {})",
969 output.len()
970 );
971 assert_eq!(sm.debug_samples_mixed, 0);
972 }
973
974 #[test]
981 fn play_buffer_installs_playing_and_resets_state() {
982 let mut chan = SndChannel::new(0x1234_0000, true);
983 chan.rate_fixed = 0x0000_4000; chan.file_paused = true; let samples = vec![0x10, 0x20, 0x30, 0x40, 0x50, 0x60];
987 let sample_rate = 11025 << 16; chan.play_buffer(
989 samples.clone(),
990 sample_rate,
991 PlaybackKind::File,
992 0xABCD_1234,
993 );
994
995 assert_eq!(
996 chan.rate_fixed, UNITY_RATE_FIXED,
997 "rate_fixed reset to unity"
998 );
999 assert!(!chan.file_paused, "file_paused cleared");
1000 assert_eq!(chan.playback_kind, Some(PlaybackKind::File));
1001 assert_eq!(chan.file_completion_addr, 0xABCD_1234);
1002 let playing = chan.playing.as_ref().expect("playing installed");
1003 assert_eq!(
1004 playing.samples,
1005 samples
1006 .iter()
1007 .copied()
1008 .map(StereoSample::mono)
1009 .collect::<Vec<_>>()
1010 );
1011 assert_eq!(playing.sample_rate_fixed, sample_rate);
1012 assert_eq!(playing.position, 0, "position starts at 0");
1013 assert_eq!(
1015 playing.step, 0x8000_0000,
1016 "step must be fixed_div(sample_rate, OUTPUT_RATE<<16)"
1017 );
1018 }
1019
1020 #[test]
1034 fn playback_step_at_unity_matches_sample_rate_ratio() {
1035 assert_eq!(
1039 playback_step(OUTPUT_RATE << 16, UNITY_RATE_FIXED),
1040 0x1_0000_0000,
1041 "22050 Hz source + unity rate = step 1.0"
1042 );
1043
1044 assert_eq!(
1047 playback_step((2 * OUTPUT_RATE) << 16, UNITY_RATE_FIXED),
1048 0x2_0000_0000,
1049 "44100 Hz source + unity rate = step 2.0"
1050 );
1051
1052 let half_rate = UNITY_RATE_FIXED / 2;
1055 assert_eq!(
1056 playback_step(OUTPUT_RATE << 16, half_rate),
1057 0x8000_0000,
1058 "22050 Hz source + 0.5x rate = step 0.5"
1059 );
1060 }
1061
1062 #[test]
1070 fn fixed_div_contract() {
1071 assert_eq!(fixed_div(2, 1), 0x2_0000_0000);
1073 assert_eq!(fixed_div(1, 2), 0x8000_0000);
1075 assert_eq!(fixed_div(3, 4), 0xC000_0000);
1077 assert_eq!(fixed_div(5, 2), 0x2_8000_0000);
1079 assert_eq!(fixed_div(1, 0), 0);
1081 assert_eq!(fixed_div(0, 0), 0);
1082 assert_eq!(fixed_div(0, 5), 0);
1084 }
1085
1086 #[test]
1098 fn apply_volume_scales_around_0x80_center() {
1099 let full_lr = ((FULL_VOLUME as u32) << 16) | FULL_VOLUME as u32;
1101 assert_eq!(apply_volume(0x80, full_lr), 0x80, "silence stays silent");
1102 assert_eq!(apply_volume(0xFF, full_lr), 0xFF, "max positive stays max");
1103 assert_eq!(apply_volume(0x00, full_lr), 0x00, "max negative stays max");
1104
1105 let half = ((FULL_VOLUME as u32 / 2) << 16) | (FULL_VOLUME as u32 / 2);
1107 assert_eq!(
1108 apply_volume(0x80, half),
1109 0x80,
1110 "silence at any volume stays silent"
1111 );
1112 assert_eq!(apply_volume(0xC0, half), 0xA0);
1114 assert_eq!(apply_volume(0x40, half), 0x60);
1116
1117 assert_eq!(apply_volume(0xFF, 0), 0x80);
1119 assert_eq!(apply_volume(0x00, 0), 0x80);
1120 }
1121
1122 #[test]
1128 fn set_volume_stores_packed_lr() {
1129 let mut chan = SndChannel::new(0x1234_0000, true);
1130 let full = ((FULL_VOLUME as u32) << 16) | FULL_VOLUME as u32;
1132 assert_eq!(chan.volume, full, "default volume must be FULL L+R");
1133
1134 let packed = 0x00C0_0040u32;
1136 chan.set_volume(packed);
1137 assert_eq!(chan.volume, packed, "set_volume must store the exact u32");
1138
1139 chan.set_volume(0);
1141 assert_eq!(chan.volume, 0, "set_volume must replace, not merge");
1142 }
1143
1144 #[test]
1151 fn queue_callback_and_take_drain_semantics() {
1152 let mut chan = SndChannel::new(0x1234_0000, true);
1153 assert!(chan.pending_callback_cmds.is_empty());
1154
1155 chan.queue_callback(SndCommand {
1156 cmd: 11,
1157 param1: 1,
1158 param2: 0,
1159 });
1160 chan.queue_callback(SndCommand {
1161 cmd: 11,
1162 param1: 2,
1163 param2: 0,
1164 });
1165 assert_eq!(
1166 chan.pending_callback_cmds.len(),
1167 2,
1168 "queue_callback must append (not replace)"
1169 );
1170 assert_eq!(chan.pending_callback_cmds[0].param1, 1);
1171 assert_eq!(chan.pending_callback_cmds[1].param1, 2);
1172
1173 let drained = chan.take_pending_callback_cmds();
1174 assert_eq!(drained.len(), 2);
1175 assert_eq!(drained[0].param1, 1);
1176 assert_eq!(drained[1].param1, 2);
1177 assert!(
1178 chan.pending_callback_cmds.is_empty(),
1179 "take must drain the Vec (mem::take semantics)"
1180 );
1181
1182 let second_drain = chan.take_pending_callback_cmds();
1184 assert!(second_drain.is_empty());
1185 }
1186
1187 #[test]
1194 fn set_rate_updates_step_on_active_playing() {
1195 let mut chan = SndChannel::new(0x1234_0000, true);
1196 chan.playing = Some(PlayingBuffer {
1198 samples: vec![StereoSample::SILENCE; 64],
1199 sample_rate_fixed: 22050 << 16,
1200 position: 0,
1201 step: fixed_div(22050 << 16, (OUTPUT_RATE as u64) << 16),
1202 });
1203 let original_step = chan.playing.as_ref().unwrap().step;
1204
1205 chan.set_rate(UNITY_RATE_FIXED);
1207 assert_eq!(chan.rate_fixed, UNITY_RATE_FIXED);
1208 let new_step = chan.playing.as_ref().unwrap().step;
1209 assert_eq!(
1210 new_step, original_step,
1211 "set_rate(UNITY) must recompute step to original for unity rate"
1212 );
1213
1214 let half_rate = UNITY_RATE_FIXED / 2;
1216 chan.set_rate(half_rate);
1217 assert_eq!(chan.rate_fixed, half_rate);
1218 let halved_step = chan.playing.as_ref().unwrap().step;
1219 assert!(
1220 halved_step < original_step,
1221 "set_rate(half) must reduce step; got {:#x} (orig {:#x})",
1222 halved_step,
1223 original_step
1224 );
1225 }
1226
1227 #[test]
1234 fn set_rate_stores_rate_without_active_playing() {
1235 let mut chan = SndChannel::new(0x1234_0000, true);
1236 assert!(chan.playing.is_none());
1237
1238 let target_rate = 0x0000_C000;
1239 chan.set_rate(target_rate);
1240
1241 assert_eq!(
1242 chan.rate_fixed, target_rate,
1243 "set_rate must store rate_fixed even when no buffer is playing"
1244 );
1245 assert_eq!(
1246 chan.current_rate(),
1247 target_rate,
1248 "current_rate() must reflect the stored rate_fixed"
1249 );
1250 }
1251
1252 #[test]
1258 fn pause_file_playback_toggle_gated_on_playback_kind() {
1259 let mut chan = SndChannel::new(0x1234_0000, true);
1260
1261 assert!(!chan.file_paused);
1263 chan.pause_file_playback_toggle();
1264 assert!(
1265 !chan.file_paused,
1266 "toggle on non-file channel must not flip file_paused"
1267 );
1268
1269 chan.playback_kind = Some(PlaybackKind::Buffer);
1271 chan.pause_file_playback_toggle();
1272 assert!(
1273 !chan.file_paused,
1274 "toggle on Buffer-kind channel must not flip file_paused"
1275 );
1276
1277 chan.playback_kind = Some(PlaybackKind::File);
1279 chan.pause_file_playback_toggle();
1280 assert!(
1281 chan.file_paused,
1282 "toggle on File-kind channel must flip file_paused (first call)"
1283 );
1284 chan.pause_file_playback_toggle();
1285 assert!(
1286 !chan.file_paused,
1287 "toggle on File-kind channel must flip file_paused (second call)"
1288 );
1289 }
1290
1291 #[test]
1303 fn is_playing_vs_has_active_playback_distinction() {
1304 let mut chan = SndChannel::new(0x1234_0000, true);
1305 assert!(!chan.is_playing(), "fresh channel: is_playing = false");
1307 assert!(
1308 !chan.has_active_playback(),
1309 "fresh channel: has_active_playback = false"
1310 );
1311
1312 chan.playing = Some(PlayingBuffer {
1314 samples: vec![StereoSample::SILENCE; 8],
1315 sample_rate_fixed: 22050 << 16,
1316 position: 0,
1317 step: 0x0001_0000,
1318 });
1319 assert!(chan.is_playing(), "active buffer: is_playing = true");
1320 assert!(
1321 chan.has_active_playback(),
1322 "active buffer: has_active_playback = true"
1323 );
1324
1325 chan.file_paused = true;
1328 assert!(chan.is_playing(), "playing+paused: is_playing = true");
1329 assert!(
1330 chan.has_active_playback(),
1331 "playing+paused: has_active_playback = true"
1332 );
1333
1334 chan.playing = None;
1336 assert!(
1337 !chan.is_playing(),
1338 "paused without buffer: is_playing = false"
1339 );
1340 assert!(
1341 chan.has_active_playback(),
1342 "paused without buffer: has_active_playback = true \
1343 (pause state alone keeps channel alive for resume)"
1344 );
1345 }
1346
1347 #[test]
1355 fn flush_clears_queue_only_not_playback_state() {
1356 let mut chan = SndChannel::new(0x1234_0000, true);
1357
1358 chan.enqueue(SndCommand {
1361 cmd: 81,
1362 param1: 0,
1363 param2: 0,
1364 });
1365 chan.q_head = 7;
1366 chan.q_tail = 11;
1367 chan.playing = Some(PlayingBuffer {
1368 samples: vec![StereoSample::SILENCE; 16],
1369 sample_rate_fixed: 22050 << 16,
1370 position: 0,
1371 step: 0x0001_0000,
1372 });
1373 chan.playback_kind = Some(PlaybackKind::Buffer);
1374 chan.file_completion_addr = 0xABCD_1234;
1375 chan.file_paused = true;
1376 chan.rate_fixed = 0x0000_8000;
1377 chan.pending_callback_cmds.push(SndCommand {
1378 cmd: 11,
1379 param1: 0,
1380 param2: 0,
1381 });
1382
1383 chan.flush();
1384
1385 assert!(chan.queue.is_empty(), "flush must clear the queue");
1386 assert_eq!(chan.q_head, 0, "flush must reset q_head");
1387 assert_eq!(chan.q_tail, 0, "flush must reset q_tail");
1388
1389 assert!(chan.playing.is_some(), "flush must NOT clear playing");
1391 assert!(
1392 chan.playback_kind.is_some(),
1393 "flush must NOT clear playback_kind"
1394 );
1395 assert_eq!(
1396 chan.file_completion_addr, 0xABCD_1234,
1397 "flush must NOT clear file_completion_addr"
1398 );
1399 assert!(chan.file_paused, "flush must NOT clear file_paused");
1400 assert_eq!(
1401 chan.rate_fixed, 0x0000_8000,
1402 "flush must NOT reset rate_fixed"
1403 );
1404 assert_eq!(
1405 chan.pending_callback_cmds.len(),
1406 1,
1407 "flush must NOT clear pending_callback_cmds"
1408 );
1409 }
1410
1411 #[test]
1424 fn quiet_clears_all_playback_state() {
1425 let mut chan = SndChannel::new(0x1234_0000, true);
1426
1427 chan.enqueue(SndCommand {
1430 cmd: 81,
1431 param1: 0,
1432 param2: 0,
1433 });
1434 chan.playing = Some(PlayingBuffer {
1435 samples: vec![StereoSample::SILENCE; 16],
1436 sample_rate_fixed: 22050 << 16,
1437 position: 0,
1438 step: 0x0001_0000,
1439 });
1440 chan.playback_kind = Some(PlaybackKind::File);
1441 chan.file_completion_addr = 0xABCD_1234;
1442 chan.file_paused = true;
1443 chan.rate_fixed = 0x0000_8000; chan.pending_callback_cmds.push(SndCommand {
1445 cmd: 11, param1: 0,
1447 param2: 0,
1448 });
1449 chan.quiet();
1456
1457 assert_eq!(chan.queue.len(), 1, "quiet must not flush queued commands");
1458 assert_eq!(chan.q_head, 0);
1459 assert_eq!(chan.q_tail, 0);
1460 assert!(chan.playing.is_none(), "quiet must clear playing");
1461 assert!(
1462 chan.playback_kind.is_none(),
1463 "quiet must clear playback_kind"
1464 );
1465 assert!(
1466 chan.pending_callback_cmds.is_empty(),
1467 "quiet must clear pending_callback_cmds"
1468 );
1469 assert_eq!(chan.file_completion_addr, 0);
1470 assert!(!chan.file_paused, "quiet must clear file_paused");
1471 assert_eq!(
1472 chan.rate_fixed, UNITY_RATE_FIXED,
1473 "quiet must reset rate_fixed to unity"
1474 );
1475 assert!(
1476 chan.double_buffer.is_none(),
1477 "quiet must clear double_buffer"
1478 );
1479 }
1480
1481 #[test]
1487 fn enqueue_returns_false_when_queue_full() {
1488 let mut chan = SndChannel::new(0x1234_0000, true);
1489 for i in 0..STD_Q_LENGTH {
1491 assert!(
1492 chan.enqueue(SndCommand {
1493 cmd: 3, param1: i as i16,
1495 param2: 0,
1496 }),
1497 "enqueue at slot {} must succeed while queue has space",
1498 i
1499 );
1500 }
1501 assert!(
1503 !chan.enqueue(SndCommand {
1504 cmd: 3,
1505 param1: STD_Q_LENGTH as i16,
1506 param2: 0,
1507 }),
1508 "enqueue must return false when queue is full"
1509 );
1510 assert_eq!(chan.queue.len(), STD_Q_LENGTH);
1512 }
1513
1514 #[test]
1522 fn find_channel_mut_matches_on_guest_ptr() {
1523 let mut sm = SoundManager::new();
1524 sm.channels.push(SndChannel::new(0xAAAA_0000, true));
1525 sm.channels.push(SndChannel::new(0xBBBB_0000, true));
1526
1527 let found = sm.find_channel_mut(0xBBBB_0000);
1529 assert!(
1530 found.is_some(),
1531 "find_channel_mut must return Some for known ptr"
1532 );
1533 assert_eq!(found.unwrap().guest_ptr, 0xBBBB_0000);
1534
1535 assert!(
1537 sm.find_channel_mut(0xCCCC_0000).is_none(),
1538 "find_channel_mut must return None for unknown ptr"
1539 );
1540
1541 assert!(
1543 sm.find_channel_mut(0).is_none(),
1544 "find_channel_mut must return None for zero ptr"
1545 );
1546 }
1547
1548 #[test]
1554 fn remove_channel_returns_true_and_shrinks_on_hit() {
1555 let mut sm = SoundManager::new();
1556 sm.channels.push(SndChannel::new(0x1234_0000, true));
1557 sm.channels.push(SndChannel::new(0x1234_1000, true));
1558 assert_eq!(sm.channels.len(), 2);
1559
1560 assert!(!sm.remove_channel(0xDEAD_0000));
1562 assert_eq!(sm.channels.len(), 2);
1563
1564 assert!(sm.remove_channel(0x1234_0000));
1566 assert_eq!(sm.channels.len(), 1);
1567 assert_eq!(sm.channels[0].guest_ptr, 0x1234_1000);
1568
1569 assert!(!sm.remove_channel(0x1234_0000));
1571 assert_eq!(sm.channels.len(), 1);
1572 }
1573
1574 #[test]
1582 fn mix_frame_continues_playback_position_across_calls() {
1583 let mut sm = SoundManager::new();
1584 let mut chan = SndChannel::new(0x1234_0000, true);
1585 chan.play_buffer(
1586 vec![0x90, 0xA0, 0xB0, 0xC0],
1587 OUTPUT_RATE << 16,
1588 PlaybackKind::Buffer,
1589 0,
1590 );
1591 sm.channels.push(chan);
1592
1593 let out = sm.mix_frame(2);
1595 assert_eq!(out, vec![0x90, 0xA0], "first mix_frame emits head half");
1596 assert!(sm.channels[0].is_playing(), "playback continues mid-buffer");
1597
1598 let out = sm.mix_frame(2);
1600 assert_eq!(out, vec![0xB0, 0xC0], "second mix_frame emits tail half");
1601 assert!(
1602 !sm.channels[0].is_playing(),
1603 "playback cleared on final_idx >= samples.len()"
1604 );
1605
1606 let out = sm.mix_frame(2);
1608 assert!(out.is_empty(), "post-exhaust mix_frame returns empty");
1609
1610 assert_eq!(sm.debug_samples_mixed, 4);
1613 }
1614
1615 #[test]
1616 fn samples_until_next_exhaustion_tracks_resampled_boundary() {
1617 let mut sm = SoundManager::new();
1618 let mut chan = SndChannel::new(0x1234_0000, true);
1619 chan.play_buffer(
1620 vec![0x90, 0xA0],
1621 (OUTPUT_RATE / 2) << 16,
1622 PlaybackKind::Buffer,
1623 0,
1624 );
1625 sm.channels.push(chan);
1626
1627 assert_eq!(
1628 sm.samples_until_next_exhaustion(),
1629 Some(4),
1630 "half-rate two-sample buffer emits four output samples"
1631 );
1632
1633 let output = sm.mix_frame(1);
1634 assert_eq!(output, vec![0x90]);
1635 assert_eq!(
1636 sm.samples_until_next_exhaustion(),
1637 Some(3),
1638 "boundary query must follow playback position across calls"
1639 );
1640 }
1641
1642 #[test]
1652 fn mix_frame_applies_channel_volume_to_sample() {
1653 let mut sm = SoundManager::new();
1654 let mut chan = SndChannel::new(0x1234_0000, true);
1655
1656 chan.play_buffer(
1657 vec![0xA0; 8], OUTPUT_RATE << 16,
1659 PlaybackKind::Buffer,
1660 0,
1661 );
1662 chan.set_volume(0x0080_0080);
1665 sm.channels.push(chan);
1666
1667 let output = sm.mix_frame(4);
1668
1669 assert!(
1670 output.iter().all(|&b| b == 0x90),
1671 "half-volume must halve centered amplitude (0xA0 → 0x90), got {:02X}",
1672 output[0]
1673 );
1674
1675 let mut sm = SoundManager::new();
1677 let mut chan = SndChannel::new(0x1234_0000, true);
1678 chan.play_buffer(vec![0xA0; 8], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
1679 sm.channels.push(chan);
1681 let output = sm.mix_frame(4);
1682 assert!(
1683 output.iter().all(|&b| b == 0xA0),
1684 "full volume passes sample through unchanged, got {:02X}",
1685 output[0]
1686 );
1687
1688 let mut sm = SoundManager::new();
1690 let mut chan = SndChannel::new(0x1234_0000, true);
1691 chan.play_buffer(vec![0xA0; 8], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
1692 chan.set_volume(0);
1693 sm.channels.push(chan);
1694 let output = sm.mix_frame(4);
1695 assert!(
1696 output.iter().all(|&b| b == 0x80),
1697 "zero volume collapses sample to silence (0x80), got {:02X}",
1698 output[0]
1699 );
1700 }
1701
1702 #[test]
1703 fn mix_frame_preserves_audio_when_default_output_volume_changes() {
1704 let mut sm = SoundManager::new();
1705 sm.set_default_output_volume(0x0080_0080);
1706 let mut chan = SndChannel::new(0x1234_0000, true);
1707 chan.play_buffer(vec![0xA0; 8], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
1708 sm.channels.push(chan);
1709
1710 let output = sm.mix_frame(4);
1711
1712 assert!(
1713 output.iter().all(|&b| b == 0xA0),
1714 "default output volume stores the device default and must not attenuate the current mixed stream, got {:02X}",
1715 output[0]
1716 );
1717
1718 let mut sm = SoundManager::new();
1719 sm.set_default_output_volume(0);
1720 let mut chan = SndChannel::new(0x1234_0000, true);
1721 chan.play_buffer(vec![0xA0; 8], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
1722 sm.channels.push(chan);
1723
1724 let output = sm.mix_frame(4);
1725
1726 assert!(
1727 output.iter().all(|&b| b == 0xA0),
1728 "zero default output volume must not silence active channel audio, got {:02X}",
1729 output[0]
1730 );
1731 }
1732
1733 #[test]
1744 fn mix_frame_resamples_half_rate_with_sample_hold() {
1745 let mut sm = SoundManager::new();
1746 let mut chan = SndChannel::new(0x1234_0000, true);
1747
1748 chan.play_buffer(
1753 vec![0x90, 0xA0],
1754 (OUTPUT_RATE / 2) << 16,
1755 PlaybackKind::Buffer,
1756 0,
1757 );
1758 sm.channels.push(chan);
1759
1760 let output = sm.mix_frame(6);
1761
1762 assert_eq!(output.len(), 6);
1770 assert_eq!(output[0], 0x90, "source[0] at step 0");
1771 assert_eq!(
1772 output[1], 0x90,
1773 "low-rate upsampling must hold the source sample, not smooth it"
1774 );
1775 assert_eq!(output[2], 0xA0, "source[1] at step 2 (position 1.0)");
1776 assert_eq!(output[3], 0xA0, "tail sample held at step 3 (position 1.5)");
1777 assert_eq!(output[4], 0x80, "break leaves default silence");
1778 assert_eq!(output[5], 0x80, "break leaves default silence");
1779
1780 assert!(!sm.channels[0].is_playing());
1782 }
1783
1784 #[test]
1785 fn mix_frame_stereo_preserves_channel_separation() {
1786 let stereo_samples = vec![
1787 StereoSample {
1788 left: 0x00,
1789 right: 0xFF,
1790 },
1791 StereoSample {
1792 left: 0x40,
1793 right: 0xC0,
1794 },
1795 ];
1796
1797 let mut stereo_sm = SoundManager::new();
1798 let mut stereo_chan = SndChannel::new(0x1234_0000, true);
1799 stereo_chan.play_stereo_buffer(
1800 stereo_samples.clone(),
1801 OUTPUT_RATE << 16,
1802 PlaybackKind::Buffer,
1803 0,
1804 );
1805 stereo_sm.channels.push(stereo_chan);
1806
1807 assert_eq!(stereo_sm.mix_frame_stereo(2), vec![0x00, 0xFF, 0x40, 0xC0]);
1808
1809 let mut mono_sm = SoundManager::new();
1810 let mut mono_chan = SndChannel::new(0x1234_0000, true);
1811 mono_chan.play_stereo_buffer(stereo_samples, OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
1812 mono_sm.channels.push(mono_chan);
1813
1814 assert_eq!(mono_sm.mix_frame(2), vec![0x80, 0x80]);
1815 }
1816
1817 #[test]
1818 fn mix_frame_resamples_classic_rate22khz_with_fractional_interpolation() {
1819 const RATE_22KHZ_FIXED: u32 = 0x56EE_8BA3;
1826
1827 let mut sm = SoundManager::new();
1828 let mut chan = SndChannel::new(0x1234_0000, true);
1829 chan.play_buffer(
1830 vec![0x80, 0x00, 0xFF, 0x80],
1831 RATE_22KHZ_FIXED,
1832 PlaybackKind::Buffer,
1833 0,
1834 );
1835 sm.channels.push(chan);
1836
1837 let output = sm.mix_frame(3);
1838
1839 assert_eq!(output[0], 0x80, "position 0.0 reads source[0]");
1840 assert!(
1841 (0x01..=0x0F).contains(&output[1]),
1842 "position just after source[1] should interpolate toward source[2], got {:#04X}",
1843 output[1]
1844 );
1845 assert!(
1846 output[2] > 0x80,
1847 "next fractional sample stays on the rising edge"
1848 );
1849 }
1850
1851 #[test]
1861 fn mix_frame_resamples_2x_source_via_step_advance() {
1862 let mut sm = SoundManager::new();
1863 let mut chan = SndChannel::new(0x1234_0000, true);
1864
1865 chan.play_buffer(
1869 vec![0x90, 0xA0, 0xB0, 0xC0],
1870 (OUTPUT_RATE * 2) << 16,
1871 PlaybackKind::Buffer,
1872 0,
1873 );
1874 sm.channels.push(chan);
1875
1876 let output = sm.mix_frame(3);
1881
1882 assert_eq!(output.len(), 3);
1883 assert_eq!(output[0], 0x90, "source[0] at step 0");
1884 assert_eq!(output[1], 0xB0, "source[2] at step 1 (2.0 advance)");
1885 assert_eq!(output[2], 0x80, "break left default silence");
1886
1887 assert!(
1889 !sm.channels[0].is_playing(),
1890 "playback cleared once source position >= samples.len()"
1891 );
1892 assert_eq!(sm.debug_samples_mixed, 3);
1895 }
1896
1897 #[test]
1907 fn apply_volume_amplifies_above_full_volume_and_clamps() {
1908 let two_x = ((FULL_VOLUME as u32 * 2) << 16) | (FULL_VOLUME as u32 * 2);
1910
1911 assert_eq!(
1914 apply_volume(0xA0, two_x),
1915 0xC0,
1916 "+0x20 doubled = +0x40 → 0xC0"
1917 );
1918
1919 assert_eq!(
1922 apply_volume(0x60, two_x),
1923 0x40,
1924 "-0x20 doubled = -0x40 → 0x40"
1925 );
1926
1927 assert_eq!(
1930 apply_volume(0xFF, two_x),
1931 0xFF,
1932 "+0x7F doubled saturates at 0xFF"
1933 );
1934
1935 assert_eq!(
1938 apply_volume(0x00, two_x),
1939 0x00,
1940 "-0x80 doubled saturates at 0x00"
1941 );
1942
1943 assert_eq!(
1945 apply_volume(0x80, two_x),
1946 0x80,
1947 "silence is silence regardless of gain"
1948 );
1949 }
1950
1951 #[test]
1958 fn quiet_clears_file_paused_after_pause_toggle() {
1959 let mut chan = SndChannel::new(0x1234_0000, true);
1960 chan.play_buffer(vec![0x80; 16], OUTPUT_RATE << 16, PlaybackKind::File, 0);
1961 chan.pause_file_playback_toggle();
1963 assert!(
1964 chan.has_active_playback(),
1965 "playing OR file_paused → active"
1966 );
1967
1968 chan.quiet();
1970 assert!(!chan.is_playing(), "quiet clears playing");
1971 assert!(
1972 !chan.has_active_playback(),
1973 "quiet must clear file_paused too — has_active_playback = playing OR file_paused"
1974 );
1975 }
1976
1977 #[test]
1989 fn mix_frame_channel_with_db_but_no_playing_outputs_silence() {
1990 let mut sm = SoundManager::new();
1991 let mut chan = SndChannel::new(0x1234_0000, true);
1992 chan.double_buffer = Some(DoubleBufferState {
1994 header_ptr: 0x0070_0000,
1995 current_buffer: 1,
1996 callback_addr: 0xCAFE_0000,
1997 chan_ptr: 0x1234_0000,
1998 sample_rate: OUTPUT_RATE << 16,
1999 num_channels: 1,
2000 sample_size: 8,
2001 last_buffer_seen: false,
2002 waiting_for_callback: true,
2003 pending_callback_buffers: [false, true],
2004 });
2005 assert!(!chan.is_playing(), "playing stays None pre-mix");
2006 sm.channels.push(chan);
2007
2008 let output = sm.mix_frame(32);
2009
2010 assert_eq!(
2011 output.len(),
2012 32,
2013 "DB-waiting channel must still produce num_samples (non-empty)"
2014 );
2015 assert!(
2016 output.iter().all(|&b| b == 0x80),
2017 "no playing buffer → output is pure silence (0x80), got {:02X}",
2018 output[0]
2019 );
2020 let db = sm.channels[0]
2023 .double_buffer
2024 .as_ref()
2025 .expect("double_buffer must remain installed");
2026 assert!(
2027 db.waiting_for_callback,
2028 "waiting_for_callback must stay true across idle mix_frame"
2029 );
2030 assert_eq!(db.current_buffer, 1, "current_buffer must not flip on idle");
2031 assert!(
2032 sm.pending_callbacks.is_empty(),
2033 "no new callback pushed on idle-wait frame"
2034 );
2035 }
2036
2037 #[test]
2038 fn mix_frame_idle_double_buffer_requests_refill_once() {
2039 let mut sm = SoundManager::new();
2040 let mut chan = SndChannel::new(0x1234_0000, true);
2041 chan.double_buffer = Some(DoubleBufferState {
2042 header_ptr: 0x0070_0000,
2043 current_buffer: 1,
2044 callback_addr: 0xCAFE_0000,
2045 chan_ptr: 0x1234_0000,
2046 sample_rate: OUTPUT_RATE << 16,
2047 num_channels: 1,
2048 sample_size: 8,
2049 last_buffer_seen: false,
2050 waiting_for_callback: false,
2051 pending_callback_buffers: [false; 2],
2052 });
2053 sm.channels.push(chan);
2054
2055 let output = sm.mix_frame(32);
2056
2057 assert_eq!(
2058 output.len(),
2059 32,
2060 "idle DB channel stays active while requesting a refill"
2061 );
2062 assert!(
2063 output.iter().all(|&b| b == 0x80),
2064 "no ready buffer means the active output is silence"
2065 );
2066 assert_eq!(sm.pending_callbacks.len(), 1);
2067 let callback = &sm.pending_callbacks[0];
2068 assert_eq!(callback.callback_addr, 0xCAFE_0000);
2069 assert_eq!(callback.chan_ptr, 0x1234_0000);
2070 assert_eq!(callback.header_ptr, 0x0070_0000);
2071 assert_eq!(
2072 callback.exhausted_buffer_index, 1,
2073 "retry asks the guest to refill the current missing buffer"
2074 );
2075 let db = sm.channels[0].double_buffer.as_ref().unwrap();
2076 assert!(db.waiting_for_callback);
2077
2078 sm.mix_frame(32);
2079 assert_eq!(
2080 sm.pending_callbacks.len(),
2081 1,
2082 "waiting_for_callback prevents refill callback spam"
2083 );
2084 }
2085
2086 #[test]
2094 fn mix_frame_two_active_channels_sum_arithmetically_and_clamp() {
2095 let mut sm = SoundManager::new();
2098 let mut a = SndChannel::new(0x1000_0000, true);
2099 let mut b = SndChannel::new(0x2000_0000, true);
2100 a.play_buffer(vec![0x90; 32], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2101 b.play_buffer(vec![0xA0; 32], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2102 sm.channels.push(a);
2103 sm.channels.push(b);
2104
2105 let output = sm.mix_frame(16);
2106
2107 assert_eq!(output.len(), 16, "active-channel mix produces num_samples");
2108 assert!(
2109 output.iter().all(|&b| b == 0xB0),
2110 "two-channel sum: 0x80 + (0x90-0x80) + (0xA0-0x80) = 0xB0, got {:02X}",
2111 output[0]
2112 );
2113 assert_eq!(sm.debug_samples_mixed, 16);
2116
2117 let mut sm = SoundManager::new();
2119 let mut a = SndChannel::new(0x1000_0000, true);
2120 let mut b = SndChannel::new(0x2000_0000, true);
2121 a.play_buffer(vec![0xFF; 32], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2122 b.play_buffer(vec![0xFF; 32], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2123 sm.channels.push(a);
2124 sm.channels.push(b);
2125 let output = sm.mix_frame(4);
2126 assert!(
2127 output.iter().all(|&v| v == 0xFF),
2128 "0xFF + 0xFF saturates to 0xFF (upper clamp), got {:02X}",
2129 output[0]
2130 );
2131
2132 let mut sm = SoundManager::new();
2134 let mut a = SndChannel::new(0x1000_0000, true);
2135 let mut b = SndChannel::new(0x2000_0000, true);
2136 a.play_buffer(vec![0x00; 32], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2137 b.play_buffer(vec![0x00; 32], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2138 sm.channels.push(a);
2139 sm.channels.push(b);
2140 let output = sm.mix_frame(4);
2141 assert!(
2142 output.iter().all(|&v| v == 0x00),
2143 "0x00 + 0x00 saturates to 0x00 (lower clamp), got {:02X}",
2144 output[0]
2145 );
2146 }
2147
2148 #[test]
2160 fn mix_frame_double_buffer_guards_inhibit_callback_push() {
2161 {
2163 let mut sm = SoundManager::new();
2164 let mut chan = SndChannel::new(0x1234_0000, true);
2165 chan.play_buffer(vec![0x80, 0x80], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2166 chan.double_buffer = Some(DoubleBufferState {
2167 header_ptr: 0x0070_0000,
2168 current_buffer: 0,
2169 callback_addr: 0xCAFE_0000,
2170 chan_ptr: 0x1234_0000,
2171 sample_rate: OUTPUT_RATE << 16,
2172 num_channels: 1,
2173 sample_size: 8,
2174 last_buffer_seen: true,
2175 waiting_for_callback: false,
2176 pending_callback_buffers: [false; 2],
2177 });
2178 sm.channels.push(chan);
2179
2180 sm.mix_frame(4);
2181
2182 assert!(
2183 sm.pending_callbacks.is_empty(),
2184 "last_buffer_seen=true must inhibit callback push"
2185 );
2186 assert_eq!(
2189 sm.channels[0]
2190 .double_buffer
2191 .as_ref()
2192 .unwrap()
2193 .current_buffer,
2194 0,
2195 "current_buffer must NOT flip when last_buffer_seen=true"
2196 );
2197 }
2198
2199 {
2201 let mut sm = SoundManager::new();
2202 let mut chan = SndChannel::new(0x1234_0000, true);
2203 chan.play_buffer(vec![0x80, 0x80], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2204 chan.double_buffer = Some(DoubleBufferState {
2205 header_ptr: 0x0070_0000,
2206 current_buffer: 0,
2207 callback_addr: 0xCAFE_0000,
2208 chan_ptr: 0x1234_0000,
2209 sample_rate: OUTPUT_RATE << 16,
2210 num_channels: 1,
2211 sample_size: 8,
2212 last_buffer_seen: false,
2213 waiting_for_callback: true,
2214 pending_callback_buffers: [true, false],
2215 });
2216 sm.channels.push(chan);
2217
2218 sm.mix_frame(4);
2219
2220 assert!(
2221 sm.pending_callbacks.is_empty(),
2222 "pending_callback_buffers[current]=true must inhibit duplicate callback push"
2223 );
2224 assert_eq!(
2225 sm.channels[0]
2226 .double_buffer
2227 .as_ref()
2228 .unwrap()
2229 .current_buffer,
2230 1,
2231 "current_buffer still advances to the paired slot"
2232 );
2233 }
2234 }
2235
2236 #[test]
2237 fn mix_frame_allows_other_double_buffer_callback_while_one_slot_is_pending() {
2238 let mut sm = SoundManager::new();
2239 let mut chan = SndChannel::new(0x1234_0000, true);
2240 chan.play_buffer(vec![0x80, 0x80], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2241 chan.double_buffer = Some(DoubleBufferState {
2242 header_ptr: 0x0070_0000,
2243 current_buffer: 1,
2244 callback_addr: 0xCAFE_0000,
2245 chan_ptr: 0x1234_0000,
2246 sample_rate: OUTPUT_RATE << 16,
2247 num_channels: 1,
2248 sample_size: 8,
2249 last_buffer_seen: false,
2250 waiting_for_callback: true,
2251 pending_callback_buffers: [true, false],
2252 });
2253 sm.channels.push(chan);
2254
2255 sm.mix_frame(4);
2256
2257 assert_eq!(
2258 sm.pending_callbacks.len(),
2259 1,
2260 "pending refill for buffer 0 must not suppress buffer 1's doubleback"
2261 );
2262 assert_eq!(sm.pending_callbacks[0].exhausted_buffer_index, 1);
2263 let db = sm.channels[0].double_buffer.as_ref().unwrap();
2264 assert_eq!(db.current_buffer, 0);
2265 assert_eq!(
2266 db.pending_callback_buffers,
2267 [true, true],
2268 "both slots can have outstanding refills independently"
2269 );
2270 assert!(db.waiting_for_callback);
2271 }
2272
2273 #[test]
2290 fn mix_frame_double_buffer_exhaust_queues_callback_and_flips_slot() {
2291 let mut sm = SoundManager::new();
2292 let mut chan = SndChannel::new(0x1234_0000, true);
2293
2294 chan.play_buffer(vec![0x80, 0x80], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2297 chan.double_buffer = Some(DoubleBufferState {
2298 header_ptr: 0x0070_0000,
2299 current_buffer: 0,
2300 callback_addr: 0xCAFE_0000,
2301 chan_ptr: 0x1234_0000,
2302 sample_rate: OUTPUT_RATE << 16,
2303 num_channels: 1,
2304 sample_size: 8,
2305 last_buffer_seen: false,
2306 waiting_for_callback: false,
2307 pending_callback_buffers: [false; 2],
2308 });
2309 sm.channels.push(chan);
2310
2311 sm.mix_frame(4); assert_eq!(
2315 sm.pending_callbacks.len(),
2316 1,
2317 "one double-back callback queued"
2318 );
2319 let p = &sm.pending_callbacks[0];
2320 assert_eq!(p.callback_addr, 0xCAFE_0000);
2321 assert_eq!(p.chan_ptr, 0x1234_0000);
2322 assert_eq!(p.header_ptr, 0x0070_0000);
2323 assert_eq!(
2324 p.exhausted_buffer_index, 0,
2325 "exhausted index is the OLD current_buffer, not the flipped one"
2326 );
2327
2328 let db = sm.channels[0]
2330 .double_buffer
2331 .as_ref()
2332 .expect("db still present");
2333 assert_eq!(db.current_buffer, 1, "current_buffer flipped 0 → 1");
2334 assert!(
2335 db.waiting_for_callback,
2336 "waiting_for_callback armed so next frame doesn't re-trigger"
2337 );
2338 assert_eq!(db.pending_callback_buffers, [true, false]);
2339 }
2340
2341 #[test]
2351 fn mix_frame_file_playback_exhaust_queues_file_completion_callback() {
2352 let mut sm = SoundManager::new();
2353 let mut chan = SndChannel::new(0x1234_0000, true);
2354
2355 chan.play_buffer(
2356 vec![0x80; 2],
2357 OUTPUT_RATE << 16,
2358 PlaybackKind::File,
2359 0xABCD_1234, );
2361 assert_eq!(chan.file_completion_addr, 0xABCD_1234);
2362 sm.channels.push(chan);
2363
2364 sm.mix_frame(4); assert_eq!(sm.pending_sound_callbacks.len(), 1);
2368 match &sm.pending_sound_callbacks[0] {
2369 PendingSoundCallback::FileCompletion {
2370 callback_addr,
2371 chan_ptr,
2372 } => {
2373 assert_eq!(
2374 *callback_addr, 0xABCD_1234,
2375 "file_completion_addr propagates"
2376 );
2377 assert_eq!(*chan_ptr, 0x1234_0000, "chan guest_ptr propagates");
2378 }
2379 other => panic!("expected FileCompletion, got {:?}", other),
2380 }
2381 assert_eq!(
2384 sm.channels[0].file_completion_addr, 0,
2385 "file_completion_addr must be cleared after push"
2386 );
2387 assert!(!sm.channels[0].is_playing());
2389 assert!(!sm.channels[0].has_active_playback());
2390 }
2391
2392 #[test]
2404 fn mix_frame_buffer_exhaust_queues_pending_sound_callback_per_cmd() {
2405 let mut sm = SoundManager::new();
2406 let mut chan = SndChannel::new(0x1234_0000, true);
2407
2408 chan.play_buffer(vec![0x80, 0x80], OUTPUT_RATE << 16, PlaybackKind::Buffer, 0);
2411 chan.callback_addr = 0xBEEF_0000;
2412 chan.queue_callback(SndCommand {
2413 cmd: cmd::CALLBACK,
2414 param1: 7,
2415 param2: 0x1111,
2416 });
2417 chan.queue_callback(SndCommand {
2418 cmd: cmd::CALLBACK,
2419 param1: 9,
2420 param2: 0x2222,
2421 });
2422 sm.channels.push(chan);
2423
2424 sm.mix_frame(4); assert!(
2428 !sm.channels[0].is_playing(),
2429 "playback cleared on exhaustion"
2430 );
2431 assert_eq!(
2433 sm.pending_sound_callbacks.len(),
2434 2,
2435 "one PendingSoundCallback::Command per queued callback cmd"
2436 );
2437 for (i, pending) in sm.pending_sound_callbacks.iter().enumerate() {
2438 match pending {
2439 PendingSoundCallback::Command {
2440 callback_addr,
2441 chan_ptr,
2442 cmd,
2443 } => {
2444 assert_eq!(*callback_addr, 0xBEEF_0000, "callback_addr propagates");
2445 assert_eq!(*chan_ptr, 0x1234_0000, "chan_ptr propagates");
2446 let expected_param1 = if i == 0 { 7 } else { 9 };
2447 assert_eq!(cmd.param1, expected_param1, "cmd ordering preserved");
2448 }
2449 _ => panic!("expected Command variant, got {:?}", pending),
2450 }
2451 }
2452 assert!(
2454 sm.channels[0].take_pending_callback_cmds().is_empty(),
2455 "pending_callback_cmds drained on exhaustion"
2456 );
2457 }
2458
2459 #[test]
2469 fn mix_frame_paused_file_channel_outputs_silence_not_empty() {
2470 let mut sm = SoundManager::new();
2471 let mut chan = SndChannel::new(0x1234_0000, true);
2472
2473 chan.play_buffer(vec![0x80; 128], OUTPUT_RATE << 16, PlaybackKind::File, 0);
2475 chan.pause_file_playback_toggle();
2476 assert!(chan.file_paused, "file_paused must be set after toggle");
2477 sm.channels.push(chan);
2478
2479 let output = sm.mix_frame(64);
2480
2481 assert_eq!(
2482 output.len(),
2483 64,
2484 "paused file channel must still produce num_samples of output"
2485 );
2486 assert!(
2487 output.iter().all(|&b| b == 0x80),
2488 "paused file channel output must be pure silence (0x80)"
2489 );
2490 assert_eq!(
2493 sm.debug_samples_mixed, 64,
2494 "debug_samples_mixed tracks samples even for paused channels"
2495 );
2496 }
2497
2498 #[test]
2507 fn mix_frame_flush_cmd_discards_subsequent_queued_cmds() {
2508 let mut sm = SoundManager::new();
2509 let mut chan = SndChannel::new(0x1234_0000, true);
2510 chan.callback_addr = 0x00AB_CDEF;
2511
2512 chan.enqueue(SndCommand {
2515 cmd: cmd::FLUSH,
2516 param1: 0,
2517 param2: 0,
2518 });
2519 chan.enqueue(SndCommand {
2520 cmd: cmd::CALLBACK,
2521 param1: 7,
2522 param2: 0x1111_2222,
2523 });
2524 assert_eq!(chan.queue.len(), 2, "two cmds queued pre mix_frame");
2525 sm.channels.push(chan);
2526
2527 let output = sm.mix_frame(64);
2528
2529 assert!(output.is_empty(), "idle command drain produces no audio");
2530 assert!(
2531 sm.channels[0].queue.is_empty(),
2532 "queue must be empty after mix_frame"
2533 );
2534 assert!(
2535 sm.pending_sound_callbacks.is_empty(),
2536 "callback after FLUSH must be discarded, not executed"
2537 );
2538 }
2539
2540 #[test]
2547 fn sndchannel_new_initial_state_matches_mac_defaults() {
2548 let mut chan = SndChannel::new(0x1234_0000, true);
2549
2550 assert_eq!(
2552 chan.guest_ptr, 0x1234_0000,
2553 "guest_ptr must match constructor arg"
2554 );
2555 assert!(chan.allocated, "allocated=true must propagate");
2556
2557 assert_eq!(
2559 chan.callback_addr, 0,
2560 "callback_addr starts 0 (no userRoutine yet)"
2561 );
2562 assert!(
2563 chan.double_buffer.is_none(),
2564 "double_buffer starts None (not in SndPlayDoubleBuffer)"
2565 );
2566
2567 assert!(!chan.is_playing(), "fresh channel reports is_playing=false");
2569 assert!(
2570 !chan.has_active_playback(),
2571 "fresh channel reports has_active_playback=false"
2572 );
2573
2574 assert_eq!(
2578 chan.current_rate(),
2579 0x0001_0000,
2580 "rate_fixed must default to UNITY_RATE_FIXED (0x0001_0000)"
2581 );
2582
2583 assert!(
2585 chan.take_pending_callback_cmds().is_empty(),
2586 "pending_callback_cmds must start empty"
2587 );
2588
2589 let guest_alloc = SndChannel::new(0xDEAD_0000, false);
2591 assert_eq!(guest_alloc.guest_ptr, 0xDEAD_0000);
2592 assert!(!guest_alloc.allocated, "allocated=false must propagate");
2593 }
2594}