File indexing completed on 2024-12-22 05:33:13

0001 <?php
0002 /////////////////////////////////////////////////////////////////
0003 /// getID3() by James Heinrich <info@getid3.org>               //
0004 //  available at http://getid3.sourceforge.net                 //
0005 //            or http://www.getid3.org                         //
0006 //          also https://github.com/JamesHeinrich/getID3       //
0007 /////////////////////////////////////////////////////////////////
0008 // See readme.txt for more details                             //
0009 /////////////////////////////////////////////////////////////////
0010 //                                                             //
0011 // module.audio.ogg.php                                        //
0012 // module for analyzing Ogg Vorbis, OggFLAC and Speex files    //
0013 // dependencies: module.audio.flac.php                         //
0014 //                                                            ///
0015 /////////////////////////////////////////////////////////////////
0016 
0017 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true);
0018 
0019 class getid3_ogg extends getid3_handler
0020 {
0021   // http://xiph.org/vorbis/doc/Vorbis_I_spec.html
0022   public function Analyze() {
0023     $info = &$this->getid3->info;
0024 
0025     $info['fileformat'] = 'ogg';
0026 
0027     // Warn about illegal tags - only vorbiscomments are allowed
0028     if (isset($info['id3v2'])) {
0029       $info['warning'][] = 'Illegal ID3v2 tag present.';
0030     }
0031     if (isset($info['id3v1'])) {
0032       $info['warning'][] = 'Illegal ID3v1 tag present.';
0033     }
0034     if (isset($info['ape'])) {
0035       $info['warning'][] = 'Illegal APE tag present.';
0036     }
0037 
0038 
0039     // Page 1 - Stream Header
0040 
0041     $this->fseek($info['avdataoffset']);
0042 
0043     $oggpageinfo = $this->ParseOggPageHeader();
0044     $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
0045 
0046     if ($this->ftell() >= $this->getid3->fread_buffer_size()) {
0047       $info['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?)';
0048       unset($info['fileformat']);
0049       unset($info['ogg']);
0050       return false;
0051     }
0052 
0053     $filedata = $this->fread($oggpageinfo['page_length']);
0054     $filedataoffset = 0;
0055 
0056     if (substr($filedata, 0, 4) == 'fLaC') {
0057 
0058       $info['audio']['dataformat']   = 'flac';
0059       $info['audio']['bitrate_mode'] = 'vbr';
0060       $info['audio']['lossless']     = true;
0061 
0062     } elseif (substr($filedata, 1, 6) == 'vorbis') {
0063 
0064       $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
0065 
0066     } elseif (substr($filedata, 0, 8) == 'Speex   ') {
0067 
0068       // http://www.speex.org/manual/node10.html
0069 
0070       $info['audio']['dataformat']   = 'speex';
0071       $info['mime_type']             = 'audio/speex';
0072       $info['audio']['bitrate_mode'] = 'abr';
0073       $info['audio']['lossless']     = false;
0074 
0075       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string']           =                              substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex   '
0076       $filedataoffset += 8;
0077       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']          =                              substr($filedata, $filedataoffset, 20);
0078       $filedataoffset += 20;
0079       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id']       = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0080       $filedataoffset += 4;
0081       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size']            = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0082       $filedataoffset += 4;
0083       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate']                   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0084       $filedataoffset += 4;
0085       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']                   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0086       $filedataoffset += 4;
0087       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0088       $filedataoffset += 4;
0089       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels']            = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0090       $filedataoffset += 4;
0091       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate']                = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0092       $filedataoffset += 4;
0093       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize']              = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0094       $filedataoffset += 4;
0095       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr']                    = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0096       $filedataoffset += 4;
0097       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet']      = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0098       $filedataoffset += 4;
0099       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers']          = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0100       $filedataoffset += 4;
0101       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1']              = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0102       $filedataoffset += 4;
0103       $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2']              = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0104       $filedataoffset += 4;
0105 
0106       $info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']);
0107       $info['speex']['sample_rate']   = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'];
0108       $info['speex']['channels']      = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'];
0109       $info['speex']['vbr']           = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'];
0110       $info['speex']['band_type']     = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']);
0111 
0112       $info['audio']['sample_rate']   = $info['speex']['sample_rate'];
0113       $info['audio']['channels']      = $info['speex']['channels'];
0114       if ($info['speex']['vbr']) {
0115         $info['audio']['bitrate_mode'] = 'vbr';
0116       }
0117 
0118     } elseif (substr($filedata, 0, 7) == "\x80".'theora') {
0119 
0120       // http://www.theora.org/doc/Theora.pdf (section 6.2)
0121 
0122       $info['ogg']['pageheader']['theora']['theora_magic']             =                           substr($filedata, $filedataoffset,  7); // hard-coded to "\x80.'theora'
0123       $filedataoffset += 7;
0124       $info['ogg']['pageheader']['theora']['version_major']            = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
0125       $filedataoffset += 1;
0126       $info['ogg']['pageheader']['theora']['version_minor']            = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
0127       $filedataoffset += 1;
0128       $info['ogg']['pageheader']['theora']['version_revision']         = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
0129       $filedataoffset += 1;
0130       $info['ogg']['pageheader']['theora']['frame_width_macroblocks']  = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  2));
0131       $filedataoffset += 2;
0132       $info['ogg']['pageheader']['theora']['frame_height_macroblocks'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  2));
0133       $filedataoffset += 2;
0134       $info['ogg']['pageheader']['theora']['resolution_x']             = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
0135       $filedataoffset += 3;
0136       $info['ogg']['pageheader']['theora']['resolution_y']             = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
0137       $filedataoffset += 3;
0138       $info['ogg']['pageheader']['theora']['picture_offset_x']         = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
0139       $filedataoffset += 1;
0140       $info['ogg']['pageheader']['theora']['picture_offset_y']         = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
0141       $filedataoffset += 1;
0142       $info['ogg']['pageheader']['theora']['frame_rate_numerator']     = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  4));
0143       $filedataoffset += 4;
0144       $info['ogg']['pageheader']['theora']['frame_rate_denominator']   = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  4));
0145       $filedataoffset += 4;
0146       $info['ogg']['pageheader']['theora']['pixel_aspect_numerator']   = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
0147       $filedataoffset += 3;
0148       $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
0149       $filedataoffset += 3;
0150       $info['ogg']['pageheader']['theora']['color_space_id']           = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
0151       $filedataoffset += 1;
0152       $info['ogg']['pageheader']['theora']['nominal_bitrate']          = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
0153       $filedataoffset += 3;
0154       $info['ogg']['pageheader']['theora']['flags']                    = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  2));
0155       $filedataoffset += 2;
0156 
0157       $info['ogg']['pageheader']['theora']['quality']         = ($info['ogg']['pageheader']['theora']['flags'] & 0xFC00) >> 10;
0158       $info['ogg']['pageheader']['theora']['kfg_shift']       = ($info['ogg']['pageheader']['theora']['flags'] & 0x03E0) >>  5;
0159       $info['ogg']['pageheader']['theora']['pixel_format_id'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0018) >>  3;
0160       $info['ogg']['pageheader']['theora']['reserved']        = ($info['ogg']['pageheader']['theora']['flags'] & 0x0007) >>  0; // should be 0
0161       $info['ogg']['pageheader']['theora']['color_space']     = self::TheoraColorSpace($info['ogg']['pageheader']['theora']['color_space_id']);
0162       $info['ogg']['pageheader']['theora']['pixel_format']    = self::TheoraPixelFormat($info['ogg']['pageheader']['theora']['pixel_format_id']);
0163 
0164       $info['video']['dataformat']   = 'theora';
0165       $info['mime_type']             = 'video/ogg';
0166       //$info['audio']['bitrate_mode'] = 'abr';
0167       //$info['audio']['lossless']     = false;
0168       $info['video']['resolution_x'] = $info['ogg']['pageheader']['theora']['resolution_x'];
0169       $info['video']['resolution_y'] = $info['ogg']['pageheader']['theora']['resolution_y'];
0170       if ($info['ogg']['pageheader']['theora']['frame_rate_denominator'] > 0) {
0171         $info['video']['frame_rate'] = (float) $info['ogg']['pageheader']['theora']['frame_rate_numerator'] / $info['ogg']['pageheader']['theora']['frame_rate_denominator'];
0172       }
0173       if ($info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] > 0) {
0174         $info['video']['pixel_aspect_ratio'] = (float) $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] / $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'];
0175       }
0176 $info['warning'][] = 'Ogg Theora (v3) not fully supported in this version of getID3 ['.$this->getid3->version().'] -- bitrate, playtime and all audio data are currently unavailable';
0177 
0178 
0179     } elseif (substr($filedata, 0, 8) == "fishead\x00") {
0180 
0181       // Ogg Skeleton version 3.0 Format Specification
0182       // http://xiph.org/ogg/doc/skeleton.html
0183       $filedataoffset += 8;
0184       $info['ogg']['skeleton']['fishead']['raw']['version_major']                = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  2));
0185       $filedataoffset += 2;
0186       $info['ogg']['skeleton']['fishead']['raw']['version_minor']                = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  2));
0187       $filedataoffset += 2;
0188       $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
0189       $filedataoffset += 8;
0190       $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
0191       $filedataoffset += 8;
0192       $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator']           = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
0193       $filedataoffset += 8;
0194       $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator']         = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
0195       $filedataoffset += 8;
0196       $info['ogg']['skeleton']['fishead']['raw']['utc']                          = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 20));
0197       $filedataoffset += 20;
0198 
0199       $info['ogg']['skeleton']['fishead']['version']          = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor'];
0200       $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'];
0201       $info['ogg']['skeleton']['fishead']['basetime']         = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator']         / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'];
0202       $info['ogg']['skeleton']['fishead']['utc']              = $info['ogg']['skeleton']['fishead']['raw']['utc'];
0203 
0204 
0205       $counter = 0;
0206       do {
0207         $oggpageinfo = $this->ParseOggPageHeader();
0208         $info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++] = $oggpageinfo;
0209         $filedata = $this->fread($oggpageinfo['page_length']);
0210         $this->fseek($oggpageinfo['page_end_offset']);
0211 
0212         if (substr($filedata, 0, 8) == "fisbone\x00") {
0213 
0214           $filedataoffset = 8;
0215           $info['ogg']['skeleton']['fisbone']['raw']['message_header_offset']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
0216           $filedataoffset += 4;
0217           $info['ogg']['skeleton']['fisbone']['raw']['serial_number']           = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
0218           $filedataoffset += 4;
0219           $info['ogg']['skeleton']['fisbone']['raw']['number_header_packets']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
0220           $filedataoffset += 4;
0221           $info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
0222           $filedataoffset += 8;
0223           $info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
0224           $filedataoffset += 8;
0225           $info['ogg']['skeleton']['fisbone']['raw']['basegranule']             = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
0226           $filedataoffset += 8;
0227           $info['ogg']['skeleton']['fisbone']['raw']['preroll']                 = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
0228           $filedataoffset += 4;
0229           $info['ogg']['skeleton']['fisbone']['raw']['granuleshift']            = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  1));
0230           $filedataoffset += 1;
0231           $info['ogg']['skeleton']['fisbone']['raw']['padding']                 =                              substr($filedata, $filedataoffset,  3);
0232           $filedataoffset += 3;
0233 
0234         } elseif (substr($filedata, 1, 6) == 'theora') {
0235 
0236           $info['video']['dataformat'] = 'theora1';
0237           $info['error'][] = 'Ogg Theora (v1) not correctly handled in this version of getID3 ['.$this->getid3->version().']';
0238           //break;
0239 
0240         } elseif (substr($filedata, 1, 6) == 'vorbis') {
0241 
0242           $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
0243 
0244         } else {
0245           $info['error'][] = 'unexpected';
0246           //break;
0247         }
0248       //} while ($oggpageinfo['page_seqno'] == 0);
0249       } while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
0250 
0251       $this->fseek($oggpageinfo['page_start_offset']);
0252 
0253       $info['error'][] = 'Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']';
0254       //return false;
0255 
0256     } else {
0257 
0258       $info['error'][] = 'Expecting either "Speex   " or "vorbis" identifier strings, found "'.substr($filedata, 0, 8).'"';
0259       unset($info['ogg']);
0260       unset($info['mime_type']);
0261       return false;
0262 
0263     }
0264 
0265     // Page 2 - Comment Header
0266     $oggpageinfo = $this->ParseOggPageHeader();
0267     $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
0268 
0269     switch ($info['audio']['dataformat']) {
0270       case 'vorbis':
0271         $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
0272         $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, 0, 1));
0273         $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] =                              substr($filedata, 1, 6); // hard-coded to 'vorbis'
0274 
0275         $this->ParseVorbisComments();
0276         break;
0277 
0278       case 'flac':
0279         $flac = new getid3_flac($this->getid3);
0280         if (!$flac->parseMETAdata()) {
0281           $info['error'][] = 'Failed to parse FLAC headers';
0282           return false;
0283         }
0284         unset($flac);
0285         break;
0286 
0287       case 'speex':
0288         $this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR);
0289         $this->ParseVorbisComments();
0290         break;
0291     }
0292 
0293 
0294     // Last Page - Number of Samples
0295     if (!getid3_lib::intValueSupported($info['avdataend'])) {
0296 
0297       $info['warning'][] = 'Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)';
0298 
0299     } else {
0300 
0301       $this->fseek(max($info['avdataend'] - $this->getid3->fread_buffer_size(), 0));
0302       $LastChunkOfOgg = strrev($this->fread($this->getid3->fread_buffer_size()));
0303       if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
0304         $this->fseek($info['avdataend'] - ($LastOggSpostion + strlen('SggO')));
0305         $info['avdataend'] = $this->ftell();
0306         $info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader();
0307         $info['ogg']['samples']   = $info['ogg']['pageheader']['eos']['pcm_abs_position'];
0308         if ($info['ogg']['samples'] == 0) {
0309           $info['error'][] = 'Corrupt Ogg file: eos.number of samples == zero';
0310           return false;
0311         }
0312         if (!empty($info['audio']['sample_rate'])) {
0313           $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
0314         }
0315       }
0316 
0317     }
0318 
0319     if (!empty($info['ogg']['bitrate_average'])) {
0320       $info['audio']['bitrate'] = $info['ogg']['bitrate_average'];
0321     } elseif (!empty($info['ogg']['bitrate_nominal'])) {
0322       $info['audio']['bitrate'] = $info['ogg']['bitrate_nominal'];
0323     } elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) {
0324       $info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] + $info['ogg']['bitrate_max']) / 2;
0325     }
0326     if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
0327       if ($info['audio']['bitrate'] == 0) {
0328         $info['error'][] = 'Corrupt Ogg file: bitrate_audio == zero';
0329         return false;
0330       }
0331       $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
0332     }
0333 
0334     if (isset($info['ogg']['vendor'])) {
0335       $info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
0336 
0337       // Vorbis only
0338       if ($info['audio']['dataformat'] == 'vorbis') {
0339 
0340         // Vorbis 1.0 starts with Xiph.Org
0341         if  (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
0342 
0343           if ($info['audio']['bitrate_mode'] == 'abr') {
0344 
0345             // Set -b 128 on abr files
0346             $info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
0347 
0348           } elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) {
0349             // Set -q N on vbr files
0350             $info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']);
0351 
0352           }
0353         }
0354 
0355         if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) {
0356           $info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps';
0357         }
0358       }
0359     }
0360 
0361     return true;
0362   }
0363 
0364   public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
0365     $info = &$this->getid3->info;
0366     $info['audio']['dataformat'] = 'vorbis';
0367     $info['audio']['lossless']   = false;
0368 
0369     $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
0370     $filedataoffset += 1;
0371     $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis'
0372     $filedataoffset += 6;
0373     $info['ogg']['bitstreamversion'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0374     $filedataoffset += 4;
0375     $info['ogg']['numberofchannels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
0376     $filedataoffset += 1;
0377     $info['audio']['channels']       = $info['ogg']['numberofchannels'];
0378     $info['ogg']['samplerate']       = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0379     $filedataoffset += 4;
0380     if ($info['ogg']['samplerate'] == 0) {
0381       $info['error'][] = 'Corrupt Ogg file: sample rate == zero';
0382       return false;
0383     }
0384     $info['audio']['sample_rate']    = $info['ogg']['samplerate'];
0385     $info['ogg']['samples']          = 0; // filled in later
0386     $info['ogg']['bitrate_average']  = 0; // filled in later
0387     $info['ogg']['bitrate_max']      = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0388     $filedataoffset += 4;
0389     $info['ogg']['bitrate_nominal']  = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0390     $filedataoffset += 4;
0391     $info['ogg']['bitrate_min']      = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0392     $filedataoffset += 4;
0393     $info['ogg']['blocksize_small']  = pow(2,  getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F);
0394     $info['ogg']['blocksize_large']  = pow(2, (getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4);
0395     $info['ogg']['stop_bit']         = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet
0396 
0397     $info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr
0398     if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) {
0399       unset($info['ogg']['bitrate_max']);
0400       $info['audio']['bitrate_mode'] = 'abr';
0401     }
0402     if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
0403       unset($info['ogg']['bitrate_nominal']);
0404     }
0405     if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
0406       unset($info['ogg']['bitrate_min']);
0407       $info['audio']['bitrate_mode'] = 'abr';
0408     }
0409     return true;
0410   }
0411 
0412   public function ParseOggPageHeader() {
0413     // http://xiph.org/ogg/vorbis/doc/framing.html
0414     $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file
0415 
0416     $filedata = $this->fread($this->getid3->fread_buffer_size());
0417     $filedataoffset = 0;
0418     while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) {
0419       if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) {
0420         // should be found before here
0421         return false;
0422       }
0423       if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) {
0424         if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === false)) {
0425           // get some more data, unless eof, in which case fail
0426           return false;
0427         }
0428       }
0429     }
0430     $filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS'
0431 
0432     $oggheader['stream_structver']  = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
0433     $filedataoffset += 1;
0434     $oggheader['flags_raw']         = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
0435     $filedataoffset += 1;
0436     $oggheader['flags']['fresh']    = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
0437     $oggheader['flags']['bos']      = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
0438     $oggheader['flags']['eos']      = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
0439 
0440     $oggheader['pcm_abs_position']  = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
0441     $filedataoffset += 8;
0442     $oggheader['stream_serialno']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0443     $filedataoffset += 4;
0444     $oggheader['page_seqno']        = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0445     $filedataoffset += 4;
0446     $oggheader['page_checksum']     = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
0447     $filedataoffset += 4;
0448     $oggheader['page_segments']     = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
0449     $filedataoffset += 1;
0450     $oggheader['page_length'] = 0;
0451     for ($i = 0; $i < $oggheader['page_segments']; $i++) {
0452       $oggheader['segment_table'][$i] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
0453       $filedataoffset += 1;
0454       $oggheader['page_length'] += $oggheader['segment_table'][$i];
0455     }
0456     $oggheader['header_end_offset'] = $oggheader['page_start_offset'] + $filedataoffset;
0457     $oggheader['page_end_offset']   = $oggheader['header_end_offset'] + $oggheader['page_length'];
0458     $this->fseek($oggheader['header_end_offset']);
0459 
0460     return $oggheader;
0461   }
0462 
0463     // http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
0464   public function ParseVorbisComments() {
0465     $info = &$this->getid3->info;
0466 
0467     $OriginalOffset = $this->ftell();
0468     $commentdataoffset = 0;
0469     $VorbisCommentPage = 1;
0470 
0471     switch ($info['audio']['dataformat']) {
0472       case 'vorbis':
0473       case 'speex':
0474         $CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset'];  // Second Ogg page, after header block
0475         $this->fseek($CommentStartOffset);
0476         $commentdataoffset = 27 + $info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
0477         $commentdata = $this->fread(self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) + $commentdataoffset);
0478 
0479         if ($info['audio']['dataformat'] == 'vorbis') {
0480           $commentdataoffset += (strlen('vorbis') + 1);
0481         }
0482         break;
0483 
0484       case 'flac':
0485         $CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4;
0486         $this->fseek($CommentStartOffset);
0487         $commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
0488         break;
0489 
0490       default:
0491         return false;
0492     }
0493 
0494     $VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
0495     $commentdataoffset += 4;
0496 
0497     $info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
0498     $commentdataoffset += $VendorSize;
0499 
0500     $CommentsCount = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
0501     $commentdataoffset += 4;
0502     $info['avdataoffset'] = $CommentStartOffset + $commentdataoffset;
0503 
0504     $basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
0505     $ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
0506     for ($i = 0; $i < $CommentsCount; $i++) {
0507 
0508       $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset;
0509 
0510       if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) {
0511         if ($oggpageinfo = $this->ParseOggPageHeader()) {
0512           $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
0513 
0514           $VorbisCommentPage++;
0515 
0516           // First, save what we haven't read yet
0517           $AsYetUnusedData = substr($commentdata, $commentdataoffset);
0518 
0519           // Then take that data off the end
0520           $commentdata     = substr($commentdata, 0, $commentdataoffset);
0521 
0522           // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
0523           $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
0524           $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
0525 
0526           // Finally, stick the unused data back on the end
0527           $commentdata .= $AsYetUnusedData;
0528 
0529           //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
0530           $commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
0531         }
0532 
0533       }
0534       $ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
0535 
0536       // replace avdataoffset with position just after the last vorbiscomment
0537       $info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4;
0538 
0539       $commentdataoffset += 4;
0540       while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
0541         if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
0542           $info['warning'][] = 'Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments';
0543           break 2;
0544         }
0545 
0546         $VorbisCommentPage++;
0547 
0548         $oggpageinfo = $this->ParseOggPageHeader();
0549         $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
0550 
0551         // First, save what we haven't read yet
0552         $AsYetUnusedData = substr($commentdata, $commentdataoffset);
0553 
0554         // Then take that data off the end
0555         $commentdata     = substr($commentdata, 0, $commentdataoffset);
0556 
0557         // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
0558         $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
0559         $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
0560 
0561         // Finally, stick the unused data back on the end
0562         $commentdata .= $AsYetUnusedData;
0563 
0564         //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
0565         if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
0566           $info['warning'][] = 'undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
0567           break;
0568         }
0569         $readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
0570         if ($readlength <= 0) {
0571           $info['warning'][] = 'invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
0572           break;
0573         }
0574         $commentdata .= $this->fread($readlength);
0575 
0576         //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
0577       }
0578       $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
0579       $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
0580       $commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size'];
0581 
0582       if (!$commentstring) {
0583 
0584         // no comment?
0585         $info['warning'][] = 'Blank Ogg comment ['.$i.']';
0586 
0587       } elseif (strstr($commentstring, '=')) {
0588 
0589         $commentexploded = explode('=', $commentstring, 2);
0590         $ThisFileInfo_ogg_comments_raw[$i]['key']   = strtoupper($commentexploded[0]);
0591         $ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : '');
0592 
0593         if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
0594 
0595           // http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
0596           // 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.
0597           // http://flac.sourceforge.net/format.html#metadata_block_picture
0598           $flac = new getid3_flac($this->getid3);
0599           $flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
0600           $flac->parsePICTURE();
0601           $info['ogg']['comments']['picture'][] = $flac->getid3->info['flac']['PICTURE'][0];
0602           unset($flac);
0603 
0604         } elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
0605 
0606           $data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
0607           $this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
0608           /** @todo use 'coverartmime' where available */
0609           $imageinfo = getid3_lib::GetDataImageSize($data);
0610           if ($imageinfo === false || !isset($imageinfo['mime'])) {
0611             $this->warning('COVERART vorbiscomment tag contains invalid image');
0612             continue;
0613           }
0614 
0615           $ogg = new self($this->getid3);
0616           $ogg->setStringMode($data);
0617           $info['ogg']['comments']['picture'][] = array(
0618             'image_mime' => $imageinfo['mime'],
0619             'data'       => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
0620           );
0621           unset($ogg);
0622 
0623         } else {
0624 
0625           $info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
0626 
0627         }
0628 
0629       } else {
0630 
0631         $info['warning'][] = '[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring;
0632 
0633       }
0634       unset($ThisFileInfo_ogg_comments_raw[$i]);
0635     }
0636     unset($ThisFileInfo_ogg_comments_raw);
0637 
0638 
0639     // Replay Gain Adjustment
0640     // http://privatewww.essex.ac.uk/~djmrob/replaygain/
0641     if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
0642       foreach ($info['ogg']['comments'] as $index => $commentvalue) {
0643         switch ($index) {
0644           case 'rg_audiophile':
0645           case 'replaygain_album_gain':
0646             $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
0647             unset($info['ogg']['comments'][$index]);
0648             break;
0649 
0650           case 'rg_radio':
0651           case 'replaygain_track_gain':
0652             $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
0653             unset($info['ogg']['comments'][$index]);
0654             break;
0655 
0656           case 'replaygain_album_peak':
0657             $info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
0658             unset($info['ogg']['comments'][$index]);
0659             break;
0660 
0661           case 'rg_peak':
0662           case 'replaygain_track_peak':
0663             $info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
0664             unset($info['ogg']['comments'][$index]);
0665             break;
0666 
0667           case 'replaygain_reference_loudness':
0668             $info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
0669             unset($info['ogg']['comments'][$index]);
0670             break;
0671 
0672           default:
0673             // do nothing
0674             break;
0675         }
0676       }
0677     }
0678 
0679     $this->fseek($OriginalOffset);
0680 
0681     return true;
0682   }
0683 
0684   public static function SpeexBandModeLookup($mode) {
0685     static $SpeexBandModeLookup = array();
0686     if (empty($SpeexBandModeLookup)) {
0687       $SpeexBandModeLookup[0] = 'narrow';
0688       $SpeexBandModeLookup[1] = 'wide';
0689       $SpeexBandModeLookup[2] = 'ultra-wide';
0690     }
0691     return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null);
0692   }
0693 
0694 
0695   public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
0696     for ($i = 0; $i < $SegmentNumber; $i++) {
0697       $segmentlength = 0;
0698       foreach ($OggInfoArray['segment_table'] as $key => $value) {
0699         $segmentlength += $value;
0700         if ($value < 255) {
0701           break;
0702         }
0703       }
0704     }
0705     return $segmentlength;
0706   }
0707 
0708 
0709   public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
0710 
0711     // decrease precision
0712     $nominal_bitrate = $nominal_bitrate / 1000;
0713 
0714     if ($nominal_bitrate < 128) {
0715       // q-1 to q4
0716       $qval = ($nominal_bitrate - 64) / 16;
0717     } elseif ($nominal_bitrate < 256) {
0718       // q4 to q8
0719       $qval = $nominal_bitrate / 32;
0720     } elseif ($nominal_bitrate < 320) {
0721       // q8 to q9
0722       $qval = ($nominal_bitrate + 256) / 64;
0723     } else {
0724       // q9 to q10
0725       $qval = ($nominal_bitrate + 1300) / 180;
0726     }
0727     //return $qval; // 5.031324
0728     //return intval($qval); // 5
0729     return round($qval, 1); // 5 or 4.9
0730   }
0731 
0732   public static function TheoraColorSpace($colorspace_id) {
0733     // http://www.theora.org/doc/Theora.pdf (table 6.3)
0734     static $TheoraColorSpaceLookup = array();
0735     if (empty($TheoraColorSpaceLookup)) {
0736       $TheoraColorSpaceLookup[0] = 'Undefined';
0737       $TheoraColorSpaceLookup[1] = 'Rec. 470M';
0738       $TheoraColorSpaceLookup[2] = 'Rec. 470BG';
0739       $TheoraColorSpaceLookup[3] = 'Reserved';
0740     }
0741     return (isset($TheoraColorSpaceLookup[$colorspace_id]) ? $TheoraColorSpaceLookup[$colorspace_id] : null);
0742   }
0743 
0744   public static function TheoraPixelFormat($pixelformat_id) {
0745     // http://www.theora.org/doc/Theora.pdf (table 6.4)
0746     static $TheoraPixelFormatLookup = array();
0747     if (empty($TheoraPixelFormatLookup)) {
0748       $TheoraPixelFormatLookup[0] = '4:2:0';
0749       $TheoraPixelFormatLookup[1] = 'Reserved';
0750       $TheoraPixelFormatLookup[2] = '4:2:2';
0751       $TheoraPixelFormatLookup[3] = '4:4:4';
0752     }
0753     return (isset($TheoraPixelFormatLookup[$pixelformat_id]) ? $TheoraPixelFormatLookup[$pixelformat_id] : null);
0754   }
0755 
0756 }