web: per-band onset flux, scene intensity tuning from headless captures

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Aqua 2026-11-05 19:03:51 +00:00
commit 6bd6020571
4 changed files with 27 additions and 15 deletions

View file

@ -77,9 +77,9 @@ export class DemoFeed {
for (let k = 0; k < n; k++) {
const f = k * hz + 1;
let v = -105 + 12 * Math.sin(k * 12.9898 + t * 3) * 0.3;
v += 62 * kick * Math.exp(-Math.pow(Math.log2(f / 55), 2) * 1.5);
v += 52 * snare * Math.exp(-Math.pow(Math.log2(f / 1800), 2) * 0.35);
v += 45 * hat * Math.exp(-Math.pow(Math.log2(f / 9000), 2) * 0.5);
v += 78 * kick * Math.exp(-Math.pow(Math.log2(f / 55), 2) * 1.5);
v += 62 * snare * Math.exp(-Math.pow(Math.log2(f / 1800), 2) * 0.35);
v += 55 * hat * Math.exp(-Math.pow(Math.log2(f / 9000), 2) * 0.5);
for (const c of chord) v += 55 * Math.exp(-Math.pow((f - c) / (hz * 0.8 + c * 0.01), 2));
db[k] = v;
}

View file

@ -60,11 +60,13 @@ export class OnsetDetector {
constructor(opts = {}) {
this.compress = opts.compress ?? 200; // log(1 + compress * mag)
this.window = opts.window ?? 43; // median history, frames
this.delta = opts.delta ?? 0.35; // absolute floor above the median, in flux units
this.delta = opts.delta ?? 0.08; // absolute floor above the median, in flux units
this.ratio = opts.ratio ?? 1.6; // and the flux must exceed ratio * median
this.refractory = opts.refractory ?? 0.06; // seconds
this.minBin = opts.minBin ?? 2;
this.maxBin = opts.maxBin ?? 0; // 0 = all bins
this.bands = opts.bands ?? 6; // log-spaced flux bands between minBin and maxBin
this.edges = null;
this.prev = null;
this.hist = []; // recent flux values
this.f1 = 0; this.f2 = 0; this.t1 = 0; // flux[n-1], flux[n-2], time[n-1]
@ -74,17 +76,27 @@ export class OnsetDetector {
push(mags, t) {
const n = this.maxBin || mags.length;
if (!this.edges) {
this.edges = Array.from({ length: this.bands + 1 }, (_, i) => Math.round(this.minBin * Math.pow(n / this.minBin, i / this.bands)));
for (let i = 1; i < this.edges.length; i++) this.edges[i] = Math.max(this.edges[i], this.edges[i - 1] + 1);
}
const cur = new Float32Array(n);
for (let k = 0; k < n; k++) cur[k] = Math.log1p(this.compress * mags[k]);
let flux = 0;
if (this.prev) {
const p = this.prev;
for (let k = this.minBin; k < n; k++) {
const ref = Math.max(p[k - 1] ?? 0, p[k], p[k + 1] ?? 0);
const d = cur[k] - ref;
if (d > 0) flux += d;
// flux per log-spaced band, each divided by sqrt(its width): a kick (a few bass bins) then counts
// as much as a hat (hundreds of treble bins), which one whole-spectrum sum would bury
for (let b = 0; b < this.edges.length - 1; b++) {
let bf = 0;
for (let k = this.edges[b]; k < this.edges[b + 1]; k++) {
const ref = Math.max(p[k - 1] ?? 0, p[k], p[k + 1] ?? 0);
const d = cur[k] - ref;
if (d > 0) bf += d;
}
flux += bf / Math.sqrt(this.edges[b + 1] - this.edges[b]);
}
flux /= Math.sqrt(n);
flux /= this.edges.length - 1;
}
this.prev = cur;
this.flux = flux;

View file

@ -34,7 +34,7 @@ void main(){
vec3 c = mix(base, crest, smoothstep(0.1, 0.9, h));
float lines = 0.55 + 0.45 * smoothstep(0.82, 1.0, abs(sin(seg * 256.0 * 3.14159)));
float fade = (1.0 - ring) * (1.0 - ring) * 0.9 + 0.05;
float a = (0.02 + h * h * 0.75) * lines * fade * (1.0 + 1.5 * kick);
float a = (0.015 + h * h * 0.42) * lines * fade * (1.0 + 1.2 * kick);
o = vec4(c * a, 1.0);
}`;

View file

@ -35,7 +35,7 @@ void main(){
vec3 cold = tint * 0.6;
vec3 c = mix(cold, hot, smoothstep(0.2, 0.9, flame + bass)) * (0.25 + 0.75 * exp(-depth * 0.12));
c *= mix(0.35, 1.0, tint.b + 0.2) ;
o = vec4(c * (0.15 + 0.5 * level + 0.35 * bass) * 0.22, 1.0);
o = vec4(c * (0.15 + 0.5 * level + 0.35 * bass) * 0.45, 1.0);
}`;
const PVS = GLSL_HEAD + `
@ -45,14 +45,14 @@ out float life; out float heat;
float h1(float x){ return fract(sin(x * 127.1) * 43758.5453); }
void main(){
float age = time - a.x;
if (a.x < 0.0 || age < 0.0 || age > 2.6) { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); gl_PointSize = 0.0; return; }
if (a.x < 0.0 || age < 0.0 || age > 2.0) { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); gl_PointSize = 0.0; return; }
float s = a.y, str = a.z;
float vx = (h1(s) - 0.5) * 1.4 * (0.5 + str) + (a.w - 0.5) * 0.7;
float vy = (1.2 + 2.6 * h1(s + 3.0) * str + 0.8 * str);
float x = (a.w * 2.0 - 1.0) * 0.95 + vx * age + 0.08 * sin(age * 7.0 + s * 40.0);
float y = -0.1 + vy * age - 1.9 * age * age;
if (y < -0.1 && age > 0.1) { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); gl_PointSize = 0.0; return; }
life = 1.0 - age / 2.6; heat = life * (0.4 + str);
life = 1.0 - age / 2.0; heat = life * (0.15 + 0.6 * str);
gl_Position = vec4(x / (res.x / res.y), y, 0.0, 1.0);
gl_PointSize = (2.0 + 9.0 * h1(s + 7.0) * str) * (0.4 + life) * res.y / 900.0;
}`;
@ -61,7 +61,7 @@ in float life; in float heat; uniform vec3 tint; out vec4 o;
void main(){
vec2 q = gl_PointCoord * 2.0 - 1.0; float r = dot(q, q); if (r > 1.0) discard;
vec3 fire = mix(vec3(0.9, 0.12, 0.02), vec3(1.0, 0.85, 0.4), heat);
fire = mix(fire, tint * 1.3, 0.25 * (1.0 - heat));
fire = mix(fire, tint * 1.3, 0.15 * (1.0 - heat));
o = vec4(fire * (1.0 - r) * life * 0.9, 1.0);
}`;
@ -96,7 +96,7 @@ export class BurningWater {
if (A.onset.count !== this.seen) {
this.seen = A.onset.count;
const s = A.onset.strength;
this.spawn(Math.round(80 + 520 * s), t, 0.35 + 0.65 * s);
this.spawn(Math.round(40 + 260 * s), t, 0.35 + 0.65 * s);
const r = this.rhead; this.rhead = (this.rhead + 1) % 8;
this.ripples.set([(Math.random() - 0.5) * 6, 1.5 + Math.random() * 4, t, 0.5 + s], r * 4);
}