<?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=newAudioExif;
//$file='/path/to/test.mp3';
//
//1.检查文件是否完整(onlyforwma,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('PHPExtensionmodule`mbstring`isrequiredforAudioExif',E_USER_WARNING);
returntrue;
}
//theMainClass
classAudioExif{
//publicvars
var$_wma=false;
var$_mp3=false;
//Construct
functionAudioExif(){
//nothingtodo
}
//checkthefilesize
functionCheckSize($file){
$handler=&$this->_get_handler($file);
if(!$handler)returnfalse;
return$handler->check_size($file);
}
//gettheinfomations
functionGetInfo($file){
$handler=&$this->_get_handler($file);
if(!$handler)returnfalse;
return$handler->get_info($file);
}
//writetheinfomations
functionSetInfo($file,$pa){
if(!is_writable($file)){
trigger_error('AudioExif:file`'.$file.'`cannotbeenoverwritten',E_USER_WARNING);
returnfalse;
}
$handler=&$this->_get_handler($file);
if(!$handler)returnfalse;
return$handler->set_info($file,$pa);
}
//privatemethods
function&_get_handler($file){
$ext=strtolower(strrchr($file,'.'));
$ret=false;
if($ext=='.mp3'){
//MP3
$ret=&$this->_mp3;
if(!$ret)$ret=new_Mp3Exif();
}
elseif($ext=='.wma')
{//wma
$ret=&$this->_wma;
if(!$ret)$ret=new_WmaExif();
}
else
{//unknown
trigger_error('AudioExifnotsupported`'.$ext.'`file.',E_USER_WARNING);
}
return$ret;
}
}
//DBCS=>gb2312
functiondbcs_gbk($str)
{
//stripthelast"\0\0"
$str=substr($str,0,-2);
returnmb_convert_encoding($str,'GBK','UCS-2LE');
}
//gb2312=>DBCS
functiongbk_dbcs($str)
{
$str=mb_convert_encoding($str,'UCS-2LE','GBK');
$str.="\0\0";
return$str;
}
//fileexif
class_AudioExif
{
var$fd;
var$head;
var$head_off;
var$head_buf;
//initthefilehandler
function_file_init($fpath,$write=false)
{
$mode=($write?'rb+':'rb');
$this->[email protected]($fpath,$mode);
if(!$this->fd)
{
trigger_error('AudioExif:`'.$fpath.'`cannotbeopenedwithmode`'.$mode.'`',E_USER_WARNING);
returnfalse;
}
$this->head=false;
$this->head_off=0;
$this->head_buf='';
returntrue;
}
//readbufferfromthehead_buf&movetheoffpointer
function_read_head_buf($len)
{
if($len<=0)returnNULL;
$buf=substr($this->head_buf,$this->head_off,$len);
$this->head_off+=strlen($buf);
return$buf;
}
//readoneshortvalue
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));
}
//savethefilehead
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,bufferrequired
$stat=fstat($this->fd);
$fsize=$stat['size'];
//bufrequired(4096?)应该不会nlen-olen>4096吧
$woff=0;
$roff=$olen;
//readfirstbuffer
flock($this->fd,LOCK_EX);
fseek($this->fd,$roff,SEEK_SET);
$buf=fread($this->fd,4096);
//seektostart
fseek($this->fd,$woff,SEEK_SET);
fwrite($this->fd,$head,$nlen);
$woff+=$nlen;
//seektowoff&writethedata
do
{
$buf2=$buf;
$roff+=4096;
if($roff<$fsize)
{
fseek($this->fd,$roff,SEEK_SET);
$buf=fread($this->fd,4096);
}
//savelastbuffer
$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);
}
}
//closethefile
function_file_deinit()
{
if($this->fd)
{
fclose($this->fd);
$this->fd=false;
}
}
}
//wmaclass
class_WmaExifextends_AudioExif
{
var$items1=array('Title','Artist','Copyright','Description','Reserved');
var$items2=array('Year','Genre','AlbumTitle');
//checkfilesize(length)maybeinvalidfile
functioncheck_size($file)
{
$ret=false;
if(!$this->_file_init($file))returntrue;
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;
}
//setinfo(savetheinfos)
functionset_info($file,$pa)
{
//checkthepa
settype($pa,'array');
if(!$this->_file_init($file,true))returnfalse;
if(!$this->_init_header())
{
$this->_file_deinit();
returnfalse;
}
//parsetheoldheader&generatethenewheader
$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')
{//StandardInfo
//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));
}
//leftlength
if($tmp['len']>0)$st_body2.=$this->_read_head_buf($tmp['len']);
//savetohead_body
$head_body.=pack('H32VH8',$tmp['id'],strlen($st_body1.$st_body2)+24,$tmp['unused']);
$head_body.=$st_body1.$st_body2;
$st_body2.=$data;
}
//savetohead_body
$head_body.=pack('H32Va4','3326b2758e66cf11a6d900aa0062ce6c',strlen($st_body1.$st_body2)+24,'');
$head_body.=$st_body1.$st_body2;
$this->head['num']++;
}
//exnotfound?
if(!$ex_found)
{
$inum=0;
$et_body='';
foreach($this->items2as$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']++;
}
//aftersave
$new_len=strlen($head_body)+30;
$old_len=$this->head['len'];
if($new_len<$old_len)
{
$head_body.=str_repeat("\0",$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);
//closethefile&return
$this->_file_deinit();
returntrue;
}
//getinfo
functionget_info($file)
{
$ret=array();
if(!$this->_file_init($file))returnfalse;
if(!$this->_init_header())
{
$this->_file_deinit();
returnfalse;
}
//getthedatafromhead_buf
$head_num=$this->head['num'];//numofhead_frame
while(($tmp=$this->_get_head_frame())&&$head_num>0)
{
$head_num--;
if($tmp['id']=='3326b2758e66cf11a6d900aa0062ce6c')
{//StandardInfo
$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]));
}
}
elseif($tmp['id']=='40a4d0d207e3d21197f000a0c95ea850')
{//ExtendedInfo
$inum=$this->_read_head_short();
$tmp['len']-=26;
while($inum>0&&$tmp['len']>0)
{
//attributename
$nlen=$this->_read_head_short();
$nbuf=$this->_read_head_buf($nlen);
//theflag&valuelength
$flag=$this->_read_head_short();
$vlen=$this->_read_head_short();
$vbuf=$this->_read_head_buf($vlen);
//updatetheXX
$tmp['len']-=(6+$nlen+$vlen);
$inum--;
$name=dbcs_gbk($nbuf);
$k=substr($name,3);
if(in_array($k,$this->items2))
{//allisstringvalue(refertofalgforothertags)
$ret[$k]=dbcs_gbk($vbuf);
}
}
}
else
{//skiponly
if($tmp['len']>24)$this->head_off+=($tmp['len']-24);
}
}
$this->_file_deinit();
return$ret;
}
//gettheheader?
function_init_header()
{
fseek($this->fd,0,SEEK_SET);
$buf=fread($this->fd,30);
if(strlen($buf)!=30)returnfalse;
$tmp=unpack('H32id/Vlen/Vlen2/Vnum/H4unused',$buf);
if($tmp['id']!='3026b2758e66cf11a6d900aa0062ce6c')
returnfalse;
$this->head_buf=fread($this->fd,$tmp['len']-30);
$this->head=$tmp;
returntrue;
}
//_get_head_frame()
function_get_head_frame()
{
$buf=$this->_read_head_buf(24);
if(strlen($buf)!=24)returnfalse;
$tmp=unpack('H32id/Vlen/H8unused',$buf);
return$tmp;
}
}
//mp3class(ifnotIDv2thenselectIDv1)
class_Mp3Exifextends_AudioExif
{
var$head1;
var$genres=array('Blues','ClassicRock','Country','Dance','Disco','Funk','Grunge','Hip-Hop','Jazz','Metal','NewAge','Oldies','Other','Pop','R&B','Rap','Reggae','Rock','Techno','Industrial','Alternative','Ska','DeathMetal','Pranks','Soundtrack','Euro-Techno','Ambient','Trip-Hop','Vocal','Jazz+Funk','Fusion','Trance','Classical','Instrumental','Acid','House','Game','SoundClip','Gospel','Noise','AlternRock','Bass','Soul','Punk','Space','Meditative','InstrumentalPop','InstrumentalRock','Ethnic','Gothic','Darkwave','Techno-Industrial','Electronic','Pop-Folk','Eurodance','Dream','SouthernRock','Comedy','Cult','Gangsta','Top40','ChristianRap','Pop/Funk','Jungle','NativeAmerican','Cabaret','NewWave','Psychadelic','Rave','Showtunes','Trailer','Lo-Fi','Tribal','AcidPunk','AcidJazz','Polka','Retro','Musical','Rock&Roll','HardRock','Unknown');
//MP3alwaysreturntrue
functioncheck_size($file)
{
returntrue;
}
//getinfo
functionget_info($file)
{
if(!$this->_file_init($file))returnfalse;
$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;
}
//setinfo
functionset_info($file,$pa)
{
if(!$this->_file_init($file,true))returnfalse;
if($this->_init_header())
{
//alwayssavev1info
$this->_set_v1_info($pa);
//setv2firstifneed
$this->_set_v2_info($pa);
}
$this->_file_deinit();
returntrue;
}
//gettheheaderinformation[v1+v2],callafterfile_init
function_init_header()
{
$this->head1=false;
$this->head=false;
//trytogetID3v1first
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;
}
//trytogetID3v2
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);
}
//getv1info
function_get_v1_info()
{
$ret=array();
$tmpa=array('Title','Artist','Copyright','Description','Year','AlbumTitle');
foreach($tmpaas$tmp)
{
$ret[$tmp]=$this->head1[$tmp];
if($pos=strpos($ret[$tmp],"\0"))
$ret[$tmp]=substr($ret[$tmp],0,$pos);
}
//counttheGenre,[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];
//returnthevalue
$ret['ID3v1']='yes';
return$ret;
}
//getv2info
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(1100000000000000)
if($tmp['flag']&0x6000)continue;
//mappingthedata
if($k=$items[$tmp['fid']])
{//Iffirstcharis"\0",justskip
if(substr($tmp['dat'],0,1)=="\0")$tmp['dat']=substr($tmp['dat'],1);
$ret[$k]=$tmp['dat'];
}
}
//resetthegenre
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;
}
//getmetainfoofMP3
function_get_meta_info()
{
//seektotheleadbuf: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)returnfalse;
$buf=fread($this->fd,3);
if(strlen($buf)!=3)returnfalse;
$tmp=unpack('C3',$buf);
if(($tmp[1]&0xf0)!=0xf0)returnfalse;
//getthemetainfo
$meta=array();
//getmpegversion
$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';
//bitrates
$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];
//samplerates<采样率>
$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','jointstereo','dualchannel','singlechannel');
$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/15microsecs','rreserved','CCITTJ17');
$k=($tmp[3]&0x03);
$meta['emphasis']=$emphasis[$k];
return$meta;
}
//setv1info
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;
}
//basicitems
$items=array('Title','Artist','Copyright','Description','Year','AlbumTitle');
foreach($itemsas$k)
{
if(isset($pa[$k]))$tmp[$k]=$pa[$k];
}
//genreindex
if(isset($pa['Genre']))
{
$g=0;
foreach($this->genresas$gtmp)
{
if(!strcasecmp($gtmp,$pa['Genre']))
break;
$g++;
}
$tmp['Genre']=$g;
}
if(isset($pa['Track']))$tmp['Track']=intval($pa['Track']);
//packthedata
$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);
}
//setv2info
function_set_v2_info($pa)
{
if(!$this->head)
{//insertID3
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]))
{
//thedatashouldprefixby"\0"[replace]
$data="\0".$pa[$k];
unset($pa[$k]);
}
$head_body.=pack('a4Nn',$tmp['fid'],strlen($data),$tmp['flag']).$data;
}
//reversetheitems&setthenewtags
$items=array_flip($items);
foreach($paas$k=>$v)
{
if($fid=$items[$k])
{
$head_body.=pack('a4Nn',$fid,strlen($v)+1,0)."\0".$v;
}
}
//newlength
$new_len=strlen($head_body)+10;
$old_len=$this->head['size']+10;
if($new_len<$old_len)
{
$head_body.=str_repeat("\0",$old_len-$new_len);
$new_len=$old_len;
}
//countthesize1,2,3,4,noincludetheheader
//较为变态的算法...:p(28bytesinteger)
$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_lennew_len:$new_len\n";
$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);
}
|