解析用PHP读写音频文件信息的详解(支持WMA和MP3)
author:一佰互联 2019-04-30   click:158
复制代码 代码如下:
<?php
// AudioExif.class.php
// 用PHP进行音频文件头部信息的读取与写入
// 目前只支持 WMA 和 MP3 两种格式, 只支持常用的几个头部信息
//
// 写入信息支持: Title(名称), Artist(艺术家), Copyright(版权), Description (描述)
//               Year(年代),  Genre (流派),   AlbumTitle (专辑标题)
// 其中 mp3 和 wma 略有不同, 具体返回的信息还可能更多, 但只有以上信息可以被写入
// mp3 还支持 Track (曲目编号写入)
// 对于 MP3 文件支持 ID3v1也支持ID3v2, 读取时优先 v2, 写入时总是会写入v1, 必要时写入v2
//
// 用法说明: (由于 wma 使用 Unicode 存取, 故还需要 mb_convert_encoding() 扩展
//           返回数据及写入数据均为 ANSI 编码, 即存什么就显示什么 (中文_GB2312)
//
// require ("AudioExif.class.php");
// $AE = new AudioExif;
// $file = "/path/to/test.mp3";
//
// 1. 检查文件是否完整 (only for wma, mp3始终返回 true)
//
// $AE->CheckSize($file);
//
// 2. 读取信息, 返回值由信息组成的数组, 键名解释参见上方
//
// print_r($AE->GetInfo($file));
//
// 3. 写入信息, 第二参数是一个哈希数组, 键->值, 支持的参见上方的, mp3也支持 Track
//    要求第一参数的文件路径可由本程序写入
// $pa = array("Title" => "新标题", "AlbumTitle" => "新的专辑名称");
// $AE->SetInfo($file, $pa);
//
//
// 其它: 该插件花了不少时间搜集查找 wma及mp3 的文件格式说明文档与网页, 希望对大家有用.
//       其实网上已经有不少类似的程序, 但对 wma 实在太少了, 只能在 win 平台下通过 M$ 的
//       API 来操作, 而 MP3 也很少有可以在 unix/linux 命令行操作的, 所以特意写了这个模块
//
// 如果发现 bug 或提交 patch, 或加以改进使它更加健壮, 请告诉我.
// (关于 ID3和Wma的文件格式及结构 在网上应该都可以找到参考资料)
//
if (!extension_loaded("mbstring"))
{
  trigger_error("PHP Extension module `mbstring` is required for AudioExif", E_USER_WARNING);
  return true;
}
// the Main Class
class AudioExif
{
  // public vars
  var $_wma = false;
  var $_mp3 = false;
  // Construct
  function AudioExif()
  {
    // nothing to do
  }
  // check the filesize
  function CheckSize($file)
  {
    $handler = &$this->_get_handler($file);
    if (!$handler) return false;
    return $handler->check_size($file);
  }
  // get the infomations
  function GetInfo($file)
  {
    $handler = &$this->_get_handler($file);
    if (!$handler) return false;
    return $handler->get_info($file);
  }
  // write the infomations
  function SetInfo($file, $pa)
  {
    if (!is_writable($file))
    {
      trigger_error("AudioExif: file `" . $file . "` can not been overwritten", E_USER_WARNING);
      return false;
    }
    $handler = &$this->_get_handler($file);
    if (!$handler) return false;
    return $handler->set_info($file, $pa);
  }
  // private methods
  function &_get_handler($file)
  {
    $ext = strtolower(strrchr($file, "."));
    $ret = false;
    if ($ext == ".mp3")
    {  // MP3
      $ret = &$this->_mp3;
      if (!$ret) $ret = new _Mp3Exif();
    }
    else if ($ext == ".wma")
    {  // wma
      $ret = &$this->_wma;
      if (!$ret) $ret = new _WmaExif();
    }
    else
    {  // unknown
      trigger_error("AudioExif not supported `" . $ext . "` file.", E_USER_WARNING);
    }
    return $ret;
  }
}
// DBCS => gb2312
function dbcs_gbk($str)
{
  // strip the last ""
  $str = substr($str, 0, -2);
  return mb_convert_encoding($str, "GBK", "UCS-2LE");
}
// gb2312 => DBCS
function gbk_dbcs($str)
{
  $str  = mb_convert_encoding($str, "UCS-2LE", "GBK");
  $str .= "";
  return $str;
}
// file exif
class _AudioExif
{
  var $fd;
  var $head;
  var $head_off;
  var $head_buf;  // init the file handler
  function _file_init($fpath, $write = false)
  {
    $mode = ($write ? "rb+" : "rb");
    $this->fd = @fopen($fpath, $mode);
    if (!$this->fd)
    {
      trigger_error("AudioExif: `" . $fpath . "` can not be opened with mode `" . $mode . "`", E_USER_WARNING);
      return false;
    }
    $this->head = false;
    $this->head_off = 0;
    $this->head_buf = "";
    return true;
  }
  // read buffer from the head_buf & move the off pointer
  function _read_head_buf($len)
  {
    if ($len <= 0) return NULL;
    $buf = substr($this->head_buf, $this->head_off, $len);
    $this->head_off += strlen($buf);
    return $buf;
  }
  // read one short value
  function _read_head_short()
  {
    $ord1 = ord(substr($this->head_buf, $this->head_off, 1));
    $ord2 = ord(substr($this->head_buf, $this->head_off+1, 1));
    $this->head_off += 2;
    return ($ord1 + ($ord2<<8));
  }
  // save the file head
  function _file_save($head, $olen, $nlen = 0)
  {
    if ($nlen == 0) $nlen = strlen($head);
    if ($nlen == $olen)
    {
      // shorter
      flock($this->fd, LOCK_EX);
      fseek($this->fd, 0, SEEK_SET);
      fwrite($this->fd, $head, $nlen);
      flock($this->fd, LOCK_UN);
    }
    else
    {
      // longer, buffer required
      $stat = fstat($this->fd);
      $fsize = $stat["size"];
      // buf required (4096?) 应该不会 nlen - olen > 4096 吧
      $woff = 0;
      $roff = $olen;
      // read first buffer
      flock($this->fd, LOCK_EX);
      fseek($this->fd, $roff, SEEK_SET);
      $buf = fread($this->fd, 4096);
      // seek to start
      fseek($this->fd, $woff, SEEK_SET);
      fwrite($this->fd, $head, $nlen);
      $woff += $nlen;
      // seek to woff & write the data
      do
      {
        $buf2 = $buf;
        $roff += 4096;
        if ($roff < $fsize)
        {
          fseek($this->fd, $roff, SEEK_SET);
          $buf = fread($this->fd, 4096);       
        }
        // save last buffer
        $len2 = strlen($buf2);
        fseek($this->fd, $woff, SEEK_SET);
        fwrite($this->fd, $buf2, $len2);
        $woff += $len2;
      }
      while ($roff < $fsize);
      ftruncate($this->fd, $woff);
      flock($this->fd, LOCK_UN);
    }
  }
  // close the file
  function _file_deinit()
  {
    if ($this->fd)
    {
      fclose($this->fd);
      $this->fd = false;
    } 
  }
}
// wma class
class _WmaExif extends _AudioExif
{
  var $items1 = array("Title", "Artist", "Copyright", "Description", "Reserved");
  var $items2 = array("Year", "Genre", "AlbumTitle");
  // check file size (length) maybe invalid file
  function check_size($file)
  {
    $ret = false;
    if (!$this->_file_init($file)) return true;
    if ($this->_init_header())
    {
      $buf = fread($this->fd, 24);
      $tmp = unpack("H32id/Vlen/H8unused", $buf);
      if ($tmp["id"] == "3626b2758e66cf11a6d900aa0062ce6c")
      {
        $stat = fstat($this->fd);
        $ret = ($stat["size"] == ($this->head["len"] + $tmp["len"]));
      }
    }
    $this->_file_deinit();
    return $ret;
  }
  // set info (save the infos)
  function set_info($file, $pa)
  {
    // check the pa
    settype($pa, "array");
    if (!$this->_file_init($file, true)) return false;
    if (!$this->_init_header())
    {
      $this->_file_deinit();
      return false;
    }    // parse the old header & generate the new header
    $head_body = "";
    $st_found = $ex_found = false;
    $head_num = $this->head["num"];
    while (($tmp = $this->_get_head_frame()) && ($head_num > 0))
    {
      $head_num--;
      if ($tmp["id"] == "3326b2758e66cf11a6d900aa0062ce6c")
      {  // Standard Info
        // 1-4
        $st_found = true;
        $st_body1 = $st_body2 = "";      
        $lenx = unpack("v5", $this->_read_head_buf(10));
        $tmp["len"] -= 34;  // 10 + 24
        for ($i = 0; $i < count($this->items1); $i++)
        {
          $l = $lenx[$i+1];
          $k = $this->items1[$i];
          $tmp["len"] -= $l;
          $data = $this->_read_head_buf($l);
          if (isset($pa[$k])) $data = gbk_dbcs($pa[$k]);
          $st_body2 .= $data;
          $st_body1 .= pack("v", strlen($data));
        }
        // left length
        if ($tmp["len"] > 0) $st_body2 .= $this->_read_head_buf($tmp["len"]);
        // save to head_body
        $head_body .= pack("H32VH8", $tmp["id"], strlen($st_body1 . $st_body2)+24, $tmp["unused"]);
        $head_body .= $st_body1 . $st_body2; 
      }
      else if ($tmp["id"] == "40a4d0d207e3d21197f000a0c95ea850")
      {  // extended info
        $ex_found = true;        $inum = $this->_read_head_short();
        $inum2 = $inum;
        $tmp["len"] -= 26;  // 24 + 2
        $et_body = "";
        while ($tmp["len"] > 0 && $inum > 0)
        {
          // attribute name
          $nlen = $this->_read_head_short();
          $nbuf = $this->_read_head_buf($nlen);
          // the flag & value  length
          $flag = $this->_read_head_short();
          $vlen = $this->_read_head_short();
          $vbuf = $this->_read_head_buf($vlen);
          // set the length
          $tmp["len"] -= (6 + $nlen + $vlen);
          $inum--;
          // save the data?
          $name = dbcs_gbk($nbuf);
          $k = substr($name, 3);
          if (in_array($k, $this->items2) && isset($pa[$k]))
          {
            $vbuf = gbk_dbcs($pa[$k]);
            $vlen = strlen($vbuf);
            unset($pa[$k]);
          }
          $et_body .= pack("v", $nlen) . $nbuf . pack("vv", $flag, $vlen) . $vbuf;
        }
        // new tag insert??
        foreach ($this->items2 as $k)
        {
          if (isset($pa[$k]))
          {
            $inum2++;
            $nbuf = gbk_dbcs("WM/" . $k);
            $nlen = strlen($nbuf);
            $vbuf = gbk_dbcs($pa[$k]);
            $vlen = strlen($vbuf);
            $et_body .= pack("v", $nlen) . $nbuf . pack("vv", 0, $vlen) . $vbuf;
          }
        }
        // left buf?
        if ($tmp["len"] > 0) $et_body .= $this->_read_head_buf($tmp["len"]);
        // save to head_body
        $head_body .= pack("H32VH8v", $tmp["id"], strlen($et_body)+26, $tmp["unused"], $inum2);
        $head_body .= $et_body; 
      }
      else
      {
        // just keep other head frame
        $head_body .= pack("H32VH8", $tmp["id"], $tmp["len"], $tmp["unused"]);
        if ($tmp["len"] > 24) $head_body .= $this->_read_head_buf($tmp["len"]-24);
      }
    }
    // st not found?
    if (!$st_found)
    {
      $st_body1 = $st_body2 = "";
      foreach ($this->items1 as $k)
      {
        $data = (isset($pa[$k]) ? gbk_dbcs($pa[$k]) : "");
        $st_body1 .= pack("v", strlen($data));
        $st_body2 .= $data;
      }      // save to head_body
      $head_body .= pack("H32Va4", "3326b2758e66cf11a6d900aa0062ce6c", strlen($st_body1 . $st_body2)+24, "");
      $head_body .= $st_body1 . $st_body2;
      $this->head["num"]++;
    }
    // ex not found?
    if (!$ex_found)
    {
      $inum = 0;
      $et_body = "";
      foreach ($this->items2 as $k)
      {
        $nbuf = gbk_dbcs("WM/" . $k);
        $vbuf = (isset($pa[$k]) ? gbk_dbcs($pa[$k]) : "");
        $et_body .= pack("v", strlen($nbuf)) . $nbuf . pack("vv", 0, strlen($vbuf)) . $vbuf;
        $inum++;
      }
      $head_body .= pack("H32Va4v", "40a4d0d207e3d21197f000a0c95ea850", strlen($et_body)+26, "", $inum);
      $head_body .= $et_body;
      $this->head["num"]++;
    } 
    // after save
    $new_len = strlen($head_body) + 30;
    $old_len = $this->head["len"];
    if ($new_len < $old_len)
    {
      $head_body .= str_repeat("", $old_len - $new_len);
      $new_len = $old_len;
    }
    $tmp = $this->head;
    $head_buf = pack("H32VVVH4", $tmp["id"], $new_len, $tmp["len2"], $tmp["num"], $tmp["unused"]);
    $head_buf .= $head_body;
    $this->_file_save($head_buf, $old_len, $new_len);
    // close the file & return
    $this->_file_deinit();
    return true;
  }
  // get info
  function get_info($file)
  {
    $ret = array();
    if (!$this->_file_init($file)) return false;
    if (!$this->_init_header())
    {
      $this->_file_deinit();
      return false;
    }
    // get the data from head_buf
    $head_num = $this->head["num"];  // num of head_frame
    while (($tmp = $this->_get_head_frame()) && $head_num > 0)
    {
      $head_num--;
      if ($tmp["id"] == "3326b2758e66cf11a6d900aa0062ce6c")
      {  // Standard Info
        $lenx = unpack("v*", $this->_read_head_buf(10));
        for ($i = 1; $i <= count($this->items1); $i++)
        {
          $k = $this->items1[$i-1];
          $ret[$k] = dbcs_gbk($this->_read_head_buf($lenx[$i]));
        }
      }
      else if ($tmp["id"] == "40a4d0d207e3d21197f000a0c95ea850")
      {  // Extended Info
        $inum = $this->_read_head_short();
        $tmp["len"] -= 26;
        while ($inum > 0 && $tmp["len"] > 0)
        {
          // attribute name
          $nlen = $this->_read_head_short();
          $nbuf = $this->_read_head_buf($nlen);
          // the flag & value  length
          $flag = $this->_read_head_short();
          $vlen = $this->_read_head_short();
          $vbuf = $this->_read_head_buf($vlen);
          // update the XX
          $tmp["len"] -= (6 + $nlen + $vlen);
          $inum--;
          $name = dbcs_gbk($nbuf);
          $k = substr($name, 3);
          if (in_array($k, $this->items2))
          {  // all is string value (refer to falg for other tags)
            $ret[$k] = dbcs_gbk($vbuf);
          }
        } 
      }
      else
      {  // skip only
        if ($tmp["len"] > 24) $this->head_off += ($tmp["len"] - 24);
      }
    }
    $this->_file_deinit();
    return $ret;
  }
  // get the header?
  function _init_header()
  {
    fseek($this->fd, 0, SEEK_SET);
    $buf = fread($this->fd, 30);
    if (strlen($buf) != 30) return false;
    $tmp = unpack("H32id/Vlen/Vlen2/Vnum/H4unused", $buf);
    if ($tmp["id"] != "3026b2758e66cf11a6d900aa0062ce6c")
      return false;
    $this->head_buf = fread($this->fd, $tmp["len"] - 30);
    $this->head = $tmp;
    return true;
  }
  // _get_head_frame()
  function _get_head_frame()
  {
    $buf = $this->_read_head_buf(24); 
    if (strlen($buf) != 24) return false;
    $tmp = unpack("H32id/Vlen/H8unused", $buf);
    return $tmp;
  }
}
// mp3 class (if not IDv2 then select IDv1)
class _Mp3Exif extends _AudioExif
{
  var $head1;
  var $genres = array("Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz","Metal","New Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno","Industrial","Alternative","Ska","Death Metal","Pranks","Soundtrack","Euro-Techno","Ambient","Trip-Hop","Vocal","Jazz+Funk","Fusion","Trance","Classical","Instrumental","Acid","House","Game","Sound Clip","Gospel","Noise","AlternRock","Bass","Soul","Punk","Space","Meditative","Instrumental Pop","Instrumental Rock","Ethnic","Gothic","Darkwave","Techno-Industrial","Electronic","Pop-Folk","Eurodance","Dream","Southern Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/Funk","Jungle","Native American","Cabaret","New Wave","Psychadelic","Rave","Showtunes","Trailer","Lo-Fi","Tribal","Acid Punk","Acid Jazz","Polka","Retro","Musical","Rock & Roll","Hard Rock","Unknown");
  // MP3 always return true
  function check_size($file)
  {
    return true;
  }
  // get info
  function get_info($file)
  {
    if (!$this->_file_init($file)) return false; 
    $ret = false;
    if ($this->_init_header())
    {
      $ret = ($this->head ? $this->_get_v2_info() : $this->_get_v1_info());
      $ret["meta"] = $this->_get_meta_info();
    }
    $this->_file_deinit();
    return $ret;
  }
  // set info
  function set_info($file, $pa)
  {
    if (!$this->_file_init($file, true)) return false;
    if ($this->_init_header())
    {
      // always save v1 info
      $this->_set_v1_info($pa);    
      // set v2 first if need
      $this->_set_v2_info($pa);
    }
    $this->_file_deinit();
    return true;
  }
  // get the header information[v1+v2], call after file_init
  function _init_header()
  {
    $this->head1 = false;
    $this->head = false;
    // try to get ID3v1 first
    fseek($this->fd, -128, SEEK_END);
    $buf = fread($this->fd, 128);
    if (strlen($buf) == 128 && substr($buf, 0, 3) == "TAG")
    {
      $tmp = unpack("a3id/a30Title/a30Artist/a30AlbumTitle/a4Year/a28Description/CReserved/CTrack/CGenre", $buf);
      $this->head1 = $tmp;    
    }
    // try to get ID3v2
    fseek($this->fd, 0, SEEK_SET);
    $buf = fread($this->fd, 10);
    if (strlen($buf) == 10 && substr($buf, 0, 3) == "ID3")
    {
      $tmp = unpack("a3id/Cver/Crev/Cflag/C4size", $buf);
      $tmp["size"] = ($tmp["size1"]<<21)|($tmp["size2"]<<14)|($tmp["size3"]<<7)|$tmp["size4"];
      unset($tmp["size1"], $tmp["size2"], $tmp["size3"], $tmp["size4"]);
      $this->head = $tmp;
      $this->head_buf = fread($this->fd, $tmp["size"]);
    }
    return ($this->head1 || $this->head);
  }
  // get v1 info
  function _get_v1_info()
  {
    $ret = array();
    $tmpa = array("Title", "Artist", "Copyright", "Description", "Year", "AlbumTitle");
    foreach ($tmpa as $tmp)
    {    
      $ret[$tmp] = $this->head1[$tmp];
      if ($pos = strpos($ret[$tmp], ""))
        $ret[$tmp] = substr($ret[$tmp], 0, $pos);
    }
    // count the Genre, [Track]
    if ($this->head1["Reserved"] == 0) $ret["Track"] = $this->head1["Track"];
    else $ret["Description"] .= chr($ret["Reserved"]) . chr($ret["Track"]);
    // Genre_idx
    $g = $this->head1["Genre"];
    if (!isset($this->genres[$g])) $ret["Genre"] = "Unknown";
    else $ret["Genre"] = $this->genres[$g];
    // return the value
    $ret["ID3v1"] = "yes";
    return $ret;
  }
  // get v2 info
  function _get_v2_info()
  {
    $ret = array();
    $items = array(  "TCOP"=>"Copyright", "TPE1"=>"Artist", "TIT2"=>"Title", "TRCK"=> "Track",
            "TCON"=>"Genre", "COMM"=>"Description", "TYER"=>"Year", "TALB"=>"AlbumTitle");
    while (true)
    {
      $buf = $this->_read_head_buf(10);
      if (strlen($buf) != 10) break;    
      $tmp = unpack("a4fid/Nsize/nflag", $buf);
      if ($tmp["size"] == 0) break;
      $tmp["dat"] = $this->_read_head_buf($tmp["size"]);
      // 0x6000 (11000000 00000000) 
      if ($tmp["flag"] & 0x6000) continue;
      // mapping the data
      if ($k = $items[$tmp["fid"]])
      {  // If first char is "", just skip
        if (substr($tmp["dat"], 0, 1) == "") $tmp["dat"] = substr($tmp["dat"], 1);
        $ret[$k] = $tmp["dat"];
      }
    }
    // reset the genre
    if ($g = $ret["Genre"])
    {
      if (substr($g,0,1) == "(" && substr($g,-1,1) == ")") $g = substr($g, 1, -1);
      if (is_numeric($g))
      {
        $g = intval($g);
        $ret["Genre"] = (isset($this->genres[$g]) ? $this->genres[$g] : "Unknown");
      }
    }
    $ret["ID3v1"] = "no";
    return $ret;
  }
  // get meta info of MP3
  function _get_meta_info()
  {
    // seek to the lead buf: 0xff
    $off = 0;
    if ($this->head) $off = $this->head["size"] + 10;
    fseek($this->fd, $off, SEEK_SET);
    while (!feof($this->fd))
    {
      $skip = ord(fread($this->fd, 1));
      if ($skip == 0xff) break;
    }
    if ($skip != 0xff) return false;
    $buf = fread($this->fd, 3);
    if (strlen($buf) != 3) return false;
    $tmp = unpack("C3", $buf);
    if (($tmp[1] & 0xf0) != 0xf0) return false;
    // get the meta info
    $meta = array();
    // get mpeg version
    $meta["mpeg"]  = ($tmp[1] & 0x08 ? 1 : 2);
    $meta["layer"]  = ($tmp[1] & 0x04) ? (($tmp[1] & 0x02) ? 1 : 2) : (($tmp[1] & 0x02) ? 3 : 0);
    $meta["epro"]  = ($tmp[1] & 0x01) ? "no" : "yes";
    // bit rates
    $bit_rates = array(
      1 => array(
        1 => array(0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,0),
        2 => array(0,32,48,56,64,80,96,112,128,160,192,224,256,320,384,0),
        3 => array(0,32,40,48,56,64,80,96,112,128,160,192,224,256,320,0)
      ),
      2 => array(
        1 => array(0,32,48,56,64,80,96,112,128,144,160,176,192,224,256,0),
        2 => array(0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,0),
        3 => array(0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,0)
      )
    );
    $i = $meta["mpeg"];
    $j = $meta["layer"];
    $k = ($tmp[2]>>4);
    $meta["bitrate"] = $bit_rates[$i][$j][$k];
    // sample rates <采样率>
    $sam_rates = array(1=>array(44100,48000,32000,0), 2=>array(22050,24000,16000,0));
    $meta["samrate"] = $sam_rates[$i][$k];
    $meta["padding"] = ($tmp[2] & 0x02) ? "on" : "off";
    $meta["private"] = ($tmp[2] & 0x01) ? "on" : "off";
    // mode & mode_ext
    $k = ($tmp[3]>>6);
    $channel_modes = array("stereo", "joint stereo", "dual channel", "single channel");
    $meta["mode"] = $channel_modes[$k];
    $k = (($tmp[3]>>4) & 0x03);
    $extend_modes = array("MPG_MD_LR_LR", "MPG_MD_LR_I", "MPG_MD_MS_LR", "MPG_MD_MS_I");
    $meta["ext_mode"] = $extend_modes[$k];
    $meta["copyright"] = ($tmp[3] & 0x08) ? "yes" : "no";
    $meta["original"] = ($tmp[3] & 0x04) ? "yes" : "no";
    $emphasis = array("none", "50/15 microsecs", "rreserved", "CCITT J 17");
    $k = ($tmp[3] & 0x03);
    $meta["emphasis"] = $emphasis[$k];
    return $meta;
  }
  // set v1 info
  function _set_v1_info($pa)
  {
    // ID3v1 (simpled)
    $off = -128;
    if (!($tmp = $this->head1))
    {    
      $off = 0;
      $tmp["id"] = "TAG";
      $tmp["Title"] = $tmp["Artist"] = $tmp["AlbumTitle"] = $tmp["Year"] = $tmp["Description"] = "";
      $tmp["Reserved"] = $tmp["Track"] = $tmp["Genre"] = 0;
    }    // basic items
    $items = array("Title", "Artist", "Copyright", "Description", "Year", "AlbumTitle");
    foreach ($items as $k)
    {
      if (isset($pa[$k])) $tmp[$k] = $pa[$k];
    }
    // genre index
    if (isset($pa["Genre"]))
    {
      $g = 0;
      foreach ($this->genres as $gtmp)
      {
        if (!strcasecmp($gtmp, $pa["Genre"]))
          break;
        $g++;
      }
      $tmp["Genre"] = $g;
    }
    if (isset($pa["Track"])) $tmp["Track"] = intval($pa["Track"]);
    // pack the data
    $buf = pack("a3a30a30a30a4a28CCC",  $tmp["id"], $tmp["Title"], $tmp["Artist"], $tmp["AlbumTitle"],
            $tmp["Year"], $tmp["Description"], 0, $tmp["Track"], $tmp["Genre"]);
    flock($this->fd, LOCK_EX);
    fseek($this->fd, $off, SEEK_END);
    fwrite($this->fd, $buf, 128);
    flock($this->fd, LOCK_UN);
  }
  // set v2 info
  function _set_v2_info($pa)
  {
    if (!$this->head)
    {  // insert ID3
      return;  // 没有就算了
      /**
      $tmp = array("id"=>"ID3","ver"=>3,"rev"=>0,"flag"=>0);
      $tmp["size"] = -10;  // +10 => 0
      $this->head = $tmp;
      $this->head_buf = "";
      $this->head_off = 0;
      **/
    }
    $items = array(  "TCOP"=>"Copyright", "TPE1"=>"Artist", "TIT2"=>"Title", "TRAC"=>"Track",
            "TCON"=>"Genre", "COMM"=>"Description", "TYER"=>"Year", "TALB"=>"AlbumTitle");
    $head_body = "";
    while (true)
    {
      $buf = $this->_read_head_buf(10);
      if (strlen($buf) != 10) break;
      $tmp = unpack("a4fid/Nsize/nflag", $buf);
      if ($tmp["size"] == 0) break;
      $data = $this->_read_head_buf($tmp["size"]);
      if (($k = $items[$tmp["fid"]]) && isset($pa[$k]))
      {
        // the data should prefix by "" [replace]
        $data = "" . $pa[$k];
        unset($pa[$k]);
      }
      $head_body .= pack("a4Nn", $tmp["fid"], strlen($data), $tmp["flag"]) . $data;
    }
    // reverse the items & set the new tags
    $items = array_flip($items);
    foreach ($pa as $k => $v)
    {
      if ($fid = $items[$k])
      {
        $head_body .= pack("a4Nn", $fid, strlen($v) + 1, 0) . "" . $v;
      }
    }
    // new length
    $new_len = strlen($head_body) + 10;
    $old_len = $this->head["size"] + 10;
    if ($new_len < $old_len)
    {
      $head_body .= str_repeat("", $old_len - $new_len);
      $new_len = $old_len;    
    }
    // count the size1,2,3,4, no include the header
    // 较为变态的算法... :p (28bytes integer)
    $size = array();
    $nlen = $new_len - 10;
    for ($i = 4; $i > 0; $i--)
    {
      $size[$i] = ($nlen & 0x7f);
      $nlen >>= 7;
    }
    $tmp = $this->head;
    //echo "old_len : $old_len new_len: $new_len ";
    $head_buf = pack("a3CCCCCCC", $tmp["id"], $tmp["ver"], $tmp["rev"], $tmp["flag"],
      $size[1], $size[2], $size[3], $size[4]);
    $head_buf .= $head_body;
    $this->_file_save($head_buf, $old_len, $new_len);
  }
}
?>