From 856e2cbb9ee3b445614d1d25dfad4be7714795c5 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 27 Jul 2026 14:19:52 +0700 Subject: [PATCH] fix(audio): wait for complete analyzer metrics --- lib/widgets/audio_analysis_models.dart | 2 +- lib/widgets/audio_analysis_widget.dart | 72 +++++++++++++++-------- test/audio_analysis_spectrogram_test.dart | 31 ++++++++++ 3 files changed, 79 insertions(+), 26 deletions(-) diff --git a/lib/widgets/audio_analysis_models.dart b/lib/widgets/audio_analysis_models.dart index 83444fa0..11d0d3d0 100644 --- a/lib/widgets/audio_analysis_models.dart +++ b/lib/widgets/audio_analysis_models.dart @@ -3,7 +3,7 @@ part of 'audio_analysis_widget.dart'; // Analysis result models and per-run parameter records. class AudioAnalysisData { - static const cacheVersion = 5; + static const cacheVersion = 6; final String filePath; final int fileSize; diff --git a/lib/widgets/audio_analysis_widget.dart b/lib/widgets/audio_analysis_widget.dart index e2bf7b9a..f5b371e4 100644 --- a/lib/widgets/audio_analysis_widget.dart +++ b/lib/widgets/audio_analysis_widget.dart @@ -61,6 +61,37 @@ List buildAudioSpectrogramArguments({ ]; } +class AudioAstatsSummary { + final double peakDb; + final double rmsDb; + + const AudioAstatsSummary({required this.peakDb, required this.rmsDb}); +} + +AudioAstatsSummary? parseAudioAstatsSummary(String logs) { + final overallMatch = RegExp(r'Overall([\s\S]*)').firstMatch(logs); + final section = overallMatch?.group(1) ?? logs; + final peak = _parseLastAudioAstatsValue(section, 'Peak level dB'); + final rms = _parseLastAudioAstatsValue(section, 'RMS level dB'); + if (peak == null || rms == null) return null; + return AudioAstatsSummary(peakDb: peak, rmsDb: rms); +} + +double? _parseLastAudioAstatsValue(String text, String label) { + final matches = RegExp( + '${RegExp.escape(label)}:\\s*([-+]?\\d+(?:\\.\\d+)?)', + caseSensitive: false, + ).allMatches(text); + double? value; + for (final match in matches) { + final parsed = double.tryParse(match.group(1) ?? ''); + if (parsed != null && parsed.isFinite) { + value = parsed; + } + } + return value; +} + double? estimateBroadbandSpectralCutoffHz({ required Uint8List rgba, required int width, @@ -466,9 +497,12 @@ class _AudioAnalysisCardState extends State { ), ); final levelMetrics = await _runFullStreamLevelAnalysis(workingPath); + if (levelMetrics == null) { + throw Exception('FFmpeg level analysis returned no usable metrics'); + } final loudnessMetrics = await _runLoudnessAnalysis(workingPath); - final peakAmplitude = levelMetrics?.peakDb ?? 0; - final rmsLevel = levelMetrics?.rmsDb ?? peakAmplitude; + final peakAmplitude = levelMetrics.peakDb; + final rmsLevel = levelMetrics.rmsDb; final dynamicRange = peakAmplitude - rmsLevel; return _AudioAnalysisRunResult( @@ -492,9 +526,9 @@ class _AudioAnalysisCardState extends State { rmsLevel: rmsLevel, integratedLufs: loudnessMetrics?.integratedLufs, truePeakDb: loudnessMetrics?.truePeakDb, - clippingSamples: levelMetrics?.clippingSamples ?? 0, + clippingSamples: levelMetrics.clippingSamples, spectralCutoffHz: spectralCutoffHz, - channelStats: levelMetrics?.channelStats ?? const [], + channelStats: levelMetrics.channelStats, totalSamples: info.totalSamples, ), spectrogramImage: spectrogram.image, @@ -828,20 +862,19 @@ class _AudioAnalysisCardState extends State { return null; } - final logs = await session.getLogsAsString(); - final overallMatch = RegExp(r'Overall([\s\S]*)').firstMatch(logs); - final section = overallMatch?.group(1) ?? logs; - final peak = _parseLastAstatsValue(section, 'Peak level dB'); - final rms = _parseLastAstatsValue(section, 'RMS level dB'); - if (peak == null || rms == null) return null; + // FFmpegKit delivers logs asynchronously even after the process exits. + // The non-waiting getLogsAsString() can miss the final astats summary. + final logs = await session.getAllLogsAsString() ?? ''; + final summary = parseAudioAstatsSummary(logs); + if (summary == null) return null; final channelStats = _parseChannelStats(logs); final clippingSamples = channelStats.fold(0, (sum, stats) { if (stats.peakDb == null || stats.peakDb! < -0.1) return sum; return sum + stats.peakCount; }); return _LevelMetrics( - peakDb: peak, - rmsDb: rms, + peakDb: summary.peakDb, + rmsDb: summary.rmsDb, clippingSamples: clippingSamples, channelStats: channelStats, ); @@ -870,7 +903,7 @@ class _AudioAnalysisCardState extends State { '-', ]); - final logs = await session.getLogsAsString(); + final logs = await session.getAllLogsAsString() ?? ''; final integratedMatches = RegExp( r'I:\s+(-?\d+\.?\d*)\s+LUFS', ).allMatches(logs); @@ -928,18 +961,7 @@ class _AudioAnalysisCardState extends State { } double? _parseLastAstatsValue(String text, String label) { - final matches = RegExp( - '${RegExp.escape(label)}:\\s*([-+]?\\d+(?:\\.\\d+)?)', - caseSensitive: false, - ).allMatches(text); - double? value; - for (final match in matches) { - final parsed = double.tryParse(match.group(1) ?? ''); - if (parsed != null && parsed.isFinite) { - value = parsed; - } - } - return value; + return _parseLastAudioAstatsValue(text, label); } int? _parseLastAstatsInt(String text, String label) { diff --git a/test/audio_analysis_spectrogram_test.dart b/test/audio_analysis_spectrogram_test.dart index 749387d0..7bd60cfb 100644 --- a/test/audio_analysis_spectrogram_test.dart +++ b/test/audio_analysis_spectrogram_test.dart @@ -4,6 +4,37 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spotiflac_android/widgets/audio_analysis_widget.dart'; void main() { + group('audio level analysis', () { + test('reads peak and RMS from the final astats overall summary', () { + const logs = ''' +[Parsed_astats_0] Channel: 1 +[Parsed_astats_0] Peak level dB: -0.847144 +[Parsed_astats_0] RMS level dB: -12.935500 +[Parsed_astats_0] Channel: 2 +[Parsed_astats_0] Peak level dB: -0.861847 +[Parsed_astats_0] RMS level dB: -12.752564 +[Parsed_astats_0] Overall +[Parsed_astats_0] Peak level dB: -0.847144 +[Parsed_astats_0] RMS level dB: -12.843069 +'''; + + final summary = parseAudioAstatsSummary(logs); + + expect(summary, isNotNull); + expect(summary!.peakDb, closeTo(-0.847144, 0.000001)); + expect(summary.rmsDb, closeTo(-12.843069, 0.000001)); + }); + + test('rejects logs delivered without the final RMS metric', () { + const incompleteLogs = ''' +[Parsed_astats_0] Overall +[Parsed_astats_0] Peak level dB: -0.847144 +'''; + + expect(parseAudioAstatsSummary(incompleteLogs), isNull); + }); + }); + group('audio spectrogram filter', () { test('keeps source rate and uses a full-range float pipeline', () { final filter = buildAudioSpectrogramFilter();