Re: [Patch] Added VBR support to MP3_Id

From: Date: Sat, 20 Aug 2005 08:56:26 +0000
Subject: Re: [Patch] Added VBR support to MP3_Id
References: 1  Groups: php.pear.dev 
Request: Send a blank email to pear-dev+get-39473@lists.php.net to get a copy of this message
Hi, Can you open a bug report at http://pear.php.net/bugs/report.php?package=MP3_ID With a link to the diffs so it does not get lost and marked as an "feature/change request" Thank you. Arnaud. Michaël Hompus wrote:
Hello, I've added VBR support to the MP3_Id module. Changes: - Added functions to check for the VBRI or XING header inside the audio frame and read it. - Added property to store encoding type (CBR/VBR). - Added property to store the real MP3 filelength (without the id3tag overhead). - Added properties to store number of samples, samples_per_frame and number of frames. - Added property to store quality indicator (VBR only). - Added property with the length stored in hh:mm:ss format. There are to versions of the diff file: 1. MP3_Id.orig-layout.diff Contains the changes without changing the structure of the original file. 2. MP3_Id.diff Contains the same changes. But:
    - Stripped all trailing spaces.
    - Changed all tabs into spaces
    - Changed encoding to ANSI
This according to the PEAR coding standards. Since the diff file was almost useless for people to read I included the other file. I've tested it in my application with several files (VBR and CBR). Greetings, Michaël Hompus xpindex@hompus.nl ------------------------------------------------------------------------ ? MP3_Id.orig-layout.diff Index: Id.php =================================================================== RCS file: /repository/pear/MP3_Id/Id.php,v retrieving revision 1.8 diff -u -r1.8 Id.php --- Id.php 4 Jul 2005 11:32:09 -0000 1.8 +++ Id.php 19 Aug 2005 23:09:49 -0000 @@ -179,6 +179,36 @@
    */                    var $frequency = 0;
    /**
+    * encoding type (CBR or VBR)
+    * @var string
+    */
+    var $encoding_type = 0;
+    /**
+    * number of samples per MPEG audio frame
+    * @var integer
+    */
+    var $samples_per_frame = 0;
+    /**
+    * samples in file
+    * @var integer
+    */
+    var $samples = 0;
+    /**
+    * Bytes in file without tag overhead
+    * @var integer
+    */
+    var $musicsize = -1;
+    /**
+    * number of MPEG audio frames
+    * @var integer
+    */
+    var $frames = 0;
+    /**
+    * quality indicator (0% - 100%)
+    * @var integer
+    */
+    var $quality = 0;
+    /**
    * Frames padded
    * @var boolean
    */                @@ -218,8 +248,13 @@
    * @var integer
    */                                var $frameoffset = -1;
+     /**
+    * length of mp3 format hh:mm:ss
+    * @var string
+    */
+    var $lengthh = false;
    /**
-    * length of mp3 format hh:ss
+    * length of mp3 format mm:ss
    * @var string
    */
    var $length = false; @@ -662,6 +697,113 @@
    $this->frameoffset = $frameoffset;
+    // Detect VBR header
+    if ($bits[11] == 0) {
+        if (($bits[24] == 1) && ($bits[25] == 1)) {
+            $vbroffset = 9; // MPEG 2.5 Mono
+        } else {
+            $vbroffset = 17; // MPEG 2.5 Stereo
+        }
+    } else if ($bits[12] == 0) {
+        if (($bits[24] == 1) && ($bits[25] == 1)) {
+            $vbroffset = 9; // MPEG 2 Mono
+        } else {
+            $vbroffset = 17; // MPEG 2 Stereo
+        }
+    } else {
+        if (($bits[24] == 1) && ($bits[25] == 1)) {
+            $vbroffset = 17; // MPEG 1 Mono
+        } else {
+            $vbroffset = 32; // MPEG 1 Stereo
+        }
+    }
+
+    fseek($f, ftell($f) + $vbroffset);
+    $r = fread($f, 4);
+
+    switch ($r) {
+        case 'Xing':
+            $this->encoding_type = 'VBR';
+        case 'Info':
+            // Extract info from Xing header
+
+            if ($this->debug) print('Encoding Header: ' . $r . "\n");
+
+            $r = fread($f, 4);
+            $vbrbits = sprintf("%'08b", ord($r{3}));
+
+            if ($this->debug) print('XING Header Bits: ' . $vbrbits . "\n");
+
+            if ($vbrbits[7] == 1) {
+                // Next 4 bytes contain number of frames
+                $r = fread($f, 4);
+                $this->frames = unpack('N', $r);
+                $this->frames = $this->frames[1];
+            }
+
+            if ($vbrbits[6] == 1) {
+                // Next 4 bytes contain number of bytes
+                $r = fread($f, 4);
+                $this->musicsize = unpack('N', $r);
+                $this->musicsize = $this->musicsize[1];
+            }
+
+            if ($vbrbits[5] == 1) {
+                // Next 100 bytes contain TOC entries, skip
+                fseek($f, ftell($f) + 100);
+            }
+
+            if ($vbrbits[4] == 1) {
+                // Next 4 bytes contain Quality Indicator
+                $r = fread($f, 4);
+                $this->quality = unpack('N', $r);
+                $this->quality = $this->quality[1];
+            }
+
+            break;
+
+        case 'VBRI':
+        default:
+            if ($vbroffset != 32) {
+                // VBRI Header is fixed after 32 bytes, so maybe we are looking at the wrong place.
+                fseek($f, ftell($f) + 32 - $vbroffset);
+                $r = fread($f, 4);
+
+                if ($r != 'VBRI') {
+                    $this->encoding_type = 'CBR';
+                    break;
+                }
+            } else {
+                $this->encoding_type = 'CBR';
+                break;
+            }
+
+            if ($this->debug) print('Encoding Header: ' . $r . "\n");
+
+            $this->encoding_type = 'VBR';
+
+            // Next 2 bytes contain Version ID, skip
+            fseek($f, ftell($f) + 2);
+
+            // Next 2 bytes contain Delay, skip
+            fseek($f, ftell($f) + 2);
+
+            // Next 2 bytes contain Quality Indicator
+            $r = fread($f, 2);
+            $this->quality = unpack('n', $r);
+            $this->quality = $this->quality[1];
+
+            // Next 4 bytes contain number of bytes
+            $r = fread($f, 4);
+            $this->musicsize = unpack('N', $r);
+            $this->musicsize = $this->musicsize[1];
+
+            // Next 4 bytes contain number of frames
+            $r = fread($f, 4);
+            $this->frames = unpack('N', $r);
+            $this->frames = $this->frames[1];
+    }
+
    fclose($f);
    if ($bits[11] == 0) {
@@ -744,13 +886,48 @@
             );
    $this->emphasis = $emphasis[$bits[30]][$bits[31]];
-    if ($this->bitrate == 0) {
-        $s = -1;
+    $samplesperframe = array(
+        '1' => array(
+            '1' => 384,
+            '2' => 1152,
+            '3' => 1152
+        ),
+        '2' => array(
+            '1' => 384,
+            '2' => 1152,
+            '3' => 576
+        ),
+        '2.5' => array(
+            '1' => 384,
+            '2' => 1152,
+            '3' => 576
+        ),
+    );
+    $this->samples_per_frame = $samplesperframe[$this->mpeg_ver][$this->layer];
+
+    if ($this->encoding_type != 'VBR') {
+        if ($this->bitrate == 0) {
+            $s = -1;
+        } else {
+            $s = ((8*filesize($this->file))/1000) / $this->bitrate;
+        }
+        $this->length = sprintf('%02d:%02d',floor($s/60),floor($s-(floor($s/60)*60)));
+        $this->lengthh = sprintf('%02d:%02d:%02d',floor($s/3600),floor($s/60),floor($s-(floor($s/60)*60)));
+        $this->lengths = (int)$s;
+
+        $this->samples = ceil($this->lengths * $this->frequency);
+        $this->frames = ceil($this->samples / $this->samples_per_frame);
+        $this->musicsize = ceil($this->lengths * $this->bitrate * 1000 / 8);
    } else {
-        $s = ((8*filesize($this->file))/1000) / $this->bitrate;        +        $this->samples = $this->samples_per_frame * $this->frames;
+        $s = $this->samples / $this->frequency;
+
+        $this->length = sprintf('%02d:%02d',floor($s/60),floor($s-(floor($s/60)*60)));
+        $this->lengthh = sprintf('%02d:%02d:%02d',floor($s/3600),floor($s/60),floor($s-(floor($s/60)*60)));
+        $this->lengths = (int)$s;
+
+        $this->bitrate = (int)(($this->musicsize / $s) * 8 / 1000);
    }
-    $this->length = sprintf('%02d:%02d',floor($s/60),floor($s-(floor($s/60)*60)));
-    $this->lengths = (int)$s;
    if ($this->debug) print($this->debugend);
    } // _readframe()
 
------------------------------------------------------------------------ ? MP3_Id.diff Index: Id.php =================================================================== RCS file: /repository/pear/MP3_Id/Id.php,v retrieving revision 1.8 diff -u -r1.8 Id.php --- Id.php 4 Jul 2005 11:32:09 -0000 1.8 +++ Id.php 19 Aug 2005 23:00:52 -0000 @@ -45,19 +45,19 @@ /** * Tag not found -* @const PEAR_MP3_ID_TNF +* @const PEAR_MP3_ID_TNF */ define('PEAR_MP3_ID_TNF', 3); /** * File is not a MP3 file (corrupted?) -* @const PEAR_MP3_ID_NOMP3 +* @const PEAR_MP3_ID_NOMP3 */ define('PEAR_MP3_ID_NOMP3', 4); /** * A Class for reading/writing MP3 ID3 tags - * + * * Note: This code doesn't try to deal with corrupt mp3s. So if you get * incorrect length times or something else it may be your mp3. To fix just * re-enocde from the CD. :~) @@ -76,7 +76,7 @@ * $id3->write(); * $id3->read($file); * print_r($id3 ); - * + * * @package MP3_Id * @author Sandy McArthur Jr. <Leknor@Leknor.com> * @version $Version$ @@ -87,53 +87,53 @@
    * mp3/mpeg file name
    * @var boolean
    */
-    var $file = false;      +    var $file = false;
    /**
    * ID3 v1 tag found? (also true if v1.1 found)
    * @var boolean
    */
-    var $id3v1 = false;     +    var $id3v1 = false;
    /**
-    * ID3 v1.1 tag found? +    * ID3 v1.1 tag found?
    * @var boolean
-    */    +    */
    var $id3v11 = false;
    /**
    * ID3 v2 tag found? (not used yet)
    * @var boolean
-    */        +    */
    var $id3v2 = false;
    // ID3v1.1 Fields:
    /**
    * trackname
    * @var string
-    */        +    */
    var $name = '';
    /**
    * artists
    * @var string
-    */            +    */
    var $artists = '';
    /**
    * album
    * @var string
-    */                +    */
    var $album = '';
    /**
    * year
    * @var string
-    */                -    var $year = '';  +    */
+    var $year = '';
    /**
    * comment
    * @var string
-    */                +    */
    var $comment = '';
    /**
    * track number
    * @var integer
-    */                +    */
    var $track = 0;
    /**
    * genre name
@@ -143,7 +143,7 @@
    /**
    * genre number
    * @var integer
-    */                    +    */
    var $genreno = 255;
    // MP3 Frame Stuff
@@ -152,88 +152,123 @@
    * @var boolean
    */
    var $studied = false;
-    +
    /**
    * version of mpeg
    * @var integer
-    */    +    */
    var $mpeg_ver = 0;
    /**
    * version of layer
    * @var integer
-    */        +    */
    var $layer = 0;
    /**
    * version of bitrate
    * @var integer
-    */        +    */
    var $bitrate = 0;
    /**
    * Frames are crc protected?
    * @var boolean
-    */            +    */
    var $crc = false;
    /**
    * frequency
    * @var integer
-    */                +    */
    var $frequency = 0;
    /**
+    * encoding type (CBR or VBR)
+    * @var string
+    */
+    var $encoding_type = 0;
+    /**
+    * number of samples per MPEG audio frame
+    * @var integer
+    */
+    var $samples_per_frame = 0;
+    /**
+    * samples in file
+    * @var integer
+    */
+    var $samples = 0;
+    /**
+    * Bytes in file without tag overhead
+    * @var integer
+    */
+    var $musicsize = -1;
+    /**
+    * number of MPEG audio frames
+    * @var integer
+    */
+    var $frames = 0;
+    /**
+    * quality indicator (0% - 100%)
+    * @var integer
+    */
+    var $quality = 0;
+    /**
    * Frames padded
    * @var boolean
-    */                +    */
    var $padding = false;
    /**
    * private bit set
    * @var boolean
-    */                    +    */
    var $private = false;
    /**
    * Mode (Stero etc)
    * @var string
-    */                    +    */
    var $mode = '';
    /**
    * Copyrighted
    * @var string
-    */                        -    var $copyright = false; +    */
+    var $copyright = false;
    /**
    * On Original Media? (never used)
    * @var boolean
-    */                        +    */
    var $original = false;
    /**
    * Emphasis (also never used)
    * @var boolean
-    */                        -    var $emphasis = '';     +    */
+    var $emphasis = '';
    /**
-    * Bytes in file +    * Bytes in file
    * @var integer
    */
    var $filesize = -1;
    /**
    * Byte at which the first mpeg header was found
    * @var integer
-    */                            +    */
    var $frameoffset = -1;
+     /**
+    * length of mp3 format hh:mm:ss
+    * @var string
+    */
+    var $lengthh = false;
    /**
-    * length of mp3 format hh:ss
+    * length of mp3 format mm:ss
    * @var string
    */
-    var $length = false; +    var $length = false;
    /**
    * length of mp3 in seconds
    * @var string
-    */                            +    */
    var $lengths = false;
    /**
    * if any errors they will be here
    * @var string
    */
-    var $error = false;    +    var $error = false;
    /**
    * print debugging info?
@@ -243,7 +278,7 @@
    /**
    * print debugg
    * @var string
-    */    +    */
    var $debugbeg = '<DIV STYLE="margin: 0.5 em; padding: 0.5 em; border-width: thin; border-color: black; border-style: solid">';
    /**
    * print debugg
@@ -284,7 +319,7 @@
    /**
    * sets a field
-    * +    *
    * possible names of tags are:
    * artists   - Name of band or artist
    * album     - Name of the album
@@ -295,7 +330,7 @@
    * genreno   - Number of the genre
    *
    * @param    mixed   $name   Name of the tag to set or hash with the key as fieldname
-    * @param    mixed   $value  the value to set +    * @param    mixed   $value  the value to set
    *
    * @access   public
    */
@@ -305,18 +340,18 @@
                $this -> $n = $v ;
                }
        } else {
-            $this -> $name = $value ;        +            $this -> $name = $value ;
        }
    }
-    +
    /**
    * get the value of a tag
-    * +    *
    * @param    string  $name       the name of the field to get
    * @param    mixed   $default    returned if the field not exists
-    * +    *
    * @return   mixed   The value of the field
-    * @access   public +    * @access   public
    * @see      setTag
    */
    function getTag($name, $default = 0) {
@@ -325,15 +360,15 @@
        } else {
            return $this -> $name ;
        }
-    }        -    +    }
+
    /**
     * update the id3v1 tags on the file.
     * Note: If/when ID3v2 is implemented this method will probably get another
     *       parameters.
-     *     +     *
     * @param boolean $v1   if true update/create an id3v1 tag on the file. (defaults to true)
-     * +     *
     * @access public
     */
    function write($v1 = true) {
@@ -346,7 +381,7 @@
    /**
     * study() - does extra work to get the MPEG frame info.
-     * +     *
     * @access public
     */
    function study() {
@@ -356,7 +391,7 @@
    /**
     * copy($from) - set's the ID3 fields to the same as the fields in $from
-     * +     *
     * @param string    $from   fields to copy
     * @access public
     */
@@ -590,7 +625,7 @@
     *
     * returns true if the tag was removed or none was found
     * else false if there was an error
-     * +     *
     * @return boolean true, if the tag was removed
     * @access private
     */
@@ -613,7 +648,7 @@
    if ( !PEAR::isError( $this->_decode_v1($r))) {
        $size = filesize($this->file) - 128;
        if ($this->debug) print('size: old: ' . filesize($this->file));
-        $success = ftruncate($f, $size);    +        $success = ftruncate($f, $size);
        clearstatcache();
        if ($this->debug) print(' new: ' . filesize($this->file));
    }
@@ -662,6 +697,113 @@
    $this->frameoffset = $frameoffset;
+    // Detect VBR header
+    if ($bits[11] == 0) {
+        if (($bits[24] == 1) && ($bits[25] == 1)) {
+            $vbroffset = 9; // MPEG 2.5 Mono
+        } else {
+            $vbroffset = 17; // MPEG 2.5 Stereo
+        }
+    } else if ($bits[12] == 0) {
+        if (($bits[24] == 1) && ($bits[25] == 1)) {
+            $vbroffset = 9; // MPEG 2 Mono
+        } else {
+            $vbroffset = 17; // MPEG 2 Stereo
+        }
+    } else {
+        if (($bits[24] == 1) && ($bits[25] == 1)) {
+            $vbroffset = 17; // MPEG 1 Mono
+        } else {
+            $vbroffset = 32; // MPEG 1 Stereo
+        }
+    }
+
+    fseek($f, ftell($f) + $vbroffset);
+    $r = fread($f, 4);
+
+    switch ($r) {
+        case 'Xing':
+            $this->encoding_type = 'VBR';
+        case 'Info':
+            // Extract info from Xing header
+
+            if ($this->debug) print('Encoding Header: ' . $r . "\n");
+
+            $r = fread($f, 4);
+            $vbrbits = sprintf("%'08b", ord($r{3}));
+
+            if ($this->debug) print('XING Header Bits: ' . $vbrbits . "\n");
+
+            if ($vbrbits[7] == 1) {
+                // Next 4 bytes contain number of frames
+                $r = fread($f, 4);
+                $this->frames = unpack('N', $r);
+                $this->frames = $this->frames[1];
+            }
+
+            if ($vbrbits[6] == 1) {
+                // Next 4 bytes contain number of bytes
+                $r = fread($f, 4);
+                $this->musicsize = unpack('N', $r);
+                $this->musicsize = $this->musicsize[1];
+            }
+
+            if ($vbrbits[5] == 1) {
+                // Next 100 bytes contain TOC entries, skip
+                fseek($f, ftell($f) + 100);
+            }
+
+            if ($vbrbits[4] == 1) {
+                // Next 4 bytes contain Quality Indicator
+                $r = fread($f, 4);
+                $this->quality = unpack('N', $r);
+                $this->quality = $this->quality[1];
+            }
+
+            break;
+
+        case 'VBRI':
+        default:
+            if ($vbroffset != 32) {
+                // VBRI Header is fixed after 32 bytes, so maybe we are looking at the wrong place.
+                fseek($f, ftell($f) + 32 - $vbroffset);
+                $r = fread($f, 4);
+
+                if ($r != 'VBRI') {
+                    $this->encoding_type = 'CBR';
+                    break;
+                }
+            } else {
+                $this->encoding_type = 'CBR';
+                break;
+            }
+
+            if ($this->debug) print('Encoding Header: ' . $r . "\n");
+
+            $this->encoding_type = 'VBR';
+
+            // Next 2 bytes contain Version ID, skip
+            fseek($f, ftell($f) + 2);
+
+            // Next 2 bytes contain Delay, skip
+            fseek($f, ftell($f) + 2);
+
+            // Next 2 bytes contain Quality Indicator
+            $r = fread($f, 2);
+            $this->quality = unpack('n', $r);
+            $this->quality = $this->quality[1];
+
+            // Next 4 bytes contain number of bytes
+            $r = fread($f, 4);
+            $this->musicsize = unpack('N', $r);
+            $this->musicsize = $this->musicsize[1];
+
+            // Next 4 bytes contain number of frames
+            $r = fread($f, 4);
+            $this->frames = unpack('N', $r);
+            $this->frames = $this->frames[1];
+    }
+
    fclose($f);
    if ($bits[11] == 0) {
@@ -744,13 +886,48 @@
             );
    $this->emphasis = $emphasis[$bits[30]][$bits[31]];
-    if ($this->bitrate == 0) {
-        $s = -1;
+    $samplesperframe = array(
+        '1' => array(
+            '1' => 384,
+            '2' => 1152,
+            '3' => 1152
+        ),
+        '2' => array(
+            '1' => 384,
+            '2' => 1152,
+            '3' => 576
+        ),
+        '2.5' => array(
+            '1' => 384,
+            '2' => 1152,
+            '3' => 576
+        ),
+    );
+    $this->samples_per_frame = $samplesperframe[$this->mpeg_ver][$this->layer];
+
+    if ($this->encoding_type != 'VBR') {
+        if ($this->bitrate == 0) {
+            $s = -1;
+        } else {
+            $s = ((8*filesize($this->file))/1000) / $this->bitrate;
+        }
+        $this->length = sprintf('%02d:%02d',floor($s/60),floor($s-(floor($s/60)*60)));
+        $this->lengthh = sprintf('%02d:%02d:%02d',floor($s/3600),floor($s/60),floor($s-(floor($s/60)*60)));
+        $this->lengths = (int)$s;
+
+        $this->samples = ceil($this->lengths * $this->frequency);
+        $this->frames = ceil($this->samples / $this->samples_per_frame);
+        $this->musicsize = ceil($this->lengths * $this->bitrate * 1000 / 8);
    } else {
-        $s = ((8*filesize($this->file))/1000) / $this->bitrate;        +        $this->samples = $this->samples_per_frame * $this->frames;
+        $s = $this->samples / $this->frequency;
+
+        $this->length = sprintf('%02d:%02d',floor($s/60),floor($s-(floor($s/60)*60)));
+        $this->lengthh = sprintf('%02d:%02d:%02d',floor($s/3600),floor($s/60),floor($s-(floor($s/60)*60)));
+        $this->lengths = (int)$s;
+
+        $this->bitrate = (int)(($this->musicsize / $s) * 8 / 1000);
    }
-    $this->length = sprintf('%02d:%02d',floor($s/60),floor($s-(floor($s/60)*60)));
-    $this->lengths = (int)$s;
    if ($this->debug) print($this->debugend);
    } // _readframe()
@@ -767,7 +944,7 @@
     * @param   integer $genreno Number of the genre
     * @return  mixed   false, if no genre found, else string
     *
-     * @access public     +     * @access public
     */
    function getGenre($genreno) {
    if ($this->debug) print($this->debugbeg . "getgenre($genreno)<HR>\n");


« previous php.pear.dev (#39473) next »