@@ -285,4 +285,78 @@ mod tests {
285285 assert ! ( !engine. voices[ 1 ] . muted) ;
286286 assert ! ( !engine. voices[ 2 ] . muted) ;
287287 }
288+
289+ #[ test]
290+ fn reseed_determinism_per_voice ( ) {
291+ // Given identical configs and params, reseeding a voice with a fixed seed should
292+ // produce identical first scheduled events across engines.
293+ let configs = vec ! [
294+ VoiceConfig {
295+ color_rgb: [ 1.0 , 0.0 , 0.0 ] ,
296+ waveform: Waveform :: Sine ,
297+ base_position: Vec3 :: new( -1.0 , 0.0 , 0.0 ) ,
298+ } ,
299+ VoiceConfig {
300+ color_rgb: [ 0.0 , 1.0 , 0.0 ] ,
301+ waveform: Waveform :: Saw ,
302+ base_position: Vec3 :: new( 1.0 , 0.0 , 0.0 ) ,
303+ } ,
304+ VoiceConfig {
305+ color_rgb: [ 0.0 , 0.0 , 1.0 ] ,
306+ waveform: Waveform :: Triangle ,
307+ base_position: Vec3 :: new( 0.0 , 0.0 , -1.0 ) ,
308+ } ,
309+ ] ;
310+ let params = EngineParams :: default ( ) ;
311+ let mut a = MusicEngine :: new ( configs. clone ( ) , params. clone ( ) , 111 ) ;
312+ let mut b = MusicEngine :: new ( configs, params, 222 ) ;
313+ // Force same reseed for voice 1
314+ a. reseed_voice ( 1 , Some ( 9999 ) ) ;
315+ b. reseed_voice ( 1 , Some ( 9999 ) ) ;
316+ // Advance enough time to schedule a step and collect events
317+ let mut out_a = Vec :: new ( ) ;
318+ let mut out_b = Vec :: new ( ) ;
319+ a. tick ( Duration :: from_millis ( 300 ) , 0.0 , & mut out_a) ;
320+ b. tick ( Duration :: from_millis ( 300 ) , 0.0 , & mut out_b) ;
321+ // Filter for voice 1 events and compare first if both exist
322+ let ev_a = out_a. into_iter ( ) . find ( |e| e. voice_index == 1 ) ;
323+ let ev_b = out_b. into_iter ( ) . find ( |e| e. voice_index == 1 ) ;
324+ if let ( Some ( x) , Some ( y) ) = ( ev_a, ev_b) {
325+ assert ! ( ( x. frequency_hz - y. frequency_hz) . abs( ) < 1e-3 ) ;
326+ assert ! ( ( x. duration_sec - y. duration_sec) . abs( ) < 1e-3 ) ;
327+ }
328+ }
329+
330+ #[ test]
331+ fn tempo_change_does_not_break_mute_and_solo ( ) {
332+ let configs = vec ! [
333+ VoiceConfig {
334+ color_rgb: [ 1.0 , 0.0 , 0.0 ] ,
335+ waveform: Waveform :: Sine ,
336+ base_position: Vec3 :: new( -1.0 , 0.0 , 0.0 ) ,
337+ } ,
338+ VoiceConfig {
339+ color_rgb: [ 0.0 , 1.0 , 0.0 ] ,
340+ waveform: Waveform :: Saw ,
341+ base_position: Vec3 :: new( 1.0 , 0.0 , 0.0 ) ,
342+ } ,
343+ VoiceConfig {
344+ color_rgb: [ 0.0 , 0.0 , 1.0 ] ,
345+ waveform: Waveform :: Triangle ,
346+ base_position: Vec3 :: new( 0.0 , 0.0 , -1.0 ) ,
347+ } ,
348+ ] ;
349+ let params = EngineParams :: default ( ) ;
350+ let mut engine = MusicEngine :: new ( configs, params, 7 ) ;
351+ // Solo voice 0 then change tempo
352+ engine. toggle_solo ( 0 ) ;
353+ engine. set_bpm ( 140.0 ) ;
354+ // Mute flags should still reflect solo state
355+ assert ! ( !engine. voices[ 0 ] . muted) ;
356+ assert ! ( engine. voices[ 1 ] . muted) ;
357+ assert ! ( engine. voices[ 2 ] . muted) ;
358+ // Toggle solo off and ensure unmuted
359+ engine. toggle_solo ( 0 ) ;
360+ assert ! ( engine. voices. iter( ) . all( |v| !v. muted) ) ;
361+ }
288362}
0 commit comments