Файл: moduls/PEAR/Ogg.php
Строк: 530
<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------------+
// | File_Ogg PEAR Package for Accessing Ogg Bitstreams                         |
// | Copyright (c) 2005-2007                                                    |
// | David Grant <david@grant.org.uk>                                           |
// | Tim Starling <tstarling@wikimedia.org>                                     |
// +----------------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or              |
// | modify it under the terms of the GNU Lesser General Public                 |
// | License as published by the Free Software Foundation; either               |
// | version 2.1 of the License, or (at your option) any later version.         |
// |                                                                            |
// | This library is distributed in the hope that it will be useful,            |
// | but WITHOUT ANY WARRANTY; without even the implied warranty of             |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU          |
// | Lesser General Public License for more details.                            |
// |                                                                            |
// | You should have received a copy of the GNU Lesser General Public           |
// | License along with this library; if not, write to the Free Software        |
// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA |
// +----------------------------------------------------------------------------+
/**
 * @author      David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org>
 * @category    File
 * @copyright   David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org>
 * @license     http://www.gnu.org/copyleft/lesser.html GNU LGPL
 * @link        http://pear.php.net/package/File_Ogg
 * @package     File_Ogg
 * @version     CVS: $Id: Ogg.php,v 1.15 2008/01/31 04:41:44 tstarling Exp $
 */
/**
 * @access  public
 */
define("OGG_STREAM_VORBIS",     1);
/**
 * @access  public
 */
define("OGG_STREAM_THEORA",     2);
/**
 * @access  public
 */
define("OGG_STREAM_SPEEX",      3);
/**
 * @access  public
 */
define("OGG_STREAM_FLAC",       4);
/**
 * Capture pattern to determine if a file is an Ogg physical stream.
 * 
 * @access  private
 */
define("OGG_CAPTURE_PATTERN", "OggS");
/**
 * Maximum size of an Ogg stream page plus four.  This value is specified to allow
 * efficient parsing of the physical stream.  The extra four is a paranoid measure
 * to make sure a capture pattern is not split into two parts accidentally.
 * 
 * @access  private
 */
define("OGG_MAXIMUM_PAGE_SIZE", 65311);
/**
 * Capture pattern for an Ogg Vorbis logical stream.
 * 
 * @access  private
 */
define("OGG_STREAM_CAPTURE_VORBIS", "vorbis");
/**
 * Capture pattern for an Ogg Speex logical stream.
 * @access  private
 */
define("OGG_STREAM_CAPTURE_SPEEX",  "Speex   ");
/**
 * Capture pattern for an Ogg FLAC logical stream.
 * 
 * @access  private
 */
define("OGG_STREAM_CAPTURE_FLAC",   "FLAC");
/**
 * Capture pattern for an Ogg Theora logical stream.
 * 
 * @access  private
 */
define("OGG_STREAM_CAPTURE_THEORA", "theora");
/**
 * Error thrown if the file location passed is nonexistant or unreadable.
 * 
 * @access  private
 */
define("OGG_ERROR_INVALID_FILE", 1);
/**
 * Error thrown if the user attempts to extract an unsupported logical stream.
 * 
 * @access  private
 */
define("OGG_ERROR_UNSUPPORTED",  2);
/**
 * Error thrown if the user attempts to extract an logical stream with no
 * corresponding serial number.
 * 
 * @access  private
 */
define("OGG_ERROR_BAD_SERIAL",   3);
/**
 * Error thrown if the stream appears to be corrupted.
 * 
 * @access  private
 */
define("OGG_ERROR_UNDECODABLE",      4);
require_once('PEAR.php');
require_once('Exception.php');
require_once('Ogg/Bitstream.php');
require_once("Ogg/Flac.php");
require_once("Ogg/Speex.php");
require_once("Ogg/Theora.php");
require_once("Ogg/Vorbis.php");
/**
 * Class for parsing a ogg bitstream.
 *
 * This class provides a means to access several types of logical bitstreams (e.g. Vorbis)
 * within a Ogg physical bitstream.
 *
 * @link    http://www.xiph.org/ogg/doc/
 * @package File_Ogg
 */
class File_Ogg
{
    /**
     * File pointer to Ogg container.
     *
     * This is the file pointer used for extracting data from the Ogg stream.  It is
     * the result of a standard fopen call.
     *
     * @var     pointer
     * @access  private
     */
    var $_filePointer;
    /**
     * The container for all logical streams.
     *
     * List of all of the unique streams in the Ogg physical stream.  The key
     * used is the unique serial number assigned to the logical stream by the
     * encoding application.
     *
     * @var     array
     * @access  private
     */
    var $_streamList = array();
    var $_streams = array();
    /**
     * Length in seconds of each stream group
     */
    var $_groupLengths = array();
    /**
     * Total length in seconds of the entire file
     */
    var $_totalLength;
    /**
     * Returns an interface to an Ogg physical stream.
     *
     * This method takes the path to a local file and examines it for a physical
     * ogg bitsream.  After instantiation, the user should query the object for
     * the logical bitstreams held within the ogg container.
     *
     * @access  public
     * @param   string  $fileLocation   The path of the file to be examined.
     */
    function File_Ogg($fileLocation)
    {
        clearstatcache();
        if (! file_exists($fileLocation)) {
            throw new PEAR_Exception("Couldn't Open File.  Check File Path.", OGG_ERROR_INVALID_FILE);
        }
        // Open this file as a binary, and split the file into streams.
        $this->_filePointer = fopen($fileLocation, "rb");
        if (!is_resource($this->_filePointer))
            throw new PEAR_Exception("Couldn't Open File.  Check File Permissions.", OGG_ERROR_INVALID_FILE);
        // Check for a stream at the start
        $magic = fread($this->_filePointer, strlen(OGG_CAPTURE_PATTERN));
        if ($magic !== OGG_CAPTURE_PATTERN) {
            throw new PEAR_Exception("Couldn't read file: Incorrect magic number.", OGG_ERROR_UNDECODABLE);
        }
        fseek($this->_filePointer, 0, SEEK_SET);
        $this->_splitStreams();
        fclose($this->_filePointer);
    }
    /**
     * Little-endian equivalent for bin2hex
     * @static
     */
    static function _littleEndianBin2Hex( $bin ) {
        $bigEndian = bin2hex( $bin );
        // Reverse entire string
        $reversed = strrev( $bigEndian );
        // Swap nibbles back
        for ( $i = 0, $j = strlen ( $bigEndian ); $i < $j; $i += 2 ) {
            $temp = $reversed[$i];
            $reversed[$i] = $reversed[$i+1];
            $reversed[$i+1] = $temp;
        }
        return $reversed;
    }
    /**
     * Read a binary structure from a file. An array of unsigned integers are read.
     * Large integers are upgraded to floating point on overflow.
     *
     * Format is big-endian as per Theora bit packing convention, this function 
     * won't work for Vorbis.
     *
     * @param   resource    $file
     * @param   array       $fields Associative array mapping name to length in bits
     */
    static function _readBigEndian($file, $fields) 
    {
        $bufferLength = ceil(array_sum($fields) / 8);
        $buffer = fread($file, $bufferLength);
        if (strlen($buffer) != $bufferLength) {
            throw new PEAR_Exception('Unexpected end of file', OGG_ERROR_UNDECODABLE);
        }
        $bytePos = 0;
        $bitPos = 0;
        $byteValue = ord($buffer[0]);
        $output = array();
        foreach ($fields as $name => $width) {
            if ($width % 8 == 0 && $bitPos == 0) {
                // Byte aligned case
                $bytes = $width / 8;
                $endBytePos = $bytePos + $bytes;
                $value = 0;
                while ($bytePos < $endBytePos) {
                    $value = ($value * 256) + ord($buffer[$bytePos]);
                    $bytePos++;
                }
                if ($bytePos < strlen($buffer)) {
                    $byteValue = ord($buffer[$bytePos]);
                }
            } else {
                // General case
                $bitsRemaining = $width;
                $value = 0;
                while ($bitsRemaining > 0) {
                    $bitsToRead = min($bitsRemaining, 8 - $bitPos);
                    $byteValue <<= $bitsToRead;
                    $overflow = ($byteValue & 0xff00) >> 8;
                    $byteValue &= $byteValue & 0xff;
                    $bitPos += $bitsToRead;
                    $bitsRemaining -= $bitsToRead;
                    $value += $overflow * pow(2, $bitsRemaining);
                    if ($bitPos >= 8) {
                        $bitPos = 0;
                        $bytePos++;
                        if ($bitsRemaining <= 0) {
                            break;
                        }
                        $byteValue = ord($buffer[$bytePos]);
                    }
                }
            }
            $output[$name] = $value;
            assert($bytePos <= $bufferLength);
        }
        return $output;
    }
    /**
     * Read a binary structure from a file. An array of unsigned integers are read.
     * Large integers are upgraded to floating point on overflow.
     *
     * Format is little-endian as per Vorbis bit packing convention.
     *
     * @param   resource    $file
     * @param   array       $fields Associative array mapping name to length in bits
     */
    static function _readLittleEndian( $file, $fields ) {
        $bufferLength = ceil(array_sum($fields) / 8);
        $buffer = fread($file, $bufferLength);
        if (strlen($buffer) != $bufferLength) {
            throw new PEAR_Exception('Unexpected end of file', OGG_ERROR_UNDECODABLE);
        }
        $bytePos = 0;
        $bitPos = 0;
        $byteValue = ord($buffer[0]) << 8;
        $output = array();
        foreach ($fields as $name => $width) {
            if ($width % 8 == 0 && $bitPos == 0) {
                // Byte aligned case
                $bytes = $width / 8;
                $value = 0;
                for ($i = 0; $i < $bytes; $i++, $bytePos++) {
                    $value += pow(256, $i) * ord($buffer[$bytePos]);
                }
                if ($bytePos < strlen($buffer)) {
                    $byteValue = ord($buffer[$bytePos]) << 8;
                }
            } else {
                // General case
                $bitsRemaining = $width;
                $value = 0;
                while ($bitsRemaining > 0) {
                    $bitsToRead = min($bitsRemaining, 8 - $bitPos);
                    $byteValue >>= $bitsToRead;
                    $overflow = ($byteValue & 0xff) >> (8 - $bitsToRead);
                    $byteValue &= 0xff00;
                    $value += $overflow * pow(2, $width - $bitsRemaining);
                    $bitPos += $bitsToRead;
                    $bitsRemaining -= $bitsToRead;
                    if ($bitPos >= 8) {
                        $bitPos = 0;
                        $bytePos++;
                        if ($bitsRemaining <= 0) {
                            break;
                        }
                        $byteValue = ord($buffer[$bytePos]) << 8;
                    }
                }
            }
            $output[$name] = $value;
            assert($bytePos <= $bufferLength);
        }
        return $output;
    }
    /**
     * @access  private
     */
    function _decodePageHeader($pageData, $pageOffset, $groupId)
    {
        // Extract the various bits and pieces found in each packet header.
        if (substr($pageData, 0, 4) != OGG_CAPTURE_PATTERN)
            return (false);
        $stream_version = unpack("C1data", substr($pageData, 4, 1));
        if ($stream_version['data'] != 0x00)
            return (false);
        $header_flag     = unpack("Cdata", substr($pageData, 5, 1));
        // Exact granule position
        $abs_granule_pos = self::_littleEndianBin2Hex( substr($pageData, 6, 8));
        // Approximate (floating point) granule position
        $pos = unpack("Va/Vb", substr($pageData, 6, 8));
        $approx_granule_pos = $pos['a'] + $pos['b'] * pow(2, 32);
        // Serial number for the current datastream.
        $stream_serial   = unpack("Vdata", substr($pageData, 14, 4));
        $page_sequence   = unpack("Vdata", substr($pageData, 18, 4));
        $checksum        = unpack("Vdata", substr($pageData, 22, 4));
        $page_segments   = unpack("Cdata", substr($pageData, 26, 1));
        $segments_total  = 0;
        for ($i = 0; $i < $page_segments['data']; ++$i) {
            $segment_length = unpack("Cdata", substr($pageData, 26 + ($i + 1), 1));
            $segments_total += $segment_length['data'];
        }
        $pageFinish = $pageOffset + 27 + $page_segments['data'] + $segments_total;
        $page = array(
            'stream_version'        => $stream_version['data'],
            'header_flag'           => $header_flag['data'],
            'abs_granule_pos'       => $abs_granule_pos,
            'approx_granule_pos'    => $approx_granule_pos,
            'checksum'              => sprintf("%u", $checksum['data']),
            'segments'              => $page_segments['data'],
            'head_offset'           => $pageOffset,
            'body_offset'           => $pageOffset + 27 + $page_segments['data'],
            'body_finish'           => $pageFinish,
            'data_length'           => $pageFinish - $pageOffset,
            'group'                 => $groupId,
        );
        $this->_streamList[$stream_serial['data']]['stream_page'][$page_sequence['data']] = $page;
        return $page;
    }
    
    /**
     *  @access         private
     */
    function _splitStreams()
    {
        // Loop through the physical stream until there are no more pages to read.
        $groupId = 0;
        $openStreams = 0;
        $this_page_offset = 0;
        while (!feof($this->_filePointer)) {
            $pageData = fread($this->_filePointer, 282);
            if (strval($pageData) === '') {
                break;
            }
            $page = $this->_decodePageHeader($pageData, $this_page_offset, $groupId);
            if ($page === false) {
                throw new PEAR_Exception("Cannot decode Ogg file: Invalid page at offset $this_page_offset", OGG_ERROR_UNDECODABLE);
            }
            // Keep track of multiplexed groups
            if ($page['header_flag'] & 2/*bos*/) {
                $openStreams++;
            } elseif ($page['header_flag'] & 4/*eos*/) {
                $openStreams--;
                if (!$openStreams) {
                    // End of group
                    $groupId++;
                }
            }
            if ($openStreams < 0) {
                throw new PEAR_Exception("Unexpected end of stream", OGG_ERROR_UNDECODABLE);
            }
            $this_page_offset = $page['body_finish'];
            fseek($this->_filePointer, $this_page_offset, SEEK_SET);
        }
        // Loop through the streams, and find out what type of stream is available.
        $groupLengths = array();
        foreach ($this->_streamList as $stream_serial => $pages) {
            fseek($this->_filePointer, $pages['stream_page'][0]['body_offset'], SEEK_SET);
            $pattern = fread($this->_filePointer, 8);
            if (preg_match("/" . OGG_STREAM_CAPTURE_VORBIS . "/", $pattern)) {
                $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_VORBIS;
                $stream = new File_Ogg_Vorbis($stream_serial, $pages['stream_page'], $this->_filePointer);
            } elseif (preg_match("/" . OGG_STREAM_CAPTURE_SPEEX . "/", $pattern)) {
                $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_SPEEX;
                $stream = new File_Ogg_Speex($stream_serial, $pages['stream_page'], $this->_filePointer);
            } elseif (preg_match("/" . OGG_STREAM_CAPTURE_FLAC . "/", $pattern)) {
                $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_FLAC;
                $stream = new File_Ogg_Flac($stream_serial, $pages['stream_page'], $this->_filePointer);
            } elseif (preg_match("/" . OGG_STREAM_CAPTURE_THEORA . "/", $pattern)) {
                $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_THEORA;
                $stream = new File_Ogg_Theora($stream_serial, $pages['stream_page'], $this->_filePointer);
            } else {
                $pages['stream_type'] = "unknown";
                $stream = false;
            }
            if ($stream) {
                $this->_streams[$stream_serial] = $stream;
                $group = $pages['stream_page'][0]['group'];
                if (isset($groupLengths[$group])) {
                    $groupLengths[$group] = max($groupLengths[$group], $stream->getLength());
                } else {
                    $groupLengths[$group] = $stream->getLength();
                }
            }
        }
        $this->_groupLengths = $groupLengths;
        $this->_totalLength = array_sum( $groupLengths );
        unset($this->_streamList);
    }
    
    /**
     * Returns the overead percentage used by the Ogg headers.
     * 
     * This function returns the percentage of the total stream size
     * used for Ogg headers.
     *
     * @return float
     */
    function getOverhead() {
        $header_size    = 0;
        $stream_size    = 0;
        foreach ($this->_streams as $serial => $stream) {
            foreach ($stream->_streamList as $offset => $stream_data) {
                $header_size += $stream_data['body_offset'] - $stream_data['head_offset'];
                $stream_size  = $stream_data['body_finish'];
            }
        }
        return sprintf("%0.2f", ($header_size / $stream_size) * 100);
    }
    
    /**
     * Returns the appropriate logical bitstream that corresponds to the provided serial.
     *
     * This function returns a logical bitstream contained within the Ogg physical
     * stream, corresponding to the serial used as the offset for that bitstream.
     * The returned stream may be Vorbis, Speex, FLAC or Theora, although the only
     * usable bitstream is Vorbis.
     *
     * @return File_Ogg_Bitstream
     */
    function &getStream($streamSerial)
    {
        if (! array_key_exists($streamSerial, $this->_streams))
                throw new PEAR_Exception("The stream number is invalid.", OGG_ERROR_BAD_SERIAL);
        return $this->_streams[$streamSerial];
    }
    
    /**
     * This function returns true if a logical bitstream of the requested type can be found.
     *
     * This function checks the contents of this ogg physical bitstream for of logical
     * bitstream corresponding to the supplied type.  If one is found, the function returns
     * true, otherwise it return false.
     *
     * @param   int     $streamType
     * @return  boolean
     */
    function hasStream($streamType)
    {
        foreach ($this->_stream as $stream) {
            if ($stream['stream_type'] == $streamType)
                return (true);
        }
        return (false);
    }
    
    /**
     * Returns an array of logical streams inside this physical bitstream.
     *
     * This function returns an array of logical streams found within this physical
     * bitstream.  If a filter is provided, only logical streams of the requested type
     * are returned, as an array of serial numbers.  If no filter is provided, this
     * function returns a two-dimensional array, with the stream type as the primary key,
     * and a value consisting of an array of stream serial numbers.
     *
     * @param  int      $filter
     * @return array
     */
    function listStreams($filter = null)
    {
        $streams = array();
        // Loops through the streams and assign them to an appropriate index,
        // ready for filtering the second part of this function.
        foreach ($this->_streams as $serial => $stream) {
            $stream_type = 0;
            switch (get_class($stream)) {
                case "file_ogg_flac":
                    $stream_type = OGG_STREAM_FLAC;
                    break;
                case "file_ogg_speex":
                    $stream_type = OGG_STREAM_SPEEX;
                    break;
                case "file_ogg_theora":
                    $stream_type = OGG_STREAM_THEORA;
                    break;
                case "file_ogg_vorbis":
                    $stream_type = OGG_STREAM_VORBIS;
                    break;
            }
            if (! isset($streams[$stream_type]))
                // Initialise the result list for this stream type.
                $streams[$stream_type] = array();
                        
            $streams[$stream_type][] = $serial;
        }
        // Perform filtering.
        if (is_null($filter))
            return ($streams);
        elseif (isset($streams[$filter]))
            return ($streams[$filter]);
        else
            return array();
    }
    /**
     * Get the total length of the group of streams
     */
    function getLength() {
        return $this->_totalLength;
    }
}
?>