Skip to content

Commit 64b8b4b

Browse files
committed
web: smooth note-driven visuals with attack/release pulse filter; add per-voice energy accumulator for organic response; update docs
1 parent b73a9d1 commit 64b8b4b

File tree

3 files changed

+27
-7
lines changed

3 files changed

+27
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- 3 voices, spatial audio (Web Audio + PannerNode)
99
- Lush ambient effects: global Convolver reverb and dark feedback Delay bus with per-voice sends and a master bus
1010
- Mouse-driven FX: corner-based saturation (clean ↔ fizz) and opposite-corner delay emphasis; visuals have inertial swirl motion and click ripples
11+
- Note-driven visuals use attack/release smoothing for organic response (no abrupt jumps)
1112
- Start overlay to initialize audio (Click Start; canvas-click fallback)
1213
- Drag voices in XZ plane; click to mute, Shift+Click reseed, Alt+Click solo
1314
- Keyboard: R (reseed all), Space (pause), + / - (tempo)

crates/app-web/src/lib.rs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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();

docs/TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This checklist tracks progress against the high-level plan in `docs/SPEC.md` and
3232

3333
- [x] Instanced rendering of voice markers (circle mask, emissive pulse)
3434
- [x] Audio-reactive pulses on note events
35+
- [x] Attack/release smoothing of note-driven pulses to eliminate jittery jumps
3536
- [x] Ambient visuals (ambient waves background with swirl and click ripples; optional analyser-driven spectrum dots)
3637
- [x] Sync listener orientation with camera
3738
- [x] Visual polish (colors, easing, subtle glow, vignette)

0 commit comments

Comments
 (0)