// Detect It Easy: detection rule file
// Author: Jason Hood <jadoxa@yahoo.com.au>
/*
   Ref: http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm
        http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header
*/

meta("audio", "MP3 Audio File");

includeScript("duration");

var MP3 = {
    aVersion: ["2.5", "2", "1"],
    aLayer: ["III", "II", "I"],
    aRatev1: [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, // Layer III
        32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, // Layer II
        32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448
    ], // Layer I
    aRatev2: [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, // Layers III & II
        32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256
    ], // Layer I
    aFreq: [11025, 12000, 8000, // v2.5
        22050, 24000, 16000, // v2
        44100, 48000, 32000
    ], // v1
    aCh: ["Stereo", "Joint stereo", "Dual mono", "Mono"],

    header: function (nOffset) {
        // AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
        var nHeader = X.U32(nOffset, _BE);
        if ((nHeader & 0xFFE00000) != ~~0xFFE00000) // AAAAAAAAAAA
        {
            return null;
        }
        var nVer = (nHeader >>> 19) & 3; // BB
        if (nVer == 1) // reserved
        {
            return null;
        }
        if (nVer > 1) {
            --nVer;
        }
        var nLayer = (nHeader >>> 17) & 3; // CC
        if (nLayer == 0) // reserved
        {
            return null;
        }
        --nLayer;
        var nBitrateIndex = (nHeader >>> 12) & 15; // EEEE
        if (nBitrateIndex == 0 || nBitrateIndex == 15) // free/bad
        {
            return null;
        }
        --nBitrateIndex;
        var nSamplingIndex = (nHeader >>> 10) & 3; // FF
        if (nSamplingIndex == 3) // reserved
        {
            return null;
        }
        var nPadding = (nHeader >>> 9) & 1; // G
        var nChannelMode = (nHeader >>> 6) & 3; // II

        var nBitrate = nVer == 2 ? this.aRatev1[nLayer * 14 + nBitrateIndex] :
            this.aRatev2[(nLayer == 2) * 14 + nBitrateIndex];
        var nFreq = this.aFreq[nVer * 3 + nSamplingIndex];
        var nTime;
        if (nLayer == 2) // Layer I
        {
            nTime = 384 / nFreq;
        } else if (nLayer == 1 || nVer == 2) // Layer II or v1,Layer III
        {
            nTime = 1152 / nFreq;
        } else // v2/2.5,Layer III
        {
            nTime = 576 / nFreq;
        }
        var bVBR = 1;
        var nVBR = Binary.findString(nOffset, 40, "Xing");
        if (nVBR == -1) {
            nVBR = Binary.findString(nOffset, 40, "Info");
            bVBR = 0;
        }
        if (nVBR != -1 && (X.U8(nVBR + 7) & 1)) {
            nTime *= X.U32(nVBR + 8, _BE);
            nOffset = bVBR ? -1 : -2;
        } else if (X.c("'VBRI'", nOffset + 32)) {
            nTime *= X.U32(nVBR + 46, _BE);
            nOffset = -1;
        } else {
            if (nLayer == 2) // Layer I
            {
                nOffset += Math.floor(12000 * nBitrate / nFreq + nPadding) * 4;
            } else {
                // 125 = 1000/8 = kilobits to bytes
                nOffset += Math.floor(125 * nBitrate * nTime + nPadding);
            }
        }
        return [this.aVersion[nVer],
        this.aLayer[nLayer],
            nBitrate,
            nFreq,
        this.aCh[nChannelMode],
            nTime,
            nOffset
        ];
    }
}

function detect() {
    if (X.Sz() >= 128) {
        if (X.c("'TAG'", X.Sz() - 128)) {
            sOptions = "ID3v1";
        }
        var nOffset = 0,
            bID3v2 = 0;
        if (X.c("'ID3'")) {
            nOffset = (X.U8(6) << 21) | (X.U8(7) << 14) | (X.U8(8) << 7) | (X.U8(9));
            nOffset += 10;
            if (sOptions == "") {
                sOptions = "ID3v2";
            } else {
                sOptions += "&2";
            }
            bID3v2 = 1;
        } else if (X.c("'WAVEfmt '........55", 8)) {
            nOffset = Binary.findString(50, 50, "data") + 8;
        }

        var aResult1, aResult = null;
        aResult1 = MP3.header(nOffset);

        if (aResult1 == null) {
            if (bID3v2) {
                // There may be some junk between the ID3 tag & the data (Win7
                // sample music files - C:\Users\Public\Music\Sample Music).
                // Since the ID3 tag is a fair indicator of an MP3, do a search
                // for v1,Layer III header.
                nOffset = Binary.findSignature(nOffset, 768, "FFFB");
                aResult1 = MP3.header(nOffset);
            }
            if (aResult1 == null) {
                return "";
            }
        }

        if (aResult1[6] > 0) {
            aResult = MP3.header(aResult1[6]);
            if (aResult == null) {
                return "";
            }
        }
        bDetected = true;
        // Assume version, layer & channels don't change.
        sVersion = aResult1[0] + ",Layer " + aResult1[1];

        if (File.isVerbose()) {
            var nFrames = 1,
                nRateSum = aResult1[2],
                nFreqSum = aResult1[3],
                nSeconds = aResult1[5];

            var bVBR = 0,
                bVF = 0;

            while (aResult != null) {
                nFrames++;
                nRateSum += aResult[2];
                nFreqSum += aResult[3];
                nSeconds += aResult[5];
                if (aResult[2] != aResult1[2]) {
                    bVBR = 1;
                }
                if (aResult[3] != aResult1[3]) {
                    bVF = 1;
                }
                aResult = MP3.header(aResult[6]);
            }
            sOptions = sOptions.append(Math.round(nRateSum / nFrames) + "kbps");
            if (bVBR) {
                sOptions += "(avg)";
            }
            sOptions = sOptions.append(Math.round(nFreqSum / nFrames) + "Hz");
            if (bVF) {
                sOptions += "(avg)";
            }
            sOptions = sOptions.append(aResult1[4], duration(nSeconds));
        }
    }

    return result();
}