PageRenderTime 53ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/zina/mp3.class.php

https://bitbucket.org/helmespc/zina2
PHP | 851 lines | 695 code | 99 blank | 57 comment | 209 complexity | 6444acfaf1bccca870d7041be8f5b746 MD5 | raw file
  1. <?php
  2. /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  3. * ZINA2 (Zina2 is not Zina)
  4. *
  5. * Zina2 is a graphical interface to your MP3 collection, a personal
  6. * jukebox, an MP3 streamer. It can run on its own, embeded into an
  7. * existing website, or as a Drupal/Joomla/Wordpress/etc. module.
  8. *
  9. * https://bitbucket.org/helmespc/zina2
  10. * Author: Patrick Helmes <helmespc@gmail.com>
  11. * Support: https://bitbucket.org/helmespc/zina2/wiki/Home
  12. * License: GNU GPL2 <http://www.gnu.org/copyleft/gpl.html>
  13. *
  14. * This Software is a fork of Zina (is not Andromeda):
  15. * http://www.pancake.org/zina
  16. * Author: Ryan Lathouwers <ryanlath@pacbell.net>
  17. * Support: http://sourceforge.net/projects/zina/
  18. * License: GNU GPL2 <http://www.gnu.org/copyleft/gpl.html>
  19. *
  20. * MP3/OGG/WMA/M4A file info and tags
  21. *
  22. * hacked pretty heavily from...
  23. * MP3::Info by Chris Nandor <http://sf.net/projects/mp3-info/>
  24. * class.id3.php by Sandy McArthur, Jr. <http://Leknor.com/code/>
  25. * getID3() [ogg stuff] by James Heinrich <http://www.silisoftware.com>
  26. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
  27. #TODO: propbably should be renamed
  28. #todo: make opts array instead of list of opts...
  29. class mp3 {
  30. function mp3($file, $info=false, $tag=false, $faster=true, $genre = false, $image = false) {
  31. $this->file = $file;
  32. $this->tag = 0;
  33. $this->info = 0;
  34. $this->faster = $faster;
  35. $this->get_image = $image;
  36. #TODO: get fh once???
  37. if (stristr(substr($file,-3),'mp3')) {
  38. if ($info) $this->getMP3Info();
  39. if ($tag) $this->getID3Tag($genre);
  40. } elseif (stristr(substr($file,-3),'ogg')) {
  41. if ($info || $tag) $this->getOgg($info, $tag);
  42. } elseif (strtolower(substr($file,-3)) == 'm4a' || strtolower(substr($file,-3)) == 'mp4') {
  43. $this->getM4A();
  44. } elseif (strtolower(substr($file,-3)) == 'wma') {
  45. $this->getWMA();
  46. } else {
  47. $this->getBadInfo();
  48. }
  49. }
  50. function getID3Tag($genre) {
  51. $this->tag = 0;
  52. $v2h = null;
  53. if (!($fh = @fopen($this->file, 'rb'))) { return 0; }
  54. $v2h = $this->getV2Header($fh);
  55. $this->track = 0;
  56. if (!empty($v2h) && !($v2h->major_ver < 2)) {
  57. $hlen = 10; $num = 4;
  58. if ($v2h->major_ver == 2) { $hlen = 6; $num = 3; }
  59. $off = 10; #TODO ext_header?
  60. $size = null;
  61. $map = array(
  62. '2'=>array('TT2'=>'title', 'TAL'=>'album', 'TP1'=>'artist', 'TYE'=>'year', 'TCO'=>'genre', 'TRK'=>'track', 'ULT'=>'lyrics'),
  63. '3'=>array('TIT2'=>'title', 'TALB'=>'album', 'TPE1'=>'artist', 'TYER'=>'year', 'TCON'=>'genre', 'TRCK'=>'track','USLT'=>'lyrics'),
  64. );
  65. if ($this->get_image) {
  66. $map[2]['PIC'] = 'image_raw';
  67. $map[3]['APIC'] = 'image_raw';
  68. }
  69. $fs = sizeof($map[2]);
  70. $this->title = $this->artist = null;
  71. while($off < $v2h->tag_size) {
  72. $arr = $id = null;
  73. $found = 0;
  74. fseek($fh, $off);
  75. $bytes = fread($fh, $hlen);
  76. if (preg_match("/^([A-Z0-9]{".$num."})/", $bytes, $arr)) {
  77. $id = $arr[0];
  78. $size = $hlen;
  79. $bytes = array_reverse(unpack("C$num",substr($bytes,$num,$num)));
  80. for ($i=0; $i<($num - 1); $i++) {
  81. $size += $bytes[$i] * pow(256,$i);
  82. }
  83. } else { break; }
  84. fseek($fh, $off + $hlen);
  85. if ($size > $hlen) {
  86. $bytes = fread($fh, $size - $hlen);
  87. if (isset($map[$v2h->major_ver][$id])) {
  88. if ($id == 'APIC' || $id == 'PIC') {
  89. $value = $bytes;
  90. } else {
  91. if (ord($bytes[0]) == 0) { # lang enc 0:ISO | 1,2,3:UTF variants
  92. $value = zcheck_utf8(trim($bytes), false);
  93. } else {
  94. $value = zcheck_utf8(str_replace("\0",'',substr($bytes, 3)), false);
  95. }
  96. }
  97. $this->$map[$v2h->major_ver][$id] = $value;
  98. $this->tag = 1;
  99. if (++$found == $fs) break;
  100. }
  101. }
  102. $off += $size;
  103. }
  104. if ($this->tag) $this->tag_version = "ID3v2.".$v2h->major_ver;
  105. if (isset($this->lyrics)) {
  106. #todo: lang encoding???
  107. $this->lyrics = substr($this->lyrics,4);
  108. if (empty($this->lyrics)) unset($this->lyrics);
  109. }
  110. if ($this->get_image && isset($this->image_raw)) {
  111. $frame_offset = 0;
  112. $frame_textencoding = ord(substr($this->image_raw, $frame_offset++, 1));
  113. if ($v2h->major_ver == 2) {
  114. $frame_mimetype = substr($this->image_raw, $frame_offset, 3);
  115. if (strtolower($frame_mimetype) == 'ima') { # ima hack (NOT tested)
  116. $frame_terminatorpos = @strpos($this->image_raw, "\x00", $frame_offset);
  117. $frame_mimetype = substr($this->image_raw, $frame_offset, $frame_terminatorpos - $frame_offset);
  118. $frame_offset = $frame_terminatorpos + strlen("\x00");
  119. } else {
  120. $frame_offset += 3;
  121. }
  122. } elseif ($v2h->major_ver > 2) {
  123. $frame_terminatorpos = @strpos($this->image_raw, "\x00", $frame_offset);
  124. $frame_mimetype = substr($this->image_raw, $frame_offset, $frame_terminatorpos - $frame_offset);
  125. if (ord($frame_mimetype) === 0) {
  126. $frame_mimetype = '';
  127. }
  128. $frame_offset = $frame_terminatorpos + strlen("\x00");
  129. }
  130. $terminators = array(0=>"\x00", 1=>"\x00\x00", 2=>"\x00\x00", 3=>"\x00", 255=>"\x00\x00");
  131. $terminator = @$terminators[$frame_textencoding];
  132. $frame_picturetype = ord(substr($this->image_raw, $frame_offset++, 1));
  133. $frame_terminatorpos = @strpos($this->image_raw, $terminator, $frame_offset);
  134. if (ord(substr($this->image_raw, $frame_terminatorpos + strlen($terminator), 1)) === 0) {
  135. $frame_terminatorpos++; // @strpos() fooled because 2nd byte of Unicode chars are often 0x00
  136. }
  137. $frame_description = substr($this->image_raw, $frame_offset, $frame_terminatorpos - $frame_offset);
  138. if (ord($frame_description) === 0) $frame_description = '';
  139. $image_data = substr($this->image_raw, $frame_terminatorpos + strlen($terminator));
  140. if (strlen($image_data) > 0 && @imagecreatefromstring($image_data)) {
  141. $bytes = strlen($image_data);
  142. $image_type = strtolower(str_replace('image/', '', strtolower($frame_mimetype)));
  143. if ($image_type == 'jpeg') $image_type = 'jpg';
  144. $this->image = array(
  145. 'type' => $image_type,
  146. 'data' => $image_data,
  147. 'bytes' => $bytes,
  148. 'kbytes' => round($bytes/1024,2),
  149. );
  150. }
  151. unset($this->image_raw);
  152. }
  153. }
  154. #if v2 not found look for v1
  155. if (!$this->tag) {
  156. if (fseek($fh, -128, SEEK_END) == -1) { return 0; }
  157. $tag = fread($fh, 128);
  158. if (substr($tag,0,3) == "TAG") {
  159. if ($tag[125] == Chr(0) and $tag[126] != Chr(0)) {
  160. $format = 'a3TAG/a30title/a30artist/a30album/a4year/a28comment/x1/C1track/C1genre';
  161. $this->tag_version = "ID3v1.1";
  162. } else {
  163. $format = 'a3TAG/a30title/a30artist/a30album/a4year/a30comment/C1genre';
  164. $this->tag_version = "ID3v1";
  165. }
  166. $id3tag = unpack($format, $tag);
  167. foreach ($id3tag as $key=>$value) {
  168. $this->$key = zcheck_utf8(trim($value), false);
  169. }
  170. unset($this->TAG);
  171. $this->tag = 1;
  172. }
  173. }
  174. fclose($fh);
  175. if ($genre) $this->getGenre();
  176. #$this->track = (int)$this->track;
  177. return $this->tag;
  178. }
  179. function getGenre() {
  180. $genres = array(
  181. 0=>'Blues',1=>'Classic Rock',2=>'Country',3=>'Dance',4=>'Disco',
  182. 5=>'Funk',6=>'Grunge',7=>'Hip-Hop',8=>'Jazz',9=>'Metal',10=>'New Age',
  183. 11=>'Oldies',12=>'Other',13=>'Pop',14=>'R&B',15=>'Rap',16=>'Reggae',
  184. 17=>'Rock',18=>'Techno',19=>'Industrial',20=>'Alternative',21=>'Ska',
  185. 22=>'Death Metal',23=>'Pranks',24=>'Soundtrack',25=>'Euro-Techno',
  186. 26=>'Ambient',27=>'Trip-Hop',28=>'Vocal',29=>'Jazz+Funk',30=>'Fusion',
  187. 31=>'Trance',32=>'Classical',33=>'Instrumental',34=>'Acid',35=>'House',
  188. 36=>'Game',37=>'Sound Clip',38=>'Gospel',39=>'Noise',40=>'Alternative Rock',
  189. 41=>'Bass',42=>'Soul',43=>'Punk',44=>'Space',45=>'Meditative',46=>'Instrumental Pop',
  190. 47=>'Instrumental Rock',48=>'Ethnic',49=>'Gothic',50=>'Darkwave',
  191. 51=>'Techno-Industrial',52=>'Electronic',53=>'Pop-Folk',54=>'Eurodance',
  192. 55=>'Dream',56=>'Southern Rock',57=>'Comedy',58=>'Cult',59=>'Gangsta',
  193. 60=>'Top 40',61=>'Christian Rap',62=>'Pop/Funk',63=>'Jungle',64=>'Native US',
  194. 65=>'Cabaret',66=>'New Wave',67=>'Psychadelic',68=>'Rave',69=>'Showtunes',
  195. 70=>'Trailer',71=>'Lo-Fi',72=>'Tribal',73=>'Acid Punk',74=>'Acid Jazz',
  196. 75=>'Polka',76=>'Retro',77=>'Musical',78=>'Rock & Roll',79=>'Hard Rock',
  197. 80=>'Folk',81=>'Folk-Rock',82=>'National Folk',83=>'Swing',84=>'Fast Fusion',
  198. 85=>'Bebob',86=>'Latin',87=>'Revival',88=>'Celtic',89=>'Bluegrass',90=>'Avantgarde',
  199. 91=>'Gothic Rock',92=>'Progressive Rock',93=>'Psychedelic Rock',94=>'Symphonic Rock',
  200. 95=>'Slow Rock',96=>'Big Band',97=>'Chorus',98=>'Easy Listening',99=>'Acoustic',
  201. 100=>'Humour',101=>'Speech',102=>'Chanson',103=>'Opera',104=>'Chamber Music',
  202. 105=>'Sonata',106=>'Symphony',107=>'Booty Bass',108=>'Primus',109=>'Porn Groove',
  203. 110=>'Satire',111=>'Slow Jam',112=>'Club',113=>'Tango',114=>'Samba',115=>'Folklore',
  204. 116=>'Ballad',117=>'Power Ballad',118=>'Rhytmic Soul',119=>'Freestyle',120=>'Duet',
  205. 121=>'Punk Rock',122=>'Drum Solo',123=>'Acapella',124=>'Euro-House',125=>'Dance Hall',
  206. 126=>'Goa',127=>'Drum & Bass',128=>'Club-House',129=>'Hardcore',130=>'Terror',
  207. 131=>'Indie',132=>'BritPop',133=>'Negerpunk',134=>'Polsk Punk',135=>'Beat',
  208. 136=>'Christian Gangsta Rap',137=>'Heavy Metal',138=>'Black Metal',139=>'Crossover',
  209. 140=>'Contemporary Christian',141=>'Christian Rock',142=>'Merengue',143=>'Salsa',
  210. 144=>'Trash Metal',145=>'Anime',146=>'Jpop',147=>'Synthpop',255=>'Unknown'
  211. );
  212. if ($this->tag && !empty($this->genre)) {
  213. $this->genre = (preg_match("/\((.*?)\)/",$this->genre, $match)) ? $match[1] : ucfirst(trim($this->genre));
  214. if (is_numeric($this->genre)) {
  215. $this->genre = (isset($genres[$this->genre])) ? $genres[$this->genre] : 'Unknown';
  216. }
  217. } else {
  218. $this->genre = 'Unknown';
  219. }
  220. }
  221. function getMP3Info() {
  222. $file = $this->file;
  223. if (!($f = @fopen($file, 'rb'))) {
  224. error_log(zt('Zina: mp3.class: getMP3Info(): Cannot open file: @file', array('@file'=>$file)));
  225. return false;
  226. }
  227. $this->filesize = filesize($file);
  228. $frameoffset = 0;
  229. $total = 4096;
  230. if ($frameoffset == 0) {
  231. if ($v2h = $this->getV2Header($f)) {
  232. $total += $frameoffset += $v2h->tag_size;
  233. fseek($f, $frameoffset);
  234. } else {
  235. fseek($f, 0);
  236. }
  237. }
  238. if ($this->faster) {
  239. do {
  240. while (fread($f,1) != Chr(255)) { // Find the first frame
  241. if (feof($f)) { return false; }
  242. }
  243. fseek($f, ftell($f) - 1); // back up one byte
  244. $frameoffset = ftell($f);
  245. $r = fread($f, 4);
  246. $bits = decbin($this->unpackHeader($r));
  247. #64bit machines...
  248. //if (PHP_INT_SIZE == 8) $bits = substr($bits,32);
  249. if ($frameoffset > $total) { return $this->getBadInfo(); }
  250. } while (!$this->isValidMP3Header($bits));
  251. } else { #more accurate with some VBRs
  252. $r = fread($f, 4);
  253. $bits = decbin($this->unpackHeader($r));
  254. //if (PHP_INT_SIZE == 8) $bits = substr($bits,32);
  255. while (!$this->isValidMP3Header($bits)) {
  256. if ($frameoffset > $total) { return $this->getBadInfo(); }
  257. fseek($f, ++$frameoffset);
  258. $r = fread($f, 4);
  259. $bits = decbin($this->unpackHeader($r));
  260. //if (PHP_INT_SIZE == 8) $bits = substr($bits,32);
  261. }
  262. }
  263. #$this->bits = $bits;
  264. $this->header_found = $frameoffset;
  265. $this->vbr = 0;
  266. $vbr = $this->getVBR($f, $bits[12], $bits[24] + $bits[25], $frameoffset);
  267. fclose($f);
  268. #TODO: vbr file size
  269. if ($bits[11] == 0) {
  270. $mpeg_ver = "2.5";
  271. $bitrates = array(
  272. '1'=>array(0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0),
  273. '2'=>array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0),
  274. '3'=>array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0),
  275. );
  276. } else if ($bits[12] == 0) {
  277. $mpeg_ver = "2";
  278. $bitrates = array(
  279. '1'=>array(0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0),
  280. '2'=>array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0),
  281. '3'=>array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0),
  282. );
  283. } else {
  284. $mpeg_ver = "1";
  285. $bitrates = array(
  286. '1'=>array(0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0),
  287. '2'=>array(0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0),
  288. '3'=>array(0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0),
  289. );
  290. }
  291. $layers = array(array(0,3), array(2,1),);
  292. $layer = $layers[$bits[13]][$bits[14]];
  293. if ($layer == 0) return $this->getBadInfo();
  294. $bitrate = 0;
  295. if ($bits[16] == 1) $bitrate += 8;
  296. if ($bits[17] == 1) $bitrate += 4;
  297. if ($bits[18] == 1) $bitrate += 2;
  298. if ($bits[19] == 1) $bitrate += 1;
  299. if ($bitrate == 0) return $this->getBadInfo();
  300. $this->bitrate = $bitrates[$layer][$bitrate];
  301. $frequency = array(
  302. '1'=>array(
  303. '0'=>array(44100, 48000),
  304. '1'=>array(32000, 0),
  305. ),
  306. '2'=>array(
  307. '0'=>array(22050, 24000),
  308. '1'=>array(16000, 0),
  309. ),
  310. '2.5'=>array(
  311. '0'=>array(11025, 12000),
  312. '1'=>array(8000, 0),
  313. ),
  314. );
  315. $this->frequency = $frequency[$mpeg_ver][$bits[20]][$bits[21]];
  316. $mfs = $this->frequency / ($bits[12] ? 144000 : 72000);
  317. if ($mfs == 0) return $this->getBadInfo();
  318. $frames = (int)($vbr && $vbr['frames'] ? $vbr['frames'] : $this->filesize / $this->bitrate / $mfs);
  319. if ($vbr) {
  320. $this->vbr = 1;
  321. if ($vbr['scale']) $this->vbr_scale = $vbr['scale'];
  322. $this->bitrate = (int)($this->filesize / $frames * $mfs);
  323. if (!$this->bitrate) return $this->getBadInfo();
  324. }
  325. $s = -1;
  326. if ($this->bitrate != 0) {
  327. $s = ((8*($this->filesize))/1000) / $this->bitrate;
  328. }
  329. $this->stereo = ($bits[24] == 0);
  330. $this->length = (int)$s;
  331. $this->time = sprintf('%.2d:%02d',floor($s/60),floor($s-(floor($s/60)*60)));
  332. $this->info = 1;
  333. }
  334. function getV2Header($fh) {
  335. fseek($fh, 0);
  336. $bytes = fread($fh, 3);
  337. if ($bytes != 'ID3') return false;
  338. #$bytes = fread($fh, 3);
  339. #get version
  340. $bytes = fread($fh, 2);
  341. $ver = unpack("C2",$bytes);
  342. $h = (object) array('major_ver' => $ver[1]);
  343. $h->minor_ver = $ver[2];
  344. #get flags
  345. $bytes = fread($fh, 1);
  346. #get ID3v2 tag length from bytes 7-10
  347. $tag_size = 10;
  348. $bytes = fread($fh, 4);
  349. $temp = array_reverse(unpack("C4", $bytes));
  350. for($i=0; $i<=3; $i++) {
  351. $tag_size += $temp[$i] * pow(128,$i);
  352. }
  353. $h->tag_size = $tag_size;
  354. return $h;
  355. }
  356. function getVBR($fh, $id, $mode, &$offset) {
  357. $offset += 4;
  358. if ($id) {
  359. $offset += $mode == 2 ? 17 : 32;
  360. } else {
  361. $offset += $mode == 2 ? 9 : 17;
  362. }
  363. $bytes = $this->Seek($fh, $offset);
  364. if ($bytes != "Xing") return 0;
  365. $bytes = $this->Seek($fh, $offset);
  366. $vbr['flags'] = $this->unpackHeader($bytes);
  367. if ($vbr['flags'] & 1) {
  368. $bytes = $this->Seek($fh, $offset);
  369. $vbr['frames'] = $this->unpackHeader($bytes);
  370. }
  371. if ($vbr['flags'] & 2) {
  372. $bytes = $this->Seek($fh, $offset);
  373. $vbr['bytes'] = $this->unpackHeader($bytes);
  374. }
  375. if ($vbr['flags'] & 4) {
  376. $bytes = $this->Seek($fh, $offset, 100);
  377. }
  378. if ($vbr['flags'] & 8) {
  379. $bytes = $this->Seek($fh, $offset);
  380. $vbr['scale'] = $this->unpackHeader($bytes);
  381. } else {
  382. $vbr['scale'] = -1;
  383. }
  384. return $vbr;
  385. }
  386. function isValidMP3Header($bits) {
  387. if (strlen($bits) != 32) return false;
  388. if (substr_count(substr($bits,0,11),'0') != 0) return false;
  389. if ($bits[16] + $bits[17] + $bits[18] + $bits[19] == 4) return false;
  390. return true;
  391. }
  392. function getOgg($info, $tag) {
  393. $fh = fopen($this->file, 'rb');
  394. // Page 1 - Stream Header
  395. $h = null;
  396. if (!$this->getOggHeader($fh, $h)) { return $this->getBadInfo(); }
  397. if ($info) {
  398. $this->filesize = filesize($this->file);
  399. $data = fread($fh, 23);
  400. $offset = 0;
  401. $this->frequency = implode('',unpack('V1', substr($data, 5, 4)));
  402. $bitrate_average = 0;
  403. if (substr($data, 9, 4) !== chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF)) {
  404. $bitrate_max = implode('',unpack('V1', substr($data, 9, 4)));
  405. }
  406. if (substr($data, 13, 4) !== chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF)) {
  407. $bitrate_nominal = implode('',unpack('V1', substr($data, 13, 4)));
  408. }
  409. if (substr($data, 17, 4) !== chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF)) {
  410. $bitrate_min = implode('',unpack('V1', substr($data, 17, 4)));
  411. }
  412. }
  413. if ($tag) {
  414. // Page 2 - Comment Header
  415. if (!$this->getOggHeader($fh, $h)) { return $this->getBadInfo(); }
  416. $data = fread($fh, 16384);
  417. $offset = 0;
  418. $vendorsize = implode('',unpack('V1', substr($data, $offset, 4)));
  419. $offset += (4 + $vendorsize);
  420. $totalcomments = implode('',unpack('V1', substr($data, $offset, 4)));
  421. $offset += 4;
  422. for ($i = 0; $i < $totalcomments; $i++) {
  423. $commentsize = implode('',unpack('V1', substr($data, $offset, 4)));
  424. $offset += 4;
  425. $commentstring = substr($data, $offset, $commentsize);
  426. $offset += $commentsize;
  427. $comment = explode('=', $commentstring, 2);
  428. $comment[0] = strtolower($comment[0]);
  429. $this->$comment[0] = $comment[1];
  430. }
  431. $this->tag_version = "ogg";
  432. $this->tag = 1;
  433. }
  434. if ($info) {
  435. // Last Page - Number of Samples
  436. fseek($fh, max($this->filesize - 16384, 0), SEEK_SET);
  437. $LastChunkOfOgg = strrev(fread($fh, 16384));
  438. if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
  439. fseek($fh, 0 - ($LastOggSpostion + strlen('SggO')), SEEK_END);
  440. if (!$this->getOggHeader($fh, $h)) { return $this->getBadInfo(); }
  441. $samples = $h->pcm;
  442. $bitrate_average = ($this->filesize * 8) / ($samples / $this->frequency);
  443. }
  444. if ($bitrate_average > 0) {
  445. $this->bitrate = $bitrate_average;
  446. } else if (isset($bitrate_nominal) && ($bitrate_nominal > 0)) {
  447. $this->bitrate = $bitrate_nominal;
  448. } else if (isset($bitrate_min) && isset($bitrate_max)) {
  449. $this->bitrate = ($bitrate_min + $bitrate_max) / 2;
  450. }
  451. $this->bitrate = (int) ($this->bitrate / 1000);
  452. $s = -1;
  453. if (isset($this->bitrate)) {
  454. $s = (float) (($this->filesize * 8) / $this->bitrate / 1000);
  455. }
  456. $this->length = (int)$s;
  457. $this->time = sprintf('%.2d:%02d',floor($s/60),floor($s-(floor($s/60)*60)));
  458. $this->info = 1;
  459. }
  460. return true;
  461. }
  462. function getOggHeader(&$fh, &$h) {
  463. $baseoffset = ftell($fh);
  464. $data = fread($fh, 16384);
  465. $offset = 0;
  466. while ((substr($data, $offset++, 4) != 'OggS')) {
  467. if ($offset >= 10000) { return FALSE; }
  468. }
  469. $offset += 5;
  470. $h->pcm = implode('',unpack('V1', substr($data, $offset)));
  471. $offset += 20;
  472. $segments = implode('',unpack('C1', substr($data, $offset)));
  473. $offset += ($segments + 8);
  474. fseek($fh, $offset + $baseoffset, SEEK_SET);
  475. return true;
  476. }
  477. function parseAtom($fh, $offset, $end) {
  478. while ($offset < $end && $offset < $this->filesize) {
  479. fseek($fh, $offset);
  480. if (ftell($fh) + 8 > $end) return $atoms;
  481. $bytes = fread($fh, 4);
  482. $atom_size = $this->toUInt($bytes);
  483. $atom_type = fread($fh, 4);
  484. if ($atom_size == 1) {
  485. if (ftell($fh) + 8 > $end) return $atoms;
  486. $bytes = fread($fh, 8);
  487. $atom_size = implode('',unpack("N*", $bytes));
  488. }
  489. if ($atom_size < 0 || $offset + $atom_size > $end) {
  490. return $atoms;
  491. }
  492. if (in_array($atom_type, array('moov', 'udta','meta'))) {
  493. if ($atom_type == "meta") fseek($fh, 4, SEEK_CUR);
  494. $atoms[] = array(
  495. 'type'=>$atom_type,
  496. 'size'=>$atom_size,
  497. 'children' => $this->parseAtom($fh, ftell($fh), $offset+$atom_size)
  498. );
  499. } else {
  500. if (in_array($atom_type, array('mvhd', 'ilst'))) {
  501. $atoms[] = array(
  502. 'type'=>$atom_type,
  503. 'size'=>$atom_size,
  504. 'data'=> $this->atomSpecific($atom_type, $atom_size, $fh)
  505. );
  506. }
  507. }
  508. if ($atom_size == 0) {
  509. $offset = filesize($this->file);
  510. } else {
  511. $offset += $atom_size;
  512. }
  513. if ($atom_type == 'udta') $offset += 4;
  514. }
  515. return $atoms;
  516. }
  517. function getTime($s) {
  518. $secs = (int).5+$s;
  519. $mm = (int) $s/60;
  520. $ss = (int) $s - ($mm*60);
  521. $ms = (int).5 +(1000*($s - (int) $s));
  522. $this->length = (int) $s;
  523. $this->time = sprintf('%.2d:%02d',floor($s/60),floor($s-(floor($s/60)*60)));
  524. $this->bitrate = ceil(0.5 + $this->filesize / (($mm*60+$ss+$ms/1000)*128));
  525. }
  526. function atomSpecific($atom_type, $atom_size, $fh) {
  527. switch ($atom_type) {
  528. case 'mvhd' :
  529. fseek($fh, 12, SEEK_CUR);
  530. $bytes = fread($fh, 4);
  531. $this->frequency = implode('',unpack("N*", $bytes));
  532. $bytes = fread($fh, 4);
  533. $duration = implode('',unpack("N*", $bytes));
  534. $s = $duration/$this->frequency;
  535. $this->getTime($s);
  536. /*
  537. $secs = (int).5+$s;
  538. $mm = (int) $s/60;
  539. $ss = (int) $s - ($mm*60);
  540. $ms = (int).5 +(1000*($s - (int) $s));
  541. $this->length = (int) $s;
  542. $this->time = sprintf('%.2d:%02d',floor($s/60),floor($s-(floor($s/60)*60)));
  543. $this->bitrate = ceil(0.5 + $this->filesize / (($mm*60+$ss+$ms/1000)*128));
  544. */
  545. $this->info = true;
  546. break;
  547. case 'ilst' :
  548. $this->tag = true;
  549. $end = ftell($fh) + $atom_size;
  550. while(ftell($fh) + 8 < $end) {
  551. $tag_size = $this->toUInt(fread($fh, 4));
  552. $tag = fread($fh, 4);
  553. $next = ftell($fh) + $tag_size - 8;
  554. $data_size = $this->toUInt(fread($fh, 4));
  555. $data_type = fread($fh, 4);
  556. $c = chr(0xa9);
  557. if ($data_type == 'data' && in_array($tag, array($c.'nam', 'trkn', $c.'ART', $c.'alb', 'gnre', $c.'day'))) {
  558. $type = fread($fh, 8);
  559. $type = implode('',unpack("N", $type));
  560. $bytes = fread($fh, $data_size - 16);
  561. if ($type == 0) {
  562. $value = implode('',unpack("n*", $bytes));
  563. } else {
  564. $value = $bytes;
  565. }
  566. switch ($tag) {
  567. case $c.'nam' :
  568. $this->title = $value;
  569. break;
  570. case $c.'ART' :
  571. $this->artist = $value;
  572. break;
  573. case $c.'alb' :
  574. $this->album = $value;
  575. break;
  576. case $c.'day' :
  577. $this->year = $value;
  578. break;
  579. case 'gnre' :
  580. $this->genre = $value;
  581. $this->getGenre();
  582. break;
  583. case 'trkn' :
  584. if (strlen($value) % 2 == 0) {
  585. $value = substr($value,0, strlen($value)/2);
  586. }
  587. $this->track = $value;
  588. break;
  589. }
  590. } else {
  591. fseek($fh, $next, SEEK_SET);
  592. }
  593. }
  594. break;
  595. case 'mdat' :
  596. return 'mdat is data';
  597. break;
  598. default:
  599. #return fread($fh, $atom_size - 8);
  600. break;
  601. }
  602. }
  603. function parseWMA($fh) {
  604. fseek($fh, 0);
  605. $guid = $this->bin2guid(fread($fh,16));
  606. $size = implode('',unpack("VV", fread($fh, 8)));
  607. $number = implode('',unpack("V", fread($fh, 4)));
  608. fseek($fh, 2, SEEK_CUR);
  609. if ($guid != '75B22630-668E-11CF-A6D9-00AA0062CE6C') return $this->getBadInfo();
  610. for($i=0; $i < $number; $i++) {
  611. $guid = $this->bin2guid(fread($fh,16));
  612. $size = implode('',unpack("VV", fread($fh, 8)));
  613. $offset = ftell($fh)-24+$size;
  614. if ($guid == '8CABDCA1-A947-11CF-8EE4-00C00C205365') { //ASF_File_Properties_Objecy
  615. fseek($fh, 16+8+8+8, SEEK_CUR);
  616. $duration = implode('',unpack("VV", fread($fh, 8)));
  617. $duration = ($duration < 0) ? $duration += 4294967296 : $duration;
  618. fseek($fh, 8, SEEK_CUR);
  619. $pre_roll = implode('',unpack("VV", fread($fh, 8)));
  620. $s = $duration/10000000 - $pre_roll/1000;
  621. $this->getTime($s);
  622. fseek($fh, 4+4+4,SEEK_CUR);
  623. $max_bitrate = implode('',unpack("V", fread($fh, 4)));
  624. $this->bitrate = (int) ($max_bitrate/1000);
  625. } elseif ($guid == 'D2D0A440-E307-11D2-97F0-00A0C95EA850') { //ASF_Extended_Content_Description
  626. $count = implode('',unpack("v", fread($fh, 2)));
  627. $this->tag = true;
  628. for ($j = 0; $j < $count; $j++) {
  629. $name_length = implode('',unpack("v", fread($fh, 2)));
  630. $name = $this->denull(implode('',unpack("a*", fread($fh, $name_length))));
  631. $data_type = implode('',unpack("v", fread($fh, 2)));
  632. $data_length = implode('',unpack("v", fread($fh, 2)));
  633. $data = fread($fh, $data_length);
  634. if ($name == 'WM/Genre') {
  635. $this->genre = $this->getValueByType($data_type, $data);
  636. #TODO $this->getGenre();
  637. } elseif ($name == 'WM/AlbumTitle') {
  638. $this->album = $this->getValueByType($data_type, $data);
  639. } elseif ($name == 'WM/AlbumArtist') {
  640. $this->artist = $this->getValueByType($data_type, $data);
  641. } elseif ($name == 'WM/Year') {
  642. $this->year = $this->getValueByType($data_type, $data);
  643. } elseif ($name == 'WM/TrackNumber') {
  644. $this->track = $this->getValueByType($data_type, $data);
  645. }
  646. }
  647. } elseif ($guid == '75B22633-668E-11CF-A6D9-00AA0062CE6C' && !isset($this->title)) { //ASF_Content_Description
  648. $title_length = implode('',unpack("v", fread($fh, 2)));
  649. $artist_length = implode('',unpack("v", fread($fh, 2)));
  650. fseek($fh, 6, SEEK_CUR);
  651. if (!empty($title_length)) {
  652. $this->title = $this->denull(implode('',unpack("a*", fread($fh, $title_length))));
  653. $this->tag = true;
  654. }
  655. if (!empty($artist_length)) {
  656. $this->artist = $this->denull(implode('',unpack("a*", fread($fh, $artist_length))));
  657. }
  658. } elseif ($guid == 'B7DC0791-A9B7-11CF-8EE6-00C00C205365') { // ASF_Stream_Properties
  659. $stream_type = $this->bin2guid(fread($fh,16));
  660. fseek($fh, 16+8, SEEK_CUR);
  661. $stream_length = implode('',unpack("V", fread($fh, 4)));
  662. $error_length = implode('',unpack("V", fread($fh, 4)));
  663. fseek($fh, 2+4, SEEK_CUR);
  664. if ($stream_type == 'F8699E40-5B4D-11CF-A8FD-00805F5C442B') { // ASF_Audio_Media
  665. fseek($fh, 2, SEEK_CUR);
  666. $channels = implode('',unpack("v", fread($fh, 2)));
  667. $this->stereo = ($channels > 1);
  668. $this->frequency = implode('',unpack("V", fread($fh, 4)));
  669. }
  670. }
  671. fseek($fh, $offset);
  672. }
  673. }
  674. function denull($str) {
  675. return str_replace("\0",'', $str);
  676. }
  677. # wma types
  678. function getValueByType($data_type, $data) {
  679. if ($data_type == 0) {
  680. return $this->denull(implode('', unpack('a*', $data)));
  681. } elseif ($data_type == 1) {
  682. return $data;
  683. } elseif ($data_type == 2 || $data_type == 5) {
  684. return implode('', unpack('v', $data));
  685. } elseif ($data_type == 3) {
  686. return implode('', unpack('V', $data));
  687. } elseif ($data_type == 4) {
  688. return implode('', unpack('VV', $data));
  689. } elseif ($data_type == 6) {
  690. return $this->bin2guid($data);
  691. }
  692. }
  693. function bin2guid($hs) {
  694. if (strlen($hs) == 16) {
  695. $hexstring = bin2hex($hs);
  696. return strtoupper(substr( $hexstring, 6,2).substr( $hexstring, 4,2).
  697. substr( $hexstring, 2,2).substr( $hexstring, 0,2).'-'.substr( $hexstring, 10,2).substr( $hexstring, 8,2).'-'.
  698. substr( $hexstring, 14,2).substr( $hexstring, 12,2).'-'.substr( $hexstring, 16,4).'-'.substr( $hexstring, 20,12));
  699. } else {
  700. return false;
  701. }
  702. }
  703. function getM4A() {
  704. $fh = fopen($this->file, 'rb');
  705. $this->filesize = filesize($this->file);
  706. $this->parseAtom($fh, 0, $this->filesize);
  707. $this->stereo = true;
  708. if (!$this->info) $this->getBadInfo();
  709. fclose($fh);
  710. }
  711. function getWMA() {
  712. $fh = fopen($this->file, 'rb');
  713. $this->filesize = filesize($this->file);
  714. $this->parseWMA($fh);
  715. fclose($fh);
  716. }
  717. function getBadInfo() {
  718. $this->time = $this->bitrate = $this->frequency = 0;
  719. $this->filesize = filesize($this->file);
  720. return false;
  721. }
  722. function toUInt($bytes) {
  723. return implode('',unpack("N*", $bytes));
  724. }
  725. function Seek($fh, &$offset, $n = 4) {
  726. fseek($fh, $offset);
  727. $bytes = fread($fh, $n);
  728. $offset += $n;
  729. return $bytes;
  730. }
  731. function unpackHeader($byte) {
  732. return implode('', unpack('N', $byte));
  733. }
  734. }
  735. function cddb_disc_id($track_offsets, $disc_length) {
  736. $n = 0;
  737. $count = count($track_offsets);
  738. for ($i = 0; $i < $count; $i++) {
  739. $n = $n + cddb_sum($track_offsets[$i] / 75);
  740. }
  741. // The $disc_length - 2 accounts for the 150 frame offset the RedBook standard uses... I think...
  742. return dechex(($n % 0xff) << 24 | ($disc_length - 2) << 8 | $count);
  743. }
  744. function cddb_sum($n) {
  745. $ret = 0;
  746. while ($n > 0) {
  747. $ret = $ret + ($n % 10);
  748. $n = (int) ($n / 10);
  749. }
  750. return $ret;
  751. }
  752. ?>