/* * MP3Decoder.java * Transform * * Copyright (c) 2009-2010 Flagstone Software Ltd. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Flagstone Software Ltd. nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.flagstone.transform.util.sound; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.Arrays; import java.util.zip.DataFormatException; import com.flagstone.transform.MovieTag; import com.flagstone.transform.coder.BigDecoder; import com.flagstone.transform.coder.Coder; import com.flagstone.transform.sound.DefineSound; import com.flagstone.transform.sound.SoundFormat; import com.flagstone.transform.sound.SoundStreamBlock; import com.flagstone.transform.sound.SoundStreamHead2; /** * Decoder for MP3 sounds so they can be added to a flash file. */ @SuppressWarnings("PMD.TooManyMethods") public final class MP3Decoder implements SoundProvider, SoundDecoder { /** The bit mask to obtain the ID3 identifier. */ private static final int ID3_MASK = 0xFFFFFF00; /** Value identifying ID3 Version 1 meta-data. */ private static final int ID3_V1 = 0x54414700; /** The length of the meta-data block in ID3 Version 1. */ private static final int ID3_V1_LENGTH = 128; /** Value identifying ID3 Version 2 meta-data. */ private static final int ID3_V2 = 0x49443300; /** The number of bytes in the footer of an ID3 V2 meta-data block. */ private static final int ID3_V2_FOOTER = 10; /** Mask to identify whether the header contains MP3 sync data. */ private static final int MP3_SYNC = 0xFFE00000; /** The version number of the MPEG sound format. In this case 3 for MP3. */ private static final int MPEG1 = 3; /** The number of samples in each frame according to the MPEG version. */ private static final int[] MP3_FRAME_SIZE = {576, 576, 576, 1152}; /** The number of channels supported by each MP3 version. */ private static final int[] CHANNEL_COUNT = {2, 2, 2, 1}; /** The bit rates for the different MPEG sound versions. */ private static final int[][] BIT_RATES = { // MPEG 2.5 {-1, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1}, // Reserved {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // MPEG 2.0 {-1, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1}, // MPEG 3.0 {-1, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1}, }; /** The playback rates for the different MPEG sound versions. */ private static final int[][] SAMPLE_RATES = { {11025, -1, -1, -1}, {-1, -1, -1, -1}, {22050, -1, -1, -1}, // MPEG 3.0 {44100, -1, -1, -1} }; /** The number of bytes in each sample. */ private static final int SAMPLE_SIZE = 2; /** The frame rate of the movie where the MP3 sound will be played. */ private transient float movieRate; /** The number of sound channels: 1 - mono, 2 - stereo. */ private transient int numberOfChannels; /** The number of sound samples for each channel. */ private transient int samplesPerChannel; /** The rate at which the sound will be played. */ private transient int sampleRate; /** The sound samples. */ private transient byte[] sound; /** The decoder used to read the MP3 frames. */ private transient BigDecoder coder; /** The number of sound samples in each frame. */ private transient int samplesPerFrame = 0; /** The contents of the current MP3 frame. */ private transient byte[] frame; /** Actual number of samples streamed so far. */ private transient int actualSamples; /** Expected number of samples streamed based on the movie frame rate. */ private transient int expectedSamples; /** {@inheritDoc} */ @Override public SoundDecoder newDecoder() { return new MP3Decoder(); } /** {@inheritDoc} */ @Override public void read(final File file) throws IOException, DataFormatException { final FileInputStream stream = new FileInputStream(file); try { read(stream); } finally { if (stream != null) { stream.close(); } } } /** {@inheritDoc} */ @Override public void read(final URL url) throws IOException, DataFormatException { final URLConnection connection = url.openConnection(); final int fileSize = connection.getContentLength(); if (fileSize < 0) { throw new FileNotFoundException(url.getFile()); } final InputStream stream = url.openStream(); try { read(stream); } finally { if (stream != null) { stream.close(); } } } /** {@inheritDoc} */ @Override public void read(final InputStream stream) throws IOException, DataFormatException { coder = new BigDecoder(stream); readFrame(); actualSamples += samplesPerFrame; } /** {@inheritDoc} */ @Override public DefineSound defineSound(final int identifier) throws IOException, DataFormatException { int length; sound = new byte[2]; do { length = sound.length; sound = Arrays.copyOf(sound, length + frame.length); System.arraycopy(frame, 0, sound, length, frame.length); } while (readFrame()); return new DefineSound(identifier, SoundFormat.MP3, sampleRate, numberOfChannels, SAMPLE_SIZE, samplesPerChannel, sound); } /** {@inheritDoc} */ @Override public DefineSound defineSound(final int identifier, final float duration) throws IOException, DataFormatException { sound = new byte[2]; float played = 0; int length; while (played < duration) { length = sound.length; sound = Arrays.copyOf(sound, length + frame.length); System.arraycopy(frame, 0, sound, length, frame.length); played += (float) samplesPerFrame / (float) sampleRate; if (!readFrame()) { break; } } return new DefineSound(identifier, SoundFormat.MP3, sampleRate, numberOfChannels, SAMPLE_SIZE, samplesPerChannel, sound); } /** {@inheritDoc} */ @Override public MovieTag streamHeader(final float frameRate) { movieRate = frameRate; return new SoundStreamHead2(SoundFormat.MP3, sampleRate, numberOfChannels, SAMPLE_SIZE, sampleRate, numberOfChannels, SAMPLE_SIZE, (int) (sampleRate / frameRate)); } /** {@inheritDoc} */ @Override public MovieTag streamSound() throws IOException, DataFormatException { final int seek = expectedSamples > 0 ? actualSamples - expectedSamples : 0; expectedSamples += sampleRate / movieRate; sound = new byte[4]; int sampleCount = 0; boolean hasFrames = true; int length; do { length = sound.length; sound = Arrays.copyOf(sound, length + frame.length); System.arraycopy(frame, 0, sound, length, frame.length); sampleCount += samplesPerFrame; hasFrames = readFrame(); actualSamples += samplesPerFrame; } while (hasFrames && (actualSamples < expectedSamples)); SoundStreamBlock block = null; if (hasFrames) { sound[0] = (byte) sampleCount; sound[1] = (byte) (sampleCount >> Coder.TO_LOWER_BYTE); sound[2] = (byte) seek; sound[3] = (byte) (seek >> Coder.TO_LOWER_BYTE); block = new SoundStreamBlock(sound); } return block; } /** * Read a MP3 frame. * @return true if a frame was read. * @throws IOException if there is an error reading the data. * @throws DataFormatException if the sound is not in MP3 format. */ private boolean readFrame() throws IOException, DataFormatException { boolean frameRead = false; int header; while ((!coder.eof()) && !frameRead) { header = coder.scanInt(); if (header == -1) { coder.readUnsignedShort(); } else if ((header & ID3_MASK) == ID3_V1) { readID3V1(); } else if ((header & ID3_MASK) == ID3_V2) { readID3V2(); } else if ((header & MP3_SYNC) == MP3_SYNC) { readFrame(header); frameRead = true; } else { coder.readUnsignedShort(); } } return !coder.eof(); } /** * Read the ID3 V1 meta-data. * @throws IOException if there is an error reading the data. * @throws DataFormatException if the ID3 V1 header cannot be decoded. */ private void readID3V1() throws IOException, DataFormatException { coder.skip(ID3_V1_LENGTH); } /** * Read the ID3 V2 meta-data. * @throws IOException if there is an error reading the data. * @throws DataFormatException if the ID3 V2 header cannot be decoded. */ private void readID3V2() throws IOException, DataFormatException { coder.readByte(); // I coder.readByte(); // D coder.readByte(); // 3 coder.readByte(); // major version coder.readByte(); // minor version int length; final int flags = coder.readByte(); if ((flags & Coder.BIT4) == 0) { length = 0; } else { length = ID3_V2_FOOTER; } length += coder.readByte() << 21; length += coder.readByte() << 14; length += coder.readByte() << 7; length += coder.readByte(); coder.skip(length); } /** * Read an MP3 sync frame. * @param header the header tag containing the MP3 data. * @throws IOException if there is an error reading the data. * @throws DataFormatException if the header cannot be decoded. */ private void readFrame(final int header) throws IOException, DataFormatException { final int version = (header & 0x180000) >> 19; final int layer = (header & 0x060000) >> 17; //boolean hasCRC = (header & 0x010000) != 0; samplesPerFrame = MP3_FRAME_SIZE[version]; final int bitRate = BIT_RATES[version][(header & Coder.NIB3) >> Coder.ALIGN_NIB3]; sampleRate = SAMPLE_RATES[version][(header & 0x0C00) >> 10]; final int padding = (header & 0x0200) >> 9; //int reserved = (header & 0x0100) >> 8; if (layer != 1) { throw new DataFormatException("Flash only supports MPEG Layer 3"); } if (bitRate == -1) { throw new DataFormatException("Unsupported Bit-rate"); } if (sampleRate == -1) { throw new DataFormatException("Unsupported Sampling-rate"); } numberOfChannels = CHANNEL_COUNT[(header & Coder.PAIR3) >> 6]; samplesPerChannel += samplesPerFrame; final int frameSize = 4 + (((version == MPEG1) ? 144 : 72) * bitRate * 1000 / sampleRate + padding) - 4; frame = coder.readBytes(new byte[frameSize]); } }