fix(mp3): remove dead FrameSize field, fix CBR duration ID3 exclusion, add MPEG2 bitrate table, pin CBR test assertions
This commit is contained in:
@@ -14,6 +14,10 @@ public class Mp3AudioProcessor
|
|||||||
private static readonly int[] Mpeg1Layer3Bitrates =
|
private static readonly int[] Mpeg1Layer3Bitrates =
|
||||||
[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320];
|
[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[] Mpeg1SampleRates = [44100, 48000, 32000];
|
||||||
private static readonly int[] Mpeg2SampleRates = [22050, 24000, 16000];
|
private static readonly int[] Mpeg2SampleRates = [22050, 24000, 16000];
|
||||||
private static readonly int[] Mpeg25SampleRates = [11025, 12000, 8000];
|
private static readonly int[] Mpeg25SampleRates = [11025, 12000, 8000];
|
||||||
@@ -169,7 +173,8 @@ public class Mp3AudioProcessor
|
|||||||
};
|
};
|
||||||
|
|
||||||
var bitrateIndex = (b2 >> 4) & 0x0F;
|
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 sampleRateIndex = (b2 >> 2) & 0x03;
|
||||||
var sampleRate = version switch
|
var sampleRate = version switch
|
||||||
@@ -179,13 +184,10 @@ public class Mp3AudioProcessor
|
|||||||
_ => Mpeg25SampleRates[sampleRateIndex],
|
_ => Mpeg25SampleRates[sampleRateIndex],
|
||||||
};
|
};
|
||||||
|
|
||||||
var paddingBit = (b2 >> 1) & 0x01;
|
|
||||||
var channelMode = (b3 >> 6) & 0x03;
|
var channelMode = (b3 >> 6) & 0x03;
|
||||||
var channels = channelMode == 3 ? 1 : 2;
|
var channels = channelMode == 3 ? 1 : 2;
|
||||||
var samplesPerFrame = version == MpegVersion.Mpeg1 ? 1152 : 576;
|
var samplesPerFrame = version == MpegVersion.Mpeg1 ? 1152 : 576;
|
||||||
|
|
||||||
var frameSize = (int)Math.Floor(144.0 * (bitrateKbps * 1000) / sampleRate) + paddingBit;
|
|
||||||
|
|
||||||
return new FrameHeader
|
return new FrameHeader
|
||||||
{
|
{
|
||||||
Version = version,
|
Version = version,
|
||||||
@@ -193,7 +195,6 @@ public class Mp3AudioProcessor
|
|||||||
SampleRate = sampleRate,
|
SampleRate = sampleRate,
|
||||||
Channels = channels,
|
Channels = channels,
|
||||||
SamplesPerFrame = samplesPerFrame,
|
SamplesPerFrame = samplesPerFrame,
|
||||||
FrameSize = frameSize,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,8 +217,9 @@ public class Mp3AudioProcessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CBR fallback: bitrate_kbps * 1000 / 8 bytes per second = bitrate_kbps * 125.
|
// 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;
|
var bytesPerSecond = header.BitrateKbps * 125;
|
||||||
return bytesPerSecond > 0 ? (double)buffer.Length / bytesPerSecond : FallbackDuration;
|
return bytesPerSecond > 0 ? (double)(buffer.Length - frameStart) / bytesPerSecond : FallbackDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -300,7 +302,6 @@ public class Mp3AudioProcessor
|
|||||||
public int SampleRate { get; init; }
|
public int SampleRate { get; init; }
|
||||||
public int Channels { get; init; }
|
public int Channels { get; init; }
|
||||||
public int SamplesPerFrame { get; init; }
|
public int SamplesPerFrame { get; init; }
|
||||||
public int FrameSize { get; init; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class Mp3Metadata
|
private sealed class Mp3Metadata
|
||||||
|
|||||||
@@ -164,14 +164,23 @@ public class AudioProcessorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Mp3_CbrMetadata_ParsedCorrectly()
|
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);
|
var audio = await new Mp3AudioProcessor().ProcessMp3FileAsync(path);
|
||||||
|
|
||||||
Assert.That(audio, Is.Not.Null);
|
Assert.That(audio, Is.Not.Null);
|
||||||
Assert.That(audio!.Extension, Is.EqualTo(".mp3"));
|
Assert.That(audio!.Extension, Is.EqualTo(".mp3"));
|
||||||
Assert.That(audio.Duration, Is.GreaterThan(0.0));
|
Assert.That(audio.Bitrate, Is.EqualTo(bitrateKbps));
|
||||||
Assert.That(audio.Bitrate, Is.GreaterThan(0));
|
Assert.That(audio.Duration, Is.EqualTo(expectedDuration).Within(0.01));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
Reference in New Issue
Block a user