playing with visual effects: splash color

This commit is contained in:
jess 2026-05-31 11:12:30 -07:00
parent 66ec445fdd
commit 3c079312dc
10 changed files with 269 additions and 72 deletions

View File

@ -26,8 +26,12 @@ struct Bin {
hue: f32, hue: f32,
sat: f32, sat: f32,
val: f32, val: f32,
splash: f32, // 0..1 suppressed-energy color splash
}; };
// 5/12 hue-wheel rotation per full splash, cycling all 12 wheel segments.
const SPLASH_HUE_OFFSET: f32 = 0.4166667;
@group(0) @binding(0) var<uniform> globals: Globals; @group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var<storage, read> bins: array<Bin>; @group(0) @binding(1) var<storage, read> bins: array<Bin>;
@ -84,16 +88,19 @@ fn alpha_for(b: Bin, fade: f32) -> f32 {
fn dyn_rgb(b: Bin) -> vec3<f32> { fn dyn_rgb(b: Bin) -> vec3<f32> {
let fb = final_brightness(b); let fb = final_brightness(b);
let s = clamp(b.sat * globals.hue_param, 0.0, 1.0); let hue = fract(b.hue + SPLASH_HUE_OFFSET * b.splash);
let v = clamp(b.val * fb, 0.0, 1.0); let s = clamp((b.sat + 0.4 * b.splash) * globals.hue_param, 0.0, 1.0);
return hsv_to_rgb(b.hue, s, v); let v = clamp((b.val + 0.4 * b.splash) * fb, 0.0, 1.0);
return hsv_to_rgb(hue, s, v);
} }
fn fill_rgb(b: Bin) -> vec3<f32> { fn fill_rgb(b: Bin) -> vec3<f32> {
if (flag(1u)) { if (flag(1u)) {
let fb = final_brightness(b); let fb = final_brightness(b);
let v = clamp(globals.unified_val * fb, 0.0, 1.0); let hue = fract(globals.unified_hue + SPLASH_HUE_OFFSET * b.splash);
return hsv_to_rgb(globals.unified_hue, globals.unified_sat, v); let s = clamp(globals.unified_sat + 0.4 * b.splash, 0.0, 1.0);
let v = clamp((globals.unified_val + 0.4 * b.splash) * fb, 0.0, 1.0);
return hsv_to_rgb(hue, s, v);
} }
return dyn_rgb(b); return dyn_rgb(b);
} }

View File

@ -17,6 +17,8 @@ pub struct FrameData {
pub primary_db: Vec<f32>, pub primary_db: Vec<f32>,
pub cepstrum: Vec<f32>, pub cepstrum: Vec<f32>,
pub activity: Vec<f32>,
} }
/// stereo three-band processor pool driven by a streaming hilbert source and surfacing one frame per call to step. /// stereo three-band processor pool driven by a streaming hilbert source and surfacing one frame per call to step.
@ -218,6 +220,18 @@ impl Analyzer {
} }
} }
/// toggles smoothed-cadence splash injection on every processor.
pub fn set_smooth_splashes(&mut self, on: bool) {
for p in self
.main
.iter_mut()
.chain(self.transient.iter_mut())
.chain(self.deep.iter_mut())
{
p.set_smooth_splashes(on);
}
}
/// advances one hop of analytic-signal data at the requested normalised playhead and publishes a stereo frame pair. /// advances one hop of analytic-signal data at the requested normalised playhead and publishes a stereo frame pair.
pub fn step(&mut self, position: f64) -> Option<&[FrameData]> { pub fn step(&mut self, position: f64) -> Option<&[FrameData]> {
let total = self.total_samples(); let total = self.total_samples();
@ -508,6 +522,7 @@ impl Analyzer {
db: spec_main.db, db: spec_main.db,
primary_db, primary_db,
cepstrum, cepstrum,
activity: std::mem::take(&mut spec_main.activity),
}); });
} }

View File

@ -27,6 +27,7 @@ enum Cmd {
SetGpuBlend(f32), SetGpuBlend(f32),
SetSmoothingTilt(f32), SetSmoothingTilt(f32),
SetSmoothingStrength(f32), SetSmoothingStrength(f32),
SetSmoothSplashes(bool),
SetNoiseGate(f32), SetNoiseGate(f32),
SetMode(AnalyzerMode), SetMode(AnalyzerMode),
PushLivePcm { samples: Vec<f32>, sample_rate: u32, channels: u32 }, PushLivePcm { samples: Vec<f32>, sample_rate: u32, channels: u32 },
@ -122,6 +123,11 @@ impl AnalyzerWorker {
let _ = self.cmd_tx.send(Cmd::SetSmoothingStrength(strength)); let _ = self.cmd_tx.send(Cmd::SetSmoothingStrength(strength));
} }
/// queues a smoothed-splash toggle.
pub fn set_smooth_splashes(&self, on: bool) {
let _ = self.cmd_tx.send(Cmd::SetSmoothSplashes(on));
}
/// queues a noise-gate threshold change in dB. /// queues a noise-gate threshold change in dB.
pub fn set_noise_gate(&self, db: f32) { pub fn set_noise_gate(&self, db: f32) {
let _ = self.cmd_tx.send(Cmd::SetNoiseGate(db)); let _ = self.cmd_tx.send(Cmd::SetNoiseGate(db));
@ -267,6 +273,7 @@ fn apply(analyzer: &mut Analyzer, cmd: Cmd) {
Cmd::SetGpuBlend(b) => analyzer.set_gpu_blend(b), Cmd::SetGpuBlend(b) => analyzer.set_gpu_blend(b),
Cmd::SetSmoothingTilt(t) => analyzer.set_smoothing_tilt(t as f64), Cmd::SetSmoothingTilt(t) => analyzer.set_smoothing_tilt(t as f64),
Cmd::SetSmoothingStrength(s) => analyzer.set_smoothing_strength(s as f64), Cmd::SetSmoothingStrength(s) => analyzer.set_smoothing_strength(s as f64),
Cmd::SetSmoothSplashes(on) => analyzer.set_smooth_splashes(on),
Cmd::SetNoiseGate(db) => analyzer.set_noise_gate(db), Cmd::SetNoiseGate(db) => analyzer.set_noise_gate(db),
Cmd::SetMode(_) | Cmd::PushLivePcm { .. } => {} Cmd::SetMode(_) | Cmd::PushLivePcm { .. } => {}
Cmd::Shutdown => {} Cmd::Shutdown => {}

View File

@ -16,6 +16,8 @@ pub struct Spectrum {
pub db: Vec<f32>, pub db: Vec<f32>,
pub cepstrum: Vec<f32>, pub cepstrum: Vec<f32>,
pub activity: Vec<f32>,
} }
@ -44,9 +46,13 @@ pub struct Processor {
bin_alphas: Vec<f64>, bin_alphas: Vec<f64>,
bin_smoothed: Vec<f64>, bin_smoothed: Vec<f64>,
bin_activity: Vec<f64>,
bin_reservoir: Vec<f64>,
bin_prev_delta: Vec<f64>,
bin_initialized: bool, bin_initialized: bool,
smoothing_tilt: f64, smoothing_tilt: f64,
smoothing_strength: f64, smoothing_strength: f64,
smooth_splashes: bool,
history: VecDeque<Vec<f64>>, history: VecDeque<Vec<f64>>,
smoothing_length: usize, smoothing_length: usize,
@ -83,9 +89,13 @@ impl Processor {
freqs_const: Vec::new(), freqs_const: Vec::new(),
bin_alphas: Vec::new(), bin_alphas: Vec::new(),
bin_smoothed: Vec::new(), bin_smoothed: Vec::new(),
bin_activity: Vec::new(),
bin_reservoir: Vec::new(),
bin_prev_delta: Vec::new(),
bin_initialized: false, bin_initialized: false,
smoothing_tilt: DEFAULT_SMOOTHING_TILT, smoothing_tilt: DEFAULT_SMOOTHING_TILT,
smoothing_strength: DEFAULT_SMOOTHING_STRENGTH, smoothing_strength: DEFAULT_SMOOTHING_STRENGTH,
smooth_splashes: true,
num_bins: 26, num_bins: 26,
history: VecDeque::new(), history: VecDeque::new(),
smoothing_length: 3, smoothing_length: 3,
@ -119,6 +129,11 @@ impl Processor {
self.rebuild_smoothing_alphas(); self.rebuild_smoothing_alphas();
} }
/// toggles splash injection between the smoothed-envelope-peak cadence and the raw transient cadence.
pub fn set_smooth_splashes(&mut self, on: bool) {
self.smooth_splashes = on;
}
/// caps the rolling history depth of the per-bin db time-average. /// caps the rolling history depth of the per-bin db time-average.
pub fn set_smoothing(&mut self, history_length: usize) { pub fn set_smoothing(&mut self, history_length: usize) {
self.smoothing_length = history_length.max(1); self.smoothing_length = history_length.max(1);
@ -181,7 +196,11 @@ impl Processor {
} }
self.rebuild_smoothing_alphas(); self.rebuild_smoothing_alphas();
self.bin_smoothed = vec![-100.0; self.freqs_const.len()]; let cols = self.freqs_const.len();
self.bin_smoothed = vec![-100.0; cols];
self.bin_activity = vec![0.0; cols];
self.bin_reservoir = vec![0.0; cols];
self.bin_prev_delta = vec![0.0; cols];
self.bin_initialized = false; self.bin_initialized = false;
} }
@ -339,11 +358,15 @@ impl Processor {
.map(|i| (cep_buf[i].re * cep_scale) as f32) .map(|i| (cep_buf[i].re * cep_scale) as f32)
.collect(); .collect();
if self.bin_smoothed.len() != self.freqs_const.len() { let cols = self.freqs_const.len();
self.bin_smoothed = vec![-100.0; self.freqs_const.len()]; if self.bin_smoothed.len() != cols {
self.bin_smoothed = vec![-100.0; cols];
self.bin_activity = vec![0.0; cols];
self.bin_reservoir = vec![0.0; cols];
self.bin_prev_delta = vec![0.0; cols];
self.bin_initialized = false; self.bin_initialized = false;
} }
let mut current_db = vec![0.0_f64; self.freqs_const.len()]; let mut current_db = vec![0.0_f64; cols];
for (i, &target) in self.sample_freqs.iter().enumerate() { for (i, &target) in self.sample_freqs.iter().enumerate() {
let mag = lerp_at(&freqs_full, &mag_full, target); let mag = lerp_at(&freqs_full, &mag_full, target);
let raw = 20.0 * mag.max(1e-12).log10(); let raw = 20.0 * mag.max(1e-12).log10();
@ -353,6 +376,24 @@ impl Processor {
let a = self.bin_alphas.get(i).copied().unwrap_or(1.0); let a = self.bin_alphas.get(i).copied().unwrap_or(1.0);
self.bin_smoothed[i] + a * (raw - self.bin_smoothed[i]) self.bin_smoothed[i] + a * (raw - self.bin_smoothed[i])
}; };
// suppressed-transient energy as the positive residual between raw and the smoothed display.
let residual = (raw - smoothed).max(0.0);
let norm = (residual / ACTIVITY_REF_DB).clamp(0.0, 1.0);
let delta = smoothed - self.bin_smoothed[i];
if self.smooth_splashes {
self.bin_reservoir[i] += norm;
let peaked = self.bin_prev_delta[i] > 0.0 && delta <= 0.0;
if peaked {
let dump = (self.bin_reservoir[i] * ACTIVITY_RESERVOIR_SCALE).clamp(0.0, 1.0);
self.bin_activity[i] = dump.max(self.bin_activity[i]);
self.bin_reservoir[i] = 0.0;
} else {
self.bin_activity[i] *= ACTIVITY_DECAY;
}
} else {
self.bin_activity[i] = norm.max(self.bin_activity[i] * ACTIVITY_DECAY);
}
self.bin_prev_delta[i] = delta;
self.bin_smoothed[i] = smoothed; self.bin_smoothed[i] = smoothed;
let mut val = smoothed; let mut val = smoothed;
if (self.expand_ratio - 1.0).abs() > f32::EPSILON { if (self.expand_ratio - 1.0).abs() > f32::EPSILON {
@ -366,6 +407,7 @@ impl Processor {
current_db[i] = val; current_db[i] = val;
} }
self.bin_initialized = true; self.bin_initialized = true;
let activity_ret: Vec<f32> = self.bin_activity.iter().map(|&a| a as f32).collect();
self.history.push_back(current_db); self.history.push_back(current_db);
while self.history.len() > self.smoothing_length { while self.history.len() > self.smoothing_length {
@ -391,6 +433,7 @@ impl Processor {
freqs: freqs_ret, freqs: freqs_ret,
db: averaged, db: averaged,
cepstrum, cepstrum,
activity: activity_ret,
} }
} }
@ -430,6 +473,15 @@ const DEFAULT_SMOOTHING_TILT: f64 = 0.6;
/// default per-bin smoothing mix between raw FFT and smoothed values. /// default per-bin smoothing mix between raw FFT and smoothed values.
const DEFAULT_SMOOTHING_STRENGTH: f64 = 0.7; const DEFAULT_SMOOTHING_STRENGTH: f64 = 0.7;
/// residual dB mapping to full splash activity.
const ACTIVITY_REF_DB: f64 = 12.0;
/// per-frame peak-hold decay for splash activity.
const ACTIVITY_DECAY: f64 = 0.85;
/// reservoir-to-activity scale at a smoothed-envelope peak.
const ACTIVITY_RESERVOIR_SCALE: f64 = 0.25;
/// builds a Hamming or Blackman-Harris analysis window at the requested size. /// builds a Hamming or Blackman-Harris analysis window at the requested size.
fn build_window(size: usize) -> Vec<f64> { fn build_window(size: usize) -> Vec<f64> {
if size == 0 { if size == 0 {

View File

@ -79,6 +79,9 @@ pub struct App {
pub show_settings: bool, pub show_settings: bool,
/// expands the advanced settings group at the bottom of the panel.
pub show_advanced: bool,
/// shell-side picker request flag drained by the iOS host once per tick. /// shell-side picker request flag drained by the iOS host once per tick.
pub pending_pick: u8, pub pending_pick: u8,
@ -173,6 +176,14 @@ pub struct Settings {
#[serde(default = "default_smoothing_strength")] #[serde(default = "default_smoothing_strength")]
pub smoothing_strength: f32, pub smoothing_strength: f32,
/// fires color splashes at the smoothed-envelope cadence rather than the raw transient cadence.
#[serde(default = "default_smooth_splashes")]
pub smooth_splashes: bool,
/// intensity multiplier on the suppressed-energy color splash.
#[serde(default = "default_splash")]
pub splash: f32,
/// dB cutoff for the live-mode noise gate. /// dB cutoff for the live-mode noise gate.
#[serde(default = "default_noise_gate_db")] #[serde(default = "default_noise_gate_db")]
pub noise_gate_db: f32, pub noise_gate_db: f32,
@ -193,6 +204,16 @@ fn default_smoothing_strength() -> f32 {
0.7 0.7
} }
/// default smoothed-splash toggle.
fn default_smooth_splashes() -> bool {
true
}
/// default splash intensity.
fn default_splash() -> f32 {
1.0
}
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -214,6 +235,8 @@ impl Default for Settings {
gpu_blend: 0.7, gpu_blend: 0.7,
smoothing_tilt: default_smoothing_tilt(), smoothing_tilt: default_smoothing_tilt(),
smoothing_strength: default_smoothing_strength(), smoothing_strength: default_smoothing_strength(),
smooth_splashes: default_smooth_splashes(),
splash: default_splash(),
noise_gate_db: default_noise_gate_db(), noise_gate_db: default_noise_gate_db(),
} }
} }
@ -240,6 +263,7 @@ pub enum Message {
ToggleImmersive, ToggleImmersive,
ToggleChrome, ToggleChrome,
ToggleSettings, ToggleSettings,
ToggleAdvanced,
SetPlaybackMode(PlaybackMode), SetPlaybackMode(PlaybackMode),
OpenLocalMode, OpenLocalMode,
OpenCaptureMode, OpenCaptureMode,
@ -267,6 +291,8 @@ pub enum Message {
SetGpuBlend(f32), SetGpuBlend(f32),
SetSmoothingTilt(f32), SetSmoothingTilt(f32),
SetSmoothingStrength(f32), SetSmoothingStrength(f32),
SetSmoothSplashes(bool),
SetSplash(f32),
SetNoiseGate(f32), SetNoiseGate(f32),
SetOutputDevice(Option<String>), SetOutputDevice(Option<String>),
SetInputDevice(Option<String>), SetInputDevice(Option<String>),
@ -326,6 +352,7 @@ impl App {
worker.set_gpu_blend(settings.gpu_blend); worker.set_gpu_blend(settings.gpu_blend);
worker.set_smoothing_tilt(settings.smoothing_tilt); worker.set_smoothing_tilt(settings.smoothing_tilt);
worker.set_smoothing_strength(settings.smoothing_strength); worker.set_smoothing_strength(settings.smoothing_strength);
worker.set_smooth_splashes(settings.smooth_splashes);
worker.set_noise_gate(settings.noise_gate_db); worker.set_noise_gate(settings.noise_gate_db);
let library_worker = LibraryWorker::spawn(); let library_worker = LibraryWorker::spawn();
@ -353,6 +380,7 @@ impl App {
settings, settings,
settings_inactive, settings_inactive,
show_settings: false, show_settings: false,
show_advanced: false,
pending_pick: 0, pending_pick: 0,
pending_capture_action: 0, pending_capture_action: 0,
pending_pip_request: false, pending_pip_request: false,
@ -409,6 +437,7 @@ impl App {
self.worker.set_gpu_blend(self.settings.gpu_blend); self.worker.set_gpu_blend(self.settings.gpu_blend);
self.worker.set_smoothing_tilt(self.settings.smoothing_tilt); self.worker.set_smoothing_tilt(self.settings.smoothing_tilt);
self.worker.set_smoothing_strength(self.settings.smoothing_strength); self.worker.set_smoothing_strength(self.settings.smoothing_strength);
self.worker.set_smooth_splashes(self.settings.smooth_splashes);
self.worker.set_noise_gate(self.settings.noise_gate_db); self.worker.set_noise_gate(self.settings.noise_gate_db);
} }
@ -908,6 +937,8 @@ impl App {
| Message::SetGpuBlend(_) | Message::SetGpuBlend(_)
| Message::SetSmoothingTilt(_) | Message::SetSmoothingTilt(_)
| Message::SetSmoothingStrength(_) | Message::SetSmoothingStrength(_)
| Message::SetSmoothSplashes(_)
| Message::SetSplash(_)
| Message::SetNoiseGate(_) | Message::SetNoiseGate(_)
| Message::ResetSettings, | Message::ResetSettings,
); );
@ -1024,6 +1055,9 @@ impl App {
self.restore_settings_scroll = true; self.restore_settings_scroll = true;
} }
} }
Message::ToggleAdvanced => {
self.show_advanced = !self.show_advanced;
}
Message::SetPlaybackMode(mode) => self.set_playback_mode(mode), Message::SetPlaybackMode(mode) => self.set_playback_mode(mode),
Message::OpenLocalMode => { Message::OpenLocalMode => {
self.set_playback_mode(PlaybackMode::Local); self.set_playback_mode(PlaybackMode::Local);
@ -1102,6 +1136,13 @@ impl App {
self.settings.smoothing_strength = v.clamp(0.0, 1.0); self.settings.smoothing_strength = v.clamp(0.0, 1.0);
self.worker.set_smoothing_strength(self.settings.smoothing_strength); self.worker.set_smoothing_strength(self.settings.smoothing_strength);
} }
Message::SetSmoothSplashes(on) => {
self.settings.smooth_splashes = on;
self.worker.set_smooth_splashes(on);
}
Message::SetSplash(v) => {
self.settings.splash = v.clamp(0.0, 8.0);
}
Message::SetNoiseGate(v) => { Message::SetNoiseGate(v) => {
self.settings.noise_gate_db = v.clamp(-100.0, 0.0); self.settings.noise_gate_db = v.clamp(-100.0, 0.0);
self.worker.set_noise_gate(self.settings.noise_gate_db); self.worker.set_noise_gate(self.settings.noise_gate_db);

View File

@ -406,6 +406,7 @@ fn params_from(s: &super::app::Settings) -> VizParams {
hue: s.hue, hue: s.hue,
contrast: s.contrast, contrast: s.contrast,
brightness: s.brightness, brightness: s.brightness,
splash: s.splash,
} }
} }
@ -431,13 +432,13 @@ fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Rendere
let body = column![ let body = column![
Space::new().height(Length::Fixed(TOP_BAR_H + 4.0)), Space::new().height(Length::Fixed(TOP_BAR_H + 4.0)),
header, header,
Space::new().height(Length::Fixed(10.0)), Space::new().height(Length::Fixed(12.0)),
section_label("style"), section_label("style"),
toggle_row("glass", s.glass, Message::SetGlass), toggle_row("glass", s.glass, Message::SetGlass),
toggle_row("album colors", s.album_colors, Message::SetAlbumColors), toggle_row("album colors", s.album_colors, Message::SetAlbumColors),
toggle_row("mirrored", s.mirrored, Message::SetMirrored), toggle_row("mirrored", s.mirrored, Message::SetMirrored),
toggle_row("inverted", s.inverted, Message::SetInverted), toggle_row("inverted", s.inverted, Message::SetInverted),
Space::new().height(Length::Fixed(8.0)), Space::new().height(Length::Fixed(12.0)),
section_label("color"), section_label("color"),
slider_row( slider_row(
"hue", "hue",
@ -463,18 +464,16 @@ fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Rendere
format!("{:.2}", s.brightness), format!("{:.2}", s.brightness),
Message::SetBrightness, Message::SetBrightness,
), ),
Space::new().height(Length::Fixed(8.0)),
section_label("entropy filter"),
toggle_row("enabled", s.entropy_on, Message::SetEntropy),
slider_row( slider_row(
"strength", "splash",
s.entropy_strength, s.splash,
-1.5..=1.5, 0.0..=8.0,
0.05, 0.1,
format!("{:+.2}", s.entropy_strength), format!("{:.1}", s.splash),
Message::SetEntropyStrength, Message::SetSplash,
), ),
Space::new().height(Length::Fixed(8.0)), toggle_row("smooth splashes", s.smooth_splashes, Message::SetSmoothSplashes),
Space::new().height(Length::Fixed(12.0)),
section_label("dsp"), section_label("dsp"),
slider_row( slider_row(
"bins", "bins",
@ -484,7 +483,6 @@ fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Rendere
format!("{}", s.num_bins), format!("{}", s.num_bins),
|v| Message::SetNumBins(v as u32), |v| Message::SetNumBins(v as u32),
), ),
pow2_slider_row("fft", s.fft, 9, 16, Message::SetFft), pow2_slider_row("fft", s.fft, 9, 16, Message::SetFft),
pow2_slider_row( pow2_slider_row(
"hop", "hop",
@ -509,43 +507,7 @@ fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Rendere
format!("{:.2}", s.smoothing_strength), format!("{:.2}", s.smoothing_strength),
Message::SetSmoothingStrength, Message::SetSmoothingStrength,
), ),
Space::new().height(Length::Fixed(8.0)), Space::new().height(Length::Fixed(12.0)),
section_label("cepstral smoothing"),
slider_row(
"granularity",
s.granularity as f32,
1.0..=100.0,
1.0,
format!("{}", s.granularity),
|v| Message::SetGranularity(v as i32),
),
slider_row(
"detail",
s.detail as f32,
1.0..=100.0,
1.0,
format!("{}", s.detail),
|v| Message::SetDetail(v as i32),
),
slider_row(
"strength",
s.strength,
0.0..=1.0,
0.01,
format!("{:.2}", s.strength),
Message::SetStrength,
),
Space::new().height(Length::Fixed(8.0)),
section_label("fft engine blend"),
slider_row(
"cpu ↔ gpu",
s.gpu_blend,
0.0..=1.0,
0.01,
format!("{:.2}", s.gpu_blend),
Message::SetGpuBlend,
),
Space::new().height(Length::Fixed(8.0)),
section_label("capture noise gate"), section_label("capture noise gate"),
slider_row( slider_row(
"threshold", "threshold",
@ -556,13 +518,13 @@ fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Rendere
Message::SetNoiseGate, Message::SetNoiseGate,
), ),
] ]
.spacing(8) .spacing(10)
.padding(Padding::from(16)) .padding(Padding::from(16))
.width(Length::Fixed(SETTINGS_W)); .width(Length::Fixed(SETTINGS_W));
#[cfg(all(not(target_os = "ios"), not(target_os = "android")))] #[cfg(all(not(target_os = "ios"), not(target_os = "android")))]
let body = body let body = body
.push(Space::new().height(Length::Fixed(8.0))) .push(Space::new().height(Length::Fixed(12.0)))
.push(section_label("audio devices")) .push(section_label("audio devices"))
.push(device_row("output", &app.output_devices, app.output_device.as_deref(), Message::SetOutputDevice)) .push(device_row("output", &app.output_devices, app.output_device.as_deref(), Message::SetOutputDevice))
.push(device_row("input", &app.input_devices, app.input_device.as_deref(), Message::SetInputDevice)) .push(device_row("input", &app.input_devices, app.input_device.as_deref(), Message::SetInputDevice))
@ -571,6 +533,59 @@ fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Rendere
chip_button("Refresh devices", Message::RefreshDeviceList), chip_button("Refresh devices", Message::RefreshDeviceList),
].padding(Padding::from([4, 0]))); ].padding(Padding::from([4, 0])));
let mut body = body
.push(Space::new().height(Length::Fixed(14.0)))
.push(advanced_header(app.show_advanced));
if app.show_advanced {
body = body
.push(section_label("entropy filter"))
.push(toggle_row("enabled", s.entropy_on, Message::SetEntropy))
.push(slider_row(
"strength",
s.entropy_strength,
-1.5..=1.5,
0.05,
format!("{:+.2}", s.entropy_strength),
Message::SetEntropyStrength,
))
.push(Space::new().height(Length::Fixed(10.0)))
.push(section_label("cepstral smoothing"))
.push(slider_row(
"granularity",
s.granularity as f32,
1.0..=100.0,
1.0,
format!("{}", s.granularity),
|v| Message::SetGranularity(v as i32),
))
.push(slider_row(
"detail",
s.detail as f32,
1.0..=100.0,
1.0,
format!("{}", s.detail),
|v| Message::SetDetail(v as i32),
))
.push(slider_row(
"strength",
s.strength,
0.0..=1.0,
0.01,
format!("{:.2}", s.strength),
Message::SetStrength,
))
.push(Space::new().height(Length::Fixed(10.0)))
.push(section_label("fft engine blend"))
.push(slider_row(
"cpu / gpu",
s.gpu_blend,
0.0..=1.0,
0.01,
format!("{:.2}", s.gpu_blend),
Message::SetGpuBlend,
));
}
let scroll = scrollable(body) let scroll = scrollable(body)
.id(settings_scroll_id()) .id(settings_scroll_id())
.on_scroll(|vp| Message::SettingsScrolled(vp.absolute_offset())) .on_scroll(|vp| Message::SettingsScrolled(vp.absolute_offset()))
@ -610,16 +625,16 @@ fn slider_row<'a, F>(
where where
F: 'a + Fn(f32) -> Message, F: 'a + Fn(f32) -> Message,
{ {
let label_w = 110.0; let label_w = 120.0;
let value_w = 60.0; let value_w = 64.0;
container( container(
row![ row![
container(text(label).size(13).color(palette::text_dim())) container(text(label).size(16).color(palette::text_dim()))
.width(Length::Fixed(label_w)), .width(Length::Fixed(label_w)),
slider(range, value, on_change).step(step).width(Length::Fill).height(28.0), slider(range, value, on_change).step(step).width(Length::Fill).height(38.0),
container( container(
text(value_text) text(value_text)
.size(13) .size(16)
.color(palette::text()) .color(palette::text())
.align_x(iced_wgpu::core::alignment::Horizontal::Right) .align_x(iced_wgpu::core::alignment::Horizontal::Right)
) )
@ -629,7 +644,7 @@ where
.spacing(12) .spacing(12)
.align_y(iced_wgpu::core::Alignment::Center) .align_y(iced_wgpu::core::Alignment::Center)
) )
.height(Length::Fixed(44.0)) .height(Length::Fixed(54.0))
.into() .into()
} }
@ -729,24 +744,55 @@ where
{ {
container( container(
row![ row![
container(text(label).size(13).color(palette::text_dim())) container(text(label).size(16).color(palette::text_dim()))
.width(Length::Fixed(110.0)), .width(Length::Fixed(120.0)),
checkbox(value).on_toggle(on_change).size(26), checkbox(value).on_toggle(on_change).size(32),
] ]
.spacing(12) .spacing(12)
.align_y(iced_wgpu::core::Alignment::Center) .align_y(iced_wgpu::core::Alignment::Center)
) )
.height(Length::Fixed(40.0)) .height(Length::Fixed(50.0))
.into() .into()
} }
/// dim small-caps heading separating groups of settings rows. /// dim small-caps heading separating groups of settings rows.
fn section_label<'a>(label: &'a str) -> Element<'a, Message, Theme, iced_wgpu::Renderer> { fn section_label<'a>(label: &'a str) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
container(text(label).size(10).color(palette::text_dim())) container(text(label).size(12).color(palette::text_dim()))
.padding(Padding::from([0, 0])) .padding(Padding::from([0, 0]))
.into() .into()
} }
/// full-width toggle button heading the collapsible advanced settings group.
fn advanced_header<'a>(expanded: bool) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
let caret = if expanded { "v" } else { ">" };
let inner = row![
text("advanced").size(15).color(palette::text()),
Space::new().width(Length::Fill),
text(caret).size(13).color(palette::text_dim()),
]
.align_y(iced_wgpu::core::Alignment::Center)
.padding(Padding::from([10, 12]));
iced_widget::button(inner)
.padding(0)
.width(Length::Fill)
.on_press(Message::ToggleAdvanced)
.style(|_t: &Theme, status: ButtonStatus| {
let a = if matches!(status, ButtonStatus::Hovered) { 0.10 } else { 0.05 };
button::Style {
background: Some(Background::Color(Color { a, ..palette::text() })),
text_color: palette::text(),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 8.0.into(),
},
..Default::default()
}
})
.into()
}
/// translucent backdrop styling for the settings overlay. /// translucent backdrop styling for the settings overlay.
fn settings_panel_style(_theme: &Theme) -> container::Style { fn settings_panel_style(_theme: &Theme) -> container::Style {
container::Style { container::Style {

View File

@ -657,6 +657,7 @@ fn pip_viz_params(s: &crate::ui::app::Settings) -> crate::visualizer::VizParams
hue: s.hue, hue: s.hue,
contrast: s.contrast, contrast: s.contrast,
brightness: s.brightness, brightness: s.brightness,
splash: s.splash,
} }
} }

View File

@ -27,6 +27,7 @@ pub struct VizParams {
pub hue: f32, pub hue: f32,
pub contrast: f32, pub contrast: f32,
pub brightness: f32, pub brightness: f32,
pub splash: f32,
} }
impl Default for VizParams { impl Default for VizParams {
@ -41,6 +42,7 @@ impl Default for VizParams {
hue: 0.9, hue: 0.9,
contrast: 1.0, contrast: 1.0,
brightness: 1.0, brightness: 1.0,
splash: 1.0,
} }
} }
} }

View File

@ -17,6 +17,7 @@ pub struct BinGpu {
pub hue: f32, pub hue: f32,
pub sat: f32, pub sat: f32,
pub val: f32, pub val: f32,
pub splash: f32,
} }
/// uniform block holding viewport size, layout counts, render flags, and the unified glass color. /// uniform block holding viewport size, layout counts, render flags, and the unified glass color.

View File

@ -10,6 +10,9 @@ use crate::visualizer::VizParams;
const HUE_HISTORY_LEN: usize = 40; const HUE_HISTORY_LEN: usize = 40;
const HISTORY_LEN: usize = 30; const HISTORY_LEN: usize = 30;
/// splash-to-jump response gain, reaching the full offset on a moderate splash.
const SPLASH_GAIN: f32 = 4.0;
/// per-bin smoothed magnitude, modulation offsets, palette color, and recent visual history. /// per-bin smoothed magnitude, modulation offsets, palette color, and recent visual history.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct BinState { pub struct BinState {
@ -19,6 +22,7 @@ pub struct BinState {
pub bright_mod: f32, pub bright_mod: f32,
pub alpha_mod: f32, pub alpha_mod: f32,
pub cached_color: [f32; 3], pub cached_color: [f32; 3],
pub splash: f32,
pub history: VecDeque<f32>, pub history: VecDeque<f32>,
} }
@ -108,6 +112,7 @@ impl VisState {
hue: b.cached_color[0], hue: b.cached_color[0],
sat: b.cached_color[1], sat: b.cached_color[1],
val: b.cached_color[2], val: b.cached_color[2],
splash: b.splash,
}); });
} }
} }
@ -309,6 +314,24 @@ fn ingest_channel(
} }
} }
// spreads each bin's suppressed-energy activity into a 2-bin-wide paint splash via a 0.5/1.0/0.5 kernel.
let mut splash = vec![0.0_f32; n];
if frame.activity.len() == n {
for i in 0..n {
let a = frame.activity[i];
if a <= 0.0 {
continue;
}
splash[i] += a;
if i > 0 {
splash[i - 1] += a * 0.5;
}
if i + 1 < n {
splash[i + 1] += a * 0.5;
}
}
}
let use_palette = params.album_colors let use_palette = params.album_colors
&& palette && palette
.map(|p| !p.is_empty()) .map(|p| !p.is_empty())
@ -339,6 +362,8 @@ fn ingest_channel(
} }
b.cached_color = [hue, 1.0, 1.0]; b.cached_color = [hue, 1.0, 1.0];
} }
// per-bin splash amount, applied as a hue/sat/value shift on the gpu for both fills and lines.
b.splash = (splash[i] * SPLASH_GAIN * params.splash).min(1.0);
} }
} }