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