module.audio.ogg.php 42 KB

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