feat(mix-visualizer): Phase 10 tuning — smooth waveform, bouncy collision, 8 knobs
Smooth the loudness contour (~50 ms envelope at preprocessing + decode-time, plus smootherstep render reconstruction); retune wax↔waveform collision to bouncy/sub-unity (no explosion/stuck/jitter); split the bubbles knob into fluid-amount + fluid-viscosity (cohesion via uniform-only smin/wobble); retune scroll/gravity/heat/width ranges; make the colour rotation visible and boost OKLab chroma; the controls bar now holds its layout and hides only its knobs via a Visible parameter.
This commit is contained in:
@@ -3,11 +3,20 @@ namespace DeepDrftContent.Processors;
|
||||
/// <summary>
|
||||
/// Loudness via root-mean-square amplitude per time bucket. Decodes signed PCM (8-bit unsigned,
|
||||
/// 16/24/32-bit signed little-endian), averages channels to mono, partitions the frames into
|
||||
/// equal time slices, takes the RMS of each slice, then peak-normalizes so the loudest bucket is 1.
|
||||
/// No external audio dependency — operates directly on the WAV data-chunk bytes.
|
||||
/// equal time slices, takes the RMS of each slice, applies a ~50 ms envelope-follower smoothing
|
||||
/// so the contour reads as a smooth curve rather than a spikey polygon, then peak-normalizes so
|
||||
/// the loudest bucket is 1. No external audio dependency — operates directly on the WAV data-chunk bytes.
|
||||
/// </summary>
|
||||
public class RmsLoudnessAlgorithm : ILoudnessAlgorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// Envelope-follower time constant, seconds. ~50 ms is the spec's smoothing target (Phase 10
|
||||
/// tuning): long enough to round off the per-bucket RMS spikes into a smooth ribbon contour,
|
||||
/// short enough that real loudness transients (kicks, drops) still read. Applied as a symmetric
|
||||
/// (forward+backward) one-pole filter so the smoothing introduces no time lag.
|
||||
/// </summary>
|
||||
public const double SmoothingTimeConstantSeconds = 0.05;
|
||||
|
||||
public double[] Compute(ReadOnlySpan<byte> pcmData, int channels, int sampleRate, int bitsPerSample, int bucketCount)
|
||||
{
|
||||
if (bucketCount <= 0)
|
||||
@@ -64,16 +73,28 @@ public class RmsLoudnessAlgorithm : ILoudnessAlgorithm
|
||||
counts[bucket]++;
|
||||
}
|
||||
|
||||
var peak = 0.0;
|
||||
for (var i = 0; i < bucketCount; i++)
|
||||
{
|
||||
if (counts[i] > 0)
|
||||
{
|
||||
result[i] = Math.Sqrt(sumSquares[i] / counts[i]);
|
||||
if (result[i] > peak)
|
||||
{
|
||||
peak = result[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Envelope smoothing (~50 ms): round the spikey per-bucket RMS into a smooth contour before
|
||||
// peak-normalization, so the rendered ribbon reads as a continuous curve, not faceted polygons.
|
||||
// Each bucket spans (totalSeconds / bucketCount) of audio; the filter coefficient is derived
|
||||
// from that against the time constant so the smoothing is duration-aware, not a fixed window.
|
||||
var totalSeconds = (double)frameCount / sampleRate;
|
||||
var bucketSeconds = totalSeconds / bucketCount;
|
||||
SmoothEnvelope(result, bucketSeconds);
|
||||
|
||||
var peak = 0.0;
|
||||
for (var i = 0; i < bucketCount; i++)
|
||||
{
|
||||
if (result[i] > peak)
|
||||
{
|
||||
peak = result[i];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +113,42 @@ public class RmsLoudnessAlgorithm : ILoudnessAlgorithm
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Symmetric one-pole envelope smoothing over the per-bucket loudness, in place. A forward pass
|
||||
/// then a backward pass cancels the single-pole phase lag, so the smoothed contour stays aligned
|
||||
/// with the audio (no rightward time shift). The coefficient <c>a = exp(−bucketSeconds / τ)</c>
|
||||
/// gives a ~<paramref name="bucketSeconds"/>-relative response targeting the ~50 ms time constant:
|
||||
/// each bucket blends <c>(1 − a)</c> of itself with <c>a</c> of the running envelope. A near-zero
|
||||
/// or non-finite bucket duration leaves the data untouched (nothing to smooth meaningfully).
|
||||
/// </summary>
|
||||
private static void SmoothEnvelope(double[] data, double bucketSeconds)
|
||||
{
|
||||
if (data.Length < 2 || bucketSeconds <= 0 || !double.IsFinite(bucketSeconds))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var a = Math.Exp(-bucketSeconds / SmoothingTimeConstantSeconds);
|
||||
// a→1 means buckets are far finer than τ (heavy smoothing); a→0 means each bucket already
|
||||
// spans ≫ τ, so smoothing is a no-op. Either extreme is handled by the blend below.
|
||||
|
||||
// Forward pass.
|
||||
var env = data[0];
|
||||
for (var i = 0; i < data.Length; i++)
|
||||
{
|
||||
env = a * env + (1 - a) * data[i];
|
||||
data[i] = env;
|
||||
}
|
||||
|
||||
// Backward pass (zero-phase): smooth the forward result in reverse so the net lag is zero.
|
||||
env = data[^1];
|
||||
for (var i = data.Length - 1; i >= 0; i--)
|
||||
{
|
||||
env = a * env + (1 - a) * data[i];
|
||||
data[i] = env;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes one PCM sample at <paramref name="offset"/> to a normalized amplitude in [-1, 1].
|
||||
/// 8-bit is unsigned (0..255, centered at 128); 16/24/32-bit are signed little-endian.
|
||||
|
||||
Reference in New Issue
Block a user