From 9472b58aab0b64ad3fda3b15f8448725ce975df1 Mon Sep 17 00:00:00 2001 From: Marko Zajc Date: Mon, 14 Aug 2023 00:14:03 +0200 Subject: [PATCH] Support MPEG 2.5 --- .../container/mp3/Mp3FrameReader.java | 20 +- .../lavaplayer/natives/mp3/Mp3Decoder.java | 239 ++++++++---------- 2 files changed, 124 insertions(+), 135 deletions(-) diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3FrameReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3FrameReader.java index 31e2930e..0698c626 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3FrameReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3FrameReader.java @@ -88,16 +88,22 @@ private int copyScanBufferEndToBeginning() { } private boolean parseFrameAt(int scanOffset) { - if (scanOffset >= HEADER_SIZE && (frameSize = Mp3Decoder.getFrameSize(scanBuffer, scanOffset - HEADER_SIZE)) > 0) { - for (int i = 0; i < HEADER_SIZE; i++) { - frameBuffer[i] = scanBuffer[scanOffset - HEADER_SIZE + i]; - } + int offset = scanOffset - HEADER_SIZE; + boolean invalid = offset < 0 + || !Mp3Decoder.hasFrameSync(scanBuffer, offset) + || Mp3Decoder.isUnsupportedVersion(scanBuffer, offset) + || !Mp3Decoder.isValidFrame(scanBuffer, offset); - frameBufferPosition = HEADER_SIZE; - return true; + if (invalid) + return false; + + frameSize = Mp3Decoder.getFrameSize(scanBuffer, offset); + for (int i = 0; i < HEADER_SIZE; i++) { + frameBuffer[i] = scanBuffer[offset + i]; } - return false; + frameBufferPosition = HEADER_SIZE; + return true; } /** diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3Decoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3Decoder.java index 6df56ac1..715f4aec 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3Decoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3Decoder.java @@ -2,6 +2,8 @@ import com.sedmelluq.lava.common.natives.NativeResourceHolder; +import java.util.Arrays; + import java.nio.ByteBuffer; import java.nio.ShortBuffer; @@ -69,81 +71,7 @@ protected void freeResources() { } private static int getFrameBitRate(byte[] buffer, int offset) { - return isMpegVersionOne(buffer, offset) ? getFrameBitRateV1(buffer, offset) : getFrameBitRateV2(buffer, offset); - } - - private static int getFrameBitRateV1(byte[] buffer, int offset) { - switch ((buffer[offset + 2] & 0xF0) >>> 4) { - case 1: - return 32000; - case 2: - return 40000; - case 3: - return 48000; - case 4: - return 56000; - case 5: - return 64000; - case 6: - return 80000; - case 7: - return 96000; - case 8: - return 112000; - case 9: - return 128000; - case 10: - return 160000; - case 11: - return 192000; - case 12: - return 224000; - case 13: - return 256000; - case 14: - return 320000; - default: - throw new IllegalArgumentException("Not valid bitrate"); - } - } - - private static int getFrameBitRateV2(byte[] buffer, int offset) { - switch ((buffer[offset + 2] & 0xF0) >>> 4) { - case 1: - return 8000; - case 2: - return 16000; - case 3: - return 24000; - case 4: - return 32000; - case 5: - return 40000; - case 6: - return 48000; - case 7: - return 56000; - case 8: - return 64000; - case 9: - return 80000; - case 10: - return 96000; - case 11: - return 112000; - case 12: - return 128000; - case 13: - return 144000; - case 14: - return 160000; - default: - throw new IllegalArgumentException("Not valid bitrate"); - } - } - - private static int calculateFrameSize(boolean isVersionOne, int bitRate, int sampleRate, boolean hasPadding) { - return (isVersionOne ? 144 : 72) * bitRate / sampleRate + (hasPadding ? 1 : 0); + return MpegVersion.getVersion(buffer, offset).getBitRate(buffer, offset); } /** @@ -154,7 +82,7 @@ private static int calculateFrameSize(boolean isVersionOne, int bitRate, int sam * @return Sample rate */ public static int getFrameSampleRate(byte[] buffer, int offset) { - return isMpegVersionOne(buffer, offset) ? getFrameSampleRateV1(buffer, offset) : getFrameSampleRateV2(buffer, offset); + return MpegVersion.getVersion(buffer, offset).getSampleRate(buffer, offset); } /** @@ -168,30 +96,22 @@ public static int getFrameChannelCount(byte[] buffer, int offset) { return (buffer[offset + 3] & 0xC0) == 0xC0 ? 1 : 2; } - private static int getFrameSampleRateV1(byte[] buffer, int offset) { - switch ((buffer[offset + 2] & 0x0C) >>> 2) { - case 0: - return 44100; - case 1: - return 48000; - case 2: - return 32000; - default: - throw new IllegalArgumentException("Not valid sample rate"); - } + public static boolean hasFrameSync(byte[] buffer, int offset) { + // must start with 11 high bits + return (buffer[offset] & 0xFF) == 0xFF && (buffer[offset + 1] & 0xE0) == 0xE0; } - private static int getFrameSampleRateV2(byte[] buffer, int offset) { - switch ((buffer[offset + 2] & 0x0C) >>> 2) { - case 0: - return 22050; - case 1: - return 24000; - case 2: - return 16000; - default: - throw new IllegalArgumentException("Not valid sample rate"); - } + public static boolean isUnsupportedVersion(byte[] buffer, int offset) { + return (buffer[offset + 1] & 0x18) >> 3 == 0x01; + } + + public static boolean isValidFrame(byte[] buffer, int offset) { + int second = buffer[offset + 1] & 0xFF; + int third = buffer[offset + 2] & 0xFF; + return (second & 0x06) == 0x02 // Is Layer III + && (third & 0xF0) != 0x00 // Has defined bitrate + && (third & 0xF0) != 0xF0 // Valid bitrate + && (third & 0x0C) != 0x0C; // Valid sampling rate } /** @@ -202,26 +122,7 @@ private static int getFrameSampleRateV2(byte[] buffer, int offset) { * @return Frame size, or zero if not a valid frame header */ public static int getFrameSize(byte[] buffer, int offset) { - int first = buffer[offset] & 0xFF; - int second = buffer[offset + 1] & 0xFF; - int third = buffer[offset + 2] & 0xFF; - - boolean invalid = (first != 0xFF || (second & 0xE0) != 0xE0) // Frame sync does not match - || (second & 0x10) != 0x10 // Not MPEG-1 nor MPEG-2, not dealing with this stuff - || (second & 0x06) != 0x02 // Not Layer III, not dealing with this stuff - || (third & 0xF0) == 0x00 // No defined bitrate - || (third & 0xF0) == 0xF0 // Invalid bitrate - || (third & 0x0C) == 0x0C; // Invalid sampling rate - - if (invalid) { - return 0; - } - - int bitRate = getFrameBitRate(buffer, offset); - int sampleRate = getFrameSampleRate(buffer, offset); - boolean hasPadding = (third & 0x02) != 0; - - return calculateFrameSize(isMpegVersionOne(buffer, offset), bitRate, sampleRate, hasPadding); + return MpegVersion.getVersion(buffer, offset).getFrameSize(buffer, offset); } /** @@ -232,10 +133,7 @@ public static int getFrameSize(byte[] buffer, int offset) { * @return Average frame size, assuming CBR */ public static double getAverageFrameSize(byte[] buffer, int offset) { - int bitRate = getFrameBitRate(buffer, offset); - int sampleRate = getFrameSampleRate(buffer, offset); - - return (isMpegVersionOne(buffer, offset) ? 144.0 : 72.0) * bitRate / sampleRate; + return MpegVersion.getVersion(buffer, offset).getAverageFrameSize(buffer, offset); } /** @@ -244,14 +142,99 @@ public static double getAverageFrameSize(byte[] buffer, int offset) { * @return Number of samples per frame. */ public static long getSamplesPerFrame(byte[] buffer, int offset) { - return isMpegVersionOne(buffer, offset) ? MPEG1_SAMPLES_PER_FRAME : MPEG2_SAMPLES_PER_FRAME; - } - - private static boolean isMpegVersionOne(byte[] buffer, int offset) { - return (buffer[offset + 1] & 0x08) == 0x08; + return MpegVersion.getVersion(buffer, offset).getSamplesPerFrame(); } public static int getMaximumFrameSize() { - return calculateFrameSize(true, 320000, 32000, true); + return MpegVersion.MAX_FRAME_SIZE; + } + + private static final int[] SAMPLE_RATE_BASE = { 11025, 12000, 8000 }; + + public static enum MpegVersion { + + MPEG_1(4, 1152, new int[] { 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320 }), + MPEG_2(2, 576, new int[] { 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 }), + MPEG_2_5(1, MPEG_2.samplesPerFrame, MPEG_2.bitrateIndex); + + public static final int MAX_FRAME_SIZE = getMaxFrameSize(); + + public static MpegVersion getVersion(byte[] buffer, int offset) { + // 0 - MPEG 2.5 + // 1 - reserved (unsupported) + // 2 - MPEG 2 + // 3 - MPEG 1 + int index = (buffer[offset + 1] & 0x18) >> 3; + switch (index) { + case 0: + return MPEG_2_5; + case 2: + return MPEG_2; + case 3: + return MPEG_1; + default: + throw new IllegalArgumentException("Invalid version"); + } + } + + private static int getMaxFrameSize() { + int bitRate = MPEG_1.bitrateIndex[MPEG_1.bitrateIndex.length - 1] * 1000; + int sampleRate = MPEG_1.samplerateIndex[1]; + return MPEG_1.calculateFrameSize(bitRate, sampleRate, true); + } + + private final int samplesPerFrame; + private final int frameLengthMultiplier; + private final int[] bitrateIndex; + private final int[] samplerateIndex; + + MpegVersion(int samplerateMultiplier, int samplesPerFrame, int[] bitrateIndex) { + this.samplesPerFrame = samplesPerFrame; + this.frameLengthMultiplier = samplesPerFrame / 8; + this.bitrateIndex = bitrateIndex; + this.samplerateIndex = Arrays.stream(SAMPLE_RATE_BASE).map(r -> r * samplerateMultiplier).toArray(); + } + + public int getSamplesPerFrame() { + return this.samplesPerFrame; + } + + public int getFrameLengthMultiplier() { + return this.frameLengthMultiplier; + } + + public int getBitRate(byte[] buffer, int offset) { + int index = (buffer[offset + 2] & 0xF0) >> 4; + if (index == 0 || index == 15) + throw new IllegalArgumentException("Invalid bitrate"); + + return this.bitrateIndex[index - 1] * 1000; + } + + public int getSampleRate(byte[] buffer, int offset) { + int index = (buffer[offset + 2] & 0x0C) >> 2; + if (index == 3) + throw new IllegalArgumentException("Invalid samplerate"); + + return this.samplerateIndex[index]; + } + + public int getFrameSize(byte[] buffer, int offset) { + return calculateFrameSize(getBitRate(buffer, offset), getSampleRate(buffer, offset), + hasPadding(buffer, offset)); + } + + public double getAverageFrameSize(byte[] buffer, int offset) { + return (double) getFrameLengthMultiplier() * getBitRate(buffer, offset) / getSampleRate(buffer, offset); + } + + private int calculateFrameSize(int bitRate, int sampleRate, boolean hasPadding) { + return getFrameLengthMultiplier() * bitRate / sampleRate + (hasPadding ? 1 : 0); + } + + private static boolean hasPadding(byte[] buffer, int offset) { + return (buffer[offset + 2] & 0x02) != 0; + } + } }