/*
* Copyright (C) 2009 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;
import java.io.InputStream;
import java.util.HashMap;
/**
* CheapAAC is a CheapSoundFile implementation for AAC (Advanced Audio
* Codec) encoded sound files. It supports files with an MP4 header,
* including unencrypted files encoded by Apple iTunes, and also
* files with a more basic ADTS header.
*/
public class CheapAAC extends CheapSoundFile {
public static Factory getFactory() {
return new Factory() {
public CheapSoundFile create() {
return new CheapAAC();
}
public String[] getSupportedExtensions() {
return new String[] { "aac", "m4a" };
}
};
}
class Atom {
public int start;
public int len; // including header
public byte[] data;
};
public static final int kDINF = 0x64696e66;
public static final int kFTYP = 0x66747970;
public static final int kHDLR = 0x68646c72;
public static final int kMDAT = 0x6d646174;
public static final int kMDHD = 0x6d646864;
public static final int kMDIA = 0x6d646961;
public static final int kMINF = 0x6d696e66;
public static final int kMOOV = 0x6d6f6f76;
public static final int kMP4A = 0x6d703461;
public static final int kMVHD = 0x6d766864;
public static final int kSMHD = 0x736d6864;
public static final int kSTBL = 0x7374626c;
public static final int kSTCO = 0x7374636f;
public static final int kSTSC = 0x73747363;
public static final int kSTSD = 0x73747364;
public static final int kSTSZ = 0x7374737a;
public static final int kSTTS = 0x73747473;
public static final int kTKHD = 0x746b6864;
public static final int kTRAK = 0x7472616b;
public static final int[] kRequiredAtoms = {
kDINF,
kHDLR,
kMDHD,
kMDIA,
kMINF,
kMOOV,
kMVHD,
kSMHD,
kSTBL,
kSTSD,
kSTSZ,
kSTTS,
kTKHD,
kTRAK,
};
public static final int[] kSaveDataAtoms = {
kDINF,
kHDLR,
kMDHD,
kMVHD,
kSMHD,
kTKHD,
kSTSD,
};
// Member variables containing frame info
private int mNumFrames;
private int[] mFrameOffsets;
private int[] mFrameLens;
private int[] mFrameGains;
private int mFileSize;
private HashMap<Integer, Atom> mAtomMap;
// Member variables containing sound file info
private int mBitrate;
private int mSampleRate;
private int mChannels;
private int mSamplesPerFrame;
// Member variables used only while initially parsing the file
private int mOffset;
private int mMinGain;
private int mMaxGain;
private int mMdatOffset;
private int mMdatLength;
public CheapAAC() {
}
public int getNumFrames() {
return mNumFrames;
}
public int getSamplesPerFrame() {
return mSamplesPerFrame;
}
public int[] getFrameOffsets() {
return mFrameOffsets;
}
public int[] getFrameLens() {
return mFrameLens;
}
public int[] getFrameGains() {
return mFrameGains;
}
public int getFileSizeBytes() {
return mFileSize;
}
public int getAvgBitrateKbps() {
return mFileSize / (mNumFrames * mSamplesPerFrame);
}
public int getSampleRate() {
return mSampleRate;
}
public int getChannels() {
return mChannels;
}
public String getFiletype() {
return "AAC";
}
public String atomToString(int atomType) {
String str = "";
str += (char)((atomType >> 24) & 0xff);
str += (char)((atomType >> 16) & 0xff);
str += (char)((atomType >> 8) & 0xff);
str += (char)(atomType & 0xff);
return str;
}
public void ReadFile(File inputFile)
throws java.io.FileNotFoundException,
java.io.IOException {
super.ReadFile(inputFile);
mChannels = 0;
mSampleRate = 0;
mBitrate = 0;
mSamplesPerFrame = 0;
mNumFrames = 0;
mMinGain = 255;
mMaxGain = 0;
mOffset = 0;
mMdatOffset = -1;
mMdatLength = -1;
mAtomMap = new HashMap<Integer, Atom>();
// No need to handle filesizes larger than can fit in a 32-bit int
mFileSize = (int)mInputFile.length();
/*System.out.println("File size = " + mFileSize);*/
if (mFileSize < 128) {
throw new java.io.IOException("File too small to parse");
}
// Read the first 8 bytes
FileInputStream stream = new FileInputStream(mInputFile);
byte[] header = new byte[8];
stream.read(header, 0, 8);
if (header[0] == 0 &&
header[4] == 'f' &&
header[5] == 't' &&
header[6] == 'y' &&
header[7] == 'p') {
// Create a new stream, reset to the beginning of the file
stream = new FileInputStream(mInputFile);
parseMp4(stream, mFileSize);
} else {
throw new java.io.IOException("Unknown file format");
}
if (mMdatOffset > 0 && mMdatLength > 0) {
stream = new FileInputStream(mInputFile);
stream.skip(mMdatOffset);
mOffset = mMdatOffset;
parseMdat(stream, mMdatLength);
} else {
throw new java.io.IOException("Didn't find mdat");
}
/*
for (int i = 0; i < mNumFrames; i++) {
System.out.println("Gain " + i + ": " + mFrameGains[i]);
}*/
/*
System.out.println("Atoms found:");
for (int atomType : mAtomMap.keySet()) {
System.out.println(" " + atomToString(atomType));
}*/
boolean bad = false;
for (int requiredAtomType : kRequiredAtoms) {
if (!mAtomMap.containsKey(requiredAtomType)) {
System.out.println("Missing atom: " +
atomToString(requiredAtomType));
bad = true;
}
}
if (bad) {
throw new java.io.IOException("Could not parse MP4 file");
}
}
private void parseMp4(InputStream stream, int maxLen)
throws java.io.IOException {
/*System.out.println("parseMp4 maxLen = " + maxLen);*/
while (maxLen > 8) {
int initialOffset = mOffset;
byte[] atomHeader = new byte[8];
stream.read(atomHeader, 0, 8);
int atomLen =
((0xff & atomHeader[0]) << 24) |
((0xff & atomHeader[1]) << 16) |
((0xff & atomHeader[2]) << 8) |
((0xff & atomHeader[3]));
/*System.out.println("atomType = " +
(char)atomHeader[4] +
(char)atomHeader[5] +
(char)atomHeader[6] +
(char)atomHeader[7] +
" " +
"offset = " + mOffset +
" " +
"atomLen = " + atomLen);*/
if (atomLen > maxLen)
atomLen = maxLen;
int atomType =
((0xff & atomHeader[4]) << 24) |
((0xff & atomHeader[5]) << 16) |
((0xff & atomHeader[6]) << 8) |
((0xff & atomHeader[7]));
Atom atom = new Atom();
atom.start = mOffset;
atom.len = atomLen;
mAtomMap.put(atomType, atom);
mOffset += 8;
if (atomType == kMOOV ||
atomType == kTRAK ||
atomType == kMDIA ||
atomType == kMINF ||
atomType == kSTBL) {
parseMp4(stream, atomLen);
} else if (atomType == kSTSZ) {
parseStsz(stream, atomLen - 8);
} else if (atomType == kSTTS) {
parseStts(stream, atomLen - 8);
} else if (atomType == kMDAT) {
mMdatOffset = mOffset;
mMdatLength = atomLen - 8;
} else {
for (int savedAtomType : kSaveDataAtoms) {
if (savedAtomType == atomType) {
byte[] data = new byte[atomLen - 8];
stream.read(data, 0, atomLen - 8);
mOffset += atomLen - 8;
mAtomMap.get(atomType).data = data;
}
}
}
if (atomType == kSTSD) {
parseMp4aFromStsd();
}
maxLen -= atomLen;
int skipLen = atomLen - (mOffset - initialOffset);
/*System.out.println("* atomLen: " + atomLen);*/
/*System.out.println("* mOffset: " + mOffset);*/
/*System.out.println("* initialOffset: " + initialOffset);*/
/*System.out.println("* diff: " + (mOffset - initialOffset));*/
/*System.out.println("* skipLen: " + skipLen);*/
if (skipLen < 0) {
throw new java.io.IOException(
"Went over by " + (-skipLen) + " bytes");
}
stream.skip(skipLen);
mOffset += skipLen;
}
}
void parseStts(InputStream stream, int maxLen)
throws java.io.IOException {
byte[] sttsData = new byte[16];
stream.read(sttsData, 0, 16);
mOffset += 16;
mSamplesPerFrame =
((0xff & sttsData[12]) << 24) |
((0xff & sttsData[13]) << 16) |
((0xff & sttsData[14]) << 8) |
((0xff & sttsData[15]));
/*System.out.println("STTS samples per frame: " + mSamplesPerFrame);*/
}
void parseStsz(InputStream stream, int maxLen)
throws java.io.IOException {
byte[] stszHeader = new byte[12];
stream.read(stszHeader, 0, 12);
mOffset += 12;
mNumFrames =
((0xff & stszHeader[8]) << 24) |
((0xff & stszHeader[9]) << 16) |
((0xff & stszHeader[10]) << 8) |
((0xff & stszHeader[11]));
/*System.out.println("mNumFrames = " + mNumFrames);*/
mFrameOffsets = new int[mNumFrames];
mFrameLens = new int[mNumFrames];
mFrameGains = new int[mNumFrames];
byte[] frameLenBytes = new byte[4 * mNumFrames];
stream.read(frameLenBytes, 0, 4 * mNumFrames);
mOffset += 4 * mNumFrames;
for (int i = 0; i < mNumFrames; i++) {
mFrameLens[i] =
((0xff & frameLenBytes[4 * i + 0]) << 24) |
((0xff & frameLenBytes[4 * i + 1]) << 16) |
((0xff & frameLenBytes[4 * i + 2]) << 8) |
((0xff & frameLenBytes[4 * i + 3]));
/*System.out.println("FrameLen[" + i + "] = " + mFrameLens[i]);*/
}
}
void parseMp4aFromStsd() {
byte[] stsdData = mAtomMap.get(kSTSD).data;
mChannels =
((0xff & stsdData[32]) << 8) |
((0xff & stsdData[33]));
mSampleRate =
((0xff & stsdData[40]) << 8) |
((0xff & stsdData[41]));
/*System.out.println("%% channels = " + mChannels + ", " +
"sampleRate = " + mSampleRate);*/
}
void parseMdat(InputStream stream, int maxLen)
throws java.io.IOException {
/*System.out.println("***MDAT***");*/
int initialOffset = mOffset;
for (int i = 0; i < mNumFrames; i++) {
mFrameOffsets[i] = mOffset;
/*System.out.println("&&& start: " + (mOffset - initialOffset));*/
/*System.out.println("&&& start + len: " +
(mOffset - initialOffset + mFrameLens[i]));*/
/*System.out.println("&&& maxLen: " + maxLen);*/
if (mOffset - initialOffset + mFrameLens[i] > maxLen - 8) {
mFrameGains[i] = 0;
} else {
readFrameAndComputeGain(stream, i);
}
if (mFrameGains[i] < mMinGain)
mMinGain = mFrameGains[i];
if (mFrameGains[i] > mMaxGain)
mMaxGain = mFrameGains[i];
if (mProgressListener != null) {
boolean keepGoing = mProgressListener.reportProgress(
mOffset * 1.0 / mFileSize);
if (!keepGoing) {
break;
}
}
}
}
void readFrameAndComputeGain(InputStream stream, int frameIndex)
throws java.io.IOException {
if (mFrameLens[frameIndex] < 4) {
mFrameGains[frameIndex] = 0;
stream.skip(mFrameLens[frameIndex]);
return;
}
int initialOffset = mOffset;
byte[] data = new byte[4];
stream.read(data, 0, 4);
mOffset += 4;
/*System.out.println(
"Block " + frameIndex + ": " +
data[0] + " " +
data[1] + " " +
data[2] + " " +
data[3]);*/
int idSynEle = (0xe0 & data[0]) >> 5;
/*System.out.println("idSynEle = " + idSynEle);*/
switch(idSynEle) {
case 0: // ID_SCE: mono
int monoGain = ((0x01 & data[0]) << 7) | ((0xfe & data[1]) >> 1);
/*System.out.println("monoGain = " + monoGain);*/
mFrameGains[frameIndex] = monoGain;
break;
case 1: // ID_CPE: stereo
int windowSequence = (0x60 & data[1]) >> 5;
/*System.out.println("windowSequence = " + windowSequence);*/
int windowShape = (0x10 & data[1]) >> 4;
/*System.out.println("windowShape = " + windowShape);*/
int maxSfb;
int scaleFactorGrouping;
int maskPresent;
int startBit;
if (windowSequence == 2) {
maxSfb = 0x0f & data[1];
scaleFactorGrouping = (0xfe & data[2]) >> 1;
maskPresent =
((0x01 & data[2]) << 1) |
((0x80 & data[3]) >> 7);
startBit = 25;
} else {
maxSfb =
((0x0f & data[1]) << 2) |
((0xc0 & data[2]) >> 6);
scaleFactorGrouping = -1;
maskPresent = (0x18 & data[2]) >> 3;
startBit = 21;
}
/*System.out.println("maxSfb = " + maxSfb);*/
/*System.out.println("scaleFactorGrouping = " +
scaleFactorGrouping);*/
/*System.out.println("maskPresent = " + maskPresent);*/
/*System.out.println("startBit = " + startBit);*/
if (maskPresent == 1) {
int sfgZeroBitCount = 0;
for (int b = 0; b < 7; b++) {
if ((scaleFactorGrouping & (1 << b)) == 0) {
/*System.out.println(" 1 point for bit " + b +
": " + (1 << b) +
", " + (scaleFactorGrouping & (1 << b)));*/
sfgZeroBitCount++;
}
}
/*System.out.println("sfgZeroBitCount = " + sfgZeroBitCount);*/
int numWindowGroups = 1 + sfgZeroBitCount;
/*System.out.println("numWindowGroups = " + numWindowGroups);*/
int skip = maxSfb * numWindowGroups;
/*System.out.println("skip = " + skip);*/
startBit += skip;
/*System.out.println("new startBit = " + startBit);*/
}
// We may need to fill our buffer with more than the 4
// bytes we've already read, here.
int bytesNeeded = 1 + ((startBit + 7) / 8);
byte[] oldData = data;
data = new byte[bytesNeeded];
data[0] = oldData[0];
data[1] = oldData[1];
data[2] = oldData[2];
data[3] = oldData[3];
stream.read(data, 4, bytesNeeded - 4);
mOffset += (bytesNeeded - 4);
/*System.out.println("bytesNeeded: " + bytesNeeded);*/
int firstChannelGain = 0;
for (int b = 0; b < 8; b++) {
int b0 = (b + startBit) / 8;
int b1 = 7 - ((b + startBit) % 8);
int add = (((1 << b1) & data[b0]) >> b1) << (7 - b);
/*System.out.println("Bit " + (b + startBit) + " " +
"b0 " + b0 + " " +
"b1 " + b1 + " " +
"add " + add);*/
firstChannelGain += add;
}
/*System.out.println("firstChannelGain = " + firstChannelGain);*/
mFrameGains[frameIndex] = firstChannelGain;
break;
default:
mFrameGains[frameIndex] = 0;
/*System.out.println("Unhandled idSynEle");*/
break;
}
int skip = mFrameLens[frameIndex] - (mOffset - initialOffset);
/*System.out.println("frameLen = " + mFrameLens[frameIndex]);*/
/*System.out.println("Skip = " + skip);*/
stream.skip(skip);
mOffset += skip;
}
public void StartAtom(FileOutputStream out, int atomType)
throws java.io.IOException {
byte[] atomHeader = new byte[8];
int atomLen = mAtomMap.get(atomType).len;
atomHeader[0] = (byte)((atomLen >> 24) & 0xff);
atomHeader[1] = (byte)((atomLen >> 16) & 0xff);
atomHeader[2] = (byte)((atomLen >> 8) & 0xff);
atomHeader[3] = (byte)(atomLen & 0xff);
atomHeader[4] = (byte)((atomType >> 24) & 0xff);
atomHeader[5] = (byte)((atomType >> 16) & 0xff);
atomHeader[6] = (byte)((atomType >> 8) & 0xff);
atomHeader[7] = (byte)(atomType & 0xff);
out.write(atomHeader, 0, 8);
}
public void WriteAtom(FileOutputStream out, int atomType)
throws java.io.IOException {
Atom atom = mAtomMap.get(atomType);
StartAtom(out, atomType);
out.write(atom.data, 0, atom.len - 8);
}
public void SetAtomData(int atomType, byte[] data) {
Atom atom = mAtomMap.get(atomType);
if (atom == null) {
atom = new Atom();
mAtomMap.put(atomType, atom);
}
atom.len = data.length + 8;
atom.data = data;
}
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);
SetAtomData(kFTYP, new byte[] {
'M', '4', 'A', ' ',
0, 0, 0, 0,
'M', '4', 'A', ' ',
'm', 'p', '4', '2',
'i', 's', 'o', 'm',
0, 0, 0, 0
});
SetAtomData(kSTTS, new byte[] {
0, 0, 0, 0, // version / flags
0, 0, 0, 1, // entry count
(byte) ((numFrames >> 24) & 0xff),
(byte) ((numFrames >> 16) & 0xff),
(byte) ((numFrames >> 8) & 0xff),
(byte) (numFrames & 0xff),
(byte) ((mSamplesPerFrame >> 24) & 0xff),
(byte) ((mSamplesPerFrame >> 16) & 0xff),
(byte) ((mSamplesPerFrame >> 8) & 0xff),
(byte) (mSamplesPerFrame & 0xff)
});
SetAtomData(kSTSC, new byte[] {
0, 0, 0, 0, // version / flags
0, 0, 0, 1, // entry count
0, 0, 0, 1, // first chunk
(byte) ((numFrames >> 24) & 0xff),
(byte) ((numFrames >> 16) & 0xff),
(byte) ((numFrames >> 8) & 0xff),
(byte) (numFrames & 0xff),
0, 0, 0, 1 // Smaple desc index
});
byte[] stszData = new byte[12 + 4 * numFrames];
stszData[8] = (byte)((numFrames >> 24) & 0xff);
stszData[9] = (byte)((numFrames >> 16) & 0xff);
stszData[10] = (byte)((numFrames >> 8) & 0xff);
stszData[11] = (byte)(numFrames & 0xff);
for (int i = 0; i < numFrames; i++) {
stszData[12 + 4 * i] =
(byte)((mFrameLens[startFrame + i] >> 24) & 0xff);
stszData[13 + 4 * i] =
(byte)((mFrameLens[startFrame + i] >> 16) & 0xff);
stszData[14 + 4 * i] =
(byte)((mFrameLens[startFrame + i] >> 8) & 0xff);
stszData[15 + 4 * i] =
(byte)(mFrameLens[startFrame + i] & 0xff);
}
SetAtomData(kSTSZ, stszData);
int mdatOffset =
144 +
4 * numFrames +
mAtomMap.get(kSTSD).len +
mAtomMap.get(kSTSC).len +
mAtomMap.get(kMVHD).len +
mAtomMap.get(kTKHD).len +
mAtomMap.get(kMDHD).len +
mAtomMap.get(kHDLR).len +
mAtomMap.get(kSMHD).len +
mAtomMap.get(kDINF).len;
/*System.out.println("Mdat offset: " + mdatOffset);*/
SetAtomData(kSTCO, new byte[] {
0, 0, 0, 0, // version / flags
0, 0, 0, 1, // entry count
(byte) ((mdatOffset >> 24) & 0xff),
(byte) ((mdatOffset >> 16) & 0xff),
(byte) ((mdatOffset >> 8) & 0xff),
(byte) (mdatOffset & 0xff),
});
mAtomMap.get(kSTBL).len =
8 +
mAtomMap.get(kSTSD).len +
mAtomMap.get(kSTTS).len +
mAtomMap.get(kSTSC).len +
mAtomMap.get(kSTSZ).len +
mAtomMap.get(kSTCO).len;
mAtomMap.get(kMINF).len =
8 +
mAtomMap.get(kDINF).len +
mAtomMap.get(kSMHD).len +
mAtomMap.get(kSTBL).len;
mAtomMap.get(kMDIA).len =
8 +
mAtomMap.get(kMDHD).len +
mAtomMap.get(kHDLR).len +
mAtomMap.get(kMINF).len;
mAtomMap.get(kTRAK).len =
8 +
mAtomMap.get(kTKHD).len +
mAtomMap.get(kMDIA).len;
mAtomMap.get(kMOOV).len =
8 +
mAtomMap.get(kMVHD).len +
mAtomMap.get(kTRAK).len;
int mdatLen = 8;
for (int i = 0; i < numFrames; i++) {
mdatLen += mFrameLens[startFrame + i];
}
mAtomMap.get(kMDAT).len = mdatLen;
WriteAtom(out, kFTYP);
StartAtom(out, kMOOV);
{
WriteAtom(out, kMVHD);
StartAtom(out, kTRAK);
{
WriteAtom(out, kTKHD);
StartAtom(out, kMDIA);
{
WriteAtom(out, kMDHD);
WriteAtom(out, kHDLR);
StartAtom(out, kMINF);
{
WriteAtom(out, kDINF);
WriteAtom(out, kSMHD);
StartAtom(out, kSTBL);
{
WriteAtom(out, kSTSD);
WriteAtom(out, kSTTS);
WriteAtom(out, kSTSC);
WriteAtom(out, kSTSZ);
WriteAtom(out, kSTCO);
}
}
}
}
}
StartAtom(out, kMDAT);
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) {
continue;
}
if (skip > 0) {
in.skip(skip);
pos += skip;
}
in.read(buffer, 0, len);
out.write(buffer, 0, len);
pos += len;
}
in.close();
out.close();
}
/** For debugging
public static void main(String[] argv) throws Exception {
File f = new File("");
CheapAAC c = new CheapAAC();
c.ReadFile(f);
c.WriteFile(new File(""),
0, c.getNumFrames());
} **/
};