Skip to content

Commit cde8ac0

Browse files
committed
add support for DSDIFF
#249
1 parent 13dc22a commit cde8ac0

File tree

2 files changed

+320
-1
lines changed

2 files changed

+320
-1
lines changed

getid3/getid3.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ class getID3
257257
*/
258258
protected $startup_warning = '';
259259

260-
const VERSION = '1.9.19-202005202246';
260+
const VERSION = '1.9.19-202006061653';
261261
const FREAD_BUFFER_SIZE = 32768;
262262

263263
const ATTACHMENTS_NONE = false;
@@ -853,6 +853,14 @@ public function GetFileFormatArray() {
853853
'mime_type' => 'application/octet-stream',
854854
),
855855

856+
// DSDIFF - audio - Direct Stream Digital Interchange File Format
857+
'dsdiff' => array(
858+
'pattern' => '^FRM8',
859+
'group' => 'audio',
860+
'module' => 'dsdiff',
861+
'mime_type' => 'audio/dsd',
862+
),
863+
856864
// DTS - audio - Dolby Theatre System
857865
'dts' => array(
858866
'pattern' => '^\\x7F\\xFE\\x80\\x01',
@@ -1457,6 +1465,7 @@ public function HandleAllTags() {
14571465
'flac' => array('vorbiscomment' , 'UTF-8'),
14581466
'divxtag' => array('divx' , 'ISO-8859-1'),
14591467
'iptc' => array('iptc' , 'ISO-8859-1'),
1468+
'dsdiff' => array('dsdiff' , 'ISO-8859-1'),
14601469
);
14611470
}
14621471

getid3/module.audio.dsdiff.php

+310
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
<?php
2+
3+
/////////////////////////////////////////////////////////////////
4+
/// getID3() by James Heinrich <[email protected]> //
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+
/////////////////////////////////////////////////////////////////
10+
// //
11+
// module.audio.dsdiff.php //
12+
// module for analyzing Direct Stream Digital Interchange //
13+
// File Format (DSDIFF) files //
14+
// dependencies: NONE //
15+
// ///
16+
/////////////////////////////////////////////////////////////////
17+
18+
if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
19+
exit;
20+
}
21+
22+
class getid3_dsdiff extends getid3_handler
23+
{
24+
/**
25+
* @return bool
26+
*/
27+
public function Analyze() {
28+
$info = &$this->getid3->info;
29+
30+
$this->fseek($info['avdataoffset']);
31+
$DSDIFFheader = $this->fread(4);
32+
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)).'"');
36+
return false;
37+
}
38+
unset($DSDIFFheader);
39+
$this->fseek($info['avdataoffset']);
40+
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;
47+
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');
52+
break;
53+
}
54+
$thisChunk = array();
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');
60+
}
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."
63+
64+
switch ($thisChunk['name']) {
65+
case 'FRM8':
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');
69+
break 2;
70+
}
71+
// do nothing further, prevent skipping subchunks
72+
break;
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');
77+
break 2;
78+
}
79+
// do nothing further, prevent skipping subchunks
80+
break;
81+
case 'DIIN': // eDIted master INformation chunk
82+
// do nothing, just prevent skipping subchunks
83+
break;
84+
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]);
89+
unset($FVER);
90+
} else {
91+
$this->warning('Expecting "FVER" chunk to be 4 bytes, found '.$thisChunk['size'].' bytes, skipping chunk');
92+
$this->fseek($datasize, SEEK_CUR);
93+
}
94+
break;
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'];
99+
} else {
100+
$this->warning('Expecting "FVER" chunk to be 4 bytes, found '.$thisChunk['size'].' bytes, skipping chunk');
101+
$this->fseek($datasize, SEEK_CUR);
102+
}
103+
break;
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);
109+
}
110+
for ($i = 0; $i < $thisChunk['num_channels']; $i++) {
111+
$thisChunk['channels'][$i] = $this->fread(4);
112+
}
113+
$info['audio']['channels'] = $thisChunk['num_channels'];
114+
break;
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);
123+
}
124+
unset($humanReadableByteLength);
125+
break;
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));
132+
unset($ABSS);
133+
break;
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));
140+
break;
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);
158+
}
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;
163+
164+
$commentkey = ($thisComment['comment_reference'] ?: 'comment');
165+
$info['dsdiff']['comments'][$commentkey][] = $thisComment['comment_text'];
166+
unset($thisComment);
167+
}
168+
break;
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);
184+
}
185+
$thisChunk['marker_type'] = $this->DSDIFFmarkType($thisChunk['marker_type_id']);
186+
unset($MARK);
187+
break;
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);
195+
}
196+
197+
if ($commentkey = (($thisChunk['name'] == 'DIAR') ? 'artist' : (($thisChunk['name'] == 'DITI') ? 'title' : ''))) {
198+
@$info['dsdiff']['comments'][$commentkey][] = $thisChunk['description'];
199+
}
200+
break;
201+
case 'EMID': // Edited Master ID chunk
202+
if ($thisChunk['size']) {
203+
$thisChunk['identifier'] = $this->fread($thisChunk['size']);
204+
}
205+
break;
206+
207+
case 'ID3 ':
208+
$endOfID3v2 = $this->ftell() + $datasize; // we will need to reset the filepointer after parsing ID3v2
209+
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'];
217+
}
218+
unset($getid3_temp, $getid3_id3v2);
219+
220+
$this->fseek($endOfID3v2);
221+
break;
222+
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);
227+
break;
228+
default:
229+
$this->warning('Unhandled chunk "'.$thisChunk['name'].'"');
230+
$this->fseek($datasize, SEEK_CUR);
231+
break;
232+
}
233+
234+
@$info['dsdiff']['chunks'][] = $thisChunk;
235+
//break;
236+
}
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'];
239+
}
240+
241+
return true;
242+
}
243+
244+
/**
245+
* @param int $cmtType
246+
*
247+
* @return string
248+
*/
249+
public static function DSDIFFcmtType($cmtType) {
250+
static $DSDIFFcmtType = array(
251+
0 => 'General (album) Comment',
252+
1 => 'Channel Comment',
253+
2 => 'Sound Source',
254+
3 => 'File History',
255+
);
256+
return (isset($DSDIFFcmtType[$cmtType]) ? $DSDIFFcmtType[$cmtType] : 'reserved');
257+
}
258+
259+
/**
260+
* @param int $cmtType
261+
* @param int $cmtRef
262+
*
263+
* @return string
264+
*/
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',
271+
),
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
278+
),
279+
);
280+
switch ($cmtType) {
281+
case 0:
282+
// If the comment type is General Comment the comment reference must be 0
283+
return '';
284+
case 1:
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');
287+
case 2:
288+
case 3:
289+
return (isset($DSDIFFcmtRef[$cmtType][$cmtRef]) ? $DSDIFFcmtRef[$cmtType][$cmtRef] : 'reserved');
290+
}
291+
return 'unsupported $cmtType='.$cmtType;
292+
}
293+
294+
/**
295+
* @param int $cmtType
296+
*
297+
* @return string
298+
*/
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
304+
3 => 'Obsolete', //
305+
4 => 'Index', // Entry point of an Index
306+
);
307+
return (isset($DSDIFFmarkType[$markType]) ? $DSDIFFmarkType[$markType] : 'reserved');
308+
}
309+
310+
}

0 commit comments

Comments
 (0)