@@ -138,7 +138,7 @@ impl SoundInstance {
138138
139139/// A playback context owning an output device and one active playback slot.
140140pub struct AudioContext {
141- _output_device : AudioOutputDevice ,
141+ _output_device : Option < AudioOutputDevice > ,
142142 command_queue : Arc < PlaybackCommandQueue > ,
143143 shared_state : Arc < PlaybackSharedState > ,
144144 next_instance_id : u64 ,
@@ -258,7 +258,7 @@ impl AudioContextBuilder {
258258 ) ?;
259259
260260 return Ok ( AudioContext {
261- _output_device : output_device,
261+ _output_device : Some ( output_device) ,
262262 command_queue,
263263 shared_state,
264264 next_instance_id : 1 ,
@@ -378,6 +378,37 @@ impl AudioContext {
378378mod tests {
379379 use super :: * ;
380380
381+ fn create_test_context ( sample_rate : u32 , channels : u16 ) -> AudioContext {
382+ return AudioContext {
383+ _output_device : None ,
384+ command_queue : Arc :: new ( CommandQueue :: new ( ) ) ,
385+ shared_state : Arc :: new ( PlaybackSharedState :: new ( ) ) ,
386+ next_instance_id : 1 ,
387+ output_sample_rate : sample_rate,
388+ output_channels : channels,
389+ } ;
390+ }
391+
392+ fn create_test_sound_buffer (
393+ sample_rate : u32 ,
394+ channels : u16 ,
395+ frames : usize ,
396+ ) -> SoundBuffer {
397+ let sample_count = frames * channels as usize ;
398+ let samples = vec ! [ 0.0 ; sample_count] ;
399+ return SoundBuffer :: from_interleaved_samples_for_test (
400+ samples,
401+ sample_rate,
402+ channels,
403+ )
404+ . expect ( "test sound buffer must be valid" ) ;
405+ }
406+
407+ fn fill_command_queue ( queue : & PlaybackCommandQueue ) {
408+ while queue. push ( PlaybackCommand :: StopCurrent ) . is_ok ( ) { }
409+ return ;
410+ }
411+
381412 /// `SoundInstance` methods MUST be no-ops when the instance is inactive.
382413 #[ test]
383414 fn sound_instance_is_no_op_when_inactive ( ) {
@@ -523,4 +554,161 @@ mod tests {
523554 assert_eq ! ( builder. label. as_deref( ) , Some ( "test-context" ) ) ;
524555 return ;
525556 }
557+
558+ /// The builder MUST reject invalid sample rates before device selection.
559+ #[ test]
560+ fn audio_context_builder_rejects_invalid_sample_rate ( ) {
561+ let result = AudioContextBuilder :: new ( ) . with_sample_rate ( 0 ) . build ( ) ;
562+ assert ! ( matches!(
563+ result,
564+ Err ( AudioError :: InvalidSampleRate { requested: 0 } )
565+ ) ) ;
566+ return ;
567+ }
568+
569+ /// The builder MUST reject invalid channel counts before device selection.
570+ #[ test]
571+ fn audio_context_builder_rejects_invalid_channels ( ) {
572+ let result = AudioContextBuilder :: new ( ) . with_channels ( 0 ) . build ( ) ;
573+ assert ! ( matches!(
574+ result,
575+ Err ( AudioError :: InvalidChannels { requested: 0 } )
576+ ) ) ;
577+ return ;
578+ }
579+
580+ /// `play_sound` MUST reject sound buffers with mismatched sample rates.
581+ #[ test]
582+ fn play_sound_rejects_sample_rate_mismatch ( ) {
583+ let mut context = create_test_context ( 48_000 , 2 ) ;
584+ let buffer = create_test_sound_buffer ( 44_100 , 2 , 4 ) ;
585+
586+ let result = context. play_sound ( & buffer) ;
587+ assert ! ( matches!( result, Err ( AudioError :: InvalidData { .. } ) ) ) ;
588+
589+ assert ! ( context. command_queue. pop( ) . is_none( ) ) ;
590+ assert_eq ! ( context. shared_state. active_instance_id( ) , 0 ) ;
591+ assert_eq ! ( context. shared_state. state( ) , PlaybackState :: Stopped ) ;
592+ return ;
593+ }
594+
595+ /// `play_sound` MUST reject sound buffers with mismatched channel counts.
596+ #[ test]
597+ fn play_sound_rejects_channel_mismatch ( ) {
598+ let mut context = create_test_context ( 48_000 , 2 ) ;
599+ let buffer = create_test_sound_buffer ( 48_000 , 1 , 4 ) ;
600+
601+ let result = context. play_sound ( & buffer) ;
602+ assert ! ( matches!( result, Err ( AudioError :: InvalidData { .. } ) ) ) ;
603+
604+ assert ! ( context. command_queue. pop( ) . is_none( ) ) ;
605+ assert_eq ! ( context. shared_state. active_instance_id( ) , 0 ) ;
606+ assert_eq ! ( context. shared_state. state( ) , PlaybackState :: Stopped ) ;
607+ return ;
608+ }
609+
610+ /// `play_sound` MUST reject empty sound buffers.
611+ #[ test]
612+ fn play_sound_rejects_empty_samples ( ) {
613+ let mut context = create_test_context ( 48_000 , 2 ) ;
614+ let buffer = create_test_sound_buffer ( 48_000 , 2 , 0 /* frames */ ) ;
615+
616+ let result = context. play_sound ( & buffer) ;
617+ assert ! ( matches!( result, Err ( AudioError :: InvalidData { .. } ) ) ) ;
618+
619+ assert ! ( context. command_queue. pop( ) . is_none( ) ) ;
620+ assert_eq ! ( context. shared_state. active_instance_id( ) , 0 ) ;
621+ assert_eq ! ( context. shared_state. state( ) , PlaybackState :: Stopped ) ;
622+ return ;
623+ }
624+
625+ /// `play_sound` MUST schedule stop, buffer, then play commands.
626+ #[ test]
627+ fn play_sound_enqueues_commands_and_updates_state ( ) {
628+ let mut context = create_test_context ( 48_000 , 2 ) ;
629+ let buffer = create_test_sound_buffer ( 48_000 , 2 , 4 ) ;
630+
631+ let instance = context. play_sound ( & buffer) . expect ( "must play sound" ) ;
632+ assert_eq ! ( instance. instance_id, 1 ) ;
633+ assert_eq ! ( context. shared_state. active_instance_id( ) , 1 ) ;
634+ assert_eq ! ( context. shared_state. state( ) , PlaybackState :: Playing ) ;
635+ assert_eq ! ( instance. state( ) , PlaybackState :: Playing ) ;
636+
637+ assert ! ( matches!(
638+ context. command_queue. pop( ) ,
639+ Some ( PlaybackCommand :: StopCurrent )
640+ ) ) ;
641+ match context. command_queue . pop ( ) {
642+ Some ( PlaybackCommand :: SetBuffer {
643+ instance_id,
644+ buffer : scheduled_buffer,
645+ } ) => {
646+ assert_eq ! ( instance_id, 1 ) ;
647+ assert_eq ! ( scheduled_buffer. as_ref( ) , & buffer) ;
648+ }
649+ other => {
650+ panic ! ( "expected SetBuffer command, got {other:?}" ) ;
651+ }
652+ }
653+ assert ! ( matches!(
654+ context. command_queue. pop( ) ,
655+ Some ( PlaybackCommand :: Play { instance_id: 1 } )
656+ ) ) ;
657+ assert ! ( context. command_queue. pop( ) . is_none( ) ) ;
658+ return ;
659+ }
660+
661+ /// `play_sound` MUST restore previous state when the queue is full.
662+ #[ test]
663+ fn play_sound_restores_state_when_queue_full_for_set_buffer ( ) {
664+ let mut context = create_test_context ( 48_000 , 2 ) ;
665+ let buffer = create_test_sound_buffer ( 48_000 , 2 , 4 ) ;
666+
667+ context. shared_state . set_active_instance_id ( 9 ) ;
668+ context. shared_state . set_state ( PlaybackState :: Paused ) ;
669+
670+ fill_command_queue ( & context. command_queue ) ;
671+
672+ let result = context. play_sound ( & buffer) ;
673+ assert ! ( matches!( result, Err ( AudioError :: Platform { .. } ) ) ) ;
674+
675+ assert_eq ! ( context. shared_state. active_instance_id( ) , 9 ) ;
676+ assert_eq ! ( context. shared_state. state( ) , PlaybackState :: Paused ) ;
677+ return ;
678+ }
679+
680+ /// `play_sound` MUST restore previous state when play cannot be enqueued.
681+ #[ test]
682+ fn play_sound_restores_state_when_queue_full_for_play ( ) {
683+ let mut context = create_test_context ( 48_000 , 2 ) ;
684+ let buffer = create_test_sound_buffer ( 48_000 , 2 , 4 ) ;
685+
686+ context. shared_state . set_active_instance_id ( 3 ) ;
687+ context. shared_state . set_state ( PlaybackState :: Paused ) ;
688+
689+ fill_command_queue ( & context. command_queue ) ;
690+ let _first_popped = context. command_queue . pop ( ) ;
691+ let _second_popped = context. command_queue . pop ( ) ;
692+
693+ let result = context. play_sound ( & buffer) ;
694+ assert ! ( matches!( result, Err ( AudioError :: Platform { .. } ) ) ) ;
695+
696+ assert_eq ! ( context. shared_state. active_instance_id( ) , 3 ) ;
697+ assert_eq ! ( context. shared_state. state( ) , PlaybackState :: Paused ) ;
698+ return ;
699+ }
700+
701+ /// Instance ids MUST wrap without using id `0`.
702+ #[ test]
703+ fn play_sound_instance_id_wraps_to_one ( ) {
704+ let mut context = create_test_context ( 48_000 , 2 ) ;
705+ context. next_instance_id = u64:: MAX ;
706+
707+ let buffer = create_test_sound_buffer ( 48_000 , 2 , 4 ) ;
708+
709+ let instance = context. play_sound ( & buffer) . expect ( "must play sound" ) ;
710+ assert_eq ! ( instance. instance_id, u64 :: MAX ) ;
711+ assert_eq ! ( context. next_instance_id, 1 ) ;
712+ return ;
713+ }
526714}
0 commit comments