Initiation
This commit is contained in:
commit
b2cad7745b
165
LICENSE
Normal file
165
LICENSE
Normal file
@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
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.
|
107
README.md
Normal file
107
README.md
Normal file
@ -0,0 +1,107 @@
|
||||
# Mp3Info
|
||||
The fastest PHP library to get mp3 tags&meta.
|
||||
|
||||
[](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)
|
44
bin/scan
Executable file
44
bin/scan
Executable file
@ -0,0 +1,44 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
use wapmorgan\Mp3Info\Mp3Info;
|
||||
|
||||
if ($argc == 1)
|
||||
die('Specify file names to scan');
|
||||
|
||||
function formatTime($time) {
|
||||
return floor($time / 60).':'.str_pad(floor($time % 60), 2, 0, STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
function substrIfLonger($string, $maxLength) {
|
||||
if (strlen($string) > $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;
|
12
composer.json
Normal file
12
composer.json
Normal file
@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
13
data/bitRateTable.php
Normal file
13
data/bitRateTable.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
return array(
|
||||
1 => 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
|
||||
),
|
||||
);
|
5
data/sampleRateTable.php
Normal file
5
data/sampleRateTable.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
return array(
|
||||
1 => array(44100, 48000, 32000, false), // MPEG 1
|
||||
2 => array(22050, 24000, 16000, false), // MPEG 2
|
||||
);
|
584
src/Mp3Info.php
Normal file
584
src/Mp3Info.php
Normal file
@ -0,0 +1,584 @@
|
||||
<?php
|
||||
namespace wapmorgan\Mp3Info;
|
||||
|
||||
/**
|
||||
* This class extracts information about an mpeg audio. (supported mpeg versions: MPEG-1, MPEG-2)
|
||||
* (supported mpeg audio layers: 1, 2, 3).
|
||||
*
|
||||
* It extracts:
|
||||
* * All tags stored in both at the beginning and at the end of file (id3v2 and id3v1). id3v2.4.0 and id3v2.2.0 are not supported, only the most popular id3v2.3.0 is supported.
|
||||
* * Audio parameters:
|
||||
* * * - Total duration (in seconds)
|
||||
* * * - BitRate (in bps)
|
||||
* * * - SampleRate (in Hz)
|
||||
* * * - Number of channels (stereo or not)
|
||||
* * * - ... and other information
|
||||
*
|
||||
* Used sources:
|
||||
* * {@link http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm mpeg header description}
|
||||
* * {@link http://id3.org/Developer%20Information id3v2 tag specifications}. Specially: {@link http://id3.org/id3v2.3.0 id3v2.3.0}, {@link http://id3.org/id3v2-00 id3v2.2.0}, {@link http://id3.org/id3v2.4.0-changes id3v2.4.0}
|
||||
* * {@link http://gabriel.mp3-tech.org/mp3infotag.html Xing, Info and Lame tags specifications}
|
||||
*/
|
||||
class Mp3Info {
|
||||
const TAG1_SYNC = "TAG";
|
||||
const TAG2_SYNC = "ID3";
|
||||
const VBR_SYNC = "Xing";
|
||||
const CBR_SYNC = "Info";
|
||||
|
||||
const TAGS = 1;
|
||||
const META = 2;
|
||||
|
||||
const MPEG_1 = 1;
|
||||
const MPEG_2 = 2;
|
||||
|
||||
const LAYER_1 = 1;
|
||||
const LAYER_2 = 2;
|
||||
const LAYER_3 = 3;
|
||||
|
||||
const STEREO = "stereo";
|
||||
const JOINT_STEREO = "joint_stereo";
|
||||
const DUAL_MONO = "dual_mono";
|
||||
const MONO = "mono";
|
||||
|
||||
/**
|
||||
* Boolean trigger to enable / disable trace output
|
||||
*/
|
||||
static public $traceOutput = false;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
static private $_bitRateTable;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
static private $_sampleRateTable;
|
||||
|
||||
/**
|
||||
* MPEG codec version (1 or 2)
|
||||
* @var int
|
||||
*/
|
||||
public $codecVersion;
|
||||
/**
|
||||
* Audio layer version (1 or 2 or 3)
|
||||
* @var int
|
||||
*/
|
||||
public $layerVersion;
|
||||
|
||||
/**
|
||||
* Audio size in bytes. Note that this value is NOT equals file size.
|
||||
* @var int|long
|
||||
*/
|
||||
public $audioSize;
|
||||
/**
|
||||
* Contains audio file name
|
||||
* @var string
|
||||
*/
|
||||
public $_fileName;
|
||||
/**
|
||||
* Contains file size
|
||||
* @var long
|
||||
*/
|
||||
public $_fileSize;
|
||||
/**
|
||||
* Audio duration in seconds.microseconds (e.g. 3603.0171428571)
|
||||
* @var float
|
||||
*/
|
||||
public $duration;
|
||||
/**
|
||||
* Audio bit rate in bps (e.g. 128000)
|
||||
*/
|
||||
public $bitRate;
|
||||
/**
|
||||
* Audio sample rate in Hz (e.g. 44100)
|
||||
* @var int
|
||||
*/
|
||||
public $sampleRate;
|
||||
/**
|
||||
* Contains true if audio has variable bit rate
|
||||
* @var boolean
|
||||
*/
|
||||
public $isVbr = false;
|
||||
/**
|
||||
* Channel mode (stereo or dual_mono or joint_stereo or mono)
|
||||
* @var string
|
||||
*/
|
||||
public $channel;
|
||||
/**
|
||||
* Number of audio frames in file
|
||||
* @var int
|
||||
*/
|
||||
public $framesCount = 0;
|
||||
/**
|
||||
* Contains extra flags
|
||||
* @var array
|
||||
*/
|
||||
public $extraFlags = array();
|
||||
/**
|
||||
* Audio tags ver. 1 (aka id3v1)
|
||||
* @var array
|
||||
*/
|
||||
public $tags1 = array();
|
||||
/**
|
||||
* Audio tags ver. 2 (aka id3v2)
|
||||
* @var array
|
||||
*/
|
||||
public $tags2 = array();
|
||||
/**
|
||||
* Major version of id3v2 tag (if id3v2 present) (2 or 3 or 4)
|
||||
* @var int
|
||||
*/
|
||||
public $id3v2MajorVersion;
|
||||
/**
|
||||
* Minor version of id3v2 tag (if id3v2 present)
|
||||
* @var int
|
||||
*/
|
||||
public $id3v2MinorVersion;
|
||||
/**
|
||||
* List of id3v2 header flags (if id3v2 present)
|
||||
* @var array
|
||||
*/
|
||||
public $id3v2Flags = array();
|
||||
/**
|
||||
* List of id3v2 tags flags (if id3v2 present)
|
||||
* @var array
|
||||
*/
|
||||
public $id3v2TagsFlags = array();
|
||||
/**
|
||||
* Contains time spent to read&extract audio information.
|
||||
* @var float
|
||||
*/
|
||||
public $_parsingTime;
|
||||
|
||||
/**
|
||||
* Calculated frame size for Constant Bit Rate
|
||||
* @var int
|
||||
*/
|
||||
private $__cbrFrameSize;
|
||||
|
||||
/**
|
||||
* $mode is self::META, self::TAGS or their combination.
|
||||
*/
|
||||
public function __construct($filename, $parseTags = false) {
|
||||
if (is_null(self::$_bitRateTable)) self::$_bitRateTable = require dirname(__FILE__).'/../data/bitRateTable.php';
|
||||
if (is_null(self::$_sampleRateTable)) self::$_sampleRateTable = require dirname(__FILE__).'/../data/sampleRateTable.php';
|
||||
|
||||
if (!file_exists($filename))
|
||||
throw new Exception("File ".$filename." is not present!");
|
||||
$mode = $parseTags ? self::META | self::TAGS : self::META;
|
||||
$this->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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user