commit b2cad7745b3c72cbe46d333ea057ff60a7d35184 Author: wapmorgan Date: Tue Jan 10 02:42:58 2017 +0300 Initiation diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6600f1c --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ +GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b96a19f --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# Mp3Info +The fastest PHP library to get mp3 tags&meta. + +[![Composer package](http://xn--e1adiijbgl.xn--p1acf/badge/wapmorgan/mp3info)](https://packagist.org/packages/wapmorgan/mp3info) + +This class extracts information from mpeg/mp3 audio: + +| Audio | id3v1 Tags | id3v2 Tags | +|--------------|------------|------------| +| duration | song | | +| bitRate | artist | | +| sampleRate | album | | +| channel | year | | +| framesCount | comment | | +| codecVersion | genre | | +| layerVersion | | | + +1. Usage +2. Performance +3. Console scanner +4. API + - Audio information + - Object members + - Static methods +4. Technical information + +# Usage +After creating an instance of `Mp3Info` with passing filename as the first argument to the constructor, you can retrieve data from object properties (listed below). + +If you need parse tags, you should set 2nd argument this way: +```php +use wapmorgan\Mp3Info\Mp3Info; +$audio = new Mp3Info($fileName, true); +// or omit 2nd argument to increase parsing speed +$audio = new Mp3Info($fileName); +``` +And after that access object properties to get audio information: +``` +echo 'Audio duration: '.floor($audio->duration / 60).' min '.floor($audio->duration % 60).' sec'.PHP_EOL; +echo 'Audio bitrate: '.($audio->bitRate / 1000).' kb/s'.PHP_EOL; +// and so on ... +``` +To access id3v1 tags use `$tags1` property: +``` +echo 'Song '.$audio->tags1['song'].' from '.$audio->tags1['artist'].PHP_EOL; +``` + +# Performance + +* It parses a bunch of mp3 files in less than a half of second (without tags). +* It parses a bunch of mp3 files with their tags in two seconds or less (with both id3v1 and id3v2). + +A bunch - **878 megabytes** of mp3 files (**33 tracks** with a total length **8:37:42**). + +# Console scanner +To test Mp3Info you can use built-in script that scans dirs and analyzes all mp3-files inside them. To launch script against current folder: +``` +php bin/scan ./ +``` + +# API +### Audio information + +| Property | Description | Values | +|-----------------|--------------------------------------------------------------------|-------------------------------------------------------------| +| `$codecVersion` | MPEG codec version | 1 or 2 | +| `$layerVersion` | Audio layer version | 1 or 2 or 3 | +| `$audioSize` | Audio size in bytes. Note that this value is NOT equals file size. | *int* | +| `$duration` | Audio duration in seconds.microseconds | like 3603.0171428571 (means 1 hour and 3 sec) | +| `$bitRate` | Audio bit rate in bps | like 128000 (means 128kb/s) | +| `$sampleRate` | Audio sample rate in Hz | like 44100 (means 44.1KHz) | +| `$isVbr` | Contains true if audio has variable bit rate | *boolean* | +| `$channel` | Channel mode | `'stereo'` or `'dual_mono'` or `'joint_stereo'` or `'mono'` | + +### Object members +- `float $_parsingTime` + + Contains time spent to read&extract audio information in *sec.msec*. + +- `array $tags1` + + Audio tags ver. 1 (aka id3v1). + +- `array $tags2` + + Audio tags ver. 2 (aka id3v2). + +- `public function __construct($filename, $parseTags = false)` + + Creates new instance of object and initiate parsing. If second argument is *true*, audio tags will be parsed. + +### Static methods + +- `static public function isValidAudio($filename)` + + Checks if file `$filename` looks like an mp3-file. Returns **true** if file similar to mp3, otherwise false. + +## Technical information +Supporting features: +* id3v1 +* id3v2.3.0 +* Variable Bit Rate (VBR) + +Used sources: +* [mpeg header description](http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm) +* [id3v2 tag specifications](http://id3.org/Developer%20Information). Сoncretely: [id3v2.3.0](http://id3.org/id3v2.3.0), [id3v2.2.0](http://id3.org/id3v2-00), [id3v2.4.0](http://id3.org/id3v2.4.0-changes) +* [Xing, Info and Lame tags specifications](http://gabriel.mp3-tech.org/mp3infotag.html) diff --git a/bin/scan b/bin/scan new file mode 100755 index 0000000..1266f7a --- /dev/null +++ b/bin/scan @@ -0,0 +1,44 @@ +#!/usr/bin/php + $maxLength) { + return substr($string, 0, $maxLength-3).'...'; + } + return $string; +} + +function analyze($filename, &$total_parse_time) { + if (!is_readable($filename)) return; + try { + $audio = new Mp3Info($filename, true); + } catch (Exception $e) { + return null; + } + echo sprintf('%15s | %4s | %7s | %0.1fkHz | %-11s | %-10s | %.5f', substrIfLonger(basename($filename), 15), formatTime($audio->duration), $audio->isVbr ? 'vbr' : ($audio->bitRate / 1000).'kbps', ($audio->sampleRate / 1000), isset($audio->tags1['song']) ? substrIfLonger($audio->tags1['song'], 11) : null, isset($audio->tags1['artist']) ? substrIfLonger($audio->tags1['artist'], 10) : null, $audio->_parsingTime).PHP_EOL; + $total_parse_time += $audio->_parsingTime; +} +array_shift($argv); +echo sprintf('%15s | %4s | %7s | %7s | %11s | %10s | %4s', 'File name', 'dur.', 'bitrate', 'sample', 'song', 'artist', + 'time').PHP_EOL; +$total_parse_time = 0; +foreach ($argv as $arg) { + if (is_dir($arg)) { + foreach (glob(rtrim($arg, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'*.mp3') as $f) { + if (is_file($f)) + analyze($f, $total_parse_time); + } + } else if (is_file($arg)) + analyze($arg, $total_parse_time); +} +echo sprintf('%79s', 'Total parsing time: '.round($total_parse_time, 5)).PHP_EOL; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2d3f48c --- /dev/null +++ b/composer.json @@ -0,0 +1,12 @@ +{ + "name": "wapmorgan/mp3info", + "type": "library", + "license": "GPL-3.0", + "keywords": ["mp3", "audio", "id3", "id3v1", "id3v2", "mpeg"], + "description": "The fastest php library to extract mp3 tags & meta information.", + "autoload": { + "psr-4": { + "wapmorgan\\Mp3Info\\": "src/" + } + } +} diff --git a/data/bitRateTable.php b/data/bitRateTable.php new file mode 100644 index 0000000..9673c62 --- /dev/null +++ b/data/bitRateTable.php @@ -0,0 +1,13 @@ + array( + 1 => array(null, 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000, 416000, 448000, false), // MPEG 1 layer 1 + 2 => array(null, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000, 384000, false), // MPEG 1 layer 2 + 3 => array(null, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000, false), // MPEG 1 layer 3 + ), + 2 => array( + 1 => array(null, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000, 224000, 256000, false), // MPEG 2 layer 1 + 2 => array(null, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, false), // MPEG 2 layer 2 + 3 => array(null, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, false), // MPEG 2 layer 3 + ), +); diff --git a/data/sampleRateTable.php b/data/sampleRateTable.php new file mode 100644 index 0000000..30d67e2 --- /dev/null +++ b/data/sampleRateTable.php @@ -0,0 +1,5 @@ + array(44100, 48000, 32000, false), // MPEG 1 + 2 => array(22050, 24000, 16000, false), // MPEG 2 +); diff --git a/src/Mp3Info.php b/src/Mp3Info.php new file mode 100644 index 0000000..a8eb8f1 --- /dev/null +++ b/src/Mp3Info.php @@ -0,0 +1,584 @@ +audioSize = $this->parseAudio($this->_fileName = $filename, $this->_fileSize = filesize($filename), $mode); + } + + /** + * Reads audio file in binary mode. + * mpeg audio file structure: + * ID3V2 TAG - provides a lot of meta data. [optional] + * MPEG AUDIO FRAMES - contains audio data. A frame consists of a frame header and a frame data. The first frame may contain extra information about mp3 (marked with "Xing" or "Info" string). Rest of frames can contain only audio data. + * ID3V1 TAG - provides a few of meta data. [optional] + */ + private function parseAudio($filename, $filesize, $mode) { + $time = microtime(true); + $fp = fopen($filename, "rb"); + + /** Size of audio data (exclude tags size) + * @var int */ + $audioSize = $filesize; + + // parse tags + if (fread($fp, 3) == self::TAG2_SYNC) { + if ($mode & self::TAGS) $audioSize -= ($id3v2Size = $this->readId3v2Body($fp)); + else { + fseek($fp, 2, SEEK_CUR); // 2 bytes of tag version + fseek($fp, 1, SEEK_CUR); // 1 byte of tag flags + $sizeBytes = $this->readBytes($fp, 4); + array_walk($sizeBytes, function (&$value) { + $value = substr(str_pad(base_convert($value, 10, 2), 8, 0, STR_PAD_LEFT), 1); + }); + $size = bindec(implode(null, $sizeBytes)) + 10; + $audioSize -= ($id3v2Size = $size); + } + } + fseek($fp, $filesize - 128); + if (fread($fp, 3) == self::TAG1_SYNC) { + if ($mode & self::TAGS) $audioSize -= $this->readId3v1Body($fp); + else $audioSize -= 128; + } + + fseek($fp, 0); + // audio meta + if ($mode & self::META) { + if (isset($id3v2Size)) fseek($fp, $id3v2Size); + $framesCount = $this->readFirstFrame($fp); + if (!is_null($framesCount)) $this->framesCount = $framesCount; + else $this->framesCount = ceil($audioSize / $this->__cbrFrameSize); + + // recalculate average bit rate in vbr case + if ($this->isVbr && !is_null($framesCount)) { + $avgFrameSize = $audioSize / $framesCount; + $this->bitRate = $avgFrameSize * $this->sampleRate / (1000 * $this->layerVersion == 3 ? 12 : 144); + } + + $this->duration = ($this->framesCount - 1) * ($this->layerVersion == 1 ? 384 : 1152) / $this->sampleRate; + } + fclose($fp); + + $this->_parsingTime = microtime(true) - $time; + return $audioSize; + } + + /** + * Read first frame information. + * @return int Number of frames (if present if first frame) + */ + private function readFirstFrame($fp) { + $pos = ftell($fp); + $headerBytes = $this->readBytes($fp, 4); + if (($headerBytes[0] & 0xFF) != 0xFF || (($headerBytes[1] >> 5) & 0b111) != 0b111) throw new \Exception("At ".$pos."(".dechex($pos).") should be the first frame header!"); + + switch ($headerBytes[1] >> 3 & 0b11) { + case 0b10: $this->codecVersion = self::MPEG_2; break; + case 0b11: $this->codecVersion = self::MPEG_1; break; + } + + switch ($headerBytes[1] >> 1 & 0b11) { + case 0b01: $this->layerVersion = self::LAYER_3; break; + case 0b10: $this->layerVersion = self::LAYER_2; break; + case 0b11: $this->layerVersion = self::LAYER_1; break; + } + $this->bitRate = self::$_bitRateTable[$this->codecVersion][$this->layerVersion][$headerBytes[2] >> 4]; + $this->sampleRate = self::$_sampleRateTable[$this->codecVersion][bindec($headerBytes[2] >> 2 & 0b11)]; + + switch ($headerBytes[3] >> 6) { + case 0b00: $this->channel = self::STEREO; break; + case 0b01: $this->channel = self::JOINT_STEREO; break; + case 0b10: $this->channel = self::DUAL_MONO; break; + case 0b11: $this->channel = self::MONO; break; + } + + switch ($this->codecVersion.($this->channel == self::MONO ? 'mono' : 'stereo')) { + case "1stereo": $offset = 36; break; + case "1mono": $offset = 21; break; + case "2stereo": $offset = 21; break; + case "2mono": $offset = 13; break; + } + fseek($fp, $pos + $offset); + if (fread($fp, 4) == self::VBR_SYNC) { + $this->isVbr = true; + $flagsBytes = $this->readBytes($fp, 4); + $this->extraFlags['frames'] = (bool)($flagsBytes[3] & 1); + $this->extraFlags['bytes'] = (bool)($flagsBytes[3] & 2); + $this->extraFlags['TOC'] = (bool)($flagsBytes[3] & 4); + $this->extraFlags['VBR'] = (bool)($flagsBytes[3] & 8); + if ($this->extraFlags['frames']) $framesCount = implode(null, unpack('N', fread($fp, 4))); + } + // go to the end of frame + if ($this->layerVersion == 1) { + $this->__cbrFrameSize = floor((12 * $this->bitRate / $this->sampleRate + ($headerBytes[2] >> 1 & 0b1)) * 4); + } else { + $this->__cbrFrameSize = floor(144 * $this->bitRate / $this->sampleRate + ($headerBytes[2] >> 1 & 0b1)); + } + fseek($fp, $pos + $this->__cbrFrameSize); + + return isset($framesCount) ? $framesCount : null; + } + + private function readBytes($fp, $n) { + $raw = fread($fp, $n); + $bytes = array(); + for($i = 0; $i < $n; $i++) $bytes[$i] = ord($raw[$i]); + return $bytes; + } + + /** + * Reads id3v1 tag. + * @return int Returns length of id3v1 tag. + */ + private function readId3v1Body($fp) { + $this->tags1['song'] = trim(fread($fp, 30)); + $this->tags1['artist'] = trim(fread($fp, 30)); + $this->tags1['album'] = trim(fread($fp, 30)); + $this->tags1['year'] = trim(fread($fp, 4)); + $this->tags1['comment'] = trim(fread($fp, 30)); + $this->tags1['genre'] = hexdec(fread($fp, 1)); + return 128; + } + + /** + * Reads id3v2 tag. + * ----------------------------------- + * Overall tag header structure (10 bytes) + * ID3v2/file identifier "ID3" (3 bytes) + * ID3v2 version (2 bytes) + * ID3v2 flags (1 byte) + * ID3v2 size 4 * %0xxxxxxx (4 bytes) + * ----------------------------------- + * id3v2.2.0 tag header (10 bytes) + * ID3/file identifier "ID3" (3 bytes) + * ID3 version $02 00 (2 bytes) + * ID3 flags %xx000000 (1 byte) + * ID3 size 4 * %0xxxxxxx (4 bytes) + * Flags: + * x (bit 7) - unsynchronisation + * x (bit 6) - compression + * ----------------------------------- + * id3v2.3.0 tag header (10 bytes) + * ID3v2/file identifier "ID3" (3 bytes) + * ID3v2 version $03 00 (2 bytes) + * ID3v2 flags %abc00000 (1 byte) + * ID3v2 size 4 * %0xxxxxxx (4 bytes) + * Flags: + * a - Unsynchronisation + * b - Extended header + * c - Experimental indicator + * Extended header structure (10 bytes) + * Extended header size $xx xx xx xx + * Extended Flags $xx xx + * Size of padding $xx xx xx xx + * Extended flags: + * %x0000000 00000000 + * x - CRC data present + * ----------------------------------- + * id3v2.4.0 tag header (10 bytes) + * ID3v2/file identifier "ID3" (3 bytes) + * ID3v2 version $04 00 (2 bytes) + * ID3v2 flags %abcd0000 (1 byte) + * ID3v2 size 4 * %0xxxxxxx (4 bytes) + * Flags: + * a - Unsynchronisation + * b - Extended header + * c - Experimental indicator + * d - Footer present + * @return int Returns length of id3v2 tag. + */ + private function readId3v2Body($fp) { + // read the rest of the id3v2 header + $raw = fread($fp, 7); + $data = unpack("cmajor_version/cminor_version/H*", $raw); + $this->id3v2MajorVersion = $data['major_version']; + $this->id3v2MinorVersion = $data['minor_version']; + $data = str_pad(base_convert($data[1], 16, 2), 40, 0, STR_PAD_LEFT); + $flags = substr($data, 0, 8); + if ($this->id3v2MajorVersion == 2) { // parse id3v2.2.0 header flags + $this->id3v2Flags = array( + 'unsynchronisation' => (bool)substr($flags, 0, 1), + 'compression' => (bool)substr($flags, 1, 1), + ); + } else if ($this->id3v2MajorVersion == 3) { // parse id3v2.3.0 header flags + $this->id3v2Flags = array( + 'unsynchronisation' => (bool)substr($flags, 0, 1), + 'extended_header' => (bool)substr($flags, 1, 1), + 'experimental_indicator' => (bool)substr($flags, 2, 1), + ); + if ($this->id3v2Flags['extended_header']) + throw new \Exception('NEED TO PARSE EXTENDED HEADER!'); + } else if ($this->id3v2MajorVersion == 4) { // parse id3v2.4.0 header flags + /*throw new \Exception('NEED TO PARSE id3v2.4.0 header flags!');*/ + {} + } + $size = substr($data, 8, 32); + // some fucking shit + $sizes = str_split($size, 8); + array_walk($sizes, function (&$value) { $value = substr($value, 1);}); + $size = implode("", $sizes); + $size = bindec($size); + if ($this->id3v2MajorVersion == 2) // parse id3v2.2.0 body + /*throw new \Exception('NEED TO PARSE id3v2.2.0 flags!');*/ + {} + else if ($this->id3v2MajorVersion == 3) // parse id3v2.3.0 body + $this->parseId3v23Body($fp, 10 + $size); + else if ($this->id3v2MajorVersion == 4) // parse id3v2.4.0 body + /*throw new \Exception('NEED TO PARSE id3v2.4.0 flags!');*/ + {} + + return 10 + $size; // 10 bytes - header, rest - body + } + + /** + * Parses id3v2.3.0 tag body. + * @todo Complete. + */ + private function parseId3v23Body($fp, $lastByte) { + while (ftell($fp) < $lastByte) { + $raw = fread($fp, 10); + $frame_id = substr($raw, 0, 4); + + if ($frame_id == str_repeat(chr(0), 4)) { + fseek($fp, $lastByte); + break; + } + + $data = unpack("Nframe_size/H2flags", substr($raw, 4)); + $frame_size = $data['frame_size']; + $flags = base_convert($data['flags'], 16, 2); + $this->id3v2TagsFlags[$frame_id] = array( + 'flags' => array( + 'tag_alter_preservation' => (bool)substr($flags, 0, 1), + 'file_alter_preservation' => (bool)substr($flags, 1, 1), + 'read_only' => (bool)substr($flags, 2, 1), + 'compression' => (bool)substr($flags, 8, 1), + 'encryption' => (bool)substr($flags, 9, 1), + 'grouping_identity' => (bool)substr($flags, 10, 1), + ), + ); + switch ($frame_id) { + case 'UFID': # Unique file identifier + break; + + ################# Text information frames + case 'TALB': # Album/Movie/Show title + case 'TBPM': # BPM (beats per minute) + case 'TCOM': # Composer + case 'TCON': # Content type + case 'TCOP': # Copyright message + case 'TDAT': # Date + case 'TDLY': # Playlist delay + case 'TENC': # Encoded by + case 'TEXT': # Lyricist/Text writer + case 'TFLT': # File type + case 'TIME': # Time + case 'TIT1': # Content group description + case 'TIT2': # Title/songname/content description + case 'TIT3': # Subtitle/Description refinement + case 'TKEY': # Initial key + case 'TLAN': # Language(s) + case 'TLEN': # Length + case 'TMED': # Media type + case 'TOAL': # Original album/movie/show title + case 'TOFN': # Original filename + case 'TOLY': # Original lyricist(s)/text writer(s) + case 'TOPE': # Original artist(s)/performer(s) + case 'TORY': # Original release year + case 'TOWN': # File owner/licensee + case 'TPE1': # Lead performer(s)/Soloist(s) + case 'TPE2': # Band/orchestra/accompaniment + case 'TPE3': # Conductor/performer refinement + case 'TPE4': # Interpreted, remixed, or otherwise modified by + case 'TPOS': # Part of a set + case 'TPUB': # Publisher + case 'TRCK': # Track number/Position in set + case 'TRDA': # Recording dates + case 'TRSN': # Internet radio station name + case 'TRSO': # Internet radio station owner + case 'TSIZ': # Size + case 'TSRC': # ISRC (international standard recording code) + case 'TSSE': # Software/Hardware and settings used for encoding + case 'TYER': # Year + case 'TXXX': # User defined text information frame + $raw = fread($fp, $frame_size); + $data = unpack("C1encoding/A".($frame_size - 1)."information", $raw); + if ((bool)($data['encoding'] == 0x00)) # ISO-8859-1 + $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1'); + else # utf-16 + $this->tags2[$frame_id] = mb_convert_encoding($data['information'], 'utf-8', 'utf-16'); + break; + ################# Text information frames + + ################# URL link frames + case 'WCOM': # Commercial information + break; + case 'WCOP': # Copyright/Legal information + break; + case 'WOAF': # Official audio file webpage + break; + case 'WOAR': # Official artist/performer webpage + break; + case 'WOAS': # Official audio source webpage + break; + case 'WORS': # Official internet radio station homepage + break; + case 'WPAY': # Payment + break; + case 'WPUB': # Publishers official webpage + break; + case 'WXXX': # User defined URL link frame + break; + ################# URL link frames + + case 'IPLS': # Involved people list + break; + case 'MCDI': # Music CD identifier + break; + case 'ETCO': # Event timing codes + break; + case 'MLLT': # MPEG location lookup table + break; + case 'SYTC': # Synchronized tempo codes + break; + case 'USLT': # Unsychronized lyric/text transcription + break; + case 'SYLT': # Synchronized lyric/text + break; + case 'COMM': # Comments + $dataEnd = ftell($fp) + $frame_size; + $raw = fread($fp, 4); + $data = unpack("C1encoding/A3language", $raw); + // read until \null character + $short_description = null; + while (ftell($fp) < $dataEnd) { + $char = fgetc($fp); + if ($char == chr(0)) $actual_text = null; + else if (isset($actual_text)) $actual_text .= $char; + else $short_description .= $char; + } + if (!isset($actual_text)) $actual_text = $short_description; + // list($short_description, $actual_text) = sscanf("s".chr(0)."s", $data['texts']); + // list($short_description, $actual_text) = explode(chr(0), $data['texts']); + $this->tags2[$frame_id][$data['language']] = array( + 'short' => (bool)($data['encoding'] == 0x00) ? mb_convert_encoding($short_description, 'utf-8', 'iso-8859-1') : mb_convert_encoding($short_description, 'utf-8', 'utf-16'), + 'actual' => (bool)($data['encoding'] == 0x00) ? mb_convert_encoding($actual_text, 'utf-8', 'iso-8859-1') : mb_convert_encoding($actual_text, 'utf-8', 'utf-16'), + ); + break; + case 'RVAD': # Relative volume adjustment + break; + case 'EQUA': # Equalization + break; + case 'RVRB': # Reverb + break; + case 'APIC': # Attached picture + break; + case 'GEOB': # General encapsulated object + break; + case 'PCNT': # Play counter + $raw = fread($fp, $frame_size); + $data = unpack("L", $raw); + $this->tags2[$frame_id] = $data[1]; + break; + case 'POPM': # Popularimeter + break; + case 'RBUF': # Recommended buffer size + break; + case 'AENC': # Audio encryption + break; + case 'LINK': # Linked information + break; + case 'POSS': # Position synchronisation frame + break; + case 'USER': # Terms of use + break; + case 'OWNE': # Ownership frame + break; + case 'COMR': # Commercial frame + break; + case 'ENCR': # Encryption method registration + break; + case 'GRID': # Group identification registration + break; + case 'PRIV': # Private frame + break; + } + } + } + + /** + * Simple function that checks mpeg-audio correctness of given file. + * Actually it checks that first 3 bytes of file is a id3v2 tag mark or that first 11 bits of file is a frame header sync mark. + * To perform full test create an instance of Mp3Info with given file. + * @param string $filename File to be tested. + * @return boolean True if file is looks correct, False otherwise. + */ + static public function isValidAudio($filename) { + if (!file_exists($filename)) + throw new Exception("File ".$filename." is not present!"); + $raw = file_get_contents($filename, false, null, 0, 3); + return ($raw == self::TAG2_SYNC || substr(base_convert(implode(null, unpack('H*', $raw)), 16, 2), 0, 11) == self::FRAME_SYNC); + } +}