Merge p10-remove-ts-smoothing into dev (drop client-side datum smoothing; waveform smoothing stays the server's job)

This commit is contained in:
daniel-c-harvey
2026-06-17 05:47:16 -04:00
@@ -263,9 +263,8 @@ const BLOB_RESTITUTION_SOFT = 0.05; // residual restitution at strength = 0 (al
* the soft end → elastic reflection of the inward velocity at the hard end. The waveform * the soft end → elastic reflection of the inward velocity at the hard end. The waveform
* is read-only authority: it pushes the fluid, the fluid never moves it. * is read-only authority: it pushes the fluid, the fluid never moves it.
*/ */
// Phase 10 collision retune (Daniel: "less explosive, more bouncy", no jitter, no stuck wax). The // Phase 10 collision retune (Daniel: "less explosive, more bouncy", no jitter, no stuck wax).
// smoothed waveform (item 1) gives a gently-moving boundary, so the response can be springier without // Restitution is now SUB-unity: a real bounce conserves-or-loses energy, never adds it —
// buzzing. Restitution is now SUB-unity: a real bounce conserves-or-loses energy, never adds it —
// over-unity (the old 1.1) injected energy each contact and read as "explosive". 0.85 at the hard end // over-unity (the old 1.1) injected energy each contact and read as "explosive". 0.85 at the hard end
// is lively/springy; the soft end stays near-zero (mush). // is lively/springy; the soft end stays near-zero (mush).
const WAVE_COLLIDE_SPRING = 10.0; // soft penalty stiffness pushing wax off the ribbon (slightly softer) const WAVE_COLLIDE_SPRING = 10.0; // soft penalty stiffness pushing wax off the ribbon (slightly softer)
@@ -550,43 +549,6 @@ function decodeSamples(base64: string): Uint8Array {
return out; return out;
} }
/**
* Envelope-follower smoothing time constant, seconds — mirrors C#
* RmsLoudnessAlgorithm.SmoothingTimeConstantSeconds. The ~50 ms target rounds the spikey
* per-sample loudness into a smooth ribbon contour (Phase 10 tuning).
*/
const SMOOTHING_TIME_CONSTANT_SECONDS = 0.05;
/**
* Smooth the [0,255] loudness datum in place with a symmetric (zero-phase) one-pole envelope
* follower targeting SMOOTHING_TIME_CONSTANT_SECONDS. This runs at DECODE time so EXISTING stored
* mixes — whose vault profiles predate the C#-side preprocessing smoothing — read as a smooth
* curve with no data regeneration. New mixes are already smoothed at preprocessing; a second light
* pass over an already-smooth curve is near-idempotent, so applying it unconditionally here is safe.
*
* The coefficient a = exp(secondsPerSample / τ): forward then backward pass cancels the single-pole
* lag (no time shift). Bytes stay [0,255]; we smooth in float and round back. A degenerate sample
* rate (≤0 or non-finite) leaves the data untouched.
*/
function smoothDatum(samples: Uint8Array, sampleCount: number, durationSeconds: number): void {
if (sampleCount < 2 || durationSeconds <= 0 || !Number.isFinite(durationSeconds)) return;
const secondsPerSample = durationSeconds / sampleCount;
const a = Math.exp(-secondsPerSample / SMOOTHING_TIME_CONSTANT_SECONDS);
// Float working buffer over the real samples (tail padding, if any, is untouched).
const env = new Float32Array(sampleCount);
let acc = samples[0];
for (let i = 0; i < sampleCount; i++) {
acc = a * acc + (1 - a) * samples[i];
env[i] = acc;
}
acc = env[sampleCount - 1];
for (let i = sampleCount - 1; i >= 0; i--) {
acc = a * acc + (1 - a) * env[i];
samples[i] = Math.round(Math.min(255, Math.max(0, acc)));
}
}
// ── Shaders. ───────────────────────────────────────────────────────────────────── // ── Shaders. ─────────────────────────────────────────────────────────────────────
// //
// Vertex: trivial pass-through. We draw a single triangle that more than covers the // Vertex: trivial pass-through. We draw a single triangle that more than covers the
@@ -2026,11 +1988,6 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
return null; return null;
} }
// Smooth the loudness contour at decode time so EXISTING mixes (stored before the C#-side
// preprocessing smoothing) read as a smooth curve with no regeneration. Mutates `samples` in
// place — both the GPU texture (below) and the CPU collision mirror (datum.samples) read it.
smoothDatum(samples, sampleCount, durationSeconds);
// Width = min(N, a safe power-of-two cap). The power-of-two cap (4096) is well // Width = min(N, a safe power-of-two cap). The power-of-two cap (4096) is well
// under every real GL_MAX_TEXTURE_SIZE and keeps row arithmetic clean; we // under every real GL_MAX_TEXTURE_SIZE and keeps row arithmetic clean; we
// still clamp it to the actual max in case a driver reports something smaller. // still clamp it to the actual max in case a driver reports something smaller.