2 /////////////////////////////////////////////////////////////////
3 /// getID3() by James Heinrich <info@getid3.org> //
4 // available at http://getid3.sourceforge.net //
5 // or http://www.getid3.org //
6 // also https://github.com/JamesHeinrich/getID3 //
7 /////////////////////////////////////////////////////////////////
8 // See readme.txt for more details //
9 /////////////////////////////////////////////////////////////////
11 // module.audio.ogg.php //
12 // module for analyzing Ogg Vorbis, OggFLAC and Speex files //
13 // dependencies: module.audio.flac.php //
15 /////////////////////////////////////////////////////////////////
17 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true);
19 class getid3_ogg extends getid3_handler
21 // http://xiph.org/vorbis/doc/Vorbis_I_spec.html
22 public function Analyze() {
23 $info = &$this->getid3->info;
25 $info['fileformat'] = 'ogg';
27 // Warn about illegal tags - only vorbiscomments are allowed
28 if (isset($info['id3v2'])) {
29 $info['warning'][] = 'Illegal ID3v2 tag present.';
31 if (isset($info['id3v1'])) {
32 $info['warning'][] = 'Illegal ID3v1 tag present.';
34 if (isset($info['ape'])) {
35 $info['warning'][] = 'Illegal APE tag present.';
39 // Page 1 - Stream Header
41 $this->fseek($info['avdataoffset']);
43 $oggpageinfo = $this->ParseOggPageHeader();
44 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
46 if ($this->ftell() >= $this->getid3->fread_buffer_size()) {
47 $info['error'][] = 'Could not find start of Ogg page in the first '.$this->getid3->fread_buffer_size().' bytes (this might not be an Ogg-Vorbis file?)';
48 unset($info['fileformat']);
53 $filedata = $this->fread($oggpageinfo['page_length']);
56 if (substr($filedata, 0, 4) == 'fLaC') {
58 $info['audio']['dataformat'] = 'flac';
59 $info['audio']['bitrate_mode'] = 'vbr';
60 $info['audio']['lossless'] = true;
62 } elseif (substr($filedata, 1, 6) == 'vorbis') {
64 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
66 } elseif (substr($filedata, 0, 8) == 'OpusHead') {
68 if( $this->ParseOpusPageHeader($filedata, $filedataoffset, $oggpageinfo) == false ) {
72 } elseif (substr($filedata, 0, 8) == 'Speex ') {
74 // http://www.speex.org/manual/node10.html
76 $info['audio']['dataformat'] = 'speex';
77 $info['mime_type'] = 'audio/speex';
78 $info['audio']['bitrate_mode'] = 'abr';
79 $info['audio']['lossless'] = false;
81 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex '
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));
87 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
89 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
91 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
93 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
95 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
97 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate'] = getid3_lib::LittleEndian2Int(substr($filedata, $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;
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']);
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';
124 } elseif (substr($filedata, 0, 7) == "\x80".'theora') {
126 // http://www.theora.org/doc/Theora.pdf (section 6.2)
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;
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']);
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'];
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'];
182 $info['warning'][] = 'Ogg Theora (v3) not fully supported in this version of getID3 ['.$this->getid3->version().'] -- bitrate, playtime and all audio data are currently unavailable';
185 } elseif (substr($filedata, 0, 8) == "fishead\x00") {
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;
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'];
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']);
218 if (substr($filedata, 0, 8) == "fisbone\x00") {
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;
240 } elseif (substr($filedata, 1, 6) == 'theora') {
242 $info['video']['dataformat'] = 'theora1';
243 $info['error'][] = 'Ogg Theora (v1) not correctly handled in this version of getID3 ['.$this->getid3->version().']';
246 } elseif (substr($filedata, 1, 6) == 'vorbis') {
248 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
251 $info['error'][] = 'unexpected';
254 //} while ($oggpageinfo['page_seqno'] == 0);
255 } while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
257 $this->fseek($oggpageinfo['page_start_offset']);
259 $info['error'][] = 'Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']';
264 $info['error'][] = 'Expecting either "Speex ", "OpusHead" or "vorbis" identifier strings, found "'.substr($filedata, 0, 8).'"';
266 unset($info['mime_type']);
271 // Page 2 - Comment Header
272 $oggpageinfo = $this->ParseOggPageHeader();
273 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
275 switch ($info['audio']['dataformat']) {
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'
281 $this->ParseVorbisComments();
285 $flac = new getid3_flac($this->getid3);
286 if (!$flac->parseMETAdata()) {
287 $info['error'][] = 'Failed to parse FLAC headers';
294 $this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR);
295 $this->ParseVorbisComments();
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 $info['error'][] = 'Expected "OpusTags" as header but got "'.substr($filedata, 0, 8).'"';
306 $this->ParseVorbisComments();
311 // Last Page - Number of Samples
312 if (!getid3_lib::intValueSupported($info['avdataend'])) {
314 $info['warning'][] = 'Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)';
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 $info['error'][] = 'Corrupt Ogg file: eos.number of samples == zero';
329 if (!empty($info['audio']['sample_rate'])) {
330 $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
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;
343 if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
344 if ($info['audio']['bitrate'] == 0) {
345 $info['error'][] = 'Corrupt Ogg file: bitrate_audio == zero';
348 $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
351 if (isset($info['ogg']['vendor'])) {
352 $info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
355 if ($info['audio']['dataformat'] == 'vorbis') {
357 // Vorbis 1.0 starts with Xiph.Org
358 if (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
360 if ($info['audio']['bitrate_mode'] == 'abr') {
362 // Set -b 128 on abr files
363 $info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
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']);
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';
381 public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
382 $info = &$this->getid3->info;
383 $info['audio']['dataformat'] = 'vorbis';
384 $info['audio']['lossless'] = false;
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 $info['error'][] = 'Corrupt Ogg file: sample rate == zero';
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
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';
419 if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
420 unset($info['ogg']['bitrate_nominal']);
422 if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
423 unset($info['ogg']['bitrate_min']);
424 $info['audio']['bitrate_mode'] = 'abr';
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';
435 /** @todo find a usable way to detect abr (vbr that is padded to be abr) */
436 $info['audio']['bitrate_mode'] = 'vbr';
438 $info['audio']['lossless'] = false;
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;
445 if ($info['ogg']['pageheader']['opus']['version'] < 1 || $info['ogg']['pageheader']['opus']['version'] > 15) {
446 $info['error'][] = 'Unknown opus version number (only accepting 1-15)';
450 $info['ogg']['pageheader']['opus']['out_channel_count'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
451 $filedataoffset += 1;
453 if ($info['ogg']['pageheader']['opus']['out_channel_count'] == 0) {
454 $info['error'][] = 'Invalid channel count in opus header (must not be zero)';
458 $info['ogg']['pageheader']['opus']['pre_skip'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
459 $filedataoffset += 2;
461 $info['ogg']['pageheader']['opus']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
462 $filedataoffset += 4;
464 //$info['ogg']['pageheader']['opus']['output_gain'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
465 //$filedataoffset += 2;
467 //$info['ogg']['pageheader']['opus']['channel_mapping_family'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
468 //$filedataoffset += 1;
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'];
474 $info['audio']['channels'] = $info['opus']['out_channel_count'];
475 $info['audio']['sample_rate'] = $info['opus']['sample_rate'];
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
484 $filedata = $this->fread($this->getid3->fread_buffer_size());
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
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
498 $filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS'
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)
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];
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']);
531 // http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
532 public function ParseVorbisComments() {
533 $info = &$this->getid3->info;
535 $OriginalOffset = $this->ftell();
536 $commentdataoffset = 0;
537 $VorbisCommentPage = 1;
539 switch ($info['audio']['dataformat']) {
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);
548 if ($info['audio']['dataformat'] == 'vorbis') {
549 $commentdataoffset += (strlen('vorbis') + 1);
551 else if ($info['audio']['dataformat'] == 'opus') {
552 $commentdataoffset += strlen('OpusTags');
558 $CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4;
559 $this->fseek($CommentStartOffset);
560 $commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
567 $VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
568 $commentdataoffset += 4;
570 $info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
571 $commentdataoffset += $VendorSize;
573 $CommentsCount = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
574 $commentdataoffset += 4;
575 $info['avdataoffset'] = $CommentStartOffset + $commentdataoffset;
577 $basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
578 $ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
579 for ($i = 0; $i < $CommentsCount; $i++) {
582 // https://github.com/owncloud/music/issues/212#issuecomment-43082336
583 $info['warning'][] = 'Unexpectedly large number ('.$CommentsCount.') of Ogg comments - breaking after reading '.$i.' comments';
587 $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset;
589 if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) {
590 if ($oggpageinfo = $this->ParseOggPageHeader()) {
591 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
593 $VorbisCommentPage++;
595 // First, save what we haven't read yet
596 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
598 // Then take that data off the end
599 $commentdata = substr($commentdata, 0, $commentdataoffset);
601 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
602 $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
603 $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
605 // Finally, stick the unused data back on the end
606 $commentdata .= $AsYetUnusedData;
608 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
609 $commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
613 $ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
615 // replace avdataoffset with position just after the last vorbiscomment
616 $info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4;
618 $commentdataoffset += 4;
619 while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
620 if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
621 $info['warning'][] = 'Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments';
625 $VorbisCommentPage++;
627 $oggpageinfo = $this->ParseOggPageHeader();
628 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
630 // First, save what we haven't read yet
631 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
633 // Then take that data off the end
634 $commentdata = substr($commentdata, 0, $commentdataoffset);
636 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
637 $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
638 $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
640 // Finally, stick the unused data back on the end
641 $commentdata .= $AsYetUnusedData;
643 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
644 if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
645 $info['warning'][] = 'undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
648 $readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
649 if ($readlength <= 0) {
650 $info['warning'][] = 'invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell();
653 $commentdata .= $this->fread($readlength);
655 //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
657 $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
658 $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
659 $commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size'];
661 if (!$commentstring) {
664 $info['warning'][] = 'Blank Ogg comment ['.$i.']';
666 } elseif (strstr($commentstring, '=')) {
668 $commentexploded = explode('=', $commentstring, 2);
669 $ThisFileInfo_ogg_comments_raw[$i]['key'] = strtoupper($commentexploded[0]);
670 $ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : '');
672 if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
674 // http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
675 // 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.
676 // http://flac.sourceforge.net/format.html#metadata_block_picture
677 $flac = new getid3_flac($this->getid3);
678 $flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
679 $flac->parsePICTURE();
680 $info['ogg']['comments']['picture'][] = $flac->getid3->info['flac']['PICTURE'][0];
683 } elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
685 $data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
686 $this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
687 /** @todo use 'coverartmime' where available */
688 $imageinfo = getid3_lib::GetDataImageSize($data);
689 if ($imageinfo === false || !isset($imageinfo['mime'])) {
690 $this->warning('COVERART vorbiscomment tag contains invalid image');
694 $ogg = new self($this->getid3);
695 $ogg->setStringMode($data);
696 $info['ogg']['comments']['picture'][] = array(
697 'image_mime' => $imageinfo['mime'],
698 'datalength' => strlen($data),
699 'picturetype' => 'cover art',
700 'image_height' => $imageinfo['height'],
701 'image_width' => $imageinfo['width'],
702 'data' => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
708 $info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
714 $info['warning'][] = '[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring;
717 unset($ThisFileInfo_ogg_comments_raw[$i]);
719 unset($ThisFileInfo_ogg_comments_raw);
722 // Replay Gain Adjustment
723 // http://privatewww.essex.ac.uk/~djmrob/replaygain/
724 if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
725 foreach ($info['ogg']['comments'] as $index => $commentvalue) {
727 case 'rg_audiophile':
728 case 'replaygain_album_gain':
729 $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
730 unset($info['ogg']['comments'][$index]);
734 case 'replaygain_track_gain':
735 $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
736 unset($info['ogg']['comments'][$index]);
739 case 'replaygain_album_peak':
740 $info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
741 unset($info['ogg']['comments'][$index]);
745 case 'replaygain_track_peak':
746 $info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
747 unset($info['ogg']['comments'][$index]);
750 case 'replaygain_reference_loudness':
751 $info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
752 unset($info['ogg']['comments'][$index]);
762 $this->fseek($OriginalOffset);
767 public static function SpeexBandModeLookup($mode) {
768 static $SpeexBandModeLookup = array();
769 if (empty($SpeexBandModeLookup)) {
770 $SpeexBandModeLookup[0] = 'narrow';
771 $SpeexBandModeLookup[1] = 'wide';
772 $SpeexBandModeLookup[2] = 'ultra-wide';
774 return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null);
778 public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
779 for ($i = 0; $i < $SegmentNumber; $i++) {
781 foreach ($OggInfoArray['segment_table'] as $key => $value) {
782 $segmentlength += $value;
788 return $segmentlength;
792 public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
794 // decrease precision
795 $nominal_bitrate = $nominal_bitrate / 1000;
797 if ($nominal_bitrate < 128) {
799 $qval = ($nominal_bitrate - 64) / 16;
800 } elseif ($nominal_bitrate < 256) {
802 $qval = $nominal_bitrate / 32;
803 } elseif ($nominal_bitrate < 320) {
805 $qval = ($nominal_bitrate + 256) / 64;
808 $qval = ($nominal_bitrate + 1300) / 180;
810 //return $qval; // 5.031324
811 //return intval($qval); // 5
812 return round($qval, 1); // 5 or 4.9
815 public static function TheoraColorSpace($colorspace_id) {
816 // http://www.theora.org/doc/Theora.pdf (table 6.3)
817 static $TheoraColorSpaceLookup = array();
818 if (empty($TheoraColorSpaceLookup)) {
819 $TheoraColorSpaceLookup[0] = 'Undefined';
820 $TheoraColorSpaceLookup[1] = 'Rec. 470M';
821 $TheoraColorSpaceLookup[2] = 'Rec. 470BG';
822 $TheoraColorSpaceLookup[3] = 'Reserved';
824 return (isset($TheoraColorSpaceLookup[$colorspace_id]) ? $TheoraColorSpaceLookup[$colorspace_id] : null);
827 public static function TheoraPixelFormat($pixelformat_id) {
828 // http://www.theora.org/doc/Theora.pdf (table 6.4)
829 static $TheoraPixelFormatLookup = array();
830 if (empty($TheoraPixelFormatLookup)) {
831 $TheoraPixelFormatLookup[0] = '4:2:0';
832 $TheoraPixelFormatLookup[1] = 'Reserved';
833 $TheoraPixelFormatLookup[2] = '4:2:2';
834 $TheoraPixelFormatLookup[3] = '4:4:4';
836 return (isset($TheoraPixelFormatLookup[$pixelformat_id]) ? $TheoraPixelFormatLookup[$pixelformat_id] : null);