mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-26 12:52:40 +02:00
fix(analysis): reject false low spectral cutoffs
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user