3 /////////////////////////////////////////////////////////////////
4 /// getID3() by James Heinrich <info@getid3.org> //
5 // available at https://github.com/JamesHeinrich/getID3 //
6 // or https://www.getid3.org //
7 // or http://getid3.sourceforge.net //
8 // see readme.txt for more details //
9 /////////////////////////////////////////////////////////////////
11 // module.audio.dsdiff.php //
12 // module for analyzing Direct Stream Digital Interchange //
13 // File Format (DSDIFF) files //
14 // dependencies: NONE //
16 /////////////////////////////////////////////////////////////////
18 if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
22 class getid3_dsdiff
extends getid3_handler
27 public function Analyze() {
28 $info = &$this->getid3
->info
;
30 $this->fseek($info['avdataoffset']);
31 $DSDIFFheader = $this->fread(4);
33 // https://dsd-guide.com/sites/default/files/white-papers/DSDIFF_1.5_Spec.pdf
34 if (substr($DSDIFFheader, 0, 4) != 'FRM8') {
35 $this->error('Expecting "FRM8" at offset '.$info['avdataoffset'].', found "'.getid3_lib
::PrintHexBytes(substr($DSDIFFheader, 0, 4)).'"');
39 $this->fseek($info['avdataoffset']);
41 $info['encoding'] = 'ISO-8859-1'; // not certain, but assumed
42 $info['fileformat'] = 'dsdiff';
43 $info['mime_type'] = 'audio/dsd';
44 $info['audio']['dataformat'] = 'dsdiff';
45 $info['audio']['bitrate_mode'] = 'cbr';
46 $info['audio']['bits_per_sample'] = 1;
48 $info['dsdiff'] = array();
49 while (!$this->feof() && ($ChunkHeader = $this->fread(12))) {
50 if (strlen($ChunkHeader) < 12) {
51 $this->error('Expecting chunk header at offset '.$thisChunk['offset'].', found insufficient data in file, aborting parsing');
55 $thisChunk['offset'] = $this->ftell() - 12;
56 $thisChunk['name'] = substr($ChunkHeader, 0, 4);
57 if (!preg_match('#^[\\x21-\\x7E]+ *$#', $thisChunk['name'])) {
58 // "a concatenation of four printable ASCII characters in the range ' ' (space, 0x20) through '~'(0x7E). Space (0x20) cannot precede printing characters; trailing spaces are allowed."
59 $this->error('Invalid chunk name "'.$thisChunk['name'].'" ('.getid3_lib
::PrintHexBytes($thisChunk['name']).') at offset '.$thisChunk['offset'].', aborting parsing');
61 $thisChunk['size'] = getid3_lib
::BigEndian2Int(substr($ChunkHeader, 4, 8));
62 $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."
64 switch ($thisChunk['name']) {
66 $thisChunk['form_type'] = $this->fread(4);
67 if ($thisChunk['form_type'] != 'DSD ') {
68 $this->error('Expecting "DSD " at offset '.($this->ftell() - 4).', found "'.getid3_lib
::PrintHexBytes($thisChunk['form_type']).'", aborting parsing');
71 // do nothing further, prevent skipping subchunks
73 case 'PROP': // PROPerty chunk
74 $thisChunk['prop_type'] = $this->fread(4);
75 if ($thisChunk['prop_type'] != 'SND ') {
76 $this->error('Expecting "SND " at offset '.($this->ftell() - 4).', found "'.getid3_lib
::PrintHexBytes($thisChunk['prop_type']).'", aborting parsing');
79 // do nothing further, prevent skipping subchunks
81 case 'DIIN': // eDIted master INformation chunk
82 // do nothing, just prevent skipping subchunks
85 case 'FVER': // Format VERsion chunk
86 if ($thisChunk['size'] == 4) {
87 $FVER = $this->fread(4);
88 $info['dsdiff']['format_version'] = ord($FVER[0]).'.'.ord($FVER[1]).'.'.ord($FVER[2]).'.'.ord($FVER[3]);
91 $this->warning('Expecting "FVER" chunk to be 4 bytes, found '.$thisChunk['size'].' bytes, skipping chunk');
92 $this->fseek($datasize, SEEK_CUR
);
95 case 'FS ': // sample rate chunk
96 if ($thisChunk['size'] == 4) {
97 $info['dsdiff']['sample_rate'] = getid3_lib
::BigEndian2Int($this->fread(4));
98 $info['audio']['sample_rate'] = $info['dsdiff']['sample_rate'];
100 $this->warning('Expecting "FVER" chunk to be 4 bytes, found '.$thisChunk['size'].' bytes, skipping chunk');
101 $this->fseek($datasize, SEEK_CUR
);
104 case 'CHNL': // CHaNneLs chunk
105 $thisChunk['num_channels'] = getid3_lib
::BigEndian2Int($this->fread(2));
106 if ($thisChunk['num_channels'] == 0) {
107 $this->warning('channel count should be greater than zero, skipping chunk');
108 $this->fseek($datasize - 2, SEEK_CUR
);
110 for ($i = 0; $i < $thisChunk['num_channels']; $i++
) {
111 $thisChunk['channels'][$i] = $this->fread(4);
113 $info['audio']['channels'] = $thisChunk['num_channels'];
115 case 'CMPR': // CoMPRession type chunk
116 $thisChunk['compression_type'] = $this->fread(4);
117 $info['audio']['dataformat'] = trim($thisChunk['compression_type']);
118 $humanReadableByteLength = getid3_lib
::BigEndian2Int($this->fread(1));
119 $thisChunk['compression_name'] = $this->fread($humanReadableByteLength);
120 if (($humanReadableByteLength %
2) == 0) {
121 // 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
122 $this->fseek(1, SEEK_CUR
);
124 unset($humanReadableByteLength);
126 case 'ABSS': // ABSolute Start time chunk
127 $ABSS = $this->fread(8);
128 $info['dsdiff']['absolute_start_time']['hours'] = getid3_lib
::BigEndian2Int(substr($ABSS, 0, 2));
129 $info['dsdiff']['absolute_start_time']['minutes'] = getid3_lib
::BigEndian2Int(substr($ABSS, 2, 1));
130 $info['dsdiff']['absolute_start_time']['seconds'] = getid3_lib
::BigEndian2Int(substr($ABSS, 3, 1));
131 $info['dsdiff']['absolute_start_time']['samples'] = getid3_lib
::BigEndian2Int(substr($ABSS, 4, 4));
134 case 'LSCO': // LoudSpeaker COnfiguration chunk
135 // 0 = 2-channel stereo set-up
136 // 3 = 5-channel set-up according to ITU-R BS.775-1 [ITU]
137 // 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"
138 // 65535 = Undefined channel set-up
139 $thisChunk['loundspeaker_config_id'] = getid3_lib
::BigEndian2Int($this->fread(2));
141 case 'COMT': // COMmenTs chunk
142 $thisChunk['num_comments'] = getid3_lib
::BigEndian2Int($this->fread(2));
143 for ($i = 0; $i < $thisChunk['num_comments']; $i++
) {
144 $thisComment = array();
145 $COMT = $this->fread(14);
146 $thisComment['creation_year'] = getid3_lib
::BigEndian2Int(substr($COMT, 0, 2));
147 $thisComment['creation_month'] = getid3_lib
::BigEndian2Int(substr($COMT, 2, 1));
148 $thisComment['creation_day'] = getid3_lib
::BigEndian2Int(substr($COMT, 3, 1));
149 $thisComment['creation_hour'] = getid3_lib
::BigEndian2Int(substr($COMT, 4, 1));
150 $thisComment['creation_minute'] = getid3_lib
::BigEndian2Int(substr($COMT, 5, 1));
151 $thisComment['comment_type_id'] = getid3_lib
::BigEndian2Int(substr($COMT, 6, 2));
152 $thisComment['comment_ref_id'] = getid3_lib
::BigEndian2Int(substr($COMT, 8, 2));
153 $thisComment['string_length'] = getid3_lib
::BigEndian2Int(substr($COMT, 10, 4));
154 $thisComment['comment_text'] = $this->fread($thisComment['string_length']);
155 if ($thisComment['string_length'] %
2) {
156 // 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.
157 $this->fseek(1, SEEK_CUR
);
159 $thisComment['comment_type'] = $this->DSDIFFcmtType($thisComment['comment_type_id']);
160 $thisComment['comment_reference'] = $this->DSDIFFcmtRef($thisComment['comment_type_id'], $thisComment['comment_ref_id']);
161 $thisComment['creation_unix'] = mktime($thisComment['creation_hour'], $thisComment['creation_minute'], 0, $thisComment['creation_month'], $thisComment['creation_day'], $thisComment['creation_year']);
162 $thisChunk['comments'][$i] = $thisComment;
164 $commentkey = ($thisComment['comment_reference'] ?
: 'comment');
165 $info['dsdiff']['comments'][$commentkey][] = $thisComment['comment_text'];
169 case 'MARK': // MARKer chunk
170 $MARK = $this->fread(22);
171 $thisChunk['marker_hours'] = getid3_lib
::BigEndian2Int(substr($MARK, 0, 2));
172 $thisChunk['marker_minutes'] = getid3_lib
::BigEndian2Int(substr($MARK, 2, 1));
173 $thisChunk['marker_seconds'] = getid3_lib
::BigEndian2Int(substr($MARK, 3, 1));
174 $thisChunk['marker_samples'] = getid3_lib
::BigEndian2Int(substr($MARK, 4, 4));
175 $thisChunk['marker_offset'] = getid3_lib
::BigEndian2Int(substr($MARK, 8, 4));
176 $thisChunk['marker_type_id'] = getid3_lib
::BigEndian2Int(substr($MARK, 12, 2));
177 $thisChunk['marker_channel'] = getid3_lib
::BigEndian2Int(substr($MARK, 14, 2));
178 $thisChunk['marker_flagraw'] = getid3_lib
::BigEndian2Int(substr($MARK, 16, 2));
179 $thisChunk['string_length'] = getid3_lib
::BigEndian2Int(substr($MARK, 18, 4));
180 $thisChunk['description'] = ($thisChunk['string_length'] ?
$this->fread($thisChunk['string_length']) : '');
181 if ($thisChunk['string_length'] %
2) {
182 // 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.
183 $this->fseek(1, SEEK_CUR
);
185 $thisChunk['marker_type'] = $this->DSDIFFmarkType($thisChunk['marker_type_id']);
188 case 'DIAR': // artist chunk
189 case 'DITI': // title chunk
190 $thisChunk['string_length'] = getid3_lib
::BigEndian2Int($this->fread(4));
191 $thisChunk['description'] = ($thisChunk['string_length'] ?
$this->fread($thisChunk['string_length']) : '');
192 if ($thisChunk['string_length'] %
2) {
193 // 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.
194 $this->fseek(1, SEEK_CUR
);
197 if ($commentkey = (($thisChunk['name'] == 'DIAR') ?
'artist' : (($thisChunk['name'] == 'DITI') ?
'title' : ''))) {
198 @$info['dsdiff']['comments'][$commentkey][] = $thisChunk['description'];
201 case 'EMID': // Edited Master ID chunk
202 if ($thisChunk['size']) {
203 $thisChunk['identifier'] = $this->fread($thisChunk['size']);
208 $endOfID3v2 = $this->ftell() +
$datasize; // we will need to reset the filepointer after parsing ID3v2
210 getid3_lib
::IncludeDependency(GETID3_INCLUDEPATH
.'module.tag.id3v2.php', __FILE__
, true);
211 $getid3_temp = new getID3();
212 $getid3_temp->openfile($this->getid3
->filename
, null, $this->getid3
->fp
);
213 $getid3_id3v2 = new getid3_id3v2($getid3_temp);
214 $getid3_id3v2->StartingOffset
= $this->ftell();
215 if ($thisChunk['valid'] = $getid3_id3v2->Analyze()) {
216 $info['id3v2'] = $getid3_temp->info
['id3v2'];
218 unset($getid3_temp, $getid3_id3v2);
220 $this->fseek($endOfID3v2);
223 case 'DSD ': // DSD sound data chunk
224 case 'DST ': // DST sound data chunk
225 // actual audio data, we're not interested, skip
226 $this->fseek($datasize, SEEK_CUR
);
229 $this->warning('Unhandled chunk "'.$thisChunk['name'].'"');
230 $this->fseek($datasize, SEEK_CUR
);
234 @$info['dsdiff']['chunks'][] = $thisChunk;
237 if (empty($info['audio']['bitrate']) && !empty($info['audio']['channels']) && !empty($info['audio']['sample_rate']) && !empty($info['audio']['bits_per_sample'])) {
238 $info['audio']['bitrate'] = $info['audio']['bits_per_sample'] * $info['audio']['sample_rate'] * $info['audio']['channels'];
245 * @param int $cmtType
249 public static function DSDIFFcmtType($cmtType) {
250 static $DSDIFFcmtType = array(
251 0 => 'General (album) Comment',
252 1 => 'Channel Comment',
256 return (isset($DSDIFFcmtType[$cmtType]) ?
$DSDIFFcmtType[$cmtType] : 'reserved');
260 * @param int $cmtType
265 public static function DSDIFFcmtRef($cmtType, $cmtRef) {
266 static $DSDIFFcmtRef = array(
267 2 => array( // Sound Source
268 0 => 'DSD recording',
269 1 => 'Analogue recording',
270 2 => 'PCM recording',
272 3 => array( // File History
273 0 => 'comment', // General Remark
274 1 => 'encodeby', // Name of the operator
275 2 => 'encoder', // Name or type of the creating machine
276 3 => 'timezone', // Time zone information
277 4 => 'revision', // Revision of the file
282 // If the comment type is General Comment the comment reference must be 0
285 // If the comment type is Channel Comment, the comment reference defines the channel number to which the comment belongs
286 return ($cmtRef ?
'channel '.$cmtRef : 'all channels');
289 return (isset($DSDIFFcmtRef[$cmtType][$cmtRef]) ?
$DSDIFFcmtRef[$cmtType][$cmtRef] : 'reserved');
291 return 'unsupported $cmtType='.$cmtType;
295 * @param int $cmtType
299 public static function DSDIFFmarkType($markType) {
300 static $DSDIFFmarkType = array(
301 0 => 'TrackStart', // Entry point for a Track start
302 1 => 'TrackStop', // Entry point for ending a Track
303 2 => 'ProgramStart', // Start point of 2-channel or multi-channel area
305 4 => 'Index', // Entry point of an Index
307 return (isset($DSDIFFmarkType[$markType]) ?
$DSDIFFmarkType[$markType] : 'reserved');