3 /////////////////////////////////////////////////////////////////
4 /// getID3() by James Heinrich <info@getid3.org> //
5 // available at https://github.com/JamesHeinrich/getID3 //
6 // or https://www.getid3.org //
7 // or http://getid3.sourceforge.net //
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 if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
20 getid3_lib
::IncludeDependency(GETID3_INCLUDEPATH
.'module.audio.flac.php', __FILE__
, true);
22 class getid3_ogg
extends getid3_handler
25 * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html
29 public function Analyze() {
30 $info = &$this->getid3
->info
;
32 $info['fileformat'] = 'ogg';
34 // Warn about illegal tags - only vorbiscomments are allowed
35 if (isset($info['id3v2'])) {
36 $this->warning('Illegal ID3v2 tag present.');
38 if (isset($info['id3v1'])) {
39 $this->warning('Illegal ID3v1 tag present.');
41 if (isset($info['ape'])) {
42 $this->warning('Illegal APE tag present.');
46 // Page 1 - Stream Header
48 $this->fseek($info['avdataoffset']);
50 $oggpageinfo = $this->ParseOggPageHeader();
51 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
53 if ($this->ftell() >= $this->getid3
->fread_buffer_size()) {
54 $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?)');
55 unset($info['fileformat']);
60 $filedata = $this->fread($oggpageinfo['page_length']);
63 if (substr($filedata, 0, 4) == 'fLaC') {
65 $info['audio']['dataformat'] = 'flac';
66 $info['audio']['bitrate_mode'] = 'vbr';
67 $info['audio']['lossless'] = true;
69 } elseif (substr($filedata, 1, 6) == 'vorbis') {
71 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
73 } elseif (substr($filedata, 0, 8) == 'OpusHead') {
75 if ($this->ParseOpusPageHeader($filedata, $filedataoffset, $oggpageinfo) === false) {
79 } elseif (substr($filedata, 0, 8) == 'Speex ') {
81 // http://www.speex.org/manual/node10.html
83 $info['audio']['dataformat'] = 'speex';
84 $info['mime_type'] = 'audio/speex';
85 $info['audio']['bitrate_mode'] = 'abr';
86 $info['audio']['lossless'] = false;
88 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex '
90 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version'] = substr($filedata, $filedataoffset, 20);
91 $filedataoffset +
= 20;
92 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
94 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
96 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
98 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
100 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
101 $filedataoffset +
= 4;
102 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
103 $filedataoffset +
= 4;
104 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
105 $filedataoffset +
= 4;
106 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
107 $filedataoffset +
= 4;
108 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
109 $filedataoffset +
= 4;
110 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
111 $filedataoffset +
= 4;
112 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
113 $filedataoffset +
= 4;
114 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
115 $filedataoffset +
= 4;
116 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
117 $filedataoffset +
= 4;
119 $info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']);
120 $info['speex']['sample_rate'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'];
121 $info['speex']['channels'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'];
122 $info['speex']['vbr'] = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'];
123 $info['speex']['band_type'] = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']);
125 $info['audio']['sample_rate'] = $info['speex']['sample_rate'];
126 $info['audio']['channels'] = $info['speex']['channels'];
127 if ($info['speex']['vbr']) {
128 $info['audio']['bitrate_mode'] = 'vbr';
131 } elseif (substr($filedata, 0, 7) == "\x80".'theora') {
133 // http://www.theora.org/doc/Theora.pdf (section 6.2)
135 $info['ogg']['pageheader']['theora']['theora_magic'] = substr($filedata, $filedataoffset, 7); // hard-coded to "\x80.'theora'
136 $filedataoffset +
= 7;
137 $info['ogg']['pageheader']['theora']['version_major'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
138 $filedataoffset +
= 1;
139 $info['ogg']['pageheader']['theora']['version_minor'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
140 $filedataoffset +
= 1;
141 $info['ogg']['pageheader']['theora']['version_revision'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
142 $filedataoffset +
= 1;
143 $info['ogg']['pageheader']['theora']['frame_width_macroblocks'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 2));
144 $filedataoffset +
= 2;
145 $info['ogg']['pageheader']['theora']['frame_height_macroblocks'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 2));
146 $filedataoffset +
= 2;
147 $info['ogg']['pageheader']['theora']['resolution_x'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 3));
148 $filedataoffset +
= 3;
149 $info['ogg']['pageheader']['theora']['resolution_y'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 3));
150 $filedataoffset +
= 3;
151 $info['ogg']['pageheader']['theora']['picture_offset_x'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
152 $filedataoffset +
= 1;
153 $info['ogg']['pageheader']['theora']['picture_offset_y'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
154 $filedataoffset +
= 1;
155 $info['ogg']['pageheader']['theora']['frame_rate_numerator'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 4));
156 $filedataoffset +
= 4;
157 $info['ogg']['pageheader']['theora']['frame_rate_denominator'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 4));
158 $filedataoffset +
= 4;
159 $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 3));
160 $filedataoffset +
= 3;
161 $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 3));
162 $filedataoffset +
= 3;
163 $info['ogg']['pageheader']['theora']['color_space_id'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
164 $filedataoffset +
= 1;
165 $info['ogg']['pageheader']['theora']['nominal_bitrate'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 3));
166 $filedataoffset +
= 3;
167 $info['ogg']['pageheader']['theora']['flags'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 2));
168 $filedataoffset +
= 2;
170 $info['ogg']['pageheader']['theora']['quality'] = ($info['ogg']['pageheader']['theora']['flags'] & 0xFC00) >> 10;
171 $info['ogg']['pageheader']['theora']['kfg_shift'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x03E0) >> 5;
172 $info['ogg']['pageheader']['theora']['pixel_format_id'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0018) >> 3;
173 $info['ogg']['pageheader']['theora']['reserved'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0007) >> 0; // should be 0
174 $info['ogg']['pageheader']['theora']['color_space'] = self
::TheoraColorSpace($info['ogg']['pageheader']['theora']['color_space_id']);
175 $info['ogg']['pageheader']['theora']['pixel_format'] = self
::TheoraPixelFormat($info['ogg']['pageheader']['theora']['pixel_format_id']);
177 $info['video']['dataformat'] = 'theora';
178 $info['mime_type'] = 'video/ogg';
179 //$info['audio']['bitrate_mode'] = 'abr';
180 //$info['audio']['lossless'] = false;
181 $info['video']['resolution_x'] = $info['ogg']['pageheader']['theora']['resolution_x'];
182 $info['video']['resolution_y'] = $info['ogg']['pageheader']['theora']['resolution_y'];
183 if ($info['ogg']['pageheader']['theora']['frame_rate_denominator'] > 0) {
184 $info['video']['frame_rate'] = (float) $info['ogg']['pageheader']['theora']['frame_rate_numerator'] / $info['ogg']['pageheader']['theora']['frame_rate_denominator'];
186 if ($info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] > 0) {
187 $info['video']['pixel_aspect_ratio'] = (float) $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] / $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'];
189 $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');
192 } elseif (substr($filedata, 0, 8) == "fishead\x00") {
194 // Ogg Skeleton version 3.0 Format Specification
195 // http://xiph.org/ogg/doc/skeleton.html
196 $filedataoffset +
= 8;
197 $info['ogg']['skeleton']['fishead']['raw']['version_major'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
198 $filedataoffset +
= 2;
199 $info['ogg']['skeleton']['fishead']['raw']['version_minor'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
200 $filedataoffset +
= 2;
201 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
202 $filedataoffset +
= 8;
203 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
204 $filedataoffset +
= 8;
205 $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
206 $filedataoffset +
= 8;
207 $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
208 $filedataoffset +
= 8;
209 $info['ogg']['skeleton']['fishead']['raw']['utc'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 20));
210 $filedataoffset +
= 20;
212 $info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor'];
213 $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'];
214 $info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'];
215 $info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc'];
220 $oggpageinfo = $this->ParseOggPageHeader();
221 $info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++
] = $oggpageinfo;
222 $filedata = $this->fread($oggpageinfo['page_length']);
223 $this->fseek($oggpageinfo['page_end_offset']);
225 if (substr($filedata, 0, 8) == "fisbone\x00") {
228 $info['ogg']['skeleton']['fisbone']['raw']['message_header_offset'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
229 $filedataoffset +
= 4;
230 $info['ogg']['skeleton']['fisbone']['raw']['serial_number'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
231 $filedataoffset +
= 4;
232 $info['ogg']['skeleton']['fisbone']['raw']['number_header_packets'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
233 $filedataoffset +
= 4;
234 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
235 $filedataoffset +
= 8;
236 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
237 $filedataoffset +
= 8;
238 $info['ogg']['skeleton']['fisbone']['raw']['basegranule'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
239 $filedataoffset +
= 8;
240 $info['ogg']['skeleton']['fisbone']['raw']['preroll'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
241 $filedataoffset +
= 4;
242 $info['ogg']['skeleton']['fisbone']['raw']['granuleshift'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
243 $filedataoffset +
= 1;
244 $info['ogg']['skeleton']['fisbone']['raw']['padding'] = substr($filedata, $filedataoffset, 3);
245 $filedataoffset +
= 3;
247 } elseif (substr($filedata, 1, 6) == 'theora') {
249 $info['video']['dataformat'] = 'theora1';
250 $this->error('Ogg Theora (v1) not correctly handled in this version of getID3 ['.$this->getid3
->version().']');
253 } elseif (substr($filedata, 1, 6) == 'vorbis') {
255 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
258 $this->error('unexpected');
261 //} while ($oggpageinfo['page_seqno'] == 0);
262 } while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
264 $this->fseek($oggpageinfo['page_start_offset']);
266 $this->error('Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3
->version().']');
269 } elseif (substr($filedata, 0, 5) == "\x7F".'FLAC') {
270 // https://xiph.org/flac/ogg_mapping.html
272 $info['audio']['dataformat'] = 'flac';
273 $info['audio']['bitrate_mode'] = 'vbr';
274 $info['audio']['lossless'] = true;
276 $info['ogg']['flac']['header']['version_major'] = ord(substr($filedata, 5, 1));
277 $info['ogg']['flac']['header']['version_minor'] = ord(substr($filedata, 6, 1));
278 $info['ogg']['flac']['header']['header_packets'] = getid3_lib
::BigEndian2Int(substr($filedata, 7, 2)) +
1; // "A two-byte, big-endian binary number signifying the number of header (non-audio) packets, not including this one. This number may be zero (0x0000) to signify 'unknown' but be aware that some decoders may not be able to handle such streams."
279 $info['ogg']['flac']['header']['magic'] = substr($filedata, 9, 4);
280 if ($info['ogg']['flac']['header']['magic'] != 'fLaC') {
281 $this->error('Ogg-FLAC expecting "fLaC", found "'.$info['ogg']['flac']['header']['magic'].'" ('.trim(getid3_lib
::PrintHexBytes($info['ogg']['flac']['header']['magic'])).')');
284 $info['ogg']['flac']['header']['STREAMINFO_bytes'] = getid3_lib
::BigEndian2Int(substr($filedata, 13, 4));
285 $info['flac']['STREAMINFO'] = getid3_flac
::parseSTREAMINFOdata(substr($filedata, 17, 34));
286 if (!empty($info['flac']['STREAMINFO']['sample_rate'])) {
287 $info['audio']['bitrate_mode'] = 'vbr';
288 $info['audio']['sample_rate'] = $info['flac']['STREAMINFO']['sample_rate'];
289 $info['audio']['channels'] = $info['flac']['STREAMINFO']['channels'];
290 $info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample'];
291 $info['playtime_seconds'] = $info['flac']['STREAMINFO']['samples_stream'] / $info['flac']['STREAMINFO']['sample_rate'];
296 $this->error('Expecting one of "vorbis", "Speex", "OpusHead", "vorbis", "fishhead", "theora", "fLaC" identifier strings, found "'.substr($filedata, 0, 8).'"');
298 unset($info['mime_type']);
303 // Page 2 - Comment Header
304 $oggpageinfo = $this->ParseOggPageHeader();
305 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
307 switch ($info['audio']['dataformat']) {
309 $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
310 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib
::LittleEndian2Int(substr($filedata, 0, 1));
311 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 1, 6); // hard-coded to 'vorbis'
313 $this->ParseVorbisComments();
317 $flac = new getid3_flac($this->getid3
);
318 if (!$flac->parseMETAdata()) {
319 $this->error('Failed to parse FLAC headers');
326 $this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR
);
327 $this->ParseVorbisComments();
331 $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
332 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 0, 8); // hard-coded to 'OpusTags'
333 if(substr($filedata, 0, 8) != 'OpusTags') {
334 $this->error('Expected "OpusTags" as header but got "'.substr($filedata, 0, 8).'"');
338 $this->ParseVorbisComments();
343 // Last Page - Number of Samples
344 if (!getid3_lib
::intValueSupported($info['avdataend'])) {
346 $this->warning('Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX
/ 1073741824).'GB)');
350 $this->fseek(max($info['avdataend'] - $this->getid3
->fread_buffer_size(), 0));
351 $LastChunkOfOgg = strrev($this->fread($this->getid3
->fread_buffer_size()));
352 if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
353 $this->fseek($info['avdataend'] - ($LastOggSpostion +
strlen('SggO')));
354 $info['avdataend'] = $this->ftell();
355 $info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader();
356 $info['ogg']['samples'] = $info['ogg']['pageheader']['eos']['pcm_abs_position'];
357 if ($info['ogg']['samples'] == 0) {
358 $this->error('Corrupt Ogg file: eos.number of samples == zero');
361 if (!empty($info['audio']['sample_rate'])) {
362 $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
368 if (!empty($info['ogg']['bitrate_average'])) {
369 $info['audio']['bitrate'] = $info['ogg']['bitrate_average'];
370 } elseif (!empty($info['ogg']['bitrate_nominal'])) {
371 $info['audio']['bitrate'] = $info['ogg']['bitrate_nominal'];
372 } elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) {
373 $info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] +
$info['ogg']['bitrate_max']) / 2;
375 if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
376 if ($info['audio']['bitrate'] == 0) {
377 $this->error('Corrupt Ogg file: bitrate_audio == zero');
380 $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
383 if (isset($info['ogg']['vendor'])) {
384 $info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
387 if ($info['audio']['dataformat'] == 'vorbis') {
389 // Vorbis 1.0 starts with Xiph.Org
390 if (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
392 if ($info['audio']['bitrate_mode'] == 'abr') {
394 // Set -b 128 on abr files
395 $info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
397 } elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) {
398 // Set -q N on vbr files
399 $info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']);
404 if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) {
405 $info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps';
414 * @param string $filedata
415 * @param int $filedataoffset
416 * @param array $oggpageinfo
420 public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
421 $info = &$this->getid3
->info
;
422 $info['audio']['dataformat'] = 'vorbis';
423 $info['audio']['lossless'] = false;
425 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
426 $filedataoffset +
= 1;
427 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis'
428 $filedataoffset +
= 6;
429 $info['ogg']['bitstreamversion'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
430 $filedataoffset +
= 4;
431 $info['ogg']['numberofchannels'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
432 $filedataoffset +
= 1;
433 $info['audio']['channels'] = $info['ogg']['numberofchannels'];
434 $info['ogg']['samplerate'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
435 $filedataoffset +
= 4;
436 if ($info['ogg']['samplerate'] == 0) {
437 $this->error('Corrupt Ogg file: sample rate == zero');
440 $info['audio']['sample_rate'] = $info['ogg']['samplerate'];
441 $info['ogg']['samples'] = 0; // filled in later
442 $info['ogg']['bitrate_average'] = 0; // filled in later
443 $info['ogg']['bitrate_max'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
444 $filedataoffset +
= 4;
445 $info['ogg']['bitrate_nominal'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
446 $filedataoffset +
= 4;
447 $info['ogg']['bitrate_min'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
448 $filedataoffset +
= 4;
449 $info['ogg']['blocksize_small'] = pow(2, getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F);
450 $info['ogg']['blocksize_large'] = pow(2, (getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4);
451 $info['ogg']['stop_bit'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet
453 $info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr
454 if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) {
455 unset($info['ogg']['bitrate_max']);
456 $info['audio']['bitrate_mode'] = 'abr';
458 if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
459 unset($info['ogg']['bitrate_nominal']);
461 if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
462 unset($info['ogg']['bitrate_min']);
463 $info['audio']['bitrate_mode'] = 'abr';
469 * @link http://tools.ietf.org/html/draft-ietf-codec-oggopus-03
471 * @param string $filedata
472 * @param int $filedataoffset
473 * @param array $oggpageinfo
477 public function ParseOpusPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
478 $info = &$this->getid3
->info
;
479 $info['audio']['dataformat'] = 'opus';
480 $info['mime_type'] = 'audio/ogg; codecs=opus';
482 /** @todo find a usable way to detect abr (vbr that is padded to be abr) */
483 $info['audio']['bitrate_mode'] = 'vbr';
485 $info['audio']['lossless'] = false;
487 $info['ogg']['pageheader']['opus']['opus_magic'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'OpusHead'
488 $filedataoffset +
= 8;
489 $info['ogg']['pageheader']['opus']['version'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
490 $filedataoffset +
= 1;
492 if ($info['ogg']['pageheader']['opus']['version'] < 1 ||
$info['ogg']['pageheader']['opus']['version'] > 15) {
493 $this->error('Unknown opus version number (only accepting 1-15)');
497 $info['ogg']['pageheader']['opus']['out_channel_count'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
498 $filedataoffset +
= 1;
500 if ($info['ogg']['pageheader']['opus']['out_channel_count'] == 0) {
501 $this->error('Invalid channel count in opus header (must not be zero)');
505 $info['ogg']['pageheader']['opus']['pre_skip'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
506 $filedataoffset +
= 2;
508 $info['ogg']['pageheader']['opus']['input_sample_rate'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
509 $filedataoffset +
= 4;
511 //$info['ogg']['pageheader']['opus']['output_gain'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
512 //$filedataoffset += 2;
514 //$info['ogg']['pageheader']['opus']['channel_mapping_family'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
515 //$filedataoffset += 1;
517 $info['opus']['opus_version'] = $info['ogg']['pageheader']['opus']['version'];
518 $info['opus']['sample_rate_input'] = $info['ogg']['pageheader']['opus']['input_sample_rate'];
519 $info['opus']['out_channel_count'] = $info['ogg']['pageheader']['opus']['out_channel_count'];
521 $info['audio']['channels'] = $info['opus']['out_channel_count'];
522 $info['audio']['sample_rate_input'] = $info['opus']['sample_rate_input'];
523 $info['audio']['sample_rate'] = 48000; // "All Opus audio is coded at 48 kHz, and should also be decoded at 48 kHz for playback (unless the target hardware does not support this sampling rate). However, this field may be used to resample the audio back to the original sampling rate, for example, when saving the output to a file." -- https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/structOpusHead.html
528 * @return array|false
530 public function ParseOggPageHeader() {
531 // http://xiph.org/ogg/vorbis/doc/framing.html
532 $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file
534 $filedata = $this->fread($this->getid3
->fread_buffer_size());
536 while ((substr($filedata, $filedataoffset++
, 4) != 'OggS')) {
537 if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3
->fread_buffer_size()) {
538 // should be found before here
541 if ((($filedataoffset +
28) > strlen($filedata)) ||
(strlen($filedata) < 28)) {
542 if ($this->feof() ||
(($filedata .= $this->fread($this->getid3
->fread_buffer_size())) === '')) {
543 // get some more data, unless eof, in which case fail
548 $filedataoffset +
= strlen('OggS') - 1; // page, delimited by 'OggS'
550 $oggheader['stream_structver'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
551 $filedataoffset +
= 1;
552 $oggheader['flags_raw'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
553 $filedataoffset +
= 1;
554 $oggheader['flags']['fresh'] = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
555 $oggheader['flags']['bos'] = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
556 $oggheader['flags']['eos'] = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
558 $oggheader['pcm_abs_position'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
559 $filedataoffset +
= 8;
560 $oggheader['stream_serialno'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
561 $filedataoffset +
= 4;
562 $oggheader['page_seqno'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
563 $filedataoffset +
= 4;
564 $oggheader['page_checksum'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
565 $filedataoffset +
= 4;
566 $oggheader['page_segments'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
567 $filedataoffset +
= 1;
568 $oggheader['page_length'] = 0;
569 for ($i = 0; $i < $oggheader['page_segments']; $i++
) {
570 $oggheader['segment_table'][$i] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
571 $filedataoffset +
= 1;
572 $oggheader['page_length'] +
= $oggheader['segment_table'][$i];
574 $oggheader['header_end_offset'] = $oggheader['page_start_offset'] +
$filedataoffset;
575 $oggheader['page_end_offset'] = $oggheader['header_end_offset'] +
$oggheader['page_length'];
576 $this->fseek($oggheader['header_end_offset']);
582 * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
586 public function ParseVorbisComments() {
587 $info = &$this->getid3
->info
;
589 $OriginalOffset = $this->ftell();
591 $commentdataoffset = 0;
592 $VorbisCommentPage = 1;
593 $CommentStartOffset = 0;
595 switch ($info['audio']['dataformat']) {
599 $CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset']; // Second Ogg page, after header block
600 $this->fseek($CommentStartOffset);
601 $commentdataoffset = 27 +
$info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
602 $commentdata = $this->fread(self
::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) +
$commentdataoffset);
604 if ($info['audio']['dataformat'] == 'vorbis') {
605 $commentdataoffset +
= (strlen('vorbis') +
1);
607 else if ($info['audio']['dataformat'] == 'opus') {
608 $commentdataoffset +
= strlen('OpusTags');
614 $CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] +
4;
615 $this->fseek($CommentStartOffset);
616 $commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
623 $VendorSize = getid3_lib
::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
624 $commentdataoffset +
= 4;
626 $info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
627 $commentdataoffset +
= $VendorSize;
629 $CommentsCount = getid3_lib
::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
630 $commentdataoffset +
= 4;
631 $info['avdataoffset'] = $CommentStartOffset +
$commentdataoffset;
633 $basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
634 $ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
635 for ($i = 0; $i < $CommentsCount; $i++
) {
638 // https://github.com/owncloud/music/issues/212#issuecomment-43082336
639 $this->warning('Unexpectedly large number ('.$CommentsCount.') of Ogg comments - breaking after reading '.$i.' comments');
643 $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset +
$commentdataoffset;
645 if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] +
4)) {
646 if ($oggpageinfo = $this->ParseOggPageHeader()) {
647 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
649 $VorbisCommentPage++
;
651 // First, save what we haven't read yet
652 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
654 // Then take that data off the end
655 $commentdata = substr($commentdata, 0, $commentdataoffset);
657 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
658 $commentdata .= str_repeat("\x00", 27 +
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
659 $commentdataoffset +
= (27 +
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
661 // Finally, stick the unused data back on the end
662 $commentdata .= $AsYetUnusedData;
664 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
665 $commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
669 $ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib
::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
671 // replace avdataoffset with position just after the last vorbiscomment
672 $info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] +
$ThisFileInfo_ogg_comments_raw[$i]['size'] +
4;
674 $commentdataoffset +
= 4;
675 while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
676 if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) ||
($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
677 $this->warning('Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments');
681 $VorbisCommentPage++
;
683 $oggpageinfo = $this->ParseOggPageHeader();
684 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
686 // First, save what we haven't read yet
687 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
689 // Then take that data off the end
690 $commentdata = substr($commentdata, 0, $commentdataoffset);
692 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
693 $commentdata .= str_repeat("\x00", 27 +
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
694 $commentdataoffset +
= (27 +
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
696 // Finally, stick the unused data back on the end
697 $commentdata .= $AsYetUnusedData;
699 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
700 if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
701 $this->warning('undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
704 $readlength = self
::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
705 if ($readlength <= 0) {
706 $this->warning('invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
709 $commentdata .= $this->fread($readlength);
711 //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
713 $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
714 $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
715 $commentdataoffset +
= $ThisFileInfo_ogg_comments_raw[$i]['size'];
717 if (!$commentstring) {
720 $this->warning('Blank Ogg comment ['.$i.']');
722 } elseif (strstr($commentstring, '=')) {
724 $commentexploded = explode('=', $commentstring, 2);
725 $ThisFileInfo_ogg_comments_raw[$i]['key'] = strtoupper($commentexploded[0]);
726 $ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ?
$commentexploded[1] : '');
728 if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
730 // http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
731 // 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.
732 // http://flac.sourceforge.net/format.html#metadata_block_picture
733 $flac = new getid3_flac($this->getid3
);
734 $flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
735 $flac->parsePICTURE();
736 $info['ogg']['comments']['picture'][] = $flac->getid3
->info
['flac']['PICTURE'][0];
739 } elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
741 $data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
742 $this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
743 /** @todo use 'coverartmime' where available */
744 $imageinfo = getid3_lib
::GetDataImageSize($data);
745 if ($imageinfo === false ||
!isset($imageinfo['mime'])) {
746 $this->warning('COVERART vorbiscomment tag contains invalid image');
750 $ogg = new self($this->getid3
);
751 $ogg->setStringMode($data);
752 $info['ogg']['comments']['picture'][] = array(
753 'image_mime' => $imageinfo['mime'],
754 'datalength' => strlen($data),
755 'picturetype' => 'cover art',
756 'image_height' => $imageinfo['height'],
757 'image_width' => $imageinfo['width'],
758 'data' => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
764 $info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
770 $this->warning('[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring);
773 unset($ThisFileInfo_ogg_comments_raw[$i]);
775 unset($ThisFileInfo_ogg_comments_raw);
778 // Replay Gain Adjustment
779 // http://privatewww.essex.ac.uk/~djmrob/replaygain/
780 if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
781 foreach ($info['ogg']['comments'] as $index => $commentvalue) {
783 case 'rg_audiophile':
784 case 'replaygain_album_gain':
785 $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
786 unset($info['ogg']['comments'][$index]);
790 case 'replaygain_track_gain':
791 $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
792 unset($info['ogg']['comments'][$index]);
795 case 'replaygain_album_peak':
796 $info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
797 unset($info['ogg']['comments'][$index]);
801 case 'replaygain_track_peak':
802 $info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
803 unset($info['ogg']['comments'][$index]);
806 case 'replaygain_reference_loudness':
807 $info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
808 unset($info['ogg']['comments'][$index]);
818 $this->fseek($OriginalOffset);
826 * @return string|null
828 public static function SpeexBandModeLookup($mode) {
829 static $SpeexBandModeLookup = array();
830 if (empty($SpeexBandModeLookup)) {
831 $SpeexBandModeLookup[0] = 'narrow';
832 $SpeexBandModeLookup[1] = 'wide';
833 $SpeexBandModeLookup[2] = 'ultra-wide';
835 return (isset($SpeexBandModeLookup[$mode]) ?
$SpeexBandModeLookup[$mode] : null);
839 * @param array $OggInfoArray
840 * @param int $SegmentNumber
844 public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
846 for ($i = 0; $i < $SegmentNumber; $i++
) {
848 foreach ($OggInfoArray['segment_table'] as $key => $value) {
849 $segmentlength +
= $value;
855 return $segmentlength;
859 * @param int $nominal_bitrate
863 public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
865 // decrease precision
866 $nominal_bitrate = $nominal_bitrate / 1000;
868 if ($nominal_bitrate < 128) {
870 $qval = ($nominal_bitrate - 64) / 16;
871 } elseif ($nominal_bitrate < 256) {
873 $qval = $nominal_bitrate / 32;
874 } elseif ($nominal_bitrate < 320) {
876 $qval = ($nominal_bitrate +
256) / 64;
879 $qval = ($nominal_bitrate +
1300) / 180;
881 //return $qval; // 5.031324
882 //return intval($qval); // 5
883 return round($qval, 1); // 5 or 4.9
887 * @param int $colorspace_id
889 * @return string|null
891 public static function TheoraColorSpace($colorspace_id) {
892 // http://www.theora.org/doc/Theora.pdf (table 6.3)
893 static $TheoraColorSpaceLookup = array();
894 if (empty($TheoraColorSpaceLookup)) {
895 $TheoraColorSpaceLookup[0] = 'Undefined';
896 $TheoraColorSpaceLookup[1] = 'Rec. 470M';
897 $TheoraColorSpaceLookup[2] = 'Rec. 470BG';
898 $TheoraColorSpaceLookup[3] = 'Reserved';
900 return (isset($TheoraColorSpaceLookup[$colorspace_id]) ?
$TheoraColorSpaceLookup[$colorspace_id] : null);
904 * @param int $pixelformat_id
906 * @return string|null
908 public static function TheoraPixelFormat($pixelformat_id) {
909 // http://www.theora.org/doc/Theora.pdf (table 6.4)
910 static $TheoraPixelFormatLookup = array();
911 if (empty($TheoraPixelFormatLookup)) {
912 $TheoraPixelFormatLookup[0] = '4:2:0';
913 $TheoraPixelFormatLookup[1] = 'Reserved';
914 $TheoraPixelFormatLookup[2] = '4:2:2';
915 $TheoraPixelFormatLookup[3] = '4:4:4';
917 return (isset($TheoraPixelFormatLookup[$pixelformat_id]) ?
$TheoraPixelFormatLookup[$pixelformat_id] : null);