/* * Copyright (C) 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.ringdroid.soundfile; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; /** * CheapMP3 represents an MP3 file by doing a "cheap" scan of the file, * parsing the frame headers only and getting an extremely rough estimate * of the volume level of each frame. * * TODO: Useful unit tests might be to look for sync in various places: * FF FA * FF FB * 00 FF FA * FF FF FA * ([ 00 ] * 12) FF FA * ([ 00 ] * 13) FF FA */ public class CheapMP3 extends CheapSoundFile { public static Factory getFactory() { return new Factory() { public CheapSoundFile create() { return new CheapMP3(); } public String[] getSupportedExtensions() { return new String[] { "mp3" }; } }; } // Member variables representing frame data private int mNumFrames; private int[] mFrameOffsets; private int[] mFrameLens; private int[] mFrameGains; private int mFileSize; private int mAvgBitRate; private int mGlobalSampleRate; private int mGlobalChannels; // Member variables used during initialization private int mMaxFrames; private int mBitrateSum; private int mMinGain; private int mMaxGain; public CheapMP3() { } public int getNumFrames() { return mNumFrames; } public int[] getFrameOffsets() { return mFrameOffsets; } public int getSamplesPerFrame() { return 1152; } public int[] getFrameLens() { return mFrameLens; } public int[] getFrameGains() { return mFrameGains; } public int getFileSizeBytes() { return mFileSize; } public int getAvgBitrateKbps() { return mAvgBitRate; } public int getSampleRate() { return mGlobalSampleRate; } public int getChannels() { return mGlobalChannels; } public String getFiletype() { return "MP3"; } /** * MP3 supports seeking into the middle of the file, no header needed, * so this method is supported to hear exactly what a "cut" of the file * sounds like without needing to actually save a file to disk first. */ public int getSeekableFrameOffset(int frame) { if (frame <= 0) { return 0; } else if (frame >= mNumFrames) { return mFileSize; } else { return mFrameOffsets[frame]; } } public void ReadFile(File inputFile) throws java.io.FileNotFoundException, java.io.IOException { super.ReadFile(inputFile); mNumFrames = 0; mMaxFrames = 64; // This will grow as needed mFrameOffsets = new int[mMaxFrames]; mFrameLens = new int[mMaxFrames]; mFrameGains = new int[mMaxFrames]; mBitrateSum = 0; mMinGain = 255; mMaxGain = 0; // No need to handle filesizes larger than can fit in a 32-bit int mFileSize = (int)mInputFile.length(); FileInputStream stream = new FileInputStream(mInputFile); int pos = 0; int offset = 0; byte[] buffer = new byte[12]; while (pos < mFileSize - 12) { // Read 12 bytes at a time and look for a sync code (0xFF) while (offset < 12) { offset += stream.read(buffer, offset, 12 - offset); } int bufferOffset = 0; while (bufferOffset < 12 && buffer[bufferOffset] != -1) bufferOffset++; if (mProgressListener != null) { boolean keepGoing = mProgressListener.reportProgress( pos * 1.0 / mFileSize); if (!keepGoing) { break; } } if (bufferOffset > 0) { // We didn't find a sync code (0xFF) at position 0; // shift the buffer over and try again for (int i = 0; i < 12 - bufferOffset; i++) buffer[i] = buffer[bufferOffset + i]; pos += bufferOffset; offset = 12 - bufferOffset; continue; } // Check for MPEG 1 Layer III or MPEG 2 Layer III codes int mpgVersion = 0; if (buffer[1] == -6 || buffer[1] == -5) { mpgVersion = 1; } else if (buffer[1] == -14 || buffer[1] == -13) { mpgVersion = 2; } else { bufferOffset = 1; for (int i = 0; i < 12 - bufferOffset; i++) buffer[i] = buffer[bufferOffset + i]; pos += bufferOffset; offset = 12 - bufferOffset; continue; } // The third byte has the bitrate and samplerate int bitRate; int sampleRate; if (mpgVersion == 1) { // MPEG 1 Layer III bitRate = BITRATES_MPEG1_L3[(buffer[2] & 0xF0) >> 4]; sampleRate = SAMPLERATES_MPEG1_L3[(buffer[2] & 0x0C) >> 2]; } else { // MPEG 2 Layer III bitRate = BITRATES_MPEG2_L3[(buffer[2] & 0xF0) >> 4]; sampleRate = SAMPLERATES_MPEG2_L3[(buffer[2] & 0x0C) >> 2]; } if (bitRate == 0 || sampleRate == 0) { bufferOffset = 2; for (int i = 0; i < 12 - bufferOffset; i++) buffer[i] = buffer[bufferOffset + i]; pos += bufferOffset; offset = 12 - bufferOffset; continue; } // From here on we assume the frame is good mGlobalSampleRate = sampleRate; int padding = (buffer[2] & 2) >> 1; int frameLen = 144 * bitRate * 1000 / sampleRate + padding; int gain; if ((buffer[3] & 0xC0) == 0xC0) { // 1 channel mGlobalChannels = 1; if (mpgVersion == 1) { gain = ((buffer[10] & 0x01) << 7) + ((buffer[11] & 0xFE) >> 1); } else { gain = ((buffer[9] & 0x03) << 6) + ((buffer[10] & 0xFC) >> 2); } } else { // 2 channels mGlobalChannels = 2; if (mpgVersion == 1) { gain = ((buffer[9] & 0x7F) << 1) + ((buffer[10] & 0x80) >> 7); } else { gain = 0; // ??? } } mBitrateSum += bitRate; mFrameOffsets[mNumFrames] = pos; mFrameLens[mNumFrames] = frameLen; mFrameGains[mNumFrames] = gain; if (gain < mMinGain) mMinGain = gain; if (gain > mMaxGain) mMaxGain = gain; mNumFrames++; if (mNumFrames == mMaxFrames) { // We need to grow our arrays. Rather than naively // doubling the array each time, we estimate the exact // number of frames we need and add 10% padding. In // practice this seems to work quite well, only one // resize is ever needed, however to avoid pathological // cases we make sure to always double the size at a minimum. mAvgBitRate = mBitrateSum / mNumFrames; int totalFramesGuess = ((mFileSize / mAvgBitRate) * sampleRate) / 144000; int newMaxFrames = totalFramesGuess * 11 / 10; if (newMaxFrames < mMaxFrames * 2) newMaxFrames = mMaxFrames * 2; int[] newOffsets = new int[newMaxFrames]; int[] newLens = new int[newMaxFrames]; int[] newGains = new int[newMaxFrames]; for (int i = 0; i < mNumFrames; i++) { newOffsets[i] = mFrameOffsets[i]; newLens[i] = mFrameLens[i]; newGains[i] = mFrameGains[i]; } mFrameOffsets = newOffsets; mFrameLens = newLens; mFrameGains = newGains; mMaxFrames = newMaxFrames; } stream.skip(frameLen - 12); pos += frameLen; offset = 0; } // We're done reading the file, do some postprocessing if (mNumFrames > 0) mAvgBitRate = mBitrateSum / mNumFrames; else mAvgBitRate = 0; } public void WriteFile(File outputFile, int startFrame, int numFrames) throws java.io.IOException { outputFile.createNewFile(); FileInputStream in = new FileInputStream(mInputFile); FileOutputStream out = new FileOutputStream(outputFile); int maxFrameLen = 0; for (int i = 0; i < numFrames; i++) { if (mFrameLens[startFrame + i] > maxFrameLen) maxFrameLen = mFrameLens[startFrame + i]; } byte[] buffer = new byte[maxFrameLen]; int pos = 0; for (int i = 0; i < numFrames; i++) { int skip = mFrameOffsets[startFrame + i] - pos; int len = mFrameLens[startFrame + i]; if (skip > 0) { in.skip(skip); pos += skip; } in.read(buffer, 0, len); out.write(buffer, 0, len); pos += len; } in.close(); out.close(); } static private int BITRATES_MPEG1_L3[] = { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 }; static private int BITRATES_MPEG2_L3[] = { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }; static private int SAMPLERATES_MPEG1_L3[] = { 44100, 48000, 32000, 0 }; static private int SAMPLERATES_MPEG2_L3[] = { 22050, 24000, 16000, 0 }; };