From 7691acecefc55d87d754a9101ad8b742d82f7a46 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:21:11 +0700 Subject: [PATCH] perf(analysis): reuse sorted spectral percentile windows Reuse sorted windows for related percentiles and reject unsuitable edges before computing expensive tail statistics. Preserve cutoff results with 31 regression fixtures and 400 exact old/new comparisons. --- lib/widgets/audio_analysis_widget.dart | 62 +++++++-------- test/audio_analysis_spectrogram_test.dart | 97 +++++++++++++++++++++++ 2 files changed, 126 insertions(+), 33 deletions(-) diff --git a/lib/widgets/audio_analysis_widget.dart b/lib/widgets/audio_analysis_widget.dart index b516f6ee..1b463da7 100644 --- a/lib/widgets/audio_analysis_widget.dart +++ b/lib/widgets/audio_analysis_widget.dart @@ -430,18 +430,9 @@ double? estimateEffectiveSpectralCutoffHz({ final centralStart = math.max(0, (height * 0.05).floor()); final centralEnd = math.min(height, (height * 0.95).ceil()); - final lowLevel = _spectralPercentile( - smoothed, - centralStart, - centralEnd, - 0.10, - ); - final highLevel = _spectralPercentile( - smoothed, - centralStart, - centralEnd, - 0.95, - ); + final central = _sortedSpectralWindow(smoothed, centralStart, centralEnd); + final lowLevel = _sortedSpectralPercentile(central, 0.10); + final highLevel = _sortedSpectralPercentile(central, 0.95); final dynamicSpan = highLevel - lowLevel; if (highLevel < 24) return null; if (dynamicSpan < 1) return maxFrequencyHz; @@ -485,16 +476,19 @@ double? estimateEffectiveSpectralCutoffHz({ if (belowEnd > belowStart && aboveEnd > aboveStart) { final belowLevel = _spectralMedian(smoothed, belowStart, belowEnd); final aboveLevel = _spectralMedian(smoothed, aboveStart, aboveEnd); - final tailLevel = _spectralMedian(smoothed, aboveStart, height); + if (belowLevel - aboveLevel < minimumDrop) continue; + // All tail statistics share one sorted window. Rejected local edges do + // not need to copy and sort the rest of the spectrum at all. + final tail = _sortedSpectralWindow(smoothed, aboveStart, height); + final tailLevel = _sortedSpectralPercentile(tail, 0.50); + if (belowLevel - tailLevel < minimumDrop) continue; final tailSpread = - _spectralPercentile(smoothed, aboveStart, height, 0.80) - - _spectralPercentile(smoothed, aboveStart, height, 0.20); + _sortedSpectralPercentile(tail, 0.80) - + _sortedSpectralPercentile(tail, 0.20); + if (tailSpread > stableTailSpread) continue; final baseStart = math.max(0, edgeIndex - (3000 / hzPerRow).ceil()); final baseLevel = _spectralMedian(smoothed, baseStart, belowEnd); - if (belowLevel - aboveLevel >= minimumDrop && - belowLevel - tailLevel >= minimumDrop && - baseLevel - tailLevel >= minimumDrop && - tailSpread <= stableTailSpread) { + if (baseLevel - tailLevel >= minimumDrop) { final cutoff = (edgeIndex + 0.5) * hzPerRow; return cutoff.clamp(0.0, maxFrequencyHz).toDouble(); } @@ -523,12 +517,13 @@ double? estimateEffectiveSpectralCutoffHz({ if (belowEnd <= belowStart || aboveStart >= height) continue; final belowLevel = _spectralMedian(smoothed, belowStart, belowEnd); if (belowLevel < activeThreshold) continue; - final tailLevel = _spectralMedian(smoothed, aboveStart, height); + final tail = _sortedSpectralWindow(smoothed, aboveStart, height); + final tailLevel = _sortedSpectralPercentile(tail, 0.50); + if (belowLevel - tailLevel < activeMargin) continue; final tailSpread = - _spectralPercentile(smoothed, aboveStart, height, 0.80) - - _spectralPercentile(smoothed, aboveStart, height, 0.20); - if (belowLevel - tailLevel >= activeMargin && - tailSpread <= stableTailSpread) { + _sortedSpectralPercentile(tail, 0.80) - + _sortedSpectralPercentile(tail, 0.20); + if (tailSpread <= stableTailSpread) { final cutoffIndex = edgeIndex - supportRows / 2; final cutoff = (cutoffIndex + 0.5) * hzPerRow; return cutoff.clamp(0.0, maxFrequencyHz).toDouble(); @@ -571,19 +566,20 @@ double? estimateEffectiveSpectralCutoffHz({ } double _spectralMedian(Float64List values, int start, int end) { - return _spectralPercentile(values, start, end, 0.50); + return _sortedSpectralPercentile( + _sortedSpectralWindow(values, start, end), + 0.50, + ); } -double _spectralPercentile( - Float64List values, - int start, - int end, - double percentile, -) { +Float64List _sortedSpectralWindow(Float64List values, int start, int end) { final safeStart = start.clamp(0, values.length).toInt(); final safeEnd = end.clamp(safeStart, values.length).toInt(); - if (safeEnd <= safeStart) return 0; - final sorted = values.sublist(safeStart, safeEnd)..sort(); + return values.sublist(safeStart, safeEnd)..sort(); +} + +double _sortedSpectralPercentile(Float64List sorted, double percentile) { + if (sorted.isEmpty) return 0; final index = ((sorted.length - 1) * percentile) .round() .clamp(0, sorted.length - 1) diff --git a/test/audio_analysis_spectrogram_test.dart b/test/audio_analysis_spectrogram_test.dart index 6b5b1ab2..059fb306 100644 --- a/test/audio_analysis_spectrogram_test.dart +++ b/test/audio_analysis_spectrogram_test.dart @@ -222,6 +222,58 @@ lavfi.r128.true_peak=0.907 const height = 800; const nyquist = 96000.0; + test('preserves exact cutoffs across noisy and transient spectra', () { + // Captured from the estimator before sharing sorted percentile windows. + // Cover sharp/gradual limits, full-band slopes, noise and stepped bands + // at several resolutions and Nyquist frequencies. + const expected = { + 3: 5290.322580645161, + 7: 5406.25, + 11: 5403.508771929824, + 15: 5395.0, + 31: 14952.65625, + 47: 16305.0, + 63: 32610.0, + 79: 65220.0, + 83: 5032.258064516129, + 87: 5156.25, + 91: 5162.907268170426, + 95: 5155.0, + 111: 14291.15625, + 127: 15585.0, + 143: 31230.0, + 159: 62340.0, + 175: 8000.0, + 191: 22050.0, + 207: 24000.0, + 223: 48000.0, + 239: 96000.0, + 255: 8000.0, + 271: 22050.0, + 287: 24000.0, + 303: 48000.0, + 319: 96000.0, + 335: 5795.0, + 351: 16055.15625, + 367: 17505.0, + 383: 35010.0, + 399: 70020.0, + }; + for (final entry in expected.entries) { + final spectrum = _noisySpectrum(entry.key); + expect( + estimateEffectiveSpectralCutoffHz( + intensity: spectrum.intensity, + width: spectrum.width, + height: spectrum.height, + maxFrequencyHz: spectrum.maxFrequencyHz, + ), + entry.value, + reason: 'spectrum ${entry.key}', + ); + } + }); + test('ignores a narrow ultrasonic pilot above a 22 kHz music band', () { final intensity = _blankIntensity(width, height, value: 12); _paintFrequencyBand( @@ -585,6 +637,51 @@ lavfi.r128.true_peak=0.907 }); } +({Uint8List intensity, int width, int height, double maxFrequencyHz}) +_noisySpectrum(int seed) { + final width = [17, 83, 200, 400][seed % 4]; + final height = [31, 128, 399, 800][(seed ~/ 4) % 4]; + final maxFrequencyHz = [ + 8000.0, + 22050.0, + 24000.0, + 48000.0, + 96000.0, + ][(seed ~/ 16) % 5]; + final intensity = Uint8List(width * height); + var random = seed + 1; + final mode = (seed ~/ 80) % 5; + for (var y = 0; y < height; y++) { + final frequency = (height - y - 1) / (height - 1); + for (var x = 0; x < width; x++) { + random = (1664525 * random + 1013904223) & 0xffffffff; + final noise = (random >> 24) % 13; + final base = switch (mode) { + 0 => frequency < 0.68 ? 120 : 24, + 1 => frequency < 0.65 ? (160 - frequency * 170).round() : 24, + 2 => (140 - frequency * 110).round(), + 3 => 16 + (random >> 20) % 170, + _ => + frequency < 0.35 + ? 90 + : frequency < 0.73 + ? 64 + : 35, + }; + // Five percent broadband transients remain below the temporal P90. + intensity[y * width + x] = x < width ~/ 20 + ? 255 + : (base + noise).clamp(0, 255); + } + } + return ( + intensity: intensity, + width: width, + height: height, + maxFrequencyHz: maxFrequencyHz, + ); +} + Uint8List _blankIntensity(int width, int height, {int value = 0}) { final intensity = Uint8List(width * height); if (value > 0) intensity.fillRange(0, intensity.length, value);