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

Posted

技术标签:

【中文标题】使用原生 Chrome Javascript/FileReader/DataView 读取 id3 v2.4 标签【英文标题】:Read id3 v2.4 tags with native Chrome Javascript/FileReader/DataView 【发布时间】:2013-12-11 07:54:24 【问题描述】:

根据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 感兴趣)。我很好奇是否有人知道如何:

    使用原生 DataView 读取标签 读取 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/en-US/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

【问题讨论】:

这有帮助吗? github.com/antimatter15/js-id3v2 我正在寻找原生函数...反物质使用了很多 polyfills...这使得它非常缓慢且不稳定。它在许多浏览器上都运行良好...但我只需要它用于 chrome .我只想将最新的 js 1.7+ 用于 ajax filereader dataview 等重要功能......反物质的代码适用于一个文件......但如果你需要处理多个文件,这不是一个好主意。跨度> 加上使用 chrome,您可以使用持久存储,从而操作大文件。有了更多知识,您还可以将这些标签写入文件。还可以考虑 mp4 格式。 我在 Node.js 中使用它取得了很大的成功,也许你可以通过 browserify 运行它? github.com/leetreveil/musicmetadata 因为它需要一个可读流,所以你必须找到一种方法将数据打包到 ReadableStream 中。您可以使用 XHR 获取 mp3 并将 responseType 设置为 'arraybuffer' 以使用原始字节。 这方面的好处是它可以在客户端运行......如果客户端可以做到这一点,为什么要在服务器上使用节点来计算呢???? 【参考方案1】:

您可以尝试使用id3 parser on github。

Here's your updated fiddle that logs the tags object in the console

包含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"
  

希望这会有所帮助!

【讨论】:

***.com/a/36346510/2450730 我希望我们可以进一步改进这个最小功能【参考方案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);

您应该在控制台日志中看到标题和作者。不过,请查看 parse text 函数,其中的编码决定了读取字符串的方式。 (搜索待办事项)。此外,我还没有使用扩展标头或 uses_synch true 或标记版本 3 对其进行测试。

【讨论】:

***.com/a/36346510/2450730 我希望我们可以进一步改进这个最小功能【参考方案3】:

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

我在问题中提出的问题现在可能有效。

我想要一个非常粗糙的最小函数集来仅处理 id3v2.4.0 & 并解析附加的图像。

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

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

首先对压缩脚本感到抱歉,但我对整个代码有更好的了解。对我来说更容易。如果您对代码有任何疑问,请直接询问。

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

主函数输出包含一个包含所有标签的数组,在里面你还可以找到 id3v2 Version.Last,但我想扩展很有用,我添加了一个自定义 FRAME 对象,其中包含除 textFrames 之外的 FRAMES 自定义函数。现在唯一的内部函数将图像/封面/APIC 转换为易于使用的 base64 字符串。这样做可以将数组存储为 JSON 字符串。

虽然对你们中的一些人来说兼容性很重要,但上述扩展标头或同步实际上是最小的问题。

问题

编码必须是 UTF-8 否则你会得到奇怪的文本填充和 某些图像仅被部分解析。基本坏了。

我想避免使用外部库甚至是一个非常大的函数……需要一些智能简单的解决方案来正确处理编码。 ISO-8859-1,UTF-8,UTF-16 .. big endian... 随便... #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/

【讨论】:

以上是关于使用原生 Chrome Javascript/FileReader/DataView 读取 id3 v2.4 标签的主要内容,如果未能解决你的问题,请参考以下文章

如何更改 Chrome 原生日期时间选择器的蓝色?

如何在台式机上调试 Android 的原生浏览器(不是 Chrome)?

使用chrome开发者工具在原生安卓应用中调试WebView

使用Chrome开发者工具研究JavaScript里函数的原生实现

使用Chrome开发者工具研究JavaScript里函数的原生实现

让 Chrome 浏览器也能使用苹果原生分享菜单