2 /////////////////////////////////////////////////////////////////
3 /// getID3() by James Heinrich <info@getid3.org> //
4 // available at http://getid3.sourceforge.net //
5 // or http://www.getid3.org //
6 /////////////////////////////////////////////////////////////////
7 // See readme.txt for more details //
8 /////////////////////////////////////////////////////////////////
10 // module.audio.ogg.php //
11 // module for analyzing Ogg Vorbis, OggFLAC and Speex files //
12 // dependencies: module.audio.flac.php //
14 /////////////////////////////////////////////////////////////////
16 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true);
18 class getid3_ogg extends getid3_handler
20 // http://xiph.org/vorbis/doc/Vorbis_I_spec.html
21 public function Analyze() {
22 $info = &$this->getid3->info;
24 $info['fileformat'] = 'ogg';
26 // Warn about illegal tags - only vorbiscomments are allowed
27 if (isset($info['id3v2'])) {
28 $info['warning'][] = 'Illegal ID3v2 tag present.';
30 if (isset($info['id3v1'])) {
31 $info['warning'][] = 'Illegal ID3v1 tag present.';
33 if (isset($info['ape'])) {
34 $info['warning'][] = 'Illegal APE tag present.';
38 // Page 1 - Stream Header
40 $this->fseek($info['avdataoffset']);
42 $oggpageinfo = $this->ParseOggPageHeader();
43 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
45 if ($this->ftell() >= $this->getid3->fread_buffer_size()) {
46 $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?)';
47 unset($info['fileformat']);
52 $filedata = $this->fread($oggpageinfo['page_length']);
55 if (substr($filedata, 0, 4) == 'fLaC') {
57 $info['audio']['dataformat'] = 'flac';
58 $info['audio']['bitrate_mode'] = 'vbr';
59 $info['audio']['lossless'] = true;
61 } elseif (substr($filedata, 1, 6) == 'vorbis') {
63 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
65 } elseif (substr($filedata, 0, 8) == 'Speex ') {
67 // http://www.speex.org/manual/node10.html
69 $info['audio']['dataformat'] = 'speex';
70 $info['mime_type'] = 'audio/speex';
71 $info['audio']['bitrate_mode'] = 'abr';
72 $info['audio']['lossless'] = false;
74 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex '
76 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version'] = substr($filedata, $filedataoffset, 20);
77 $filedataoffset += 20;
78 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
80 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
82 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
84 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
86 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
88 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
90 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
92 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
94 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
96 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
98 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
100 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
101 $filedataoffset += 4;
102 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
103 $filedataoffset += 4;
105 $info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']);
106 $info['speex']['sample_rate'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'];
107 $info['speex']['channels'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'];
108 $info['speex']['vbr'] = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'];
109 $info['speex']['band_type'] = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']);
111 $info['audio']['sample_rate'] = $info['speex']['sample_rate'];
112 $info['audio']['channels'] = $info['speex']['channels'];
113 if ($info['speex']['vbr']) {
114 $info['audio']['bitrate_mode'] = 'vbr';
118 } elseif (substr($filedata, 0, 8) == "fishead\x00") {
120 // Ogg Skeleton version 3.0 Format Specification
121 // http://xiph.org/ogg/doc/skeleton.html
122 $filedataoffset += 8;
123 $info['ogg']['skeleton']['fishead']['raw']['version_major'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
124 $filedataoffset += 2;
125 $info['ogg']['skeleton']['fishead']['raw']['version_minor'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
126 $filedataoffset += 2;
127 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
128 $filedataoffset += 8;
129 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
130 $filedataoffset += 8;
131 $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
132 $filedataoffset += 8;
133 $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
134 $filedataoffset += 8;
135 $info['ogg']['skeleton']['fishead']['raw']['utc'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 20));
136 $filedataoffset += 20;
138 $info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor'];
139 $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'];
140 $info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'];
141 $info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc'];
146 $oggpageinfo = $this->ParseOggPageHeader();
147 $info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++] = $oggpageinfo;
148 $filedata = $this->fread($oggpageinfo['page_length']);
149 $this->fseek($oggpageinfo['page_end_offset']);
151 if (substr($filedata, 0, 8) == "fisbone\x00") {
154 $info['ogg']['skeleton']['fisbone']['raw']['message_header_offset'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
155 $filedataoffset += 4;
156 $info['ogg']['skeleton']['fisbone']['raw']['serial_number'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
157 $filedataoffset += 4;
158 $info['ogg']['skeleton']['fisbone']['raw']['number_header_packets'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
159 $filedataoffset += 4;
160 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
161 $filedataoffset += 8;
162 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
163 $filedataoffset += 8;
164 $info['ogg']['skeleton']['fisbone']['raw']['basegranule'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
165 $filedataoffset += 8;
166 $info['ogg']['skeleton']['fisbone']['raw']['preroll'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
167 $filedataoffset += 4;
168 $info['ogg']['skeleton']['fisbone']['raw']['granuleshift'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
169 $filedataoffset += 1;
170 $info['ogg']['skeleton']['fisbone']['raw']['padding'] = substr($filedata, $filedataoffset, 3);
171 $filedataoffset += 3;
173 } elseif (substr($filedata, 1, 6) == 'theora') {
175 $info['video']['dataformat'] = 'theora';
176 $info['error'][] = 'Ogg Theora not correctly handled in this version of getID3 ['.$this->getid3->version().']';
179 } elseif (substr($filedata, 1, 6) == 'vorbis') {
181 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
184 $info['error'][] = 'unexpected';
187 //} while ($oggpageinfo['page_seqno'] == 0);
188 } while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
190 $this->fseek($oggpageinfo['page_start_offset']);
192 $info['error'][] = 'Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']';
197 $info['error'][] = 'Expecting either "Speex " or "vorbis" identifier strings, found "'.substr($filedata, 0, 8).'"';
199 unset($info['mime_type']);
204 // Page 2 - Comment Header
205 $oggpageinfo = $this->ParseOggPageHeader();
206 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
208 switch ($info['audio']['dataformat']) {
210 $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
211 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, 0, 1));
212 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 1, 6); // hard-coded to 'vorbis'
214 $this->ParseVorbisComments();
218 $flac = new getid3_flac($this->getid3);
219 if (!$flac->parseMETAdata()) {
220 $info['error'][] = 'Failed to parse FLAC headers';
227 $this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR);
228 $this->ParseVorbisComments();
233 // Last Page - Number of Samples
234 if (!getid3_lib::intValueSupported($info['avdataend'])) {
236 $info['warning'][] = 'Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)';
240 $this->fseek(max($info['avdataend'] - $this->getid3->fread_buffer_size(), 0));
241 $LastChunkOfOgg = strrev($this->fread($this->getid3->fread_buffer_size()));
242 if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
243 $this->fseek($info['avdataend'] - ($LastOggSpostion + strlen('SggO')));
244 $info['avdataend'] = $this->ftell();
245 $info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader();
246 $info['ogg']['samples'] = $info['ogg']['pageheader']['eos']['pcm_abs_position'];
247 if ($info['ogg']['samples'] == 0) {
248 $info['error'][] = 'Corrupt Ogg file: eos.number of samples == zero';
251 if (!empty($info['audio']['sample_rate'])) {
252 $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
258 if (!empty($info['ogg']['bitrate_average'])) {
259 $info['audio']['bitrate'] = $info['ogg']['bitrate_average'];
260 } elseif (!empty($info['ogg']['bitrate_nominal'])) {
261 $info['audio']['bitrate'] = $info['ogg']['bitrate_nominal'];
262 } elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) {
263 $info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] + $info['ogg']['bitrate_max']) / 2;
265 if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
266 if ($info['audio']['bitrate'] == 0) {
267 $info['error'][] = 'Corrupt Ogg file: bitrate_audio == zero';
270 $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
273 if (isset($info['ogg']['vendor'])) {
274 $info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
277 if ($info['audio']['dataformat'] == 'vorbis') {
279 // Vorbis 1.0 starts with Xiph.Org
280 if (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
282 if ($info['audio']['bitrate_mode'] == 'abr') {
284 // Set -b 128 on abr files
285 $info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
287 } elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) {
288 // Set -q N on vbr files
289 $info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']);
294 if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) {
295 $info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps';
303 public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
304 $info = &$this->getid3->info;
305 $info['audio']['dataformat'] = 'vorbis';
306 $info['audio']['lossless'] = false;
308 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
309 $filedataoffset += 1;
310 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis'
311 $filedataoffset += 6;
312 $info['ogg']['bitstreamversion'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
313 $filedataoffset += 4;
314 $info['ogg']['numberofchannels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
315 $filedataoffset += 1;
316 $info['audio']['channels'] = $info['ogg']['numberofchannels'];
317 $info['ogg']['samplerate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
318 $filedataoffset += 4;
319 if ($info['ogg']['samplerate'] == 0) {
320 $info['error'][] = 'Corrupt Ogg file: sample rate == zero';
323 $info['audio']['sample_rate'] = $info['ogg']['samplerate'];
324 $info['ogg']['samples'] = 0; // filled in later
325 $info['ogg']['bitrate_average'] = 0; // filled in later
326 $info['ogg']['bitrate_max'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
327 $filedataoffset += 4;
328 $info['ogg']['bitrate_nominal'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
329 $filedataoffset += 4;
330 $info['ogg']['bitrate_min'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
331 $filedataoffset += 4;
332 $info['ogg']['blocksize_small'] = pow(2, getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F);
333 $info['ogg']['blocksize_large'] = pow(2, (getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4);
334 $info['ogg']['stop_bit'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet
336 $info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr
337 if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) {
338 unset($info['ogg']['bitrate_max']);
339 $info['audio']['bitrate_mode'] = 'abr';
341 if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
342 unset($info['ogg']['bitrate_nominal']);
344 if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
345 unset($info['ogg']['bitrate_min']);
346 $info['audio']['bitrate_mode'] = 'abr';
351 public function ParseOggPageHeader() {
352 // http://xiph.org/ogg/vorbis/doc/framing.html
353 $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file
355 $filedata = $this->fread($this->getid3->fread_buffer_size());
357 while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) {
358 if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) {
359 // should be found before here
362 if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) {
363 if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === false)) {
364 // get some more data, unless eof, in which case fail
369 $filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS'
371 $oggheader['stream_structver'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
372 $filedataoffset += 1;
373 $oggheader['flags_raw'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
374 $filedataoffset += 1;
375 $oggheader['flags']['fresh'] = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
376 $oggheader['flags']['bos'] = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
377 $oggheader['flags']['eos'] = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
379 $oggheader['pcm_abs_position'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
380 $filedataoffset += 8;
381 $oggheader['stream_serialno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
382 $filedataoffset += 4;
383 $oggheader['page_seqno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
384 $filedataoffset += 4;
385 $oggheader['page_checksum'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
386 $filedataoffset += 4;
387 $oggheader['page_segments'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
388 $filedataoffset += 1;
389 $oggheader['page_length'] = 0;
390 for ($i = 0; $i < $oggheader['page_segments']; $i++) {
391 $oggheader['segment_table'][$i] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
392 $filedataoffset += 1;
393 $oggheader['page_length'] += $oggheader['segment_table'][$i];
395 $oggheader['header_end_offset'] = $oggheader['page_start_offset'] + $filedataoffset;
396 $oggheader['page_end_offset'] = $oggheader['header_end_offset'] + $oggheader['page_length'];
397 $this->fseek($oggheader['header_end_offset']);
402 // http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
403 public function ParseVorbisComments() {
404 $info = &$this->getid3->info;
406 $OriginalOffset = $this->ftell();
407 $commentdataoffset = 0;
408 $VorbisCommentPage = 1;
410 switch ($info['audio']['dataformat']) {
413 $CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset']; // Second Ogg page, after header block
414 $this->fseek($CommentStartOffset);
415 $commentdataoffset = 27 + $info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
416 $commentdata = $this->fread(self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) + $commentdataoffset);
418 if ($info['audio']['dataformat'] == 'vorbis') {
419 $commentdataoffset += (strlen('vorbis') + 1);
424 $CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4;
425 $this->fseek($CommentStartOffset);
426 $commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
433 $VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
434 $commentdataoffset += 4;
436 $info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
437 $commentdataoffset += $VendorSize;
439 $CommentsCount = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
440 $commentdataoffset += 4;
441 $info['avdataoffset'] = $CommentStartOffset + $commentdataoffset;
443 $basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
444 $ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
445 for ($i = 0; $i < $CommentsCount; $i++) {
447 $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset;
449 if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) {
450 if ($oggpageinfo = $this->ParseOggPageHeader()) {
451 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
453 $VorbisCommentPage++;
455 // First, save what we haven't read yet
456 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
458 // Then take that data off the end
459 $commentdata = substr($commentdata, 0, $commentdataoffset);
461 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
462 $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
463 $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
465 // Finally, stick the unused data back on the end
466 $commentdata .= $AsYetUnusedData;
468 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
469 $commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
473 $ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
475 // replace avdataoffset with position just after the last vorbiscomment
476 $info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4;
478 $commentdataoffset += 4;
479 while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
480 if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
481 $info['warning'][] = 'Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments';
485 $VorbisCommentPage++;
487 $oggpageinfo = $this->ParseOggPageHeader();
488 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
490 // First, save what we haven't read yet
491 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
493 // Then take that data off the end
494 $commentdata = substr($commentdata, 0, $commentdataoffset);
496 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
497 $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
498 $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
500 // Finally, stick the unused data back on the end
501 $commentdata .= $AsYetUnusedData;
503 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
504 if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
505 $info['warning'][] = 'undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
508 $readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
509 if ($readlength <= 0) {
510 $info['warning'][] = 'invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
513 $commentdata .= $this->fread($readlength);
515 //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
517 $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
518 $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
519 $commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size'];
521 if (!$commentstring) {
524 $info['warning'][] = 'Blank Ogg comment ['.$i.']';
526 } elseif (strstr($commentstring, '=')) {
528 $commentexploded = explode('=', $commentstring, 2);
529 $ThisFileInfo_ogg_comments_raw[$i]['key'] = strtoupper($commentexploded[0]);
530 $ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : '');
532 if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
534 // http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
535 // 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.
536 // http://flac.sourceforge.net/format.html#metadata_block_picture
537 $flac = new getid3_flac($this->getid3);
538 $flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
539 $flac->parsePICTURE();
540 $info['ogg']['comments']['picture'][] = $flac->getid3->info['flac']['PICTURE'][0];
543 } elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
545 $data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
546 $this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
547 /** @todo use 'coverartmime' where available */
548 $imageinfo = getid3_lib::GetDataImageSize($data);
549 if ($imageinfo === false || !isset($imageinfo['mime'])) {
550 $this->warning('COVERART vorbiscomment tag contains invalid image');
554 $ogg = new self($this->getid3);
555 $ogg->setStringMode($data);
556 $info['ogg']['comments']['picture'][] = array(
557 'image_mime' => $imageinfo['mime'],
558 'data' => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
564 $info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
570 $info['warning'][] = '[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring;
573 unset($ThisFileInfo_ogg_comments_raw[$i]);
575 unset($ThisFileInfo_ogg_comments_raw);
578 // Replay Gain Adjustment
579 // http://privatewww.essex.ac.uk/~djmrob/replaygain/
580 if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
581 foreach ($info['ogg']['comments'] as $index => $commentvalue) {
583 case 'rg_audiophile':
584 case 'replaygain_album_gain':
585 $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
586 unset($info['ogg']['comments'][$index]);
590 case 'replaygain_track_gain':
591 $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
592 unset($info['ogg']['comments'][$index]);
595 case 'replaygain_album_peak':
596 $info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
597 unset($info['ogg']['comments'][$index]);
601 case 'replaygain_track_peak':
602 $info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
603 unset($info['ogg']['comments'][$index]);
606 case 'replaygain_reference_loudness':
607 $info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
608 unset($info['ogg']['comments'][$index]);
618 $this->fseek($OriginalOffset);
623 public static function SpeexBandModeLookup($mode) {
624 static $SpeexBandModeLookup = array();
625 if (empty($SpeexBandModeLookup)) {
626 $SpeexBandModeLookup[0] = 'narrow';
627 $SpeexBandModeLookup[1] = 'wide';
628 $SpeexBandModeLookup[2] = 'ultra-wide';
630 return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null);
634 public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
635 for ($i = 0; $i < $SegmentNumber; $i++) {
637 foreach ($OggInfoArray['segment_table'] as $key => $value) {
638 $segmentlength += $value;
644 return $segmentlength;
648 public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
650 // decrease precision
651 $nominal_bitrate = $nominal_bitrate / 1000;
653 if ($nominal_bitrate < 128) {
655 $qval = ($nominal_bitrate - 64) / 16;
656 } elseif ($nominal_bitrate < 256) {
658 $qval = $nominal_bitrate / 32;
659 } elseif ($nominal_bitrate < 320) {
661 $qval = ($nominal_bitrate + 256) / 64;
664 $qval = ($nominal_bitrate + 1300) / 180;
666 //return $qval; // 5.031324
667 //return intval($qval); // 5
668 return round($qval, 1); // 5 or 4.9