module.audio.dsdiff.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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.dsdiff.php //
  11. // module for analyzing Direct Stream Digital Interchange //
  12. // File Format (DSDIFF) files //
  13. // dependencies: NONE //
  14. // ///
  15. /////////////////////////////////////////////////////////////////
  16. if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
  17. exit;
  18. }
  19. class getid3_dsdiff extends getid3_handler
  20. {
  21. /**
  22. * @return bool
  23. */
  24. public function Analyze() {
  25. $info = &$this->getid3->info;
  26. $this->fseek($info['avdataoffset']);
  27. $DSDIFFheader = $this->fread(4);
  28. // https://dsd-guide.com/sites/default/files/white-papers/DSDIFF_1.5_Spec.pdf
  29. if (substr($DSDIFFheader, 0, 4) != 'FRM8') {
  30. $this->error('Expecting "FRM8" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($DSDIFFheader, 0, 4)).'"');
  31. return false;
  32. }
  33. unset($DSDIFFheader);
  34. $this->fseek($info['avdataoffset']);
  35. $info['encoding'] = 'ISO-8859-1'; // not certain, but assumed
  36. $info['fileformat'] = 'dsdiff';
  37. $info['mime_type'] = 'audio/dsd';
  38. $info['audio']['dataformat'] = 'dsdiff';
  39. $info['audio']['bitrate_mode'] = 'cbr';
  40. $info['audio']['bits_per_sample'] = 1;
  41. $info['dsdiff'] = array();
  42. $thisChunk = null;
  43. while (!$this->feof() && ($ChunkHeader = $this->fread(12))) {
  44. if (strlen($ChunkHeader) < 12) {
  45. $this->error('Expecting chunk header at offset '.(isset($thisChunk['offset']) ? $thisChunk['offset'] : 'N/A').', found insufficient data in file, aborting parsing');
  46. break;
  47. }
  48. $thisChunk = array();
  49. $thisChunk['offset'] = $this->ftell() - 12;
  50. $thisChunk['name'] = substr($ChunkHeader, 0, 4);
  51. if (!preg_match('#^[\\x21-\\x7E]+ *$#', $thisChunk['name'])) {
  52. // "a concatenation of four printable ASCII characters in the range ' ' (space, 0x20) through '~'(0x7E). Space (0x20) cannot precede printing characters; trailing spaces are allowed."
  53. $this->error('Invalid chunk name "'.$thisChunk['name'].'" ('.getid3_lib::PrintHexBytes($thisChunk['name']).') at offset '.$thisChunk['offset'].', aborting parsing');
  54. }
  55. $thisChunk['size'] = getid3_lib::BigEndian2Int(substr($ChunkHeader, 4, 8));
  56. $datasize = $thisChunk['size'] + ($thisChunk['size'] % 2); // "If the data is an odd number of bytes in length, a pad byte must be added at the end. The pad byte is not included in ckDataSize."
  57. switch ($thisChunk['name']) {
  58. case 'FRM8':
  59. $thisChunk['form_type'] = $this->fread(4);
  60. if ($thisChunk['form_type'] != 'DSD ') {
  61. $this->error('Expecting "DSD " at offset '.($this->ftell() - 4).', found "'.getid3_lib::PrintHexBytes($thisChunk['form_type']).'", aborting parsing');
  62. break 2;
  63. }
  64. // do nothing further, prevent skipping subchunks
  65. break;
  66. case 'PROP': // PROPerty chunk
  67. $thisChunk['prop_type'] = $this->fread(4);
  68. if ($thisChunk['prop_type'] != 'SND ') {
  69. $this->error('Expecting "SND " at offset '.($this->ftell() - 4).', found "'.getid3_lib::PrintHexBytes($thisChunk['prop_type']).'", aborting parsing');
  70. break 2;
  71. }
  72. // do nothing further, prevent skipping subchunks
  73. break;
  74. case 'DIIN': // eDIted master INformation chunk
  75. // do nothing, just prevent skipping subchunks
  76. break;
  77. case 'FVER': // Format VERsion chunk
  78. if ($thisChunk['size'] == 4) {
  79. $FVER = $this->fread(4);
  80. $info['dsdiff']['format_version'] = ord($FVER[0]).'.'.ord($FVER[1]).'.'.ord($FVER[2]).'.'.ord($FVER[3]);
  81. unset($FVER);
  82. } else {
  83. $this->warning('Expecting "FVER" chunk to be 4 bytes, found '.$thisChunk['size'].' bytes, skipping chunk');
  84. $this->fseek($datasize, SEEK_CUR);
  85. }
  86. break;
  87. case 'FS ': // sample rate chunk
  88. if ($thisChunk['size'] == 4) {
  89. $info['dsdiff']['sample_rate'] = getid3_lib::BigEndian2Int($this->fread(4));
  90. $info['audio']['sample_rate'] = $info['dsdiff']['sample_rate'];
  91. } else {
  92. $this->warning('Expecting "FVER" chunk to be 4 bytes, found '.$thisChunk['size'].' bytes, skipping chunk');
  93. $this->fseek($datasize, SEEK_CUR);
  94. }
  95. break;
  96. case 'CHNL': // CHaNneLs chunk
  97. $thisChunk['num_channels'] = getid3_lib::BigEndian2Int($this->fread(2));
  98. if ($thisChunk['num_channels'] == 0) {
  99. $this->warning('channel count should be greater than zero, skipping chunk');
  100. $this->fseek($datasize - 2, SEEK_CUR);
  101. }
  102. for ($i = 0; $i < $thisChunk['num_channels']; $i++) {
  103. $thisChunk['channels'][$i] = $this->fread(4);
  104. }
  105. $info['audio']['channels'] = $thisChunk['num_channels'];
  106. break;
  107. case 'CMPR': // CoMPRession type chunk
  108. $thisChunk['compression_type'] = $this->fread(4);
  109. $info['audio']['dataformat'] = trim($thisChunk['compression_type']);
  110. $humanReadableByteLength = getid3_lib::BigEndian2Int($this->fread(1));
  111. $thisChunk['compression_name'] = $this->fread($humanReadableByteLength);
  112. if (($humanReadableByteLength % 2) == 0) {
  113. // need to seek to multiple of 2 bytes, human-readable string length is only one byte long so if the string is an even number of bytes we need to seek past a padding byte after the string
  114. $this->fseek(1, SEEK_CUR);
  115. }
  116. unset($humanReadableByteLength);
  117. break;
  118. case 'ABSS': // ABSolute Start time chunk
  119. $ABSS = $this->fread(8);
  120. $info['dsdiff']['absolute_start_time']['hours'] = getid3_lib::BigEndian2Int(substr($ABSS, 0, 2));
  121. $info['dsdiff']['absolute_start_time']['minutes'] = getid3_lib::BigEndian2Int(substr($ABSS, 2, 1));
  122. $info['dsdiff']['absolute_start_time']['seconds'] = getid3_lib::BigEndian2Int(substr($ABSS, 3, 1));
  123. $info['dsdiff']['absolute_start_time']['samples'] = getid3_lib::BigEndian2Int(substr($ABSS, 4, 4));
  124. unset($ABSS);
  125. break;
  126. case 'LSCO': // LoudSpeaker COnfiguration chunk
  127. // 0 = 2-channel stereo set-up
  128. // 3 = 5-channel set-up according to ITU-R BS.775-1 [ITU]
  129. // 4 = 6-channel set-up, 5-channel set-up according to ITU-R BS.775-1 [ITU], plus additional Low Frequency Enhancement (LFE) loudspeaker. Also known as "5.1 configuration"
  130. // 65535 = Undefined channel set-up
  131. $thisChunk['loundspeaker_config_id'] = getid3_lib::BigEndian2Int($this->fread(2));
  132. break;
  133. case 'COMT': // COMmenTs chunk
  134. $thisChunk['num_comments'] = getid3_lib::BigEndian2Int($this->fread(2));
  135. for ($i = 0; $i < $thisChunk['num_comments']; $i++) {
  136. $thisComment = array();
  137. $COMT = $this->fread(14);
  138. $thisComment['creation_year'] = getid3_lib::BigEndian2Int(substr($COMT, 0, 2));
  139. $thisComment['creation_month'] = getid3_lib::BigEndian2Int(substr($COMT, 2, 1));
  140. $thisComment['creation_day'] = getid3_lib::BigEndian2Int(substr($COMT, 3, 1));
  141. $thisComment['creation_hour'] = getid3_lib::BigEndian2Int(substr($COMT, 4, 1));
  142. $thisComment['creation_minute'] = getid3_lib::BigEndian2Int(substr($COMT, 5, 1));
  143. $thisComment['comment_type_id'] = getid3_lib::BigEndian2Int(substr($COMT, 6, 2));
  144. $thisComment['comment_ref_id'] = getid3_lib::BigEndian2Int(substr($COMT, 8, 2));
  145. $thisComment['string_length'] = getid3_lib::BigEndian2Int(substr($COMT, 10, 4));
  146. $thisComment['comment_text'] = $this->fread($thisComment['string_length']);
  147. if ($thisComment['string_length'] % 2) {
  148. // commentText[] is the description of the Comment. This text must be padded with a byte at the end, if needed, to make it an even number of bytes long. This pad byte, if present, is not included in count.
  149. $this->fseek(1, SEEK_CUR);
  150. }
  151. $thisComment['comment_type'] = $this->DSDIFFcmtType($thisComment['comment_type_id']);
  152. $thisComment['comment_reference'] = $this->DSDIFFcmtRef($thisComment['comment_type_id'], $thisComment['comment_ref_id']);
  153. $thisComment['creation_unix'] = mktime($thisComment['creation_hour'], $thisComment['creation_minute'], 0, $thisComment['creation_month'], $thisComment['creation_day'], $thisComment['creation_year']);
  154. $thisChunk['comments'][$i] = $thisComment;
  155. $commentkey = ($thisComment['comment_reference'] ?: 'comment');
  156. $info['dsdiff']['comments'][$commentkey][] = $thisComment['comment_text'];
  157. unset($thisComment);
  158. }
  159. break;
  160. case 'MARK': // MARKer chunk
  161. $MARK = $this->fread(22);
  162. $thisChunk['marker_hours'] = getid3_lib::BigEndian2Int(substr($MARK, 0, 2));
  163. $thisChunk['marker_minutes'] = getid3_lib::BigEndian2Int(substr($MARK, 2, 1));
  164. $thisChunk['marker_seconds'] = getid3_lib::BigEndian2Int(substr($MARK, 3, 1));
  165. $thisChunk['marker_samples'] = getid3_lib::BigEndian2Int(substr($MARK, 4, 4));
  166. $thisChunk['marker_offset'] = getid3_lib::BigEndian2Int(substr($MARK, 8, 4));
  167. $thisChunk['marker_type_id'] = getid3_lib::BigEndian2Int(substr($MARK, 12, 2));
  168. $thisChunk['marker_channel'] = getid3_lib::BigEndian2Int(substr($MARK, 14, 2));
  169. $thisChunk['marker_flagraw'] = getid3_lib::BigEndian2Int(substr($MARK, 16, 2));
  170. $thisChunk['string_length'] = getid3_lib::BigEndian2Int(substr($MARK, 18, 4));
  171. $thisChunk['description'] = ($thisChunk['string_length'] ? $this->fread($thisChunk['string_length']) : '');
  172. if ($thisChunk['string_length'] % 2) {
  173. // markerText[] is the description of the marker. This text must be padded with a byte at the end, if needed, to make it an even number of bytes long. This pad byte, if present, is not included in count.
  174. $this->fseek(1, SEEK_CUR);
  175. }
  176. $thisChunk['marker_type'] = $this->DSDIFFmarkType($thisChunk['marker_type_id']);
  177. unset($MARK);
  178. break;
  179. case 'DIAR': // artist chunk
  180. case 'DITI': // title chunk
  181. $thisChunk['string_length'] = getid3_lib::BigEndian2Int($this->fread(4));
  182. $thisChunk['description'] = ($thisChunk['string_length'] ? $this->fread($thisChunk['string_length']) : '');
  183. if ($thisChunk['string_length'] % 2) {
  184. // This text must be padded with a byte at the end, if needed, to make it an even number of bytes long. This pad byte, if present, is not included in count.
  185. $this->fseek(1, SEEK_CUR);
  186. }
  187. if ($commentkey = (($thisChunk['name'] == 'DIAR') ? 'artist' : (($thisChunk['name'] == 'DITI') ? 'title' : ''))) {
  188. @$info['dsdiff']['comments'][$commentkey][] = $thisChunk['description'];
  189. }
  190. break;
  191. case 'EMID': // Edited Master ID chunk
  192. if ($thisChunk['size']) {
  193. $thisChunk['identifier'] = $this->fread($thisChunk['size']);
  194. }
  195. break;
  196. case 'ID3 ':
  197. $endOfID3v2 = $this->ftell() + $datasize; // we will need to reset the filepointer after parsing ID3v2
  198. getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true);
  199. $getid3_temp = new getID3();
  200. $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp);
  201. $getid3_id3v2 = new getid3_id3v2($getid3_temp);
  202. $getid3_id3v2->StartingOffset = $this->ftell();
  203. if ($thisChunk['valid'] = $getid3_id3v2->Analyze()) {
  204. $info['id3v2'] = $getid3_temp->info['id3v2'];
  205. }
  206. unset($getid3_temp, $getid3_id3v2);
  207. $this->fseek($endOfID3v2);
  208. break;
  209. case 'DSD ': // DSD sound data chunk
  210. case 'DST ': // DST sound data chunk
  211. // actual audio data, we're not interested, skip
  212. $this->fseek($datasize, SEEK_CUR);
  213. break;
  214. default:
  215. $this->warning('Unhandled chunk "'.$thisChunk['name'].'"');
  216. $this->fseek($datasize, SEEK_CUR);
  217. break;
  218. }
  219. @$info['dsdiff']['chunks'][] = $thisChunk;
  220. //break;
  221. }
  222. if (empty($info['audio']['bitrate']) && !empty($info['audio']['channels']) && !empty($info['audio']['sample_rate']) && !empty($info['audio']['bits_per_sample'])) {
  223. $info['audio']['bitrate'] = $info['audio']['bits_per_sample'] * $info['audio']['sample_rate'] * $info['audio']['channels'];
  224. }
  225. return true;
  226. }
  227. /**
  228. * @param int $cmtType
  229. *
  230. * @return string
  231. */
  232. public static function DSDIFFcmtType($cmtType) {
  233. static $DSDIFFcmtType = array(
  234. 0 => 'General (album) Comment',
  235. 1 => 'Channel Comment',
  236. 2 => 'Sound Source',
  237. 3 => 'File History',
  238. );
  239. return (isset($DSDIFFcmtType[$cmtType]) ? $DSDIFFcmtType[$cmtType] : 'reserved');
  240. }
  241. /**
  242. * @param int $cmtType
  243. * @param int $cmtRef
  244. *
  245. * @return string
  246. */
  247. public static function DSDIFFcmtRef($cmtType, $cmtRef) {
  248. static $DSDIFFcmtRef = array(
  249. 2 => array( // Sound Source
  250. 0 => 'DSD recording',
  251. 1 => 'Analogue recording',
  252. 2 => 'PCM recording',
  253. ),
  254. 3 => array( // File History
  255. 0 => 'comment', // General Remark
  256. 1 => 'encodeby', // Name of the operator
  257. 2 => 'encoder', // Name or type of the creating machine
  258. 3 => 'timezone', // Time zone information
  259. 4 => 'revision', // Revision of the file
  260. ),
  261. );
  262. switch ($cmtType) {
  263. case 0:
  264. // If the comment type is General Comment the comment reference must be 0
  265. return '';
  266. case 1:
  267. // If the comment type is Channel Comment, the comment reference defines the channel number to which the comment belongs
  268. return ($cmtRef ? 'channel '.$cmtRef : 'all channels');
  269. case 2:
  270. case 3:
  271. return (isset($DSDIFFcmtRef[$cmtType][$cmtRef]) ? $DSDIFFcmtRef[$cmtType][$cmtRef] : 'reserved');
  272. }
  273. return 'unsupported $cmtType='.$cmtType;
  274. }
  275. /**
  276. * @param int $markType
  277. *
  278. * @return string
  279. */
  280. public static function DSDIFFmarkType($markType) {
  281. static $DSDIFFmarkType = array(
  282. 0 => 'TrackStart', // Entry point for a Track start
  283. 1 => 'TrackStop', // Entry point for ending a Track
  284. 2 => 'ProgramStart', // Start point of 2-channel or multi-channel area
  285. 3 => 'Obsolete', //
  286. 4 => 'Index', // Entry point of an Index
  287. );
  288. return (isset($DSDIFFmarkType[$markType]) ? $DSDIFFmarkType[$markType] : 'reserved');
  289. }
  290. }