balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

module.audio.ogg.php (43097B)


      1 <?php
      2 
      3 /////////////////////////////////////////////////////////////////
      4 /// getID3() by James Heinrich <info@getid3.org>               //
      5 //  available at https://github.com/JamesHeinrich/getID3       //
      6 //            or https://www.getid3.org                        //
      7 //            or http://getid3.sourceforge.net                 //
      8 //  see readme.txt for more details                            //
      9 /////////////////////////////////////////////////////////////////
     10 //                                                             //
     11 // module.audio.ogg.php                                        //
     12 // module for analyzing Ogg Vorbis, OggFLAC and Speex files    //
     13 // dependencies: module.audio.flac.php                         //
     14 //                                                            ///
     15 /////////////////////////////////////////////////////////////////
     16 
     17 if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
     18 	exit;
     19 }
     20 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true);
     21 
     22 class getid3_ogg extends getid3_handler
     23 {
     24 	/**
     25 	 * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html
     26 	 *
     27 	 * @return bool
     28 	 */
     29 	public function Analyze() {
     30 		$info = &$this->getid3->info;
     31 
     32 		$info['fileformat'] = 'ogg';
     33 
     34 		// Warn about illegal tags - only vorbiscomments are allowed
     35 		if (isset($info['id3v2'])) {
     36 			$this->warning('Illegal ID3v2 tag present.');
     37 		}
     38 		if (isset($info['id3v1'])) {
     39 			$this->warning('Illegal ID3v1 tag present.');
     40 		}
     41 		if (isset($info['ape'])) {
     42 			$this->warning('Illegal APE tag present.');
     43 		}
     44 
     45 
     46 		// Page 1 - Stream Header
     47 
     48 		$this->fseek($info['avdataoffset']);
     49 
     50 		$oggpageinfo = $this->ParseOggPageHeader();
     51 		$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
     52 
     53 		if ($this->ftell() >= $this->getid3->fread_buffer_size()) {
     54 			$this->error('Could not find start of Ogg page in the first '.$this->getid3->fread_buffer_size().' bytes (this might not be an Ogg-Vorbis file?)');
     55 			unset($info['fileformat']);
     56 			unset($info['ogg']);
     57 			return false;
     58 		}
     59 
     60 		$filedata = $this->fread($oggpageinfo['page_length']);
     61 		$filedataoffset = 0;
     62 
     63 		if (substr($filedata, 0, 4) == 'fLaC') {
     64 
     65 			$info['audio']['dataformat']   = 'flac';
     66 			$info['audio']['bitrate_mode'] = 'vbr';
     67 			$info['audio']['lossless']     = true;
     68 
     69 		} elseif (substr($filedata, 1, 6) == 'vorbis') {
     70 
     71 			$this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
     72 
     73 		} elseif (substr($filedata, 0, 8) == 'OpusHead') {
     74 
     75 			if ($this->ParseOpusPageHeader($filedata, $filedataoffset, $oggpageinfo) === false) {
     76 				return false;
     77 			}
     78 
     79 		} elseif (substr($filedata, 0, 8) == 'Speex   ') {
     80 
     81 			// http://www.speex.org/manual/node10.html
     82 
     83 			$info['audio']['dataformat']   = 'speex';
     84 			$info['mime_type']             = 'audio/speex';
     85 			$info['audio']['bitrate_mode'] = 'abr';
     86 			$info['audio']['lossless']     = false;
     87 
     88 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string']           =                              substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex   '
     89 			$filedataoffset += 8;
     90 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']          =                              substr($filedata, $filedataoffset, 20);
     91 			$filedataoffset += 20;
     92 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id']       = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
     93 			$filedataoffset += 4;
     94 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size']            = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
     95 			$filedataoffset += 4;
     96 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate']                   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
     97 			$filedataoffset += 4;
     98 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']                   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
     99 			$filedataoffset += 4;
    100 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    101 			$filedataoffset += 4;
    102 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels']            = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    103 			$filedataoffset += 4;
    104 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate']                = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    105 			$filedataoffset += 4;
    106 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize']              = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    107 			$filedataoffset += 4;
    108 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr']                    = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    109 			$filedataoffset += 4;
    110 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet']      = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    111 			$filedataoffset += 4;
    112 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers']          = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    113 			$filedataoffset += 4;
    114 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1']              = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    115 			$filedataoffset += 4;
    116 			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2']              = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    117 			$filedataoffset += 4;
    118 
    119 			$info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']);
    120 			$info['speex']['sample_rate']   = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'];
    121 			$info['speex']['channels']      = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'];
    122 			$info['speex']['vbr']           = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'];
    123 			$info['speex']['band_type']     = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']);
    124 
    125 			$info['audio']['sample_rate']   = $info['speex']['sample_rate'];
    126 			$info['audio']['channels']      = $info['speex']['channels'];
    127 			if ($info['speex']['vbr']) {
    128 				$info['audio']['bitrate_mode'] = 'vbr';
    129 			}
    130 
    131 		} elseif (substr($filedata, 0, 7) == "\x80".'theora') {
    132 
    133 			// http://www.theora.org/doc/Theora.pdf (section 6.2)
    134 
    135 			$info['ogg']['pageheader']['theora']['theora_magic']             =                           substr($filedata, $filedataoffset,  7); // hard-coded to "\x80.'theora'
    136 			$filedataoffset += 7;
    137 			$info['ogg']['pageheader']['theora']['version_major']            = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
    138 			$filedataoffset += 1;
    139 			$info['ogg']['pageheader']['theora']['version_minor']            = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
    140 			$filedataoffset += 1;
    141 			$info['ogg']['pageheader']['theora']['version_revision']         = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
    142 			$filedataoffset += 1;
    143 			$info['ogg']['pageheader']['theora']['frame_width_macroblocks']  = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  2));
    144 			$filedataoffset += 2;
    145 			$info['ogg']['pageheader']['theora']['frame_height_macroblocks'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  2));
    146 			$filedataoffset += 2;
    147 			$info['ogg']['pageheader']['theora']['resolution_x']             = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
    148 			$filedataoffset += 3;
    149 			$info['ogg']['pageheader']['theora']['resolution_y']             = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
    150 			$filedataoffset += 3;
    151 			$info['ogg']['pageheader']['theora']['picture_offset_x']         = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
    152 			$filedataoffset += 1;
    153 			$info['ogg']['pageheader']['theora']['picture_offset_y']         = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
    154 			$filedataoffset += 1;
    155 			$info['ogg']['pageheader']['theora']['frame_rate_numerator']     = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  4));
    156 			$filedataoffset += 4;
    157 			$info['ogg']['pageheader']['theora']['frame_rate_denominator']   = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  4));
    158 			$filedataoffset += 4;
    159 			$info['ogg']['pageheader']['theora']['pixel_aspect_numerator']   = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
    160 			$filedataoffset += 3;
    161 			$info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
    162 			$filedataoffset += 3;
    163 			$info['ogg']['pageheader']['theora']['color_space_id']           = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
    164 			$filedataoffset += 1;
    165 			$info['ogg']['pageheader']['theora']['nominal_bitrate']          = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
    166 			$filedataoffset += 3;
    167 			$info['ogg']['pageheader']['theora']['flags']                    = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  2));
    168 			$filedataoffset += 2;
    169 
    170 			$info['ogg']['pageheader']['theora']['quality']         = ($info['ogg']['pageheader']['theora']['flags'] & 0xFC00) >> 10;
    171 			$info['ogg']['pageheader']['theora']['kfg_shift']       = ($info['ogg']['pageheader']['theora']['flags'] & 0x03E0) >>  5;
    172 			$info['ogg']['pageheader']['theora']['pixel_format_id'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0018) >>  3;
    173 			$info['ogg']['pageheader']['theora']['reserved']        = ($info['ogg']['pageheader']['theora']['flags'] & 0x0007) >>  0; // should be 0
    174 			$info['ogg']['pageheader']['theora']['color_space']     = self::TheoraColorSpace($info['ogg']['pageheader']['theora']['color_space_id']);
    175 			$info['ogg']['pageheader']['theora']['pixel_format']    = self::TheoraPixelFormat($info['ogg']['pageheader']['theora']['pixel_format_id']);
    176 
    177 			$info['video']['dataformat']   = 'theora';
    178 			$info['mime_type']             = 'video/ogg';
    179 			//$info['audio']['bitrate_mode'] = 'abr';
    180 			//$info['audio']['lossless']     = false;
    181 			$info['video']['resolution_x'] = $info['ogg']['pageheader']['theora']['resolution_x'];
    182 			$info['video']['resolution_y'] = $info['ogg']['pageheader']['theora']['resolution_y'];
    183 			if ($info['ogg']['pageheader']['theora']['frame_rate_denominator'] > 0) {
    184 				$info['video']['frame_rate'] = (float) $info['ogg']['pageheader']['theora']['frame_rate_numerator'] / $info['ogg']['pageheader']['theora']['frame_rate_denominator'];
    185 			}
    186 			if ($info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] > 0) {
    187 				$info['video']['pixel_aspect_ratio'] = (float) $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] / $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'];
    188 			}
    189 			$this->warning('Ogg Theora (v3) not fully supported in this version of getID3 ['.$this->getid3->version().'] -- bitrate, playtime and all audio data are currently unavailable');
    190 
    191 
    192 		} elseif (substr($filedata, 0, 8) == "fishead\x00") {
    193 
    194 			// Ogg Skeleton version 3.0 Format Specification
    195 			// http://xiph.org/ogg/doc/skeleton.html
    196 			$filedataoffset += 8;
    197 			$info['ogg']['skeleton']['fishead']['raw']['version_major']                = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  2));
    198 			$filedataoffset += 2;
    199 			$info['ogg']['skeleton']['fishead']['raw']['version_minor']                = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  2));
    200 			$filedataoffset += 2;
    201 			$info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
    202 			$filedataoffset += 8;
    203 			$info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
    204 			$filedataoffset += 8;
    205 			$info['ogg']['skeleton']['fishead']['raw']['basetime_numerator']           = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
    206 			$filedataoffset += 8;
    207 			$info['ogg']['skeleton']['fishead']['raw']['basetime_denominator']         = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
    208 			$filedataoffset += 8;
    209 			$info['ogg']['skeleton']['fishead']['raw']['utc']                          = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 20));
    210 			$filedataoffset += 20;
    211 
    212 			$info['ogg']['skeleton']['fishead']['version']          = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor'];
    213 			$info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'];
    214 			$info['ogg']['skeleton']['fishead']['basetime']         = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator']         / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'];
    215 			$info['ogg']['skeleton']['fishead']['utc']              = $info['ogg']['skeleton']['fishead']['raw']['utc'];
    216 
    217 
    218 			$counter = 0;
    219 			do {
    220 				$oggpageinfo = $this->ParseOggPageHeader();
    221 				$info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++] = $oggpageinfo;
    222 				$filedata = $this->fread($oggpageinfo['page_length']);
    223 				$this->fseek($oggpageinfo['page_end_offset']);
    224 
    225 				if (substr($filedata, 0, 8) == "fisbone\x00") {
    226 
    227 					$filedataoffset = 8;
    228 					$info['ogg']['skeleton']['fisbone']['raw']['message_header_offset']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
    229 					$filedataoffset += 4;
    230 					$info['ogg']['skeleton']['fisbone']['raw']['serial_number']           = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
    231 					$filedataoffset += 4;
    232 					$info['ogg']['skeleton']['fisbone']['raw']['number_header_packets']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
    233 					$filedataoffset += 4;
    234 					$info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
    235 					$filedataoffset += 8;
    236 					$info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
    237 					$filedataoffset += 8;
    238 					$info['ogg']['skeleton']['fisbone']['raw']['basegranule']             = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
    239 					$filedataoffset += 8;
    240 					$info['ogg']['skeleton']['fisbone']['raw']['preroll']                 = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
    241 					$filedataoffset += 4;
    242 					$info['ogg']['skeleton']['fisbone']['raw']['granuleshift']            = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  1));
    243 					$filedataoffset += 1;
    244 					$info['ogg']['skeleton']['fisbone']['raw']['padding']                 =                              substr($filedata, $filedataoffset,  3);
    245 					$filedataoffset += 3;
    246 
    247 				} elseif (substr($filedata, 1, 6) == 'theora') {
    248 
    249 					$info['video']['dataformat'] = 'theora1';
    250 					$this->error('Ogg Theora (v1) not correctly handled in this version of getID3 ['.$this->getid3->version().']');
    251 					//break;
    252 
    253 				} elseif (substr($filedata, 1, 6) == 'vorbis') {
    254 
    255 					$this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
    256 
    257 				} else {
    258 					$this->error('unexpected');
    259 					//break;
    260 				}
    261 			//} while ($oggpageinfo['page_seqno'] == 0);
    262 			} while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
    263 
    264 			$this->fseek($oggpageinfo['page_start_offset']);
    265 
    266 			$this->error('Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']');
    267 			//return false;
    268 
    269 		} elseif (substr($filedata, 0, 5) == "\x7F".'FLAC') {
    270 			// https://xiph.org/flac/ogg_mapping.html
    271 
    272 			$info['audio']['dataformat']   = 'flac';
    273 			$info['audio']['bitrate_mode'] = 'vbr';
    274 			$info['audio']['lossless']     = true;
    275 
    276 			$info['ogg']['flac']['header']['version_major']  =                         ord(substr($filedata,  5, 1));
    277 			$info['ogg']['flac']['header']['version_minor']  =                         ord(substr($filedata,  6, 1));
    278 			$info['ogg']['flac']['header']['header_packets'] =   getid3_lib::BigEndian2Int(substr($filedata,  7, 2)) + 1; // "A two-byte, big-endian binary number signifying the number of header (non-audio) packets, not including this one. This number may be zero (0x0000) to signify 'unknown' but be aware that some decoders may not be able to handle such streams."
    279 			$info['ogg']['flac']['header']['magic']          =                             substr($filedata,  9, 4);
    280 			if ($info['ogg']['flac']['header']['magic'] != 'fLaC') {
    281 				$this->error('Ogg-FLAC expecting "fLaC", found "'.$info['ogg']['flac']['header']['magic'].'" ('.trim(getid3_lib::PrintHexBytes($info['ogg']['flac']['header']['magic'])).')');
    282 				return false;
    283 			}
    284 			$info['ogg']['flac']['header']['STREAMINFO_bytes'] = getid3_lib::BigEndian2Int(substr($filedata, 13, 4));
    285 			$info['flac']['STREAMINFO'] = getid3_flac::parseSTREAMINFOdata(substr($filedata, 17, 34));
    286 			if (!empty($info['flac']['STREAMINFO']['sample_rate'])) {
    287 				$info['audio']['bitrate_mode']    = 'vbr';
    288 				$info['audio']['sample_rate']     = $info['flac']['STREAMINFO']['sample_rate'];
    289 				$info['audio']['channels']        = $info['flac']['STREAMINFO']['channels'];
    290 				$info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample'];
    291 				$info['playtime_seconds']         = $info['flac']['STREAMINFO']['samples_stream'] / $info['flac']['STREAMINFO']['sample_rate'];
    292 			}
    293 
    294 		} else {
    295 
    296 			$this->error('Expecting one of "vorbis", "Speex", "OpusHead", "vorbis", "fishhead", "theora", "fLaC" identifier strings, found "'.substr($filedata, 0, 8).'"');
    297 			unset($info['ogg']);
    298 			unset($info['mime_type']);
    299 			return false;
    300 
    301 		}
    302 
    303 		// Page 2 - Comment Header
    304 		$oggpageinfo = $this->ParseOggPageHeader();
    305 		$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
    306 
    307 		switch ($info['audio']['dataformat']) {
    308 			case 'vorbis':
    309 				$filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
    310 				$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, 0, 1));
    311 				$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] =                              substr($filedata, 1, 6); // hard-coded to 'vorbis'
    312 
    313 				$this->ParseVorbisComments();
    314 				break;
    315 
    316 			case 'flac':
    317 				$flac = new getid3_flac($this->getid3);
    318 				if (!$flac->parseMETAdata()) {
    319 					$this->error('Failed to parse FLAC headers');
    320 					return false;
    321 				}
    322 				unset($flac);
    323 				break;
    324 
    325 			case 'speex':
    326 				$this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR);
    327 				$this->ParseVorbisComments();
    328 				break;
    329 
    330 			case 'opus':
    331 				$filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
    332 				$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 0, 8); // hard-coded to 'OpusTags'
    333 				if(substr($filedata, 0, 8)  != 'OpusTags') {
    334 					$this->error('Expected "OpusTags" as header but got "'.substr($filedata, 0, 8).'"');
    335 					return false;
    336 				}
    337 
    338 				$this->ParseVorbisComments();
    339 				break;
    340 
    341 		}
    342 
    343 		// Last Page - Number of Samples
    344 		if (!getid3_lib::intValueSupported($info['avdataend'])) {
    345 
    346 			$this->warning('Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)');
    347 
    348 		} else {
    349 
    350 			$this->fseek(max($info['avdataend'] - $this->getid3->fread_buffer_size(), 0));
    351 			$LastChunkOfOgg = strrev($this->fread($this->getid3->fread_buffer_size()));
    352 			if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
    353 				$this->fseek($info['avdataend'] - ($LastOggSpostion + strlen('SggO')));
    354 				$info['avdataend'] = $this->ftell();
    355 				$info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader();
    356 				$info['ogg']['samples']   = $info['ogg']['pageheader']['eos']['pcm_abs_position'];
    357 				if ($info['ogg']['samples'] == 0) {
    358 					$this->error('Corrupt Ogg file: eos.number of samples == zero');
    359 					return false;
    360 				}
    361 				if (!empty($info['audio']['sample_rate'])) {
    362 					$info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
    363 				}
    364 			}
    365 
    366 		}
    367 
    368 		if (!empty($info['ogg']['bitrate_average'])) {
    369 			$info['audio']['bitrate'] = $info['ogg']['bitrate_average'];
    370 		} elseif (!empty($info['ogg']['bitrate_nominal'])) {
    371 			$info['audio']['bitrate'] = $info['ogg']['bitrate_nominal'];
    372 		} elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) {
    373 			$info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] + $info['ogg']['bitrate_max']) / 2;
    374 		}
    375 		if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
    376 			if ($info['audio']['bitrate'] == 0) {
    377 				$this->error('Corrupt Ogg file: bitrate_audio == zero');
    378 				return false;
    379 			}
    380 			$info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
    381 		}
    382 
    383 		if (isset($info['ogg']['vendor'])) {
    384 			$info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
    385 
    386 			// Vorbis only
    387 			if ($info['audio']['dataformat'] == 'vorbis') {
    388 
    389 				// Vorbis 1.0 starts with Xiph.Org
    390 				if  (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
    391 
    392 					if ($info['audio']['bitrate_mode'] == 'abr') {
    393 
    394 						// Set -b 128 on abr files
    395 						$info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
    396 
    397 					} elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) {
    398 						// Set -q N on vbr files
    399 						$info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']);
    400 
    401 					}
    402 				}
    403 
    404 				if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) {
    405 					$info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps';
    406 				}
    407 			}
    408 		}
    409 
    410 		return true;
    411 	}
    412 
    413 	/**
    414 	 * @param string $filedata
    415 	 * @param int    $filedataoffset
    416 	 * @param array  $oggpageinfo
    417 	 *
    418 	 * @return bool
    419 	 */
    420 	public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
    421 		$info = &$this->getid3->info;
    422 		$info['audio']['dataformat'] = 'vorbis';
    423 		$info['audio']['lossless']   = false;
    424 
    425 		$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
    426 		$filedataoffset += 1;
    427 		$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis'
    428 		$filedataoffset += 6;
    429 		$info['ogg']['bitstreamversion'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    430 		$filedataoffset += 4;
    431 		$info['ogg']['numberofchannels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
    432 		$filedataoffset += 1;
    433 		$info['audio']['channels']       = $info['ogg']['numberofchannels'];
    434 		$info['ogg']['samplerate']       = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    435 		$filedataoffset += 4;
    436 		if ($info['ogg']['samplerate'] == 0) {
    437 			$this->error('Corrupt Ogg file: sample rate == zero');
    438 			return false;
    439 		}
    440 		$info['audio']['sample_rate']    = $info['ogg']['samplerate'];
    441 		$info['ogg']['samples']          = 0; // filled in later
    442 		$info['ogg']['bitrate_average']  = 0; // filled in later
    443 		$info['ogg']['bitrate_max']      = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    444 		$filedataoffset += 4;
    445 		$info['ogg']['bitrate_nominal']  = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    446 		$filedataoffset += 4;
    447 		$info['ogg']['bitrate_min']      = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    448 		$filedataoffset += 4;
    449 		$info['ogg']['blocksize_small']  = pow(2,  getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F);
    450 		$info['ogg']['blocksize_large']  = pow(2, (getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4);
    451 		$info['ogg']['stop_bit']         = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet
    452 
    453 		$info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr
    454 		if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) {
    455 			unset($info['ogg']['bitrate_max']);
    456 			$info['audio']['bitrate_mode'] = 'abr';
    457 		}
    458 		if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
    459 			unset($info['ogg']['bitrate_nominal']);
    460 		}
    461 		if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
    462 			unset($info['ogg']['bitrate_min']);
    463 			$info['audio']['bitrate_mode'] = 'abr';
    464 		}
    465 		return true;
    466 	}
    467 
    468 	/**
    469 	 * @link http://tools.ietf.org/html/draft-ietf-codec-oggopus-03
    470 	 *
    471 	 * @param string $filedata
    472 	 * @param int    $filedataoffset
    473 	 * @param array  $oggpageinfo
    474 	 *
    475 	 * @return bool
    476 	 */
    477 	public function ParseOpusPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
    478 		$info = &$this->getid3->info;
    479 		$info['audio']['dataformat']   = 'opus';
    480 		$info['mime_type']             = 'audio/ogg; codecs=opus';
    481 
    482 		/** @todo find a usable way to detect abr (vbr that is padded to be abr) */
    483 		$info['audio']['bitrate_mode'] = 'vbr';
    484 
    485 		$info['audio']['lossless']     = false;
    486 
    487 		$info['ogg']['pageheader']['opus']['opus_magic'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'OpusHead'
    488 		$filedataoffset += 8;
    489 		$info['ogg']['pageheader']['opus']['version']    = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  1));
    490 		$filedataoffset += 1;
    491 
    492 		if ($info['ogg']['pageheader']['opus']['version'] < 1 || $info['ogg']['pageheader']['opus']['version'] > 15) {
    493 			$this->error('Unknown opus version number (only accepting 1-15)');
    494 			return false;
    495 		}
    496 
    497 		$info['ogg']['pageheader']['opus']['out_channel_count'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  1));
    498 		$filedataoffset += 1;
    499 
    500 		if ($info['ogg']['pageheader']['opus']['out_channel_count'] == 0) {
    501 			$this->error('Invalid channel count in opus header (must not be zero)');
    502 			return false;
    503 		}
    504 
    505 		$info['ogg']['pageheader']['opus']['pre_skip'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  2));
    506 		$filedataoffset += 2;
    507 
    508 		$info['ogg']['pageheader']['opus']['input_sample_rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
    509 		$filedataoffset += 4;
    510 
    511 		//$info['ogg']['pageheader']['opus']['output_gain'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  2));
    512 		//$filedataoffset += 2;
    513 
    514 		//$info['ogg']['pageheader']['opus']['channel_mapping_family'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  1));
    515 		//$filedataoffset += 1;
    516 
    517 		$info['opus']['opus_version']       = $info['ogg']['pageheader']['opus']['version'];
    518 		$info['opus']['sample_rate_input']  = $info['ogg']['pageheader']['opus']['input_sample_rate'];
    519 		$info['opus']['out_channel_count']  = $info['ogg']['pageheader']['opus']['out_channel_count'];
    520 
    521 		$info['audio']['channels']          = $info['opus']['out_channel_count'];
    522 		$info['audio']['sample_rate_input'] = $info['opus']['sample_rate_input'];
    523 		$info['audio']['sample_rate']       = 48000; // "All Opus audio is coded at 48 kHz, and should also be decoded at 48 kHz for playback (unless the target hardware does not support this sampling rate). However, this field may be used to resample the audio back to the original sampling rate, for example, when saving the output to a file." -- https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/structOpusHead.html
    524 		return true;
    525 	}
    526 
    527 	/**
    528 	 * @return array|false
    529 	 */
    530 	public function ParseOggPageHeader() {
    531 		// http://xiph.org/ogg/vorbis/doc/framing.html
    532 		$oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file
    533 
    534 		$filedata = $this->fread($this->getid3->fread_buffer_size());
    535 		$filedataoffset = 0;
    536 		while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) {
    537 			if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) {
    538 				// should be found before here
    539 				return false;
    540 			}
    541 			if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) {
    542 				if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === '')) {
    543 					// get some more data, unless eof, in which case fail
    544 					return false;
    545 				}
    546 			}
    547 		}
    548 		$filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS'
    549 
    550 		$oggheader['stream_structver']  = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
    551 		$filedataoffset += 1;
    552 		$oggheader['flags_raw']         = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
    553 		$filedataoffset += 1;
    554 		$oggheader['flags']['fresh']    = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
    555 		$oggheader['flags']['bos']      = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
    556 		$oggheader['flags']['eos']      = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
    557 
    558 		$oggheader['pcm_abs_position']  = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
    559 		$filedataoffset += 8;
    560 		$oggheader['stream_serialno']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    561 		$filedataoffset += 4;
    562 		$oggheader['page_seqno']        = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    563 		$filedataoffset += 4;
    564 		$oggheader['page_checksum']     = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
    565 		$filedataoffset += 4;
    566 		$oggheader['page_segments']     = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
    567 		$filedataoffset += 1;
    568 		$oggheader['page_length'] = 0;
    569 		for ($i = 0; $i < $oggheader['page_segments']; $i++) {
    570 			$oggheader['segment_table'][$i] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
    571 			$filedataoffset += 1;
    572 			$oggheader['page_length'] += $oggheader['segment_table'][$i];
    573 		}
    574 		$oggheader['header_end_offset'] = $oggheader['page_start_offset'] + $filedataoffset;
    575 		$oggheader['page_end_offset']   = $oggheader['header_end_offset'] + $oggheader['page_length'];
    576 		$this->fseek($oggheader['header_end_offset']);
    577 
    578 		return $oggheader;
    579 	}
    580 
    581 	/**
    582 	 * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
    583 	 *
    584 	 * @return bool
    585 	 */
    586 	public function ParseVorbisComments() {
    587 		$info = &$this->getid3->info;
    588 
    589 		$OriginalOffset = $this->ftell();
    590 		$commentdata = null;
    591 		$commentdataoffset = 0;
    592 		$VorbisCommentPage = 1;
    593 		$CommentStartOffset = 0;
    594 
    595 		switch ($info['audio']['dataformat']) {
    596 			case 'vorbis':
    597 			case 'speex':
    598 			case 'opus':
    599 				$CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset'];  // Second Ogg page, after header block
    600 				$this->fseek($CommentStartOffset);
    601 				$commentdataoffset = 27 + $info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
    602 				$commentdata = $this->fread(self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) + $commentdataoffset);
    603 
    604 				if ($info['audio']['dataformat'] == 'vorbis') {
    605 					$commentdataoffset += (strlen('vorbis') + 1);
    606 				}
    607 				else if ($info['audio']['dataformat'] == 'opus') {
    608 					$commentdataoffset += strlen('OpusTags');
    609 				}
    610 
    611 				break;
    612 
    613 			case 'flac':
    614 				$CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4;
    615 				$this->fseek($CommentStartOffset);
    616 				$commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
    617 				break;
    618 
    619 			default:
    620 				return false;
    621 		}
    622 
    623 		$VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
    624 		$commentdataoffset += 4;
    625 
    626 		$info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
    627 		$commentdataoffset += $VendorSize;
    628 
    629 		$CommentsCount = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
    630 		$commentdataoffset += 4;
    631 		$info['avdataoffset'] = $CommentStartOffset + $commentdataoffset;
    632 
    633 		$basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
    634 		$ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
    635 		for ($i = 0; $i < $CommentsCount; $i++) {
    636 
    637 			if ($i >= 10000) {
    638 				// https://github.com/owncloud/music/issues/212#issuecomment-43082336
    639 				$this->warning('Unexpectedly large number ('.$CommentsCount.') of Ogg comments - breaking after reading '.$i.' comments');
    640 				break;
    641 			}
    642 
    643 			$ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset;
    644 
    645 			if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) {
    646 				if ($oggpageinfo = $this->ParseOggPageHeader()) {
    647 					$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
    648 
    649 					$VorbisCommentPage++;
    650 
    651 					// First, save what we haven't read yet
    652 					$AsYetUnusedData = substr($commentdata, $commentdataoffset);
    653 
    654 					// Then take that data off the end
    655 					$commentdata     = substr($commentdata, 0, $commentdataoffset);
    656 
    657 					// Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
    658 					$commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
    659 					$commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
    660 
    661 					// Finally, stick the unused data back on the end
    662 					$commentdata .= $AsYetUnusedData;
    663 
    664 					//$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
    665 					$commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
    666 				}
    667 
    668 			}
    669 			$ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
    670 
    671 			// replace avdataoffset with position just after the last vorbiscomment
    672 			$info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4;
    673 
    674 			$commentdataoffset += 4;
    675 			while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
    676 				if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
    677 					$this->warning('Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments');
    678 					break 2;
    679 				}
    680 
    681 				$VorbisCommentPage++;
    682 
    683 				$oggpageinfo = $this->ParseOggPageHeader();
    684 				$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
    685 
    686 				// First, save what we haven't read yet
    687 				$AsYetUnusedData = substr($commentdata, $commentdataoffset);
    688 
    689 				// Then take that data off the end
    690 				$commentdata     = substr($commentdata, 0, $commentdataoffset);
    691 
    692 				// Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
    693 				$commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
    694 				$commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
    695 
    696 				// Finally, stick the unused data back on the end
    697 				$commentdata .= $AsYetUnusedData;
    698 
    699 				//$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
    700 				if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
    701 					$this->warning('undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
    702 					break;
    703 				}
    704 				$readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
    705 				if ($readlength <= 0) {
    706 					$this->warning('invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
    707 					break;
    708 				}
    709 				$commentdata .= $this->fread($readlength);
    710 
    711 				//$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
    712 			}
    713 			$ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
    714 			$commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
    715 			$commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size'];
    716 
    717 			if (!$commentstring) {
    718 
    719 				// no comment?
    720 				$this->warning('Blank Ogg comment ['.$i.']');
    721 
    722 			} elseif (strstr($commentstring, '=')) {
    723 
    724 				$commentexploded = explode('=', $commentstring, 2);
    725 				$ThisFileInfo_ogg_comments_raw[$i]['key']   = strtoupper($commentexploded[0]);
    726 				$ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : '');
    727 
    728 				if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
    729 
    730 					// http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
    731 					// The unencoded format is that of the FLAC picture block. The fields are stored in big endian order as in FLAC, picture data is stored according to the relevant standard.
    732 					// http://flac.sourceforge.net/format.html#metadata_block_picture
    733 					$flac = new getid3_flac($this->getid3);
    734 					$flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
    735 					$flac->parsePICTURE();
    736 					$info['ogg']['comments']['picture'][] = $flac->getid3->info['flac']['PICTURE'][0];
    737 					unset($flac);
    738 
    739 				} elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
    740 
    741 					$data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
    742 					$this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
    743 					/** @todo use 'coverartmime' where available */
    744 					$imageinfo = getid3_lib::GetDataImageSize($data);
    745 					if ($imageinfo === false || !isset($imageinfo['mime'])) {
    746 						$this->warning('COVERART vorbiscomment tag contains invalid image');
    747 						continue;
    748 					}
    749 
    750 					$ogg = new self($this->getid3);
    751 					$ogg->setStringMode($data);
    752 					$info['ogg']['comments']['picture'][] = array(
    753 						'image_mime'   => $imageinfo['mime'],
    754 						'datalength'   => strlen($data),
    755 						'picturetype'  => 'cover art',
    756 						'image_height' => $imageinfo['height'],
    757 						'image_width'  => $imageinfo['width'],
    758 						'data'         => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
    759 					);
    760 					unset($ogg);
    761 
    762 				} else {
    763 
    764 					$info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
    765 
    766 				}
    767 
    768 			} else {
    769 
    770 				$this->warning('[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring);
    771 
    772 			}
    773 			unset($ThisFileInfo_ogg_comments_raw[$i]);
    774 		}
    775 		unset($ThisFileInfo_ogg_comments_raw);
    776 
    777 
    778 		// Replay Gain Adjustment
    779 		// http://privatewww.essex.ac.uk/~djmrob/replaygain/
    780 		if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
    781 			foreach ($info['ogg']['comments'] as $index => $commentvalue) {
    782 				switch ($index) {
    783 					case 'rg_audiophile':
    784 					case 'replaygain_album_gain':
    785 						$info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
    786 						unset($info['ogg']['comments'][$index]);
    787 						break;
    788 
    789 					case 'rg_radio':
    790 					case 'replaygain_track_gain':
    791 						$info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
    792 						unset($info['ogg']['comments'][$index]);
    793 						break;
    794 
    795 					case 'replaygain_album_peak':
    796 						$info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
    797 						unset($info['ogg']['comments'][$index]);
    798 						break;
    799 
    800 					case 'rg_peak':
    801 					case 'replaygain_track_peak':
    802 						$info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
    803 						unset($info['ogg']['comments'][$index]);
    804 						break;
    805 
    806 					case 'replaygain_reference_loudness':
    807 						$info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
    808 						unset($info['ogg']['comments'][$index]);
    809 						break;
    810 
    811 					default:
    812 						// do nothing
    813 						break;
    814 				}
    815 			}
    816 		}
    817 
    818 		$this->fseek($OriginalOffset);
    819 
    820 		return true;
    821 	}
    822 
    823 	/**
    824 	 * @param int $mode
    825 	 *
    826 	 * @return string|null
    827 	 */
    828 	public static function SpeexBandModeLookup($mode) {
    829 		static $SpeexBandModeLookup = array();
    830 		if (empty($SpeexBandModeLookup)) {
    831 			$SpeexBandModeLookup[0] = 'narrow';
    832 			$SpeexBandModeLookup[1] = 'wide';
    833 			$SpeexBandModeLookup[2] = 'ultra-wide';
    834 		}
    835 		return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null);
    836 	}
    837 
    838 	/**
    839 	 * @param array $OggInfoArray
    840 	 * @param int   $SegmentNumber
    841 	 *
    842 	 * @return int
    843 	 */
    844 	public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
    845 		$segmentlength = 0;
    846 		for ($i = 0; $i < $SegmentNumber; $i++) {
    847 			$segmentlength = 0;
    848 			foreach ($OggInfoArray['segment_table'] as $key => $value) {
    849 				$segmentlength += $value;
    850 				if ($value < 255) {
    851 					break;
    852 				}
    853 			}
    854 		}
    855 		return $segmentlength;
    856 	}
    857 
    858 	/**
    859 	 * @param int $nominal_bitrate
    860 	 *
    861 	 * @return float
    862 	 */
    863 	public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
    864 
    865 		// decrease precision
    866 		$nominal_bitrate = $nominal_bitrate / 1000;
    867 
    868 		if ($nominal_bitrate < 128) {
    869 			// q-1 to q4
    870 			$qval = ($nominal_bitrate - 64) / 16;
    871 		} elseif ($nominal_bitrate < 256) {
    872 			// q4 to q8
    873 			$qval = $nominal_bitrate / 32;
    874 		} elseif ($nominal_bitrate < 320) {
    875 			// q8 to q9
    876 			$qval = ($nominal_bitrate + 256) / 64;
    877 		} else {
    878 			// q9 to q10
    879 			$qval = ($nominal_bitrate + 1300) / 180;
    880 		}
    881 		//return $qval; // 5.031324
    882 		//return intval($qval); // 5
    883 		return round($qval, 1); // 5 or 4.9
    884 	}
    885 
    886 	/**
    887 	 * @param int $colorspace_id
    888 	 *
    889 	 * @return string|null
    890 	 */
    891 	public static function TheoraColorSpace($colorspace_id) {
    892 		// http://www.theora.org/doc/Theora.pdf (table 6.3)
    893 		static $TheoraColorSpaceLookup = array();
    894 		if (empty($TheoraColorSpaceLookup)) {
    895 			$TheoraColorSpaceLookup[0] = 'Undefined';
    896 			$TheoraColorSpaceLookup[1] = 'Rec. 470M';
    897 			$TheoraColorSpaceLookup[2] = 'Rec. 470BG';
    898 			$TheoraColorSpaceLookup[3] = 'Reserved';
    899 		}
    900 		return (isset($TheoraColorSpaceLookup[$colorspace_id]) ? $TheoraColorSpaceLookup[$colorspace_id] : null);
    901 	}
    902 
    903 	/**
    904 	 * @param int $pixelformat_id
    905 	 *
    906 	 * @return string|null
    907 	 */
    908 	public static function TheoraPixelFormat($pixelformat_id) {
    909 		// http://www.theora.org/doc/Theora.pdf (table 6.4)
    910 		static $TheoraPixelFormatLookup = array();
    911 		if (empty($TheoraPixelFormatLookup)) {
    912 			$TheoraPixelFormatLookup[0] = '4:2:0';
    913 			$TheoraPixelFormatLookup[1] = 'Reserved';
    914 			$TheoraPixelFormatLookup[2] = '4:2:2';
    915 			$TheoraPixelFormatLookup[3] = '4:4:4';
    916 		}
    917 		return (isset($TheoraPixelFormatLookup[$pixelformat_id]) ? $TheoraPixelFormatLookup[$pixelformat_id] : null);
    918 	}
    919 
    920 }