fix(analysis): detect full-band spectral cutoff

This commit is contained in:
zarzet
2026-07-30 15:14:36 +07:00
parent 5357e14b06
commit d85c8f6a17
19 changed files with 144 additions and 15 deletions
+9 -11
View File
@@ -171,13 +171,16 @@ class _AudioInfoCard extends StatelessWidget {
value: _formatClipping(context, data.clippingSamples),
cs: cs,
),
if (data.spectralCutoffHz != null)
_MetricChip(
icon: Icons.filter_alt_outlined,
label: context.l10n.audioAnalysisSpectralCutoff,
value: _formatFrequency(data.spectralCutoffHz!),
cs: cs,
_MetricChip(
icon: Icons.filter_alt_outlined,
label: context.l10n.audioAnalysisSpectralCutoff,
value: formatAudioAnalysisSpectralCutoff(
data.spectralCutoffHz,
notDetectedLabel:
context.l10n.audioAnalysisCutoffNotDetected,
),
cs: cs,
),
_MetricChip(
icon: Icons.numbers,
label: context.l10n.audioAnalysisSamples,
@@ -228,11 +231,6 @@ class _AudioInfoCard extends StatelessWidget {
return data.channels > 0 ? '${data.channels}' : 'N/A';
}
String _formatFrequency(double hz) {
if (hz >= 1000) return '${(hz / 1000).toStringAsFixed(1)} kHz';
return '${hz.round()} Hz';
}
String _formatBitrate(int bitsPerSecond) {
if (bitsPerSecond >= 1000000) {
return '${(bitsPerSecond / 1000000).toStringAsFixed(2)} Mbps';
+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 = 7;
static const cacheVersion = 8;
final String filePath;
final int fileSize;
+19 -3
View File
@@ -24,6 +24,19 @@ const int audioSpectrogramHeight = 800;
const int audioSpectralAnalysisWidth = 400;
const double audioSpectrogramDynamicRangeDb = 120;
String formatAudioAnalysisSpectralCutoff(
double? cutoffHz, {
required String notDetectedLabel,
}) {
if (cutoffHz == null || !cutoffHz.isFinite || cutoffHz <= 0) {
return notDetectedLabel;
}
if (cutoffHz >= 1000) {
return '${(cutoffHz / 1000).toStringAsFixed(1)} kHz';
}
return '${cutoffHz.round()} Hz';
}
String _buildShowspectrumOptions({required int width, required String color}) {
return 'showspectrumpic='
's=${width}x$audioSpectrogramHeight:'
@@ -385,8 +398,10 @@ double? estimateEffectiveSpectralCutoffHz({
}
// A genuinely broadband signal with no internal falling edge reaches the
// analysis ceiling. Report Nyquist only when both its baseband and top band
// are populated; silence or an isolated high-frequency line returns null.
// 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.
final basebandLevel = _spectralMedian(
smoothed,
(height * 0.05).floor(),
@@ -397,7 +412,8 @@ double? estimateEffectiveSpectralCutoffHz({
(height * 0.90).floor(),
math.max(1, (height * 0.98).floor()),
);
if (basebandLevel >= 24 && topBandLevel >= basebandLevel - minimumDrop) {
final populatedTopFloor = math.max(24.0, lowLevel * 0.60);
if (basebandLevel >= 24 && topBandLevel >= populatedTopFloor) {
return maxFrequencyHz;
}
return null;