playing with visual effects: splash color
This commit is contained in:
parent
66ec445fdd
commit
3c079312dc
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 => {}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
172
src/ui/player.rs
172
src/ui/player.rs
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue