1<?php
2
3/////////////////////////////////////////////////////////////////
4/// getID3() by James Heinrich <info@getid3.org>               //
5//  available at https://github.com/JamesHeinrich/getID3       //
6//            or https://www.getid3.org                        //
7//            or http://getid3.sourceforge.net                 //
8//  see readme.txt for more details                            //
9/////////////////////////////////////////////////////////////////
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
17if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
18	exit;
19}
20getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true);
21
22class getid3_ogg extends getid3_handler
23{
24	/**
25	 * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html
26	 *
27	 * @return bool
28	 */
29	public function Analyze() {
30		$info = &$this->getid3->info;
31
32		$info['fileformat'] = 'ogg';
33
34		// Warn about illegal tags - only vorbiscomments are allowed
35		if (isset($info['id3v2'])) {
36			$this->warning('Illegal ID3v2 tag present.');
37		}
38		if (isset($info['id3v1'])) {
39			$this->warning('Illegal ID3v1 tag present.');
40		}
41		if (isset($info['ape'])) {
42			$this->warning('Illegal APE tag present.');
43		}
44
45
46		// Page 1 - Stream Header
47
48		$this->fseek($info['avdataoffset']);
49
50		$oggpageinfo = $this->ParseOggPageHeader();
51		$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
52
53		if ($this->ftell() >= $this->getid3->fread_buffer_size()) {
54			$this->error('Could not find start of Ogg page in the first '.$this->getid3->fread_buffer_size().' bytes (this might not be an Ogg-Vorbis file?)');
55			unset($info['fileformat']);
56			unset($info['ogg']);
57			return false;
58		}
59
60		$filedata = $this->fread($oggpageinfo['page_length']);
61		$filedataoffset = 0;
62
63		if (substr($filedata, 0, 4) == 'fLaC') {
64
65			$info['audio']['dataformat']   = 'flac';
66			$info['audio']['bitrate_mode'] = 'vbr';
67			$info['audio']['lossless']     = true;
68
69		} elseif (substr($filedata, 1, 6) == 'vorbis') {
70
71			$this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
72
73		} elseif (substr($filedata, 0, 8) == 'OpusHead') {
74
75			if ($this->ParseOpusPageHeader($filedata, $filedataoffset, $oggpageinfo) === false) {
76				return false;
77			}
78
79		} elseif (substr($filedata, 0, 8) == 'Speex   ') {
80
81			// http://www.speex.org/manual/node10.html
82
83			$info['audio']['dataformat']   = 'speex';
84			$info['mime_type']             = 'audio/speex';
85			$info['audio']['bitrate_mode'] = 'abr';
86			$info['audio']['lossless']     = false;
87
88			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string']           =                              substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex   '
89			$filedataoffset += 8;
90			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']          =                              substr($filedata, $filedataoffset, 20);
91			$filedataoffset += 20;
92			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id']       = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
93			$filedataoffset += 4;
94			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size']            = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
95			$filedataoffset += 4;
96			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate']                   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
97			$filedataoffset += 4;
98			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']                   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
99			$filedataoffset += 4;
100			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
101			$filedataoffset += 4;
102			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels']            = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
103			$filedataoffset += 4;
104			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate']                = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
105			$filedataoffset += 4;
106			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize']              = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
107			$filedataoffset += 4;
108			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr']                    = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
109			$filedataoffset += 4;
110			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet']      = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
111			$filedataoffset += 4;
112			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers']          = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
113			$filedataoffset += 4;
114			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1']              = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
115			$filedataoffset += 4;
116			$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2']              = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
117			$filedataoffset += 4;
118
119			$info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']);
120			$info['speex']['sample_rate']   = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'];
121			$info['speex']['channels']      = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'];
122			$info['speex']['vbr']           = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'];
123			$info['speex']['band_type']     = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']);
124
125			$info['audio']['sample_rate']   = $info['speex']['sample_rate'];
126			$info['audio']['channels']      = $info['speex']['channels'];
127			if ($info['speex']['vbr']) {
128				$info['audio']['bitrate_mode'] = 'vbr';
129			}
130
131		} elseif (substr($filedata, 0, 7) == "\x80".'theora') {
132
133			// http://www.theora.org/doc/Theora.pdf (section 6.2)
134
135			$info['ogg']['pageheader']['theora']['theora_magic']             =                           substr($filedata, $filedataoffset,  7); // hard-coded to "\x80.'theora'
136			$filedataoffset += 7;
137			$info['ogg']['pageheader']['theora']['version_major']            = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
138			$filedataoffset += 1;
139			$info['ogg']['pageheader']['theora']['version_minor']            = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
140			$filedataoffset += 1;
141			$info['ogg']['pageheader']['theora']['version_revision']         = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
142			$filedataoffset += 1;
143			$info['ogg']['pageheader']['theora']['frame_width_macroblocks']  = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  2));
144			$filedataoffset += 2;
145			$info['ogg']['pageheader']['theora']['frame_height_macroblocks'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  2));
146			$filedataoffset += 2;
147			$info['ogg']['pageheader']['theora']['resolution_x']             = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
148			$filedataoffset += 3;
149			$info['ogg']['pageheader']['theora']['resolution_y']             = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
150			$filedataoffset += 3;
151			$info['ogg']['pageheader']['theora']['picture_offset_x']         = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
152			$filedataoffset += 1;
153			$info['ogg']['pageheader']['theora']['picture_offset_y']         = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
154			$filedataoffset += 1;
155			$info['ogg']['pageheader']['theora']['frame_rate_numerator']     = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  4));
156			$filedataoffset += 4;
157			$info['ogg']['pageheader']['theora']['frame_rate_denominator']   = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  4));
158			$filedataoffset += 4;
159			$info['ogg']['pageheader']['theora']['pixel_aspect_numerator']   = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
160			$filedataoffset += 3;
161			$info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
162			$filedataoffset += 3;
163			$info['ogg']['pageheader']['theora']['color_space_id']           = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  1));
164			$filedataoffset += 1;
165			$info['ogg']['pageheader']['theora']['nominal_bitrate']          = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  3));
166			$filedataoffset += 3;
167			$info['ogg']['pageheader']['theora']['flags']                    = getid3_lib::BigEndian2Int(substr($filedata, $filedataoffset,  2));
168			$filedataoffset += 2;
169
170			$info['ogg']['pageheader']['theora']['quality']         = ($info['ogg']['pageheader']['theora']['flags'] & 0xFC00) >> 10;
171			$info['ogg']['pageheader']['theora']['kfg_shift']       = ($info['ogg']['pageheader']['theora']['flags'] & 0x03E0) >>  5;
172			$info['ogg']['pageheader']['theora']['pixel_format_id'] = ($info['ogg']['pageheader']['theora']['flags'] & 0x0018) >>  3;
173			$info['ogg']['pageheader']['theora']['reserved']        = ($info['ogg']['pageheader']['theora']['flags'] & 0x0007) >>  0; // should be 0
174			$info['ogg']['pageheader']['theora']['color_space']     = self::TheoraColorSpace($info['ogg']['pageheader']['theora']['color_space_id']);
175			$info['ogg']['pageheader']['theora']['pixel_format']    = self::TheoraPixelFormat($info['ogg']['pageheader']['theora']['pixel_format_id']);
176
177			$info['video']['dataformat']   = 'theora';
178			$info['mime_type']             = 'video/ogg';
179			//$info['audio']['bitrate_mode'] = 'abr';
180			//$info['audio']['lossless']     = false;
181			$info['video']['resolution_x'] = $info['ogg']['pageheader']['theora']['resolution_x'];
182			$info['video']['resolution_y'] = $info['ogg']['pageheader']['theora']['resolution_y'];
183			if ($info['ogg']['pageheader']['theora']['frame_rate_denominator'] > 0) {
184				$info['video']['frame_rate'] = (float) $info['ogg']['pageheader']['theora']['frame_rate_numerator'] / $info['ogg']['pageheader']['theora']['frame_rate_denominator'];
185			}
186			if ($info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] > 0) {
187				$info['video']['pixel_aspect_ratio'] = (float) $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] / $info['ogg']['pageheader']['theora']['pixel_aspect_denominator'];
188			}
189			$this->warning('Ogg Theora (v3) not fully supported in this version of getID3 ['.$this->getid3->version().'] -- bitrate, playtime and all audio data are currently unavailable');
190
191
192		} elseif (substr($filedata, 0, 8) == "fishead\x00") {
193
194			// Ogg Skeleton version 3.0 Format Specification
195			// http://xiph.org/ogg/doc/skeleton.html
196			$filedataoffset += 8;
197			$info['ogg']['skeleton']['fishead']['raw']['version_major']                = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  2));
198			$filedataoffset += 2;
199			$info['ogg']['skeleton']['fishead']['raw']['version_minor']                = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  2));
200			$filedataoffset += 2;
201			$info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
202			$filedataoffset += 8;
203			$info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
204			$filedataoffset += 8;
205			$info['ogg']['skeleton']['fishead']['raw']['basetime_numerator']           = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
206			$filedataoffset += 8;
207			$info['ogg']['skeleton']['fishead']['raw']['basetime_denominator']         = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
208			$filedataoffset += 8;
209			$info['ogg']['skeleton']['fishead']['raw']['utc']                          = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 20));
210			$filedataoffset += 20;
211
212			$info['ogg']['skeleton']['fishead']['version']          = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor'];
213			$info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'];
214			$info['ogg']['skeleton']['fishead']['basetime']         = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator']         / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'];
215			$info['ogg']['skeleton']['fishead']['utc']              = $info['ogg']['skeleton']['fishead']['raw']['utc'];
216
217
218			$counter = 0;
219			do {
220				$oggpageinfo = $this->ParseOggPageHeader();
221				$info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++] = $oggpageinfo;
222				$filedata = $this->fread($oggpageinfo['page_length']);
223				$this->fseek($oggpageinfo['page_end_offset']);
224
225				if (substr($filedata, 0, 8) == "fisbone\x00") {
226
227					$filedataoffset = 8;
228					$info['ogg']['skeleton']['fisbone']['raw']['message_header_offset']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
229					$filedataoffset += 4;
230					$info['ogg']['skeleton']['fisbone']['raw']['serial_number']           = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
231					$filedataoffset += 4;
232					$info['ogg']['skeleton']['fisbone']['raw']['number_header_packets']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
233					$filedataoffset += 4;
234					$info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
235					$filedataoffset += 8;
236					$info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
237					$filedataoffset += 8;
238					$info['ogg']['skeleton']['fisbone']['raw']['basegranule']             = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  8));
239					$filedataoffset += 8;
240					$info['ogg']['skeleton']['fisbone']['raw']['preroll']                 = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
241					$filedataoffset += 4;
242					$info['ogg']['skeleton']['fisbone']['raw']['granuleshift']            = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  1));
243					$filedataoffset += 1;
244					$info['ogg']['skeleton']['fisbone']['raw']['padding']                 =                              substr($filedata, $filedataoffset,  3);
245					$filedataoffset += 3;
246
247				} elseif (substr($filedata, 1, 6) == 'theora') {
248
249					$info['video']['dataformat'] = 'theora1';
250					$this->error('Ogg Theora (v1) not correctly handled in this version of getID3 ['.$this->getid3->version().']');
251					//break;
252
253				} elseif (substr($filedata, 1, 6) == 'vorbis') {
254
255					$this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo);
256
257				} else {
258					$this->error('unexpected');
259					//break;
260				}
261			//} while ($oggpageinfo['page_seqno'] == 0);
262			} while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00"));
263
264			$this->fseek($oggpageinfo['page_start_offset']);
265
266			$this->error('Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']');
267			//return false;
268
269		} elseif (substr($filedata, 0, 5) == "\x7F".'FLAC') {
270			// https://xiph.org/flac/ogg_mapping.html
271
272			$info['audio']['dataformat']   = 'flac';
273			$info['audio']['bitrate_mode'] = 'vbr';
274			$info['audio']['lossless']     = true;
275
276			$info['ogg']['flac']['header']['version_major']  =                         ord(substr($filedata,  5, 1));
277			$info['ogg']['flac']['header']['version_minor']  =                         ord(substr($filedata,  6, 1));
278			$info['ogg']['flac']['header']['header_packets'] =   getid3_lib::BigEndian2Int(substr($filedata,  7, 2)) + 1; // "A two-byte, big-endian binary number signifying the number of header (non-audio) packets, not including this one. This number may be zero (0x0000) to signify 'unknown' but be aware that some decoders may not be able to handle such streams."
279			$info['ogg']['flac']['header']['magic']          =                             substr($filedata,  9, 4);
280			if ($info['ogg']['flac']['header']['magic'] != 'fLaC') {
281				$this->error('Ogg-FLAC expecting "fLaC", found "'.$info['ogg']['flac']['header']['magic'].'" ('.trim(getid3_lib::PrintHexBytes($info['ogg']['flac']['header']['magic'])).')');
282				return false;
283			}
284			$info['ogg']['flac']['header']['STREAMINFO_bytes'] = getid3_lib::BigEndian2Int(substr($filedata, 13, 4));
285			$info['flac']['STREAMINFO'] = getid3_flac::parseSTREAMINFOdata(substr($filedata, 17, 34));
286			if (!empty($info['flac']['STREAMINFO']['sample_rate'])) {
287				$info['audio']['bitrate_mode']    = 'vbr';
288				$info['audio']['sample_rate']     = $info['flac']['STREAMINFO']['sample_rate'];
289				$info['audio']['channels']        = $info['flac']['STREAMINFO']['channels'];
290				$info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample'];
291				$info['playtime_seconds']         = $info['flac']['STREAMINFO']['samples_stream'] / $info['flac']['STREAMINFO']['sample_rate'];
292			}
293
294		} else {
295
296			$this->error('Expecting one of "vorbis", "Speex", "OpusHead", "vorbis", "fishhead", "theora", "fLaC" identifier strings, found "'.substr($filedata, 0, 8).'"');
297			unset($info['ogg']);
298			unset($info['mime_type']);
299			return false;
300
301		}
302
303		// Page 2 - Comment Header
304		$oggpageinfo = $this->ParseOggPageHeader();
305		$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
306
307		switch ($info['audio']['dataformat']) {
308			case 'vorbis':
309				$filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
310				$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, 0, 1));
311				$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] =                              substr($filedata, 1, 6); // hard-coded to 'vorbis'
312
313				$this->ParseVorbisComments();
314				break;
315
316			case 'flac':
317				$flac = new getid3_flac($this->getid3);
318				if (!$flac->parseMETAdata()) {
319					$this->error('Failed to parse FLAC headers');
320					return false;
321				}
322				unset($flac);
323				break;
324
325			case 'speex':
326				$this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR);
327				$this->ParseVorbisComments();
328				break;
329
330			case 'opus':
331				$filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
332				$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 0, 8); // hard-coded to 'OpusTags'
333				if(substr($filedata, 0, 8)  != 'OpusTags') {
334					$this->error('Expected "OpusTags" as header but got "'.substr($filedata, 0, 8).'"');
335					return false;
336				}
337
338				$this->ParseVorbisComments();
339				break;
340
341		}
342
343		// Last Page - Number of Samples
344		if (!getid3_lib::intValueSupported($info['avdataend'])) {
345
346			$this->warning('Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)');
347
348		} else {
349
350			$this->fseek(max($info['avdataend'] - $this->getid3->fread_buffer_size(), 0));
351			$LastChunkOfOgg = strrev($this->fread($this->getid3->fread_buffer_size()));
352			if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) {
353				$this->fseek($info['avdataend'] - ($LastOggSpostion + strlen('SggO')));
354				$info['avdataend'] = $this->ftell();
355				$info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader();
356				$info['ogg']['samples']   = $info['ogg']['pageheader']['eos']['pcm_abs_position'];
357				if ($info['ogg']['samples'] == 0) {
358					$this->error('Corrupt Ogg file: eos.number of samples == zero');
359					return false;
360				}
361				if (!empty($info['audio']['sample_rate'])) {
362					$info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']);
363				}
364			}
365
366		}
367
368		if (!empty($info['ogg']['bitrate_average'])) {
369			$info['audio']['bitrate'] = $info['ogg']['bitrate_average'];
370		} elseif (!empty($info['ogg']['bitrate_nominal'])) {
371			$info['audio']['bitrate'] = $info['ogg']['bitrate_nominal'];
372		} elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) {
373			$info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] + $info['ogg']['bitrate_max']) / 2;
374		}
375		if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) {
376			if ($info['audio']['bitrate'] == 0) {
377				$this->error('Corrupt Ogg file: bitrate_audio == zero');
378				return false;
379			}
380			$info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']);
381		}
382
383		if (isset($info['ogg']['vendor'])) {
384			$info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']);
385
386			// Vorbis only
387			if ($info['audio']['dataformat'] == 'vorbis') {
388
389				// Vorbis 1.0 starts with Xiph.Org
390				if  (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) {
391
392					if ($info['audio']['bitrate_mode'] == 'abr') {
393
394						// Set -b 128 on abr files
395						$info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000);
396
397					} elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) {
398						// Set -q N on vbr files
399						$info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']);
400
401					}
402				}
403
404				if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) {
405					$info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps';
406				}
407			}
408		}
409
410		return true;
411	}
412
413	/**
414	 * @param string $filedata
415	 * @param int    $filedataoffset
416	 * @param array  $oggpageinfo
417	 *
418	 * @return bool
419	 */
420	public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
421		$info = &$this->getid3->info;
422		$info['audio']['dataformat'] = 'vorbis';
423		$info['audio']['lossless']   = false;
424
425		$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
426		$filedataoffset += 1;
427		$info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis'
428		$filedataoffset += 6;
429		$info['ogg']['bitstreamversion'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
430		$filedataoffset += 4;
431		$info['ogg']['numberofchannels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
432		$filedataoffset += 1;
433		$info['audio']['channels']       = $info['ogg']['numberofchannels'];
434		$info['ogg']['samplerate']       = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
435		$filedataoffset += 4;
436		if ($info['ogg']['samplerate'] == 0) {
437			$this->error('Corrupt Ogg file: sample rate == zero');
438			return false;
439		}
440		$info['audio']['sample_rate']    = $info['ogg']['samplerate'];
441		$info['ogg']['samples']          = 0; // filled in later
442		$info['ogg']['bitrate_average']  = 0; // filled in later
443		$info['ogg']['bitrate_max']      = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
444		$filedataoffset += 4;
445		$info['ogg']['bitrate_nominal']  = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
446		$filedataoffset += 4;
447		$info['ogg']['bitrate_min']      = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
448		$filedataoffset += 4;
449		$info['ogg']['blocksize_small']  = pow(2,  getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F);
450		$info['ogg']['blocksize_large']  = pow(2, (getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4);
451		$info['ogg']['stop_bit']         = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet
452
453		$info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr
454		if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) {
455			unset($info['ogg']['bitrate_max']);
456			$info['audio']['bitrate_mode'] = 'abr';
457		}
458		if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) {
459			unset($info['ogg']['bitrate_nominal']);
460		}
461		if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) {
462			unset($info['ogg']['bitrate_min']);
463			$info['audio']['bitrate_mode'] = 'abr';
464		}
465		return true;
466	}
467
468	/**
469	 * @link http://tools.ietf.org/html/draft-ietf-codec-oggopus-03
470	 *
471	 * @param string $filedata
472	 * @param int    $filedataoffset
473	 * @param array  $oggpageinfo
474	 *
475	 * @return bool
476	 */
477	public function ParseOpusPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) {
478		$info = &$this->getid3->info;
479		$info['audio']['dataformat']   = 'opus';
480		$info['mime_type']             = 'audio/ogg; codecs=opus';
481
482		/** @todo find a usable way to detect abr (vbr that is padded to be abr) */
483		$info['audio']['bitrate_mode'] = 'vbr';
484
485		$info['audio']['lossless']     = false;
486
487		$info['ogg']['pageheader']['opus']['opus_magic'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'OpusHead'
488		$filedataoffset += 8;
489		$info['ogg']['pageheader']['opus']['version']    = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  1));
490		$filedataoffset += 1;
491
492		if ($info['ogg']['pageheader']['opus']['version'] < 1 || $info['ogg']['pageheader']['opus']['version'] > 15) {
493			$this->error('Unknown opus version number (only accepting 1-15)');
494			return false;
495		}
496
497		$info['ogg']['pageheader']['opus']['out_channel_count'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  1));
498		$filedataoffset += 1;
499
500		if ($info['ogg']['pageheader']['opus']['out_channel_count'] == 0) {
501			$this->error('Invalid channel count in opus header (must not be zero)');
502			return false;
503		}
504
505		$info['ogg']['pageheader']['opus']['pre_skip'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  2));
506		$filedataoffset += 2;
507
508		$info['ogg']['pageheader']['opus']['input_sample_rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  4));
509		$filedataoffset += 4;
510
511		//$info['ogg']['pageheader']['opus']['output_gain'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  2));
512		//$filedataoffset += 2;
513
514		//$info['ogg']['pageheader']['opus']['channel_mapping_family'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset,  1));
515		//$filedataoffset += 1;
516
517		$info['opus']['opus_version']       = $info['ogg']['pageheader']['opus']['version'];
518		$info['opus']['sample_rate_input']  = $info['ogg']['pageheader']['opus']['input_sample_rate'];
519		$info['opus']['out_channel_count']  = $info['ogg']['pageheader']['opus']['out_channel_count'];
520
521		$info['audio']['channels']          = $info['opus']['out_channel_count'];
522		$info['audio']['sample_rate_input'] = $info['opus']['sample_rate_input'];
523		$info['audio']['sample_rate']       = 48000; // "All Opus audio is coded at 48 kHz, and should also be decoded at 48 kHz for playback (unless the target hardware does not support this sampling rate). However, this field may be used to resample the audio back to the original sampling rate, for example, when saving the output to a file." -- https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/structOpusHead.html
524		return true;
525	}
526
527	/**
528	 * @return array|false
529	 */
530	public function ParseOggPageHeader() {
531		// http://xiph.org/ogg/vorbis/doc/framing.html
532		$oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file
533
534		$filedata = $this->fread($this->getid3->fread_buffer_size());
535		$filedataoffset = 0;
536		while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) {
537			if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) {
538				// should be found before here
539				return false;
540			}
541			if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) {
542				if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === '')) {
543					// get some more data, unless eof, in which case fail
544					return false;
545				}
546			}
547		}
548		$filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS'
549
550		$oggheader['stream_structver']  = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
551		$filedataoffset += 1;
552		$oggheader['flags_raw']         = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
553		$filedataoffset += 1;
554		$oggheader['flags']['fresh']    = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
555		$oggheader['flags']['bos']      = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
556		$oggheader['flags']['eos']      = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
557
558		$oggheader['pcm_abs_position']  = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
559		$filedataoffset += 8;
560		$oggheader['stream_serialno']   = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
561		$filedataoffset += 4;
562		$oggheader['page_seqno']        = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
563		$filedataoffset += 4;
564		$oggheader['page_checksum']     = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
565		$filedataoffset += 4;
566		$oggheader['page_segments']     = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
567		$filedataoffset += 1;
568		$oggheader['page_length'] = 0;
569		for ($i = 0; $i < $oggheader['page_segments']; $i++) {
570			$oggheader['segment_table'][$i] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
571			$filedataoffset += 1;
572			$oggheader['page_length'] += $oggheader['segment_table'][$i];
573		}
574		$oggheader['header_end_offset'] = $oggheader['page_start_offset'] + $filedataoffset;
575		$oggheader['page_end_offset']   = $oggheader['header_end_offset'] + $oggheader['page_length'];
576		$this->fseek($oggheader['header_end_offset']);
577
578		return $oggheader;
579	}
580
581	/**
582	 * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
583	 *
584	 * @return bool
585	 */
586	public function ParseVorbisComments() {
587		$info = &$this->getid3->info;
588
589		$OriginalOffset = $this->ftell();
590		$commentdata = null;
591		$commentdataoffset = 0;
592		$VorbisCommentPage = 1;
593		$CommentStartOffset = 0;
594
595		switch ($info['audio']['dataformat']) {
596			case 'vorbis':
597			case 'speex':
598			case 'opus':
599				$CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset'];  // Second Ogg page, after header block
600				$this->fseek($CommentStartOffset);
601				$commentdataoffset = 27 + $info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
602				$commentdata = $this->fread(self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) + $commentdataoffset);
603
604				if ($info['audio']['dataformat'] == 'vorbis') {
605					$commentdataoffset += (strlen('vorbis') + 1);
606				}
607				else if ($info['audio']['dataformat'] == 'opus') {
608					$commentdataoffset += strlen('OpusTags');
609				}
610
611				break;
612
613			case 'flac':
614				$CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4;
615				$this->fseek($CommentStartOffset);
616				$commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
617				break;
618
619			default:
620				return false;
621		}
622
623		$VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
624		$commentdataoffset += 4;
625
626		$info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
627		$commentdataoffset += $VendorSize;
628
629		$CommentsCount = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
630		$commentdataoffset += 4;
631		$info['avdataoffset'] = $CommentStartOffset + $commentdataoffset;
632
633		$basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
634		$ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
635		for ($i = 0; $i < $CommentsCount; $i++) {
636
637			if ($i >= 10000) {
638				// https://github.com/owncloud/music/issues/212#issuecomment-43082336
639				$this->warning('Unexpectedly large number ('.$CommentsCount.') of Ogg comments - breaking after reading '.$i.' comments');
640				break;
641			}
642
643			$ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset;
644
645			if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) {
646				if ($oggpageinfo = $this->ParseOggPageHeader()) {
647					$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
648
649					$VorbisCommentPage++;
650
651					// First, save what we haven't read yet
652					$AsYetUnusedData = substr($commentdata, $commentdataoffset);
653
654					// Then take that data off the end
655					$commentdata     = substr($commentdata, 0, $commentdataoffset);
656
657					// Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
658					$commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
659					$commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
660
661					// Finally, stick the unused data back on the end
662					$commentdata .= $AsYetUnusedData;
663
664					//$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
665					$commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
666				}
667
668			}
669			$ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
670
671			// replace avdataoffset with position just after the last vorbiscomment
672			$info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4;
673
674			$commentdataoffset += 4;
675			while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
676				if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
677					$this->warning('Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments');
678					break 2;
679				}
680
681				$VorbisCommentPage++;
682
683				$oggpageinfo = $this->ParseOggPageHeader();
684				$info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
685
686				// First, save what we haven't read yet
687				$AsYetUnusedData = substr($commentdata, $commentdataoffset);
688
689				// Then take that data off the end
690				$commentdata     = substr($commentdata, 0, $commentdataoffset);
691
692				// Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
693				$commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
694				$commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
695
696				// Finally, stick the unused data back on the end
697				$commentdata .= $AsYetUnusedData;
698
699				//$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
700				if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
701					$this->warning('undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
702					break;
703				}
704				$readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
705				if ($readlength <= 0) {
706					$this->warning('invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
707					break;
708				}
709				$commentdata .= $this->fread($readlength);
710
711				//$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
712			}
713			$ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
714			$commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
715			$commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size'];
716
717			if (!$commentstring) {
718
719				// no comment?
720				$this->warning('Blank Ogg comment ['.$i.']');
721
722			} elseif (strstr($commentstring, '=')) {
723
724				$commentexploded = explode('=', $commentstring, 2);
725				$ThisFileInfo_ogg_comments_raw[$i]['key']   = strtoupper($commentexploded[0]);
726				$ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : '');
727
728				if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
729
730					// http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
731					// The unencoded format is that of the FLAC picture block. The fields are stored in big endian order as in FLAC, picture data is stored according to the relevant standard.
732					// http://flac.sourceforge.net/format.html#metadata_block_picture
733					$flac = new getid3_flac($this->getid3);
734					$flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
735					$flac->parsePICTURE();
736					$info['ogg']['comments']['picture'][] = $flac->getid3->info['flac']['PICTURE'][0];
737					unset($flac);
738
739				} elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
740
741					$data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
742					$this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
743					/** @todo use 'coverartmime' where available */
744					$imageinfo = getid3_lib::GetDataImageSize($data);
745					if ($imageinfo === false || !isset($imageinfo['mime'])) {
746						$this->warning('COVERART vorbiscomment tag contains invalid image');
747						continue;
748					}
749
750					$ogg = new self($this->getid3);
751					$ogg->setStringMode($data);
752					$info['ogg']['comments']['picture'][] = array(
753						'image_mime'   => $imageinfo['mime'],
754						'datalength'   => strlen($data),
755						'picturetype'  => 'cover art',
756						'image_height' => $imageinfo['height'],
757						'image_width'  => $imageinfo['width'],
758						'data'         => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
759					);
760					unset($ogg);
761
762				} else {
763
764					$info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
765
766				}
767
768			} else {
769
770				$this->warning('[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring);
771
772			}
773			unset($ThisFileInfo_ogg_comments_raw[$i]);
774		}
775		unset($ThisFileInfo_ogg_comments_raw);
776
777
778		// Replay Gain Adjustment
779		// http://privatewww.essex.ac.uk/~djmrob/replaygain/
780		if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
781			foreach ($info['ogg']['comments'] as $index => $commentvalue) {
782				switch ($index) {
783					case 'rg_audiophile':
784					case 'replaygain_album_gain':
785						$info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
786						unset($info['ogg']['comments'][$index]);
787						break;
788
789					case 'rg_radio':
790					case 'replaygain_track_gain':
791						$info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
792						unset($info['ogg']['comments'][$index]);
793						break;
794
795					case 'replaygain_album_peak':
796						$info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
797						unset($info['ogg']['comments'][$index]);
798						break;
799
800					case 'rg_peak':
801					case 'replaygain_track_peak':
802						$info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
803						unset($info['ogg']['comments'][$index]);
804						break;
805
806					case 'replaygain_reference_loudness':
807						$info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
808						unset($info['ogg']['comments'][$index]);
809						break;
810
811					default:
812						// do nothing
813						break;
814				}
815			}
816		}
817
818		$this->fseek($OriginalOffset);
819
820		return true;
821	}
822
823	/**
824	 * @param int $mode
825	 *
826	 * @return string|null
827	 */
828	public static function SpeexBandModeLookup($mode) {
829		static $SpeexBandModeLookup = array();
830		if (empty($SpeexBandModeLookup)) {
831			$SpeexBandModeLookup[0] = 'narrow';
832			$SpeexBandModeLookup[1] = 'wide';
833			$SpeexBandModeLookup[2] = 'ultra-wide';
834		}
835		return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null);
836	}
837
838	/**
839	 * @param array $OggInfoArray
840	 * @param int   $SegmentNumber
841	 *
842	 * @return int
843	 */
844	public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
845		$segmentlength = 0;
846		for ($i = 0; $i < $SegmentNumber; $i++) {
847			$segmentlength = 0;
848			foreach ($OggInfoArray['segment_table'] as $key => $value) {
849				$segmentlength += $value;
850				if ($value < 255) {
851					break;
852				}
853			}
854		}
855		return $segmentlength;
856	}
857
858	/**
859	 * @param int $nominal_bitrate
860	 *
861	 * @return float
862	 */
863	public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
864
865		// decrease precision
866		$nominal_bitrate = $nominal_bitrate / 1000;
867
868		if ($nominal_bitrate < 128) {
869			// q-1 to q4
870			$qval = ($nominal_bitrate - 64) / 16;
871		} elseif ($nominal_bitrate < 256) {
872			// q4 to q8
873			$qval = $nominal_bitrate / 32;
874		} elseif ($nominal_bitrate < 320) {
875			// q8 to q9
876			$qval = ($nominal_bitrate + 256) / 64;
877		} else {
878			// q9 to q10
879			$qval = ($nominal_bitrate + 1300) / 180;
880		}
881		//return $qval; // 5.031324
882		//return intval($qval); // 5
883		return round($qval, 1); // 5 or 4.9
884	}
885
886	/**
887	 * @param int $colorspace_id
888	 *
889	 * @return string|null
890	 */
891	public static function TheoraColorSpace($colorspace_id) {
892		// http://www.theora.org/doc/Theora.pdf (table 6.3)
893		static $TheoraColorSpaceLookup = array();
894		if (empty($TheoraColorSpaceLookup)) {
895			$TheoraColorSpaceLookup[0] = 'Undefined';
896			$TheoraColorSpaceLookup[1] = 'Rec. 470M';
897			$TheoraColorSpaceLookup[2] = 'Rec. 470BG';
898			$TheoraColorSpaceLookup[3] = 'Reserved';
899		}
900		return (isset($TheoraColorSpaceLookup[$colorspace_id]) ? $TheoraColorSpaceLookup[$colorspace_id] : null);
901	}
902
903	/**
904	 * @param int $pixelformat_id
905	 *
906	 * @return string|null
907	 */
908	public static function TheoraPixelFormat($pixelformat_id) {
909		// http://www.theora.org/doc/Theora.pdf (table 6.4)
910		static $TheoraPixelFormatLookup = array();
911		if (empty($TheoraPixelFormatLookup)) {
912			$TheoraPixelFormatLookup[0] = '4:2:0';
913			$TheoraPixelFormatLookup[1] = 'Reserved';
914			$TheoraPixelFormatLookup[2] = '4:2:2';
915			$TheoraPixelFormatLookup[3] = '4:4:4';
916		}
917		return (isset($TheoraPixelFormatLookup[$pixelformat_id]) ? $TheoraPixelFormatLookup[$pixelformat_id] : null);
918	}
919
920}
921