implemented real bpm analize
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
target/
|
target/
|
||||||
*.mp4
|
*.mp4
|
||||||
*.flac
|
*.flac
|
||||||
|
*.png
|
||||||
|
|||||||
+157
-3
@@ -56,6 +56,16 @@ const AGC_DECAY: f32 = 0.9992;
|
|||||||
const AGC_FLOOR: f32 = 1e-4;
|
const AGC_FLOOR: f32 = 1e-4;
|
||||||
// Onset: instantaneous attack on rising spectral flux, fast release -> a hit.
|
// Onset: instantaneous attack on rising spectral flux, fast release -> a hit.
|
||||||
const ONSET_RELEASE: f32 = 0.78;
|
const ONSET_RELEASE: f32 = 0.78;
|
||||||
|
// Autocorrelation tempo: window of broadband-flux history, beat-period search
|
||||||
|
// range, and confidence gates. Period search 0.34..0.95 s ~= 63..176 BPM;
|
||||||
|
// detected BPM is octave-folded into [BPM_FOLD_LO, BPM_FOLD_HI] (one octave).
|
||||||
|
const ACF_WIN_SECS: f32 = 3.0;
|
||||||
|
const ACF_PERIOD_LO: f32 = 0.34; // s/beat (fast end of the lag search)
|
||||||
|
const ACF_PERIOD_HI: f32 = 0.95; // s/beat (slow end of the lag search)
|
||||||
|
const ACF_CONF_MIN: f32 = 0.15; // below this the ACF peak is noise -> ignore
|
||||||
|
const ACF_SNAP: f32 = 0.30; // strong + wrong-octave IOI -> snap, don't glide
|
||||||
|
const BPM_FOLD_LO: f32 = 88.0;
|
||||||
|
const BPM_FOLD_HI: f32 = 176.0;
|
||||||
|
|
||||||
/// Per-band level (AGC-normalised, smoothed) + onset spike + rich descriptors.
|
/// Per-band level (AGC-normalised, smoothed) + onset spike + rich descriptors.
|
||||||
/// All scalar fields are 0..~1.
|
/// All scalar fields are 0..~1.
|
||||||
@@ -73,6 +83,13 @@ pub struct Bands {
|
|||||||
pub centroid: f32,
|
pub centroid: f32,
|
||||||
/// Broadband half-wave spectral flux onset -> global transient flashes.
|
/// Broadband half-wave spectral flux onset -> global transient flashes.
|
||||||
pub flux: f32,
|
pub flux: f32,
|
||||||
|
/// Rectified complex-domain onset (phase-deviation, AGC'd + spiked like
|
||||||
|
/// `flux`). Magnitude flux misses *soft* attacks (a pad swell, a new synth
|
||||||
|
/// note at steady level); a fresh note still disrupts per-bin phase
|
||||||
|
/// predictability, so this fires on tonal/harmonic onsets `flux` can't see.
|
||||||
|
/// Zero-latency chord-attack drive for chroma / harmonic-spoke visuals and
|
||||||
|
/// a sharper feed for breakcore's `Structure::novelty`.
|
||||||
|
pub csd: f32,
|
||||||
/// Overall loudness (mean magnitude), AGC-normalised -> lightness/scale.
|
/// Overall loudness (mean magnitude), AGC-normalised -> lightness/scale.
|
||||||
pub loud: f32,
|
pub loud: f32,
|
||||||
/// Short decaying pulse on a predicted beat (like an onset, but tempo-gated).
|
/// Short decaying pulse on a predicted beat (like an onset, but tempo-gated).
|
||||||
@@ -80,6 +97,11 @@ pub struct Bands {
|
|||||||
/// Sawtooth 0..1: fraction of the predicted beat interval elapsed. Lets
|
/// Sawtooth 0..1: fraction of the predicted beat interval elapsed. Lets
|
||||||
/// the visualiser *anticipate* the next hit instead of reacting late.
|
/// the visualiser *anticipate* the next hit instead of reacting late.
|
||||||
pub beat_phase: f32,
|
pub beat_phase: f32,
|
||||||
|
/// Autocorrelation-estimated dominant tempo (BPM), octave-folded into a
|
||||||
|
/// stable range; 0 until the ACF is confident. The predictive-IOI model
|
||||||
|
/// is *anchored* to this, so `beat_phase` no longer drifts an octave on
|
||||||
|
/// syncopated breakcore fills — sync pulses/dolly-punches to this grid.
|
||||||
|
pub bpm: f32,
|
||||||
/// Spectral flatness 0 (tonal/pad) .. 1 (noisy/break) -> smooth vs jagged.
|
/// Spectral flatness 0 (tonal/pad) .. 1 (noisy/break) -> smooth vs jagged.
|
||||||
pub flatness: f32,
|
pub flatness: f32,
|
||||||
/// Relative pitch-class energy (max-normalised) -> harmonic accent hues.
|
/// Relative pitch-class energy (max-normalised) -> harmonic accent hues.
|
||||||
@@ -101,9 +123,11 @@ impl Default for Bands {
|
|||||||
spec: [0.0; SPEC_N],
|
spec: [0.0; SPEC_N],
|
||||||
centroid: 0.0,
|
centroid: 0.0,
|
||||||
flux: 0.0,
|
flux: 0.0,
|
||||||
|
csd: 0.0,
|
||||||
loud: 0.0,
|
loud: 0.0,
|
||||||
beat: 0.0,
|
beat: 0.0,
|
||||||
beat_phase: 0.0,
|
beat_phase: 0.0,
|
||||||
|
bpm: 0.0,
|
||||||
flatness: 0.0,
|
flatness: 0.0,
|
||||||
chroma: [0.0; CHROMA_N],
|
chroma: [0.0; CHROMA_N],
|
||||||
wave: [0.0; WAVE_N],
|
wave: [0.0; WAVE_N],
|
||||||
@@ -464,6 +488,11 @@ pub struct Analyzer {
|
|||||||
since_hop: usize,
|
since_hop: usize,
|
||||||
spectrum: Vec<Complex<f32>>,
|
spectrum: Vec<Complex<f32>>,
|
||||||
prev_mag: Vec<f32>,
|
prev_mag: Vec<f32>,
|
||||||
|
// Per-bin phase at t-1 / t-2 for the complex-domain (CSD) onset: a steady
|
||||||
|
// tone advances phase linearly, so `2*ph[-1] - ph[-2]` predicts the next
|
||||||
|
// bin; the Euclidean miss from that prediction is the onset energy.
|
||||||
|
prev_phase: Vec<f32>,
|
||||||
|
prev_phase2: Vec<f32>,
|
||||||
bin_hz: f32,
|
bin_hz: f32,
|
||||||
env: Bands,
|
env: Bands,
|
||||||
// AGC ceilings: 3 level, 3 flux, SPEC_N spectrum, centroid, loud, broad flux.
|
// AGC ceilings: 3 level, 3 flux, SPEC_N spectrum, centroid, loud, broad flux.
|
||||||
@@ -473,8 +502,10 @@ pub struct Analyzer {
|
|||||||
agc_centroid: f32,
|
agc_centroid: f32,
|
||||||
agc_loud: f32,
|
agc_loud: f32,
|
||||||
agc_broad: f32,
|
agc_broad: f32,
|
||||||
|
agc_csd: f32,
|
||||||
pop: [f32; 3], // low/mid/high onset envelopes
|
pop: [f32; 3], // low/mid/high onset envelopes
|
||||||
broad_pop: f32, // broadband onset envelope
|
broad_pop: f32, // broadband onset envelope
|
||||||
|
csd_pop: f32, // complex-domain onset envelope
|
||||||
spec_edges: [(usize, usize); SPEC_N],
|
spec_edges: [(usize, usize); SPEC_N],
|
||||||
// Predictive-IOI beat model (robust for breakcore's unstable tempo: it
|
// Predictive-IOI beat model (robust for breakcore's unstable tempo: it
|
||||||
// tracks the running inter-onset interval, not a brittle global BPM).
|
// tracks the running inter-onset interval, not a brittle global BPM).
|
||||||
@@ -483,6 +514,15 @@ pub struct Analyzer {
|
|||||||
beat_clock: f32, // seconds since the last accepted beat
|
beat_clock: f32, // seconds since the last accepted beat
|
||||||
prev_broad: f32, // for the broadband-onset rising edge
|
prev_broad: f32, // for the broadband-onset rising edge
|
||||||
beat_pop: f32, // decaying beat pulse
|
beat_pop: f32, // decaying beat pulse
|
||||||
|
// Autocorrelation tempo tracker: ring of recent broadband-flux samples,
|
||||||
|
// a reused time-ordered scratch, the lag search bounds (frames), and the
|
||||||
|
// smoothed octave-folded BPM that anchors `beat_ioi`.
|
||||||
|
flux_hist: Vec<f32>,
|
||||||
|
flux_head: usize,
|
||||||
|
acf_buf: Vec<f32>,
|
||||||
|
acf_lag_min: usize,
|
||||||
|
acf_lag_max: usize,
|
||||||
|
bpm: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn norm(v: f32, c: &mut f32) -> f32 {
|
fn norm(v: f32, c: &mut f32) -> f32 {
|
||||||
@@ -518,6 +558,13 @@ impl Analyzer {
|
|||||||
*e = (a, b);
|
*e = (a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ACF tempo sizing (frames). hop_dt = seconds advanced per STFT hop;
|
||||||
|
// window must comfortably exceed the slowest searched period.
|
||||||
|
let hop_dt = HOP as f32 / sample_rate.max(1.0);
|
||||||
|
let acf_lag_min = (ACF_PERIOD_LO / hop_dt).round().max(1.0) as usize;
|
||||||
|
let acf_lag_max = (ACF_PERIOD_HI / hop_dt).round() as usize;
|
||||||
|
let acf_n = ((ACF_WIN_SECS / hop_dt).round() as usize).max(acf_lag_max + 2);
|
||||||
|
|
||||||
Analyzer {
|
Analyzer {
|
||||||
hann,
|
hann,
|
||||||
fft,
|
fft,
|
||||||
@@ -526,6 +573,8 @@ impl Analyzer {
|
|||||||
since_hop: 0,
|
since_hop: 0,
|
||||||
spectrum: vec![Complex::new(0.0, 0.0); FFT_SIZE],
|
spectrum: vec![Complex::new(0.0, 0.0); FFT_SIZE],
|
||||||
prev_mag: vec![0.0; half],
|
prev_mag: vec![0.0; half],
|
||||||
|
prev_phase: vec![0.0; half],
|
||||||
|
prev_phase2: vec![0.0; half],
|
||||||
bin_hz,
|
bin_hz,
|
||||||
env: Bands::default(),
|
env: Bands::default(),
|
||||||
agc_lvl: [AGC_FLOOR; 3],
|
agc_lvl: [AGC_FLOOR; 3],
|
||||||
@@ -534,14 +583,22 @@ impl Analyzer {
|
|||||||
agc_centroid: AGC_FLOOR,
|
agc_centroid: AGC_FLOOR,
|
||||||
agc_loud: AGC_FLOOR,
|
agc_loud: AGC_FLOOR,
|
||||||
agc_broad: AGC_FLOOR,
|
agc_broad: AGC_FLOOR,
|
||||||
|
agc_csd: AGC_FLOOR,
|
||||||
pop: [0.0; 3],
|
pop: [0.0; 3],
|
||||||
broad_pop: 0.0,
|
broad_pop: 0.0,
|
||||||
|
csd_pop: 0.0,
|
||||||
spec_edges,
|
spec_edges,
|
||||||
hop_dt: HOP as f32 / sample_rate.max(1.0),
|
hop_dt,
|
||||||
beat_ioi: 0.5, // ~120 BPM until the track tells us otherwise
|
beat_ioi: 0.5, // ~120 BPM until the track tells us otherwise
|
||||||
beat_clock: 0.0,
|
beat_clock: 0.0,
|
||||||
prev_broad: 0.0,
|
prev_broad: 0.0,
|
||||||
beat_pop: 0.0,
|
beat_pop: 0.0,
|
||||||
|
flux_hist: vec![0.0; acf_n],
|
||||||
|
flux_head: 0,
|
||||||
|
acf_buf: vec![0.0; acf_n],
|
||||||
|
acf_lag_min,
|
||||||
|
acf_lag_max,
|
||||||
|
bpm: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,19 +629,33 @@ impl Analyzer {
|
|||||||
(a, b.max(a + 1))
|
(a, b.max(a + 1))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Magnitudes (cache once), running flux + centroid + loudness.
|
// Magnitudes (cache once), running flux + centroid + loudness, plus
|
||||||
|
// the rectified complex-domain onset: predict each bin assuming a
|
||||||
|
// steady tone (constant magnitude, phase advancing at last frame's
|
||||||
|
// rate) and sum the Euclidean miss where energy *rose* (onset, not
|
||||||
|
// offset). Phase shifts t-1 -> t-2 in the same pass (no extra Vec).
|
||||||
let mut mags = vec![0.0f32; half];
|
let mut mags = vec![0.0f32; half];
|
||||||
let mut broad_flux = 0.0f32;
|
let mut broad_flux = 0.0f32;
|
||||||
|
let mut csd_raw = 0.0f32;
|
||||||
let mut cen_num = 0.0f32;
|
let mut cen_num = 0.0f32;
|
||||||
let mut cen_den = 0.0f32;
|
let mut cen_den = 0.0f32;
|
||||||
let mut loud_sum = 0.0f32;
|
let mut loud_sum = 0.0f32;
|
||||||
for k in 0..half {
|
for k in 0..half {
|
||||||
let m = self.spectrum[k].norm();
|
let c = self.spectrum[k];
|
||||||
|
let m = c.norm();
|
||||||
mags[k] = m;
|
mags[k] = m;
|
||||||
let d = m - self.prev_mag[k];
|
let d = m - self.prev_mag[k];
|
||||||
if d > 0.0 {
|
if d > 0.0 {
|
||||||
broad_flux += d;
|
broad_flux += d;
|
||||||
|
let pred_ph = 2.0 * self.prev_phase[k] - self.prev_phase2[k];
|
||||||
|
let pred = Complex::new(
|
||||||
|
self.prev_mag[k] * pred_ph.cos(),
|
||||||
|
self.prev_mag[k] * pred_ph.sin(),
|
||||||
|
);
|
||||||
|
csd_raw += (c - pred).norm();
|
||||||
}
|
}
|
||||||
|
self.prev_phase2[k] = self.prev_phase[k];
|
||||||
|
self.prev_phase[k] = c.arg();
|
||||||
let f = k as f32 * bin_hz;
|
let f = k as f32 * bin_hz;
|
||||||
cen_num += f * m;
|
cen_num += f * m;
|
||||||
cen_den += m;
|
cen_den += m;
|
||||||
@@ -673,6 +744,7 @@ impl Analyzer {
|
|||||||
let centroid = norm(centroid_hz, &mut self.agc_centroid);
|
let centroid = norm(centroid_hz, &mut self.agc_centroid);
|
||||||
let loud = norm(loud_sum / half as f32, &mut self.agc_loud);
|
let loud = norm(loud_sum / half as f32, &mut self.agc_loud);
|
||||||
let broad = norm(broad_flux / half as f32, &mut self.agc_broad);
|
let broad = norm(broad_flux / half as f32, &mut self.agc_broad);
|
||||||
|
let csd = norm(csd_raw / half as f32, &mut self.agc_csd);
|
||||||
|
|
||||||
// Smoothed levels.
|
// Smoothed levels.
|
||||||
follow(&mut self.env.low, l[0]);
|
follow(&mut self.env.low, l[0]);
|
||||||
@@ -698,12 +770,23 @@ impl Analyzer {
|
|||||||
} else {
|
} else {
|
||||||
self.broad_pop * ONSET_RELEASE
|
self.broad_pop * ONSET_RELEASE
|
||||||
};
|
};
|
||||||
|
self.csd_pop = if csd > self.csd_pop {
|
||||||
|
csd
|
||||||
|
} else {
|
||||||
|
self.csd_pop * ONSET_RELEASE
|
||||||
|
};
|
||||||
self.env.low_on = self.pop[0];
|
self.env.low_on = self.pop[0];
|
||||||
self.env.mid_on = self.pop[1];
|
self.env.mid_on = self.pop[1];
|
||||||
self.env.high_on = self.pop[2];
|
self.env.high_on = self.pop[2];
|
||||||
self.env.flux = self.broad_pop;
|
self.env.flux = self.broad_pop;
|
||||||
|
self.env.csd = self.csd_pop;
|
||||||
follow(&mut self.env.flatness, flatness);
|
follow(&mut self.env.flatness, flatness);
|
||||||
|
|
||||||
|
// Autocorrelation tempo: anchor the predictive IOI to the dominant
|
||||||
|
// period in ~3 s of broadband-flux history *before* the beat block
|
||||||
|
// runs, so a syncopated fill can't drag `beat_phase` off the grid.
|
||||||
|
self.update_tempo(broad);
|
||||||
|
|
||||||
// Predictive beat model. A rising broadband-onset edge past a floor
|
// Predictive beat model. A rising broadband-onset edge past a floor
|
||||||
// (with a refractory gap) is a candidate beat; the inter-onset
|
// (with a refractory gap) is a candidate beat; the inter-onset
|
||||||
// interval is smoothed only when it stays a plausible multiple of the
|
// interval is smoothed only when it stays a plausible multiple of the
|
||||||
@@ -728,6 +811,7 @@ impl Analyzer {
|
|||||||
}
|
}
|
||||||
self.env.beat = self.beat_pop;
|
self.env.beat = self.beat_pop;
|
||||||
self.env.beat_phase = (self.beat_clock / self.beat_ioi.max(1e-3)).clamp(0.0, 1.0);
|
self.env.beat_phase = (self.beat_clock / self.beat_ioi.max(1e-3)).clamp(0.0, 1.0);
|
||||||
|
self.env.bpm = self.bpm;
|
||||||
|
|
||||||
// Raw waveform tap: decimate the un-windowed sample window so the scope
|
// Raw waveform tap: decimate the un-windowed sample window so the scope
|
||||||
// mode has a real time-domain trace. Same numbers live + offline.
|
// mode has a real time-domain trace. Same numbers live + offline.
|
||||||
@@ -738,6 +822,76 @@ impl Analyzer {
|
|||||||
|
|
||||||
self.env
|
self.env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push one broadband-flux sample into the rolling history and re-estimate
|
||||||
|
/// the dominant tempo by autocorrelation. The ACF peak in the searched lag
|
||||||
|
/// window is the beat period; its height is the confidence. Octave-fold
|
||||||
|
/// into a stable BPM range, smooth it, and anchor `beat_ioi`: glide toward
|
||||||
|
/// it normally, but *snap* when the predictive model has locked a wrong
|
||||||
|
/// octave (so `beat_phase` recovers fast instead of fighting the ACF).
|
||||||
|
fn update_tempo(&mut self, x: f32) {
|
||||||
|
let n = self.flux_hist.len();
|
||||||
|
self.flux_hist[self.flux_head] = x;
|
||||||
|
self.flux_head = (self.flux_head + 1) % n;
|
||||||
|
|
||||||
|
// Oldest -> newest, DC-removed (a steady flux floor must not dominate).
|
||||||
|
for i in 0..n {
|
||||||
|
self.acf_buf[i] = self.flux_hist[(self.flux_head + i) % n];
|
||||||
|
}
|
||||||
|
let mean = self.acf_buf.iter().sum::<f32>() / n as f32;
|
||||||
|
for v in &mut self.acf_buf {
|
||||||
|
*v -= mean;
|
||||||
|
}
|
||||||
|
let var = self.acf_buf.iter().map(|v| v * v).sum::<f32>() / n as f32;
|
||||||
|
if var < 1e-9 {
|
||||||
|
return; // silence / no flux structure -> keep last estimate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unbiased, variance-normalised ACF so peaks are comparable across
|
||||||
|
// lags (raw ACF decays with lag and would bias fast).
|
||||||
|
let lag_hi = self.acf_lag_max.min(n - 1);
|
||||||
|
let mut best = 0.0f32;
|
||||||
|
let mut best_lag = 0usize;
|
||||||
|
for lag in self.acf_lag_min..=lag_hi {
|
||||||
|
let mut s = 0.0f32;
|
||||||
|
for i in lag..n {
|
||||||
|
s += self.acf_buf[i] * self.acf_buf[i - lag];
|
||||||
|
}
|
||||||
|
let r = s / ((n - lag) as f32 * var);
|
||||||
|
if r > best {
|
||||||
|
best = r;
|
||||||
|
best_lag = lag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if best_lag == 0 || best < ACF_CONF_MIN {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beat period -> BPM, octave-folded into one stable range.
|
||||||
|
let mut bpm = 60.0 / (best_lag as f32 * self.hop_dt).max(1e-3);
|
||||||
|
while bpm > BPM_FOLD_HI {
|
||||||
|
bpm *= 0.5;
|
||||||
|
}
|
||||||
|
while bpm < BPM_FOLD_LO {
|
||||||
|
bpm *= 2.0;
|
||||||
|
}
|
||||||
|
self.bpm = if self.bpm <= 0.0 {
|
||||||
|
bpm
|
||||||
|
} else {
|
||||||
|
self.bpm + (bpm - self.bpm) * 0.10
|
||||||
|
};
|
||||||
|
|
||||||
|
// Anchor the predictive IOI. Snap on a confident wrong-octave lock,
|
||||||
|
// otherwise glide proportionally to confidence.
|
||||||
|
let ioi = (60.0 / self.bpm.max(1e-3)).clamp(0.18, 1.0);
|
||||||
|
let ratio = self.beat_ioi / ioi.max(1e-3);
|
||||||
|
if best > ACF_SNAP && !(0.75..1.34).contains(&ratio) {
|
||||||
|
self.beat_ioi = ioi;
|
||||||
|
} else {
|
||||||
|
self.beat_ioi = (self.beat_ioi + (ioi - self.beat_ioi) * (best * 0.20))
|
||||||
|
.clamp(0.18, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analysis_loop(
|
fn analysis_loop(
|
||||||
|
|||||||
+16
-1
@@ -327,14 +327,26 @@ fn main() {
|
|||||||
match audio::analyze_file(&f) {
|
match audio::analyze_file(&f) {
|
||||||
Ok(tl) => {
|
Ok(tl) => {
|
||||||
let mut peak = Bands::default();
|
let mut peak = Bands::default();
|
||||||
|
let mut bpms: Vec<f32> = Vec::new();
|
||||||
for b in &tl.frames {
|
for b in &tl.frames {
|
||||||
peak.low = peak.low.max(b.low);
|
peak.low = peak.low.max(b.low);
|
||||||
peak.loud = peak.loud.max(b.loud);
|
peak.loud = peak.loud.max(b.loud);
|
||||||
peak.flux = peak.flux.max(b.flux);
|
peak.flux = peak.flux.max(b.flux);
|
||||||
|
peak.csd = peak.csd.max(b.csd);
|
||||||
peak.centroid = peak.centroid.max(b.centroid);
|
peak.centroid = peak.centroid.max(b.centroid);
|
||||||
|
if b.bpm > 0.0 {
|
||||||
|
bpms.push(b.bpm);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Median BPM = the track's anchored tempo (ACF-stabilised).
|
||||||
|
let med_bpm = if bpms.is_empty() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
bpms.sort_by(|a, b| a.total_cmp(b));
|
||||||
|
bpms[bpms.len() / 2]
|
||||||
|
};
|
||||||
println!(
|
println!(
|
||||||
"ok: {} frames, {:.2}s, {} Hz, {:.1} fps\n peak low {:.2} loud {:.2} flux {:.2} centroid {:.2}",
|
"ok: {} frames, {:.2}s, {} Hz, {:.1} fps\n peak low {:.2} loud {:.2} flux {:.2} csd {:.2} centroid {:.2}\n tempo {:.1} BPM (median of {} locked frames)",
|
||||||
tl.frames.len(),
|
tl.frames.len(),
|
||||||
tl.duration(),
|
tl.duration(),
|
||||||
tl.sample_rate as u32,
|
tl.sample_rate as u32,
|
||||||
@@ -342,7 +354,10 @@ fn main() {
|
|||||||
peak.low,
|
peak.low,
|
||||||
peak.loud,
|
peak.loud,
|
||||||
peak.flux,
|
peak.flux,
|
||||||
|
peak.csd,
|
||||||
peak.centroid,
|
peak.centroid,
|
||||||
|
med_bpm,
|
||||||
|
bpms.len(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => die(format!("analyze: {e}")),
|
Err(e) => die(format!("analyze: {e}")),
|
||||||
|
|||||||
+14
-11
@@ -246,7 +246,7 @@ impl Arch {
|
|||||||
match self {
|
match self {
|
||||||
// Wide, slow, thin, dim. Knot (ordered) backbone, shell barely on.
|
// Wide, slow, thin, dim. Knot (ordered) backbone, shell barely on.
|
||||||
Arch::Ambient => Regime {
|
Arch::Ambient => Regime {
|
||||||
scale: 0.95,
|
scale: 1.00,
|
||||||
melt: 0.3,
|
melt: 0.3,
|
||||||
glow: 0.40,
|
glow: 0.40,
|
||||||
speed: 0.55,
|
speed: 0.55,
|
||||||
@@ -262,7 +262,7 @@ impl Arch {
|
|||||||
// Contracting toward a dense core; everything ramps with tension
|
// Contracting toward a dense core; everything ramps with tension
|
||||||
// (see Breakcore::update — these are the *base* before tension).
|
// (see Breakcore::update — these are the *base* before tension).
|
||||||
Arch::Build => Regime {
|
Arch::Build => Regime {
|
||||||
scale: 0.78,
|
scale: 0.84,
|
||||||
melt: 0.7,
|
melt: 0.7,
|
||||||
glow: 0.60,
|
glow: 0.60,
|
||||||
speed: 0.9,
|
speed: 0.9,
|
||||||
@@ -292,7 +292,7 @@ impl Arch {
|
|||||||
},
|
},
|
||||||
// Hollowed out after a drop: medium, soft, cool, slow.
|
// Hollowed out after a drop: medium, soft, cool, slow.
|
||||||
Arch::Breakdown => Regime {
|
Arch::Breakdown => Regime {
|
||||||
scale: 0.85,
|
scale: 0.90,
|
||||||
melt: 0.45,
|
melt: 0.45,
|
||||||
glow: 0.50,
|
glow: 0.50,
|
||||||
speed: 0.7,
|
speed: 0.7,
|
||||||
@@ -307,7 +307,7 @@ impl Arch {
|
|||||||
},
|
},
|
||||||
// Balanced sustained default.
|
// Balanced sustained default.
|
||||||
Arch::Groove => Regime {
|
Arch::Groove => Regime {
|
||||||
scale: 0.82,
|
scale: 0.88,
|
||||||
melt: 0.5,
|
melt: 0.5,
|
||||||
glow: 0.65,
|
glow: 0.65,
|
||||||
speed: 1.0,
|
speed: 1.0,
|
||||||
@@ -636,7 +636,7 @@ impl Breakcore {
|
|||||||
self.sp_scale.step(scale_t, 14.0, dt);
|
self.sp_scale.step(scale_t, 14.0, dt);
|
||||||
self.sp_tube
|
self.sp_tube
|
||||||
.step(rg.tube + 0.05 * b.mid + 0.02 * b.mid_on, 11.0, dt);
|
.step(rg.tube + 0.05 * b.mid + 0.02 * b.mid_on, 11.0, dt);
|
||||||
self.sp_dist.step(3.4 - 0.9 * b.low, 6.0, dt);
|
self.sp_dist.step(3.2 - 0.85 * b.low, 6.0, dt);
|
||||||
self.sp_glow.step(
|
self.sp_glow.step(
|
||||||
rg.glow + 0.45 * b.loud + 0.4 * b.flux + 0.35 * tn,
|
rg.glow + 0.45 * b.loud + 0.4 * b.flux + 0.35 * tn,
|
||||||
9.0,
|
9.0,
|
||||||
@@ -729,7 +729,10 @@ impl Breakcore {
|
|||||||
/// shock, so every structure pulses out together on a big hit and the
|
/// shock, so every structure pulses out together on a big hit and the
|
||||||
/// bounding sphere (which uses this too) never clips them.
|
/// bounding sphere (which uses this too) never clips them.
|
||||||
fn world_scale(&self) -> f32 {
|
fn world_scale(&self) -> f32 {
|
||||||
self.sp_scale.x.clamp(0.4, 2.4) * (1.0 + 0.12 * self.shock)
|
// Hard upper bound: this also feeds the bounding-sphere radius, and a
|
||||||
|
// sphere that fills the viewport defeats the background early-out that
|
||||||
|
// protects the GPU. A release/loud spike must not balloon it.
|
||||||
|
self.sp_scale.x.clamp(0.4, 1.8) * (1.0 + 0.12 * self.shock)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sample the backbone into the spine slots, blending from→to with the
|
/// Sample the backbone into the spine slots, blending from→to with the
|
||||||
@@ -927,11 +930,11 @@ impl Breakcore {
|
|||||||
let lo = self.b.loud;
|
let lo = self.b.loud;
|
||||||
let base = oklch(
|
let base = oklch(
|
||||||
(0.55 + 0.30 * lo + 0.12 * tn).min(0.95),
|
(0.55 + 0.30 * lo + 0.12 * tn).min(0.95),
|
||||||
0.10 + 0.06 * lo,
|
0.13 + 0.06 * lo,
|
||||||
self.hue_b,
|
self.hue_b,
|
||||||
);
|
);
|
||||||
let sat = (0.13 + 0.10 * (lo * 0.5 + self.b.mid * 0.4))
|
let sat = (0.17 + 0.12 * (lo * 0.5 + self.b.mid * 0.4))
|
||||||
* (1.0 - 0.30 * self.b.flatness);
|
* (1.0 - 0.25 * self.b.flatness);
|
||||||
let acc = oklch((0.62 + 0.28 * lo).min(0.97), sat, self.hue_a);
|
let acc = oklch((0.62 + 0.28 * lo).min(0.97), sat, self.hue_a);
|
||||||
|
|
||||||
let mut u = [0.0f32; UBO_LEN];
|
let mut u = [0.0f32; UBO_LEN];
|
||||||
@@ -946,7 +949,7 @@ impl Breakcore {
|
|||||||
// row1 scale,tube,glow,ca
|
// row1 scale,tube,glow,ca
|
||||||
u[4] = scale;
|
u[4] = scale;
|
||||||
u[5] = self.sp_tube.x;
|
u[5] = self.sp_tube.x;
|
||||||
u[6] = self.sp_glow.x.clamp(0.40, 1.2);
|
u[6] = self.sp_glow.x.clamp(0.40, 1.0);
|
||||||
// build-up creeps the aberration; release spikes it (drive scales
|
// build-up creeps the aberration; release spikes it (drive scales
|
||||||
// the swing, not the user's base `ca`).
|
// the swing, not the user's base `ca`).
|
||||||
u[7] = ca_px * rg.ca * (1.0 + (0.7 * tn + 2.0 * rel) * dr);
|
u[7] = ca_px * rg.ca * (1.0 + (0.7 * tn + 2.0 * rel) * dr);
|
||||||
@@ -1001,7 +1004,7 @@ impl Breakcore {
|
|||||||
// row9 swirl_zoom, swirl_rot, bg_glow, beat
|
// row9 swirl_zoom, swirl_rot, bg_glow, beat
|
||||||
u[36] = (0.004 + (0.018 * self.b.loud + 0.020 * self.shock) * dr).clamp(0.0, 0.05);
|
u[36] = (0.004 + (0.018 * self.b.loud + 0.020 * self.shock) * dr).clamp(0.0, 0.05);
|
||||||
u[37] = ((0.006 * self.b.mid + 0.020 * tn) * dr).clamp(0.0, 0.05);
|
u[37] = ((0.006 * self.b.mid + 0.020 * tn) * dr).clamp(0.0, 0.05);
|
||||||
u[38] = (0.15 + 0.70 * self.b.loud).clamp(0.0, 1.0);
|
u[38] = (0.05 + 0.40 * self.b.loud).clamp(0.0, 1.0);
|
||||||
u[39] = self.b.beat;
|
u[39] = self.b.beat;
|
||||||
// points (after 10 std140 rows = 40 f32)
|
// points (after 10 std140 rows = 40 f32)
|
||||||
for (i, p) in pts.iter().enumerate() {
|
for (i, p) in pts.iter().enumerate() {
|
||||||
|
|||||||
+27
-6
@@ -211,7 +211,9 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|||||||
// binary hit→white. Bright within ~0.012, gone by ~0.03, so a dense
|
// binary hit→white. Bright within ~0.012, gone by ~0.03, so a dense
|
||||||
// tangle reads as separated glowing wires, not a filled slab.
|
// tangle reads as separated glowing wires, not a filled slab.
|
||||||
let dl = max(dmin, 0.0);
|
let dl = max(dmin, 0.0);
|
||||||
inten = exp(-dl * dl * 6000.0) + 0.10 * exp(-dl * 30.0);
|
// Tighter halo (0.06·e^-40d, was 0.10·e^-30d) so a dense tangle reads
|
||||||
|
// as separated glowing wires, not one glow mass.
|
||||||
|
inten = exp(-dl * dl * 6000.0) + 0.06 * exp(-dl * 40.0);
|
||||||
let tt = select(t, hit_t, hit_t > 0.0);
|
let tt = select(t, hit_t, hit_t > 0.0);
|
||||||
depth = clamp((tt + b) / max(2.0 * sq, 1e-3), 0.0, 1.0);
|
depth = clamp((tt + b) / max(2.0 * sq, 1e-3), 0.0, 1.0);
|
||||||
// Reactive volumetric fog: near strands brighter than far.
|
// Reactive volumetric fog: near strands brighter than far.
|
||||||
@@ -222,20 +224,34 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|||||||
// Shade point (local/rotated space, same as map()).
|
// Shade point (local/rotated space, same as map()).
|
||||||
let rp = rot(ro + rd * tt);
|
let rp = rot(ro + rd * tt);
|
||||||
// Per-structure hue: each layer keeps its own colour identity.
|
// Per-structure hue: each layer keeps its own colour identity.
|
||||||
let gid = nearest_gid(rp);
|
// `nearest_gid` is a full NP loop, so — like the normal — it is
|
||||||
|
// gated to near-surface pixels; the faint outer glow just uses
|
||||||
|
// the base hue (it is dim enough not to matter) so the expensive
|
||||||
|
// path can never run over a screen-sized halo.
|
||||||
var wire = base;
|
var wire = base;
|
||||||
|
if (dmin < 0.020) {
|
||||||
|
let gid = nearest_gid(rp);
|
||||||
if (gid > 2.5) { wire = mix(base, accent, 0.5); } // spokes
|
if (gid > 2.5) { wire = mix(base, accent, 0.5); } // spokes
|
||||||
else if (gid > 1.5) { wire = mix(accent, vec3<f32>(1.0), 0.7); } // debris
|
else if (gid > 1.5) { wire = mix(accent, vec3<f32>(1.0, 0.55, 0.25), 0.6); } // debris (hot)
|
||||||
else if (gid > 0.5) { wire = accent; } // ribs
|
else if (gid > 0.5) { wire = accent; } // ribs
|
||||||
|
}
|
||||||
// A little depth blend toward accent keeps the form readable in 3D.
|
// A little depth blend toward accent keeps the form readable in 3D.
|
||||||
let wcol = mix(wire, accent, 0.25 * depth);
|
let wcol = mix(wire, accent, 0.25 * depth);
|
||||||
|
|
||||||
col = wcol * inten;
|
col = wcol * inten;
|
||||||
col = col + mix(accent, vec3<f32>(1.0), 0.6) * pow(inten, 6.0) * 0.6;
|
// Hotter exponent + lower weight: only the very centre of a wire
|
||||||
|
// blows toward white, so hue survives across the body.
|
||||||
|
col = col + mix(accent, vec3<f32>(1.0), 0.5) * pow(inten, 8.0) * 0.35;
|
||||||
col = col + accent * flash * pow(inten, 3.0) * 0.35; // onset spark
|
col = col + accent * flash * pow(inten, 3.0) * 0.35; // onset spark
|
||||||
|
|
||||||
// Surface lighting — rim/fresnel + a hi-band specular glint give
|
// Surface lighting — rim/fresnel + a hi-band specular glint give
|
||||||
// the tube real 3D form instead of a flat glow ribbon.
|
// the tube real 3D form. The 6×map() normal is the single most
|
||||||
|
// expensive thing in the shader, so it is gated to pixels that
|
||||||
|
// genuinely landed ON a wire (tiny `dmin`), NOT the whole soft
|
||||||
|
// glow halo. This caps the expensive-pixel count to the thin wire
|
||||||
|
// silhouette regardless of framing — it is what keeps a dense
|
||||||
|
// drop from blowing the GPU frame budget (device-lost).
|
||||||
|
if (dmin < 0.006) {
|
||||||
let n = calc_normal(rp);
|
let n = calc_normal(rp);
|
||||||
let vdir = normalize(rot(-rd));
|
let vdir = normalize(rot(-rd));
|
||||||
let rim_e = mix(4.0, 1.6, flat_n); // noisy → broader rim
|
let rim_e = mix(4.0, 1.6, flat_n); // noisy → broader rim
|
||||||
@@ -245,6 +261,7 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|||||||
let hdir = normalize(ldir + vdir);
|
let hdir = normalize(ldir + vdir);
|
||||||
let spec = pow(clamp(dot(n, hdir), 0.0, 1.0), 42.0);
|
let spec = pow(clamp(dot(n, hdir), 0.0, 1.0), 42.0);
|
||||||
col = col + vec3<f32>(1.0) * spec * inten * (0.25 + 0.9 * high_on);
|
col = col + vec3<f32>(1.0) * spec * inten * (0.25 + 0.9 * high_on);
|
||||||
|
}
|
||||||
|
|
||||||
// Build-up heat: warm toward accent + a warm hot core.
|
// Build-up heat: warm toward accent + a warm hot core.
|
||||||
col = col + accent * heat * pow(inten, 2.0) * 0.20;
|
col = col + accent * heat * pow(inten, 2.0) * 0.20;
|
||||||
@@ -292,8 +309,12 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|||||||
// Faint base-hue background so the void breathes with loudness without
|
// Faint base-hue background so the void breathes with loudness without
|
||||||
// ever washing (≤0.05, centre-weighted). Added after feedback so it is a
|
// ever washing (≤0.05, centre-weighted). Added after feedback so it is a
|
||||||
// stable floor, not something the trail can accumulate.
|
// stable floor, not something the trail can accumulate.
|
||||||
|
// Force a dark *cool* tint (not the raw base hue, which lands olive and
|
||||||
|
// the phosphor feedback then accumulates into a full-frame wash). Tiny
|
||||||
|
// amplitude so it can never build up through the trail.
|
||||||
let vig = max(1.0 - 0.75 * length(ndc), 0.0);
|
let vig = max(1.0 - 0.75 * length(ndc), 0.0);
|
||||||
col = col + base * u.p6.z * vig * 0.05;
|
let bgt = mix(base, vec3<f32>(0.04, 0.08, 0.16), 0.70);
|
||||||
|
col = col + bgt * u.p6.z * vig * 0.02;
|
||||||
|
|
||||||
// CRT scanline — depth is audio-driven (loudness + tension) and the
|
// CRT scanline — depth is audio-driven (loudness + tension) and the
|
||||||
// lines crawl with the beat phase, so the "display" feels alive.
|
// lines crawl with the beat phase, so the "display" feels alive.
|
||||||
|
|||||||
Reference in New Issue
Block a user