From 2f8b91e1e2602bd37a3f44a245bafeed2047b1d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 01:36:18 +0000 Subject: [PATCH] Stream LoudnessMeter blocks through a ring buffer (drops ~127 MB analysis spike) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit integratedLUFS allocated a full-length [Double] weighted buffer (~127 MB for a 6-minute track) on top of the mono analysis buffer — a transient spike that ran while music plays and fed the jetsam budget. The weighted samples now stream through a single block-sized ring (~140 KB); each 400 ms block's mean-square is summed from the ring in the same order as before, so results are bit-identical (regression test compares against the old implementation verbatim, including at sample rates where the block isn't a whole multiple of the hop). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_013TJoWkqg8bzkGdzxWhWjWP --- .../ContinuityCore/LoudnessMeter.swift | 26 +++++---- .../LoudnessMeterTests.swift | 53 +++++++++++++++++++ 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/Packages/ContinuityCore/Sources/ContinuityCore/LoudnessMeter.swift b/Packages/ContinuityCore/Sources/ContinuityCore/LoudnessMeter.swift index 7e8738d..a56ab8b 100644 --- a/Packages/ContinuityCore/Sources/ContinuityCore/LoudnessMeter.swift +++ b/Packages/ContinuityCore/Sources/ContinuityCore/LoudnessMeter.swift @@ -23,26 +23,32 @@ public enum LoudnessMeter { var highPass = Biquad.highPass(f0: 38.13547087602444, q: 0.5003270373238773, sampleRate: sampleRate) - var weighted = [Double](repeating: 0, count: samples.count) - for i in 0.. 0, hop > 0, weighted.count >= block else { return nil } + guard block > 0, hop > 0, samples.count >= block else { return nil } + // Single streaming pass: weighted samples land in a block-sized ring instead of a + // full-length buffer (which cost ~127 MB on a 6-minute track — a jetsam-feeding spike + // while analysis runs during playback). Each block's mean-square is summed from the + // ring in logical order, so the arithmetic — order included — matches the old + // whole-buffer implementation exactly. + var ring = [Double](repeating: 0, count: block) var blockLoudness: [Double] = [] var blockMeanSquare: [Double] = [] - var start = 0 - while start + block <= weighted.count { + for i in 0..= block, (filled - block) % hop == 0 else { continue } + let start = filled - block var sum = 0.0 - for i in start..<(start + block) { sum += weighted[i] * weighted[i] } + for j in start.. Float in + // Deterministic multi-tone with a quiet stretch so both gates engage. + let t = Double(i) / rate + let quiet = t > 2.5 ? 0.001 : 1.0 + return Float(quiet * (0.08 * sin(2 * .pi * 997 * t) + 0.04 * sin(2 * .pi * 210 * t))) + } + let streamed = LoudnessMeter.integratedLUFS(samples: samples, sampleRate: rate) + let reference = Self.referenceLUFS(samples: samples, sampleRate: rate) + XCTAssertEqual(streamed, reference, "sample rate \(rate)") + } + } + + /// The pre-optimization implementation: full-length weighted buffer, then blocks. + private static func referenceLUFS(samples: [Float], sampleRate: Double) -> Double? { + guard sampleRate > 0, !samples.isEmpty else { return nil } + var shelf = LoudnessMeter.Biquad.highShelf(f0: 1681.974450955533, gainDB: 3.999843853973347, + q: 0.7071752369554196, sampleRate: sampleRate) + var highPass = LoudnessMeter.Biquad.highPass(f0: 38.13547087602444, q: 0.5003270373238773, + sampleRate: sampleRate) + var weighted = [Double](repeating: 0, count: samples.count) + for i in 0.. 0, hop > 0, weighted.count >= block else { return nil } + var blockLoudness: [Double] = [] + var blockMeanSquare: [Double] = [] + var start = 0 + while start + block <= weighted.count { + var sum = 0.0 + for i in start..<(start + block) { sum += weighted[i] * weighted[i] } + let ms = sum / Double(block) + blockMeanSquare.append(ms) + blockLoudness.append(-0.691 + 10 * log10(max(ms, .leastNormalMagnitude))) + start += hop + } + var gated = zip(blockMeanSquare, blockLoudness).filter { $0.1 > -70 } + guard !gated.isEmpty else { return nil } + let meanMS = gated.reduce(0) { $0 + $1.0 } / Double(gated.count) + let relativeThreshold = -0.691 + 10 * log10(meanMS) - 10 + gated = gated.filter { $0.1 > relativeThreshold } + guard !gated.isEmpty else { return nil } + let finalMS = gated.reduce(0) { $0 + $1.0 } / Double(gated.count) + return -0.691 + 10 * log10(finalMS) + } }