From 5e7624c78c82e47a5d6040ac27186e55f5ff4c8f Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Mon, 18 Mar 2013 11:17:28 +0100 Subject: [PATCH] Add custom MP3 parser. --- .../net/pterodactylus/sonitus/io/mp3/Frame.java | 309 +++++++++++++++++++++ .../net/pterodactylus/sonitus/io/mp3/Parser.java | 126 +++++++++ 2 files changed, 435 insertions(+) create mode 100644 src/main/java/net/pterodactylus/sonitus/io/mp3/Frame.java create mode 100644 src/main/java/net/pterodactylus/sonitus/io/mp3/Parser.java diff --git a/src/main/java/net/pterodactylus/sonitus/io/mp3/Frame.java b/src/main/java/net/pterodactylus/sonitus/io/mp3/Frame.java new file mode 100644 index 0000000..1d3a350 --- /dev/null +++ b/src/main/java/net/pterodactylus/sonitus/io/mp3/Frame.java @@ -0,0 +1,309 @@ +/* + * Sonitus - Frame.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sonitus.io.mp3; + +import java.util.Map; + +import com.google.common.base.Optional; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableMap; + +/** + * A single MPEG audio frame. + * + * @author David ‘Bombe’ Roden + */ +public class Frame { + + /** The MPEG audio version. */ + public enum MpegAudioVersion { + + /** Verstion 2.5. */ + VERSION_2_5, + + /** Reserved. */ + RESERVED, + + /** Version 2. */ + VERSION_2, + + /** Version 1. */ + VERSION_1 + + } + + /** The MPEG layer description. */ + public enum LayerDescription { + + /** Reserved. */ + RESERVED, + + /** Layer III. */ + LAYER_3, + + /** Layer II. */ + LAYER_2, + + /** Layer I. */ + LAYER_1 + + } + + /** The channel mode. */ + public enum ChannelMode { + + /* Stereo. */ + STEREO, + + /** Joint stereo. */ + JOINT_STEREO, + + /** Dual-channel stereo. */ + DUAL_CHANNEL, + + /** Single channel, aka mono. */ + SINGLE_CHANNEL + + } + + /** Mode of emphasis. */ + public enum Emphasis { + + /** No emphasis. */ + NONE, + + /** 50/15 ms. */ + _50_15_MS, + + /** Reserved. */ + RESERVED, + + /** CCIT J.17. */ + CCIT_J_17 + + } + + /** Bitrate table. */ + private static final Supplier>>> bitrateSupplier = Suppliers.memoize(new Supplier>>>() { + + @Override + public Map>> get() { + ImmutableMap.Builder>> mpegAudioVersionMapBuilder = ImmutableMap.builder(); + + /* MPEG 1. */ + ImmutableMap.Builder> mpeg1Builder = ImmutableMap.builder(); + + /* Layer 1. */ + ImmutableMap.Builder bitrates = ImmutableMap.builder(); + bitrates.put(0, 0).put(1, 32).put(2, 64).put(3, 96).put(4, 128).put(5, 160).put(6, 192).put(7, 224); + bitrates.put(8, 256).put(9, 288).put(10, 320).put(11, 352).put(12, 384).put(13, 416).put(14, 448).put(15, -1); + mpeg1Builder.put(LayerDescription.LAYER_1, bitrates.build()); + + /* MPEG 1, Layer 2 bitrates. */ + bitrates = ImmutableMap.builder(); + bitrates.put(0, 0).put(1, 32).put(2, 48).put(3, 56).put(4, 64).put(5, 80).put(6, 96).put(7, 112); + bitrates.put(8, 128).put(9, 160).put(10, 192).put(11, 224).put(12, 256).put(13, 320).put(14, 384).put(15, -1); + mpeg1Builder.put(LayerDescription.LAYER_2, bitrates.build()); + + /* MPEG 1, Layer 3 bitrates. */ + bitrates = ImmutableMap.builder(); + bitrates.put(0, 0).put(1, 32).put(2, 40).put(3, 48).put(4, 56).put(5, 64).put(6, 80).put(7, 96); + bitrates.put(8, 112).put(9, 128).put(10, 160).put(11, 192).put(12, 224).put(13, 256).put(14, 320).put(15, -1); + mpeg1Builder.put(LayerDescription.LAYER_3, bitrates.build()); + mpegAudioVersionMapBuilder.put(MpegAudioVersion.VERSION_1, mpeg1Builder.build()); + + /* MPEG 2 & 2.5. */ + ImmutableBiMap.Builder> mpeg2Builder = ImmutableBiMap.builder(); + + /* Layer 1. */ + bitrates = ImmutableMap.builder(); + bitrates.put(0, 0).put(1, 32).put(2, 48).put(3, 56).put(4, 64).put(5, 80).put(6, 96).put(7, 112); + bitrates.put(8, 128).put(9, 144).put(10, 160).put(11, 176).put(12, 192).put(13, 224).put(14, 256).put(15, -1); + mpeg2Builder.put(LayerDescription.LAYER_1, bitrates.build()); + + /* Layer 2 & 3. */ + bitrates = ImmutableMap.builder(); + bitrates.put(0, 0).put(1, 8).put(2, 16).put(3, 24).put(4, 32).put(5, 40).put(6, 48).put(7, 56); + bitrates.put(8, 64).put(9, 80).put(10, 96).put(11, 112).put(12, 128).put(13, 144).put(14, 160).put(15, -1); + mpeg2Builder.put(LayerDescription.LAYER_2, bitrates.build()); + mpeg2Builder.put(LayerDescription.LAYER_3, bitrates.build()); + + mpegAudioVersionMapBuilder.put(MpegAudioVersion.VERSION_2, mpeg2Builder.build()); + mpegAudioVersionMapBuilder.put(MpegAudioVersion.VERSION_2_5, mpeg2Builder.build()); + + return mpegAudioVersionMapBuilder.build(); + } + }); + + /** Sampling rate table. */ + private static final Supplier>> samplingRateSupplier = Suppliers.memoize(new Supplier>>() { + + @Override + public Map> get() { + ImmutableMap.Builder> mpegAudioVersions = ImmutableMap.builder(); + + /* MPEG 1. */ + ImmutableMap.Builder samplingRates = ImmutableMap.builder(); + samplingRates.put(0, 44100).put(1, 48000).put(2, 32000).put(3, 0); + mpegAudioVersions.put(MpegAudioVersion.VERSION_1, samplingRates.build()); + + /* MPEG 2. */ + samplingRates = ImmutableMap.builder(); + samplingRates.put(0, 22050).put(1, 24000).put(2, 16000).put(3, 0); + mpegAudioVersions.put(MpegAudioVersion.VERSION_2, samplingRates.build()); + + /* MPEG 2.5. */ + samplingRates = ImmutableMap.builder(); + samplingRates.put(0, 11025).put(1, 12000).put(2, 8000).put(3, 0); + mpegAudioVersions.put(MpegAudioVersion.VERSION_2_5, samplingRates.build()); + + return mpegAudioVersions.build(); + } + }); + + /** The decoded MPEG audio version ID. */ + private final int mpegAudioVersionId; + + /** The decoded layer description. */ + private final int layerDescription; + + /** The decoded protection bit. */ + private final int protectionBit; + + /** The decoded bitrate index. */ + private final int bitrateIndex; + + /** The deocded sampling rate frequency index. */ + private final int samplingRateFrequencyIndex; + + /** The decoded padding bit. */ + private final int paddingBit; + + /** The decoded private bit. */ + private final int privateBit; + + /** The decoded channel mode. */ + private final int channelMode; + + /** The deocded mode extension. */ + private final int modeExtension; + + /** The decoded copyright bit. */ + private final int copyright; + + /** The deocded original bit. */ + private final int original; + + /** The decoded emphasis mode. */ + private final int emphasis; + + private Frame(int mpegAudioVersionId, int layerDescription, int protectionBit, int bitrateIndex, int samplingRateFrequencyIndex, int paddingBit, int privateBit, int channelMode, int modeExtension, int copyright, int original, int emphasis) { + this.mpegAudioVersionId = mpegAudioVersionId; + this.layerDescription = layerDescription; + this.protectionBit = protectionBit; + this.bitrateIndex = bitrateIndex; + this.samplingRateFrequencyIndex = samplingRateFrequencyIndex; + this.paddingBit = paddingBit; + this.privateBit = privateBit; + this.channelMode = channelMode; + this.modeExtension = modeExtension; + this.copyright = copyright; + this.original = original; + this.emphasis = emphasis; + } + + // + // ACCESSORS + // + + public MpegAudioVersion mpegAudioVersion() { + return MpegAudioVersion.values()[mpegAudioVersionId]; + } + + public LayerDescription layerDescription() { + return LayerDescription.values()[layerDescription]; + } + + public boolean protectionBit() { + return protectionBit != 0; + } + + public int bitrate() { + return bitrateSupplier.get().get(mpegAudioVersion()).get(layerDescription()).get(bitrateIndex); + } + + public int samplingRate() { + return samplingRateSupplier.get().get(mpegAudioVersion()).get(samplingRateFrequencyIndex); + } + + public boolean paddingBit() { + return paddingBit != 0; + } + + public boolean privateBit() { + return privateBit != 0; + } + + public ChannelMode channelMode() { + return ChannelMode.values()[channelMode]; + } + + /* TODO - mode extension. */ + + public boolean copyrightBit() { + return copyright != 0; + } + + public boolean originalBit() { + return original != 0; + } + + public Emphasis emphasis() { + return Emphasis.values()[emphasis]; + } + + // + // STATIC METHODS + // + + public static boolean isFrame(byte[] buffer, int offset, int length) { + return (((buffer[offset] & 0xff) == 0xff) && ((buffer[offset + 1] & 0xe0) == 0xe0)); + } + + public static Optional create(byte[] buffer, int offset, int length) { + if (isFrame(buffer, offset, length)) { + int mpegAudioVersionId = (buffer[offset + 1] & 0x18) >>> 3; + int layerDescription = (buffer[offset + 1] & 0x06) >>> 1; + int protectionBit = buffer[offset + 1] & 0x01; + int bitrateIndex = (buffer[offset + 2] & 0xf0) >>> 4; + int samplingRateFrequencyIndex = (buffer[offset + 2] & 0x0c) >>> 2; + int paddingBit = (buffer[offset + 2] & 0x02) >>> 1; + int privateBit = buffer[offset + 2] & 0x01; + int channelMode = (buffer[offset + 3] & 0xc0) >> 6; + int modeExtension = (buffer[offset + 3] & 0x60) >> 4; + int copyright = (buffer[offset + 3] & 0x08) >> 3; + int original = (buffer[offset + 3] & 0x04) >> 2; + int emphasis = buffer[offset + 3] & 0x03; + return Optional.of(new Frame(mpegAudioVersionId, layerDescription, protectionBit, bitrateIndex, samplingRateFrequencyIndex, paddingBit, privateBit, channelMode, modeExtension, copyright, original, emphasis)); + } + return Optional.absent(); + } + +} diff --git a/src/main/java/net/pterodactylus/sonitus/io/mp3/Parser.java b/src/main/java/net/pterodactylus/sonitus/io/mp3/Parser.java new file mode 100644 index 0000000..eb2b441 --- /dev/null +++ b/src/main/java/net/pterodactylus/sonitus/io/mp3/Parser.java @@ -0,0 +1,126 @@ +/* + * Sonitus - Mp3Parser.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sonitus.io.mp3; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +import com.google.common.base.Optional; +import com.google.common.io.ByteStreams; + +/** + * A parser for MP3 files. It can recognize (and skip) ID3v2 header tags and + * MPEG audio frames. + * + * @author David ‘Bombe’ Roden + */ +public class Parser { + + /** The input stream to parse. */ + private final InputStream inputStream; + + /** The complete ID3v2 tag. */ + private final byte[] id3Tag; + + /** The current read buffer. */ + private final byte[] buffer = new byte[4]; + + /** + * Creates a new parser. + * + * @param inputStream + * The input stream to parse + * @throws IOException + * if an I/O error occurs + */ + public Parser(InputStream inputStream) throws IOException { + this.inputStream = inputStream; + readFully(inputStream, buffer, 0, 3); + if ((buffer[0] == 'I') && (buffer[1] == 'D') && (buffer[2] == '3')) { + readFully(inputStream, buffer, 0, 3); + byte[] lengthBuffer = new byte[4]; + readFully(inputStream, lengthBuffer, 0, 4); + int headerLength = (lengthBuffer[0] << 21) | (lengthBuffer[1] << 14) | (lengthBuffer[2] << 7) | lengthBuffer[3]; + id3Tag = new byte[headerLength + 10]; + System.arraycopy(new byte[] { 'I', 'D', '3', buffer[0], buffer[1], buffer[2], lengthBuffer[0], lengthBuffer[1], lengthBuffer[2], lengthBuffer[3] }, 0, id3Tag, 0, 10); + readFully(inputStream, id3Tag, 10, headerLength); + readFully(inputStream, buffer, 0, 3); + } else { + id3Tag = null; + } + } + + /** + * Returns the ID3v2 tag. + * + * @return The ID3v2 tag, or {@link Optional#absent()} if there is no ID3v2 + * tag + */ + public Optional getId3Tag() { + return Optional.fromNullable(id3Tag); + } + + /** + * Returns the next frame. + * + * @return The next frame + * @throws IOException + * if an I/O error occurs, or EOF is reached + */ + public Frame nextFrame() throws IOException { + while (true) { + int r = inputStream.read(); + if (r == -1) { + throw new EOFException(); + } + System.arraycopy(buffer, 1, buffer, 0, 3); + buffer[3] = (byte) r; + Optional frame = Frame.create(buffer, 0, 4); + if (frame.isPresent()) { + return frame.get(); + } + } + } + + // + // STATIC METHODS + // + + /** + * Reads exactly {@code length} bytes from the given input stream, throwing an + * {@link EOFException} if there are not enough bytes left in the stream. + * + * @param inputStream + * The input stream to read from + * @param buffer + * The buffer in which to read + * @param offset + * The offset at which to start writing into the buffer + * @param length + * The amount of bytes to read + * @throws IOException + * if an I/O error occurs, or EOF is reached + */ + private static void readFully(InputStream inputStream, byte[] buffer, int offset, int length) throws IOException { + if (ByteStreams.read(inputStream, buffer, offset, length) < length) { + throw new EOFException(); + } + } + +} -- 2.7.4