Lots of bugfixes; New Smoothing function for softer dynamics which remain responsive but are less prone to jitter.
This commit is contained in:
parent
43d65d751a
commit
66ec445fdd
|
|
@ -194,6 +194,30 @@ impl Analyzer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// updates the per-bin smoothing frequency-tilt on every processor.
|
||||||
|
pub fn set_smoothing_tilt(&mut self, tilt: f64) {
|
||||||
|
for p in self
|
||||||
|
.main
|
||||||
|
.iter_mut()
|
||||||
|
.chain(self.transient.iter_mut())
|
||||||
|
.chain(self.deep.iter_mut())
|
||||||
|
{
|
||||||
|
p.set_smoothing_tilt(tilt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// updates the per-bin smoothing strength on every processor.
|
||||||
|
pub fn set_smoothing_strength(&mut self, strength: f64) {
|
||||||
|
for p in self
|
||||||
|
.main
|
||||||
|
.iter_mut()
|
||||||
|
.chain(self.transient.iter_mut())
|
||||||
|
.chain(self.deep.iter_mut())
|
||||||
|
{
|
||||||
|
p.set_smoothing_strength(strength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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();
|
||||||
|
|
@ -250,7 +274,7 @@ impl Analyzer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// appends interleaved PCM into the live-mode buffer with stereo conversion. propagates sample-rate changes and caps the buffer length.
|
/// appends stereo-converted PCM into the bounded live-mode buffer, tracking the incoming sample rate.
|
||||||
pub fn push_live_pcm(&mut self, samples: &[f32], sample_rate: u32, channels: u32) {
|
pub fn push_live_pcm(&mut self, samples: &[f32], sample_rate: u32, channels: u32) {
|
||||||
if samples.is_empty() || channels == 0 {
|
if samples.is_empty() || channels == 0 {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ enum Cmd {
|
||||||
SetNumBins(usize),
|
SetNumBins(usize),
|
||||||
SetSmoothing { granularity: i32, detail: i32, strength: f32 },
|
SetSmoothing { granularity: i32, detail: i32, strength: f32 },
|
||||||
SetGpuBlend(f32),
|
SetGpuBlend(f32),
|
||||||
|
SetSmoothingTilt(f32),
|
||||||
|
SetSmoothingStrength(f32),
|
||||||
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 },
|
||||||
|
|
@ -110,6 +112,16 @@ impl AnalyzerWorker {
|
||||||
let _ = self.cmd_tx.send(Cmd::SetGpuBlend(blend));
|
let _ = self.cmd_tx.send(Cmd::SetGpuBlend(blend));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// queues a smoothing-tilt change.
|
||||||
|
pub fn set_smoothing_tilt(&self, tilt: f32) {
|
||||||
|
let _ = self.cmd_tx.send(Cmd::SetSmoothingTilt(tilt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// queues a smoothing-strength change.
|
||||||
|
pub fn set_smoothing_strength(&self, strength: f32) {
|
||||||
|
let _ = self.cmd_tx.send(Cmd::SetSmoothingStrength(strength));
|
||||||
|
}
|
||||||
|
|
||||||
/// 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));
|
||||||
|
|
@ -253,6 +265,8 @@ fn apply(analyzer: &mut Analyzer, cmd: Cmd) {
|
||||||
strength,
|
strength,
|
||||||
} => analyzer.set_smoothing_params(granularity, detail, strength),
|
} => analyzer.set_smoothing_params(granularity, detail, strength),
|
||||||
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::SetSmoothingStrength(s) => analyzer.set_smoothing_strength(s as f64),
|
||||||
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 => {}
|
||||||
|
|
|
||||||
|
|
@ -537,7 +537,7 @@ fn redirect_stdio_to_logcat() {
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// opens the AAudio mic input stream and stores the handle on the AndroidViewport. no-op if a stream is already running.
|
/// opens the AAudio mic input stream and stores the handle on the AndroidViewport, idempotent against an already-running stream.
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportStartMicCapture<'a>(
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportStartMicCapture<'a>(
|
||||||
_env: JNIEnv<'a>,
|
_env: JNIEnv<'a>,
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ impl AudioEngine {
|
||||||
Self::with_output_device(None)
|
Self::with_output_device(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// opens an output device by name (system default when name is None), builds the f32 stream, and starts the audio thread paused.
|
/// opens a named output device or the system default, building a paused f32 stream against a started audio thread.
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn with_output_device(name: Option<&str>) -> Result<Self, String> {
|
pub fn with_output_device(name: Option<&str>) -> Result<Self, String> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ struct Args {
|
||||||
|
|
||||||
const ARGS_BYTES: u64 = std::mem::size_of::<Args>() as u64;
|
const ARGS_BYTES: u64 = std::mem::size_of::<Args>() as u64;
|
||||||
|
|
||||||
/// pipelined slot count. two slots sustain the one-frame lag pattern without barrier-serialization on a shared scratch buffer.
|
/// pipelined slot count sustaining the one-frame lag pattern without scratch-buffer barrier-serialization.
|
||||||
const NUM_SLOTS: usize = 2;
|
const NUM_SLOTS: usize = 2;
|
||||||
|
|
||||||
/// per-slot gpu resources owned exclusively by one pending submission.
|
/// per-slot gpu resources owned exclusively by one pending submission.
|
||||||
|
|
@ -298,7 +298,7 @@ impl GpuFft1D {
|
||||||
self.cached_inverse.set(Some(inverse));
|
self.cached_inverse.set(Some(inverse));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// uploads input to the next slot, dispatches bit-reversal and log2(N) butterfly passes, queues the readback copy, and records the submission index in the pending FIFO.
|
/// dispatches one bit-reversal and log2(N) butterfly transform of the input into the next pipelined slot.
|
||||||
fn submit(&mut self, input: &[Complex64], inverse: bool) {
|
fn submit(&mut self, input: &[Complex64], inverse: bool) {
|
||||||
debug_assert_eq!(input.len(), self.n as usize);
|
debug_assert_eq!(input.len(), self.n as usize);
|
||||||
self.ensure_all_args(inverse);
|
self.ensure_all_args(inverse);
|
||||||
|
|
|
||||||
145
src/processor.rs
145
src/processor.rs
|
|
@ -18,6 +18,7 @@ pub struct Spectrum {
|
||||||
pub cepstrum: Vec<f32>,
|
pub cepstrum: Vec<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// single-channel windowed-fft pipeline with optional gpu offload, log-spaced binning, and cepstral envelope blending.
|
/// single-channel windowed-fft pipeline with optional gpu offload, log-spaced binning, and cepstral envelope blending.
|
||||||
pub struct Processor {
|
pub struct Processor {
|
||||||
frame_size: usize,
|
frame_size: usize,
|
||||||
|
|
@ -41,6 +42,12 @@ pub struct Processor {
|
||||||
freqs_const: Vec<f64>,
|
freqs_const: Vec<f64>,
|
||||||
num_bins: usize,
|
num_bins: usize,
|
||||||
|
|
||||||
|
bin_alphas: Vec<f64>,
|
||||||
|
bin_smoothed: Vec<f64>,
|
||||||
|
bin_initialized: bool,
|
||||||
|
smoothing_tilt: f64,
|
||||||
|
smoothing_strength: f64,
|
||||||
|
|
||||||
history: VecDeque<Vec<f64>>,
|
history: VecDeque<Vec<f64>>,
|
||||||
smoothing_length: usize,
|
smoothing_length: usize,
|
||||||
|
|
||||||
|
|
@ -53,6 +60,7 @@ pub struct Processor {
|
||||||
detail: i32,
|
detail: i32,
|
||||||
cepstral_strength: f32,
|
cepstral_strength: f32,
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
weave_caps: Option<weave::Caps>,
|
weave_caps: Option<weave::Caps>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +81,11 @@ impl Processor {
|
||||||
buffer: Vec::new(),
|
buffer: Vec::new(),
|
||||||
sample_freqs: Vec::new(),
|
sample_freqs: Vec::new(),
|
||||||
freqs_const: Vec::new(),
|
freqs_const: Vec::new(),
|
||||||
|
bin_alphas: Vec::new(),
|
||||||
|
bin_smoothed: Vec::new(),
|
||||||
|
bin_initialized: false,
|
||||||
|
smoothing_tilt: DEFAULT_SMOOTHING_TILT,
|
||||||
|
smoothing_strength: DEFAULT_SMOOTHING_STRENGTH,
|
||||||
num_bins: 26,
|
num_bins: 26,
|
||||||
history: VecDeque::new(),
|
history: VecDeque::new(),
|
||||||
smoothing_length: 3,
|
smoothing_length: 3,
|
||||||
|
|
@ -94,6 +107,18 @@ impl Processor {
|
||||||
self.sample_rate = rate;
|
self.sample_rate = rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// updates the per-bin smoothing alpha curve's frequency-tilt.
|
||||||
|
pub fn set_smoothing_tilt(&mut self, tilt: f64) {
|
||||||
|
self.smoothing_tilt = tilt.clamp(0.0, 1.0);
|
||||||
|
self.rebuild_smoothing_alphas();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// updates the per-bin smoothing mix between raw FFT and smoothed values.
|
||||||
|
pub fn set_smoothing_strength(&mut self, strength: f64) {
|
||||||
|
self.smoothing_strength = strength.clamp(0.0, 1.0);
|
||||||
|
self.rebuild_smoothing_alphas();
|
||||||
|
}
|
||||||
|
|
||||||
/// 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);
|
||||||
|
|
@ -131,7 +156,7 @@ impl Processor {
|
||||||
self.rebuild_bins();
|
self.rebuild_bins();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// rebuilds the FFT sample frequencies and the log-spaced display centers.
|
/// rebuilds the log-spaced display bin centers from 40Hz to 11kHz with a 10Hz sentinel prepended.
|
||||||
fn rebuild_bins(&mut self) {
|
fn rebuild_bins(&mut self) {
|
||||||
self.sample_freqs.clear();
|
self.sample_freqs.clear();
|
||||||
self.freqs_const.clear();
|
self.freqs_const.clear();
|
||||||
|
|
@ -140,25 +165,41 @@ impl Processor {
|
||||||
let n = self.num_bins.max(1);
|
let n = self.num_bins.max(1);
|
||||||
let min_freq = 40.0_f64;
|
let min_freq = 40.0_f64;
|
||||||
let max_freq = 11_000.0_f64;
|
let max_freq = 11_000.0_f64;
|
||||||
let linear = self.frame_size > 0 && self.frame_size <= SMALL_FFT_THRESHOLD;
|
|
||||||
|
|
||||||
let mut sample_edges: Vec<f64> = Vec::with_capacity(n + 1);
|
let mut edges: Vec<f64> = Vec::with_capacity(n + 1);
|
||||||
let mut display_edges: Vec<f64> = Vec::with_capacity(n + 1);
|
|
||||||
for i in 0..=n {
|
for i in 0..=n {
|
||||||
let t = i as f64 / n as f64;
|
let t = i as f64 / n as f64;
|
||||||
sample_edges.push(if linear {
|
edges.push(min_freq * (max_freq / min_freq).powf(t));
|
||||||
min_freq + (max_freq - min_freq) * t
|
|
||||||
} else {
|
|
||||||
min_freq * (max_freq / min_freq).powf(t)
|
|
||||||
});
|
|
||||||
display_edges.push(min_freq * (max_freq / min_freq).powf(t));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sample_freqs.push(10.0);
|
self.sample_freqs.push(10.0);
|
||||||
self.freqs_const.push(10.0);
|
self.freqs_const.push(10.0);
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
self.sample_freqs.push((sample_edges[i] + sample_edges[i + 1]) / 2.0);
|
let center = (edges[i] + edges[i + 1]) / 2.0;
|
||||||
self.freqs_const.push((display_edges[i] + display_edges[i + 1]) / 2.0);
|
self.sample_freqs.push(center);
|
||||||
|
self.freqs_const.push(center);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rebuild_smoothing_alphas();
|
||||||
|
self.bin_smoothed = vec![-100.0; self.freqs_const.len()];
|
||||||
|
self.bin_initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// rebuilds per-bin one-pole smoothing alpha values from tilt and strength.
|
||||||
|
fn rebuild_smoothing_alphas(&mut self) {
|
||||||
|
let tilt = self.smoothing_tilt.clamp(0.0, 1.0);
|
||||||
|
let strength = self.smoothing_strength.clamp(0.0, 1.0);
|
||||||
|
let f_low = 40.0_f64.ln();
|
||||||
|
let f_high = 11_000.0_f64.ln();
|
||||||
|
let span = f_high - f_low;
|
||||||
|
self.bin_alphas.clear();
|
||||||
|
self.bin_alphas.reserve(self.freqs_const.len());
|
||||||
|
for &fc in self.freqs_const.iter() {
|
||||||
|
let f_norm = ((fc.max(1.0).ln() - f_low) / span).clamp(0.0, 1.0);
|
||||||
|
let curve = 0.5 + 0.45 * tilt * (2.0 * f_norm - 1.0);
|
||||||
|
let smooth_alpha = curve.clamp(0.05, 0.95);
|
||||||
|
let effective = 1.0 - strength * (1.0 - smooth_alpha);
|
||||||
|
self.bin_alphas.push(effective);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,8 +318,8 @@ impl Processor {
|
||||||
} else if freq == 0.0 {
|
} else if freq == 0.0 {
|
||||||
mag = 0.0;
|
mag = 0.0;
|
||||||
}
|
}
|
||||||
mag_full[i] = mag;
|
|
||||||
freqs_full[i] = freq;
|
freqs_full[i] = freq;
|
||||||
|
mag_full[i] = mag;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cep_buf: Vec<Complex64> = Vec::with_capacity(n);
|
let mut cep_buf: Vec<Complex64> = Vec::with_capacity(n);
|
||||||
|
|
@ -298,23 +339,22 @@ 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.cepstral_strength > 0.0 {
|
if self.bin_smoothed.len() != self.freqs_const.len() {
|
||||||
let envelope = weave::idealize_curve(
|
self.bin_smoothed = vec![-100.0; self.freqs_const.len()];
|
||||||
&mag_full,
|
self.bin_initialized = false;
|
||||||
self.granularity,
|
|
||||||
self.detail,
|
|
||||||
self.weave_caps,
|
|
||||||
);
|
|
||||||
let s = self.cepstral_strength as f64;
|
|
||||||
for (m, e) in mag_full.iter_mut().zip(envelope.iter()) {
|
|
||||||
*m = *m * (1.0 - s) + e * s;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let mut current_db = vec![0.0_f64; self.freqs_const.len()];
|
let mut current_db = vec![0.0_f64; self.freqs_const.len()];
|
||||||
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 mut val = 20.0 * mag.max(1e-12).log10();
|
let raw = 20.0 * mag.max(1e-12).log10();
|
||||||
|
let smoothed = if !self.bin_initialized {
|
||||||
|
raw
|
||||||
|
} else {
|
||||||
|
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] = smoothed;
|
||||||
|
let mut val = smoothed;
|
||||||
if (self.expand_ratio - 1.0).abs() > f32::EPSILON {
|
if (self.expand_ratio - 1.0).abs() > f32::EPSILON {
|
||||||
let t = self.expand_threshold as f64;
|
let t = self.expand_threshold as f64;
|
||||||
let r = self.expand_ratio as f64;
|
let r = self.expand_ratio as f64;
|
||||||
|
|
@ -325,6 +365,7 @@ impl Processor {
|
||||||
}
|
}
|
||||||
current_db[i] = val;
|
current_db[i] = val;
|
||||||
}
|
}
|
||||||
|
self.bin_initialized = true;
|
||||||
|
|
||||||
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 {
|
||||||
|
|
@ -359,10 +400,37 @@ impl Processor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// fft-size cutoff below which the processor switches to linear binning and a Hamming window.
|
/// linearly interpolates a value from the freqs/values table at the given target frequency with endpoint clamping.
|
||||||
|
fn lerp_at(freqs: &[f64], values: &[f64], target: f64) -> f64 {
|
||||||
|
if freqs.is_empty() {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
if target <= freqs[0] {
|
||||||
|
return values[0];
|
||||||
|
}
|
||||||
|
if target >= freqs[freqs.len() - 1] {
|
||||||
|
return values[values.len() - 1];
|
||||||
|
}
|
||||||
|
let upper = freqs.partition_point(|&f| f < target);
|
||||||
|
let lower = upper - 1;
|
||||||
|
let f0 = freqs[lower];
|
||||||
|
let f1 = freqs[upper];
|
||||||
|
let v0 = values[lower];
|
||||||
|
let v1 = values[upper];
|
||||||
|
let t = (target - f0) / (f1 - f0);
|
||||||
|
v0 + t * (v1 - v0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hamming/Blackman-Harris window cutoff in fft samples.
|
||||||
const SMALL_FFT_THRESHOLD: usize = 4096;
|
const SMALL_FFT_THRESHOLD: usize = 4096;
|
||||||
|
|
||||||
/// builds the analysis window: Hamming below the small-fft threshold for a narrower main lobe, Blackman-Harris above for cleaner sidelobes.
|
/// default per-bin smoothing alpha-curve frequency-tilt.
|
||||||
|
const DEFAULT_SMOOTHING_TILT: f64 = 0.6;
|
||||||
|
|
||||||
|
/// default per-bin smoothing mix between raw FFT and smoothed values.
|
||||||
|
const DEFAULT_SMOOTHING_STRENGTH: f64 = 0.7;
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
|
@ -387,27 +455,6 @@ fn build_window(size: usize) -> Vec<f64> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// linearly interpolates a value from the freqs/values table at the given target frequency with endpoint clamping.
|
|
||||||
fn lerp_at(freqs: &[f64], values: &[f64], target: f64) -> f64 {
|
|
||||||
if freqs.is_empty() {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
if target <= freqs[0] {
|
|
||||||
return values[0];
|
|
||||||
}
|
|
||||||
if target >= freqs[freqs.len() - 1] {
|
|
||||||
return values[values.len() - 1];
|
|
||||||
}
|
|
||||||
let upper = freqs.partition_point(|&f| f < target);
|
|
||||||
let lower = upper - 1;
|
|
||||||
let f0 = freqs[lower];
|
|
||||||
let f1 = freqs[upper];
|
|
||||||
let v0 = values[lower];
|
|
||||||
let v1 = values[upper];
|
|
||||||
let t = (target - f0) / (f1 - f0);
|
|
||||||
v0 + t * (v1 - v0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// chooses lighter idealisation caps on mobile targets and unlimited caps on desktop.
|
/// chooses lighter idealisation caps on mobile targets and unlimited caps on desktop.
|
||||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||||
fn default_caps() -> Option<weave::Caps> {
|
fn default_caps() -> Option<weave::Caps> {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ pub struct CaptureState {
|
||||||
/// foreign-session playback flag pushed by the host MediaController callback.
|
/// foreign-session playback flag pushed by the host MediaController callback.
|
||||||
pub playing: bool,
|
pub playing: bool,
|
||||||
|
|
||||||
/// flips true once any media session has reported metadata. drives the transport-enable state.
|
/// transport-enable latch flipped true the moment any media session reports metadata.
|
||||||
pub has_session: bool,
|
pub has_session: bool,
|
||||||
|
|
||||||
/// host-pushed flag tracking notification-listener access for now-playing metadata.
|
/// host-pushed flag tracking notification-listener access for now-playing metadata.
|
||||||
|
|
@ -165,6 +165,14 @@ pub struct Settings {
|
||||||
/// fraction of FFT work routed to the GPU pipeline, blended against the CPU result.
|
/// fraction of FFT work routed to the GPU pipeline, blended against the CPU result.
|
||||||
pub gpu_blend: f32,
|
pub gpu_blend: f32,
|
||||||
|
|
||||||
|
/// per-bin smoothing alpha-curve frequency-tilt.
|
||||||
|
#[serde(default = "default_smoothing_tilt")]
|
||||||
|
pub smoothing_tilt: f32,
|
||||||
|
|
||||||
|
/// per-bin smoothing mix between raw FFT and smoothed values.
|
||||||
|
#[serde(default = "default_smoothing_strength")]
|
||||||
|
pub smoothing_strength: 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,
|
||||||
|
|
@ -175,6 +183,16 @@ fn default_noise_gate_db() -> f32 {
|
||||||
-60.0
|
-60.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// default per-bin smoothing tilt.
|
||||||
|
fn default_smoothing_tilt() -> f32 {
|
||||||
|
0.6
|
||||||
|
}
|
||||||
|
|
||||||
|
/// default per-bin smoothing strength.
|
||||||
|
fn default_smoothing_strength() -> f32 {
|
||||||
|
0.7
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -194,6 +212,8 @@ impl Default for Settings {
|
||||||
detail: 50,
|
detail: 50,
|
||||||
strength: 0.0,
|
strength: 0.0,
|
||||||
gpu_blend: 0.7,
|
gpu_blend: 0.7,
|
||||||
|
smoothing_tilt: default_smoothing_tilt(),
|
||||||
|
smoothing_strength: default_smoothing_strength(),
|
||||||
noise_gate_db: default_noise_gate_db(),
|
noise_gate_db: default_noise_gate_db(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,6 +265,8 @@ pub enum Message {
|
||||||
SetDetail(i32),
|
SetDetail(i32),
|
||||||
SetStrength(f32),
|
SetStrength(f32),
|
||||||
SetGpuBlend(f32),
|
SetGpuBlend(f32),
|
||||||
|
SetSmoothingTilt(f32),
|
||||||
|
SetSmoothingStrength(f32),
|
||||||
SetNoiseGate(f32),
|
SetNoiseGate(f32),
|
||||||
SetOutputDevice(Option<String>),
|
SetOutputDevice(Option<String>),
|
||||||
SetInputDevice(Option<String>),
|
SetInputDevice(Option<String>),
|
||||||
|
|
@ -302,6 +324,8 @@ impl App {
|
||||||
worker.set_dsp_params(settings.fft as usize, settings.hop as usize);
|
worker.set_dsp_params(settings.fft as usize, settings.hop as usize);
|
||||||
worker.set_smoothing(settings.granularity, settings.detail, settings.strength);
|
worker.set_smoothing(settings.granularity, settings.detail, settings.strength);
|
||||||
worker.set_gpu_blend(settings.gpu_blend);
|
worker.set_gpu_blend(settings.gpu_blend);
|
||||||
|
worker.set_smoothing_tilt(settings.smoothing_tilt);
|
||||||
|
worker.set_smoothing_strength(settings.smoothing_strength);
|
||||||
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();
|
||||||
|
|
||||||
|
|
@ -354,7 +378,7 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// sets the top-level playback mode. swaps the active/inactive settings slots and re-applies the worker config when the mode actually changes.
|
/// sets the top-level playback mode, swapping the per-mode settings slots and re-applying worker config on an actual change.
|
||||||
pub fn set_playback_mode(&mut self, mode: PlaybackMode) {
|
pub fn set_playback_mode(&mut self, mode: PlaybackMode) {
|
||||||
let prev = self.playback_mode;
|
let prev = self.playback_mode;
|
||||||
self.playback_mode = mode;
|
self.playback_mode = mode;
|
||||||
|
|
@ -383,6 +407,8 @@ impl App {
|
||||||
self.settings.strength,
|
self.settings.strength,
|
||||||
);
|
);
|
||||||
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_strength(self.settings.smoothing_strength);
|
||||||
self.worker.set_noise_gate(self.settings.noise_gate_db);
|
self.worker.set_noise_gate(self.settings.noise_gate_db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -565,7 +591,7 @@ impl App {
|
||||||
self.show_settings && x >= viewport_width - player::SETTINGS_W && x <= viewport_width
|
self.show_settings && x >= viewport_width - player::SETTINGS_W && x <= viewport_width
|
||||||
}
|
}
|
||||||
|
|
||||||
/// scans a folder, replaces the library, queues art, and starts decoding the first track.
|
/// replaces the library from a folder scan and begins decoding the leading track.
|
||||||
fn apply_picked_folder(&mut self, folder: PathBuf) {
|
fn apply_picked_folder(&mut self, folder: PathBuf) {
|
||||||
#[cfg(all(target_os = "ios", debug_assertions))]
|
#[cfg(all(target_os = "ios", debug_assertions))]
|
||||||
eprintln!("[Rust.dbg] apply_picked_folder enter path={}", folder.display());
|
eprintln!("[Rust.dbg] apply_picked_folder enter path={}", folder.display());
|
||||||
|
|
@ -880,6 +906,8 @@ impl App {
|
||||||
| Message::SetDetail(_)
|
| Message::SetDetail(_)
|
||||||
| Message::SetStrength(_)
|
| Message::SetStrength(_)
|
||||||
| Message::SetGpuBlend(_)
|
| Message::SetGpuBlend(_)
|
||||||
|
| Message::SetSmoothingTilt(_)
|
||||||
|
| Message::SetSmoothingStrength(_)
|
||||||
| Message::SetNoiseGate(_)
|
| Message::SetNoiseGate(_)
|
||||||
| Message::ResetSettings,
|
| Message::ResetSettings,
|
||||||
);
|
);
|
||||||
|
|
@ -1066,6 +1094,14 @@ impl App {
|
||||||
self.settings.gpu_blend = v.clamp(0.0, 1.0);
|
self.settings.gpu_blend = v.clamp(0.0, 1.0);
|
||||||
self.worker.set_gpu_blend(self.settings.gpu_blend);
|
self.worker.set_gpu_blend(self.settings.gpu_blend);
|
||||||
}
|
}
|
||||||
|
Message::SetSmoothingTilt(v) => {
|
||||||
|
self.settings.smoothing_tilt = v.clamp(0.0, 1.0);
|
||||||
|
self.worker.set_smoothing_tilt(self.settings.smoothing_tilt);
|
||||||
|
}
|
||||||
|
Message::SetSmoothingStrength(v) => {
|
||||||
|
self.settings.smoothing_strength = v.clamp(0.0, 1.0);
|
||||||
|
self.worker.set_smoothing_strength(self.settings.smoothing_strength);
|
||||||
|
}
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -493,6 +493,22 @@ fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Rendere
|
||||||
(s.fft / 2).max(64).trailing_zeros(),
|
(s.fft / 2).max(64).trailing_zeros(),
|
||||||
Message::SetHop,
|
Message::SetHop,
|
||||||
),
|
),
|
||||||
|
slider_row(
|
||||||
|
"tilt",
|
||||||
|
s.smoothing_tilt,
|
||||||
|
0.0..=1.0,
|
||||||
|
0.01,
|
||||||
|
format!("{:.2}", s.smoothing_tilt),
|
||||||
|
Message::SetSmoothingTilt,
|
||||||
|
),
|
||||||
|
slider_row(
|
||||||
|
"strength",
|
||||||
|
s.smoothing_strength,
|
||||||
|
0.0..=1.0,
|
||||||
|
0.01,
|
||||||
|
format!("{:.2}", s.smoothing_strength),
|
||||||
|
Message::SetSmoothingStrength,
|
||||||
|
),
|
||||||
Space::new().height(Length::Fixed(8.0)),
|
Space::new().height(Length::Fixed(8.0)),
|
||||||
section_label("cepstral smoothing"),
|
section_label("cepstral smoothing"),
|
||||||
slider_row(
|
slider_row(
|
||||||
|
|
@ -747,7 +763,7 @@ fn settings_panel_style(_theme: &Theme) -> container::Style {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// bottom transport bar. file-mode renders the local skip/play/scrub set; capture mode renders the metadata strip with a PiP toggle.
|
/// bottom transport bar, branched between the file-mode skip/play/scrub set and the capture-mode metadata strip with PiP toggle.
|
||||||
fn transport(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
fn transport(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
||||||
let bar: Element<'_, Message, Theme, iced_wgpu::Renderer> = match app.playback_mode {
|
let bar: Element<'_, Message, Theme, iced_wgpu::Renderer> = match app.playback_mode {
|
||||||
PlaybackMode::Local => local_transport_bar(app),
|
PlaybackMode::Local => local_transport_bar(app),
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ pub struct ViewportHandle {
|
||||||
/// marks an active touch originating over the sidebar, routing vertical drags to wheel scrolls.
|
/// marks an active touch originating over the sidebar, routing vertical drags to wheel scrolls.
|
||||||
touch_in_sidebar: bool,
|
touch_in_sidebar: bool,
|
||||||
|
|
||||||
/// classification of an active touch over the settings panel: pending until SLOP, then tap/drag/scroll.
|
/// classification of an active touch over the settings panel into pending, tap, drag, or scroll.
|
||||||
settings_touch: SettingsTouch,
|
settings_touch: SettingsTouch,
|
||||||
pub state: App,
|
pub state: App,
|
||||||
|
|
||||||
|
|
@ -401,7 +401,7 @@ impl ViewportHandle {
|
||||||
self.state.take_pending_pip_request()
|
self.state.take_pending_pip_request()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns a cloned handle to the most recent analyzer frame pair for read-only host inspection (PiP mirror, debug).
|
/// returns a cloned handle to the most recent analyzer frame pair.
|
||||||
pub fn frame_data_snapshot(&self) -> std::sync::Arc<Vec<crate::analyzer::FrameData>> {
|
pub fn frame_data_snapshot(&self) -> std::sync::Arc<Vec<crate::analyzer::FrameData>> {
|
||||||
self.state.frame_data.clone()
|
self.state.frame_data.clone()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// drops empty placeholders into dist/ if the wasm-pack output is missing, so the server bin compiles standalone via cargo check.
|
// drops empty dist/ placeholders, keeping the server bin standalone-compilable.
|
||||||
// the real bytes get written by scripts/web/build.sh phase 1 (wasm-pack) right before phase 2 (cargo build) embeds them.
|
// real bytes populated by the scripts/web/build.sh wasm-pack stage.
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let dist = std::path::Path::new("dist");
|
let dist = std::path::Path::new("dist");
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ impl App {
|
||||||
self.surface.configure(&self.device, &self.config);
|
self.surface.configure(&self.device, &self.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// renders one frame: refreshes synthetic spectrum frames, ingests them into the pipeline state, uploads globals and bin packs, and dispatches the render pass.
|
/// renders one frame of the visualizer at the surface's current size.
|
||||||
fn render(&mut self) {
|
fn render(&mut self) {
|
||||||
let now = web_sys::window()
|
let now = web_sys::window()
|
||||||
.and_then(|w| w.performance())
|
.and_then(|w| w.performance())
|
||||||
|
|
@ -268,7 +268,7 @@ pub fn start_on_canvas(canvas: HtmlCanvasElement) -> Result<(), JsValue> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// constructs the wgpu instance, requests an adapter against the canvas, configures the surface, and builds the visualizer pipeline.
|
/// builds the App against the canvas, including the wgpu instance, adapter, surface configuration, and visualizer pipeline.
|
||||||
async fn build_app(
|
async fn build_app(
|
||||||
canvas: HtmlCanvasElement,
|
canvas: HtmlCanvasElement,
|
||||||
width: u32,
|
width: u32,
|
||||||
|
|
@ -333,7 +333,7 @@ async fn build_app(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// rAF-driven loop: the closure schedules its own re-fire after each render. publishes the App into APP_HANDLE so the resize export can reach it.
|
/// rAF-driven render loop publishing the App into APP_HANDLE for the resize export.
|
||||||
fn run_loop(app: App) {
|
fn run_loop(app: App) {
|
||||||
let app = Rc::new(RefCell::new(app));
|
let app = Rc::new(RefCell::new(app));
|
||||||
APP_HANDLE.with(|cell| {
|
APP_HANDLE.with(|cell| {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
'gourded': { fft: 16384, hop: 2048 },
|
'gourded': { fft: 16384, hop: 2048 },
|
||||||
'moron': { fft: 16384, hop: 2048 },
|
'moron': { fft: 16384, hop: 2048 },
|
||||||
'never give an angel a front': { fft: 8192, hop: 4096 },
|
'never give an angel a front': { fft: 8192, hop: 4096 },
|
||||||
"now you're speaking my language":{ fft: 16384, hop: 2048 },
|
'now youre speaking my language': { fft: 16384, hop: 2048 },
|
||||||
'ornery': { fft: 16384, hop: 2048 },
|
'ornery': { fft: 16384, hop: 2048 },
|
||||||
'quicksand': { fft: 16384, hop: 2048 },
|
'quicksand': { fft: 16384, hop: 2048 },
|
||||||
'stolen art': { fft: 8192, hop: 2048 },
|
'stolen art': { fft: 8192, hop: 2048 },
|
||||||
|
|
@ -185,6 +185,7 @@
|
||||||
const outBins = new Float32Array(NUM_BINS);
|
const outBins = new Float32Array(NUM_BINS);
|
||||||
|
|
||||||
let audioCtx = null;
|
let audioCtx = null;
|
||||||
|
let srcNode = null;
|
||||||
let analyser = null;
|
let analyser = null;
|
||||||
let gainNode = null;
|
let gainNode = null;
|
||||||
let fftBuf = null;
|
let fftBuf = null;
|
||||||
|
|
@ -199,13 +200,13 @@
|
||||||
const Ctor = window.AudioContext || window.webkitAudioContext;
|
const Ctor = window.AudioContext || window.webkitAudioContext;
|
||||||
if (!Ctor) return;
|
if (!Ctor) return;
|
||||||
audioCtx = new Ctor();
|
audioCtx = new Ctor();
|
||||||
const src = audioCtx.createMediaElementSource(audio);
|
srcNode = audioCtx.createMediaElementSource(audio);
|
||||||
analyser = audioCtx.createAnalyser();
|
analyser = audioCtx.createAnalyser();
|
||||||
analyser.fftSize = DEFAULT_FFT;
|
analyser.fftSize = DEFAULT_FFT;
|
||||||
analyser.smoothingTimeConstant = 0.2;
|
analyser.smoothingTimeConstant = 0.2;
|
||||||
gainNode = audioCtx.createGain();
|
gainNode = audioCtx.createGain();
|
||||||
gainNode.gain.value = isMuted ? 0 : 1;
|
gainNode.gain.value = isMuted ? 0 : 1;
|
||||||
src.connect(analyser);
|
srcNode.connect(analyser);
|
||||||
analyser.connect(gainNode);
|
analyser.connect(gainNode);
|
||||||
gainNode.connect(audioCtx.destination);
|
gainNode.connect(audioCtx.destination);
|
||||||
audio.muted = false;
|
audio.muted = false;
|
||||||
|
|
@ -254,13 +255,30 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// applies the per-track FFT and hop override, falling back to defaults on no match.
|
// applies the per-track FFT and hop override, falling back to defaults on no match.
|
||||||
|
function normalizeTrackName(s) {
|
||||||
|
return (s || '').toLowerCase().replace(/['"]/g, '').replace(/[_\-\s]+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
const TRACK_PARAMS_NORM = {};
|
||||||
|
for (const k of Object.keys(TRACK_PARAMS)) {
|
||||||
|
TRACK_PARAMS_NORM[normalizeTrackName(k)] = TRACK_PARAMS[k];
|
||||||
|
}
|
||||||
function applyTrackParams(name) {
|
function applyTrackParams(name) {
|
||||||
const key = (name || '').trim().toLowerCase();
|
const key = normalizeTrackName(name);
|
||||||
const params = TRACK_PARAMS[key] || { fft: DEFAULT_FFT, hop: DEFAULT_HOP };
|
const hit = TRACK_PARAMS_NORM[key];
|
||||||
|
const params = hit || { fft: DEFAULT_FFT, hop: DEFAULT_HOP };
|
||||||
|
console.log('[yrxtls] track', JSON.stringify(name), 'key', JSON.stringify(key), 'params', params, hit ? '(override)' : '(default)');
|
||||||
currentHop = params.hop;
|
currentHop = params.hop;
|
||||||
if (analyser && analyser.fftSize !== params.fft) {
|
if (analyser && srcNode && gainNode && analyser.fftSize !== params.fft) {
|
||||||
|
const smoothing = analyser.smoothingTimeConstant;
|
||||||
|
try { srcNode.disconnect(analyser); } catch (e) {}
|
||||||
|
try { analyser.disconnect(gainNode); } catch (e) {}
|
||||||
|
analyser = audioCtx.createAnalyser();
|
||||||
analyser.fftSize = params.fft;
|
analyser.fftSize = params.fft;
|
||||||
|
analyser.smoothingTimeConstant = smoothing;
|
||||||
|
srcNode.connect(analyser);
|
||||||
|
analyser.connect(gainNode);
|
||||||
fftBuf = new Float32Array(analyser.frequencyBinCount);
|
fftBuf = new Float32Array(analyser.frequencyBinCount);
|
||||||
|
console.log('[yrxtls] analyser recreated at fftSize', analyser.fftSize, 'bins', analyser.frequencyBinCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue