@@ -951,6 +951,9 @@ async fn init() -> anyhow::Result<()> {
951951 let mut swirl_pos: [ f32 ; 2 ] = [ 0.5 , 0.5 ] ;
952952 let mut swirl_vel: [ f32 ; 2 ] = [ 0.0 , 0.0 ] ;
953953 let mut swirl_initialized: bool = false ;
954+ // Per-voice energy accumulator for organic pulse smoothing
955+ // This soaks up instantaneous note events and lets visuals chase it smoothly
956+ let mut pulse_energy: [ f32 ; 3 ] = [ 0.0 , 0.0 , 0.0 ] ;
954957 * tick. borrow_mut ( ) = Some ( Closure :: wrap ( Box :: new ( move || {
955958 let now = Instant :: now ( ) ;
956959 let dt = now - last_instant;
@@ -967,14 +970,29 @@ async fn init() -> anyhow::Result<()> {
967970
968971 {
969972 let mut ps = pulses_tick. borrow_mut ( ) ;
973+ let n = ps. len ( ) . min ( 3 ) ;
974+ // Accumulate note energy per voice (cap to keep visuals tame)
970975 for ev in & note_events {
971- // Smooth in the pulse so visuals don't jump
972- let target = ( ps[ ev. voice_index ] + ev. velocity as f32 ) . min ( 1.5 ) ;
973- ps[ ev. voice_index ] = 0.7 * ps[ ev. voice_index ] + 0.3 * target;
976+ if ev. voice_index < n {
977+ pulse_energy[ ev. voice_index ] = ( pulse_energy[ ev. voice_index ]
978+ + ev. velocity as f32 )
979+ . min ( 1.8 ) ;
980+ }
981+ }
982+ // Energy decays at a fixed rate; independent of output smoothing
983+ let energy_decay = ( -dt_sec * 1.6 ) . exp ( ) ;
984+ for i in 0 ..n {
985+ pulse_energy[ i] *= energy_decay;
974986 }
975- for p in ps. iter_mut ( ) {
976- // Exponential decay for smoother falloff
977- * p *= 1.0 - ( dt_sec * 1.8 ) . min ( 0.9 ) ;
987+ // Output pulses chase energy with attack/release time constants
988+ let tau_up = 0.10_f32 ; // faster rise
989+ let tau_down = 0.45_f32 ; // slower fall for organic tails
990+ let alpha_up = 1.0 - ( -dt_sec / tau_up) . exp ( ) ;
991+ let alpha_down = 1.0 - ( -dt_sec / tau_down) . exp ( ) ;
992+ for i in 0 ..n {
993+ let target = pulse_energy[ i] . clamp ( 0.0 , 1.5 ) ;
994+ let alpha = if target > ps[ i] { alpha_up } else { alpha_down } ;
995+ ps[ i] += ( target - ps[ i] ) * alpha;
978996 }
979997 // Mouse-driven swirl effect intensity (visual + global audio whoosh)
980998 let w = canvas_for_tick. width ( ) . max ( 1 ) as f32 ;
@@ -1176,7 +1194,7 @@ async fn init() -> anyhow::Result<()> {
11761194
11771195 // Compute camera eye
11781196 let cam_eye = Vec3 :: new ( 0.0 , 0.0 , CAMERA_Z ) ;
1179-
1197+
11801198 let cam_target = Vec3 :: ZERO ;
11811199 // Sync AudioListener position + orientation to camera
11821200 let fwd = ( cam_target - cam_eye) . normalize ( ) ;
0 commit comments