1 <?php |
1 <?php |
|
2 |
2 ///////////////////////////////////////////////////////////////// |
3 ///////////////////////////////////////////////////////////////// |
3 /// getID3() by James Heinrich <info@getid3.org> // |
4 /// getID3() by James Heinrich <info@getid3.org> // |
4 // available at http://getid3.sourceforge.net // |
5 // available at https://github.com/JamesHeinrich/getID3 // |
5 // or http://www.getid3.org // |
6 // or https://www.getid3.org // |
6 // also https://github.com/JamesHeinrich/getID3 // |
7 // or http://getid3.sourceforge.net // |
7 ///////////////////////////////////////////////////////////////// |
8 // see readme.txt for more details // |
8 // See readme.txt for more details // |
|
9 ///////////////////////////////////////////////////////////////// |
9 ///////////////////////////////////////////////////////////////// |
10 // // |
10 // // |
11 // module.audio.ogg.php // |
11 // module.audio.ogg.php // |
12 // module for analyzing Ogg Vorbis, OggFLAC and Speex files // |
12 // module for analyzing Ogg Vorbis, OggFLAC and Speex files // |
13 // dependencies: module.audio.flac.php // |
13 // dependencies: module.audio.flac.php // |
14 // /// |
14 // /// |
15 ///////////////////////////////////////////////////////////////// |
15 ///////////////////////////////////////////////////////////////// |
16 |
16 |
|
17 if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers |
|
18 exit; |
|
19 } |
17 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true); |
20 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true); |
18 |
21 |
19 class getid3_ogg extends getid3_handler |
22 class getid3_ogg extends getid3_handler |
20 { |
23 { |
21 // http://xiph.org/vorbis/doc/Vorbis_I_spec.html |
24 /** |
|
25 * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html |
|
26 * |
|
27 * @return bool |
|
28 */ |
22 public function Analyze() { |
29 public function Analyze() { |
23 $info = &$this->getid3->info; |
30 $info = &$this->getid3->info; |
24 |
31 |
25 $info['fileformat'] = 'ogg'; |
32 $info['fileformat'] = 'ogg'; |
26 |
33 |
63 |
70 |
64 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo); |
71 $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo); |
65 |
72 |
66 } elseif (substr($filedata, 0, 8) == 'OpusHead') { |
73 } elseif (substr($filedata, 0, 8) == 'OpusHead') { |
67 |
74 |
68 if( $this->ParseOpusPageHeader($filedata, $filedataoffset, $oggpageinfo) == false ) { |
75 if ($this->ParseOpusPageHeader($filedata, $filedataoffset, $oggpageinfo) === false) { |
69 return false; |
76 return false; |
70 } |
77 } |
71 |
78 |
72 } elseif (substr($filedata, 0, 8) == 'Speex ') { |
79 } elseif (substr($filedata, 0, 8) == 'Speex ') { |
73 |
80 |
177 $info['video']['frame_rate'] = (float) $info['ogg']['pageheader']['theora']['frame_rate_numerator'] / $info['ogg']['pageheader']['theora']['frame_rate_denominator']; |
184 $info['video']['frame_rate'] = (float) $info['ogg']['pageheader']['theora']['frame_rate_numerator'] / $info['ogg']['pageheader']['theora']['frame_rate_denominator']; |
178 } |
185 } |
179 if ($info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] > 0) { |
186 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']; |
187 $info['video']['pixel_aspect_ratio'] = (float) $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] / $info['ogg']['pageheader']['theora']['pixel_aspect_denominator']; |
181 } |
188 } |
182 $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 ['.$this->getid3->version().'] -- bitrate, playtime and all audio data are currently unavailable'); |
189 $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'); |
183 |
190 |
184 |
191 |
185 } elseif (substr($filedata, 0, 8) == "fishead\x00") { |
192 } elseif (substr($filedata, 0, 8) == "fishead\x00") { |
186 |
193 |
187 // Ogg Skeleton version 3.0 Format Specification |
194 // Ogg Skeleton version 3.0 Format Specification |
257 $this->fseek($oggpageinfo['page_start_offset']); |
264 $this->fseek($oggpageinfo['page_start_offset']); |
258 |
265 |
259 $this->error('Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']'); |
266 $this->error('Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']'); |
260 //return false; |
267 //return false; |
261 |
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 |
262 } else { |
294 } else { |
263 |
295 |
264 $this->error('Expecting either "Speex ", "OpusHead" or "vorbis" identifier strings, found "'.substr($filedata, 0, 8).'"'); |
296 $this->error('Expecting one of "vorbis", "Speex", "OpusHead", "vorbis", "fishhead", "theora", "fLaC" identifier strings, found "'.substr($filedata, 0, 8).'"'); |
265 unset($info['ogg']); |
297 unset($info['ogg']); |
266 unset($info['mime_type']); |
298 unset($info['mime_type']); |
267 return false; |
299 return false; |
268 |
300 |
269 } |
301 } |
376 } |
408 } |
377 |
409 |
378 return true; |
410 return true; |
379 } |
411 } |
380 |
412 |
|
413 /** |
|
414 * @param string $filedata |
|
415 * @param int $filedataoffset |
|
416 * @param array $oggpageinfo |
|
417 * |
|
418 * @return bool |
|
419 */ |
381 public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) { |
420 public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) { |
382 $info = &$this->getid3->info; |
421 $info = &$this->getid3->info; |
383 $info['audio']['dataformat'] = 'vorbis'; |
422 $info['audio']['dataformat'] = 'vorbis'; |
384 $info['audio']['lossless'] = false; |
423 $info['audio']['lossless'] = false; |
385 |
424 |
424 $info['audio']['bitrate_mode'] = 'abr'; |
463 $info['audio']['bitrate_mode'] = 'abr'; |
425 } |
464 } |
426 return true; |
465 return true; |
427 } |
466 } |
428 |
467 |
429 // http://tools.ietf.org/html/draft-ietf-codec-oggopus-03 |
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 */ |
430 public function ParseOpusPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) { |
477 public function ParseOpusPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) { |
431 $info = &$this->getid3->info; |
478 $info = &$this->getid3->info; |
432 $info['audio']['dataformat'] = 'opus'; |
479 $info['audio']['dataformat'] = 'opus'; |
433 $info['mime_type'] = 'audio/ogg; codecs=opus'; |
480 $info['mime_type'] = 'audio/ogg; codecs=opus'; |
434 |
481 |
456 } |
503 } |
457 |
504 |
458 $info['ogg']['pageheader']['opus']['pre_skip'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2)); |
505 $info['ogg']['pageheader']['opus']['pre_skip'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2)); |
459 $filedataoffset += 2; |
506 $filedataoffset += 2; |
460 |
507 |
461 $info['ogg']['pageheader']['opus']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); |
508 $info['ogg']['pageheader']['opus']['input_sample_rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); |
462 $filedataoffset += 4; |
509 $filedataoffset += 4; |
463 |
510 |
464 //$info['ogg']['pageheader']['opus']['output_gain'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2)); |
511 //$info['ogg']['pageheader']['opus']['output_gain'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2)); |
465 //$filedataoffset += 2; |
512 //$filedataoffset += 2; |
466 |
513 |
467 //$info['ogg']['pageheader']['opus']['channel_mapping_family'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); |
514 //$info['ogg']['pageheader']['opus']['channel_mapping_family'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); |
468 //$filedataoffset += 1; |
515 //$filedataoffset += 1; |
469 |
516 |
470 $info['opus']['opus_version'] = $info['ogg']['pageheader']['opus']['version']; |
517 $info['opus']['opus_version'] = $info['ogg']['pageheader']['opus']['version']; |
471 $info['opus']['sample_rate'] = $info['ogg']['pageheader']['opus']['sample_rate']; |
518 $info['opus']['sample_rate_input'] = $info['ogg']['pageheader']['opus']['input_sample_rate']; |
472 $info['opus']['out_channel_count'] = $info['ogg']['pageheader']['opus']['out_channel_count']; |
519 $info['opus']['out_channel_count'] = $info['ogg']['pageheader']['opus']['out_channel_count']; |
473 |
520 |
474 $info['audio']['channels'] = $info['opus']['out_channel_count']; |
521 $info['audio']['channels'] = $info['opus']['out_channel_count']; |
475 $info['audio']['sample_rate'] = $info['opus']['sample_rate']; |
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 |
476 return true; |
524 return true; |
477 } |
525 } |
478 |
526 |
479 |
527 /** |
|
528 * @return array|false |
|
529 */ |
480 public function ParseOggPageHeader() { |
530 public function ParseOggPageHeader() { |
481 // http://xiph.org/ogg/vorbis/doc/framing.html |
531 // http://xiph.org/ogg/vorbis/doc/framing.html |
482 $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file |
532 $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file |
483 |
533 |
484 $filedata = $this->fread($this->getid3->fread_buffer_size()); |
534 $filedata = $this->fread($this->getid3->fread_buffer_size()); |
487 if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) { |
537 if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) { |
488 // should be found before here |
538 // should be found before here |
489 return false; |
539 return false; |
490 } |
540 } |
491 if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) { |
541 if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) { |
492 if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === false)) { |
542 if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === '')) { |
493 // get some more data, unless eof, in which case fail |
543 // get some more data, unless eof, in which case fail |
494 return false; |
544 return false; |
495 } |
545 } |
496 } |
546 } |
497 } |
547 } |
526 $this->fseek($oggheader['header_end_offset']); |
576 $this->fseek($oggheader['header_end_offset']); |
527 |
577 |
528 return $oggheader; |
578 return $oggheader; |
529 } |
579 } |
530 |
580 |
531 // http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005 |
581 /** |
|
582 * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005 |
|
583 * |
|
584 * @return bool |
|
585 */ |
532 public function ParseVorbisComments() { |
586 public function ParseVorbisComments() { |
533 $info = &$this->getid3->info; |
587 $info = &$this->getid3->info; |
534 |
588 |
535 $OriginalOffset = $this->ftell(); |
589 $OriginalOffset = $this->ftell(); |
|
590 $commentdata = null; |
536 $commentdataoffset = 0; |
591 $commentdataoffset = 0; |
537 $VorbisCommentPage = 1; |
592 $VorbisCommentPage = 1; |
|
593 $CommentStartOffset = 0; |
538 |
594 |
539 switch ($info['audio']['dataformat']) { |
595 switch ($info['audio']['dataformat']) { |
540 case 'vorbis': |
596 case 'vorbis': |
541 case 'speex': |
597 case 'speex': |
542 case 'opus': |
598 case 'opus': |
763 $this->fseek($OriginalOffset); |
818 $this->fseek($OriginalOffset); |
764 |
819 |
765 return true; |
820 return true; |
766 } |
821 } |
767 |
822 |
|
823 /** |
|
824 * @param int $mode |
|
825 * |
|
826 * @return string|null |
|
827 */ |
768 public static function SpeexBandModeLookup($mode) { |
828 public static function SpeexBandModeLookup($mode) { |
769 static $SpeexBandModeLookup = array(); |
829 static $SpeexBandModeLookup = array(); |
770 if (empty($SpeexBandModeLookup)) { |
830 if (empty($SpeexBandModeLookup)) { |
771 $SpeexBandModeLookup[0] = 'narrow'; |
831 $SpeexBandModeLookup[0] = 'narrow'; |
772 $SpeexBandModeLookup[1] = 'wide'; |
832 $SpeexBandModeLookup[1] = 'wide'; |
773 $SpeexBandModeLookup[2] = 'ultra-wide'; |
833 $SpeexBandModeLookup[2] = 'ultra-wide'; |
774 } |
834 } |
775 return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null); |
835 return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null); |
776 } |
836 } |
777 |
837 |
778 |
838 /** |
|
839 * @param array $OggInfoArray |
|
840 * @param int $SegmentNumber |
|
841 * |
|
842 * @return int |
|
843 */ |
779 public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) { |
844 public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) { |
|
845 $segmentlength = 0; |
780 for ($i = 0; $i < $SegmentNumber; $i++) { |
846 for ($i = 0; $i < $SegmentNumber; $i++) { |
781 $segmentlength = 0; |
847 $segmentlength = 0; |
782 foreach ($OggInfoArray['segment_table'] as $key => $value) { |
848 foreach ($OggInfoArray['segment_table'] as $key => $value) { |
783 $segmentlength += $value; |
849 $segmentlength += $value; |
784 if ($value < 255) { |
850 if ($value < 255) { |
811 //return $qval; // 5.031324 |
881 //return $qval; // 5.031324 |
812 //return intval($qval); // 5 |
882 //return intval($qval); // 5 |
813 return round($qval, 1); // 5 or 4.9 |
883 return round($qval, 1); // 5 or 4.9 |
814 } |
884 } |
815 |
885 |
|
886 /** |
|
887 * @param int $colorspace_id |
|
888 * |
|
889 * @return string|null |
|
890 */ |
816 public static function TheoraColorSpace($colorspace_id) { |
891 public static function TheoraColorSpace($colorspace_id) { |
817 // http://www.theora.org/doc/Theora.pdf (table 6.3) |
892 // http://www.theora.org/doc/Theora.pdf (table 6.3) |
818 static $TheoraColorSpaceLookup = array(); |
893 static $TheoraColorSpaceLookup = array(); |
819 if (empty($TheoraColorSpaceLookup)) { |
894 if (empty($TheoraColorSpaceLookup)) { |
820 $TheoraColorSpaceLookup[0] = 'Undefined'; |
895 $TheoraColorSpaceLookup[0] = 'Undefined'; |
823 $TheoraColorSpaceLookup[3] = 'Reserved'; |
898 $TheoraColorSpaceLookup[3] = 'Reserved'; |
824 } |
899 } |
825 return (isset($TheoraColorSpaceLookup[$colorspace_id]) ? $TheoraColorSpaceLookup[$colorspace_id] : null); |
900 return (isset($TheoraColorSpaceLookup[$colorspace_id]) ? $TheoraColorSpaceLookup[$colorspace_id] : null); |
826 } |
901 } |
827 |
902 |
|
903 /** |
|
904 * @param int $pixelformat_id |
|
905 * |
|
906 * @return string|null |
|
907 */ |
828 public static function TheoraPixelFormat($pixelformat_id) { |
908 public static function TheoraPixelFormat($pixelformat_id) { |
829 // http://www.theora.org/doc/Theora.pdf (table 6.4) |
909 // http://www.theora.org/doc/Theora.pdf (table 6.4) |
830 static $TheoraPixelFormatLookup = array(); |
910 static $TheoraPixelFormatLookup = array(); |
831 if (empty($TheoraPixelFormatLookup)) { |
911 if (empty($TheoraPixelFormatLookup)) { |
832 $TheoraPixelFormatLookup[0] = '4:2:0'; |
912 $TheoraPixelFormatLookup[0] = '4:2:0'; |