@@ -480,13 +480,11 @@ fn main() {
480480 if new_hover != hover {
481481 // update colors to highlight hovered voice
482482 let mut vis = state. shared . lock ( ) . unwrap ( ) ;
483- if let Some ( prev) = hover {
484- // restore base color
485- let base = DEFAULT_VOICE_COLORS [ prev] ;
486- vis. colors [ prev] = Vec4 :: new ( base[ 0 ] , base[ 1 ] , base[ 2 ] , 1.0 ) ;
483+ // restore all to base first then apply hover brighten for determinism
484+ for ( i, base) in DEFAULT_VOICE_COLORS . iter ( ) . enumerate ( ) {
485+ vis. colors [ i] = Vec4 :: new ( base[ 0 ] , base[ 1 ] , base[ 2 ] , 1.0 ) ;
487486 }
488487 if let Some ( i) = new_hover {
489- // brighten
490488 vis. colors [ i] . x = ( vis. colors [ i] . x * 1.4 ) . min ( 1.0 ) ;
491489 vis. colors [ i] . y = ( vis. colors [ i] . y * 1.4 ) . min ( 1.0 ) ;
492490 vis. colors [ i] . z = ( vis. colors [ i] . z * 1.4 ) . min ( 1.0 ) ;
@@ -543,7 +541,18 @@ struct AudioState {
543541 oscillators : Vec < ActiveOscillator > ,
544542}
545543
546- fn start_audio_engine ( shared_vis : Arc < Mutex < VisState > > , shared_engine : Arc < Mutex < MusicEngine > > ) -> Option < cpal:: Stream > {
544+ fn compute_equal_power_gains ( pos_x_engine : f32 ) -> ( f32 , f32 ) {
545+ // Map engine-space X (roughly -1..1 typical) into pan -1..1
546+ let pan = ( pos_x_engine / 1.5 ) . clamp ( -1.0 , 1.0 ) ;
547+ // Equal-power panning
548+ let angle = ( pan + 1.0 ) * std:: f32:: consts:: FRAC_PI_4 ; // 0..pi/2
549+ ( angle. cos ( ) , angle. sin ( ) )
550+ }
551+
552+ fn start_audio_engine (
553+ shared_vis : Arc < Mutex < VisState > > ,
554+ shared_engine : Arc < Mutex < MusicEngine > > ,
555+ ) -> Option < cpal:: Stream > {
547556 let host = cpal:: default_host ( ) ;
548557 let device = host. default_output_device ( ) ?;
549558 let config = device. default_output_config ( ) . ok ( ) ?;
@@ -579,12 +588,13 @@ fn start_audio_engine(shared_vis: Arc<Mutex<VisState>>, shared_engine: Arc<Mutex
579588 last = now;
580589 let now_sec = start_instant. elapsed ( ) . as_secs_f64 ( ) ;
581590 events. clear ( ) ;
582- engine. tick ( dt, now_sec, & mut events) ;
583- // Apply any changes back to shared engine voices (positions/mute/solo)
591+ // Pull latest voice state from shared engine to reflect input changes
584592 {
585- let mut guard = shared. lock ( ) . unwrap ( ) ;
586- guard. voices = engine. voices . clone ( ) ;
593+ if let Ok ( guard) = shared. lock ( ) {
594+ engine. voices = guard. voices . clone ( ) ;
595+ }
587596 }
597+ engine. tick ( dt, now_sec, & mut events) ;
588598
589599 if !events. is_empty ( ) {
590600 let mut guard = state_clone. lock ( ) . unwrap ( ) ;
@@ -602,10 +612,7 @@ fn start_audio_engine(shared_vis: Arc<Mutex<VisState>>, shared_engine: Arc<Mutex
602612 } ;
603613 // Stereo pan from voice X position (engine-space)
604614 let pos_x = engine. voices [ ev. voice_index ] . position . x ;
605- let pan = ( pos_x / 1.5 ) . clamp ( -1.0 , 1.0 ) ; // -1 left .. 1 right
606- let angle = ( pan + 1.0 ) * std:: f32:: consts:: FRAC_PI_4 ; // 0..pi/2
607- let left_gain = angle. cos ( ) ;
608- let right_gain = angle. sin ( ) ;
615+ let ( left_gain, right_gain) = compute_equal_power_gains ( pos_x) ;
609616 guard. oscillators . push ( ActiveOscillator {
610617 amplitude : ev. velocity . min ( 1.0 ) ,
611618 phase : 0.0 ,
0 commit comments