diff --git a/src/ID3/Object.php b/src/ID3/Object.php index bed2cfe..4d05fb5 100644 --- a/src/ID3/Object.php +++ b/src/ID3/Object.php @@ -63,8 +63,7 @@ abstract class ID3_Object private $_options; /** - * Constructs the class with given parameters and reads object related data - * from the ID3v2 tag. + * Constructs the class with given parameters. * * @param Reader $reader The reader object. * @param Array $options The options array. diff --git a/src/MPEG/Audio.php b/src/MPEG/Audio.php new file mode 100644 index 0000000..05e4ac8 --- /dev/null +++ b/src/MPEG/Audio.php @@ -0,0 +1,353 @@ + + * @author Sven Vollbehr + * @copyright Copyright (c) 2008 The PHP Reader Project Workgroup + * @license http://code.google.com/p/php-reader/wiki/License New BSD License + * @version $Rev: 1 $ + */ +final class MPEG_Audio extends MPEG_Audio_Object +{ + /** @var integer */ + private $_bytes; + + /** @var Array */ + private $_frames = array(); + + /** @var MPEG_Audio_XINGHeader */ + private $_xingHeader = null; + + /** @var MPEG_Audio_LAMEHeader */ + private $_lameHeader = null; + + /** @var MPEG_Audio_VBRIHeader */ + private $_vbriHeader = null; + + /** @var integer */ + private $_cumulativeBitrate = 0; + + /** @var integer */ + private $_cumulativePlayDuration = 0; + + /** @var integer */ + private $_estimatedBitrate = 0; + + /** @var integer */ + private $_estimatedPlayDuration = 0; + + /** @var integer */ + private $_lastFrameOffset = false; + + + /** + * Constructs the MPEG_Audio class with given file and options. + * + * The following options are currently recognized: + * o readmode -- Can be one of full or lazy and determines when the read of + * frames and their data happens. When in full mode the data is read + * automatically during the instantiation of the frame and all the frames + * are read during the instantiation of this class. While this allows + * faster validation and data fetching, it is unnecessary in terms of + * determining just the play duration of the file. Defaults to lazy. + * + * o estimatePrecision -- Only applicaple with lazy read mode to determine + * the precision of play duration estimate. This precision is equal to how + * many frames are read before fixing the average bitrate that is used to + * calculate the play duration estimate of the whole file. Each frame adds + * about 0.1-0.2ms to the processing of the file. Defaults to 1000. + * + * When in lazy data reading mode it is first checked whether a VBR header is + * found in a file. If so, the play duration is calculated based no its data + * and no further frames are read from the file. If no VBR header is found, + * frames up to estimatePrecision are read to calculate an average bitrate. + * + * Hence, only zero or estimatePrecision number of frames are read + * in lazy data reading mode. The rest of the frames are read automatically + * when directly referenced, ie the data is read when it is needed. + * + * @param string|Reader $filename The path to the file, file descriptor of an + * opened file, or {@link Reader} instance. + * @param Array $options The options array. + */ + public function __construct($filename, $options = array()) + { + if ($filename instanceof Reader) + $reader = &$filename; + else + $reader = new Reader($filename); + + parent::__construct($reader, $options); + + $offset = $this->_reader->getOffset(); + $this->_bytes = $this->_reader->getSize(); + + /* Skip ID3v1 tag */ + $this->_reader->setOffset(-128); + if ($this->_reader->read(3) == "TAG") + $this->_bytes -= 128; + $this->_reader->setOffset($offset); + + /* Skip ID3v2 tag */ + if ($this->_reader->readString8(3) == "ID3") { + require_once("ID3/Header.php"); + $header = new ID3_Header($this->_reader); + $this->_reader->skip + ($header->getSize() + ($header->hasFlag(ID3_Header::FOOTER) ? 10 : 0)); + } + else + $this->_reader->setOffset($offset); + + + $offset = $this->_reader->getOffset(); + + /* Check for VBR headers */ + $firstFrame = new MPEG_Audio_Frame($this->_reader, $options); + + $this->_reader->setOffset + ($offset + 4 + self::$sidesizes + [$firstFrame->getFrequencyType()][$firstFrame->getMode()]); + if (($xing = $this->_reader->readString8(4)) == "Xing" || $xing == "Info") { + require_once("MPEG/Audio/XINGHeader.php"); + $this->_xingHeader = new MPEG_Audio_XINGHeader($this->_reader, $options); + if ($this->_reader->readString8(4) == "LAME") { + require_once("MPEG/Audio/LAMEHeader.php"); + $this->_lameHeader = + new MPEG_Audio_LAMEHeader($this->_reader, $options); + } + } + + $this->_reader->setOffset($offset + 4 + 32); + if ($this->_reader->readString8(4) == "VBRI") { + require_once("MPEG/Audio/VBRIHeader.php"); + $this->_vbriHeader = new MPEG_Audio_VBRIHeader($this->_reader, $options); + } + + $this->_reader->setOffset($offset); + + /* Read necessary frames */ + if ($this->getOption("readmode", "lazy") == "lazy") { + if (($header = $this->_xingHeader) !== null || + ($header = $this->_vbriHeader) !== null) { + $this->_estimatedPlayDuration = $header->getFrames() * + $firstFrame->getSamples() / $firstFrame->getSamplingFrequency(); + if ($this->_lameHeader !== null) { + $this->_estimatedBitrate = $this->_lameHeader->getBitrate(); + if ($this->_estimatedBitrate == 255) + $this->_estimatedBitrate = round + (($this->_lameHeader->getMusicLength()) / + (($header->getFrames() * $firstFrame->getSamples()) / + $firstFrame->getSamplingFrequency()) / 1000 * 8); + } + else + $this->_estimatedBitrate = ($this->_bytes - $offset) / + $this->_estimatedPlayDuration / 1000 * 8; + } + else { + $this->_readFrames($this->getOption("estimatePrecision", 1000)); + + $this->_estimatedBitrate = + $this->_cumulativeBitrate / count($this->_frames); + $this->_estimatedPlayDuration = + ($this->_bytes - $offset) / ($this->_estimatedBitrate * 1000 / 8); + } + } + else { + $this->_readFrames(); + + $this->_estimatedBitrate = + $this->_cumulativeBitrate / count($this->_frames); + $this->_estimatedPlayDuration = $this->_cumulativePlayDuration; + } + } + + /** + * Returns the bitrate estimate. This value is either fetched from one of the + * headers or calculated based on the read frames. + * + * @return integer + */ + public function getBitrateEstimate() + { + return $this->_estimatedBitrate; + } + + /** + * For variable bitrate files this method returns the exact average bitrate of + * the whole file. + * + * @return integer + */ + public function getBitrate() + { + if ($this->getOption("readmode", "lazy") == "lazy") + $this->_readFrames(); + return $this->_cumulativeBitrate / count($this->_frames); + } + + /** + * Returns the playtime estimate, in seconds. + * + * @return integer + */ + public function getLengthEstimate() + { + return $this->_estimatedPlayDuration; + } + + /** + * Returns the exact playtime in seconds. In lazy reading mode the frames are + * read from the file the first time you call this method to get the exact + * playtime of the file. + * + * @return integer + */ + public function getLength() + { + if ($this->getOption("readmode", "lazy") == "lazy") + $this->_readFrames(); + return $this->_cumulativePlayDuration; + } + + /** + * Returns the playtime estimate as a string in the form of + * [hours]:minutes:seconds.milliseconds. + * + * @param integer $seconds The playtime in seconds. + * @return string + */ + public function getFormattedLengthEstimate() + { + return $this->_formatTime($this->getLengthEstimate()); + } + + /** + * Returns the exact playtime given in seconds as a string in the form of + * [hours]:minutes:seconds.milliseconds. In lazy reading mode the frames are + * read from the file the first time you call this method to get the exact + * playtime of the file. + * + * @param integer $seconds The playtime in seconds. + * @return string + */ + public function getFormattedLength() + { + return $this->_formatTime($this->getLength()); + } + + /** + * Formats given time in seconds into the form of + * [hours]:minutes:seconds.milliseconds. + * + * @param integer $seconds The time to format, in seconds + * @return string + */ + private function _formatTime($seconds) + { + $milliseconds = round(($seconds - floor($seconds)) * 1000); + $seconds = floor($seconds); + $minutes = floor($seconds / 60); + $hours = floor($minutes / 60); + return + ($minutes > 0 ? + ($hours > 0 ? $hours . ":" . + str_pad($minutes % 60, 2, "0", STR_PAD_LEFT) : $minutes % 60) . ":" . + str_pad($seconds % 60, 2, "0", STR_PAD_LEFT) : $seconds % 60) . "." . + str_pad($milliseconds, 3, "0", STR_PAD_LEFT); + } + + /** + * Returns all the frames of the audio bitstream as an array. In lazy reading + * mode the frames are read from the file the first time you call this method. + * + * @return Array + */ + public function getFrames() + { + if ($this->getOption("readmode", "lazy") == "lazy" && + $this->_frames === false) { + $this->_readFrames(); + } + return $this->_frames; + } + + /** + * Reads frames up to given limit. If called subsequently the method continues + * after the last frame read in the last call, again to read up to the limit + * or just the rest of the frames. + * + * @param integer $limit The maximum number of frames read from the bitstream + */ + private function _readFrames($limit = false) + { + if ($this->_lastFrameOffset !== false) + $this->_reader->setOffset($this->_lastFrameOffset); + + for ($i = 0; $this->_reader->getOffset() < $this->_bytes; $i++) { + $frame = new MPEG_Audio_Frame($this->_reader, $options); + + $this->_cumulativePlayDuration += + (double)($frame->getLength() / ($frame->getBitrate() * 1000 / 8)); + $this->_cumulativeBitrate += $frame->getBitrate(); + $this->_frames[] = $frame; + + if ($limit !== false && $i == $limit) { + $this->_lastFrameOffset = $this->_reader->getOffset(); + break; + } + } + } +} diff --git a/src/MPEG/Audio/Frame.php b/src/MPEG/Audio/Frame.php new file mode 100644 index 0000000..e6c16ab --- /dev/null +++ b/src/MPEG/Audio/Frame.php @@ -0,0 +1,507 @@ + + * @author Sven Vollbehr + * @copyright Copyright (c) 2008 The PHP Reader Project Workgroup + * @license http://code.google.com/p/php-reader/wiki/License New BSD License + * @version $Rev: 1 $ + */ +class MPEG_Audio_Frame extends MPEG_Audio_Object +{ + /** + * The bitrate lookup table. The table has the following format. + * + * + * array ( + * SAMPLING_FREQUENCY_HIGH | SAMPLING_FREQUENCY_LOW => array ( + * LAYER_ONE | LAYER_TWO | LAYER_TREE => array ( ) + * ) + * ) + * + * + * @var Array + */ + private static $bitrates = array ( + self::SAMPLING_FREQUENCY_HIGH => array ( + self::LAYER_ONE => array ( + 1 => 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448 + ), + self::LAYER_TWO => array ( + 1 => 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384 + ), + self::LAYER_THREE => array ( + 1 => 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320 + ) + ), + self::SAMPLING_FREQUENCY_LOW => array ( + self::LAYER_ONE => array ( + 1 => 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256 + ), + self::LAYER_TWO => array ( + 1 => 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 + ), + self::LAYER_THREE => array ( + 1 => 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 + ) + ) + ); + + /** + * Sample rate lookup table. The table has the following format. + * + * + * array ( + * LAYER_ONE | LAYER_TWO | LAYER_TREE => array ( ) + * ) + * + * + * @var Array + */ + private static $samplingFrequencies = array ( + self::VERSION_ONE => array (44100, 48000, 32000), + self::VERSION_TWO => array (22050, 24000, 16000), + self::VERSION_TWO_FIVE => array (11025, 12000, 8000) + ); + + /** + * Samples per frame lookup table. The table has the following format. + * + * + * array ( + * SAMPLING_FREQUENCY_HIGH | SAMPLING_FREQUENCY_LOW => array ( + * LAYER_ONE | LAYER_TWO | LAYER_TREE => + * ) + * ) + * + * + * @var Array + */ + private static $samples = array ( + self::SAMPLING_FREQUENCY_HIGH => array ( + self::LAYER_ONE => 384, + self::LAYER_TWO => 1152, + self::LAYER_THREE => 1152), + self::SAMPLING_FREQUENCY_LOW => array ( + self::LAYER_ONE => 384, + self::LAYER_TWO => 1152, + self::LAYER_THREE => 576)); + + /** + * Coefficient lookup table. The table has the following format. + * + * + * array ( + * SAMPLING_FREQUENCY_HIGH | SAMPLING_FREQUENCY_LOW => array ( + * LAYER_ONE | LAYER_TWO | LAYER_TREE => array ( ) + * ) + * ) + * + * + * @var Array + */ + private static $coefficients = array ( + self::SAMPLING_FREQUENCY_HIGH => array ( + self::LAYER_ONE => 12, self::LAYER_TWO => 144, self::LAYER_THREE => 144 + ), + self::SAMPLING_FREQUENCY_LOW => array ( + self::LAYER_ONE => 12, self::LAYER_TWO => 144, self::LAYER_THREE => 72 + ) + ); + + /** + * Slot size per layer lookup table. The table has the following format. + * + * + * array ( + * LAYER_ONE | LAYER_TWO | LAYER_TREE => + * ) + * + * + * + * @var Array + */ + private static $slotsizes = array ( + self::LAYER_ONE => 4, self::LAYER_TWO => 1, self::LAYER_THREE => 1 + ); + + + /** @var integer */ + private $_offset; + + /** @var integer */ + private $_version; + + /** @var integer */ + private $_frequencyType; + + /** @var integer */ + private $_layer; + + /** @var integer */ + private $_redundancy; + + /** @var integer */ + private $_bitrate; + + /** @var integer */ + private $_samplingFrequency; + + /** @var integer */ + private $_padding; + + /** @var integer */ + private $_mode; + + /** @var integer */ + private $_modeExtension; + + /** @var integer */ + private $_copyright; + + /** @var integer */ + private $_original; + + /** @var integer */ + private $_emphasis; + + /** @var integer */ + private $_length; + + /** @var integer */ + private $_samples; + + /** @var integer */ + private $_crc = false; + + /** @var string */ + private $_data = false; + + /** + * Constructs the class with given parameters and reads object related data + * from the MPEG frame. + * + * @param Reader $reader The reader object. + * @param Array $options Array of options. + */ + public function __construct($reader, &$options = array()) + { + parent::__construct($reader, $options); + + $this->_offset = $this->_reader->getOffset(); + + $header = Transform::fromUInt32BE($this->_reader->read(4)); + $this->_version = Twiddling::getValue($header, 19, 20); + $this->_frequencyType = Twiddling::testBit($header, 19); + $this->_layer = Twiddling::getValue($header, 17, 18); + $this->_redundancy = !Twiddling::testBit($header, 16); + $this->_bitrate = isset + (self::$bitrates[$this->_frequencyType][$this->_layer] + [$index = Twiddling::getValue($header, 12, 15)]) ? + self::$bitrates[$this->_frequencyType][$this->_layer][$index] : false; + $this->_samplingFrequency = isset + (self::$samplingFrequencies[$this->_version] + [$index = Twiddling::getValue($header, 10, 11)]) ? + self::$samplingFrequencies[$this->_version][$index] : false; + $this->_padding = Twiddling::testBit($header, 9); + $this->_mode = Twiddling::getValue($header, 6, 7); + $this->_modeExtension = Twiddling::getValue($header, 4, 5); + $this->_copyright = Twiddling::testBit($header, 3); + $this->_original = Twiddling::testBit($header, 2); + $this->_emphasis = Twiddling::getValue($header, 0, 1); + + $this->_length = (int) + ((self::$coefficients[$this->_frequencyType][$this->_layer] * + ($this->_bitrate * 1000) / $this->_samplingFrequency) + + ($this->_padding ? 1 : 0)) * self::$slotsizes[$this->_layer]; + $this->_samples = self::$samples[$this->_frequencyType][$this->_layer]; + + if ($this->getOption("readmode", "lazy") == "lazy") + $this->_reader->skip($this->_length - 4); + else { // full + $this->_readCrc(); + $this->_readData(); + } + } + + /** + * Returns the version identifier of the algorithm. + * + * @see VERSION_ONE, VERSION_TWO, VERSION_TWO_FIVE + * @return integer + */ + public function getVersion() { return $this->_version; } + + /** + * Returns the sampling frequency type. This can be one of the following + * values. + * + * o {@link SAMPLING_FREQUENCY_HIGH} -- Higher Sampling Frequency + * (Version 1) + * o {@link SAMPLING_FREQUENCY_LOW} -- Lower Sampling Frequency + * (Version 2 and 2.5) + * + * @see SAMPLING_FREQUENCY_LOW, SAMPLING_FREQUENCY_HIGH + * @return integer + */ + public function getFrequencyType() { return $this->_frequencyType; } + + /** + * Returns the type of layer used. + * + * @see LAYER_ONE, LAYER_TWO, LAYER_THREE + * @return integer + */ + public function getLayer() { return $this->_layer; } + + /** + * An alias to getRedundancy(). + * + * @see getRedundancy + * @return boolean + */ + public function hasRedundancy() { return $this->getRedundancy(); } + + /** + * Returns boolean corresponding to whether redundancy has been added in the + * audio bitstream to facilitate error detection and concealment. Equals + * false if no redundancy has been added, true if + * redundancy has been added. + * + * @return boolean + */ + public function getRedundancy() { return $this->_redundancy; } + + /** + * Returns the bitrate in kbps. The returned value indicates the total bitrate + * irrespective of the mode (stereo, joint_stereo, dual_channel, + * single_channel). + * + * @return integer + */ + public function getBitrate() { return $this->_bitrate; } + + /** + * Returns the sampling frequency in Hz. + * + * @return integer + */ + public function getSamplingFrequency() { return $this->_samplingFrequency; } + + /** + * An alias to getPadding(). + * + * @see getPadding + * @return boolean + */ + public function hasPadding() { return $this->getPadding(); } + + /** + * Returns boolean corresponding the frame contains an additional slot to + * adjust the mean bitrate to the sampling frequency. Equals to + * true if padding has been added, false otherwise. + * + * Padding is only necessary with a sampling frequency of 44.1kHz. + * + * @return boolean + */ + public function getPadding() { return $this->_padding; } + + /** + * Returns the mode. In Layer I and II the CHANNEL_JOINT_STEREO mode is + * intensity_stereo, in Layer III it is intensity_stereo and/or ms_stereo. + * + * @see CHANNEL_STEREO, CHANNEL_JOINT_STEREO, CHANNEL_DUAL_CHANNEL, + * CHANNEL_SINGLE_CHANNEL + * @return integer + */ + public function getMode() { return $this->_mode; } + + /** + * Returns the mode extension used in CHANNEL_JOINT_STEREO mode. + * + * In Layer I and II the return type indicates which subbands are in + * intensity_stereo. All other subbands are coded in stereo. + * + * o {@link MODE_SUBBAND_4_TO_31} -- subbands 4-31 in + * intensity_stereo, bound==4 + * o {@link MODE_SUBBAND_8_TO_31} -- subbands 8-31 in + * intensity_stereo, bound==8 + * o {@link MODE_SUBBAND_12_TO_31} -- subbands 12-31 in + * intensity_stereo, bound==12 + * o {@link MODE_SUBBAND_16_TO_31} -- subbands 16-31 in + * intensity_stereo, bound==16 + * + * In Layer III they indicate which type of joint stereo coding method is + * applied. The frequency ranges over which the intensity_stereo and ms_stereo + * modes are applied are implicit in the algorithm. Please see + * {@link MODE_ISOFF_MSSOFF}, {@link MODE_ISON_MSSOFF}, + * {@link MODE_ISOFF_MSSON}, and {@link MODE_ISON_MSSON}. + * + * @return integer + */ + public function getModeExtension() { return $this->_modeExtension; } + + /** + * An alias to getCopyright(). + * + * @see getCopyright + * @return boolean + */ + public function hasCopyright() { return $this->getCopyright(); } + + /** + * Returns true if the coded bitstream is copyright protected, + * false otherwise. + * + * @return boolean + */ + public function getCopyright() { return $this->_copyright; } + + /** + * An alias to getOriginal(). + * + * @see getOriginal + * @return boolean + */ + public function isOriginal() { return $this->getOriginal(); } + + /** + * Returns whether the bitstream is original or home made. + * + * @return boolean + */ + public function getOriginal() { return $this->_original; } + + /** + * Returns the type of de-emphasis that shall be used. The value is one of the + * following. + * + * o {@link EMPHASIS_NONE} -- No emphasis + * o {@link EMPHASIS_50_15} -- 50/15 microsec. emphasis + * o {@link EMPHASIS_CCIT_J17} -- CCITT J.17 + * + * @see EMPHASIS_NONE, EMPHASIS_50_15, EMPHASIS_CCIT_J17 + * @return integer + */ + public function getEmphasis() { return $this->_emphasis; } + + /** + * Returns the length of the frame based on the current layer, bit rate, + * sampling frequency and padding, in bytes. + * + * @return integer + */ + public function getLength() { return $this->_length; } + + /** + * Returns the number of samples contained in the frame. + * + * @return integer + */ + public function getSamples() { return $this->_samples; } + + /** + * Returns the 16-bit CRC of the frame or false if not present. + * + * @return integer + */ + public function getCrc() + { + if ($this->getOption("readmode", "lazy") == "lazy" && + $this->hasRedundancy() && $this->_crc === false) { + $this->_readCrc(); + } + return $this->_crc; + } + + /** + * Reads the CRC data. + */ + private function _readCrc() + { + if ($this->hasRedundancy()) { + $offset = $this->_reader->getOffset(); + $this->_reader->setOffset($this->_offset + 4); + $this->_crc = $reader->readUInt16BE(); + $this->_reader->setOffset($offset); + } + } + + /** + * Returns the audio data. + * + * @return string + */ + public function getData() + { + if ($this->getOption("readmode", "lazy") == "lazy" && + $this->_data === false) { + $this->_readData(); + } + return $this->_data; + } + + /** + * Reads the frame data. + */ + private function _readData() + { + $offset = $this->_reader->getOffset(); + $this->_reader->setOffset + ($this->_offset + 4 + ($this->hasRedundancy() ? 2 : 0)); + $this->_data = $this->_reader->read + ($this->getLength() - 4 - ($this->hasRedundancy() ? 2 : 0)); + $this->_reader->setOffset($offset); + } +} diff --git a/src/MPEG/Audio/LAMEHeader.php b/src/MPEG/Audio/LAMEHeader.php new file mode 100644 index 0000000..f5e9df0 --- /dev/null +++ b/src/MPEG/Audio/LAMEHeader.php @@ -0,0 +1,481 @@ + + * @copyright Copyright (c) 2008 The PHP Reader Project Workgroup + * @license http://code.google.com/p/php-reader/wiki/License New BSD License + * @version $Rev: 1 $ + */ +class MPEG_Audio_LAMEHeader extends MPEG_Object +{ + + /** @var integer */ + const VBR_METHOD_CONSTANT = 1; + + /** @var integer */ + const VBR_METHOD_ABR = 2; + + /** @var integer */ + const VBR_METHOD_RH = 3; + + /** @var integer */ + const VBR_METHOD_MTRH = 4; + + /** @var integer */ + const VBR_METHOD_MT = 5; + + /** @var integer */ + const ENCODING_FLAG_NSPSYTUNE = 1; + + /** @var integer */ + const ENCODING_FLAG_NSSAFEJOINT = 2; + + /** @var integer */ + const ENCODING_FLAG_NOGAP_CONTINUED = 4; + + /** @var integer */ + const ENCODING_FLAG_NOGAP_CONTINUATION = 8; + + /** @var integer */ + const MODE_MONO = 0; + + /** @var integer */ + const MODE_STEREO = 1; + + /** @var integer */ + const MODE_DUAL = 2; + + /** @var integer */ + const MODE_JOINT = 3; + + /** @var integer */ + const MODE_FORCE = 4; + + /** @var integer */ + const MODE_AUTO = 5; + + /** @var integer */ + const MODE_INTENSITY = 6; + + /** @var integer */ + const MODE_UNDEFINED = 7; + + /** @var integer */ + const SOURCE_FREQUENCY_32000_OR_LOWER = 0; + + /** @var integer */ + const SOURCE_FREQUENCY_44100 = 1; + + /** @var integer */ + const SOURCE_FREQUENCY_48000 = 2; + + /** @var integer */ + const SOURCE_FREQUENCY_HIGHER = 3; + + /** @var integer */ + const SURROUND_NONE = 0; + + /** @var integer */ + const SURROUND_DPL = 1; + + /** @var integer */ + const SURROUND_DPL2 = 2; + + /** @var integer */ + const SURROUND_AMBISONIC = 3; + + /** @var string */ + private $_version; + + /** @var integer */ + private $_revision; + + /** @var integer */ + private $_vbrMethod; + + /** @var integer */ + private $_lowpass; + + /** @var integer */ + private $_peakSignalAmplitude; + + /** @var integer */ + private $_radioReplayGain; + + /** @var integer */ + private $_audiophileReplayGain; + + /** @var integer */ + private $_encodingFlags; + + /** @var integer */ + private $_athType; + + /** @var integer */ + private $_bitrate; + + /** @var integer */ + private $_encoderDelaySamples; + + /** @var integer */ + private $_paddedSamples; + + /** @var integer */ + private $_sourceSampleFrequency; + + /** @var boolean */ + private $_unwiseSettingsUsed; + + /** @var integer */ + private $_mode; + + /** @var integer */ + private $_noiseShaping; + + /** @var integer */ + private $_mp3Gain; + + /** @var integer */ + private $_surroundInfo; + + /** @var integer */ + private $_presetUsed; + + /** @var integer */ + private $_musicLength; + + /** @var integer */ + private $_musicCrc; + + /** @var integer */ + private $_crc; + + /** + * Constructs the class with given parameters and reads object related data + * from the bitstream. + * + * @param Reader $reader The reader object. + * @param Array $options Array of options. + */ + public function __construct($reader, &$options = array()) + { + parent::__construct($reader, $options); + + $this->_version = $this->_reader->readString8(5); + + $tmp = $this->_reader->readUInt8(); + $this->_revision = Twiddling::getValue($tmp, 4, 8); + $this->_vbrMethod = Twiddling::getValue($tmp, 0, 3); + + $this->_lowpass = $this->_reader->readUInt8() * 100; + + $this->_peakSignalAmplitude = $this->_reader->readUInt32BE(); + + $tmp = $this->_reader->readUInt16BE(); + $this->_radioReplayGain = array( + "name" => Twiddling::getValue($tmp, 0, 2), + "originator" => Twiddling::getValue($tmp, 3, 5), + "absoluteGainAdjustment" => Twiddling::getValue($tmp, 7, 15) / 10 + ); + + $tmp = $this->_reader->readUInt16BE(); + $this->_audiophileReplayGain = array( + "name" => Twiddling::getValue($tmp, 0, 2), + "originator" => Twiddling::getValue($tmp, 3, 5), + "absoluteGainAdjustment" => Twiddling::getValue($tmp, 7, 15) / 10 + ); + + $tmp = $this->_reader->readUInt8(); + $this->_encodingFlags = Twiddling::getValue($tmp, 4, 8); + $this->_athType = Twiddling::getValue($tmp, 0, 3); + + $this->_bitrate = $this->_reader->readUInt8(); + + $tmp = $this->_reader->readUInt32BE(); + // Encoder delay fields + $this->_encoderDelaySamples = Twiddling::getValue($tmp, 20, 31); + $this->_paddedSamples = Twiddling::getValue($tmp, 8, 19); + // Misc field + $this->_sourceSampleFrequency = Twiddling::getValue($tmp, 6, 7); + $this->_unwiseSettingsUsed = Twiddling::testBit($tmp, 5); + $this->_mode = Twiddling::getValue($tmp, 2, 4); + $this->_noiseShaping = Twiddling::getValue($tmp, 0, 1); + + $this->_mp3Gain = pow(2, $this->_reader->readInt8() / 4); + + $tmp = $this->_reader->readUInt16BE(); + $this->_surroundInfo = Twiddling::getValue($tmp, 11, 14); + $this->_presetUsed = Twiddling::getValue($tmp, 0, 10); + + $this->_musicLength = $this->_reader->readUInt32BE(); + + $this->_musicCrc = $this->_reader->readUInt16BE(); + $this->_crc = $this->_reader->readUInt16BE(); + } + + /** + * Returns the version string of the header. + * + * @return string + */ + public function getVersion() { return $this->_version; } + + /** + * Returns the info tag revision. + * + * @return integer + */ + public function getRevision() { return $this->_revision; } + + /** + * Returns the VBR method used for encoding. See the corresponding constants + * for possible return values. + * + * @return integer + */ + public function getVbrMethod() { return $this->_vbrMethod; } + + /** + * Returns the lowpass filter value. + * + * @return integer + */ + public function getLowpass() { return $this->_lowpass; } + + /** + * Returns the peak signal amplitude field of replay gain. The value of 1.0 + * (ie 100%) represents maximal signal amplitude storeable in decoding format. + * + * @return integer + */ + public function getPeakSignalAmplitude() + { + return $this->_peakSignalAmplitude; + } + + /** + * Returns the radio replay gain field of replay gain, required to make all + * tracks equal loudness, as an array that consists of the following keys. + * + * o name -- Specifies the name of the gain adjustment. Can be one of the + * following values: 0 = not set, 1 = radio, or 2 = audiophile. + * + * o originator -- Specifies the originator of the gain adjustment. Can be + * one of the following values: 0 = not set, 1 = set by artist, 2 = set + * by user, 3 = set by my model, 4 = set by simple RMS average. + * + * o absoluteGainAdjustment -- Speficies the absolute gain adjustment. + * + * @return Array + */ + public function getRadioReplayGain() { return $this->_radioReplayGain; } + + /** + * Returns the audiophile replay gain field of replay gain, required to give + * ideal listening loudness, as an array that consists of the following keys. + * + * o name -- Specifies the name of the gain adjustment. Can be one of the + * following values: 0 = not set, 1 = radio, or 2 = audiophile. + * + * o originator -- Specifies the originator of the gain adjustment. Can be + * one of the following values: 0 = not set, 1 = set by artist, 2 = set + * by user, 3 = set by my model, 4 = set by simple RMS average. + * + * o absoluteGainAdjustment -- Speficies the absolute gain adjustment. + * + * @return Array + */ + public function getAudiophileReplayGain() + { + return $this->_audiophileReplayGain; + } + + /** + * Returns the encoding flags. See the corresponding flag constants for + * possible values. + * + * @return integer + */ + public function getEncodingFlags() { return $this->_encodingFlags; } + + /** + * Returns the ATH type. + * + * @return integer + */ + public function getAthType() { return $this->_athType; } + + /** + * Returns the bitrate for CBR encoded files and the minimal birate for + * VBR encoded file. The maximum value of this field is 255 even with higher + * actual bitrates. + * + * @return integer + */ + public function getBitrate() { return $this->_bitrate; } + + /** + * Returns the encoder delay or number of samples added at start. + * + * @return integer + */ + public function getEncoderDelaySamples() + { + return $this->_encoderDelaySamples; + } + + /** + * Returns the number of padded samples to complete the last frame. + * + * @return integer + */ + public function getPaddedSamples() { return $this->_paddedSamples; } + + /** + * Returns the source sample frequency. See corresponding constants for + * possible values. + * + * @return integer + */ + public function getSourceSampleFrequency() + { + return $this->_sourceSampleFrequency; + } + + /** + * An alias to getUnwiseSettingsUsed(). + * + * @see getUnwiseSettingsUsed + * @return boolean + */ + public function areUnwiseSettingsUsed() + { + return $this->getUnwiseSettingsUsed(); + } + + /** + * Returns whether unwise settings were used to encode the file. + * + * @return boolean + */ + public function getUnwiseSettingsUsed() { return $this->_unwiseSettingsUsed; } + + /** + * Returns the stereo mode. See corresponding constants for possible values. + * + * @return integer + */ + public function getMode() { return $this->_mode; } + + /** + * Returns the noise shaping. + * + * @return integer + */ + public function getNoiseShaping() { return $this->_noiseShaping; } + + /** + * Returns the MP3 gain change. Any MP3 can be amplified in a lossless manner. + * If done so, this field can be used to log such transformation happened so + * that any given time it can be undone. + * + * @return integer + */ + public function getMp3Gain() { return $this->_mp3Gain; } + + /** + * Returns the surround info. See corresponding contants for possible values. + * + * @return integer + */ + public function getSurroundInfo() { return $this->_surroundInfo; } + + /** + * Returns the preset used in encoding. + * + * @return integer + */ + public function getPresetUsed() { return $this->_presetUsed; } + + /** + * Returns the exact length in bytes of the MP3 file originally made by LAME + * excluded ID3 tag info at the end. + * + * The first byte it counts is the first byte of this LAME header and the last + * byte it counts is the last byte of the last MP3 frame containing music. + * The value should be equal to file length at the time of LAME encoding, + * except when using ID3 tags. + * + * @return integer + */ + public function getMusicLength() { return $this->_musicLength; } + + /** + * Returns the CRC-16 of the complete MP3 music data as made originally by + * LAME. + * + * @return integer + */ + public function getMusicCrc() { return $this->_musicCrc; } + + /** + * Returns the CRC-16 of the first 190 bytes of the header frame. + * + * @return integer + */ + public function getCrc() { return $this->_crc; } +} diff --git a/src/MPEG/Audio/Object.php b/src/MPEG/Audio/Object.php new file mode 100644 index 0000000..89a4a6d --- /dev/null +++ b/src/MPEG/Audio/Object.php @@ -0,0 +1,167 @@ + + * @author Sven Vollbehr + * @copyright Copyright (c) 2008 The PHP Reader Project Workgroup + * @license http://code.google.com/p/php-reader/wiki/License New BSD License + * @version $Rev: 107 $ + */ +abstract class MPEG_Audio_Object extends MPEG_Object +{ + /** @var integer */ + const VERSION_ONE = 3; + + /** @var integer */ + const VERSION_TWO = 2; + + /** @var integer */ + const VERSION_TWO_FIVE = 0; + + /** @var integer */ + const SAMPLING_FREQUENCY_LOW = 0; + + /** @var integer */ + const SAMPLING_FREQUENCY_HIGH = 1; + + /** @var integer */ + const LAYER_ONE = 3; + + /** @var integer */ + const LAYER_TWO = 2; + + /** @var integer */ + const LAYER_THREE = 1; + + /** @var integer */ + const CHANNEL_STEREO = 0; + + /** @var integer */ + const CHANNEL_JOINT_STEREO = 1; + + /** @var integer */ + const CHANNEL_DUAL_CHANNEL = 2; + + /** @var integer */ + const CHANNEL_SINGLE_CHANNEL = 3; + + /** @var integer */ + const MODE_SUBBAND_4_TO_31 = 0; + + /** @var integer */ + const MODE_SUBBAND_8_TO_31 = 1; + + /** @var integer */ + const MODE_SUBBAND_12_TO_31 = 2; + + /** @var integer */ + const MODE_SUBBAND_16_TO_31 = 3; + + /** @var integer */ + const MODE_ISOFF_MSSOFF = 0; + + /** @var integer */ + const MODE_ISON_MSSOFF = 1; + + /** @var integer */ + const MODE_ISOFF_MSSON = 2; + + /** @var integer */ + const MODE_ISON_MSSON = 3; + + /** @var integer */ + const EMPHASIS_NONE = 0; + + /** @var integer */ + const EMPHASIS_50_15 = 1; + + /** @var integer */ + const EMPHASIS_CCIT_J17 = 3; + + + /** + * Layer III side information size lookup table. The table has the following + * format. + * + * + * array ( + * SAMPLING_FREQUENCY_HIGH | SAMPLING_FREQUENCY_LOW => array ( + * CHANNEL_STEREO | CHANNEL_JOINT_STEREO | CHANNEL_DUAL_CHANNEL | + * CHANNEL_SINGLE_CHANNEL => + * ) + * ) + * + * + * + * @var Array + */ + protected static $sidesizes = array( + self::SAMPLING_FREQUENCY_HIGH => array( + self::CHANNEL_STEREO => 32, + self::CHANNEL_JOINT_STEREO => 32, + self::CHANNEL_DUAL_CHANNEL => 32, + self::CHANNEL_SINGLE_CHANNEL => 17 + ), + self::SAMPLING_FREQUENCY_LOW => array( + self::CHANNEL_STEREO => 17, + self::CHANNEL_JOINT_STEREO => 17, + self::CHANNEL_DUAL_CHANNEL => 17, + self::CHANNEL_SINGLE_CHANNEL => 9 + ) + ); + + + /** + * Constructs the class with given parameters. + * + * @param Reader $reader The reader object. + * @param Array $options The options array. + */ + public function __construct($reader, &$options = array()) + { + parent::__construct($reader, $options); + } +} diff --git a/src/MPEG/Audio/VBRIHeader.php b/src/MPEG/Audio/VBRIHeader.php new file mode 100644 index 0000000..37be03e --- /dev/null +++ b/src/MPEG/Audio/VBRIHeader.php @@ -0,0 +1,165 @@ + + * @author Sven Vollbehr + * @copyright Copyright (c) 2008 The PHP Reader Project Workgroup + * @license http://code.google.com/p/php-reader/wiki/License New BSD License + * @version $Rev: 1 $ + */ +class MPEG_Audio_VBRIHeader extends MPEG_Object +{ + /** @var integer */ + private $_version; + + /** @var integer */ + private $_delay; + + /** @var integer */ + private $_qualityIndicator; + + /** @var integer */ + private $_bytes; + + /** @var integer */ + private $_frames; + + /** @var Array */ + private $_toc = array(); + + /** @var integer */ + private $_tocFramesPerEntry; + + /** @var integer */ + private $_length; + + /** + * Constructs the class with given parameters and reads object related data + * from the bitstream. + * + * @param Reader $reader The reader object. + * @param Array $options Array of options. + */ + public function __construct($reader, &$options = array()) + { + $offset = $this->_reader->getOffset(); + $this->_version = $this->_reader->readUInt16BE(); + $this->_delay = $this->_reader->readUInt16BE(); + $this->_qualityIndicator = $this->_reader->readUInt16BE(); + $this->_bytes = $this->_reader->readUInt32BE(); + $this->_frames = $this->_reader->readUInt32BE(); + $tocEntries = $this->_reader->readUInt16BE(); + $tocEntryScale = $this->_reader->readUInt16BE(); + $tocEntrySize = $this->_reader->readUInt16BE(); + $this->_tocFramesPerEntry = $this->_reader->readUInt16BE(); + $this->_toc = array_merge(unpack(($tocEntrySize == 1) ? "C*" : + ($tocEntrySize == 2) ? "n*" : "N*", + $this->_reader->read($tocCount * $tocEntrySize))); + foreach ($this->_toc as $key => $value) + $this->_toc[$key] = $tocEntryScale * $value; + $this->_length = $this->_reader->getOffset() - $offset; + } + + /** + * Returns the header version. + * + * @return integer + */ + public function getVersion() { return $this->_version; } + + /** + * Returns the delay. + * + * @return integer + */ + public function getDelay() { return $this->_delay; } + + /** + * Returns the quality indicator. Return value varies from 0 (best quality) to + * 100 (worst quality). + * + * @return integer + */ + public function getQualityIndicator() { return $this->_qualityIndicator; } + + /** + * Returns the number of bytes in the file. + * + * @return integer + */ + public function getBytes() { return $this->_bytes; } + + /** + * Returns the number of frames in the file. + * + * @return integer + */ + public function getFrames() { return $this->_frames; } + + /** + * Returns the table of contents array. + * + * @return Array + */ + public function getToc() { return $this->_toc; } + + /** + * Returns the number of frames per TOC entry. + * + * @return integer + */ + public function getTocFramesPerEntry() { return $this->_tocFramesPerEntry; } + + /** + * Returns the length of the header in bytes. + * + * @return integer + */ + public function getLength() { return $this->_length; } +} diff --git a/src/MPEG/Audio/XINGHeader.php b/src/MPEG/Audio/XINGHeader.php new file mode 100644 index 0000000..8ee0d22 --- /dev/null +++ b/src/MPEG/Audio/XINGHeader.php @@ -0,0 +1,136 @@ + + * @author Sven Vollbehr + * @copyright Copyright (c) 2008 The PHP Reader Project Workgroup + * @license http://code.google.com/p/php-reader/wiki/License New BSD License + * @version $Rev: 1 $ + */ +class MPEG_Audio_XINGHeader extends MPEG_Object +{ + /** @var integer */ + private $_frames = false; + + /** @var integer */ + private $_bytes = false; + + /** @var Array */ + private $_toc = array(); + + /** @var integer */ + private $_qualityIndicator = false; + + /** + * Constructs the class with given parameters and reads object related data + * from the bitstream. + * + * @param Reader $reader The reader object. + * @param Array $options Array of options. + */ + public function __construct($reader, &$options = array()) + { + parent::__construct($reader, $options); + + $flags = $reader->readUInt32BE(); + + if (Twiddling::testAnyBits($flags, 0x1)) + $this->_frames = $this->_reader->readUInt32BE(); + if (Twiddling::testAnyBits($flags, 0x2)) + $this->_bytes = $this->_reader->readUInt32BE(); + if (Twiddling::testAnyBits($flags, 0x4)) + $this->_toc = array_merge(unpack("C*", $this->_reader->read(100))); + if (Twiddling::testAnyBits($flags, 0x8)) + $this->_qualityIndicator = $this->_reader->readUInt32BE(); + } + + /** + * Returns the number of frames in the file. + * + * @return integer + */ + public function getFrames() { return $this->_frames; } + + /** + * Returns the number of bytes in the file. + * + * @return integer + */ + public function getBytes() { return $this->_bytes; } + + /** + * Returns the table of contents array. The returned array has a fixed amount + * of 100 seek points to the file. + * + * @return Array + */ + public function getToc() { return $this->_toc; } + + /** + * Returns the quality indicator. The indicator is from 0 (best quality) to + * 100 (worst quality). + * + * @return integer + */ + public function getQualityIndicator() { return $this->_qualityIndicator; } + + /** + * Returns the length of the header in bytes. + * + * @return integer + */ + public function getLength() + { + return 4 + + ($this->_frames !== false ? 4 : 0) + + ($this->_bytes !== false ? 4 : 0) + + (empty($this->_toc) ? 0 : 100) + + ($this->_qualityIndicator !== false ? 4 : 0); + } +} diff --git a/src/MPEG/Exception.php b/src/MPEG/Exception.php new file mode 100644 index 0000000..f87b375 --- /dev/null +++ b/src/MPEG/Exception.php @@ -0,0 +1,51 @@ + + * @copyright Copyright (c) 2008 The PHP Reader Project Workgroup + * @license http://code.google.com/p/php-reader/wiki/License New BSD License + * @version $Rev: 85 $ + */ +class MPEG_Exception extends Exception +{ +} diff --git a/src/MPEG/Object.php b/src/MPEG/Object.php new file mode 100644 index 0000000..1e4889c --- /dev/null +++ b/src/MPEG/Object.php @@ -0,0 +1,225 @@ + + * @copyright Copyright (c) 2008 The PHP Reader Project Workgroup + * @license http://code.google.com/p/php-reader/wiki/License New BSD License + * @version $Rev: 107 $ + */ +abstract class MPEG_Object +{ + /** + * The reader object. + * + * @var Reader + */ + protected $_reader; + + /** + * The options array. + * + * @var Array + */ + private $_options; + + /** + * Constructs the class with given parameters. + * + * @param Reader $reader The reader object. + * @param Array $options The options array. + */ + public function __construct($reader, &$options = array()) + { + $this->_reader = $reader; + $this->_options = &$options; + } + + /** + * Returns the options array. + * + * @return Array + */ + public function getOptions() { return $this->_options; } + + /** + * Returns the given option value, or the default value if the option is not + * defined. + * + * @param string $option The name of the option. + * @param mixed $defaultValue The default value to be returned. + */ + public function getOption($option, $defaultValue = false) + { + if (isset($this->_options[$option])) + return $this->_options[$option]; + return $defaultValue; + } + + /** + * Sets the options array. See {@link MPEG} class for available options. + * + * @param Array $options The options array. + */ + public function setOptions(&$options) { $this->_options = &$options; } + + /** + * Sets the given option the given value. + * + * @param string $option The name of the option. + * @param mixed $value The value to set for the option. + */ + public function setOption($option, $value) + { + $this->_options[$option] = $value; + } + + /** + * Finds and returns the next start code. Start codes are reserved bit + * patterns in the video file that do not otherwise occur in the video stream. + * + * All start codes are byte aligned and start with the following byte + * sequence: 0x00 0x00 0x01. + * + * @return integer + */ + protected function nextStartCode() + { + $buffer = " "; + for ($i = 0; $i < 4; $i++) { + $start = $this->_reader->getOffset(); + if (($buffer = substr($buffer, -4) . $this->_reader->read(512)) === false) + throw new MPEG_Exception("Invalid data"); + $limit = strlen($buffer); + $pos = 0; + while ($pos < $limit - 3) { + if (Transform::fromUInt8($buffer{$pos++}) == 0 && + Transform::fromUInt16BE(substr($buffer, $pos, 2)) == 1) { + if (($pos += 2) < $limit - 2) + if (Transform::fromUInt16BE(substr($buffer, $pos, 2)) == 0 && + Transform::fromUInt8($buffer{$pos + 2}) == 1) + continue; + $this->_reader->setOffset($start + $pos - 3); + return Transform::fromUInt8($buffer{$pos++}) & 0xff | 0x100; + } + } + $this->_reader->setOffset($start + $limit); + } + + /* No start code found within 2048 bytes, the maximum size of a pack */ + throw new MPEG_Exception("Invalid data"); + } + + /** + * Finds and returns the previous start code. Start codes are reserved bit + * patterns in the video file that do not otherwise occur in the video stream. + * + * All start codes are byte aligned and start with the following byte + * sequence: 0x00 0x00 0x01. + * + * @return integer + */ + protected function prevStartCode() + { + $buffer = " "; + $start; + $position = $this->_reader->getOffset(); + while ($position > 0) { + $start = 0; + $position = $position - 512; + if ($position < 0) + throw new MPEG_Exception("Invalid data"); + $this->_reader->setOffset($position); + $buffer = $this->_reader->read(512) . substr($buffer, 0, 4); + $pos = 512 - 8; + while ($pos > 3) { + if (Transform::fromUInt8($buffer{$pos}) == 0 && + Transform::fromUInt16BE(substr($buffer, $pos + 1, 2)) == 1) { + + if ($pos + 2 < 512 && + Transform::fromUInt16BE(substr($buffer, $pos + 3, 2)) == 0 && + Transform::fromUInt8($buffer{$pos + 5}) == 1) { + $pos --; + continue; + } + $this->_reader->setOffset($position + $pos); + return Transform::fromUInt8($buffer{$pos + 3}) & 0xff | 0x100; + } + $pos--; + } + $this->_reader->setOffset($position = $position + 3); + } + return 0; + } + + /** + * Magic function so that $obj->value will work. + * + * @param string $name The field name. + * @return mixed + */ + public function __get($name) + { + if (method_exists($this, "get" . ucfirst($name))) + return call_user_func(array($this, "get" . ucfirst($name))); + else throw new MPEG_Exception("Unknown field: " . $name); + } + + /** + * Magic function so that assignments with $obj->value will work. + * + * @param string $name The field name. + * @param string $value The field value. + * @return mixed + */ + public function __set($name, $value) + { + if (method_exists($this, "set" . ucfirst($name))) + call_user_func + (array($this, "set" . ucfirst($name)), $value); + else throw new MPEG_Exception("Unknown field: " . $name); + } +} diff --git a/src/Twiddling.php b/src/Twiddling.php index dd3a4f6..f48ef0a 100644 --- a/src/Twiddling.php +++ b/src/Twiddling.php @@ -39,6 +39,7 @@ * * @package php-reader * @author Ryan Butterfield + * @author Sven Vollbehr * @copyright Copyright (c) 2008 The PHP Reader Project Workgroup * @license http://code.google.com/p/php-reader/wiki/License New BSD License * @version $Rev$ @@ -200,8 +201,8 @@ final class Twiddling */ public static function setValue($integer, $start, $end, $value) { - return self::clearBits($integer, self::getMask($start, $end) << $start) | - ($value << $start); + return self::clearBits + ($integer, self::getMask($start, $end) << $start) | ($value << $start); } /** @@ -227,9 +228,6 @@ final class Twiddling */ public static function getMask($start, $end) { - $mask = 0; - for (; $start <= $end; $start++) - $mask |= 1 << $start; - return $mask; + return ($tmp = (1 << $end)) + $tmp - (1 << $start); } }