[SPIP][PLUGINS] v3.0-->v3.2
[lhc/web/www.git] / www / plugins-dist / medias / lib / getid3 / module.audio.ogg.php
1 <?php
2 /////////////////////////////////////////////////////////////////
3 /// getID3() by James Heinrich <info@getid3.org> //
4 // available at http://getid3.sourceforge.net //
5 // or http://www.getid3.org //
6 // also https://github.com/JamesHeinrich/getID3 //
7 /////////////////////////////////////////////////////////////////
8 // See readme.txt for more details //
9 /////////////////////////////////////////////////////////////////
10 // //
11 // module.audio.ogg.php //
12 // module for analyzing Ogg Vorbis, OggFLAC and Speex files //
13 // dependencies: module.audio.flac.php //
14 // ///
15 /////////////////////////////////////////////////////////////////
16
17 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true);
18
19 class getid3_ogg extends getid3_handler
20 {
21 // http://xiph.org/vorbis/doc/Vorbis_I_spec.html
22 public function Analyze() {
23 $info = &$this->getid3->info;
24
25 $info['fileformat'] = 'ogg';
26
27 // Warn about illegal tags - only vorbiscomments are allowed
28 if (isset($info['id3v2'])) {
29 $this->warning('Illegal ID3v2 tag present.');
30 }
31 if (isset($info['id3v1'])) {
32 $this->warning('Illegal ID3v1 tag present.');
33 }
34 if (isset($info['ape'])) {
35 $this->warning('Illegal APE tag present.');
36 }
37
38
39 // Page 1 - Stream Header
40
41 $this->fseek($info['avdataoffset']);
42
43 $oggpageinfo = $this->ParseOggPageHeader();
44 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
45
46 if ($this->ftell() >= $this->getid3->fread_buffer_size()) {
47 $this->error('Could not find start of Ogg page in the first '.$this->getid3->fread_buffer_size().' bytes (this might not be an Ogg-Vorbis file?)');
48 unset($info['fileformat']);
49 unset($info['ogg']);
50 return false;
51 }
52
53 $filedata = $this->fread($oggpageinfo['page_length']);
54 $filedataoffset = 0;
55
56 if (substr($filedata, 0, 4) == 'fLaC') {
57
58 $info['audio']['dataformat'] = 'flac';
59 $info['audio']['bitrate_mode'] = 'vbr';
60 $info['audio']['lossless'] = true;
61
62 } elseif (substr($filedata, 1, 6) == 'vorbis') {
63
64 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
65
66 } elseif (substr($filedata, 0, 8) == 'OpusHead') {
67
68 if( $this->ParseOpusPageHeader($filedata, $filedataoffset, $oggpageinfo) == false ) {
69 return false;
70 }
71
72 } elseif (substr($filedata, 0, 8) == 'Speex ') {
73
74 // http://www.speex.org/manual/node10.html
75
76 $info['audio']['dataformat'] = 'speex';
77 $info['mime_type'] = 'audio/speex';
78 $info['audio']['bitrate_mode'] = 'abr';
79 $info['audio']['lossless'] = false;
80
81 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex '
82 $filedataoffset += 8;
83 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version'] = substr($filedata, $filedataoffset, 20);
84 $filedataoffset += 20;
85 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
86 $filedataoffset += 4;
87 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
88 $filedataoffset += 4;
89 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
90 $filedataoffset += 4;
91 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
92 $filedataoffset += 4;
93 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
94 $filedataoffset += 4;
95 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
96 $filedataoffset += 4;
97 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
98 $filedataoffset += 4;
99 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
100 $filedataoffset += 4;
101 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
102 $filedataoffset += 4;
103 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
104 $filedataoffset += 4;
105 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
106 $filedataoffset += 4;
107 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
108 $filedataoffset += 4;
109 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
110 $filedataoffset += 4;
111
112 $info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']);
113 $info['speex']['sample_rate'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'];
114 $info['speex']['channels'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'];
115 $info['speex']['vbr'] = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'];
116 $info['speex']['band_type'] = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']);
117
118 $info['audio']['sample_rate'] = $info['speex']['sample_rate'];
119 $info['audio']['channels'] = $info['speex']['channels'];
120 if ($info['speex']['vbr']) {
121 $info['audio']['bitrate_mode'] = 'vbr';
122 }
123
124 } elseif (substr($filedata, 0, 7) == "\x80".'theora') {
125
126 // http://www.theora.org/doc/Theora.pdf (section 6.2)
127
128 $info['ogg']['pageheader']['theora']['theora_magic'] = substr($filedata, $filedataoffset, 7); // hard-coded to "\x80.'theora'
129 $filedataoffset += 7;
130 $info['ogg']['pageheader']['theora']['version_major'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
131 $filedataoffset += 1;
132 $info['ogg']['pageheader']['theora']['version_minor'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
133 $filedataoffset += 1;
134 $info['ogg']['pageheader']['theora']['version_revision'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
135 $filedataoffset += 1;
136 $info['ogg']['pageheader']['theora']['frame_width_macroblocks'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 2));
137 $filedataoffset += 2;
138 $info['ogg']['pageheader']['theora']['frame_height_macroblocks'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 2));
139 $filedataoffset += 2;
140 $info['ogg']['pageheader']['theora']['resolution_x'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 3));
141 $filedataoffset += 3;
142 $info['ogg']['pageheader']['theora']['resolution_y'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 3));
143 $filedataoffset += 3;
144 $info['ogg']['pageheader']['theora']['picture_offset_x'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
145 $filedataoffset += 1;
146 $info['ogg']['pageheader']['theora']['picture_offset_y'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
147 $filedataoffset += 1;
148 $info['ogg']['pageheader']['theora']['frame_rate_numerator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 4));
149 $filedataoffset += 4;
150 $info['ogg']['pageheader']['theora']['frame_rate_denominator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 4));
151 $filedataoffset += 4;
152 $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 3));
153 $filedataoffset += 3;
154 $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 3));
155 $filedataoffset += 3;
156 $info['ogg']['pageheader']['theora']['color_space_id'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 1));
157 $filedataoffset += 1;
158 $info['ogg']['pageheader']['theora']['nominal_bitrate'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 3));
159 $filedataoffset += 3;
160 $info['ogg']['pageheader']['theora']['flags'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset, 2));
161 $filedataoffset += 2;
162
163 $info['ogg']['pageheader']['theora']['quality'] = ($info['ogg']['pageheader']['theora']['flags'] & 0xFC00) >> 10;
164 $info['ogg']['pageheader']['theora']['kfg_shift'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x03E0) >> 5;
165 $info['ogg']['pageheader']['theora']['pixel_format_id'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0018) >> 3;
166 $info['ogg']['pageheader']['theora']['reserved'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0007) >> 0; // should be 0
167 $info['ogg']['pageheader']['theora']['color_space'] = self::TheoraColorSpace($info['ogg']['pageheader']['theora']['color_space_id']);
168 $info['ogg']['pageheader']['theora']['pixel_format'] = self::TheoraPixelFormat($info['ogg']['pageheader']['theora']['pixel_format_id']);
169
170 $info['video']['dataformat'] = 'theora';
171 $info['mime_type'] = 'video/ogg';
172 //$info['audio']['bitrate_mode'] = 'abr';
173 //$info['audio']['lossless'] = false;
174 $info['video']['resolution_x'] = $info['ogg']['pageheader']['theora']['resolution_x'];
175 $info['video']['resolution_y'] = $info['ogg']['pageheader']['theora']['resolution_y'];
176 if ($info['ogg']['pageheader']['theora']['frame_rate_denominator'] > 0) {
177 $info['video']['frame_rate'] = (float) $info['ogg']['pageheader']['theora']['frame_rate_numerator'] / $info['ogg']['pageheader']['theora']['frame_rate_denominator'];
178 }
179 if ($info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] > 0) {
180 $info['video']['pixel_aspect_ratio'] = (float) $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] / $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'];
181 }
182 $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 ['.$this->getid3->version().'] -- bitrate, playtime and all audio data are currently unavailable');
183
184
185 } elseif (substr($filedata, 0, 8) == "fishead\x00") {
186
187 // Ogg Skeleton version 3.0 Format Specification
188 // http://xiph.org/ogg/doc/skeleton.html
189 $filedataoffset += 8;
190 $info['ogg']['skeleton']['fishead']['raw']['version_major'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
191 $filedataoffset += 2;
192 $info['ogg']['skeleton']['fishead']['raw']['version_minor'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
193 $filedataoffset += 2;
194 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
195 $filedataoffset += 8;
196 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
197 $filedataoffset += 8;
198 $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
199 $filedataoffset += 8;
200 $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
201 $filedataoffset += 8;
202 $info['ogg']['skeleton']['fishead']['raw']['utc'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 20));
203 $filedataoffset += 20;
204
205 $info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor'];
206 $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'];
207 $info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'];
208 $info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc'];
209
210
211 $counter = 0;
212 do {
213 $oggpageinfo = $this->ParseOggPageHeader();
214 $info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++] = $oggpageinfo;
215 $filedata = $this->fread($oggpageinfo['page_length']);
216 $this->fseek($oggpageinfo['page_end_offset']);
217
218 if (substr($filedata, 0, 8) == "fisbone\x00") {
219
220 $filedataoffset = 8;
221 $info['ogg']['skeleton']['fisbone']['raw']['message_header_offset'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
222 $filedataoffset += 4;
223 $info['ogg']['skeleton']['fisbone']['raw']['serial_number'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
224 $filedataoffset += 4;
225 $info['ogg']['skeleton']['fisbone']['raw']['number_header_packets'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
226 $filedataoffset += 4;
227 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
228 $filedataoffset += 8;
229 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
230 $filedataoffset += 8;
231 $info['ogg']['skeleton']['fisbone']['raw']['basegranule'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
232 $filedataoffset += 8;
233 $info['ogg']['skeleton']['fisbone']['raw']['preroll'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
234 $filedataoffset += 4;
235 $info['ogg']['skeleton']['fisbone']['raw']['granuleshift'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
236 $filedataoffset += 1;
237 $info['ogg']['skeleton']['fisbone']['raw']['padding'] = substr($filedata, $filedataoffset, 3);
238 $filedataoffset += 3;
239
240 } elseif (substr($filedata, 1, 6) == 'theora') {
241
242 $info['video']['dataformat'] = 'theora1';
243 $this->error('Ogg Theora (v1) not correctly handled in this version of getID3 ['.$this->getid3->version().']');
244 //break;
245
246 } elseif (substr($filedata, 1, 6) == 'vorbis') {
247
248 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
249
250 } else {
251 $this->error('unexpected');
252 //break;
253 }
254 //} while ($oggpageinfo['page_seqno'] == 0);
255 } while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
256
257 $this->fseek($oggpageinfo['page_start_offset']);
258
259 $this->error('Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']');
260 //return false;
261
262 } else {
263
264 $this->error('Expecting either "Speex ", "OpusHead" or "vorbis" identifier strings, found "'.substr($filedata, 0, 8).'"');
265 unset($info['ogg']);
266 unset($info['mime_type']);
267 return false;
268
269 }
270
271 // Page 2 - Comment Header
272 $oggpageinfo = $this->ParseOggPageHeader();
273 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
274
275 switch ($info['audio']['dataformat']) {
276 case 'vorbis':
277 $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
278 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, 0, 1));
279 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 1, 6); // hard-coded to 'vorbis'
280
281 $this->ParseVorbisComments();
282 break;
283
284 case 'flac':
285 $flac = new getid3_flac($this->getid3);
286 if (!$flac->parseMETAdata()) {
287 $this->error('Failed to parse FLAC headers');
288 return false;
289 }
290 unset($flac);
291 break;
292
293 case 'speex':
294 $this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR);
295 $this->ParseVorbisComments();
296 break;
297
298 case 'opus':
299 $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
300 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 0, 8); // hard-coded to 'OpusTags'
301 if(substr($filedata, 0, 8) != 'OpusTags') {
302 $this->error('Expected "OpusTags" as header but got "'.substr($filedata, 0, 8).'"');
303 return false;
304 }
305
306 $this->ParseVorbisComments();
307 break;
308
309 }
310
311 // Last Page - Number of Samples
312 if (!getid3_lib::intValueSupported($info['avdataend'])) {
313
314 $this->warning('Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)');
315
316 } else {
317
318 $this->fseek(max($info['avdataend'] - $this->getid3->fread_buffer_size(), 0));
319 $LastChunkOfOgg = strrev($this->fread($this->getid3->fread_buffer_size()));
320 if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
321 $this->fseek($info['avdataend'] - ($LastOggSpostion + strlen('SggO')));
322 $info['avdataend'] = $this->ftell();
323 $info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader();
324 $info['ogg']['samples'] = $info['ogg']['pageheader']['eos']['pcm_abs_position'];
325 if ($info['ogg']['samples'] == 0) {
326 $this->error('Corrupt Ogg file: eos.number of samples == zero');
327 return false;
328 }
329 if (!empty($info['audio']['sample_rate'])) {
330 $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
331 }
332 }
333
334 }
335
336 if (!empty($info['ogg']['bitrate_average'])) {
337 $info['audio']['bitrate'] = $info['ogg']['bitrate_average'];
338 } elseif (!empty($info['ogg']['bitrate_nominal'])) {
339 $info['audio']['bitrate'] = $info['ogg']['bitrate_nominal'];
340 } elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) {
341 $info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] + $info['ogg']['bitrate_max']) / 2;
342 }
343 if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
344 if ($info['audio']['bitrate'] == 0) {
345 $this->error('Corrupt Ogg file: bitrate_audio == zero');
346 return false;
347 }
348 $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
349 }
350
351 if (isset($info['ogg']['vendor'])) {
352 $info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
353
354 // Vorbis only
355 if ($info['audio']['dataformat'] == 'vorbis') {
356
357 // Vorbis 1.0 starts with Xiph.Org
358 if (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
359
360 if ($info['audio']['bitrate_mode'] == 'abr') {
361
362 // Set -b 128 on abr files
363 $info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
364
365 } elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) {
366 // Set -q N on vbr files
367 $info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']);
368
369 }
370 }
371
372 if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) {
373 $info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps';
374 }
375 }
376 }
377
378 return true;
379 }
380
381 public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
382 $info = &$this->getid3->info;
383 $info['audio']['dataformat'] = 'vorbis';
384 $info['audio']['lossless'] = false;
385
386 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
387 $filedataoffset += 1;
388 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis'
389 $filedataoffset += 6;
390 $info['ogg']['bitstreamversion'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
391 $filedataoffset += 4;
392 $info['ogg']['numberofchannels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
393 $filedataoffset += 1;
394 $info['audio']['channels'] = $info['ogg']['numberofchannels'];
395 $info['ogg']['samplerate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
396 $filedataoffset += 4;
397 if ($info['ogg']['samplerate'] == 0) {
398 $this->error('Corrupt Ogg file: sample rate == zero');
399 return false;
400 }
401 $info['audio']['sample_rate'] = $info['ogg']['samplerate'];
402 $info['ogg']['samples'] = 0; // filled in later
403 $info['ogg']['bitrate_average'] = 0; // filled in later
404 $info['ogg']['bitrate_max'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
405 $filedataoffset += 4;
406 $info['ogg']['bitrate_nominal'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
407 $filedataoffset += 4;
408 $info['ogg']['bitrate_min'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
409 $filedataoffset += 4;
410 $info['ogg']['blocksize_small'] = pow(2, getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F);
411 $info['ogg']['blocksize_large'] = pow(2, (getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4);
412 $info['ogg']['stop_bit'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet
413
414 $info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr
415 if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) {
416 unset($info['ogg']['bitrate_max']);
417 $info['audio']['bitrate_mode'] = 'abr';
418 }
419 if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
420 unset($info['ogg']['bitrate_nominal']);
421 }
422 if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
423 unset($info['ogg']['bitrate_min']);
424 $info['audio']['bitrate_mode'] = 'abr';
425 }
426 return true;
427 }
428
429 // http://tools.ietf.org/html/draft-ietf-codec-oggopus-03
430 public function ParseOpusPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
431 $info = &$this->getid3->info;
432 $info['audio']['dataformat'] = 'opus';
433 $info['mime_type'] = 'audio/ogg; codecs=opus';
434
435 /** @todo find a usable way to detect abr (vbr that is padded to be abr) */
436 $info['audio']['bitrate_mode'] = 'vbr';
437
438 $info['audio']['lossless'] = false;
439
440 $info['ogg']['pageheader']['opus']['opus_magic'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'OpusHead'
441 $filedataoffset += 8;
442 $info['ogg']['pageheader']['opus']['version'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
443 $filedataoffset += 1;
444
445 if ($info['ogg']['pageheader']['opus']['version'] < 1 || $info['ogg']['pageheader']['opus']['version'] > 15) {
446 $this->error('Unknown opus version number (only accepting 1-15)');
447 return false;
448 }
449
450 $info['ogg']['pageheader']['opus']['out_channel_count'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
451 $filedataoffset += 1;
452
453 if ($info['ogg']['pageheader']['opus']['out_channel_count'] == 0) {
454 $this->error('Invalid channel count in opus header (must not be zero)');
455 return false;
456 }
457
458 $info['ogg']['pageheader']['opus']['pre_skip'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
459 $filedataoffset += 2;
460
461 $info['ogg']['pageheader']['opus']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
462 $filedataoffset += 4;
463
464 //$info['ogg']['pageheader']['opus']['output_gain'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
465 //$filedataoffset += 2;
466
467 //$info['ogg']['pageheader']['opus']['channel_mapping_family'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
468 //$filedataoffset += 1;
469
470 $info['opus']['opus_version'] = $info['ogg']['pageheader']['opus']['version'];
471 $info['opus']['sample_rate'] = $info['ogg']['pageheader']['opus']['sample_rate'];
472 $info['opus']['out_channel_count'] = $info['ogg']['pageheader']['opus']['out_channel_count'];
473
474 $info['audio']['channels'] = $info['opus']['out_channel_count'];
475 $info['audio']['sample_rate'] = $info['opus']['sample_rate'];
476 return true;
477 }
478
479
480 public function ParseOggPageHeader() {
481 // http://xiph.org/ogg/vorbis/doc/framing.html
482 $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file
483
484 $filedata = $this->fread($this->getid3->fread_buffer_size());
485 $filedataoffset = 0;
486 while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) {
487 if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) {
488 // should be found before here
489 return false;
490 }
491 if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) {
492 if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === false)) {
493 // get some more data, unless eof, in which case fail
494 return false;
495 }
496 }
497 }
498 $filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS'
499
500 $oggheader['stream_structver'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
501 $filedataoffset += 1;
502 $oggheader['flags_raw'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
503 $filedataoffset += 1;
504 $oggheader['flags']['fresh'] = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
505 $oggheader['flags']['bos'] = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
506 $oggheader['flags']['eos'] = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
507
508 $oggheader['pcm_abs_position'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
509 $filedataoffset += 8;
510 $oggheader['stream_serialno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
511 $filedataoffset += 4;
512 $oggheader['page_seqno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
513 $filedataoffset += 4;
514 $oggheader['page_checksum'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
515 $filedataoffset += 4;
516 $oggheader['page_segments'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
517 $filedataoffset += 1;
518 $oggheader['page_length'] = 0;
519 for ($i = 0; $i < $oggheader['page_segments']; $i++) {
520 $oggheader['segment_table'][$i] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
521 $filedataoffset += 1;
522 $oggheader['page_length'] += $oggheader['segment_table'][$i];
523 }
524 $oggheader['header_end_offset'] = $oggheader['page_start_offset'] + $filedataoffset;
525 $oggheader['page_end_offset'] = $oggheader['header_end_offset'] + $oggheader['page_length'];
526 $this->fseek($oggheader['header_end_offset']);
527
528 return $oggheader;
529 }
530
531 // http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
532 public function ParseVorbisComments() {
533 $info = &$this->getid3->info;
534
535 $OriginalOffset = $this->ftell();
536 $commentdataoffset = 0;
537 $VorbisCommentPage = 1;
538
539 switch ($info['audio']['dataformat']) {
540 case 'vorbis':
541 case 'speex':
542 case 'opus':
543 $CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset']; // Second Ogg page, after header block
544 $this->fseek($CommentStartOffset);
545 $commentdataoffset = 27 + $info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
546 $commentdata = $this->fread(self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) + $commentdataoffset);
547
548 if ($info['audio']['dataformat'] == 'vorbis') {
549 $commentdataoffset += (strlen('vorbis') + 1);
550 }
551 else if ($info['audio']['dataformat'] == 'opus') {
552 $commentdataoffset += strlen('OpusTags');
553 }
554
555 break;
556
557 case 'flac':
558 $CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4;
559 $this->fseek($CommentStartOffset);
560 $commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
561 break;
562
563 default:
564 return false;
565 break;
566 }
567
568 $VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
569 $commentdataoffset += 4;
570
571 $info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
572 $commentdataoffset += $VendorSize;
573
574 $CommentsCount = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
575 $commentdataoffset += 4;
576 $info['avdataoffset'] = $CommentStartOffset + $commentdataoffset;
577
578 $basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
579 $ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
580 for ($i = 0; $i < $CommentsCount; $i++) {
581
582 if ($i >= 10000) {
583 // https://github.com/owncloud/music/issues/212#issuecomment-43082336
584 $this->warning('Unexpectedly large number ('.$CommentsCount.') of Ogg comments - breaking after reading '.$i.' comments');
585 break;
586 }
587
588 $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset;
589
590 if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) {
591 if ($oggpageinfo = $this->ParseOggPageHeader()) {
592 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
593
594 $VorbisCommentPage++;
595
596 // First, save what we haven't read yet
597 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
598
599 // Then take that data off the end
600 $commentdata = substr($commentdata, 0, $commentdataoffset);
601
602 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
603 $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
604 $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
605
606 // Finally, stick the unused data back on the end
607 $commentdata .= $AsYetUnusedData;
608
609 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
610 $commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
611 }
612
613 }
614 $ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
615
616 // replace avdataoffset with position just after the last vorbiscomment
617 $info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4;
618
619 $commentdataoffset += 4;
620 while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
621 if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
622 $this->warning('Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments');
623 break 2;
624 }
625
626 $VorbisCommentPage++;
627
628 $oggpageinfo = $this->ParseOggPageHeader();
629 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
630
631 // First, save what we haven't read yet
632 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
633
634 // Then take that data off the end
635 $commentdata = substr($commentdata, 0, $commentdataoffset);
636
637 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
638 $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
639 $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
640
641 // Finally, stick the unused data back on the end
642 $commentdata .= $AsYetUnusedData;
643
644 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
645 if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
646 $this->warning('undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
647 break;
648 }
649 $readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
650 if ($readlength <= 0) {
651 $this->warning('invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
652 break;
653 }
654 $commentdata .= $this->fread($readlength);
655
656 //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
657 }
658 $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
659 $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
660 $commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size'];
661
662 if (!$commentstring) {
663
664 // no comment?
665 $this->warning('Blank Ogg comment ['.$i.']');
666
667 } elseif (strstr($commentstring, '=')) {
668
669 $commentexploded = explode('=', $commentstring, 2);
670 $ThisFileInfo_ogg_comments_raw[$i]['key'] = strtoupper($commentexploded[0]);
671 $ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : '');
672
673 if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
674
675 // http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
676 // 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.
677 // http://flac.sourceforge.net/format.html#metadata_block_picture
678 $flac = new getid3_flac($this->getid3);
679 $flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
680 $flac->parsePICTURE();
681 $info['ogg']['comments']['picture'][] = $flac->getid3->info['flac']['PICTURE'][0];
682 unset($flac);
683
684 } elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
685
686 $data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
687 $this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
688 /** @todo use 'coverartmime' where available */
689 $imageinfo = getid3_lib::GetDataImageSize($data);
690 if ($imageinfo === false || !isset($imageinfo['mime'])) {
691 $this->warning('COVERART vorbiscomment tag contains invalid image');
692 continue;
693 }
694
695 $ogg = new self($this->getid3);
696 $ogg->setStringMode($data);
697 $info['ogg']['comments']['picture'][] = array(
698 'image_mime' => $imageinfo['mime'],
699 'datalength' => strlen($data),
700 'picturetype' => 'cover art',
701 'image_height' => $imageinfo['height'],
702 'image_width' => $imageinfo['width'],
703 'data' => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
704 );
705 unset($ogg);
706
707 } else {
708
709 $info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
710
711 }
712
713 } else {
714
715 $this->warning('[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring);
716
717 }
718 unset($ThisFileInfo_ogg_comments_raw[$i]);
719 }
720 unset($ThisFileInfo_ogg_comments_raw);
721
722
723 // Replay Gain Adjustment
724 // http://privatewww.essex.ac.uk/~djmrob/replaygain/
725 if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
726 foreach ($info['ogg']['comments'] as $index => $commentvalue) {
727 switch ($index) {
728 case 'rg_audiophile':
729 case 'replaygain_album_gain':
730 $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
731 unset($info['ogg']['comments'][$index]);
732 break;
733
734 case 'rg_radio':
735 case 'replaygain_track_gain':
736 $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
737 unset($info['ogg']['comments'][$index]);
738 break;
739
740 case 'replaygain_album_peak':
741 $info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
742 unset($info['ogg']['comments'][$index]);
743 break;
744
745 case 'rg_peak':
746 case 'replaygain_track_peak':
747 $info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
748 unset($info['ogg']['comments'][$index]);
749 break;
750
751 case 'replaygain_reference_loudness':
752 $info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
753 unset($info['ogg']['comments'][$index]);
754 break;
755
756 default:
757 // do nothing
758 break;
759 }
760 }
761 }
762
763 $this->fseek($OriginalOffset);
764
765 return true;
766 }
767
768 public static function SpeexBandModeLookup($mode) {
769 static $SpeexBandModeLookup = array();
770 if (empty($SpeexBandModeLookup)) {
771 $SpeexBandModeLookup[0] = 'narrow';
772 $SpeexBandModeLookup[1] = 'wide';
773 $SpeexBandModeLookup[2] = 'ultra-wide';
774 }
775 return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null);
776 }
777
778
779 public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
780 for ($i = 0; $i < $SegmentNumber; $i++) {
781 $segmentlength = 0;
782 foreach ($OggInfoArray['segment_table'] as $key => $value) {
783 $segmentlength += $value;
784 if ($value < 255) {
785 break;
786 }
787 }
788 }
789 return $segmentlength;
790 }
791
792
793 public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
794
795 // decrease precision
796 $nominal_bitrate = $nominal_bitrate / 1000;
797
798 if ($nominal_bitrate < 128) {
799 // q-1 to q4
800 $qval = ($nominal_bitrate - 64) / 16;
801 } elseif ($nominal_bitrate < 256) {
802 // q4 to q8
803 $qval = $nominal_bitrate / 32;
804 } elseif ($nominal_bitrate < 320) {
805 // q8 to q9
806 $qval = ($nominal_bitrate + 256) / 64;
807 } else {
808 // q9 to q10
809 $qval = ($nominal_bitrate + 1300) / 180;
810 }
811 //return $qval; // 5.031324
812 //return intval($qval); // 5
813 return round($qval, 1); // 5 or 4.9
814 }
815
816 public static function TheoraColorSpace($colorspace_id) {
817 // http://www.theora.org/doc/Theora.pdf (table 6.3)
818 static $TheoraColorSpaceLookup = array();
819 if (empty($TheoraColorSpaceLookup)) {
820 $TheoraColorSpaceLookup[0] = 'Undefined';
821 $TheoraColorSpaceLookup[1] = 'Rec. 470M';
822 $TheoraColorSpaceLookup[2] = 'Rec. 470BG';
823 $TheoraColorSpaceLookup[3] = 'Reserved';
824 }
825 return (isset($TheoraColorSpaceLookup[$colorspace_id]) ? $TheoraColorSpaceLookup[$colorspace_id] : null);
826 }
827
828 public static function TheoraPixelFormat($pixelformat_id) {
829 // http://www.theora.org/doc/Theora.pdf (table 6.4)
830 static $TheoraPixelFormatLookup = array();
831 if (empty($TheoraPixelFormatLookup)) {
832 $TheoraPixelFormatLookup[0] = '4:2:0';
833 $TheoraPixelFormatLookup[1] = 'Reserved';
834 $TheoraPixelFormatLookup[2] = '4:2:2';
835 $TheoraPixelFormatLookup[3] = '4:4:4';
836 }
837 return (isset($TheoraPixelFormatLookup[$pixelformat_id]) ? $TheoraPixelFormatLookup[$pixelformat_id] : null);
838 }
839
840 }