fix(analysis): reject false low spectral cutoffs

This commit is contained in:
zarzet
2026-08-23 11:07:59 +07:00
parent c9cf26fea6
commit 818f1d3a10
3 changed files with 122 additions and 11 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ part of 'audio_analysis_widget.dart';
// Analysis result models and per-run parameter records.
class AudioAnalysisData {
static const cacheVersion = 9;
static const cacheVersion = 10;
final String filePath;
final int fileSize;
+53 -10
View File
@@ -438,13 +438,14 @@ double? estimateEffectiveSpectralCutoffHz({
});
final minimumDrop = math.max(12.0, dynamicSpan * 0.18);
final gapRows = math.max(1, (100 / hzPerRow).ceil());
final supportRows = math.max(3, (1200 / hzPerRow).ceil());
final stableTailSpread = math.max(4.0, dynamicSpan * 0.06);
for (final bestStart in candidateStarts) {
final bestLocalDrop =
smoothed[bestStart] - smoothed[bestStart + edgeSpanRows];
if (bestLocalDrop < minimumDrop * 0.60) break;
final edgeIndex = (bestStart + edgeSpanRows / 2).round();
final gapRows = math.max(1, (100 / hzPerRow).ceil());
final supportRows = math.max(3, (1200 / hzPerRow).ceil());
final belowEnd = edgeIndex - gapRows;
final belowStart = math.max(0, belowEnd - supportRows);
final aboveStart = edgeIndex + gapRows;
@@ -453,22 +454,55 @@ double? estimateEffectiveSpectralCutoffHz({
final belowLevel = _spectralMedian(smoothed, belowStart, belowEnd);
final aboveLevel = _spectralMedian(smoothed, aboveStart, aboveEnd);
final tailLevel = _spectralMedian(smoothed, aboveStart, height);
final tailSpread =
_spectralPercentile(smoothed, aboveStart, height, 0.80) -
_spectralPercentile(smoothed, aboveStart, height, 0.20);
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) {
baseLevel - tailLevel >= minimumDrop &&
tailSpread <= stableTailSpread) {
final cutoff = (edgeIndex + 0.5) * hzPerRow;
return cutoff.clamp(0.0, maxFrequencyHz).toDouble();
}
}
}
// A genuinely broadband signal with no internal falling edge reaches the
// analysis ceiling. Natural music has a pronounced spectral tilt, so the
// top band does not need to be almost as loud as the baseband. It must still
// sit clearly above the measured low-level floor; silence or an isolated
// high-frequency line therefore continues to return null.
// Some masters roll off over several kilohertz instead of ending at one
// sharp edge. Find the highest sustained band that remains clearly above a
// stable high-frequency floor so ordinary musical spectral tilt is not
// mistaken for a low-pass cutoff.
final tailReferenceLevel = _spectralMedian(
smoothed,
(height * 0.90).floor(),
math.max(1, (height * 0.98).floor()),
);
final activeMargin = math.max(10.0, dynamicSpan * 0.20);
final activeThreshold = tailReferenceLevel + activeMargin;
for (var edgeIndex = searchEnd - 1; edgeIndex >= searchStart; edgeIndex--) {
final belowEnd = edgeIndex - gapRows;
final belowStart = math.max(0, belowEnd - supportRows);
final aboveStart = edgeIndex + gapRows;
if (belowEnd <= belowStart || aboveStart >= height) continue;
final belowLevel = _spectralMedian(smoothed, belowStart, belowEnd);
if (belowLevel < activeThreshold) continue;
final tailLevel = _spectralMedian(smoothed, aboveStart, height);
final tailSpread =
_spectralPercentile(smoothed, aboveStart, height, 0.80) -
_spectralPercentile(smoothed, aboveStart, height, 0.20);
if (belowLevel - tailLevel >= activeMargin &&
tailSpread <= stableTailSpread) {
final cutoffIndex = edgeIndex - supportRows / 2;
final cutoff = (cutoffIndex + 0.5) * hzPerRow;
return cutoff.clamp(0.0, maxFrequencyHz).toDouble();
}
}
// A genuinely broadband signal with no stable tail plateau reaches the
// analysis ceiling. Natural music may retain a pronounced spectral tilt,
// so require populated baseband and top bands instead of similar levels.
// Silence and isolated high-frequency lines still return null.
final basebandLevel = _spectralMedian(
smoothed,
(height * 0.05).floor(),
@@ -479,8 +513,17 @@ double? estimateEffectiveSpectralCutoffHz({
(height * 0.90).floor(),
math.max(1, (height * 0.98).floor()),
);
final populatedTopFloor = math.max(24.0, lowLevel * 0.60);
if (basebandLevel >= 24 && topBandLevel >= populatedTopFloor) {
final lowerTopBandLevel = _spectralMedian(
smoothed,
(height * 0.80).floor(),
math.max(1, (height * 0.90).floor()),
);
final topBandNearPeak = topBandLevel >= highLevel - minimumDrop;
final topBandStillSloping =
lowerTopBandLevel - topBandLevel > stableTailSpread;
if (basebandLevel >= 24 &&
topBandLevel >= 24 &&
(topBandNearPeak || topBandStillSloping)) {
return maxFrequencyHz;
}
return null;
+68
View File
@@ -275,6 +275,50 @@ lavfi.r128.true_peak=0.907
expect(cutoff!, inInclusiveRange(15300, 16300));
});
test('ignores a tonal drop before a gradual 22 kHz bandwidth limit', () {
const hiresNyquist = 48000.0;
final intensity = _blankIntensity(width, height, value: 46);
_paintFrequencyBand(
intensity,
width: width,
height: height,
maxFrequencyHz: hiresNyquist,
lowHz: 0,
highHz: 4000,
intensity: 120,
);
_paintFrequencySlope(
intensity,
width: width,
height: height,
maxFrequencyHz: hiresNyquist,
lowHz: 4000,
highHz: 20000,
lowIntensity: 115,
highIntensity: 75,
);
_paintFrequencySlope(
intensity,
width: width,
height: height,
maxFrequencyHz: hiresNyquist,
lowHz: 20000,
highHz: 23500,
lowIntensity: 75,
highIntensity: 46,
);
final cutoff = estimateEffectiveSpectralCutoffHz(
intensity: intensity,
width: width,
height: height,
maxFrequencyHz: hiresNyquist,
);
expect(cutoff, isNotNull);
expect(cutoff!, inInclusiveRange(20500, 22500));
});
test('retains genuine broadband ultrasonic content', () {
final intensity = _blankIntensity(width, height, value: 10);
_paintFrequencyBand(
@@ -418,3 +462,27 @@ void _paintNaturalSpectralTilt(
}
}
}
void _paintFrequencySlope(
Uint8List values, {
required int width,
required int height,
required double maxFrequencyHz,
required double lowHz,
required double highHz,
required int lowIntensity,
required int highIntensity,
}) {
final frequencySpan = highHz - lowHz;
for (var y = 0; y < height; y++) {
final frequency = (height - y - 0.5) / height * maxFrequencyHz;
if (frequency < lowHz || frequency > highHz) continue;
final progress = (frequency - lowHz) / frequencySpan;
final intensity = (lowIntensity + (highIntensity - lowIntensity) * progress)
.round()
.clamp(0, 255);
for (var x = 0; x < width; x++) {
values[y * width + x] = intensity;
}
}
}