Jubeat Analyzer(mciSendString)对待MP3奇怪的行为

作为一个Jubeat玩家,并且还想过那些比较难的曲子,怎么样也会听说过Jubeat Analyzer吧。这个工具可以把街机上那些谱面在电脑上重现出来,这样就可以先看清楚它是怎么回事,然后再去玩,增加过关的概率。

这软件读取的谱面格式是TXT,音乐是MP3。看它的readme,里面有提到谱面的TXT的格式。里面有写曲子的速度(每分钟多少拍),从什么地方开始是第一拍,之类的。

它自带两个曲子,我当时研究了一下那个twilight goose,发现里面写的曲子第一拍开始的时间是1747毫秒。但是实际上我把MP3丢到Audacity里面打开,仔细观察,第一拍在1.5秒左右就开始了。

神奇的是,在Jubeat Analyzer里面播放的时候,它第一拍还真的是1.747开始……

因为Jubeat Analyzer的谱面里,写关于这个“偏移” 的方式有两种,手头上其他谱面基本都是“r=xxx”这种,而它是“o=xxx”这种。作者的readme里说到,其实区别只是后者的数字比前者多100。

※注意点 2:「それでは譜面の冒頭に "r=840" と書くのと "o=840" と書くのでは効果が全く同じなのではないか??」

と思われる方がいらっしゃるかもしれません。そう思いますよね。

その通りならよかったのですが………実は、前者は後者より100ミリ秒譜面が遅れて再生されます。(これ以上の詳細は、ホームページをご覧ください)

 

但是我总觉得和这个有点相关,研究了半天无果,最后只是很武断地把所有“o=xxx”的曲子都往后推200毫秒,这样才差不多有点对头。

但是这个200还是让人不安心,毕竟是试验出来的东西,而且没有什么依据,换了其他文件会不会就不行了呢,之类的。为了知道里面更详细的东西,我给作者写了电子邮件,问他关于o=的仕样,是不是有什么独特的地方。作者回信里看来,确实就是100毫秒的差别,还让我自己两个互换着试试看。我把twilight goose里面的o=1747改成r=1647,还真的播放起来和原来无异样。

这么说,问题出在MP3上面了。这两个MP3,用MediaInfo去打开,对照里面的信息,也没发现有什么类似Delay的东西。看来MediaInfo也不够可靠了,那我用十六进制编辑器去开起来看看好了

这个ID3赫然写在上面,总觉得就是它的问题了。因为Jubeat Analyzer播放MP3用的是mciSendString这种接口,如果MCI播放MP3的时候把ID3v2当成是数据流来解码,那么就会产生奇怪的延迟了吧。

这个网站找了关于ID3v2的相关内容,得知ID3v2的长度是可变的,可以根据第7~10字节去掉最高位(那就是28位)以后的二进制数来确定长度。看twilight goose的7~10字节,是00 00 20 25,那就是00×80³+0x00×80²+20×80¹+25 = 1025(全部是16进制)。这样就是4133字节。但是这些到底会被当作什么样的数据流来解码呢。鉴于以前很常见的都是128kbps的mp3,那我就根据128kbps来计算,于是4133(字节)×8(比特每字节)÷128000(比特每秒)=0.258(秒)。这样把源文件延迟0.258秒,就是mciSendString播放出来的效果了。

但是后来又遇到了点问题,又不是所有有ID3v2的文件都会出这问题。考虑到没延迟的MP3是Jubeat OST里面的MP3,制作的人还嵌入了标题图片之类, 似乎ID3v2都挺大的,……于是猜想可能是解码的时候遇到了什么问题而跳过了。于是去参考了一下这个网站上的MP3每个帧的组成,得知头部信息是4个字节,之后可能有可能没有4个字节的CRC,有没有CRC是根据头部信息决定,然后是数据。如果ID3v2被当作数据来解码的话,"ID3"和后面一个字节被当作头部信息。这样就变成可以解释为“有CRC保护”。然后接下来四个字节变成CRC,其中这四个字节里后两个字节就是ID3v2大小的前两个字节……好吧大概知道了,如果ID3v2比较小,只占用了两个字节,那么前两个字节就是0,这样的话它就认为没有CRC信息,于是数据包是好是坏不知道,解码之。如果ID3v2比较大,占用了3个或4个字节,这样当作帧数据来解释的话CRC就不是全0了。因为它本来就不是帧数据,CRC怎么可能会能通过(不会那么狗屎运吧-_,-),于是此时这个帧就被认为是坏的,跳过,于是延迟就没了。

然后我写了这么一段代码来计算它产生的延迟的大小。当作128kbps来计算的。因为实在不知道它到底是怎么识别码率的,毕竟如果从帧头里面抽取码率信息的话,算出来的延迟超大,感觉不对头。而且帧头又没有连续11个二进制1来当作同步标记。

于是这段不知道算出来延迟是对是错的代码长得这样:

double CheckMp3_mciSendStringWorkAround(std::istream& mp3file)
{
    char id3[3];

    mp3file.read(id3, 3);
    if(mp3file.gcount() != 3) throw MyException("Fail to read id3v2 tag");

    if(std::equal(id3, id3 + 3, "ID3")) {
        //start workaround
        //skip 3 byte, advanced to id3v2 frame size
        mp3file.ignore(3);
        unsigned char id3v2size[4];

        mp3file.read(reinterpret_cast<char*>(id3v2size), 4);
        if (mp3file.gcount() != 4) throw MyException("Fail to read id3v2 size");

        int nId3v2size = id3v2size[0] * 0x80 * 0x80 * 0x80
                       + id3v2size[1] * 0x80 * 0x80
                       + id3v2size[2] * 0x80
                       + id3v2size[3];

        int id3end = nId3v2size + 10;
        mp3file.seekg(0, std::ios::beg);
        char buf[8]; //frame header + crc
        mp3file.read(buf, 8);
        if (mp3file.gcount() != 8) throw MyException("Fail to read mp3 frames");
        if (buf[1] & 1) {//check if there are crc
            nId3v2size -= 4;
        } else {
            if ( (buf[4] | buf[5] | buf[6] | buf[7]) == 0 )
                nId3v2size -= 8;
            else
                return 0;
        }

        //get duration
        return double(nId3v2size) * 8 / 128000;
    } else
        //no need to work around
        return 0;
}

于是mciSendString播放MP3的时候由于ID3v2而产生的延迟,可以计算了。

那个128000……唔,想不到更好的办法猜测它内部到底淦了什么。只能这么勉强了。

发表评论