From 05dfac6be40c1d6f650ddf2a8f94dce9fd8dd9b3 Mon Sep 17 00:00:00 2001 From: Courtney Tompos Date: Sat, 24 Oct 2026 13:36:26 -0400 Subject: [PATCH] fix: preserve decoder length and correct MP3 bitrate metadata --- Src/Metadata/Readers/Format/Mp3Reader.cs | 3 +- Src/Providers/StreamDataProvider.cs | 55 ++++++++---------------- 2 files changed, 19 insertions(+), 39 deletions(-) diff --git a/Src/Metadata/Readers/Format/Mp3Reader.cs b/Src/Metadata/Readers/Format/Mp3Reader.cs index 7e7e3e51..8bc077e7 100644 --- a/Src/Metadata/Readers/Format/Mp3Reader.cs +++ b/Src/Metadata/Readers/Format/Mp3Reader.cs @@ -183,7 +183,8 @@ private Result ParseFrameHeader(byte[] header, SoundFormatInfo info) new[,] { { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 } }; } - info.Bitrate = layerBitrateTable[0, bitrateIndex] * 1000; + var bitrateRow = layerId == 1 ? mpegVersionIndex : 0; + info.Bitrate = layerBitrateTable[bitrateRow, bitrateIndex] * 1000; info.SampleRate = SampleRateTable[mpegVersionIndex, sampleRateIndex]; info.ChannelCount = channelMode == 3 ? 1 : 2; diff --git a/Src/Providers/StreamDataProvider.cs b/Src/Providers/StreamDataProvider.cs index 22bc951b..7e1ec27d 100644 --- a/Src/Providers/StreamDataProvider.cs +++ b/Src/Providers/StreamDataProvider.cs @@ -123,51 +123,30 @@ public StreamDataProvider(AudioEngine engine, AudioFormat format, Stream stream) /// Gets the total length of the audio data in samples. /// /// - /// Two independent length estimates are available at this layer: - /// - /// _decoder.Length — returned by the underlying codec (e.g. ma_decoder_get_length_in_pcm_frames). - /// FormatInfo.Duration — read by from ID3/Xing/Info tags. - /// - /// Both can be wrong for VBR MP3s without a Xing/Info header, but they fail independently: the metadata - /// reader and the codec use different parsing paths. When they disagree, we pick the larger value. - /// A too-small length is far worse than a too-large one — it makes the displayed Duration lie, - /// breaks seeking near the end, can trigger premature end-of-stream handling, and produces impossible - /// states like Time > Duration + /// Prefers the decoder's reported length when available. Metadata duration is a fallback + /// estimate and can overstate the audio length, for example for VBR MP3s without a Xing header. + /// The fallback uses the decoder's output sample rate and channel count so its sample units + /// agree with decoded data, including when the source is resampled or remixed. /// public int Length { get { var decoderLength = _decoder.Length; - - int metadataLength; - if (FormatInfo != null - && FormatInfo.Duration > TimeSpan.Zero - && FormatInfo.SampleRate > 0 - && FormatInfo.ChannelCount > 0) - { - var metadataLengthSamples = FormatInfo.Duration.TotalSeconds - * FormatInfo.SampleRate - * FormatInfo.ChannelCount; - - if (metadataLengthSamples is >= 0 and <= int.MaxValue) - metadataLength = (int)metadataLengthSamples; - else - metadataLength = 0; - } - else - { - metadataLength = 0; - } + if (decoderLength > 0) + return decoderLength; - return decoderLength switch - { - // Prefer the larger value when both are available. See the XML doc above for the rationale. - // Special case: if one of them is 0 (unknown), use the other. - > 0 when metadataLength > 0 => Math.Max(decoderLength, metadataLength), - > 0 => decoderLength, - _ => metadataLength - }; + if (FormatInfo == null || FormatInfo.Duration <= TimeSpan.Zero + || _decoder.SampleRate <= 0 || _decoder.Channels <= 0) + return 0; + + var metadataLengthSamples = FormatInfo.Duration.TotalSeconds + * _decoder.SampleRate + * _decoder.Channels; + + return metadataLengthSamples is >= 0 and <= int.MaxValue + ? (int)metadataLengthSamples + : 0; } }