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