From 4a46ec36b374ea6145b8f8f65a6b3ce12a034003 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Thu, 11 Jun 2026 06:13:20 -0400 Subject: [PATCH] fix(mp3): remove dead FrameSize field, fix CBR duration ID3 exclusion, add MPEG2 bitrate table, pin CBR test assertions --- DeepDrftContent/Processors/Mp3AudioProcessor.cs | 15 ++++++++------- DeepDrftTests/AudioProcessorTests.cs | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/DeepDrftContent/Processors/Mp3AudioProcessor.cs b/DeepDrftContent/Processors/Mp3AudioProcessor.cs index d8f1296..398ea7d 100644 --- a/DeepDrftContent/Processors/Mp3AudioProcessor.cs +++ b/DeepDrftContent/Processors/Mp3AudioProcessor.cs @@ -14,6 +14,10 @@ public class Mp3AudioProcessor private static readonly int[] Mpeg1Layer3Bitrates = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]; + // MPEG2/2.5 Layer III bitrate table (kbps), indexed by 4-bit bitrate index. 0 = free, 15 = bad. + private static readonly int[] Mpeg2Layer3Bitrates = + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]; + private static readonly int[] Mpeg1SampleRates = [44100, 48000, 32000]; private static readonly int[] Mpeg2SampleRates = [22050, 24000, 16000]; private static readonly int[] Mpeg25SampleRates = [11025, 12000, 8000]; @@ -169,7 +173,8 @@ public class Mp3AudioProcessor }; var bitrateIndex = (b2 >> 4) & 0x0F; - var bitrateKbps = Mpeg1Layer3Bitrates[bitrateIndex]; + var bitrateTable = version == MpegVersion.Mpeg1 ? Mpeg1Layer3Bitrates : Mpeg2Layer3Bitrates; + var bitrateKbps = bitrateTable[bitrateIndex]; var sampleRateIndex = (b2 >> 2) & 0x03; var sampleRate = version switch @@ -179,13 +184,10 @@ public class Mp3AudioProcessor _ => Mpeg25SampleRates[sampleRateIndex], }; - var paddingBit = (b2 >> 1) & 0x01; var channelMode = (b3 >> 6) & 0x03; var channels = channelMode == 3 ? 1 : 2; var samplesPerFrame = version == MpegVersion.Mpeg1 ? 1152 : 576; - var frameSize = (int)Math.Floor(144.0 * (bitrateKbps * 1000) / sampleRate) + paddingBit; - return new FrameHeader { Version = version, @@ -193,7 +195,6 @@ public class Mp3AudioProcessor SampleRate = sampleRate, Channels = channels, SamplesPerFrame = samplesPerFrame, - FrameSize = frameSize, }; } @@ -216,8 +217,9 @@ public class Mp3AudioProcessor } // CBR fallback: bitrate_kbps * 1000 / 8 bytes per second = bitrate_kbps * 125. + // Exclude the ID3v2 tag bytes (everything before frameStart) from the estimate. var bytesPerSecond = header.BitrateKbps * 125; - return bytesPerSecond > 0 ? (double)buffer.Length / bytesPerSecond : FallbackDuration; + return bytesPerSecond > 0 ? (double)(buffer.Length - frameStart) / bytesPerSecond : FallbackDuration; } /// @@ -300,7 +302,6 @@ public class Mp3AudioProcessor public int SampleRate { get; init; } public int Channels { get; init; } public int SamplesPerFrame { get; init; } - public int FrameSize { get; init; } } private sealed class Mp3Metadata diff --git a/DeepDrftTests/AudioProcessorTests.cs b/DeepDrftTests/AudioProcessorTests.cs index 9c66bed..edac1cc 100644 --- a/DeepDrftTests/AudioProcessorTests.cs +++ b/DeepDrftTests/AudioProcessorTests.cs @@ -164,14 +164,23 @@ public class AudioProcessorTests [Test] public async Task Mp3_CbrMetadata_ParsedCorrectly() { - var path = await WriteAudioAsync(BuildMinimalMp3(bitrateKbps: 128, sampleRate: 44100, stereo: true), ".mp3"); + // BuildMinimalMp3: bitrateKbps=128, sampleRate=44100, stereo=true, no Xing tag, no ID3 tag. + // frameSize = floor(144 * 128000 / 44100) = 417 bytes; bufferSize = max(417, 48) = 417. + // CBR duration = (bufferLength - frameStart) / (bitrateKbps * 125) = 417 / 16000 ≈ 0.0261 s. + const int bitrateKbps = 128; + const int sampleRate = 44100; + var frameSize = (int)Math.Floor(144.0 * (bitrateKbps * 1000) / sampleRate); // 417 + var bufferSize = Math.Max(frameSize, 4 + 32 + 12); // max(417, 48) = 417 + var expectedDuration = (double)bufferSize / (bitrateKbps * 125); // frameStart = 0 (no ID3) + + var path = await WriteAudioAsync(BuildMinimalMp3(bitrateKbps: bitrateKbps, sampleRate: sampleRate, stereo: true), ".mp3"); var audio = await new Mp3AudioProcessor().ProcessMp3FileAsync(path); Assert.That(audio, Is.Not.Null); Assert.That(audio!.Extension, Is.EqualTo(".mp3")); - Assert.That(audio.Duration, Is.GreaterThan(0.0)); - Assert.That(audio.Bitrate, Is.GreaterThan(0)); + Assert.That(audio.Bitrate, Is.EqualTo(bitrateKbps)); + Assert.That(audio.Duration, Is.EqualTo(expectedDuration).Within(0.01)); } [Test]