Skip to content

Commit e2c6d5b

Browse files
Merge pull request #23 from nevermore23274/enhance/visualizer-profiler
feat: adjustable visualizer smoothing + profiler improvements (v0.7.4)
2 parents e0333b7 + 89aadb4 commit e2c6d5b

5 files changed

Lines changed: 146 additions & 31 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "AetherTune"
3-
version = "0.7.3"
3+
version = "0.7.4"
44
edition = "2024"
55

66
[dependencies]

docs/PROFILING.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Press `` ` `` (backtick) to toggle the profiler overlay. While it's open:
88

99
- `>` or `.` — decrease tick rate by 10ms (faster updates, more CPU)
1010
- `<` or `,` — increase tick rate by 10ms (slower updates, less CPU)
11+
- `}` — increase smoothing by 5% (smoother bars, more visual lag)
12+
- `{` — decrease smoothing by 5% (snappier bars, more jitter)
1113

1214
## Reading the Profiler
1315

@@ -67,11 +69,35 @@ These should be well under 100µs each. If IPC poll is consistently high, it may
6769
| **Idle wait** | Time spent sleeping in `event::poll()`, waiting for input or timeout. This is not CPU work. |
6870
| **Frame** | Full wall-clock time per loop iteration (work + idle) |
6971

72+
Idle wait and Frame use **inverted color coding**: high values are green (the CPU is mostly sleeping, which is good), low values are red (the app is struggling to find idle time).
73+
74+
### Visualizer Responsiveness
75+
76+
This section shows how quickly the visualizer bars react to changes in the audio signal.
77+
78+
```
79+
Smoothing 70% │ { } adjust
80+
Settling 7 frames (~70ms @ 10ms tick)
81+
```
82+
83+
**Smoothing** is the noise reduction weight used in the visualizer's integral smoothing (an exponential moving average). Higher values produce smoother, more flowing bar animation but introduce visual lag. Lower values make bars snap to the audio faster but may look jittery. The default is 70% (CAVA uses 77%). Adjustable in 5% steps with `{` and `}`.
84+
85+
**Settling** is the theoretical number of frames for bars to reach 90% of a new target value, calculated as `⌈ln(0.1) / ln(smoothing)⌉`. The millisecond estimate multiplies this by your current tick rate. Color coding: green under 200ms, yellow under 500ms, red above.
86+
87+
| Smoothing | Settling (frames) | @ 10ms tick | @ 30ms tick | Feel |
88+
|-----------|-------------------|-------------|-------------|------|
89+
| 50% | 4 | 40ms | 120ms | Snappy, some jitter |
90+
| 70% | 7 | 70ms | 210ms | Default — responsive with smooth flow |
91+
| 85% | 15 | 150ms | 450ms | Very smooth, noticeable lag |
92+
| 95% | 45 | 450ms | 1350ms | Flowing but sluggish |
93+
94+
The smoothing value is runtime-only and resets to the default (70%) on restart.
95+
7096
### The avg and max Columns
7197

7298
**avg** is the mean over the rolling window. **max** is the highest value seen in that same window. Both use a 2-second rolling window (~60 frames), so old spikes fall off naturally — you're always seeing recent performance, not a startup spike from minutes ago.
7399

74-
Max values are color-coded: green under 5,000µs, yellow under 10,000µs, red above.
100+
For CPU work metrics (Draw, Key input, IPC poll, Visualizer, CPU work), values are color-coded where low is green and high is red. For idle metrics (Idle wait, Frame), the coloring is inverted — high values are green because they indicate the CPU is mostly sleeping.
75101

76102
## Optimizing for Your System
77103

src/audio/visualizer.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use crate::audio::pipe::{SharedAnalysis, NUM_BANDS};
55
const NUM_BARS: usize = NUM_BANDS; // Match the frequency bands
66
const MAX_HEIGHT: u16 = 12;
77

8-
/// Noise reduction factor (0.0 = fast/noisy, 1.0 = slow/smooth)
8+
/// Default noise reduction factor (0.0 = fast/noisy, 1.0 = slow/smooth)
99
/// Controls both integral smoothing weight and gravity modifier.
1010
/// CAVA default is 0.77; we use 0.70 for slightly more responsiveness in a TUI.
11-
const NOISE_REDUCTION: f64 = 0.70;
11+
const DEFAULT_NOISE_REDUCTION: f64 = 0.70;
1212

1313
/// Gravity acceleration increment per frame when a bar is falling.
1414
/// CAVA uses 0.028; we match that.
@@ -43,6 +43,9 @@ pub struct Visualizer {
4343
sensitivity: f64,
4444
/// Whether we're still in the initial ramp-up phase
4545
sens_init: bool,
46+
/// Noise reduction / smoothing weight (0.0 = instant, 1.0 = frozen)
47+
/// Adjustable at runtime via the profiler overlay
48+
pub noise_reduction: f64,
4649
}
4750

4851
impl Visualizer {
@@ -58,6 +61,7 @@ impl Visualizer {
5861
cava_mem: vec![0.0; NUM_BARS],
5962
sensitivity: 1.0,
6063
sens_init: true,
64+
noise_reduction: DEFAULT_NOISE_REDUCTION,
6165
}
6266
}
6367

@@ -83,7 +87,7 @@ impl Visualizer {
8387
let vol_scale = volume as f64 / 100.0;
8488

8589
// Gravity modifier: higher noise_reduction = lower gravity = slower fall
86-
let gravity_mod = 1.0 - (NOISE_REDUCTION * 0.5);
90+
let gravity_mod = 1.0 - (self.noise_reduction * 0.5);
8791

8892
let mut overshoot = false;
8993
let silence = rms < 0.001;
@@ -113,7 +117,7 @@ impl Visualizer {
113117
// --- Integral smoothing ---
114118
// Weighted running average: memory accumulates over time,
115119
// blending the previous memory with the current value
116-
out = self.cava_mem[i] * NOISE_REDUCTION + out;
120+
out = self.cava_mem[i] * self.noise_reduction + out;
117121
self.cava_mem[i] = out;
118122

119123
// --- Autosens clamping ---
@@ -158,7 +162,7 @@ impl Visualizer {
158162

159163
if !is_playing {
160164
// Apply gravity fall-off even in simulated mode for smooth stop
161-
let gravity_mod = 1.0 - (NOISE_REDUCTION * 0.5);
165+
let gravity_mod = 1.0 - (self.noise_reduction * 0.5);
162166
for i in 0..NUM_BARS {
163167
if self.bars[i] > 0 {
164168
self.cava_fall[i] += GRAVITY_STEP;
@@ -183,7 +187,7 @@ impl Visualizer {
183187
self.active = true;
184188
let mut rng = rand::rng();
185189
let vol_scale = volume as f64 / 100.0;
186-
let gravity_mod = 1.0 - (NOISE_REDUCTION * 0.5);
190+
let gravity_mod = 1.0 - (self.noise_reduction * 0.5);
187191

188192
if self.frame % 3 == 0 {
189193
let has_real_data = audio_level > 0.01;
@@ -220,7 +224,7 @@ impl Visualizer {
220224
self.prev_out[i] = out;
221225

222226
// Integral smoothing
223-
out = self.cava_mem[i] * NOISE_REDUCTION + out;
227+
out = self.cava_mem[i] * self.noise_reduction + out;
224228
self.cava_mem[i] = out;
225229

226230
out = out.min(1.0);
@@ -319,7 +323,7 @@ mod tests {
319323

320324
#[test]
321325
fn test_gravity_constants_are_sane() {
322-
assert!(NOISE_REDUCTION > 0.0 && NOISE_REDUCTION < 1.0);
326+
assert!(DEFAULT_NOISE_REDUCTION > 0.0 && DEFAULT_NOISE_REDUCTION < 1.0);
323327
assert!(GRAVITY_STEP > 0.0 && GRAVITY_STEP < 0.1);
324328
assert!(AUTOSENS_DECREASE < 1.0);
325329
assert!(AUTOSENS_INCREASE > 1.0);

src/main.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
286286
} else if app.show_perf && app.keybindings.perf_tick_faster.matches(kc) {
287287
app.tick_rate_ms = app.tick_rate_ms.saturating_sub(10).max(10);
288288
app.save_config();
289+
290+
// Smoothing adjustment (only when profiler is open)
291+
} else if app.show_perf && kc == KeyCode::Char('{') {
292+
// Decrease smoothing (more responsive)
293+
let nr = app.visualizer.noise_reduction;
294+
app.visualizer.noise_reduction = ((nr - 0.05) * 100.0).round() / 100.0;
295+
if app.visualizer.noise_reduction < 0.05 {
296+
app.visualizer.noise_reduction = 0.05;
297+
}
298+
} else if app.show_perf && kc == KeyCode::Char('}') {
299+
// Increase smoothing (smoother)
300+
let nr = app.visualizer.noise_reduction;
301+
app.visualizer.noise_reduction = ((nr + 0.05) * 100.0).round() / 100.0;
302+
if app.visualizer.noise_reduction > 0.95 {
303+
app.visualizer.noise_reduction = 0.95;
304+
}
289305
}
290306
}
291307
InputMode::Editing => match key.code {

src/ui/perf_overlay.rs

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -92,23 +92,69 @@ pub fn draw(f: &mut Frame, app: &App, area: Rect) {
9292
// Section header
9393
lines.push(section_header("Per-frame work (all frames)"));
9494
lines.push(column_header());
95-
lines.push(timing_line("Draw", avg.draw_us, max.draw_us));
96-
lines.push(timing_line("Key input", avg.event_handle_us, max.event_handle_us));
95+
lines.push(timing_line("Draw", avg.draw_us, max.draw_us, false));
96+
lines.push(timing_line("Key input", avg.event_handle_us, max.event_handle_us, false));
9797
lines.push(Line::from(""));
9898

9999
// Tick-only section
100100
lines.push(section_header("Tick work (tick frames only)"));
101101
lines.push(column_header());
102-
lines.push(timing_line("IPC poll", summary.tick_avg_poll_us, summary.tick_max_poll_us));
103-
lines.push(timing_line("Visualizer", summary.tick_avg_vis_us, summary.tick_max_vis_us));
102+
lines.push(timing_line("IPC poll", summary.tick_avg_poll_us, summary.tick_max_poll_us, false));
103+
lines.push(timing_line("Visualizer", summary.tick_avg_vis_us, summary.tick_max_vis_us, false));
104+
lines.push(Line::from(""));
105+
106+
// Visualizer responsiveness section
107+
lines.push(section_header("Visualizer responsiveness"));
108+
// Settling frames = ln(0.1) / ln(noise_reduction) — frames for bars to reach 90% of target
109+
let noise_reduction = app.visualizer.noise_reduction;
110+
let settling_frames = if noise_reduction > 0.0 && noise_reduction < 1.0 {
111+
(0.1_f64.ln() / noise_reduction.ln()).ceil() as u64
112+
} else {
113+
0
114+
};
115+
let settling_ms = settling_frames * tick_ms;
116+
let settling_color = if settling_ms > 200 {
117+
RED
118+
} else if settling_ms > 100 {
119+
YELLOW
120+
} else {
121+
NEON_GREEN
122+
};
123+
lines.push(Line::from(vec![
124+
Span::styled(
125+
format!(" Smoothing "),
126+
Style::default().fg(Color::Rgb(140, 140, 160)),
127+
),
128+
Span::styled(
129+
format!("{:.0}%", noise_reduction * 100.0),
130+
Style::default().fg(CYAN).add_modifier(Modifier::BOLD),
131+
),
132+
Span::styled(" │ ", Style::default().fg(Color::Rgb(50, 50, 70))),
133+
Span::styled("{ } ", Style::default().fg(Color::Rgb(80, 80, 110))),
134+
Span::styled("adjust", Style::default().fg(Color::Rgb(60, 60, 90))),
135+
]));
136+
lines.push(Line::from(vec![
137+
Span::styled(
138+
format!(" Settling "),
139+
Style::default().fg(Color::Rgb(140, 140, 160)),
140+
),
141+
Span::styled(
142+
format!("{} frames", settling_frames),
143+
Style::default().fg(settling_color),
144+
),
145+
Span::styled(
146+
format!(" (~{}ms @ {}ms tick)", settling_ms, tick_ms),
147+
Style::default().fg(Color::Rgb(100, 100, 130)),
148+
),
149+
]));
104150
lines.push(Line::from(""));
105151

106152
// Totals
107153
lines.push(section_header("Totals"));
108154
lines.push(column_header());
109-
lines.push(timing_line("CPU work", work_avg, max.work_us()));
110-
lines.push(timing_line("Idle wait", avg.event_wait_us, max.event_wait_us));
111-
lines.push(timing_line("Frame", avg.total_us, max.total_us));
155+
lines.push(timing_line("CPU work", work_avg, max.work_us(), false));
156+
lines.push(timing_line("Idle wait", avg.event_wait_us, max.event_wait_us, true));
157+
lines.push(timing_line("Frame", avg.total_us, max.total_us, true));
112158
lines.push(Line::from(""));
113159

114160
lines.push(Line::from(""));
@@ -124,7 +170,7 @@ pub fn draw(f: &mut Frame, app: &App, area: Rect) {
124170
Span::styled(" >80%", Style::default().fg(Color::Rgb(60, 60, 90))),
125171
]));
126172
lines.push(Line::from(Span::styled(
127-
" ` close │ < > tick rate │ 2s rolling window",
173+
" ` close │ < > tick rate │ { } smoothing │ 2s rolling window",
128174
Style::default().fg(Color::Rgb(60, 60, 90)),
129175
)));
130176

@@ -157,23 +203,46 @@ fn column_header() -> Line<'static> {
157203
))
158204
}
159205

160-
fn timing_line(label: &str, avg: u64, max: u64) -> Line<'static> {
161-
let color = if avg > 5000 {
162-
RED
163-
} else if avg > 2000 {
164-
YELLOW
165-
} else if avg > 500 {
166-
Color::Rgb(180, 200, 180)
206+
fn timing_line(label: &str, avg: u64, max: u64, invert: bool) -> Line<'static> {
207+
// For inverted rows (idle wait, frame), high values are good (green)
208+
// For normal rows (CPU work, draw, etc.), low values are good (green)
209+
let color = if invert {
210+
// High = good (sleeping a lot), low = bad (busy)
211+
if avg < 2000 {
212+
RED
213+
} else if avg < 5000 {
214+
YELLOW
215+
} else {
216+
NEON_GREEN
217+
}
167218
} else {
168-
NEON_GREEN
219+
if avg > 5000 {
220+
RED
221+
} else if avg > 2000 {
222+
YELLOW
223+
} else if avg > 500 {
224+
Color::Rgb(180, 200, 180)
225+
} else {
226+
NEON_GREEN
227+
}
169228
};
170229

171-
let max_color = if max > 10000 {
172-
RED
173-
} else if max > 5000 {
174-
YELLOW
230+
let max_color = if invert {
231+
if max < 2000 {
232+
RED
233+
} else if max < 5000 {
234+
YELLOW
235+
} else {
236+
Color::Rgb(100, 100, 130)
237+
}
175238
} else {
176-
Color::Rgb(100, 100, 130)
239+
if max > 10000 {
240+
RED
241+
} else if max > 5000 {
242+
YELLOW
243+
} else {
244+
Color::Rgb(100, 100, 130)
245+
}
177246
};
178247

179248
Line::from(vec![

0 commit comments

Comments
 (0)