This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 73c816079b [TIKA-4791] Use LSF frame size for MPEG2/2.5 Layer III
(#2954)
73c816079b is described below
commit 73c816079bb1c7ebb32b31ea248b67eaf6df4270
Author: Dominik Schmidt <[email protected]>
AuthorDate: Wed Jul 15 23:04:14 2026 +0200
[TIKA-4791] Use LSF frame size for MPEG2/2.5 Layer III (#2954)
MpegStream computed every non-Layer-I frame with the MPEG1 values:
144 * bitrate / samplerate bytes and 1152 samples. MPEG2 and MPEG2.5
Layer III (the low sampling frequency mode of ISO/IEC 13818-3) carry a
single granule of 576 samples per frame, so the coefficient is 72.
Reference decoders (FFmpeg, mpg123, libmad) all halve these values for
LSF streams.
With the doubled length the frame walk skipped over the following
frame, visiting at best every second frame (where the doubled skip
landed mid-frame, the sync byte scan dropped further frames), and
AudioFrame.getLength()/getDuration() were wrong by exactly 2x. The
summed duration nearly cancelled for constant bitrate files but came
out over 10 percent short on 22050 Hz variable bitrate files.
Layer II keeps 1152 samples in every MPEG version and layer I was
already correct, so the frame length and duration now branch on both
version and layer. Regression tests cover all four combinations plus
an end-to-end duration check on a real ffmpeg-encoded 22050 Hz MPEG2
file that walks 79 frames to a clean EOF.
---
.../org/apache/tika/parser/mp3/MpegStream.java | 33 ++++++++--
.../org/apache/tika/parser/mp3/Mp3ParserTest.java | 17 ++++++
.../org/apache/tika/parser/mp3/MpegStreamTest.java | 68 +++++++++++++++++++++
.../test/resources/test-documents/testMP3mpeg2.mp3 | Bin 0 -> 8255 bytes
4 files changed, 112 insertions(+), 6 deletions(-)
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
index 28b7a71fe6..a87757253a 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
@@ -96,10 +96,18 @@ class MpegStream extends PushbackInputStream {
private static final int SAMPLE_COUNT_L1 = 384;
/**
- * Constant for the number of samples for a layer 2 or 3 frame.
+ * Constant for the number of samples for a layer 2 frame (all MPEG
+ * versions) and a layer 3 frame in MPEG1.
*/
private static final int SAMPLE_COUNT_L2 = 1152;
+ /**
+ * Constant for the number of samples for a layer 3 frame in MPEG2 and
+ * MPEG2.5: the low sampling frequency mode of ISO/IEC 13818-3 halves the
+ * frame to a single granule of 576 samples.
+ */
+ private static final int SAMPLE_COUNT_L3_LSF = 576;
+
/**
* Constant for the size of an MPEG frame header in bytes.
*/
@@ -172,15 +180,20 @@ class MpegStream extends PushbackInputStream {
/**
* Calculates the length of an MPEG frame based on the given parameters.
*
+ * @param mpegVer the MPEG version
* @param layer the layer
* @param bitRate the bit rate
* @param sampleRate the sample rate
* @param padding the padding flag
* @return the length of the frame in bytes
*/
- private static int calculateFrameLength(int layer, int bitRate, int
sampleRate, int padding) {
+ private static int calculateFrameLength(int mpegVer, int layer, int
bitRate, int sampleRate,
+ int padding) {
if (layer == AudioFrame.LAYER_1) {
return (12 * bitRate / sampleRate + padding) * 4;
+ } else if (layer == AudioFrame.LAYER_3 && mpegVer !=
AudioFrame.MPEG_V1) {
+ //MPEG2/2.5 layer 3 frames carry 576 samples instead of 1152
+ return 72 * bitRate / sampleRate + padding;
} else {
return 144 * bitRate / sampleRate + padding;
}
@@ -189,12 +202,20 @@ class MpegStream extends PushbackInputStream {
/**
* Calculates the duration of a MPEG frame based on the given parameters.
*
+ * @param mpegVer the MPEG version
* @param layer the layer
* @param sampleRate the sample rate
* @return the duration of this frame in milliseconds
*/
- private static float calculateDuration(int layer, int sampleRate) {
- int sampleCount = (layer == AudioFrame.LAYER_1) ? SAMPLE_COUNT_L1 :
SAMPLE_COUNT_L2;
+ private static float calculateDuration(int mpegVer, int layer, int
sampleRate) {
+ int sampleCount;
+ if (layer == AudioFrame.LAYER_1) {
+ sampleCount = SAMPLE_COUNT_L1;
+ } else if (layer == AudioFrame.LAYER_3 && mpegVer !=
AudioFrame.MPEG_V1) {
+ sampleCount = SAMPLE_COUNT_L3_LSF;
+ } else {
+ sampleCount = SAMPLE_COUNT_L2;
+ }
return (1000.0f / sampleRate) * sampleCount;
}
@@ -329,8 +350,8 @@ class MpegStream extends PushbackInputStream {
int bitRate = calculateBitRate(mpegVer, layer, bitRateCode);
int sampleRate = calculateSampleRate(mpegVer, sampleRateCode);
- int length = calculateFrameLength(layer, bitRate, sampleRate, padding);
- float duration = calculateDuration(layer, sampleRate);
+ int length = calculateFrameLength(mpegVer, layer, bitRate, sampleRate,
padding);
+ float duration = calculateDuration(mpegVer, layer, sampleRate);
int channels = calculateChannels(bits.get(6, 7));
return new AudioFrame(mpegVer, layer, bitRate, sampleRate, channels,
length, duration);
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
index 0bf7053c72..e59f5716ca 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
@@ -424,4 +424,21 @@ public class Mp3ParserTest extends TikaTest {
mp3.write(audio);
return mp3.toByteArray();
}
+
+ /**
+ * MPEG2 layer 3 frames carry 576 samples instead of 1152, so the frame
+ * walk must use the halved LSF frame length or it lands mid-frame and
+ * the summed duration drifts (TIKA-4791). The fixture is a 22050 Hz CBR
+ * file with 79 frames: 79 * 576 / 22050 = 2.06 seconds.
+ */
+ @Test
+ public void testMp3Mpeg2LowSamplingFrequency() throws Exception {
+ Metadata metadata = new Metadata();
+ getText("testMP3mpeg2.mp3", metadata);
+
+ assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE));
+ assertEquals("MPEG 3 Layer III Version 2", metadata.get("version"));
+ assertEquals("22050", metadata.get(XMPDM.AUDIO_SAMPLE_RATE));
+ assertEquals(2.0637f, Float.parseFloat(metadata.get(XMPDM.DURATION)),
0.005f);
+ }
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/MpegStreamTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/MpegStreamTest.java
index 25dc65563e..049ee0b288 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/MpegStreamTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/MpegStreamTest.java
@@ -140,6 +140,74 @@ public class MpegStreamTest {
assertNull(stream.nextFrame(), "Got a frame");
}
+ /**
+ * Parses a single frame header from the given three header bytes
+ * following the initial 0xFF sync byte.
+ */
+ private AudioFrame parseHeader(int b2, int b3, int b4) throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ writeFrame(bos, b2, b3, b4);
+ TikaInputStream tis = TikaInputStream.get(bos.toByteArray());
+ stream = new MpegStream(tis);
+ AudioFrame header = stream.nextFrame();
+ assertNotNull(header, "No header found");
+ return header;
+ }
+
+ /**
+ * An MPEG1 layer 3 frame carries 1152 samples: 128 kbps at 44100 Hz
+ * gives 144 * 128000 / 44100 = 417 bytes and 26.12 ms.
+ */
+ @Test
+ public void testFrameLengthAndDurationMpeg1Layer3() throws IOException {
+ AudioFrame header = parseHeader(0xFB, 0x90, 0);
+ assertEquals(AudioFrame.MPEG_V1, header.getVersionCode(), "Wrong MPEG
version");
+ assertEquals(AudioFrame.LAYER_3, header.getLayer(), "Wrong layer");
+ assertEquals(417, header.getLength(), "Wrong frame length");
+ assertEquals(1152000f / 44100, header.getDuration(), 0.01f, "Wrong
duration");
+ }
+
+ /**
+ * An MPEG2 layer 3 frame carries only 576 samples (LSF mode), so
+ * 80 kbps at 24000 Hz with padding gives 72 * 80000 / 24000 + 1 = 241
+ * bytes and 24 ms, half of what the MPEG1 formula would report.
+ */
+ @Test
+ public void testFrameLengthAndDurationMpeg2Layer3() throws IOException {
+ AudioFrame header = parseHeader(0xF3, 0x96, 0);
+ assertEquals(AudioFrame.MPEG_V2, header.getVersionCode(), "Wrong MPEG
version");
+ assertEquals(AudioFrame.LAYER_3, header.getLayer(), "Wrong layer");
+ assertEquals(241, header.getLength(), "Wrong frame length");
+ assertEquals(24.0f, header.getDuration(), 0.01f, "Wrong duration");
+ }
+
+ /**
+ * MPEG2.5 layer 3 also uses the halved LSF frame: 80 kbps at 12000 Hz
+ * with padding gives 72 * 80000 / 12000 + 1 = 481 bytes and 48 ms.
+ */
+ @Test
+ public void testFrameLengthAndDurationMpeg25Layer3() throws IOException {
+ AudioFrame header = parseHeader(0xE3, 0x96, 0);
+ assertEquals(AudioFrame.MPEG_V2_5, header.getVersionCode(), "Wrong
MPEG version");
+ assertEquals(AudioFrame.LAYER_3, header.getLayer(), "Wrong layer");
+ assertEquals(481, header.getLength(), "Wrong frame length");
+ assertEquals(48.0f, header.getDuration(), 0.01f, "Wrong duration");
+ }
+
+ /**
+ * Layer 2 keeps 1152 samples in every MPEG version, so the MPEG2 frame
+ * stays at the full length: 80 kbps at 24000 Hz with padding gives
+ * 144 * 80000 / 24000 + 1 = 481 bytes and 48 ms.
+ */
+ @Test
+ public void testFrameLengthAndDurationMpeg2Layer2() throws IOException {
+ AudioFrame header = parseHeader(0xF5, 0x96, 0);
+ assertEquals(AudioFrame.MPEG_V2, header.getVersionCode(), "Wrong MPEG
version");
+ assertEquals(AudioFrame.LAYER_2, header.getLayer(), "Wrong layer");
+ assertEquals(481, header.getLength(), "Wrong frame length");
+ assertEquals(48.0f, header.getDuration(), 0.01f, "Wrong duration");
+ }
+
/**
* Tries to skip a frame if no current header is available.
*/
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3mpeg2.mp3
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3mpeg2.mp3
new file mode 100644
index 0000000000..da66ead5ea
Binary files /dev/null and
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3mpeg2.mp3
differ