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 getid3_lib
::IncludeDependency(GETID3_INCLUDEPATH
.'module.audio.flac.php', __FILE__
, true);
19 class getid3_ogg
extends getid3_handler
22 * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html
26 public function Analyze() {
27 $info = &$this->getid3
->info
;
29 $info['fileformat'] = 'ogg';
31 // Warn about illegal tags - only vorbiscomments are allowed
32 if (isset($info['id3v2'])) {
33 $this->warning('Illegal ID3v2 tag present.');
35 if (isset($info['id3v1'])) {
36 $this->warning('Illegal ID3v1 tag present.');
38 if (isset($info['ape'])) {
39 $this->warning('Illegal APE tag present.');
43 // Page 1 - Stream Header
45 $this->fseek($info['avdataoffset']);
47 $oggpageinfo = $this->ParseOggPageHeader();
48 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
50 if ($this->ftell() >= $this->getid3
->fread_buffer_size()) {
51 $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?)');
52 unset($info['fileformat']);
57 $filedata = $this->fread($oggpageinfo['page_length']);
60 if (substr($filedata, 0, 4) == 'fLaC') {
62 $info['audio']['dataformat'] = 'flac';
63 $info['audio']['bitrate_mode'] = 'vbr';
64 $info['audio']['lossless'] = true;
66 } elseif (substr($filedata, 1, 6) == 'vorbis') {
68 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
70 } elseif (substr($filedata, 0, 8) == 'OpusHead') {
72 if ($this->ParseOpusPageHeader($filedata, $filedataoffset, $oggpageinfo) === false) {
76 } elseif (substr($filedata, 0, 8) == 'Speex ') {
78 // http://www.speex.org/manual/node10.html
80 $info['audio']['dataformat'] = 'speex';
81 $info['mime_type'] = 'audio/speex';
82 $info['audio']['bitrate_mode'] = 'abr';
83 $info['audio']['lossless'] = false;
85 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex '
87 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version'] = substr($filedata, $filedataoffset, 20);
88 $filedataoffset +
= 20;
89 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
91 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
93 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
95 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
97 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
99 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
100 $filedataoffset +
= 4;
101 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
102 $filedataoffset +
= 4;
103 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
104 $filedataoffset +
= 4;
105 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
106 $filedataoffset +
= 4;
107 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
108 $filedataoffset +
= 4;
109 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
110 $filedataoffset +
= 4;
111 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
112 $filedataoffset +
= 4;
113 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
114 $filedataoffset +
= 4;
116 $info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']);
117 $info['speex']['sample_rate'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'];
118 $info['speex']['channels'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'];
119 $info['speex']['vbr'] = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'];
120 $info['speex']['band_type'] = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']);
122 $info['audio']['sample_rate'] = $info['speex']['sample_rate'];
123 $info['audio']['channels'] = $info['speex']['channels'];
124 if ($info['speex']['vbr']) {
125 $info['audio']['bitrate_mode'] = 'vbr';
128 } elseif (substr($filedata, 0, 7) == "\x80".'theora') {
130 // http://www.theora.org/doc/Theora.pdf (section 6.2)
132 $info['ogg']['pageheader']['theora']['theora_magic'] = substr($filedata, $filedataoffset, 7); // hard-coded to "\x80.'theora'
133 $filedataoffset +
= 7;
134 $info['ogg']['pageheader']['theora']['version_major'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
135 $filedataoffset +
= 1;
136 $info['ogg']['pageheader']['theora']['version_minor'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
137 $filedataoffset +
= 1;
138 $info['ogg']['pageheader']['theora']['version_revision'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
139 $filedataoffset +
= 1;
140 $info['ogg']['pageheader']['theora']['frame_width_macroblocks'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 2));
141 $filedataoffset +
= 2;
142 $info['ogg']['pageheader']['theora']['frame_height_macroblocks'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 2));
143 $filedataoffset +
= 2;
144 $info['ogg']['pageheader']['theora']['resolution_x'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 3));
145 $filedataoffset +
= 3;
146 $info['ogg']['pageheader']['theora']['resolution_y'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 3));
147 $filedataoffset +
= 3;
148 $info['ogg']['pageheader']['theora']['picture_offset_x'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
149 $filedataoffset +
= 1;
150 $info['ogg']['pageheader']['theora']['picture_offset_y'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
151 $filedataoffset +
= 1;
152 $info['ogg']['pageheader']['theora']['frame_rate_numerator'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 4));
153 $filedataoffset +
= 4;
154 $info['ogg']['pageheader']['theora']['frame_rate_denominator'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 4));
155 $filedataoffset +
= 4;
156 $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 3));
157 $filedataoffset +
= 3;
158 $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 3));
159 $filedataoffset +
= 3;
160 $info['ogg']['pageheader']['theora']['color_space_id'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 1));
161 $filedataoffset +
= 1;
162 $info['ogg']['pageheader']['theora']['nominal_bitrate'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 3));
163 $filedataoffset +
= 3;
164 $info['ogg']['pageheader']['theora']['flags'] = getid3_lib
::BigEndian2Int(substr($filedata, $filedataoffset, 2));
165 $filedataoffset +
= 2;
167 $info['ogg']['pageheader']['theora']['quality'] = ($info['ogg']['pageheader']['theora']['flags'] & 0xFC00) >> 10;
168 $info['ogg']['pageheader']['theora']['kfg_shift'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x03E0) >> 5;
169 $info['ogg']['pageheader']['theora']['pixel_format_id'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0018) >> 3;
170 $info['ogg']['pageheader']['theora']['reserved'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0007) >> 0; // should be 0
171 $info['ogg']['pageheader']['theora']['color_space'] = self
::TheoraColorSpace($info['ogg']['pageheader']['theora']['color_space_id']);
172 $info['ogg']['pageheader']['theora']['pixel_format'] = self
::TheoraPixelFormat($info['ogg']['pageheader']['theora']['pixel_format_id']);
174 $info['video']['dataformat'] = 'theora';
175 $info['mime_type'] = 'video/ogg';
176 //$info['audio']['bitrate_mode'] = 'abr';
177 //$info['audio']['lossless'] = false;
178 $info['video']['resolution_x'] = $info['ogg']['pageheader']['theora']['resolution_x'];
179 $info['video']['resolution_y'] = $info['ogg']['pageheader']['theora']['resolution_y'];
180 if ($info['ogg']['pageheader']['theora']['frame_rate_denominator'] > 0) {
181 $info['video']['frame_rate'] = (float) $info['ogg']['pageheader']['theora']['frame_rate_numerator'] / $info['ogg']['pageheader']['theora']['frame_rate_denominator'];
183 if ($info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] > 0) {
184 $info['video']['pixel_aspect_ratio'] = (float) $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] / $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'];
186 $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');
189 } elseif (substr($filedata, 0, 8) == "fishead\x00") {
191 // Ogg Skeleton version 3.0 Format Specification
192 // http://xiph.org/ogg/doc/skeleton.html
193 $filedataoffset +
= 8;
194 $info['ogg']['skeleton']['fishead']['raw']['version_major'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
195 $filedataoffset +
= 2;
196 $info['ogg']['skeleton']['fishead']['raw']['version_minor'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
197 $filedataoffset +
= 2;
198 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
199 $filedataoffset +
= 8;
200 $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
201 $filedataoffset +
= 8;
202 $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
203 $filedataoffset +
= 8;
204 $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
205 $filedataoffset +
= 8;
206 $info['ogg']['skeleton']['fishead']['raw']['utc'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 20));
207 $filedataoffset +
= 20;
209 $info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor'];
210 $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'];
211 $info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'];
212 $info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc'];
217 $oggpageinfo = $this->ParseOggPageHeader();
218 $info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++
] = $oggpageinfo;
219 $filedata = $this->fread($oggpageinfo['page_length']);
220 $this->fseek($oggpageinfo['page_end_offset']);
222 if (substr($filedata, 0, 8) == "fisbone\x00") {
225 $info['ogg']['skeleton']['fisbone']['raw']['message_header_offset'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
226 $filedataoffset +
= 4;
227 $info['ogg']['skeleton']['fisbone']['raw']['serial_number'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
228 $filedataoffset +
= 4;
229 $info['ogg']['skeleton']['fisbone']['raw']['number_header_packets'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
230 $filedataoffset +
= 4;
231 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
232 $filedataoffset +
= 8;
233 $info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
234 $filedataoffset +
= 8;
235 $info['ogg']['skeleton']['fisbone']['raw']['basegranule'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
236 $filedataoffset +
= 8;
237 $info['ogg']['skeleton']['fisbone']['raw']['preroll'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
238 $filedataoffset +
= 4;
239 $info['ogg']['skeleton']['fisbone']['raw']['granuleshift'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
240 $filedataoffset +
= 1;
241 $info['ogg']['skeleton']['fisbone']['raw']['padding'] = substr($filedata, $filedataoffset, 3);
242 $filedataoffset +
= 3;
244 } elseif (substr($filedata, 1, 6) == 'theora') {
246 $info['video']['dataformat'] = 'theora1';
247 $this->error('Ogg Theora (v1) not correctly handled in this version of getID3 ['.$this->getid3
->version().']');
250 } elseif (substr($filedata, 1, 6) == 'vorbis') {
252 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
255 $this->error('unexpected');
258 //} while ($oggpageinfo['page_seqno'] == 0);
259 } while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
261 $this->fseek($oggpageinfo['page_start_offset']);
263 $this->error('Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3
->version().']');
266 } elseif (substr($filedata, 0, 5) == "\x7F".'FLAC') {
267 // https://xiph.org/flac/ogg_mapping.html
269 $info['audio']['dataformat'] = 'flac';
270 $info['audio']['bitrate_mode'] = 'vbr';
271 $info['audio']['lossless'] = true;
273 $info['ogg']['flac']['header']['version_major'] = ord(substr($filedata, 5, 1));
274 $info['ogg']['flac']['header']['version_minor'] = ord(substr($filedata, 6, 1));
275 $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."
276 $info['ogg']['flac']['header']['magic'] = substr($filedata, 9, 4);
277 if ($info['ogg']['flac']['header']['magic'] != 'fLaC') {
278 $this->error('Ogg-FLAC expecting "fLaC", found "'.$info['ogg']['flac']['header']['magic'].'" ('.trim(getid3_lib
::PrintHexBytes($info['ogg']['flac']['header']['magic'])).')');
281 $info['ogg']['flac']['header']['STREAMINFO_bytes'] = getid3_lib
::BigEndian2Int(substr($filedata, 13, 4));
282 $info['flac']['STREAMINFO'] = getid3_flac
::parseSTREAMINFOdata(substr($filedata, 17, 34));
283 if (!empty($info['flac']['STREAMINFO']['sample_rate'])) {
284 $info['audio']['bitrate_mode'] = 'vbr';
285 $info['audio']['sample_rate'] = $info['flac']['STREAMINFO']['sample_rate'];
286 $info['audio']['channels'] = $info['flac']['STREAMINFO']['channels'];
287 $info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample'];
288 $info['playtime_seconds'] = $info['flac']['STREAMINFO']['samples_stream'] / $info['flac']['STREAMINFO']['sample_rate'];
293 $this->error('Expecting one of "vorbis", "Speex", "OpusHead", "vorbis", "fishhead", "theora", "fLaC" identifier strings, found "'.substr($filedata, 0, 8).'"');
295 unset($info['mime_type']);
300 // Page 2 - Comment Header
301 $oggpageinfo = $this->ParseOggPageHeader();
302 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
304 switch ($info['audio']['dataformat']) {
306 $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
307 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib
::LittleEndian2Int(substr($filedata, 0, 1));
308 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 1, 6); // hard-coded to 'vorbis'
310 $this->ParseVorbisComments();
314 $flac = new getid3_flac($this->getid3
);
315 if (!$flac->parseMETAdata()) {
316 $this->error('Failed to parse FLAC headers');
323 $this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR
);
324 $this->ParseVorbisComments();
328 $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
329 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 0, 8); // hard-coded to 'OpusTags'
330 if(substr($filedata, 0, 8) != 'OpusTags') {
331 $this->error('Expected "OpusTags" as header but got "'.substr($filedata, 0, 8).'"');
335 $this->ParseVorbisComments();
340 // Last Page - Number of Samples
341 if (!getid3_lib
::intValueSupported($info['avdataend'])) {
343 $this->warning('Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX
/ 1073741824).'GB)');
347 $this->fseek(max($info['avdataend'] - $this->getid3
->fread_buffer_size(), 0));
348 $LastChunkOfOgg = strrev($this->fread($this->getid3
->fread_buffer_size()));
349 if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
350 $this->fseek($info['avdataend'] - ($LastOggSpostion +
strlen('SggO')));
351 $info['avdataend'] = $this->ftell();
352 $info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader();
353 $info['ogg']['samples'] = $info['ogg']['pageheader']['eos']['pcm_abs_position'];
354 if ($info['ogg']['samples'] == 0) {
355 $this->error('Corrupt Ogg file: eos.number of samples == zero');
358 if (!empty($info['audio']['sample_rate'])) {
359 $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
365 if (!empty($info['ogg']['bitrate_average'])) {
366 $info['audio']['bitrate'] = $info['ogg']['bitrate_average'];
367 } elseif (!empty($info['ogg']['bitrate_nominal'])) {
368 $info['audio']['bitrate'] = $info['ogg']['bitrate_nominal'];
369 } elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) {
370 $info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] +
$info['ogg']['bitrate_max']) / 2;
372 if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
373 if ($info['audio']['bitrate'] == 0) {
374 $this->error('Corrupt Ogg file: bitrate_audio == zero');
377 $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
380 if (isset($info['ogg']['vendor'])) {
381 $info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
384 if ($info['audio']['dataformat'] == 'vorbis') {
386 // Vorbis 1.0 starts with Xiph.Org
387 if (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
389 if ($info['audio']['bitrate_mode'] == 'abr') {
391 // Set -b 128 on abr files
392 $info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
394 } elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) {
395 // Set -q N on vbr files
396 $info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']);
401 if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) {
402 $info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps';
411 * @param string $filedata
412 * @param int $filedataoffset
413 * @param array $oggpageinfo
417 public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
418 $info = &$this->getid3
->info
;
419 $info['audio']['dataformat'] = 'vorbis';
420 $info['audio']['lossless'] = false;
422 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
423 $filedataoffset +
= 1;
424 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis'
425 $filedataoffset +
= 6;
426 $info['ogg']['bitstreamversion'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
427 $filedataoffset +
= 4;
428 $info['ogg']['numberofchannels'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
429 $filedataoffset +
= 1;
430 $info['audio']['channels'] = $info['ogg']['numberofchannels'];
431 $info['ogg']['samplerate'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
432 $filedataoffset +
= 4;
433 if ($info['ogg']['samplerate'] == 0) {
434 $this->error('Corrupt Ogg file: sample rate == zero');
437 $info['audio']['sample_rate'] = $info['ogg']['samplerate'];
438 $info['ogg']['samples'] = 0; // filled in later
439 $info['ogg']['bitrate_average'] = 0; // filled in later
440 $info['ogg']['bitrate_max'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
441 $filedataoffset +
= 4;
442 $info['ogg']['bitrate_nominal'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
443 $filedataoffset +
= 4;
444 $info['ogg']['bitrate_min'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
445 $filedataoffset +
= 4;
446 $info['ogg']['blocksize_small'] = pow(2, getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F);
447 $info['ogg']['blocksize_large'] = pow(2, (getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4);
448 $info['ogg']['stop_bit'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet
450 $info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr
451 if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) {
452 unset($info['ogg']['bitrate_max']);
453 $info['audio']['bitrate_mode'] = 'abr';
455 if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
456 unset($info['ogg']['bitrate_nominal']);
458 if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
459 unset($info['ogg']['bitrate_min']);
460 $info['audio']['bitrate_mode'] = 'abr';
466 * @link http://tools.ietf.org/html/draft-ietf-codec-oggopus-03
468 * @param string $filedata
469 * @param int $filedataoffset
470 * @param array $oggpageinfo
474 public function ParseOpusPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
475 $info = &$this->getid3
->info
;
476 $info['audio']['dataformat'] = 'opus';
477 $info['mime_type'] = 'audio/ogg; codecs=opus';
479 /** @todo find a usable way to detect abr (vbr that is padded to be abr) */
480 $info['audio']['bitrate_mode'] = 'vbr';
482 $info['audio']['lossless'] = false;
484 $info['ogg']['pageheader']['opus']['opus_magic'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'OpusHead'
485 $filedataoffset +
= 8;
486 $info['ogg']['pageheader']['opus']['version'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
487 $filedataoffset +
= 1;
489 if ($info['ogg']['pageheader']['opus']['version'] < 1 ||
$info['ogg']['pageheader']['opus']['version'] > 15) {
490 $this->error('Unknown opus version number (only accepting 1-15)');
494 $info['ogg']['pageheader']['opus']['out_channel_count'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
495 $filedataoffset +
= 1;
497 if ($info['ogg']['pageheader']['opus']['out_channel_count'] == 0) {
498 $this->error('Invalid channel count in opus header (must not be zero)');
502 $info['ogg']['pageheader']['opus']['pre_skip'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
503 $filedataoffset +
= 2;
505 $info['ogg']['pageheader']['opus']['input_sample_rate'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
506 $filedataoffset +
= 4;
508 //$info['ogg']['pageheader']['opus']['output_gain'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2));
509 //$filedataoffset += 2;
511 //$info['ogg']['pageheader']['opus']['channel_mapping_family'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
512 //$filedataoffset += 1;
514 $info['opus']['opus_version'] = $info['ogg']['pageheader']['opus']['version'];
515 $info['opus']['sample_rate_input'] = $info['ogg']['pageheader']['opus']['input_sample_rate'];
516 $info['opus']['out_channel_count'] = $info['ogg']['pageheader']['opus']['out_channel_count'];
518 $info['audio']['channels'] = $info['opus']['out_channel_count'];
519 $info['audio']['sample_rate_input'] = $info['opus']['sample_rate_input'];
520 $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
525 * @return array|false
527 public function ParseOggPageHeader() {
528 // http://xiph.org/ogg/vorbis/doc/framing.html
529 $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file
531 $filedata = $this->fread($this->getid3
->fread_buffer_size());
533 while ((substr($filedata, $filedataoffset++
, 4) != 'OggS')) {
534 if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3
->fread_buffer_size()) {
535 // should be found before here
538 if ((($filedataoffset +
28) > strlen($filedata)) ||
(strlen($filedata) < 28)) {
539 if ($this->feof() ||
(($filedata .= $this->fread($this->getid3
->fread_buffer_size())) === '')) {
540 // get some more data, unless eof, in which case fail
545 $filedataoffset +
= strlen('OggS') - 1; // page, delimited by 'OggS'
547 $oggheader['stream_structver'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
548 $filedataoffset +
= 1;
549 $oggheader['flags_raw'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
550 $filedataoffset +
= 1;
551 $oggheader['flags']['fresh'] = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
552 $oggheader['flags']['bos'] = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
553 $oggheader['flags']['eos'] = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
555 $oggheader['pcm_abs_position'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
556 $filedataoffset +
= 8;
557 $oggheader['stream_serialno'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
558 $filedataoffset +
= 4;
559 $oggheader['page_seqno'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
560 $filedataoffset +
= 4;
561 $oggheader['page_checksum'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
562 $filedataoffset +
= 4;
563 $oggheader['page_segments'] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
564 $filedataoffset +
= 1;
565 $oggheader['page_length'] = 0;
566 for ($i = 0; $i < $oggheader['page_segments']; $i++
) {
567 $oggheader['segment_table'][$i] = getid3_lib
::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
568 $filedataoffset +
= 1;
569 $oggheader['page_length'] +
= $oggheader['segment_table'][$i];
571 $oggheader['header_end_offset'] = $oggheader['page_start_offset'] +
$filedataoffset;
572 $oggheader['page_end_offset'] = $oggheader['header_end_offset'] +
$oggheader['page_length'];
573 $this->fseek($oggheader['header_end_offset']);
579 * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
583 public function ParseVorbisComments() {
584 $info = &$this->getid3
->info
;
586 $OriginalOffset = $this->ftell();
588 $commentdataoffset = 0;
589 $VorbisCommentPage = 1;
590 $CommentStartOffset = 0;
592 switch ($info['audio']['dataformat']) {
596 $CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset']; // Second Ogg page, after header block
597 $this->fseek($CommentStartOffset);
598 $commentdataoffset = 27 +
$info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
599 $commentdata = $this->fread(self
::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) +
$commentdataoffset);
601 if ($info['audio']['dataformat'] == 'vorbis') {
602 $commentdataoffset +
= (strlen('vorbis') +
1);
604 else if ($info['audio']['dataformat'] == 'opus') {
605 $commentdataoffset +
= strlen('OpusTags');
611 $CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] +
4;
612 $this->fseek($CommentStartOffset);
613 $commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
621 $VendorSize = getid3_lib
::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
622 $commentdataoffset +
= 4;
624 $info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
625 $commentdataoffset +
= $VendorSize;
627 $CommentsCount = getid3_lib
::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
628 $commentdataoffset +
= 4;
629 $info['avdataoffset'] = $CommentStartOffset +
$commentdataoffset;
631 $basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
632 $ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
633 for ($i = 0; $i < $CommentsCount; $i++
) {
636 // https://github.com/owncloud/music/issues/212#issuecomment-43082336
637 $this->warning('Unexpectedly large number ('.$CommentsCount.') of Ogg comments - breaking after reading '.$i.' comments');
641 $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset +
$commentdataoffset;
643 if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] +
4)) {
644 if ($oggpageinfo = $this->ParseOggPageHeader()) {
645 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
647 $VorbisCommentPage++
;
649 // First, save what we haven't read yet
650 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
652 // Then take that data off the end
653 $commentdata = substr($commentdata, 0, $commentdataoffset);
655 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
656 $commentdata .= str_repeat("\x00", 27 +
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
657 $commentdataoffset +
= (27 +
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
659 // Finally, stick the unused data back on the end
660 $commentdata .= $AsYetUnusedData;
662 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
663 $commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
667 $ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib
::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
669 // replace avdataoffset with position just after the last vorbiscomment
670 $info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] +
$ThisFileInfo_ogg_comments_raw[$i]['size'] +
4;
672 $commentdataoffset +
= 4;
673 while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
674 if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) ||
($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
675 $this->warning('Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments');
679 $VorbisCommentPage++
;
681 $oggpageinfo = $this->ParseOggPageHeader();
682 $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
684 // First, save what we haven't read yet
685 $AsYetUnusedData = substr($commentdata, $commentdataoffset);
687 // Then take that data off the end
688 $commentdata = substr($commentdata, 0, $commentdataoffset);
690 // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
691 $commentdata .= str_repeat("\x00", 27 +
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
692 $commentdataoffset +
= (27 +
$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
694 // Finally, stick the unused data back on the end
695 $commentdata .= $AsYetUnusedData;
697 //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
698 if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
699 $this->warning('undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
702 $readlength = self
::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
703 if ($readlength <= 0) {
704 $this->warning('invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
707 $commentdata .= $this->fread($readlength);
709 //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
711 $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
712 $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
713 $commentdataoffset +
= $ThisFileInfo_ogg_comments_raw[$i]['size'];
715 if (!$commentstring) {
718 $this->warning('Blank Ogg comment ['.$i.']');
720 } elseif (strstr($commentstring, '=')) {
722 $commentexploded = explode('=', $commentstring, 2);
723 $ThisFileInfo_ogg_comments_raw[$i]['key'] = strtoupper($commentexploded[0]);
724 $ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ?
$commentexploded[1] : '');
726 if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
728 // http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
729 // 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.
730 // http://flac.sourceforge.net/format.html#metadata_block_picture
731 $flac = new getid3_flac($this->getid3
);
732 $flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
733 $flac->parsePICTURE();
734 $info['ogg']['comments']['picture'][] = $flac->getid3
->info
['flac']['PICTURE'][0];
737 } elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
739 $data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
740 $this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
741 /** @todo use 'coverartmime' where available */
742 $imageinfo = getid3_lib
::GetDataImageSize($data);
743 if ($imageinfo === false ||
!isset($imageinfo['mime'])) {
744 $this->warning('COVERART vorbiscomment tag contains invalid image');
748 $ogg = new self($this->getid3
);
749 $ogg->setStringMode($data);
750 $info['ogg']['comments']['picture'][] = array(
751 'image_mime' => $imageinfo['mime'],
752 'datalength' => strlen($data),
753 'picturetype' => 'cover art',
754 'image_height' => $imageinfo['height'],
755 'image_width' => $imageinfo['width'],
756 'data' => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
762 $info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
768 $this->warning('[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring);
771 unset($ThisFileInfo_ogg_comments_raw[$i]);
773 unset($ThisFileInfo_ogg_comments_raw);
776 // Replay Gain Adjustment
777 // http://privatewww.essex.ac.uk/~djmrob/replaygain/
778 if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
779 foreach ($info['ogg']['comments'] as $index => $commentvalue) {
781 case 'rg_audiophile':
782 case 'replaygain_album_gain':
783 $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
784 unset($info['ogg']['comments'][$index]);
788 case 'replaygain_track_gain':
789 $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
790 unset($info['ogg']['comments'][$index]);
793 case 'replaygain_album_peak':
794 $info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
795 unset($info['ogg']['comments'][$index]);
799 case 'replaygain_track_peak':
800 $info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
801 unset($info['ogg']['comments'][$index]);
804 case 'replaygain_reference_loudness':
805 $info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
806 unset($info['ogg']['comments'][$index]);
816 $this->fseek($OriginalOffset);
824 * @return string|null
826 public static function SpeexBandModeLookup($mode) {
827 static $SpeexBandModeLookup = array();
828 if (empty($SpeexBandModeLookup)) {
829 $SpeexBandModeLookup[0] = 'narrow';
830 $SpeexBandModeLookup[1] = 'wide';
831 $SpeexBandModeLookup[2] = 'ultra-wide';
833 return (isset($SpeexBandModeLookup[$mode]) ?
$SpeexBandModeLookup[$mode] : null);
837 * @param array $OggInfoArray
838 * @param int $SegmentNumber
842 public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
844 for ($i = 0; $i < $SegmentNumber; $i++
) {
846 foreach ($OggInfoArray['segment_table'] as $key => $value) {
847 $segmentlength +
= $value;
853 return $segmentlength;
857 * @param int $nominal_bitrate
861 public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
863 // decrease precision
864 $nominal_bitrate = $nominal_bitrate / 1000;
866 if ($nominal_bitrate < 128) {
868 $qval = ($nominal_bitrate - 64) / 16;
869 } elseif ($nominal_bitrate < 256) {
871 $qval = $nominal_bitrate / 32;
872 } elseif ($nominal_bitrate < 320) {
874 $qval = ($nominal_bitrate +
256) / 64;
877 $qval = ($nominal_bitrate +
1300) / 180;
879 //return $qval; // 5.031324
880 //return intval($qval); // 5
881 return round($qval, 1); // 5 or 4.9
885 * @param int $colorspace_id
887 * @return string|null
889 public static function TheoraColorSpace($colorspace_id) {
890 // http://www.theora.org/doc/Theora.pdf (table 6.3)
891 static $TheoraColorSpaceLookup = array();
892 if (empty($TheoraColorSpaceLookup)) {
893 $TheoraColorSpaceLookup[0] = 'Undefined';
894 $TheoraColorSpaceLookup[1] = 'Rec. 470M';
895 $TheoraColorSpaceLookup[2] = 'Rec. 470BG';
896 $TheoraColorSpaceLookup[3] = 'Reserved';
898 return (isset($TheoraColorSpaceLookup[$colorspace_id]) ?
$TheoraColorSpaceLookup[$colorspace_id] : null);
902 * @param int $pixelformat_id
904 * @return string|null
906 public static function TheoraPixelFormat($pixelformat_id) {
907 // http://www.theora.org/doc/Theora.pdf (table 6.4)
908 static $TheoraPixelFormatLookup = array();
909 if (empty($TheoraPixelFormatLookup)) {
910 $TheoraPixelFormatLookup[0] = '4:2:0';
911 $TheoraPixelFormatLookup[1] = 'Reserved';
912 $TheoraPixelFormatLookup[2] = '4:2:2';
913 $TheoraPixelFormatLookup[3] = '4:4:4';
915 return (isset($TheoraPixelFormatLookup[$pixelformat_id]) ?
$TheoraPixelFormatLookup[$pixelformat_id] : null);