|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* Class ID3 |
|
4
|
|
|
* |
|
5
|
|
|
* @created 22.09.2018 |
|
6
|
|
|
* @author smiley <[email protected]> |
|
7
|
|
|
* @copyright 2018 smiley |
|
8
|
|
|
* @license MIT |
|
9
|
|
|
*/ |
|
10
|
|
|
|
|
11
|
|
|
namespace chillerlan\ID3Tag; |
|
12
|
|
|
|
|
13
|
|
|
use stdClass; |
|
14
|
|
|
|
|
15
|
|
|
use function fclose, file_exists, floor, fopen, fread, fseek, ftell, in_array, is_file, |
|
16
|
|
|
is_readable, is_resource, ord, realpath, round, sprintf, strlen, strtolower, substr, unpack; |
|
17
|
|
|
|
|
18
|
|
|
use const PHP_INT_MAX, SEEK_END, SEEK_SET; |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* @link http://id3.org/id3guide |
|
22
|
|
|
* @link http://www.mp3-tech.org/ |
|
23
|
|
|
* @link https://en.wikipedia.org/wiki/ID3 |
|
24
|
|
|
* @link https://docs.microsoft.com/windows/desktop/wmformat/id3-tag-support |
|
25
|
|
|
* @link https://github.com/brokencube/id3 |
|
26
|
|
|
* @link http://www.zedwood.com/article/php-calculate-duration-of-mp3 |
|
27
|
|
|
* @link https://www.mp3tag.de/en/ |
|
28
|
|
|
* @link https://github.com/taglib/taglib/tree/master/tests |
|
29
|
|
|
*/ |
|
30
|
|
|
final class ID3{ |
|
31
|
|
|
|
|
32
|
|
|
private const BITRATES = [ |
|
33
|
|
|
0b0000 => [ 0, 0, 0, 0, 0], |
|
34
|
|
|
0b0001 => [ 32, 32, 32, 32, 8], |
|
35
|
|
|
0b0010 => [ 64, 48, 40, 48, 16], |
|
36
|
|
|
0b0011 => [ 96, 56, 48, 56, 24], |
|
37
|
|
|
0b0100 => [128, 64, 56, 64, 32], |
|
38
|
|
|
0b0101 => [160, 80, 64, 80, 40], |
|
39
|
|
|
0b0110 => [192, 96, 80, 96, 48], |
|
40
|
|
|
0b0111 => [224, 112, 96, 112, 56], |
|
41
|
|
|
0b1000 => [256, 128, 112, 128, 64], |
|
42
|
|
|
0b1001 => [288, 160, 128, 144, 80], |
|
43
|
|
|
0b1010 => [320, 192, 160, 160, 96], |
|
44
|
|
|
0b1011 => [352, 224, 192, 176, 112], |
|
45
|
|
|
0b1100 => [384, 256, 224, 192, 128], |
|
46
|
|
|
0b1101 => [416, 320, 256, 224, 144], |
|
47
|
|
|
0b1110 => [448, 384, 320, 256, 160], |
|
48
|
|
|
0b1111 => [ -1, -1, -1, -1, -1], |
|
49
|
|
|
]; |
|
50
|
|
|
|
|
51
|
|
|
private const SAMPLE_RATES = [ |
|
52
|
|
|
0b00 => [ |
|
53
|
|
|
0b00 => 11025, |
|
54
|
|
|
0b01 => 12000, |
|
55
|
|
|
0b10 => 8000, |
|
56
|
|
|
0b11 => 0, |
|
57
|
|
|
], |
|
58
|
|
|
0b10 => [ |
|
59
|
|
|
0b00 => 22050, |
|
60
|
|
|
0b01 => 24000, |
|
61
|
|
|
0b10 => 16000, |
|
62
|
|
|
0b11 => 0, |
|
63
|
|
|
], |
|
64
|
|
|
0b11 => [ |
|
65
|
|
|
0b00 => 44100, |
|
66
|
|
|
0b01 => 48000, |
|
67
|
|
|
0b10 => 32000, |
|
68
|
|
|
0b11 => 0, |
|
69
|
|
|
], |
|
70
|
|
|
]; |
|
71
|
|
|
|
|
72
|
|
|
/** |
|
73
|
|
|
* "Xing"/"Info" identification string at 0x0D (13), 0x15 (21) or 0x24 (36) |
|
74
|
|
|
*/ |
|
75
|
|
|
private const VBR_ID_STRING_POS = [ |
|
76
|
|
|
// v2.5 |
|
77
|
|
|
0b00 => [ |
|
78
|
|
|
0b00 => 21, // stereo |
|
79
|
|
|
0b01 => 21, // jntstereo |
|
80
|
|
|
0b10 => 21, // dual channel |
|
81
|
|
|
0b11 => 21, // mono |
|
82
|
|
|
], |
|
83
|
|
|
// v2 |
|
84
|
|
|
0b10 => [ |
|
85
|
|
|
0b00 => 21, |
|
86
|
|
|
0b01 => 21, |
|
87
|
|
|
0b10 => 21, |
|
88
|
|
|
0b11 => 13, |
|
89
|
|
|
], |
|
90
|
|
|
// v1 |
|
91
|
|
|
0b11 => [ |
|
92
|
|
|
0b00 => 36, |
|
93
|
|
|
0b01 => 36, |
|
94
|
|
|
0b10 => 36, |
|
95
|
|
|
0b11 => 21, |
|
96
|
|
|
], |
|
97
|
|
|
]; |
|
98
|
|
|
|
|
99
|
|
|
/** |
|
100
|
|
|
* @var resource |
|
101
|
|
|
*/ |
|
102
|
|
|
private $fh; |
|
103
|
|
|
|
|
104
|
|
|
/** |
|
105
|
|
|
* @return void |
|
106
|
|
|
*/ |
|
107
|
|
|
public function __destruct(){ |
|
108
|
|
|
|
|
109
|
|
|
if(is_resource($this->fh)){ |
|
110
|
|
|
fclose($this->fh); // @codeCoverageIgnore |
|
111
|
|
|
} |
|
112
|
|
|
|
|
113
|
|
|
} |
|
114
|
|
|
|
|
115
|
|
|
/** |
|
116
|
|
|
* @throws \chillerlan\ID3Tag\ID3Exception |
|
117
|
|
|
*/ |
|
118
|
|
|
public function read(string $filename):ID3Data{ |
|
119
|
|
|
$file = realpath($filename); |
|
120
|
|
|
|
|
121
|
|
|
if(!$file || !file_exists($file) || !is_file($file) || !is_readable($file)){ |
|
122
|
|
|
throw new ID3Exception(sprintf('invalid file: %s', $filename)); |
|
123
|
|
|
} |
|
124
|
|
|
|
|
125
|
|
|
$data = new ID3Data($file); |
|
126
|
|
|
$this->fh = fopen($file, 'rb'); |
|
127
|
|
|
|
|
128
|
|
|
// invalid resource or 2GB limit on 32-bit |
|
129
|
|
|
if(!$this->fh || $data->filesize > PHP_INT_MAX){ |
|
130
|
|
|
return $data; // @codeCoverageIgnore |
|
131
|
|
|
} |
|
132
|
|
|
|
|
133
|
|
|
// check for an ID3v1 tag |
|
134
|
|
|
fseek($this->fh, -128, SEEK_END); |
|
135
|
|
|
|
|
136
|
|
|
if(fread($this->fh, 3) === 'TAG'){ |
|
137
|
|
|
fseek($this->fh, -256, SEEK_END); |
|
138
|
|
|
|
|
139
|
|
|
$properties = [ |
|
140
|
|
|
'id3v1' => (new ID3v1)->parse(fread($this->fh, 256)), |
|
141
|
|
|
'v1tagsize' => 256, |
|
142
|
|
|
]; |
|
143
|
|
|
|
|
144
|
|
|
$data->setProperties($properties); |
|
145
|
|
|
} |
|
146
|
|
|
|
|
147
|
|
|
// check for an id3v2 tag |
|
148
|
|
|
fseek($this->fh, 0, SEEK_SET); |
|
149
|
|
|
|
|
150
|
|
|
if(fread($this->fh, 3) === 'ID3'){ |
|
151
|
|
|
fseek($this->fh, 6, SEEK_SET); |
|
152
|
|
|
$tagsize = ID3Helpers::syncSafeInteger(unpack('N', fread($this->fh, 4))[1]); |
|
153
|
|
|
fseek($this->fh, 0, SEEK_SET); |
|
154
|
|
|
|
|
155
|
|
|
$properties = [ |
|
156
|
|
|
'id3v2' => $this->readID3v2Tag(fread($this->fh, $tagsize)), |
|
157
|
|
|
'v2tagsize' => $tagsize + 10, |
|
158
|
|
|
]; |
|
159
|
|
|
|
|
160
|
|
|
$data->setProperties($properties); |
|
161
|
|
|
} |
|
162
|
|
|
|
|
163
|
|
|
$data->setProperties($this->getMP3Stats($data->filesize, $data->v1tagsize, $data->v2tagsize)); |
|
164
|
|
|
|
|
165
|
|
|
fclose($this->fh); |
|
166
|
|
|
|
|
167
|
|
|
return $data; |
|
168
|
|
|
} |
|
169
|
|
|
|
|
170
|
|
|
/** |
|
171
|
|
|
* |
|
172
|
|
|
* @noinspection PhpUnusedLocalVariableInspection |
|
173
|
|
|
*/ |
|
174
|
|
|
private function readID3v2Tag(string $rawdata):?array{ |
|
175
|
|
|
|
|
176
|
|
|
/** |
|
177
|
|
|
* 3.1. ID3v2 header |
|
178
|
|
|
* |
|
179
|
|
|
* The ID3v2 tag header, which should be the first information in the |
|
180
|
|
|
* file, is 10 bytes as follows: |
|
181
|
|
|
* |
|
182
|
|
|
* ID3v2/file identifier "ID3" |
|
183
|
|
|
* ID3v2 version $03 00 |
|
184
|
|
|
* ID3v2 flags %abc00000 |
|
185
|
|
|
* ID3v2 size 4 * %0xxxxxxx |
|
186
|
|
|
* |
|
187
|
|
|
* @link http://id3.org/id3v2.3.0#ID3v2_header |
|
188
|
|
|
*/ |
|
189
|
|
|
$id3version = ord(substr($rawdata, 3, 1)); |
|
190
|
|
|
|
|
191
|
|
|
if(!in_array($id3version, [2, 3, 4], true)){ |
|
192
|
|
|
# throw new ID3Exception('invalid id3 version'); |
|
193
|
|
|
return null; |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
$flags = ord(substr($rawdata, 5, 1)); |
|
197
|
|
|
|
|
198
|
|
|
$compression = false; |
|
|
|
|
|
|
199
|
|
|
$exthead = false; |
|
200
|
|
|
$experimental = false; |
|
|
|
|
|
|
201
|
|
|
$hasfooter = false; |
|
|
|
|
|
|
202
|
|
|
|
|
203
|
|
|
/** |
|
204
|
|
|
* a - Unsynchronisation |
|
205
|
|
|
* |
|
206
|
|
|
* Bit 7 in the 'ID3v2 flags' indicates whether or not |
|
207
|
|
|
* unsynchronisation is used (see section 5 for details); a set bit |
|
208
|
|
|
* indicates usage. |
|
209
|
|
|
*/ |
|
210
|
|
|
$unsync = (bool)($flags & 0b10000000); |
|
211
|
|
|
|
|
212
|
|
|
if($id3version === 2){ |
|
213
|
|
|
|
|
214
|
|
|
/** |
|
215
|
|
|
* b - Compression |
|
216
|
|
|
* |
|
217
|
|
|
* Bit 6 is indicating whether or not compression is used; |
|
218
|
|
|
* a set bit indicates usage. Since no compression scheme has been |
|
219
|
|
|
* decided yet, the ID3 decoder (for now) should just ignore the entire |
|
220
|
|
|
* tag if the compression bit is set. |
|
221
|
|
|
*/ |
|
222
|
|
|
$compression = (bool)($flags & 0b01000000); |
|
223
|
|
|
} |
|
224
|
|
|
|
|
225
|
|
|
if($id3version === 3 || $id3version === 4){ |
|
226
|
|
|
|
|
227
|
|
|
/** |
|
228
|
|
|
* b - Extended header |
|
229
|
|
|
* |
|
230
|
|
|
* Bit 6 indicates whether or not the header is followed by an |
|
231
|
|
|
* extended header. The extended header is described in section 3.2. |
|
232
|
|
|
*/ |
|
233
|
|
|
$exthead = (bool)($flags & 0b01000000); |
|
234
|
|
|
|
|
235
|
|
|
/** |
|
236
|
|
|
* c - Experimental indicator |
|
237
|
|
|
* |
|
238
|
|
|
* Bit 5 should be used as an 'experimental indicator'. |
|
239
|
|
|
* This flag should always be set when the tag is in an experimental stage. |
|
240
|
|
|
*/ |
|
241
|
|
|
$experimental = (bool)($flags & 0b00100000); |
|
242
|
|
|
} |
|
243
|
|
|
|
|
244
|
|
|
if($id3version === 4){ |
|
245
|
|
|
|
|
246
|
|
|
/** |
|
247
|
|
|
* d - Footer present |
|
248
|
|
|
* |
|
249
|
|
|
* Bit 4 indicates that a footer (section 3.4) is present at the very |
|
250
|
|
|
* end of the tag. A set bit indicates the presence of a footer. |
|
251
|
|
|
*/ |
|
252
|
|
|
$hasfooter = (bool)($flags & 0b00010000); |
|
253
|
|
|
} |
|
254
|
|
|
|
|
255
|
|
|
$start = 10; |
|
256
|
|
|
|
|
257
|
|
|
/** |
|
258
|
|
|
* 3.2. ID3v2.3 extended header |
|
259
|
|
|
* |
|
260
|
|
|
* @link http://id3.org/id3v2.3.0#ID3v2_extended_header |
|
261
|
|
|
* |
|
262
|
|
|
* Extended header size $xx xx xx xx |
|
263
|
|
|
* Extended Flags $xx xx |
|
264
|
|
|
* Size of padding $xx xx xx xx |
|
265
|
|
|
*/ |
|
266
|
|
|
if($exthead){ |
|
267
|
|
|
$extHeaderSize = ID3Helpers::syncSafeInteger(unpack('N', substr($rawdata, $start, 4))[1]); |
|
268
|
|
|
$start += 4; |
|
269
|
|
|
// @todo (skipping the extended header for now...) |
|
270
|
|
|
/** @noinspection PhpUnusedLocalVariableInspection */ |
|
271
|
|
|
$extHeader = substr($rawdata, $start, $extHeaderSize); |
|
|
|
|
|
|
272
|
|
|
$start += $extHeaderSize; |
|
273
|
|
|
} |
|
274
|
|
|
|
|
275
|
|
|
$tagdata = substr($rawdata, $start); |
|
276
|
|
|
|
|
277
|
|
|
if($unsync && $id3version <= 3){ |
|
278
|
|
|
$tagdata = ID3Helpers::unsyncString($tagdata); |
|
279
|
|
|
} |
|
280
|
|
|
|
|
281
|
|
|
$parser = __NAMESPACE__.'\\ID3v2'.$id3version; |
|
282
|
|
|
|
|
283
|
|
|
return (new $parser)->parse($tagdata); |
|
284
|
|
|
} |
|
285
|
|
|
|
|
286
|
|
|
/** |
|
287
|
|
|
* |
|
288
|
|
|
*/ |
|
289
|
|
|
private function getMP3Stats(int $filesize, int $v1Tagsize, int $v2Tagsize):array{ |
|
290
|
|
|
$offset = $this->getMP3FrameStart($filesize, $v1Tagsize, $v2Tagsize); |
|
291
|
|
|
$framecount = 0; |
|
292
|
|
|
$duration = 0.0; |
|
293
|
|
|
$bitrate = 0; |
|
294
|
|
|
|
|
295
|
|
|
if($offset === null){ |
|
296
|
|
|
return []; |
|
297
|
|
|
} |
|
298
|
|
|
|
|
299
|
|
|
while($offset < $filesize){ |
|
300
|
|
|
$frame = $this->parseMP3FrameHeader($offset); |
|
301
|
|
|
|
|
302
|
|
|
if($frame === null){ |
|
303
|
|
|
$offset = $this->getMP3FrameStart($filesize, $v1Tagsize, $offset); |
|
304
|
|
|
|
|
305
|
|
|
if($offset !== null){ |
|
306
|
|
|
continue; |
|
307
|
|
|
} |
|
308
|
|
|
|
|
309
|
|
|
break; |
|
310
|
|
|
} |
|
311
|
|
|
|
|
312
|
|
|
/** |
|
313
|
|
|
* In the Info Tag, the "Xing" identification string at 0x0D (13), |
|
314
|
|
|
* 0x15 (21) or 0x24 (36) (depending on MPEG layer and number of channels) |
|
315
|
|
|
* of the header is replaced by "Info" in case of a CBR file. |
|
316
|
|
|
* |
|
317
|
|
|
* This was done to avoid CBR files to be recognized as traditional |
|
318
|
|
|
* Xing VBR files by some decoders. Although the two identification |
|
319
|
|
|
* strings "Xing" and "Info" are both valid, it is suggested that you |
|
320
|
|
|
* keep the identification string "Xing" in case of VBR bistream |
|
321
|
|
|
* in order to keep compatibility. |
|
322
|
|
|
* |
|
323
|
|
|
* @link http://gabriel.mp3-tech.org/mp3infotag.html |
|
324
|
|
|
*/ |
|
325
|
|
|
if($framecount === 0){ |
|
326
|
|
|
$pos = $this::VBR_ID_STRING_POS[$frame->version][$frame->channelmode] ?? 36; |
|
327
|
|
|
fseek($this->fh, $offset + $pos, SEEK_SET); |
|
328
|
|
|
|
|
329
|
|
|
if(strtolower(fread($this->fh, 4)) !== 'xing'){ // lower just in case someone thinks they're special |
|
330
|
|
|
|
|
331
|
|
|
return [ |
|
332
|
|
|
'duration' => (int)round(($filesize - $v1Tagsize - $v2Tagsize) / (($frame->bitrate * 1000) / 8)), |
|
333
|
|
|
'bitrate' => $frame->bitrate, |
|
334
|
|
|
]; |
|
335
|
|
|
} |
|
336
|
|
|
|
|
337
|
|
|
} |
|
338
|
|
|
|
|
339
|
|
|
$duration += $frame->duration; |
|
340
|
|
|
$offset += $frame->length; |
|
341
|
|
|
$bitrate += $frame->bitrate; |
|
342
|
|
|
|
|
343
|
|
|
$framecount++; |
|
344
|
|
|
} |
|
345
|
|
|
|
|
346
|
|
|
return [ |
|
347
|
|
|
'duration' => (int)round($duration), |
|
348
|
|
|
'bitrate' => (int)round($bitrate / $framecount), |
|
349
|
|
|
'framecount' => $framecount, |
|
350
|
|
|
]; |
|
351
|
|
|
} |
|
352
|
|
|
|
|
353
|
|
|
/** |
|
354
|
|
|
* |
|
355
|
|
|
*/ |
|
356
|
|
|
private function getMP3FrameStart(int $filesize, int $v1Tagsize, int $offset):?int{ |
|
357
|
|
|
fseek($this->fh, $offset, SEEK_SET); |
|
358
|
|
|
$size = $filesize - $v1Tagsize; |
|
359
|
|
|
|
|
360
|
|
|
while($offset < $size){ |
|
361
|
|
|
$offset++; |
|
362
|
|
|
|
|
363
|
|
|
$byte = fread($this->fh, 1); |
|
364
|
|
|
|
|
365
|
|
|
if($byte === false){ |
|
366
|
|
|
return null; |
|
367
|
|
|
} |
|
368
|
|
|
|
|
369
|
|
|
if($byte === "\xff"){ |
|
370
|
|
|
$byte = fread($this->fh, 1); |
|
371
|
|
|
|
|
372
|
|
|
if($byte === "\xf3" ||$byte === "\xfa" || $byte === "\xfb"){// |
|
373
|
|
|
$offset = ftell($this->fh) - 2; |
|
374
|
|
|
fseek($this->fh, $offset, SEEK_SET); |
|
375
|
|
|
|
|
376
|
|
|
return $offset; |
|
377
|
|
|
} |
|
378
|
|
|
|
|
379
|
|
|
} |
|
380
|
|
|
|
|
381
|
|
|
} |
|
382
|
|
|
|
|
383
|
|
|
return null; |
|
384
|
|
|
} |
|
385
|
|
|
|
|
386
|
|
|
/** |
|
387
|
|
|
* |
|
388
|
|
|
*/ |
|
389
|
|
|
private function parseMP3FrameHeader(int $offset):?stdClass{ |
|
390
|
|
|
fseek($this->fh, $offset, SEEK_SET); |
|
391
|
|
|
|
|
392
|
|
|
$headerBytes = fread($this->fh, 4); |
|
393
|
|
|
|
|
394
|
|
|
if(strlen($headerBytes) !== 4 || !($headerBytes[0] === "\xff" && (ord($headerBytes[1]) & 0xe0))){ |
|
395
|
|
|
return null; |
|
396
|
|
|
} |
|
397
|
|
|
|
|
398
|
|
|
$b1 = ord($headerBytes[1]); |
|
399
|
|
|
$b2 = ord($headerBytes[2]); |
|
400
|
|
|
$b3 = ord($headerBytes[3]); |
|
401
|
|
|
|
|
402
|
|
|
$info = new stdClass; |
|
403
|
|
|
|
|
404
|
|
|
/** |
|
405
|
|
|
* http://www.mp3-tech.org/programmer/frame_header.html |
|
406
|
|
|
* AAAA AAAA AAAB BCCD EEEE FFGH IIJJ KLMM |
|
407
|
|
|
* A - Frame sync (all bits set) |
|
408
|
|
|
* B - MPEG Audio version ID |
|
409
|
|
|
* C - Layer description |
|
410
|
|
|
* D - Protection bit |
|
411
|
|
|
* E - Bitrate index |
|
412
|
|
|
* F - Sampling rate frequency index |
|
413
|
|
|
* G - Padding bit |
|
414
|
|
|
* H - Private bit |
|
415
|
|
|
* I - Channel Mode |
|
416
|
|
|
* J - Mode extension (Only if Joint stereo) |
|
417
|
|
|
* K - Copyright |
|
418
|
|
|
* L - Original |
|
419
|
|
|
* M - Emphasis |
|
420
|
|
|
*/ |
|
421
|
|
|
# $info->sync = (bigEndian2Int(substr($headerBytes, 0, 2)) & 0xFFE0) >> 4; |
|
422
|
|
|
$info->version = ($b1 & 0x18) >> 3; // BB |
|
423
|
|
|
$info->layer = ($b1 & 0x06) >> 1; // CC |
|
424
|
|
|
# $info->protection = (bool)($b1 & 0x01); // D |
|
425
|
|
|
$info->bitrateIndex = ($b2 & 0xF0) >> 4; // EEEE |
|
426
|
|
|
$info->samplerate = ($b2 & 0x0C) >> 2; // FF |
|
427
|
|
|
$info->padding = (bool)(($b2 & 0x02) >> 1); // G |
|
428
|
|
|
# $info->private = (bool)($b2 & 0x01); // H |
|
429
|
|
|
$info->channelmode = ($b3 & 0xC0) >> 6; // II |
|
430
|
|
|
# $info->modeextension = ($b3 & 0x30) >> 4; // JJ |
|
431
|
|
|
# $info->copyright = (bool)(($b3 & 0x08) >> 3); // K |
|
432
|
|
|
# $info->original = (bool)(($b3 & 0x04) >> 2); // L |
|
433
|
|
|
# $info->emphasis = ($b3 & 0x03); // MM |
|
434
|
|
|
|
|
435
|
|
|
if(!in_array($info->version, [0b00, 0b10, 0b11]) || !in_array($info->layer, [0b01, 0b10, 0b11])){ |
|
436
|
|
|
return null; |
|
437
|
|
|
} |
|
438
|
|
|
|
|
439
|
|
|
$sampleRate = $this::SAMPLE_RATES[$info->version][$info->samplerate]; |
|
440
|
|
|
|
|
441
|
|
|
if($sampleRate <= 0){ |
|
442
|
|
|
// Invalid sample rate value |
|
443
|
|
|
return null; |
|
444
|
|
|
} |
|
445
|
|
|
|
|
446
|
|
|
$bitRate = 0; |
|
447
|
|
|
|
|
448
|
|
|
if($info->version === 0b11){ |
|
449
|
|
|
switch($info->layer){ |
|
450
|
|
|
case 0b11: |
|
451
|
|
|
$bitRate = $this::BITRATES[$info->bitrateIndex][0]; |
|
452
|
|
|
break; |
|
453
|
|
|
case 0b10: |
|
454
|
|
|
$bitRate = $this::BITRATES[$info->bitrateIndex][1]; |
|
455
|
|
|
break; |
|
456
|
|
|
case 0b01: |
|
457
|
|
|
$bitRate = $this::BITRATES[$info->bitrateIndex][2]; |
|
458
|
|
|
break; |
|
459
|
|
|
} |
|
460
|
|
|
} |
|
461
|
|
|
else{ |
|
462
|
|
|
switch($info->layer){ |
|
463
|
|
|
case 0b11: |
|
464
|
|
|
$bitRate = $this::BITRATES[$info->bitrateIndex][3]; |
|
465
|
|
|
break; |
|
466
|
|
|
case 0b10: |
|
467
|
|
|
case 0b01: |
|
468
|
|
|
$bitRate = $this::BITRATES[$info->bitrateIndex][4]; |
|
469
|
|
|
break; |
|
470
|
|
|
} |
|
471
|
|
|
} |
|
472
|
|
|
|
|
473
|
|
|
if($bitRate <= 0){ |
|
474
|
|
|
return null; // bitrate "free" |
|
475
|
|
|
} |
|
476
|
|
|
|
|
477
|
|
|
$info->bitrate = $bitRate; |
|
478
|
|
|
$bitRate *= 1000; |
|
479
|
|
|
|
|
480
|
|
|
if($info->layer === 0b11){ |
|
481
|
|
|
$info->length = (((12 * $bitRate) / $sampleRate) + (int)$info->padding) * 4; |
|
482
|
|
|
} |
|
483
|
|
|
elseif($info->layer === 0b10 || $info->layer === 0b01){ |
|
484
|
|
|
$info->length = ((144 * $bitRate) / $sampleRate) + (int)$info->padding; |
|
485
|
|
|
} |
|
486
|
|
|
|
|
487
|
|
|
$info->length = floor($info->length); |
|
488
|
|
|
|
|
489
|
|
|
if($info->length <= 0){ |
|
490
|
|
|
return null; |
|
491
|
|
|
} |
|
492
|
|
|
|
|
493
|
|
|
$info->duration = $info->length * 8 / $bitRate; |
|
494
|
|
|
|
|
495
|
|
|
return $info; |
|
496
|
|
|
} |
|
497
|
|
|
|
|
498
|
|
|
/** |
|
499
|
|
|
* @todo |
|
500
|
|
|
*/ |
|
501
|
|
|
# public function write(iterable $data):bool{ |
|
502
|
|
|
# return false; |
|
503
|
|
|
# } |
|
504
|
|
|
|
|
505
|
|
|
} |
|
506
|
|
|
|