Node.js ID3v2.3标准Tag写入测试代码

之前说好的在XiamiThief v0.5中添加自动写入ID3标签这一功能, 于是这两天我就看着ID3v2.3的文档边写代码边用Mp3tag测试, 文档中写得不好的我就用Mp3tag生成一遍然后开WinHex对比, 这个功能总算在今晚被我搞定了.

因为JavaScript无法直接表示二进制数值, 所以我写了个bin函数作为String.fromCharCode方法的别名, 将字符串形式的二进制数值转换成Unicode字符. 后来我才发现这是一个非常愚蠢的做法, 因为最后写入数据时ASCII编码会自动将String.fromCharCode得到的0x00(Unciode)编码成0x20(ASCII), 我不得不把空字节单独分离开用UTF8输出.

看来以后有空要把代码改成Node的C++模块, 用JavaScript直接操作数据的太吃亏了(或许用新版本中Buffer类型的几个新方法配合十六进制数值效果会好一点).

虽然可以达到目的但是非常愚蠢的代码:

CoffeeScript

fs = require 'fs'

String::times = (n)->Array::join.call length:n+1, @

split = (str, len)->  
    chunks = []
    pos = str.length
    while pos > 0
        temp = if pos - len > 0 then pos - len else 0
        chunks.unshift str.slice(temp, pos)
        pos = temp
    chunks

prefixInteger = (num, length)->  
    (num / Math.pow(10, length)).toFixed(length).substr(2);

isArray = (input)->  
    typeof(input) is 'object' and input instanceof Array

hex = (input)->  
    if isArray(input)
        String.fromCharCode.apply @, input
    else
        String.fromCharCode input

bin = (input)->  
    if isArray(input)
        String.fromCharCode.apply @, (parseInt(i,2) for i in input)
    else
        String.fromCharCode parseInt(input,2)

dec2bin = (input, len=8)->  
    bin (prefixInteger(i,8) for i in split(input.toString(2), len) )

class id3v23  
    constructor: (@filename,@tags={})->
    setTag:(key,value)->
        @tags[key] = value
    write:(cb)->
        oldData = fs.readFileSync(@filename)
        fd = fs.openSync(@filename,'w+')

        ###
            标签头 固定为10个字节
            顺序 Identifier(3) Version(2) Flags(1) Size(4)
        ###
        header = new Buffer(10)
        headerFileIdentifier = 'ID3'
        headerVersion = hex [0x03,0x00]
        headerFlags = bin '00000000'

        ###
            帧
            顺序 Id(4) Size(4) Flags(2) Data(?)
            Id + Size + Flags 共10个字节
            Size为帧的总大小-10, 即Data的大小
        ###
        frames = []
        for tag, value of @tags
            pure = not isNaN value # 区分全数字字符串
            pic = Buffer.isBuffer value # 区分图像数据
            frameId = tag
            frameFlags = bin ['00000000','00000000']
            if pic
                type = 'image/jpeg' + hex([0x00,0x00,0x00])
                frameData = new Buffer(type.length + value.length)
                frameData.write type,0,type.length
                value.copy frameData, type.length
            else
                frameData = value
            if pure or pic
                bom = hex [0x00]
                frameSize = dec2bin frameData.length + bom.length
                frame = new Buffer(10 + frameData.length + bom.length)
            else
                bom = hex [0x01, 0xFF, 0xFE]
                frameSize = dec2bin frameData.length * 2 + bom.length
                frame = new Buffer(10 + frameData.length * 2 + bom.length)
            frame.write frameId,0,4,'utf8'
            blankLength = 4-frameSize.length
            frame.write hex([0x00]).times(blankLength),4,blankLength,'utf8'
            frame.write frameSize,4 + blankLength,frameSize.length,'ascii'
            frame.write frameFlags,8,2,'utf8'
            if pure or pic
                frame.write bom,10,bom.length,'utf8'
                if pic
                    frameData.copy frame, 10 + bom.length
                else
                    frame.write frameData,10 + bom.length, frameData.length, 'ascii'
            else
                frame.write bom,10,bom.length,'ascii'
                frame.write frameData,10 + bom.length, frameData.length * 2, 'ucs2'
            frames.push(frame)

        framesLength = 0
        for frame in frames
            framesLength += frame.length

        headerSize = dec2bin framesLength + 10 -1, 7 # 补码
        header.write headerFileIdentifier + headerVersion + headerFlags
        blankLength = 4-headerSize.length
        header.write hex([0x00]).times(blankLength),6,blankLength,'utf8'
        header.write headerSize,6 + blankLength,10,'ascii'

        fs.writeSync fd,header,0,header.length,0 # 写入标签头
        for frame in frames
            fs.writeSync fd,frame,0,frame.length #循环写入帧
        fs.writeSync fd,new Buffer(new Array(800)),0,800 # 不知道为什么Mp3Tag只认写了800个空字节的, 不写入就报!BAD ID3V2
        fs.writeSync fd,oldData,0,oldData.length # 写入原数据

        fs.closeSync(fd)

writer = new id3v23('White Prism - 初音ミク.mp3')

#TALB 专辑名
writer.setTag 'TALB', 'Fictional World'

#TPE1 主唱
writer.setTag 'TPE1', '初音ミク'

#TIT2 歌名
writer.setTag 'TIT2', 'White Prism'

#TRCK 音轨号
writer.setTag 'TRCK', '8'

#TYER 灌录年份
writer.setTag 'TYER', '2013'

#APIC 专辑封面
writer.setTag 'APIC', fs.readFileSync('Fictional World.jpg')

console.time 'Encode & Write'  
writer.write()  
console.timeEnd 'Encode & Write'  

JavaScript

// Generated by CoffeeScript 1.6.3
(function() {
  var bin, dec2bin, fs, hex, id3v23, isArray, prefixInteger, split, writer;

  fs = require('fs');

  String.prototype.times = function(n) {
    return Array.prototype.join.call({
      length: n + 1
    }, this);
  };

  split = function(str, len) {
    var chunks, pos, temp;
    chunks = [];
    pos = str.length;
    while (pos > 0) {
      temp = pos - len > 0 ? pos - len : 0;
      chunks.unshift(str.slice(temp, pos));
      pos = temp;
    }
    return chunks;
  };

  prefixInteger = function(num, length) {
    return (num / Math.pow(10, length)).toFixed(length).substr(2);
  };

  isArray = function(input) {
    return typeof input === 'object' && input instanceof Array;
  };

  hex = function(input) {
    if (isArray(input)) {
      return String.fromCharCode.apply(this, input);
    } else {
      return String.fromCharCode(input);
    }
  };

  bin = function(input) {
    var i;
    if (isArray(input)) {
      return String.fromCharCode.apply(this, (function() {
        var _i, _len, _results;
        _results = [];
        for (_i = 0, _len = input.length; _i < _len; _i++) {
          i = input[_i];
          _results.push(parseInt(i, 2));
        }
        return _results;
      })());
    } else {
      return String.fromCharCode(parseInt(input, 2));
    }
  };

  dec2bin = function(input, len) {
    var i;
    if (len == null) {
      len = 8;
    }
    return bin((function() {
      var _i, _len, _ref, _results;
      _ref = split(input.toString(2), len);
      _results = [];
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        i = _ref[_i];
        _results.push(prefixInteger(i, 8));
      }
      return _results;
    })());
  };

  id3v23 = (function() {
    function id3v23(filename, tags) {
      this.filename = filename;
      this.tags = tags != null ? tags : {};
    }

    id3v23.prototype.setTag = function(key, value) {
      return this.tags[key] = value;
    };

    id3v23.prototype.write = function(cb) {
      var blankLength, bom, fd, frame, frameData, frameFlags, frameId, frameSize, frames, framesLength, header, headerFileIdentifier, headerFlags, headerSize, headerVersion, oldData, pic, pure, tag, type, value, _i, _j, _len, _len1, _ref;
      oldData = fs.readFileSync(this.filename);
      fd = fs.openSync(this.filename, 'w+');
      /*
          标签头 固定为10个字节
          顺序 Identifier(3) Version(2) Flags(1) Size(4)
      */

      header = new Buffer(10);
      headerFileIdentifier = 'ID3';
      headerVersion = hex([0x03, 0x00]);
      headerFlags = bin('00000000');
      /*
          帧
          顺序 Id(4) Size(4) Flags(2) Data(?)
          Id + Size + Flags 共10个字节
          Size为帧的总大小-10, 即Data的大小
      */

      frames = [];
      _ref = this.tags;
      for (tag in _ref) {
        value = _ref[tag];
        pure = !isNaN(value);
        pic = Buffer.isBuffer(value);
        frameId = tag;
        frameFlags = bin(['00000000', '00000000']);
        if (pic) {
          type = 'image/jpeg' + hex([0x00, 0x00, 0x00]);
          frameData = new Buffer(type.length + value.length);
          frameData.write(type, 0, type.length);
          value.copy(frameData, type.length);
        } else {
          frameData = value;
        }
        if (pure || pic) {
          bom = hex([0x00]);
          frameSize = dec2bin(frameData.length + bom.length);
          frame = new Buffer(10 + frameData.length + bom.length);
        } else {
          bom = hex([0x01, 0xFF, 0xFE]);
          frameSize = dec2bin(frameData.length * 2 + bom.length);
          frame = new Buffer(10 + frameData.length * 2 + bom.length);
        }
        frame.write(frameId, 0, 4, 'utf8');
        blankLength = 4 - frameSize.length;
        frame.write(hex([0x00]).times(blankLength), 4, blankLength, 'utf8');
        frame.write(frameSize, 4 + blankLength, frameSize.length, 'ascii');
        frame.write(frameFlags, 8, 2, 'utf8');
        if (pure || pic) {
          frame.write(bom, 10, bom.length, 'utf8');
          if (pic) {
            frameData.copy(frame, 10 + bom.length);
          } else {
            frame.write(frameData, 10 + bom.length, frameData.length, 'ascii');
          }
        } else {
          frame.write(bom, 10, bom.length, 'ascii');
          frame.write(frameData, 10 + bom.length, frameData.length * 2, 'ucs2');
        }
        frames.push(frame);
      }
      framesLength = 0;
      for (_i = 0, _len = frames.length; _i < _len; _i++) {
        frame = frames[_i];
        framesLength += frame.length;
      }
      headerSize = dec2bin(framesLength + 10 - 1, 7);
      header.write(headerFileIdentifier + headerVersion + headerFlags);
      blankLength = 4 - headerSize.length;
      header.write(hex([0x00]).times(blankLength), 6, blankLength, 'utf8');
      header.write(headerSize, 6 + blankLength, 10, 'ascii');
      fs.writeSync(fd, header, 0, header.length, 0);
      for (_j = 0, _len1 = frames.length; _j < _len1; _j++) {
        frame = frames[_j];
        fs.writeSync(fd, frame, 0, frame.length);
      }
      fs.writeSync(fd, new Buffer(new Array(800)), 0, 800);
      fs.writeSync(fd, oldData, 0, oldData.length);
      return fs.closeSync(fd);
    };

    return id3v23;

  })();

  writer = new id3v23('White Prism - 初音ミク.mp3');

  writer.setTag('TALB', 'Fictional World');

  writer.setTag('TPE1', '初音ミク');

  writer.setTag('TIT2', 'White Prism');

  writer.setTag('TRCK', '8');

  writer.setTag('TYER', '2013');

  writer.setTag('APIC', fs.readFileSync('Fictional World.jpg'));

  console.time('Encode & Write');

  writer.write();

  console.timeEnd('Encode & Write');

}).call(this);