module.audio.ogg.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920
  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['page_start_offset'] = $this->ftell(); // where we started from in the file
  444. $filedata = $this->fread($this->getid3->fread_buffer_size());
  445. $filedataoffset = 0;
  446. while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) {
  447. if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) {
  448. // should be found before here
  449. return false;
  450. }
  451. if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) {
  452. if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === '')) {
  453. // get some more data, unless eof, in which case fail
  454. return false;
  455. }
  456. }
  457. }
  458. $filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS'
  459. $oggheader['stream_structver'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
  460. $filedataoffset += 1;
  461. $oggheader['flags_raw'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
  462. $filedataoffset += 1;
  463. $oggheader['flags']['fresh'] = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet
  464. $oggheader['flags']['bos'] = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos)
  465. $oggheader['flags']['eos'] = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos)
  466. $oggheader['pcm_abs_position'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8));
  467. $filedataoffset += 8;
  468. $oggheader['stream_serialno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
  469. $filedataoffset += 4;
  470. $oggheader['page_seqno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
  471. $filedataoffset += 4;
  472. $oggheader['page_checksum'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4));
  473. $filedataoffset += 4;
  474. $oggheader['page_segments'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
  475. $filedataoffset += 1;
  476. $oggheader['page_length'] = 0;
  477. for ($i = 0; $i < $oggheader['page_segments']; $i++) {
  478. $oggheader['segment_table'][$i] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1));
  479. $filedataoffset += 1;
  480. $oggheader['page_length'] += $oggheader['segment_table'][$i];
  481. }
  482. $oggheader['header_end_offset'] = $oggheader['page_start_offset'] + $filedataoffset;
  483. $oggheader['page_end_offset'] = $oggheader['header_end_offset'] + $oggheader['page_length'];
  484. $this->fseek($oggheader['header_end_offset']);
  485. return $oggheader;
  486. }
  487. /**
  488. * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
  489. *
  490. * @return bool
  491. */
  492. public function ParseVorbisComments() {
  493. $info = &$this->getid3->info;
  494. $OriginalOffset = $this->ftell();
  495. $commentdata = null;
  496. $commentdataoffset = 0;
  497. $VorbisCommentPage = 1;
  498. $CommentStartOffset = 0;
  499. switch ($info['audio']['dataformat']) {
  500. case 'vorbis':
  501. case 'speex':
  502. case 'opus':
  503. $CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset']; // Second Ogg page, after header block
  504. $this->fseek($CommentStartOffset);
  505. $commentdataoffset = 27 + $info['ogg']['pageheader'][$VorbisCommentPage]['page_segments'];
  506. $commentdata = $this->fread(self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) + $commentdataoffset);
  507. if ($info['audio']['dataformat'] == 'vorbis') {
  508. $commentdataoffset += (strlen('vorbis') + 1);
  509. }
  510. else if ($info['audio']['dataformat'] == 'opus') {
  511. $commentdataoffset += strlen('OpusTags');
  512. }
  513. break;
  514. case 'flac':
  515. $CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4;
  516. $this->fseek($CommentStartOffset);
  517. $commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']);
  518. break;
  519. default:
  520. return false;
  521. }
  522. $VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
  523. $commentdataoffset += 4;
  524. $info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize);
  525. $commentdataoffset += $VendorSize;
  526. $CommentsCount = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
  527. $commentdataoffset += 4;
  528. $info['avdataoffset'] = $CommentStartOffset + $commentdataoffset;
  529. $basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT');
  530. $ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw'];
  531. for ($i = 0; $i < $CommentsCount; $i++) {
  532. if ($i >= 10000) {
  533. // https://github.com/owncloud/music/issues/212#issuecomment-43082336
  534. $this->warning('Unexpectedly large number ('.$CommentsCount.') of Ogg comments - breaking after reading '.$i.' comments');
  535. break;
  536. }
  537. $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset;
  538. if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) {
  539. if ($oggpageinfo = $this->ParseOggPageHeader()) {
  540. $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
  541. $VorbisCommentPage++;
  542. // First, save what we haven't read yet
  543. $AsYetUnusedData = substr($commentdata, $commentdataoffset);
  544. // Then take that data off the end
  545. $commentdata = substr($commentdata, 0, $commentdataoffset);
  546. // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
  547. $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
  548. $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
  549. // Finally, stick the unused data back on the end
  550. $commentdata .= $AsYetUnusedData;
  551. //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
  552. $commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1));
  553. }
  554. }
  555. $ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4));
  556. // replace avdataoffset with position just after the last vorbiscomment
  557. $info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4;
  558. $commentdataoffset += 4;
  559. while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) {
  560. if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) {
  561. $this->warning('Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments');
  562. break 2;
  563. }
  564. $VorbisCommentPage++;
  565. $oggpageinfo = $this->ParseOggPageHeader();
  566. $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo;
  567. // First, save what we haven't read yet
  568. $AsYetUnusedData = substr($commentdata, $commentdataoffset);
  569. // Then take that data off the end
  570. $commentdata = substr($commentdata, 0, $commentdataoffset);
  571. // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct
  572. $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
  573. $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']);
  574. // Finally, stick the unused data back on the end
  575. $commentdata .= $AsYetUnusedData;
  576. //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']);
  577. if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) {
  578. $this->warning('undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
  579. break;
  580. }
  581. $readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1);
  582. if ($readlength <= 0) {
  583. $this->warning('invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell());
  584. break;
  585. }
  586. $commentdata .= $this->fread($readlength);
  587. //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset'];
  588. }
  589. $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset;
  590. $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']);
  591. $commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size'];
  592. if (!$commentstring) {
  593. // no comment?
  594. $this->warning('Blank Ogg comment ['.$i.']');
  595. } elseif (strstr($commentstring, '=')) {
  596. $commentexploded = explode('=', $commentstring, 2);
  597. $ThisFileInfo_ogg_comments_raw[$i]['key'] = strtoupper($commentexploded[0]);
  598. $ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : '');
  599. if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') {
  600. // http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
  601. // 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.
  602. // http://flac.sourceforge.net/format.html#metadata_block_picture
  603. $flac = new getid3_flac($this->getid3);
  604. $flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']));
  605. $flac->parsePICTURE();
  606. $info['ogg']['comments']['picture'][] = $flac->getid3->info['flac']['PICTURE'][0];
  607. unset($flac);
  608. } elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') {
  609. $data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']);
  610. $this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure');
  611. /** @todo use 'coverartmime' where available */
  612. $imageinfo = getid3_lib::GetDataImageSize($data);
  613. if ($imageinfo === false || !isset($imageinfo['mime'])) {
  614. $this->warning('COVERART vorbiscomment tag contains invalid image');
  615. continue;
  616. }
  617. $ogg = new self($this->getid3);
  618. $ogg->setStringMode($data);
  619. $info['ogg']['comments']['picture'][] = array(
  620. 'image_mime' => $imageinfo['mime'],
  621. 'datalength' => strlen($data),
  622. 'picturetype' => 'cover art',
  623. 'image_height' => $imageinfo['height'],
  624. 'image_width' => $imageinfo['width'],
  625. 'data' => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']),
  626. );
  627. unset($ogg);
  628. } else {
  629. $info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value'];
  630. }
  631. } else {
  632. $this->warning('[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring);
  633. }
  634. unset($ThisFileInfo_ogg_comments_raw[$i]);
  635. }
  636. unset($ThisFileInfo_ogg_comments_raw);
  637. // Replay Gain Adjustment
  638. // http://privatewww.essex.ac.uk/~djmrob/replaygain/
  639. if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) {
  640. foreach ($info['ogg']['comments'] as $index => $commentvalue) {
  641. switch ($index) {
  642. case 'rg_audiophile':
  643. case 'replaygain_album_gain':
  644. $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0];
  645. unset($info['ogg']['comments'][$index]);
  646. break;
  647. case 'rg_radio':
  648. case 'replaygain_track_gain':
  649. $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0];
  650. unset($info['ogg']['comments'][$index]);
  651. break;
  652. case 'replaygain_album_peak':
  653. $info['replay_gain']['album']['peak'] = (double) $commentvalue[0];
  654. unset($info['ogg']['comments'][$index]);
  655. break;
  656. case 'rg_peak':
  657. case 'replaygain_track_peak':
  658. $info['replay_gain']['track']['peak'] = (double) $commentvalue[0];
  659. unset($info['ogg']['comments'][$index]);
  660. break;
  661. case 'replaygain_reference_loudness':
  662. $info['replay_gain']['reference_volume'] = (double) $commentvalue[0];
  663. unset($info['ogg']['comments'][$index]);
  664. break;
  665. default:
  666. // do nothing
  667. break;
  668. }
  669. }
  670. }
  671. $this->fseek($OriginalOffset);
  672. return true;
  673. }
  674. /**
  675. * @param int $mode
  676. *
  677. * @return string|null
  678. */
  679. public static function SpeexBandModeLookup($mode) {
  680. static $SpeexBandModeLookup = array();
  681. if (empty($SpeexBandModeLookup)) {
  682. $SpeexBandModeLookup[0] = 'narrow';
  683. $SpeexBandModeLookup[1] = 'wide';
  684. $SpeexBandModeLookup[2] = 'ultra-wide';
  685. }
  686. return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null);
  687. }
  688. /**
  689. * @param array $OggInfoArray
  690. * @param int $SegmentNumber
  691. *
  692. * @return int
  693. */
  694. public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) {
  695. $segmentlength = 0;
  696. for ($i = 0; $i < $SegmentNumber; $i++) {
  697. $segmentlength = 0;
  698. foreach ($OggInfoArray['segment_table'] as $key => $value) {
  699. $segmentlength += $value;
  700. if ($value < 255) {
  701. break;
  702. }
  703. }
  704. }
  705. return $segmentlength;
  706. }
  707. /**
  708. * @param int $nominal_bitrate
  709. *
  710. * @return float
  711. */
  712. public static function get_quality_from_nominal_bitrate($nominal_bitrate) {
  713. // decrease precision
  714. $nominal_bitrate = $nominal_bitrate / 1000;
  715. if ($nominal_bitrate < 128) {
  716. // q-1 to q4
  717. $qval = ($nominal_bitrate - 64) / 16;
  718. } elseif ($nominal_bitrate < 256) {
  719. // q4 to q8
  720. $qval = $nominal_bitrate / 32;
  721. } elseif ($nominal_bitrate < 320) {
  722. // q8 to q9
  723. $qval = ($nominal_bitrate + 256) / 64;
  724. } else {
  725. // q9 to q10
  726. $qval = ($nominal_bitrate + 1300) / 180;
  727. }
  728. //return $qval; // 5.031324
  729. //return intval($qval); // 5
  730. return round($qval, 1); // 5 or 4.9
  731. }
  732. /**
  733. * @param int $colorspace_id
  734. *
  735. * @return string|null
  736. */
  737. public static function TheoraColorSpace($colorspace_id) {
  738. // http://www.theora.org/doc/Theora.pdf (table 6.3)
  739. static $TheoraColorSpaceLookup = array();
  740. if (empty($TheoraColorSpaceLookup)) {
  741. $TheoraColorSpaceLookup[0] = 'Undefined';
  742. $TheoraColorSpaceLookup[1] = 'Rec. 470M';
  743. $TheoraColorSpaceLookup[2] = 'Rec. 470BG';
  744. $TheoraColorSpaceLookup[3] = 'Reserved';
  745. }
  746. return (isset($TheoraColorSpaceLookup[$colorspace_id]) ? $TheoraColorSpaceLookup[$colorspace_id] : null);
  747. }
  748. /**
  749. * @param int $pixelformat_id
  750. *
  751. * @return string|null
  752. */
  753. public static function TheoraPixelFormat($pixelformat_id) {
  754. // http://www.theora.org/doc/Theora.pdf (table 6.4)
  755. static $TheoraPixelFormatLookup = array();
  756. if (empty($TheoraPixelFormatLookup)) {
  757. $TheoraPixelFormatLookup[0] = '4:2:0';
  758. $TheoraPixelFormatLookup[1] = 'Reserved';
  759. $TheoraPixelFormatLookup[2] = '4:2:2';
  760. $TheoraPixelFormatLookup[3] = '4:4:4';
  761. }
  762. return (isset($TheoraPixelFormatLookup[$pixelformat_id]) ? $TheoraPixelFormatLookup[$pixelformat_id] : null);
  763. }
  764. }