@@ -17,7 +17,7 @@ use crate::mixer::{
1717 DEFAULT_TEXTURE_GAIN , LayerGains , Mixer ,
1818} ;
1919use crate :: sample_player:: SamplePlayer ;
20- use crate :: scheduler:: { CycleItem , Scheduler } ;
20+ use crate :: scheduler:: { AdvanceResult , CycleItem , Scheduler } ;
2121
2222/// Default sample rate for mobile audio.
2323pub const DEFAULT_SAMPLE_RATE : f32 = 48000.0 ;
@@ -55,6 +55,8 @@ pub struct Engine {
5555 scheduler : Scheduler ,
5656 mixer : Mixer ,
5757 sample_rate : f32 ,
58+ /// Set when all cycle items are exhausted (all oneshot, past first iter).
59+ binaural_muted : bool ,
5860
5961 // -- Atomic shared parameters (written by control, read by audio) --
6062 base_gain : AtomicU32 ,
@@ -63,6 +65,8 @@ pub struct Engine {
6365 binaural_gain : AtomicU32 ,
6466 master_gain : AtomicU32 ,
6567 running : AtomicBool ,
68+ /// Atomic flag for graceful stop request from control thread.
69+ graceful_stop_requested : AtomicBool ,
6670
6771 // -- Pending configuration (written by control, consumed by audio) --
6872 pending_config : Mutex < Option < PendingConfig > > ,
@@ -95,14 +99,17 @@ impl Default for EngineConfig {
9599 CycleItem {
96100 frequency_delta: 3.0 ,
97101 duration_seconds: 30.0 ,
102+ oneshot: false ,
98103 } ,
99104 CycleItem {
100105 frequency_delta: 4.0 ,
101106 duration_seconds: 30.0 ,
107+ oneshot: false ,
102108 } ,
103109 CycleItem {
104110 frequency_delta: 5.0 ,
105111 duration_seconds: 30.0 ,
112+ oneshot: false ,
106113 } ,
107114 ] ,
108115 sample_rate : DEFAULT_SAMPLE_RATE ,
@@ -152,12 +159,14 @@ impl Engine {
152159 scheduler,
153160 mixer,
154161 sample_rate : config. sample_rate ,
162+ binaural_muted : false ,
155163 base_gain : AtomicU32 :: new ( config. base_gain . to_bits ( ) ) ,
156164 texture_gain : AtomicU32 :: new ( config. texture_gain . to_bits ( ) ) ,
157165 event_gain : AtomicU32 :: new ( config. event_gain . to_bits ( ) ) ,
158166 binaural_gain : AtomicU32 :: new ( config. binaural_gain . to_bits ( ) ) ,
159167 master_gain : AtomicU32 :: new ( config. master_gain . to_bits ( ) ) ,
160168 running : AtomicBool :: new ( false ) ,
169+ graceful_stop_requested : AtomicBool :: new ( false ) ,
161170 pending_config : Mutex :: new ( None ) ,
162171 } )
163172 }
@@ -166,14 +175,24 @@ impl Engine {
166175
167176 /// Mark the engine as running. Audio will be generated in [`render`].
168177 pub fn start ( & self ) {
178+ self . graceful_stop_requested . store ( false , Ordering :: Release ) ;
169179 self . running . store ( true , Ordering :: Release ) ;
170180 }
171181
172182 /// Mark the engine as stopped. [`render`] will output silence.
173183 pub fn stop ( & self ) {
184+ self . graceful_stop_requested . store ( false , Ordering :: Release ) ;
174185 self . running . store ( false , Ordering :: Release ) ;
175186 }
176187
188+ /// Request a graceful stop: the engine continues playing until the
189+ /// current cycle iteration completes, then stops automatically.
190+ /// The scheduler's `stop_at_cycle_end` flag is set via an atomic bool
191+ /// and applied on the next render call.
192+ pub fn stop_graceful ( & self ) {
193+ self . graceful_stop_requested . store ( true , Ordering :: Release ) ;
194+ }
195+
177196 /// Whether the engine is currently running.
178197 pub fn is_running ( & self ) -> bool {
179198 self . running . load ( Ordering :: Acquire )
@@ -349,14 +368,43 @@ impl Engine {
349368 }
350369
351370 for i in 0 ..num_frames {
352- // 1. Frequency scheduler update
353- if self . scheduler . advance ( ) {
354- let new_delta = self . scheduler . current_delta ( ) ;
355- self . binaural . set_delta ( new_delta) ;
371+ // 1. Apply graceful stop flag from control thread
372+ if self
373+ . graceful_stop_requested
374+ . compare_exchange ( true , false , Ordering :: Acquire , Ordering :: Relaxed )
375+ . is_ok ( )
376+ {
377+ self . scheduler . set_stop_at_cycle_end ( true ) ;
378+ }
379+
380+ // 2. Frequency scheduler update
381+ match self . scheduler . advance ( ) {
382+ AdvanceResult :: ItemChanged => {
383+ let new_delta = self . scheduler . current_delta ( ) ;
384+ self . binaural . set_delta ( new_delta) ;
385+ }
386+ AdvanceResult :: AllExhausted => {
387+ self . binaural_muted = true ;
388+ }
389+ AdvanceResult :: CycleCompleteStop => {
390+ self . running . store ( false , Ordering :: Release ) ;
391+ // Fill remaining frames with silence and return
392+ for j in i..num_frames {
393+ let idx = j * 2 ;
394+ output[ idx] = 0.0 ;
395+ output[ idx + 1 ] = 0.0 ;
396+ }
397+ return ;
398+ }
399+ AdvanceResult :: NoChange => { }
356400 }
357401
358- // 2. Generate binaural/AM tone
359- let tone = self . binaural . generate ( ) ;
402+ // 3. Generate binaural/AM tone (silent if exhausted)
403+ let tone = if self . binaural_muted {
404+ StereoSample :: default ( )
405+ } else {
406+ self . binaural . generate ( )
407+ } ;
360408
361409 // 3. Base layer
362410 let base = match self . base_layer . as_mut ( ) {
@@ -405,6 +453,7 @@ impl Engine {
405453 }
406454 if let Some ( items) = config. cycle_items {
407455 self . scheduler . set_items ( items) ;
456+ self . binaural_muted = false ;
408457 let new_delta = self . scheduler . current_delta ( ) ;
409458 self . binaural . set_delta ( new_delta) ;
410459 }
@@ -445,10 +494,12 @@ mod tests {
445494 CycleItem {
446495 frequency_delta: 3.0 ,
447496 duration_seconds: 0.01 ,
497+ oneshot: false ,
448498 } ,
449499 CycleItem {
450500 frequency_delta: 5.0 ,
451501 duration_seconds: 0.01 ,
502+ oneshot: false ,
452503 } ,
453504 ] ,
454505 sample_rate : 48000.0 ,
0 commit comments