/*
 * Copyright (C) 2015 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.audioDroid.soundfile;

class Atom {  // note: latest versions of spec simply call it 'box' instead of 'atom'.
    private int mSize;  // includes atom header (8 bytes)
    private int mType;
    private byte[] mData;  // an atom can either contain data or children, but not both.
    private Atom[] mChildren;
    private byte mVersion;  // if negative, then the atom does not contain version and flags data.
    private int mFlags;

    // create an empty atom of the given type.
    public Atom(String type) {
        mSize = 8;
        mType = getTypeInt(type);
        mData = null;
        mChildren = null;
        mVersion = -1;
        mFlags = 0;
    }

    // create an empty atom of type type, with a given version and flags.
    public Atom(String type, byte version, int flags) {
        mSize = 12;
        mType = getTypeInt(type);
        mData = null;
        mChildren = null;
        mVersion = version;
        mFlags = flags;
    }

    // set the size field of the atom based on its content.
    private void setSize() {
        int size = 8;  // type + size
        if (mVersion >= 0) {
            size += 4; // version + flags
        }
        if (mData != null) {
            size += mData.length;
        } else if (mChildren != null) {
            for (Atom child : mChildren) {
                size += child.getSize();
            }
        }
        mSize = size;
    }

    // get the size of the this atom.
    public int getSize() {
        return mSize;
    }

    private int getTypeInt(String type_str) {
        int type = 0;
        type |= (byte)(type_str.charAt(0)) << 24;
        type |= (byte)(type_str.charAt(1)) << 16;
        type |= (byte)(type_str.charAt(2)) << 8;
        type |= (byte)(type_str.charAt(3));
        return type;
    }

    public int getTypeInt() {
        return mType;
    }

    public String getTypeStr() {
        String type = "";
        type += (char)((byte)((mType >> 24) & 0xFF));
        type += (char)((byte)((mType >> 16) & 0xFF));
        type += (char)((byte)((mType >> 8) & 0xFF));
        type += (char)((byte)(mType & 0xFF));
        return type;
    }

    public boolean setData(byte[] data) {
        if (mChildren != null || data == null) {
            // TODO(nfaralli): log something here
            return false;
        }
        mData = data;
        setSize();
        return true;
    }

    public byte[] getData() {
        return mData;
    }

    public boolean addChild(Atom child) {
        if (mData != null || child == null) {
            // TODO(nfaralli): log something here
            return false;
        }
        int numChildren = 1;
        if (mChildren != null) {
            numChildren += mChildren.length;
        }
        Atom[] children = new Atom[numChildren];
        if (mChildren != null) {
            System.arraycopy(mChildren, 0, children, 0, mChildren.length);
        }
        children[numChildren - 1] = child;
        mChildren = children;
        setSize();
        return true;
    }

    // return the child atom of the corresponding type.
    // type can contain grand children: e.g. type = "trak.mdia.minf"
    // return null if the atom does not contain such a child.
    public Atom getChild(String type) {
        if (mChildren == null) {
            return null;
        }
        String[] types = type.split("\\.", 2);
        for (Atom child : mChildren) {
            if (child.getTypeStr().equals(types[0])) {
                if (types.length == 1) {
                    return child;
                } else {
                    return child.getChild(types[1]);
                }
            }
        }
        return null;
    }

    // return a byte array containing the full content of the atom (including header)
    public byte[] getBytes() {
        byte[] atom_bytes = new byte[mSize];
        int offset = 0;

        atom_bytes[offset++] = (byte)((mSize >> 24) & 0xFF);
        atom_bytes[offset++] = (byte)((mSize >> 16) & 0xFF);
        atom_bytes[offset++] = (byte)((mSize >> 8) & 0xFF);
        atom_bytes[offset++] = (byte)(mSize & 0xFF);
        atom_bytes[offset++] = (byte)((mType >> 24) & 0xFF);
        atom_bytes[offset++] = (byte)((mType >> 16) & 0xFF);
        atom_bytes[offset++] = (byte)((mType >> 8) & 0xFF);
        atom_bytes[offset++] = (byte)(mType & 0xFF);
        if (mVersion >= 0) {
            atom_bytes[offset++] = mVersion;
            atom_bytes[offset++] = (byte)((mFlags >> 16) & 0xFF);
            atom_bytes[offset++] = (byte)((mFlags >> 8) & 0xFF);
            atom_bytes[offset++] = (byte)(mFlags & 0xFF);
        }
        if (mData != null) {
            System.arraycopy(mData, 0, atom_bytes, offset, mData.length);
        } else if (mChildren != null) {
            byte[] child_bytes;
            for (Atom child : mChildren) {
                child_bytes = child.getBytes();
                System.arraycopy(child_bytes, 0, atom_bytes, offset, child_bytes.length);
                offset += child_bytes.length;
            }
        }
        return atom_bytes;
    }

    // Used for debugging purpose only.
    public String toString() {
        String str = "";
        byte[] atom_bytes = getBytes();

        for (int i = 0; i < atom_bytes.length; i++) {
            if(i % 8 == 0 && i > 0) {
                str += '\n';
            }
            str += String.format("0x%02X", atom_bytes[i]);
            if (i < atom_bytes.length - 1) {
                str += ',';
                if (i % 8 < 7) {
                    str += ' ';
                }
            }
        }
        str += '\n';
        return str;
    }
}

public class MP4Header {
    private int[] mFrameSize;    // size of each AAC frames, in bytes. First one should be 2.
    private int mMaxFrameSize;   // size of the biggest frame.
    private int mTotSize;        // size of the AAC stream.
    private int mBitrate;        // bitrate used to encode the AAC stream.
    private byte[] mTime;        // time used for 'creation time' and 'modification time' fields.
    private byte[] mDurationMS;  // duration of stream in milliseconds.
    private byte[] mNumSamples;  // number of samples in the stream.
    private byte[] mHeader;      // the complete header.
    private int mSampleRate;     // sampling frequency in Hz (e.g. 44100).
    private int mChannels;       // number of channels.

    // Creates a new MP4Header object that should be used to generate an .m4a file header.
    public MP4Header(int sampleRate, int numChannels, int[] frame_size, int bitrate) {
        if (frame_size == null || frame_size.length < 2 || frame_size[0] != 2) {
            //TODO(nfaralli): log something here
            return;
        }
        mSampleRate = sampleRate;
        mChannels = numChannels;
        mFrameSize = frame_size;
        mBitrate = bitrate;
        mMaxFrameSize = mFrameSize[0];
        mTotSize = mFrameSize[0];
        for (int i=1; i<mFrameSize.length; i++) {
            if (mMaxFrameSize < mFrameSize[i]) {
                mMaxFrameSize = mFrameSize[i];
            }
            mTotSize += mFrameSize[i];
        }
        long time = System.currentTimeMillis() / 1000;
        time += (66 * 365 + 16) * 24 * 60 * 60;  // number of seconds between 1904 and 1970
        mTime = new byte[4];
        mTime[0] = (byte)((time >> 24) & 0xFF);
        mTime[1] = (byte)((time >> 16) & 0xFF);
        mTime[2] = (byte)((time >> 8) & 0xFF);
        mTime[3] = (byte)(time & 0xFF);
        int numSamples = 1024 * (frame_size.length - 1);  // 1st frame does not contain samples.
        int durationMS = (numSamples * 1000) / mSampleRate;
        if ((numSamples * 1000) % mSampleRate > 0) {  // round the duration up.
            durationMS++;
        }
        mNumSamples= new byte[] {
                (byte)((numSamples >> 26) & 0XFF),
                (byte)((numSamples >> 16) & 0XFF),
                (byte)((numSamples >> 8) & 0XFF),
                (byte)(numSamples & 0XFF)
        };
        mDurationMS = new byte[] {
                (byte)((durationMS >> 26) & 0XFF),
                (byte)((durationMS >> 16) & 0XFF),
                (byte)((durationMS >> 8) & 0XFF),
                (byte)(durationMS & 0XFF)
        };
        setHeader();
    }

    public byte[] getMP4Header() {
        return mHeader;
    }

    public static byte[] getMP4Header(
            int sampleRate, int numChannels, int[] frame_size, int bitrate) {
        return new MP4Header(sampleRate, numChannels, frame_size, bitrate).mHeader;
    }

    public String toString() {
        String str = "";
        if (mHeader == null) {
            return str;
        }
        int num_32bits_per_lines = 8;
        int count = 0;
        for (byte b : mHeader) {
            boolean break_line = count > 0 && count % (num_32bits_per_lines * 4) == 0;
            boolean insert_space = count > 0 && count % 4 == 0 && !break_line;
            if (break_line) {
                str += '\n';
            }
            if (insert_space) {
                str += ' ';
            }
            str += String.format("%02X", b);
            count++;
        }

        return str;
    }

    private void setHeader() {
        // create the atoms needed to build the header.
        Atom a_ftyp = getFTYPAtom();
        Atom a_moov = getMOOVAtom();
        Atom a_mdat = new Atom("mdat");  // create an empty atom. The AAC stream data should follow
                                         // immediately after. The correct size will be set later.

        // set the correct chunk offset in the stco atom.
        Atom a_stco = a_moov.getChild("trak.mdia.minf.stbl.stco");
        if (a_stco == null) {
            mHeader = null;
            return;
        }
        byte[] data = a_stco.getData();
        int chunk_offset = a_ftyp.getSize() + a_moov.getSize() + a_mdat.getSize();
        int offset = data.length - 4;  // here stco should contain only one chunk offset.
        data[offset++] = (byte)((chunk_offset >> 24) & 0xFF);
        data[offset++] = (byte)((chunk_offset >> 16) & 0xFF);
        data[offset++] = (byte)((chunk_offset >> 8) & 0xFF);
        data[offset++] = (byte)(chunk_offset & 0xFF);

        // create the header byte array based on the previous atoms.
        byte[] header = new byte[chunk_offset];  // here chunk_offset is also the size of the header
        offset = 0;
        for (Atom atom : new Atom[] {a_ftyp, a_moov, a_mdat}) {
            byte[] atom_bytes = atom.getBytes();
            System.arraycopy(atom_bytes, 0, header, offset, atom_bytes.length);
            offset += atom_bytes.length;
        }

        //set the correct size of the mdat atom
        int size = 8 + mTotSize;
        offset -= 8;
        header[offset++] = (byte)((size >> 24) & 0xFF);
        header[offset++] = (byte)((size >> 16) & 0xFF);
        header[offset++] = (byte)((size >> 8) & 0xFF);
        header[offset++] = (byte)(size & 0xFF);

        mHeader = header;
    }

    private Atom getFTYPAtom() {
        Atom atom = new Atom("ftyp");
        atom.setData(new byte[] {
                'M', '4', 'A', ' ',  // Major brand
                0, 0, 0, 0,          // Minor version
                'M', '4', 'A', ' ',  // compatible brands
                'm', 'p', '4', '2',
                'i', 's', 'o', 'm'
        });
        return atom;
    }

    private Atom getMOOVAtom() {
        Atom atom = new Atom("moov");
        atom.addChild(getMVHDAtom());
        atom.addChild(getTRAKAtom());
        return atom;
    }

    private Atom getMVHDAtom() {
        Atom atom = new Atom("mvhd", (byte)0, 0);
        atom.setData(new byte[] {
                mTime[0], mTime[1], mTime[2], mTime[3],  // creation time.
                mTime[0], mTime[1], mTime[2], mTime[3],  // modification time.
                0, 0, 0x03, (byte)0xE8,  // timescale = 1000 => duration expressed in ms.
                mDurationMS[0], mDurationMS[1], mDurationMS[2], mDurationMS[3],  // duration in ms.
                0, 1, 0, 0,  // rate = 1.0
                1, 0,        // volume = 1.0
                0, 0,        // reserved
                0, 0, 0, 0,  // reserved
                0, 0, 0, 0,  // reserved
                0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // unity matrix
                0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
                0, 0, 0, 0, 0, 0, 0, 0, 0x40, 0, 0, 0,
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 0,  // pre-defined
                0, 0, 0, 2   // next track ID
        });
        return atom;
    }

    private Atom getTRAKAtom() {
        Atom atom = new Atom("trak");
        atom.addChild(getTKHDAtom());
        atom.addChild(getMDIAAtom());
        return atom;
    }

    private Atom getTKHDAtom() {
        Atom atom = new Atom("tkhd", (byte)0, 0x07);  // track enabled, in movie, and in preview.
        atom.setData(new byte[] {
                mTime[0], mTime[1], mTime[2], mTime[3],  // creation time.
                mTime[0], mTime[1], mTime[2], mTime[3],  // modification time.
                0, 0, 0, 1,  // track ID
                0, 0, 0, 0,  // reserved
                mDurationMS[0], mDurationMS[1], mDurationMS[2], mDurationMS[3],  // duration in ms.
                0, 0, 0, 0,  // reserved
                0, 0, 0, 0,  // reserved
                0, 0,        // layer
                0, 0,        // alternate group
                1, 0,        // volume = 1.0
                0, 0,        // reserved
                0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // unity matrix
                0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
                0, 0, 0, 0, 0, 0, 0, 0, 0x40, 0, 0, 0,
                0, 0, 0, 0,  // width
                0, 0, 0, 0   // height
        });
        return atom;
    }

    private Atom getMDIAAtom() {
        Atom atom = new Atom("mdia");
        atom.addChild(getMDHDAtom());
        atom.addChild(getHDLRAtom());
        atom.addChild(getMINFAtom());
        return atom;
    }

    private Atom getMDHDAtom() {
        Atom atom = new Atom("mdhd", (byte)0, 0);
        atom.setData(new byte[] {
                mTime[0], mTime[1], mTime[2], mTime[3],  // creation time.
                mTime[0], mTime[1], mTime[2], mTime[3],  // modification time.
                (byte)(mSampleRate >> 24), (byte)(mSampleRate >> 16),  // timescale = Fs =>
                (byte)(mSampleRate >> 8), (byte)(mSampleRate),  // duration expressed in samples.
                mNumSamples[0], mNumSamples[1], mNumSamples[2], mNumSamples[3],  // duration
                0, 0,     // languages
                0, 0      // pre-defined
        });
        return atom;
    }

    private Atom getHDLRAtom() {
        Atom atom = new Atom("hdlr", (byte)0, 0);
        atom.setData(new byte[] {
                0, 0, 0, 0,  // pre-defined
                's', 'o', 'u', 'n',  // handler type
                0, 0, 0, 0,  // reserved
                0, 0, 0, 0,  // reserved
                0, 0, 0, 0,  // reserved
                'S', 'o', 'u', 'n',  // name (used only for debugging and inspection purposes).
                'd', 'H', 'a', 'n',
                'd', 'l', 'e', '\0'
        });
        return atom;
    }

    private Atom getMINFAtom() {
        Atom atom = new Atom("minf");
        atom.addChild(getSMHDAtom());
        atom.addChild(getDINFAtom());
        atom.addChild(getSTBLAtom());
        return atom;
    }

    private Atom getSMHDAtom() {
        Atom atom = new Atom("smhd", (byte)0, 0);
        atom.setData(new byte[] {
                0, 0,     // balance (center)
                0, 0      // reserved
        });
        return atom;
    }

    private Atom getDINFAtom() {
        Atom atom = new Atom("dinf");
        atom.addChild(getDREFAtom());
        return atom;
    }

    private Atom getDREFAtom() {
        Atom atom = new Atom("dref", (byte)0, 0);
        byte[] url = getURLAtom().getBytes();
        byte[] data = new byte[4 + url.length];
        data[3] = 0x01;  // entry count = 1
        System.arraycopy(url, 0, data, 4, url.length);
        atom.setData(data);
        return atom;
    }

    private Atom getURLAtom() {
        Atom atom = new Atom("url ", (byte)0, 0x01);  // flags = 0x01: data is self contained.
        return atom;
    }

    private Atom getSTBLAtom() {
        Atom atom = new Atom("stbl");
        atom.addChild(getSTSDAtom());
        atom.addChild(getSTTSAtom());
        atom.addChild(getSTSCAtom());
        atom.addChild(getSTSZAtom());
        atom.addChild(getSTCOAtom());
        return atom;
    }

    private Atom getSTSDAtom() {
        Atom atom = new Atom("stsd", (byte)0, 0);
        byte[] mp4a = getMP4AAtom().getBytes();
        byte[] data = new byte[4 + mp4a.length];
        data[3] = 0x01;  // entry count = 1
        System.arraycopy(mp4a, 0, data, 4, mp4a.length);
        atom.setData(data);
        return atom;
    }

    // See also Part 14 section 5.6.1 of ISO/IEC 14496 for this atom.
    private Atom getMP4AAtom() {
        Atom atom = new Atom("mp4a");
        byte[] ase = new byte[] {  // Audio Sample Entry data
                0, 0, 0, 0, 0, 0,  // reserved
                0, 1,  // data reference index
                0, 0, 0, 0,  // reserved
                0, 0, 0, 0,  // reserved
                (byte)(mChannels >> 8), (byte)mChannels,  // channel count
                0, 0x10, // sample size
                0, 0,  // pre-defined
                0, 0,  // reserved
                (byte)(mSampleRate >> 8), (byte)(mSampleRate), 0, 0,  // sample rate
        };
        byte[] esds = getESDSAtom().getBytes();
        byte[] data = new byte[ase.length + esds.length];
        System.arraycopy(ase, 0, data, 0, ase.length);
        System.arraycopy(esds, 0, data, ase.length, esds.length);
        atom.setData(data);
        return atom;
    }

    private Atom getESDSAtom() {
        Atom atom = new Atom("esds", (byte)0, 0);
        atom.setData(getESDescriptor());
        return atom;
    }

    // Returns an ES Descriptor for an ISO/IEC 14496-3 audio stream, AAC LC, 44100Hz, 2 channels,
    // 1024 samples per frame per channel. The decoder buffer size is set so that it can contain at
    // least 2 frames. (See section 7.2.6.5 of ISO/IEC 14496-1 for more details).
    private byte[] getESDescriptor() {
        int[] samplingFrequencies = new int[] {96000, 88200, 64000, 48000, 44100, 32000, 24000,
                22050, 16000, 12000, 11025, 8000, 7350};
        // First 5 bytes of the ES Descriptor.
        byte[] ESDescriptor_top = new byte[] {0x03, 0x19, 0x00, 0x00, 0x00};
        // First 4 bytes of Decoder Configuration Descriptor. Audio ISO/IEC 14496-3, AudioStream.
        byte[] decConfigDescr_top = new byte[] {0x04, 0x11, 0x40, 0x15};
        // Audio Specific Configuration: AAC LC, 1024 samples/frame/channel.
        // Sampling frequency and channels configuration are not set yet.
        byte[] audioSpecificConfig = new byte[] {0x05, 0x02, 0x10, 0x00};
        byte[] slConfigDescr = new byte[] {0x06, 0x01, 0x02};  // specific for MP4 file.
        int offset;
        int bufferSize = 0x300;
        while (bufferSize < 2 * mMaxFrameSize) {
            // TODO(nfaralli): what should be the minimum size of the decoder buffer?
            // Should it be a multiple of 256?
            bufferSize += 0x100;
        }

        // create the Decoder Configuration Descriptor
        byte[] decConfigDescr = new byte[2 + decConfigDescr_top[1]];
        System.arraycopy(decConfigDescr_top, 0, decConfigDescr, 0, decConfigDescr_top.length);
        offset = decConfigDescr_top.length;
        decConfigDescr[offset++] = (byte)((bufferSize >> 16) & 0xFF);
        decConfigDescr[offset++] = (byte)((bufferSize >> 8) & 0xFF);
        decConfigDescr[offset++] = (byte)(bufferSize & 0xFF);
        decConfigDescr[offset++] = (byte)((mBitrate >> 24) & 0xFF);
        decConfigDescr[offset++] = (byte)((mBitrate >> 16) & 0xFF);
        decConfigDescr[offset++] = (byte)((mBitrate >> 8) & 0xFF);
        decConfigDescr[offset++] = (byte)(mBitrate & 0xFF);
        decConfigDescr[offset++] = (byte)((mBitrate >> 24) & 0xFF);
        decConfigDescr[offset++] = (byte)((mBitrate >> 16) & 0xFF);
        decConfigDescr[offset++] = (byte)((mBitrate >> 8) & 0xFF);
        decConfigDescr[offset++] = (byte)(mBitrate & 0xFF);
        int index;
        for (index=0; index<samplingFrequencies.length; index++) {
            if (samplingFrequencies[index] == mSampleRate) {
                break;
            }
        }
        if (index == samplingFrequencies.length) {
            // TODO(nfaralli): log something here.
            // Invalid sampling frequency. Default to 44100Hz...
            index = 4;
        }
        audioSpecificConfig[2] |= (byte)((index >> 1) & 0x07);
        audioSpecificConfig[3] |= (byte)(((index & 1) << 7) | ((mChannels & 0x0F) << 3));
        System.arraycopy(
                audioSpecificConfig, 0, decConfigDescr, offset, audioSpecificConfig.length);

        // create the ES Descriptor
        byte[] ESDescriptor = new byte[2 + ESDescriptor_top[1]];
        System.arraycopy(ESDescriptor_top, 0, ESDescriptor, 0, ESDescriptor_top.length);
        offset = ESDescriptor_top.length;
        System.arraycopy(decConfigDescr, 0, ESDescriptor, offset, decConfigDescr.length);
        offset += decConfigDescr.length;
        System.arraycopy(slConfigDescr, 0, ESDescriptor, offset, slConfigDescr.length);
        return ESDescriptor;
    }

    private Atom getSTTSAtom() {
        Atom atom = new Atom("stts", (byte)0, 0);
        int numAudioFrames = mFrameSize.length - 1;
        atom.setData(new byte[] {
                0, 0, 0, 0x02,  // entry count
                0, 0, 0, 0x01,  // first frame contains no audio
                0, 0, 0, 0,
                (byte)((numAudioFrames >> 24) & 0xFF), (byte)((numAudioFrames >> 16) & 0xFF),
                (byte)((numAudioFrames >> 8) & 0xFF), (byte)(numAudioFrames & 0xFF),
                0, 0, 0x04, 0,  // delay between frames = 1024 samples (cf. timescale = Fs)
        });
        return atom;
    }

    private Atom getSTSCAtom() {
        Atom atom = new Atom("stsc", (byte)0, 0);
        int numFrames = mFrameSize.length;
        atom.setData(new byte[] {
                0, 0, 0, 0x01,  // entry count
                0, 0, 0, 0x01,  // first chunk
                (byte)((numFrames >> 24) & 0xFF), (byte)((numFrames >> 16) & 0xFF),  // samples per
                (byte)((numFrames >> 8) & 0xFF), (byte)(numFrames & 0xFF),           // chunk
                0, 0, 0, 0x01,  // sample description index
        });
        return atom;
    }

    private Atom getSTSZAtom() {
        Atom atom = new Atom("stsz", (byte)0, 0);
        int numFrames = mFrameSize.length;
        byte[] data = new byte[8 + 4 * numFrames];
        int offset = 0;
        data[offset++] = 0;  // sample size (=0 => each frame can have a different size)
        data[offset++] = 0;
        data[offset++] = 0;
        data[offset++] = 0;
        data[offset++] = (byte)((numFrames >> 24) & 0xFF);  // sample count
        data[offset++] = (byte)((numFrames >> 16) & 0xFF);
        data[offset++] = (byte)((numFrames >> 8) & 0xFF);
        data[offset++] = (byte)(numFrames & 0xFF);
        for (int size : mFrameSize) {
            data[offset++] = (byte)((size >> 24) & 0xFF);
            data[offset++] = (byte)((size >> 16) & 0xFF);
            data[offset++] = (byte)((size >> 8) & 0xFF);
            data[offset++] = (byte)(size & 0xFF);
        }
        atom.setData(data);
        return atom;
    }

    private Atom getSTCOAtom() {
        Atom atom = new Atom("stco", (byte)0, 0);
        atom.setData(new byte[] {
                0, 0, 0, 0x01,   // entry count
                0, 0, 0, 0  // chunk offset. Set to 0 here. Must be set later. Here it should be
                            // the size of the complete header, as the AAC stream will follow
                            // immediately.
        });
        return atom;
    }
}
