使用原生Chrome Javascript/FileReader/DataView读取id3 v2.4标签

36
根据ebidel的回答,可以使用jDataView读取id3v1标签。
document.querySelector('input[type="file"]').onchange = function (e) {
    var reader = new FileReader();

    reader.onload = function (e) {
        var dv = new jDataView(this.result);

        // "TAG" starts at byte -128 from EOF.
        // See http://en.wikipedia.org/wiki/ID3
        if (dv.getString(3, dv.byteLength - 128) == 'TAG') {
            var title = dv.getString(30, dv.tell());
            var artist = dv.getString(30, dv.tell());
            var album = dv.getString(30, dv.tell());
            var year = dv.getString(4, dv.tell());
        } else {
            // no ID3v1 data found.
        }
    };

    reader.readAsArrayBuffer(this.files[0]);
};

现在Chrome和其他浏览器已经实现了DataView(我只关心Chrome)。 我想知道有没有人知道如何:

  1. 使用本地DataView读取标签
  2. 读取id3 v2.4标签(包括APIC图像“coverart”)

问题在于我没有处理二进制文件的经验,也不知道如何跳转到正确的标记位置,也不知道什么是小端和大端(或其他一些术语)。 我只需要一个标记的示例 - 比如说标题,TIT2标记,希望这可以帮助我理解如何跳转到正确的位置并读取其他标记:

function readID3() {
    //https://developer.mozilla.org/en-US/docs/Web/API/DataView
    //and the position
    //http://id3.org/id3v2.4.0-frames
    //var id3={};
    //id3.TIT2=new DataView(this.result,?offset?,?length?)

    /*
     ?
     var a=new DataView(this.result);
     console.dir(String.fromCharCode(a.getUint8(0)));
     ?
    */
}
function readFile() {
    var a = new FileReader();
    a.onload = readID3;
    a.readAsArrayBuffer(this.files[0]);
}
fileBox.addEventListener('change', readFile, false);

这里是JSFiddle


更新

http://jsfiddle.net/s492L/3/

我添加了getString函数,这样我就能读取第一行并检查其是否包含ID3。现在我需要找到第一个标签(TIT2)的位置以及该字符串的“可变”长度,还要检查它是否是2.4版。

//Header
//ID3v2/file identifier    "ID3"
//ID3v2 version            $04 00
//ID3v2 flags         (%ab000000 in v2.2, %abc00000 in v2.3, %abcd0000 in v2.4.x)
//ID3v2 size                 4 * %0xxxxxxx

可能的外部资源:

https://developer.mozilla.org/zh-CN/docs/Web/API/DataView

http://id3.org/id3v2.4.0-frames

http://id3.org/id3v2.4.0-structure

http://blog.nihilogic.dk/2008/08/reading-id3-tags-with-javascript.html

http://ericbidelman.tumblr.com/post/8343485440/reading-mp3-id3-tags-in-javascript

https://github.com/aadsm/JavaScript-ID3-Reader

目前我正在使用 PHP 的 getid3 库...

http://getid3.sourceforge.net/

http://getid3.sourceforge.net/source2/module.tag.id3v2.phps


这个有用吗?https://github.com/antimatter15/js-id3v2 - Fez Vrasta
我正在寻找本地函数... antimatter使用了很多polyfills...这使得它非常缓慢和不稳定。它在许多浏览器上运行良好...但我只需要它用于Chrome。我只想使用最新的js 1.7+来处理重要的功能,如ajax filereader dataview.... antimatter的代码适用于一个文件...但如果你需要处理多个文件,这不是一个好主意。 - cocco
另外,使用Chrome您可以使用持久性存储,从而操作大文件。随着更多的知识,这也将允许您将这些标签写入文件。还要考虑MP4格式。 - cocco
我在Node.js中使用这个库非常成功,也许你可以通过browserify运行它?https://github.com/leetreveil/musicmetadata 由于它需要一个可读流,所以你必须找到一种方法将数据打包成一个ReadableStream。你可以使用XHR获取mp3并将responseType设置为'arraybuffer'以处理原始字节。 - Nick Desaulniers
这个好处在于它可以在客户端运行...如果客户端可以做数学运算,为什么要在服务器上使用Node呢? - cocco
无论如何...没有找到合适的解决方案...代码应该只在Chrome和Safari等WebKit浏览器上工作...这意味着可以减少70-80%的代码。如果有人能够给我提示如何正确解析第一个字符串,我就能够编写其余部分并分享它...antimatter15的代码很棒,但大多数函数现在都是本地的。base64= atob() & btoa(), dataview... - cocco
3个回答

3
你可以尝试使用 GitHub上的id3解析器这是你更新后的fiddle,它会在控制台中记录标签对象。 只需在代码中包含id3.js,然后执行以下操作即可:
function readFile(){
   id3(this.files[0], function(err, tags) {
       console.log(tags);
   })
}
document.getElementsByTagName('input')[0].addEventListener('change',readFile,false);

这里是由id3创建的tags对象:

{
  "title": "Stairway To Heaven",
  "album": "Stairway To Heaven",
  "artist": "Led Zeppelin",
  "year": "1999",
  "v1": {
    "title": "Stairway To Heaven",
    "artist": "Led Zeppelin",
    "album": "Stairway To Heaven",
    "year": "1999",
    "comment": "Classic Rock",
    "track": 13,
    "version": 1.1,
    "genre": "Other"
  },
  "v2": {
    "version": [3, 0],
    "title": "Stairway To Heaven",
    "album": "Stairway To Heaven",
    "comments": "Classic Rock",
    "publisher": "Virgin Records"
  }
}

希望这有所帮助!

希望我们能进一步改进这个简化函数。 - cocco

2
使用我在这里找到的代码:http://www.ulduzsoft.com/2012/07/parsing-id3v2-tags-in-the-mp3-files/,我将其翻译成了Javascript代码,并发布在这里:http://jsfiddle.net/eb7rrbw4/ 以下是我编写的代码:
DataView.prototype.getChar=function(start) {
    return String.fromCharCode(this.getUint8(start));
};
DataView.prototype.getString=function(start,length) {
    for(var i=0,v='';i<length;++i) {
        v+=this.getChar(start+i);
    }
    return v;
};
DataView.prototype.getInt=function(start) {
    return (this.getUint8(start) << 21) | (this.getUint8(start+1) << 14) | (this.getUint8(start+2) << 7) | this.getUint8(start+3);
};

function readID3(){
    var a=new DataView(this.result);
    // Parse it quickly
    if ( a.getString(0,3)!="ID3" )
    {
        return false;
    }

    // True if the tag is pre-V3 tag (shorter headers)
    var TagVersion = a.getUint8(3);

    // Check the version
    if ( TagVersion < 0 || TagVersion > 4 )
    {
        return false;
    }

    // Get the ID3 tag size and flags; see 3.1
    var tagsize = a.getInt(6)+10;
        //(a.getUint8(9) & 0xFF) | ((a.getUint8(8) & 0xFF) << 7 ) | ((a.getUint8(7) & 0xFF) << 14 ) | ((a.getUint8(6) & 0xFF) << 21 ) + 10;
    var uses_synch = (a.getUint8(5) & 0x80) != 0 ? true : false;
    var has_extended_hdr = (a.getUint8(5) & 0x40) != 0 ? true : false;

    var headersize=0;         
    // Read the extended header length and skip it
    if ( has_extended_hdr )
    {
        var headersize = a.getInt(10);
            //(a.getUint8(10) << 21) | (a.getUint8(11) << 14) | (a.getUint8(12) << 7) | a.getUint8(13); 
    }

    // Read the whole tag
    var buffer=new DataView(a.buffer.slice(10+headersize,tagsize));

    // Prepare to parse the tag
    var length = buffer.byteLength;

    // Recreate the tag if desynchronization is used inside; we need to replace 0xFF 0x00 with 0xFF
    if ( uses_synch )
    {
        var newpos = 0;
        var newbuffer = new DataView(new ArrayBuffer(tagsize));

        for ( var i = 0; i < tagsize; i++ )
        {
            if ( i < tagsize - 1 && (buffer.getUint8(i) & 0xFF) == 0xFF && buffer.getUint8(i+1) == 0 )
            {
                newbuffer.setUint8(newpos++,0xFF);
                i++;
                continue;
            }

            newbuffer.setUint8(newpos++,buffer.getUint8(i));                 
        }

        length = newpos;
        buffer = newbuffer;
    }

    // Set some params
    var pos = 0;
    var ID3FrameSize = TagVersion < 3 ? 6 : 10;
    var m_title;
    var m_artist;

    // Parse the tags
    while ( true )
    {
        var rembytes = length - pos;

        // Do we have the frame header?
        if ( rembytes < ID3FrameSize )
            break;

        // Is there a frame?
        if ( buffer.getChar(pos) < 'A' || buffer.getChar(pos) > 'Z' )
            break;

        // Frame name is 3 chars in pre-ID3v3 and 4 chars after
        var framename;
        var framesize;

        if ( TagVersion < 3 )
        {
            framename = buffer.getString(pos,3);
            framesize = ((buffer.getUint8(pos+5) & 0xFF) << 8 ) | ((buffer.getUint8(pos+4) & 0xFF) << 16 ) | ((buffer.getUint8(pos+3) & 0xFF) << 24 );
        }
        else
        {
            framename = buffer.getString(pos,4);
            framesize = buffer.getInt(pos+4);
                //(buffer.getUint8(pos+7) & 0xFF) | ((buffer.getUint8(pos+6) & 0xFF) << 8 ) | ((buffer.getUint8(pos+5) & 0xFF) << 16 ) | ((buffer.getUint8(pos+4) & 0xFF) << 24 );
        }

        if ( pos + framesize > length )
            break;

        if ( framename== "TPE1"  || framename== "TPE2"  || framename== "TPE3"  || framename== "TPE" )
        {
            if ( m_artist == null )
                m_artist = parseTextField( buffer, pos + ID3FrameSize, framesize );
        }

        if ( framename== "TIT2" || framename== "TIT" )
        {
            if ( m_title == null )
                m_title = parseTextField( buffer, pos + ID3FrameSize, framesize );
        }

        pos += framesize + ID3FrameSize;
        continue;
    }
    console.log(m_title,m_artist);
    return m_title != null || m_artist != null;
}

function parseTextField( buffer, pos, size )
{
    if ( size < 2 )
        return null;

    var charcode = buffer.getUint8(pos); 

    //TODO string decoding         
    /*if ( charcode == 0 )
        charset = Charset.forName( "ISO-8859-1" );
    else if ( charcode == 3 )
        charset = Charset.forName( "UTF-8" );
    else
        charset = Charset.forName( "UTF-16" );

    return charset.decode( ByteBuffer.wrap( buffer, pos + 1, size - 1) ).toString();*/
    return buffer.getString(pos+1,size-1);
}

您应该在控制台日志中看到标题和作者。不过,请查看解析文本函数,其中编码确定读取字符串的方式(搜索TODO)。此外,我尚未测试它是否与扩展头部、uses_synch true或tag版本3一起使用。


我希望我们能够进一步改进这个最小功能。 - cocco

1

部分正确的答案(它可以正确读取utf8格式的id3v2.4.0,包括封面)

我在问题中提出的事情现在可能已经有效了。

我想要一个非常简单的最小功能集来处理只有id3v2.4.0并解析附加的图像。

在@Siderite Zackwehdex的帮助下,他的答案被标记为正确,我理解了缺失的代码的重要部分。

因为我有一些时间来玩它,所以我对代码进行了各种修改。

首先抱歉脚本压缩,但我对整体代码有更好的概述。这对我来说更容易。如果您对代码有任何疑问,请随时问。

无论如何,我删除了uses_synch...真的很难找到使用synch的文件。同样适用于has_extended_hdr。我还删除了对id3v2.0.0到id3v2.2.0的支持。我添加了版本检查,该版本适用于所有id3v2子版本。

主要功能输出包含一个带有所有标签的数组,内部还可以找到id3v2版本。最后,但我想扩展一下,我添加了一个自定义的FRAME对象,其中包含除textFrames以外的自定义函数。现在唯一的函数将image/cover/APIC转换为易于使用的base64字符串。这样做,数组可以存储为JSON字符串。
虽然对于一些人来说兼容性很重要,但上述扩展头或同步实际上是最小的问题。
问题:
编码需要是UTF-8,否则您会得到奇怪的文本填充,有些图像仅被部分解析。基本上是损坏的。
我想避免使用外部库甚至是真正大的函数...需要一些聪明简单的解决方案来正确处理编码。 ISO-8859-1,UTF-8,UTF-16..大端...无论如何...#00 vs#00 00..
如果完成了这项工作,支持可以呈指数级提高。
我希望你们中的一些人有解决办法。
代码
DataView.prototype.str=function(a,b,c,d){//start,length,placeholder,placeholder
 b=b||1;c=0;d='';for(;c<b;)d+=String.fromCharCode(this.getUint8(a+c++));return d
}
DataView.prototype.int=function(a){//start
 return (this.getUint8(a)<<21)|(this.getUint8(a+1)<<14)|
 (this.getUint8(a+2)<<7)|this.getUint8(a+3)
}
var frID3={
 'APIC':function(x,y,z,q){
  var b=0,c=['',0,''],d=1,e,b64;
  while(b<3)e=x.getUint8(y+z+d++),c[b]+=String.fromCharCode(e),
  e!=0||(b+=b==0?(c[1]=x.getUint8(y+z+d),2):1);
  b64='data:'+c[0]+';base64,'+
  btoa(String.fromCharCode.apply(null,new Uint8Array(x.buffer.slice(y+z+++d,q))));
  return {mime:c[0],description:c[2],type:c[1],base64:b64}
 }
}
function readID3(a,b,c,d,e,f,g,h){
 if(!(a=new DataView(this.result))||a.str(0,3)!='ID3')return;
 g={Version:'ID3v2.'+a.getUint8(3)+'.'+a.getUint8(4)};
 a=new DataView(a.buffer.slice(10+((a.getUint8(5)&0x40)!=0?a.int(10):0),a.int(6)+10));
 b=a.byteLength;c=0;d=10;
 while(true){
  f=a.str(c);e=a.int(c+4);
  if(b-c<d||(f<'A'||f>'Z')||c+e>b)break;
  g[h=a.str(c,4)]=frID3[h]?frID3[h](a,c,d,e):a.str(c+d,e);
  c+=e+d;
 }
 console.log(g);
}

演示

https://jsfiddle.net/2awq6pz7/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接