ILIAS  release_5-2 Revision v5.2.25-18-g3f80b828510
Ogg.php
Go to the documentation of this file.
1 <?php
2 
3 namespace 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 
30 class 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 }
static SpeexBandModeLookup($mode)
array $SpeexBandModeLookup
Definition: Ogg.php:714
ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo)
Definition: Ogg.php:328
static get_quality_from_nominal_bitrate($nominal_bitrate)
Definition: Ogg.php:752
static GetDataImageSize($imgData, &$imageinfo)
string $tempdir
Definition: Helper.php:1622
GetId3() by James Heinrich info@getid3.org //.
Definition: BaseHandler.php:25
fseek($bytes, $whence=SEEK_SET)
$counter
$info
Definition: example_052.php:80
GetId3() by James Heinrich info@getid3.org //.
Definition: Ogg.php:30
GetId3() by James Heinrich info@getid3.org //.
Definition: GetId3Core.php:25
GetId3() by James Heinrich info@getid3.org //.
Definition: Flac.php:30
static OggPageSegmentLength($OggInfoArray, $SegmentNumber=1)
Definition: Ogg.php:732
Create styles array
The data for the language used.
static LittleEndian2Int($byteword, $signed=false)
Definition: Helper.php:413
static intValueSupported($num)
null $hasINT64
Definition: Helper.php:130