ILIAS  release_5-2 Revision v5.2.25-18-g3f80b828510
Ogg.php
Go to the documentation of this file.
1<?php
2
3namespace GetId3\Module\Audio;
4
8
11// available at http://getid3.sourceforge.net //
12// or http://www.getid3.org //
14// See readme.txt for more details //
16// //
17// module.audio.ogg.php //
18// module for analyzing Ogg Vorbis, OggFLAC and Speex files //
19// dependencies: module.audio.flac.php //
20// ///
22
30class Ogg extends BaseHandler
31{
36 public $inline_attachments = true; // true: return full data for all attachments; false: return no data for all attachments; integer: return data for attachments <= than this; string: save as file to this directory
37
42 public function analyze()
43 {
44 $info = &$this->getid3->info;
45
46 $info['fileformat'] = 'ogg';
47
48 // Warn about illegal tags - only vorbiscomments are allowed
49 if (isset($info['id3v2'])) {
50 $info['warning'][] = 'Illegal ID3v2 tag present.';
51 }
52 if (isset($info['id3v1'])) {
53 $info['warning'][] = 'Illegal ID3v1 tag present.';
54 }
55 if (isset($info['ape'])) {
56 $info['warning'][] = 'Illegal APE tag present.';
57 }
58
59 // Page 1 - Stream Header
60
61 $this->fseek($info['avdataoffset']);
62
63 $oggpageinfo = $this->ParseOggPageHeader();
64 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
65
66 if ($this->ftell() >= $this->getid3->fread_buffer_size()) {
67 $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?)';
68 unset($info['fileformat']);
69 unset($info['ogg']);
70
71 return false;
72 }
73
74 $filedata = $this->fread($oggpageinfo['page_length']);
75 $filedataoffset = 0;
76
77 if (substr($filedata, 0, 4) == 'fLaC') {
78
79 $info['audio']['dataformat'] = 'flac';
80 $info['audio']['bitrate_mode'] = 'vbr';
81 $info['audio']['lossless'] = true;
82
83 } elseif (substr($filedata, 1, 6) == 'vorbis') {
84
85 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
86
87 } elseif (substr($filedata, 0, 8) == 'Speex ') {
88
89 // http://www.speex.org/manual/node10.html
90
91 $info['audio']['dataformat'] = 'speex';
92 $info['mime_type'] = 'audio/speex';
93 $info['audio']['bitrate_mode'] = 'abr';
94 $info['audio']['lossless'] = false;
95
96 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex '
97 $filedataoffset += 8;
98 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version'] = substr($filedata, $filedataoffset, 20);
99 $filedataoffset += 20;
100 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
101 $filedataoffset += 4;
102 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
103 $filedataoffset += 4;
104 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
105 $filedataoffset += 4;
106 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
107 $filedataoffset += 4;
108 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
109 $filedataoffset += 4;
110 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
111 $filedataoffset += 4;
112 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
113 $filedataoffset += 4;
114 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
115 $filedataoffset += 4;
116 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
117 $filedataoffset += 4;
118 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
119 $filedataoffset += 4;
120 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
121 $filedataoffset += 4;
122 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
123 $filedataoffset += 4;
124 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
125 $filedataoffset += 4;
126
127 $info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']);
128 $info['speex']['sample_rate'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'];
129 $info['speex']['channels'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'];
130 $info['speex']['vbr'] = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'];
131 $info['speex']['band_type'] = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']);
132
133 $info['audio']['sample_rate'] = $info['speex']['sample_rate'];
134 $info['audio']['channels'] = $info['speex']['channels'];
135 if ($info['speex']['vbr']) {
136 $info['audio']['bitrate_mode'] = 'vbr';
137 }
138
139 } elseif (substr($filedata, 0, 8) == "fishead\x00") {
140
141 // Ogg Skeleton version 3.0 Format Specification
142 // http://xiph.org/ogg/doc/skeleton.html
143 $filedataoffset += 8;
144 $info['ogg']['skeleton']['fishead']['raw']['version_major'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
145 $filedataoffset += 2;
146 $info['ogg']['skeleton']['fishead']['raw']['version_minor'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
147 $filedataoffset += 2;
148 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
149 $filedataoffset += 8;
150 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
151 $filedataoffset += 8;
152 $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
153 $filedataoffset += 8;
154 $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
155 $filedataoffset += 8;
156 $info['ogg']['skeleton']['fishead']['raw']['utc'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 20));
157 $filedataoffset += 20;
158
159 $info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor'];
160 $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'];
161 $info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'];
162 $info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc'];
163
164 $counter = 0;
165 do {
166 $oggpageinfo = $this->ParseOggPageHeader();
167 $info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++] = $oggpageinfo;
168 $filedata = $this->fread($oggpageinfo['page_length']);
169 $this->fseek($oggpageinfo['page_end_offset']);
170
171 if (substr($filedata, 0, 8) == "fisbone\x00") {
172
173 $filedataoffset = 8;
174 $info['ogg']['skeleton']['fisbone']['raw']['message_header_offset'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
175 $filedataoffset += 4;
176 $info['ogg']['skeleton']['fisbone']['raw']['serial_number'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
177 $filedataoffset += 4;
178 $info['ogg']['skeleton']['fisbone']['raw']['number_header_packets'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
179 $filedataoffset += 4;
180 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
181 $filedataoffset += 8;
182 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
183 $filedataoffset += 8;
184 $info['ogg']['skeleton']['fisbone']['raw']['basegranule'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
185 $filedataoffset += 8;
186 $info['ogg']['skeleton']['fisbone']['raw']['preroll'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
187 $filedataoffset += 4;
188 $info['ogg']['skeleton']['fisbone']['raw']['granuleshift'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
189 $filedataoffset += 1;
190 $info['ogg']['skeleton']['fisbone']['raw']['padding'] = substr($filedata, $filedataoffset, 3);
191 $filedataoffset += 3;
192
193 } elseif (substr($filedata, 1, 6) == 'theora') {
194
195 $info['video']['dataformat'] = 'theora';
196 $info['error'][] = 'Ogg Theora not correctly handled in this version of GetId3 ['.$this->getid3->version().']';
197 //break;
198
199 } elseif (substr($filedata, 1, 6) == 'vorbis') {
200
201 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
202
203 } else {
204 $info['error'][] = 'unexpected';
205 //break;
206 }
207 //} while ($oggpageinfo['page_seqno'] == 0);
208 } while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
209
210 $this->fseek($oggpageinfo['page_start_offset']);
211
212 $info['error'][] = 'Ogg Skeleton not correctly handled in this version of GetId3 ['.$this->getid3->version().']';
213 //return false;
214
215 } else {
216
217 $info['error'][] = 'Expecting either "Speex " or "vorbis" identifier strings, found "'.substr($filedata, 0, 8).'"';
218 unset($info['ogg']);
219 unset($info['mime_type']);
220
221 return false;
222
223 }
224
225 // Page 2 - Comment Header
226 $oggpageinfo = $this->ParseOggPageHeader();
227 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
228
229 switch ($info['audio']['dataformat']) {
230 case 'vorbis':
231 $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
232 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = Helper::LittleEndian2Int(substr($filedata, 0, 1));
233 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 1, 6); // hard-coded to 'vorbis'
234
235 $this->ParseVorbisComments();
236 break;
237
238 case 'flac':
239 $getid3_flac = new Flac($this->getid3);
240 if (!$getid3_flac->parseMETAdata()) {
241 $info['error'][] = 'Failed to parse FLAC headers';
242
243 return false;
244 }
245 unset($getid3_flac);
246 break;
247
248 case 'speex':
249 $this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR);
250 $this->ParseVorbisComments();
251 break;
252
253 }
254
255 // Last Page - Number of Samples
256
257 if (!Helper::intValueSupported($info['avdataend'])) {
258
259 $info['warning'][] = 'Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)';
260
261 } else {
262
263 $this->fseek(max($info['avdataend'] - $this->getid3->fread_buffer_size(), 0));
264 $LastChunkOfOgg = strrev($this->fread($this->getid3->fread_buffer_size()));
265 if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
266 $this->fseek($info['avdataend'] - ($LastOggSpostion + strlen('SggO')));
267 $info['avdataend'] = $this->ftell();
268 $info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader();
269 $info['ogg']['samples'] = $info['ogg']['pageheader']['eos']['pcm_abs_position'];
270 if ($info['ogg']['samples'] == 0) {
271 $info['error'][] = 'Corrupt Ogg file: eos.number of samples == zero';
272
273 return false;
274 }
275 if (!empty($info['audio']['sample_rate'])) {
276 $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
277 }
278 }
279
280 }
281
282 if (!empty($info['ogg']['bitrate_average'])) {
283 $info['audio']['bitrate'] = $info['ogg']['bitrate_average'];
284 } elseif (!empty($info['ogg']['bitrate_nominal'])) {
285 $info['audio']['bitrate'] = $info['ogg']['bitrate_nominal'];
286 } elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) {
287 $info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] + $info['ogg']['bitrate_max']) / 2;
288 }
289 if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
290 if ($info['audio']['bitrate'] == 0) {
291 $info['error'][] = 'Corrupt Ogg file: bitrate_audio == zero';
292
293 return false;
294 }
295 $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
296 }
297
298 if (isset($info['ogg']['vendor'])) {
299 $info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
300
301 // Vorbis only
302 if ($info['audio']['dataformat'] == 'vorbis') {
303
304 // Vorbis 1.0 starts with Xiph.Org
305 if (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
306
307 if ($info['audio']['bitrate_mode'] == 'abr') {
308
309 // Set -b 128 on abr files
310 $info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
311
312 } elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) {
313 // Set -q N on vbr files
314 $info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']);
315
316 }
317 }
318
319 if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) {
320 $info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps';
321 }
322 }
323 }
324
325 return true;
326 }
327
328 public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo)
329 {
330 $info = &$this->getid3->info;
331 $info['audio']['dataformat'] = 'vorbis';
332 $info['audio']['lossless'] = false;
333
334 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
335 $filedataoffset += 1;
336 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis'
337 $filedataoffset += 6;
338 $info['ogg']['bitstreamversion'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
339 $filedataoffset += 4;
340 $info['ogg']['numberofchannels'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
341 $filedataoffset += 1;
342 $info['audio']['channels'] = $info['ogg']['numberofchannels'];
343 $info['ogg']['samplerate'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
344 $filedataoffset += 4;
345 if ($info['ogg']['samplerate'] == 0) {
346 $info['error'][] = 'Corrupt Ogg file: sample rate == zero';
347
348 return false;
349 }
350 $info['audio']['sample_rate'] = $info['ogg']['samplerate'];
351 $info['ogg']['samples'] = 0; // filled in later
352 $info['ogg']['bitrate_average'] = 0; // filled in later
353 $info['ogg']['bitrate_max'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
354 $filedataoffset += 4;
355 $info['ogg']['bitrate_nominal'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
356 $filedataoffset += 4;
357 $info['ogg']['bitrate_min'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
358 $filedataoffset += 4;
359 $info['ogg']['blocksize_small'] = pow(2, Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F);
360 $info['ogg']['blocksize_large'] = pow(2, (Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4);
361 $info['ogg']['stop_bit'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet
362
363 $info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr
364 if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) {
365 unset($info['ogg']['bitrate_max']);
366 $info['audio']['bitrate_mode'] = 'abr';
367 }
368 if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
369 unset($info['ogg']['bitrate_nominal']);
370 }
371 if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
372 unset($info['ogg']['bitrate_min']);
373 $info['audio']['bitrate_mode'] = 'abr';
374 }
375
376 return true;
377 }
378
384 public function ParseOggPageHeader()
385 {
386 $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file
387
388 $filedata = $this->fread($this->getid3->fread_buffer_size());
389 $filedataoffset = 0;
390 while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) {
391 if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) {
392 // should be found before here
393 return false;
394 }
395 if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) {
396 if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === false)) {
397 // get some more data, unless eof, in which case fail
398 return false;
399 }
400 }
401 }
402 $filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS'
403
404 $oggheader['stream_structver'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
405 $filedataoffset += 1;
406 $oggheader['flags_raw'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
407 $filedataoffset += 1;
408 $oggheader['flags']['fresh'] = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
409 $oggheader['flags']['bos'] = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
410 $oggheader['flags']['eos'] = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
411
412 $oggheader['pcm_abs_position'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
413 $filedataoffset += 8;
414 $oggheader['stream_serialno'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
415 $filedataoffset += 4;
416 $oggheader['page_seqno'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
417 $filedataoffset += 4;
418 $oggheader['page_checksum'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
419 $filedataoffset += 4;
420 $oggheader['page_segments'] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
421 $filedataoffset += 1;
422 $oggheader['page_length'] = 0;
423 for ($i = 0; $i < $oggheader['page_segments']; $i++) {
424 $oggheader['segment_table'][$i] = Helper::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
425 $filedataoffset += 1;
426 $oggheader['page_length'] += $oggheader['segment_table'][$i];
427 }
428 $oggheader['header_end_offset'] = $oggheader['page_start_offset'] + $filedataoffset;
429 $oggheader['page_end_offset'] = $oggheader['header_end_offset'] + $oggheader['page_length'];
430 $this->fseek($oggheader['header_end_offset']);
431
432 return $oggheader;
433 }
434
439 public function ParseVorbisComments()
440 {
441 $info = &$this->getid3->info;
442
443 $OriginalOffset = $this->ftell();
444 $commentdataoffset = 0;
445 $VorbisCommentPage = 1;
446
447 switch ($info['audio']['dataformat']) {
448 case 'vorbis':
449 case 'speex':
450 $CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset']; // Second Ogg page, after header block
451 $this->fseek($CommentStartOffset);
452 $commentdataoffset = 27 + $info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
453 $commentdata = $this->fread(self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) + $commentdataoffset);
454
455 if ($info['audio']['dataformat'] == 'vorbis') {
456 $commentdataoffset += (strlen('vorbis') + 1);
457 }
458 break;
459
460 case 'flac':
461 $CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4;
462 $this->fseek($CommentStartOffset);
463 $commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
464 break;
465
466 default:
467 return false;
468 }
469
470 $VendorSize = Helper::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
471 $commentdataoffset += 4;
472
473 $info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
474 $commentdataoffset += $VendorSize;
475
476 $CommentsCount = Helper::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
477 $commentdataoffset += 4;
478 $info['avdataoffset'] = $CommentStartOffset + $commentdataoffset;
479
480 $basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
481 $ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
482 for ($i = 0; $i < $CommentsCount; $i++) {
483
484 $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset;
485
486 if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) {
487 if ($oggpageinfo = $this->ParseOggPageHeader()) {
488 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
489
490 $VorbisCommentPage++;
491
492 // First, save what we haven't read yet
493 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
494
495 // Then take that data off the end
496 $commentdata = substr($commentdata, 0, $commentdataoffset);
497
498 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
499 $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
500 $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
501
502 // Finally, stick the unused data back on the end
503 $commentdata .= $AsYetUnusedData;
504
505 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
506 $commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
507 }
508
509 }
510 $ThisFileInfo_ogg_comments_raw[$i]['size'] = Helper::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
511
512 // replace avdataoffset with position just after the last vorbiscomment
513 $info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4;
514
515 $commentdataoffset += 4;
516 while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
517 if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
518 $info['warning'][] = 'Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments';
519 break 2;
520 }
521
522 $VorbisCommentPage++;
523
524 $oggpageinfo = $this->ParseOggPageHeader();
525 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
526
527 // First, save what we haven't read yet
528 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
529
530 // Then take that data off the end
531 $commentdata = substr($commentdata, 0, $commentdataoffset);
532
533 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
534 $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
535 $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
536
537 // Finally, stick the unused data back on the end
538 $commentdata .= $AsYetUnusedData;
539
540 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
541 if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
542 $info['warning'][] = 'undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
543 break;
544 }
545 $readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
546 if ($readlength <= 0) {
547 $info['warning'][] = 'invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
548 break;
549 }
550 $commentdata .= $this->fread($readlength);
551
552 //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
553 }
554 $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
555 $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
556 $commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size'];
557
558 if (!$commentstring) {
559
560 // no comment?
561 $info['warning'][] = 'Blank Ogg comment ['.$i.']';
562
563 } elseif (strstr($commentstring, '=')) {
564
565 $commentexploded = explode('=', $commentstring, 2);
566 $ThisFileInfo_ogg_comments_raw[$i]['key'] = strtoupper($commentexploded[0]);
567 $ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : '');
568 $ThisFileInfo_ogg_comments_raw[$i]['data'] = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
569 $ThisFileInfo_ogg_comments_raw[$i]['data_length'] = strlen($ThisFileInfo_ogg_comments_raw[$i]['data']);
570
571 if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
572 // http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
573 // 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.
574 // http://flac.sourceforge.net/format.html#metadata_block_picture
575 $getid3_temp = new GetId3Core();
576 $getid3_flac = new Flac($getid3_temp);
577 $getid3_flac->data_string = $ThisFileInfo_ogg_comments_raw[$i]['data'];
578 $getid3_flac->data_string_flag = true;
579 if ($getid3_flac->parsePICTURE()) {
580 if (!empty($getid3_temp->info['flac']['PICTURE'])) {
581 foreach ($getid3_temp->info['flac']['PICTURE'] as $key => $value) {
582 $ThisFileInfo_ogg_comments_raw[$i]['data'] = $value['data'];
583 $ThisFileInfo_ogg_comments_raw[$i]['data_length'] = strlen($value['data']);
584 $ThisFileInfo_ogg_comments_raw[$i]['image_mime'] = $value['image_mime'];
585 $ThisFileInfo_ogg_comments_raw[$i]['width'] = $value['width'];
586 $ThisFileInfo_ogg_comments_raw[$i]['height'] = $value['height'];
587 $ThisFileInfo_ogg_comments_raw[$i]['type'] = $value['type'];
588 $ThisFileInfo_ogg_comments_raw[$i]['typeid'] = $value['typeid'];
589 $ThisFileInfo_ogg_comments_raw[$i]['color_depth'] = $value['color_depth'];
590 $ThisFileInfo_ogg_comments_raw[$i]['colors_indexed'] = $value['colors_indexed'];
591 }
592 }
593 } else {
594 $info['warning'][] = 'Failed to GetId3_flac.parsePICTURE()';
595 }
596 unset($getid3_flac, $getid3_temp);
597 }
598
599 if (preg_match('#^(BM|GIF|\xFF\xD8\xFF|\x89\x50\x4E\x47\x0D\x0A\x1A\x0A|II\x2A\x00|MM\x00\x2A)#s', $ThisFileInfo_ogg_comments_raw[$i]['data'])) {
600 $imageinfo = array();
601 $imagechunkcheck = Helper::GetDataImageSize($ThisFileInfo_ogg_comments_raw[$i]['data'], $imageinfo);
602 unset($imageinfo);
603 if (!empty($imagechunkcheck)) {
604 $ThisFileInfo_ogg_comments_raw[$i]['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]);
605 if ($ThisFileInfo_ogg_comments_raw[$i]['image_mime'] && ($ThisFileInfo_ogg_comments_raw[$i]['image_mime'] != 'application/octet-stream')) {
606 unset($ThisFileInfo_ogg_comments_raw[$i]['value']);
607 }
608 }
609 }
610
611 if (isset($ThisFileInfo_ogg_comments_raw[$i]['value'])) {
612 unset($ThisFileInfo_ogg_comments_raw[$i]['data']);
613 $info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
614 } else {
615 do {
616 if ($this->inline_attachments === false) {
617 // skip entirely
618 unset($ThisFileInfo_ogg_comments_raw[$i]['data']);
619 break;
620 }
621 if ($this->inline_attachments === true) {
622 // great
623 } elseif (is_int($this->inline_attachments)) {
624 if ($this->inline_attachments < $ThisFileInfo_ogg_comments_raw[$i]['data_length']) {
625 // too big, skip
626 $info['warning'][] = 'attachment at '.$ThisFileInfo_ogg_comments_raw[$i]['offset'].' is too large to process inline ('.number_format($ThisFileInfo_ogg_comments_raw[$i]['data_length']).' bytes)';
627 unset($ThisFileInfo_ogg_comments_raw[$i]['data']);
628 break;
629 }
630 } elseif (is_string($this->inline_attachments)) {
631 $this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR);
632 if (!is_dir($this->inline_attachments) || !is_writable($this->inline_attachments)) {
633 // cannot write, skip
634 $info['warning'][] = 'attachment at '.$ThisFileInfo_ogg_comments_raw[$i]['offset'].' cannot be saved to "'.$this->inline_attachments.'" (not writable)';
635 unset($ThisFileInfo_ogg_comments_raw[$i]['data']);
636 break;
637 }
638 }
639 // if we get this far, must be OK
640 if (is_string($this->inline_attachments)) {
641 $destination_filename = $this->inline_attachments.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$ThisFileInfo_ogg_comments_raw[$i]['offset'];
642 if (!file_exists($destination_filename) || is_writable($destination_filename)) {
643 file_put_contents($destination_filename, $ThisFileInfo_ogg_comments_raw[$i]['data']);
644 } else {
645 $info['warning'][] = 'attachment at '.$ThisFileInfo_ogg_comments_raw[$i]['offset'].' cannot be saved to "'.$destination_filename.'" (not writable)';
646 }
647 $ThisFileInfo_ogg_comments_raw[$i]['data_filename'] = $destination_filename;
648 unset($ThisFileInfo_ogg_comments_raw[$i]['data']);
649 } else {
650 $info['ogg']['comments']['picture'][] = array('data'=>$ThisFileInfo_ogg_comments_raw[$i]['data'], 'image_mime'=>$ThisFileInfo_ogg_comments_raw[$i]['image_mime']);
651 }
652 } while (false);
653
654 }
655
656 } else {
657
658 $info['warning'][] = '[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring;
659
660 }
661 }
662
663 // Replay Gain Adjustment
664 // http://privatewww.essex.ac.uk/~djmrob/replaygain/
665 if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
666 foreach ($info['ogg']['comments'] as $index => $commentvalue) {
667 switch ($index) {
668 case 'rg_audiophile':
669 case 'replaygain_album_gain':
670 $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
671 unset($info['ogg']['comments'][$index]);
672 break;
673
674 case 'rg_radio':
675 case 'replaygain_track_gain':
676 $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
677 unset($info['ogg']['comments'][$index]);
678 break;
679
680 case 'replaygain_album_peak':
681 $info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
682 unset($info['ogg']['comments'][$index]);
683 break;
684
685 case 'rg_peak':
686 case 'replaygain_track_peak':
687 $info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
688 unset($info['ogg']['comments'][$index]);
689 break;
690
691 case 'replaygain_reference_loudness':
692 $info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
693 unset($info['ogg']['comments'][$index]);
694 break;
695
696 default:
697 // do nothing
698 break;
699 }
700 }
701 }
702
703 $this->fseek($OriginalOffset);
704
705 return true;
706 }
707
714 public static function SpeexBandModeLookup($mode)
715 {
716 static $SpeexBandModeLookup = array();
717 if (empty($SpeexBandModeLookup)) {
718 $SpeexBandModeLookup[0] = 'narrow';
719 $SpeexBandModeLookup[1] = 'wide';
720 $SpeexBandModeLookup[2] = 'ultra-wide';
721 }
722
723 return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null);
724 }
725
732 public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1)
733 {
734 for ($i = 0; $i < $SegmentNumber; $i++) {
735 $segmentlength = 0;
736 foreach ($OggInfoArray['segment_table'] as $key => $value) {
737 $segmentlength += $value;
738 if ($value < 255) {
739 break;
740 }
741 }
742 }
743
744 return $segmentlength;
745 }
746
752 public static function get_quality_from_nominal_bitrate($nominal_bitrate)
753 {
754 // decrease precision
755 $nominal_bitrate = $nominal_bitrate / 1000;
756
757 if ($nominal_bitrate < 128) {
758 // q-1 to q4
759 $qval = ($nominal_bitrate - 64) / 16;
760 } elseif ($nominal_bitrate < 256) {
761 // q4 to q8
762 $qval = $nominal_bitrate / 32;
763 } elseif ($nominal_bitrate < 320) {
764 // q8 to q9
765 $qval = ($nominal_bitrate + 256) / 64;
766 } else {
767 // q9 to q10
768 $qval = ($nominal_bitrate + 1300) / 180;
769 }
770 //return $qval; // 5.031324
771 //return intval($qval); // 5
772 return round($qval, 1); // 5 or 4.9
773 }
774
775}
An exception for terminatinating execution or to throw for unit testing.
GetId3() by James Heinrich info@getid3.org //.
Definition: GetId3Core.php:26
GetId3() by James Heinrich info@getid3.org //.
Definition: BaseHandler.php:26
fseek($bytes, $whence=SEEK_SET)
GetId3() by James Heinrich info@getid3.org //.
Definition: Helper.php:27
static intValueSupported($num)
@staticvar null $hasINT64
Definition: Helper.php:130
static LittleEndian2Int($byteword, $signed=false)
Definition: Helper.php:413
static GetDataImageSize($imgData, &$imageinfo)
@staticvar string $tempdir
Definition: Helper.php:1622
GetId3() by James Heinrich info@getid3.org //.
Definition: Flac.php:31
GetId3() by James Heinrich info@getid3.org //.
Definition: Ogg.php:31
ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo)
Definition: Ogg.php:328
static get_quality_from_nominal_bitrate($nominal_bitrate)
Definition: Ogg.php:752
static OggPageSegmentLength($OggInfoArray, $SegmentNumber=1)
Definition: Ogg.php:732
static SpeexBandModeLookup($mode)
@staticvar array $SpeexBandModeLookup
Definition: Ogg.php:714
$counter
$info
Definition: example_052.php:80