module.audio.ogg.php 42 KB

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