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) + } }