2 /////////////////////////////////////////////////////////////////
3 /// getID3() by James Heinrich <info@getid3.org> //
4 // available at http://getid3.sourceforge.net //
5 // or http://www.getid3.org //
6 // also https://github.com/JamesHeinrich/getID3 //
7 /////////////////////////////////////////////////////////////////
8 // See readme.txt for more details //
9 /////////////////////////////////////////////////////////////////
11 // module.audio.ogg.php //
12 // module for analyzing Ogg Vorbis, OggFLAC and Speex files //
13 // dependencies: module.audio.flac.php //
15 /////////////////////////////////////////////////////////////////
17 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true);
19 class getid3_ogg extends getid3_handler
21 // http://xiph.org/vorbis/doc/Vorbis_I_spec.html
22 public function Analyze() {
23 $info = &$this->getid3->info;
25 $info['fileformat'] = 'ogg';
27 // Warn about illegal tags - only vorbiscomments are allowed
28 if (isset($info['id3v2'])) {
29 $info['warning'][] = 'Illegal ID3v2 tag present.';
31 if (isset($info['id3v1'])) {
32 $info['warning'][] = 'Illegal ID3v1 tag present.';
34 if (isset($info['ape'])) {
35 $info['warning'][] = 'Illegal APE tag present.';
39 // Page 1 - Stream Header
41 $this->fseek($info['avdataoffset']);
43 $oggpageinfo = $this->ParseOggPageHeader();
44 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
46 if ($this->ftell() >= $this->getid3->fread_buffer_size()) {
47 $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?)';
48 unset($info['fileformat']);
53 $filedata = $this->fread($oggpageinfo['page_length']);
56 if (substr($filedata, 0, 4) == 'fLaC') {
58 $info['audio']['dataformat'] = 'flac';
59 $info['audio']['bitrate_mode'] = 'vbr';
60 $info['audio']['lossless'] = true;
62 } elseif (substr($filedata, 1, 6) == 'vorbis') {
64 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
66 } elseif (substr($filedata, 0, 8) == 'Speex ') {
68 // http://www.speex.org/manual/node10.html
70 $info['audio']['dataformat'] = 'speex';
71 $info['mime_type'] = 'audio/speex';
72 $info['audio']['bitrate_mode'] = 'abr';
73 $info['audio']['lossless'] = false;
75 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex '
77 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version'] = substr($filedata, $filedataoffset, 20);
78 $filedataoffset += 20;
79 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
81 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
83 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
85 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
87 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
89 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
91 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
93 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
95 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
97 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
99 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
100 $filedataoffset += 4;
101 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
102 $filedataoffset += 4;
103 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
104 $filedataoffset += 4;
106 $info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']);
107 $info['speex']['sample_rate'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'];
108 $info['speex']['channels'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'];
109 $info['speex']['vbr'] = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'];
110 $info['speex']['band_type'] = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']);
112 $info['audio']['sample_rate'] = $info['speex']['sample_rate'];
113 $info['audio']['channels'] = $info['speex']['channels'];
114 if ($info['speex']['vbr']) {
115 $info['audio']['bitrate_mode'] = 'vbr';
118 } elseif (substr($filedata, 0, 7) == "\x80".'theora') {
120 // http://www.theora.org/doc/Theora.pdf (section 6.2)
122 $info['ogg']['pageheader']['theora']['theora_magic'] = substr($filedata, $filedataoffset, 7); // hard-coded to "\x80.'theora'
123 $filedataoffset += 7;
124 $info['ogg']['pageheader']['theora']['version_major'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
125 $filedataoffset += 1;
126 $info['ogg']['pageheader']['theora']['version_minor'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
127 $filedataoffset += 1;
128 $info['ogg']['pageheader']['theora']['version_revision'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
129 $filedataoffset += 1;
130 $info['ogg']['pageheader']['theora']['frame_width_macroblocks'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 2));
131 $filedataoffset += 2;
132 $info['ogg']['pageheader']['theora']['frame_height_macroblocks'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 2));
133 $filedataoffset += 2;
134 $info['ogg']['pageheader']['theora']['resolution_x'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 3));
135 $filedataoffset += 3;
136 $info['ogg']['pageheader']['theora']['resolution_y'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 3));
137 $filedataoffset += 3;
138 $info['ogg']['pageheader']['theora']['picture_offset_x'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
139 $filedataoffset += 1;
140 $info['ogg']['pageheader']['theora']['picture_offset_y'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
141 $filedataoffset += 1;
142 $info['ogg']['pageheader']['theora']['frame_rate_numerator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 4));
143 $filedataoffset += 4;
144 $info['ogg']['pageheader']['theora']['frame_rate_denominator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 4));
145 $filedataoffset += 4;
146 $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 3));
147 $filedataoffset += 3;
148 $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 3));
149 $filedataoffset += 3;
150 $info['ogg']['pageheader']['theora']['color_space_id'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
151 $filedataoffset += 1;
152 $info['ogg']['pageheader']['theora']['nominal_bitrate'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 3));
153 $filedataoffset += 3;
154 $info['ogg']['pageheader']['theora']['flags'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 2));
155 $filedataoffset += 2;
157 $info['ogg']['pageheader']['theora']['quality'] = ($info['ogg']['pageheader']['theora']['flags'] & 0xFC00) >> 10;
158 $info['ogg']['pageheader']['theora']['kfg_shift'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x03E0) >> 5;
159 $info['ogg']['pageheader']['theora']['pixel_format_id'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0018) >> 3;
160 $info['ogg']['pageheader']['theora']['reserved'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0007) >> 0; // should be 0
161 $info['ogg']['pageheader']['theora']['color_space'] = self::TheoraColorSpace($info['ogg']['pageheader']['theora']['color_space_id']);
162 $info['ogg']['pageheader']['theora']['pixel_format'] = self::TheoraPixelFormat($info['ogg']['pageheader']['theora']['pixel_format_id']);
164 $info['video']['dataformat'] = 'theora';
165 $info['mime_type'] = 'video/ogg';
166 //$info['audio']['bitrate_mode'] = 'abr';
167 //$info['audio']['lossless'] = false;
168 $info['video']['resolution_x'] = $info['ogg']['pageheader']['theora']['resolution_x'];
169 $info['video']['resolution_y'] = $info['ogg']['pageheader']['theora']['resolution_y'];
170 if ($info['ogg']['pageheader']['theora']['frame_rate_denominator'] > 0) {
171 $info['video']['frame_rate'] = (float) $info['ogg']['pageheader']['theora']['frame_rate_numerator'] / $info['ogg']['pageheader']['theora']['frame_rate_denominator'];
173 if ($info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] > 0) {
174 $info['video']['pixel_aspect_ratio'] = (float) $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] / $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'];
176 $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';
179 } elseif (substr($filedata, 0, 8) == "fishead\x00") {
181 // Ogg Skeleton version 3.0 Format Specification
182 // http://xiph.org/ogg/doc/skeleton.html
183 $filedataoffset += 8;
184 $info['ogg']['skeleton']['fishead']['raw']['version_major'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
185 $filedataoffset += 2;
186 $info['ogg']['skeleton']['fishead']['raw']['version_minor'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
187 $filedataoffset += 2;
188 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
189 $filedataoffset += 8;
190 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
191 $filedataoffset += 8;
192 $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
193 $filedataoffset += 8;
194 $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
195 $filedataoffset += 8;
196 $info['ogg']['skeleton']['fishead']['raw']['utc'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 20));
197 $filedataoffset += 20;
199 $info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor'];
200 $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'];
201 $info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'];
202 $info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc'];
207 $oggpageinfo = $this->ParseOggPageHeader();
208 $info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++] = $oggpageinfo;
209 $filedata = $this->fread($oggpageinfo['page_length']);
210 $this->fseek($oggpageinfo['page_end_offset']);
212 if (substr($filedata, 0, 8) == "fisbone\x00") {
215 $info['ogg']['skeleton']['fisbone']['raw']['message_header_offset'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
216 $filedataoffset += 4;
217 $info['ogg']['skeleton']['fisbone']['raw']['serial_number'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
218 $filedataoffset += 4;
219 $info['ogg']['skeleton']['fisbone']['raw']['number_header_packets'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
220 $filedataoffset += 4;
221 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
222 $filedataoffset += 8;
223 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
224 $filedataoffset += 8;
225 $info['ogg']['skeleton']['fisbone']['raw']['basegranule'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
226 $filedataoffset += 8;
227 $info['ogg']['skeleton']['fisbone']['raw']['preroll'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
228 $filedataoffset += 4;
229 $info['ogg']['skeleton']['fisbone']['raw']['granuleshift'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
230 $filedataoffset += 1;
231 $info['ogg']['skeleton']['fisbone']['raw']['padding'] = substr($filedata, $filedataoffset, 3);
232 $filedataoffset += 3;
234 } elseif (substr($filedata, 1, 6) == 'theora') {
236 $info['video']['dataformat'] = 'theora1';
237 $info['error'][] = 'Ogg Theora (v1) not correctly handled in this version of getID3 ['.$this->getid3->version().']';
240 } elseif (substr($filedata, 1, 6) == 'vorbis') {
242 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
245 $info['error'][] = 'unexpected';
248 //} while ($oggpageinfo['page_seqno'] == 0);
249 } while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
251 $this->fseek($oggpageinfo['page_start_offset']);
253 $info['error'][] = 'Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']';
258 $info['error'][] = 'Expecting either "Speex " or "vorbis" identifier strings, found "'.substr($filedata, 0, 8).'"';
260 unset($info['mime_type']);
265 // Page 2 - Comment Header
266 $oggpageinfo = $this->ParseOggPageHeader();
267 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
269 switch ($info['audio']['dataformat']) {
271 $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
272 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, 0, 1));
273 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 1, 6); // hard-coded to 'vorbis'
275 $this->ParseVorbisComments();
279 $flac = new getid3_flac($this->getid3);
280 if (!$flac->parseMETAdata()) {
281 $info['error'][] = 'Failed to parse FLAC headers';
288 $this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR);
289 $this->ParseVorbisComments();
294 // Last Page - Number of Samples
295 if (!getid3_lib::intValueSupported($info['avdataend'])) {
297 $info['warning'][] = 'Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)';
301 $this->fseek(max($info['avdataend'] - $this->getid3->fread_buffer_size(), 0));
302 $LastChunkOfOgg = strrev($this->fread($this->getid3->fread_buffer_size()));
303 if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
304 $this->fseek($info['avdataend'] - ($LastOggSpostion + strlen('SggO')));
305 $info['avdataend'] = $this->ftell();
306 $info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader();
307 $info['ogg']['samples'] = $info['ogg']['pageheader']['eos']['pcm_abs_position'];
308 if ($info['ogg']['samples'] == 0) {
309 $info['error'][] = 'Corrupt Ogg file: eos.number of samples == zero';
312 if (!empty($info['audio']['sample_rate'])) {
313 $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
319 if (!empty($info['ogg']['bitrate_average'])) {
320 $info['audio']['bitrate'] = $info['ogg']['bitrate_average'];
321 } elseif (!empty($info['ogg']['bitrate_nominal'])) {
322 $info['audio']['bitrate'] = $info['ogg']['bitrate_nominal'];
323 } elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) {
324 $info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] + $info['ogg']['bitrate_max']) / 2;
326 if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
327 if ($info['audio']['bitrate'] == 0) {
328 $info['error'][] = 'Corrupt Ogg file: bitrate_audio == zero';
331 $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
334 if (isset($info['ogg']['vendor'])) {
335 $info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
338 if ($info['audio']['dataformat'] == 'vorbis') {
340 // Vorbis 1.0 starts with Xiph.Org
341 if (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
343 if ($info['audio']['bitrate_mode'] == 'abr') {
345 // Set -b 128 on abr files
346 $info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
348 } elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) {
349 // Set -q N on vbr files
350 $info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']);
355 if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) {
356 $info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps';
364 public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
365 $info = &$this->getid3->info;
366 $info['audio']['dataformat'] = 'vorbis';
367 $info['audio']['lossless'] = false;
369 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
370 $filedataoffset += 1;
371 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis'
372 $filedataoffset += 6;
373 $info['ogg']['bitstreamversion'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
374 $filedataoffset += 4;
375 $info['ogg']['numberofchannels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
376 $filedataoffset += 1;
377 $info['audio']['channels'] = $info['ogg']['numberofchannels'];
378 $info['ogg']['samplerate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
379 $filedataoffset += 4;
380 if ($info['ogg']['samplerate'] == 0) {
381 $info['error'][] = 'Corrupt Ogg file: sample rate == zero';
384 $info['audio']['sample_rate'] = $info['ogg']['samplerate'];
385 $info['ogg']['samples'] = 0; // filled in later
386 $info['ogg']['bitrate_average'] = 0; // filled in later
387 $info['ogg']['bitrate_max'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
388 $filedataoffset += 4;
389 $info['ogg']['bitrate_nominal'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
390 $filedataoffset += 4;
391 $info['ogg']['bitrate_min'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
392 $filedataoffset += 4;
393 $info['ogg']['blocksize_small'] = pow(2, getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F);
394 $info['ogg']['blocksize_large'] = pow(2, (getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4);
395 $info['ogg']['stop_bit'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet
397 $info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr
398 if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) {
399 unset($info['ogg']['bitrate_max']);
400 $info['audio']['bitrate_mode'] = 'abr';
402 if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
403 unset($info['ogg']['bitrate_nominal']);
405 if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
406 unset($info['ogg']['bitrate_min']);
407 $info['audio']['bitrate_mode'] = 'abr';
412 public function ParseOggPageHeader() {
413 // http://xiph.org/ogg/vorbis/doc/framing.html
414 $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file
416 $filedata = $this->fread($this->getid3->fread_buffer_size());
418 while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) {
419 if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) {
420 // should be found before here
423 if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) {
424 if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === false)) {
425 // get some more data, unless eof, in which case fail
430 $filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS'
432 $oggheader['stream_structver'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
433 $filedataoffset += 1;
434 $oggheader['flags_raw'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
435 $filedataoffset += 1;
436 $oggheader['flags']['fresh'] = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
437 $oggheader['flags']['bos'] = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
438 $oggheader['flags']['eos'] = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
440 $oggheader['pcm_abs_position'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
441 $filedataoffset += 8;
442 $oggheader['stream_serialno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
443 $filedataoffset += 4;
444 $oggheader['page_seqno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
445 $filedataoffset += 4;
446 $oggheader['page_checksum'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
447 $filedataoffset += 4;
448 $oggheader['page_segments'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
449 $filedataoffset += 1;
450 $oggheader['page_length'] = 0;
451 for ($i = 0; $i < $oggheader['page_segments']; $i++) {
452 $oggheader['segment_table'][$i] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
453 $filedataoffset += 1;
454 $oggheader['page_length'] += $oggheader['segment_table'][$i];
456 $oggheader['header_end_offset'] = $oggheader['page_start_offset'] + $filedataoffset;
457 $oggheader['page_end_offset'] = $oggheader['header_end_offset'] + $oggheader['page_length'];
458 $this->fseek($oggheader['header_end_offset']);
463 // http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
464 public function ParseVorbisComments() {
465 $info = &$this->getid3->info;
467 $OriginalOffset = $this->ftell();
468 $commentdataoffset = 0;
469 $VorbisCommentPage = 1;
471 switch ($info['audio']['dataformat']) {
474 $CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset']; // Second Ogg page, after header block
475 $this->fseek($CommentStartOffset);
476 $commentdataoffset = 27 + $info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
477 $commentdata = $this->fread(self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) + $commentdataoffset);
479 if ($info['audio']['dataformat'] == 'vorbis') {
480 $commentdataoffset += (strlen('vorbis') + 1);
485 $CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4;
486 $this->fseek($CommentStartOffset);
487 $commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
494 $VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
495 $commentdataoffset += 4;
497 $info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
498 $commentdataoffset += $VendorSize;
500 $CommentsCount = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
501 $commentdataoffset += 4;
502 $info['avdataoffset'] = $CommentStartOffset + $commentdataoffset;
504 $basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
505 $ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
506 for ($i = 0; $i < $CommentsCount; $i++) {
508 $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset;
510 if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) {
511 if ($oggpageinfo = $this->ParseOggPageHeader()) {
512 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
514 $VorbisCommentPage++;
516 // First, save what we haven't read yet
517 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
519 // Then take that data off the end
520 $commentdata = substr($commentdata, 0, $commentdataoffset);
522 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
523 $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
524 $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
526 // Finally, stick the unused data back on the end
527 $commentdata .= $AsYetUnusedData;
529 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
530 $commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
534 $ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
536 // replace avdataoffset with position just after the last vorbiscomment
537 $info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4;
539 $commentdataoffset += 4;
540 while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
541 if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
542 $info['warning'][] = 'Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments';
546 $VorbisCommentPage++;
548 $oggpageinfo = $this->ParseOggPageHeader();
549 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
551 // First, save what we haven't read yet
552 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
554 // Then take that data off the end
555 $commentdata = substr($commentdata, 0, $commentdataoffset);
557 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
558 $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
559 $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
561 // Finally, stick the unused data back on the end
562 $commentdata .= $AsYetUnusedData;
564 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
565 if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
566 $info['warning'][] = 'undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
569 $readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
570 if ($readlength <= 0) {
571 $info['warning'][] = 'invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
574 $commentdata .= $this->fread($readlength);
576 //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
578 $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
579 $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
580 $commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size'];
582 if (!$commentstring) {
585 $info['warning'][] = 'Blank Ogg comment ['.$i.']';
587 } elseif (strstr($commentstring, '=')) {
589 $commentexploded = explode('=', $commentstring, 2);
590 $ThisFileInfo_ogg_comments_raw[$i]['key'] = strtoupper($commentexploded[0]);
591 $ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : '');
593 if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
595 // http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
596 // 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.
597 // http://flac.sourceforge.net/format.html#metadata_block_picture
598 $flac = new getid3_flac($this->getid3);
599 $flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
600 $flac->parsePICTURE();
601 $info['ogg']['comments']['picture'][] = $flac->getid3->info['flac']['PICTURE'][0];
604 } elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
606 $data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
607 $this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
608 /** @todo use 'coverartmime' where available */
609 $imageinfo = getid3_lib::GetDataImageSize($data);
610 if ($imageinfo === false || !isset($imageinfo['mime'])) {
611 $this->warning('COVERART vorbiscomment tag contains invalid image');
615 $ogg = new self($this->getid3);
616 $ogg->setStringMode($data);
617 $info['ogg']['comments']['picture'][] = array(
618 'image_mime' => $imageinfo['mime'],
619 'data' => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
625 $info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
631 $info['warning'][] = '[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring;
634 unset($ThisFileInfo_ogg_comments_raw[$i]);
636 unset($ThisFileInfo_ogg_comments_raw);
639 // Replay Gain Adjustment
640 // http://privatewww.essex.ac.uk/~djmrob/replaygain/
641 if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
642 foreach ($info['ogg']['comments'] as $index => $commentvalue) {
644 case 'rg_audiophile':
645 case 'replaygain_album_gain':
646 $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
647 unset($info['ogg']['comments'][$index]);
651 case 'replaygain_track_gain':
652 $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
653 unset($info['ogg']['comments'][$index]);
656 case 'replaygain_album_peak':
657 $info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
658 unset($info['ogg']['comments'][$index]);
662 case 'replaygain_track_peak':
663 $info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
664 unset($info['ogg']['comments'][$index]);
667 case 'replaygain_reference_loudness':
668 $info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
669 unset($info['ogg']['comments'][$index]);
679 $this->fseek($OriginalOffset);
684 public static function SpeexBandModeLookup($mode) {
685 static $SpeexBandModeLookup = array();
686 if (empty($SpeexBandModeLookup)) {
687 $SpeexBandModeLookup[0] = 'narrow';
688 $SpeexBandModeLookup[1] = 'wide';
689 $SpeexBandModeLookup[2] = 'ultra-wide';
691 return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null);
695 public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
696 for ($i = 0; $i < $SegmentNumber; $i++) {
698 foreach ($OggInfoArray['segment_table'] as $key => $value) {
699 $segmentlength += $value;
705 return $segmentlength;
709 public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
711 // decrease precision
712 $nominal_bitrate = $nominal_bitrate / 1000;
714 if ($nominal_bitrate < 128) {
716 $qval = ($nominal_bitrate - 64) / 16;
717 } elseif ($nominal_bitrate < 256) {
719 $qval = $nominal_bitrate / 32;
720 } elseif ($nominal_bitrate < 320) {
722 $qval = ($nominal_bitrate + 256) / 64;
725 $qval = ($nominal_bitrate + 1300) / 180;
727 //return $qval; // 5.031324
728 //return intval($qval); // 5
729 return round($qval, 1); // 5 or 4.9
732 public static function TheoraColorSpace($colorspace_id) {
733 // http://www.theora.org/doc/Theora.pdf (table 6.3)
734 static $TheoraColorSpaceLookup = array();
735 if (empty($TheoraColorSpaceLookup)) {
736 $TheoraColorSpaceLookup[0] = 'Undefined';
737 $TheoraColorSpaceLookup[1] = 'Rec. 470M';
738 $TheoraColorSpaceLookup[2] = 'Rec. 470BG';
739 $TheoraColorSpaceLookup[3] = 'Reserved';
741 return (isset($TheoraColorSpaceLookup[$colorspace_id]) ? $TheoraColorSpaceLookup[$colorspace_id] : null);
744 public static function TheoraPixelFormat($pixelformat_id) {
745 // http://www.theora.org/doc/Theora.pdf (table 6.4)
746 static $TheoraPixelFormatLookup = array();
747 if (empty($TheoraPixelFormatLookup)) {
748 $TheoraPixelFormatLookup[0] = '4:2:0';
749 $TheoraPixelFormatLookup[1] = 'Reserved';
750 $TheoraPixelFormatLookup[2] = '4:2:2';
751 $TheoraPixelFormatLookup[3] = '4:4:4';
753 return (isset($TheoraPixelFormatLookup[$pixelformat_id]) ? $TheoraPixelFormatLookup[$pixelformat_id] : null);