music-metadata 7.12.3 → 7.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +9 -9
- package/README.md +434 -434
- package/lib/ParserFactory.d.ts +48 -48
- package/lib/ParserFactory.js +252 -252
- package/lib/aiff/AiffParser.d.ts +14 -14
- package/lib/aiff/AiffParser.js +84 -84
- package/lib/aiff/AiffToken.d.ts +22 -22
- package/lib/aiff/AiffToken.js +43 -43
- package/lib/apev2/APEv2Parser.d.ts +30 -30
- package/lib/apev2/APEv2Parser.js +164 -164
- package/lib/apev2/APEv2TagMapper.d.ts +4 -4
- package/lib/apev2/APEv2TagMapper.js +86 -86
- package/lib/apev2/APEv2Token.d.ts +100 -100
- package/lib/apev2/APEv2Token.js +126 -126
- package/lib/asf/AsfObject.d.ts +319 -319
- package/lib/asf/AsfObject.js +381 -381
- package/lib/asf/AsfParser.d.ts +17 -17
- package/lib/asf/AsfParser.js +135 -135
- package/lib/asf/AsfTagMapper.d.ts +7 -7
- package/lib/asf/AsfTagMapper.js +95 -95
- package/lib/asf/AsfUtil.d.ts +13 -13
- package/lib/asf/AsfUtil.js +38 -38
- package/lib/asf/GUID.d.ts +84 -84
- package/lib/asf/GUID.js +121 -121
- package/lib/common/BasicParser.d.ts +17 -17
- package/lib/common/BasicParser.js +18 -18
- package/lib/common/CaseInsensitiveTagMap.d.ts +10 -10
- package/lib/common/CaseInsensitiveTagMap.js +21 -21
- package/lib/common/CombinedTagMapper.d.ts +19 -19
- package/lib/common/CombinedTagMapper.js +51 -51
- package/lib/common/FourCC.d.ts +6 -6
- package/lib/common/FourCC.js +28 -28
- package/lib/common/GenericTagMapper.d.ts +51 -51
- package/lib/common/GenericTagMapper.js +55 -55
- package/lib/common/GenericTagTypes.d.ts +33 -33
- package/lib/common/GenericTagTypes.js +131 -131
- package/lib/common/MetadataCollector.d.ts +76 -76
- package/lib/common/MetadataCollector.js +275 -275
- package/lib/common/RandomFileReader.d.ts +22 -22
- package/lib/common/RandomFileReader.js +34 -34
- package/lib/common/RandomUint8ArrayReader.d.ts +18 -18
- package/lib/common/RandomUint8ArrayReader.js +25 -25
- package/lib/common/Util.d.ts +57 -57
- package/lib/common/Util.js +157 -157
- package/lib/core.d.ts +48 -48
- package/lib/core.js +90 -90
- package/lib/dsdiff/DsdiffParser.d.ts +14 -14
- package/lib/dsdiff/DsdiffParser.js +143 -143
- package/lib/dsdiff/DsdiffToken.d.ts +9 -9
- package/lib/dsdiff/DsdiffToken.js +21 -21
- package/lib/dsf/DsfChunk.d.ts +86 -86
- package/lib/dsf/DsfChunk.js +54 -54
- package/lib/dsf/DsfParser.d.ts +9 -9
- package/lib/dsf/DsfParser.js +56 -56
- package/lib/flac/FlacParser.d.ts +28 -28
- package/lib/flac/FlacParser.js +175 -175
- package/lib/id3v1/ID3v1Parser.d.ts +13 -13
- package/lib/id3v1/ID3v1Parser.js +134 -134
- package/lib/id3v1/ID3v1TagMap.d.ts +4 -4
- package/lib/id3v1/ID3v1TagMap.js +22 -22
- package/lib/id3v2/AbstractID3Parser.d.ts +17 -17
- package/lib/id3v2/AbstractID3Parser.js +60 -60
- package/lib/id3v2/FrameParser.d.ts +31 -31
- package/lib/id3v2/FrameParser.js +329 -329
- package/lib/id3v2/ID3v22TagMapper.d.ts +9 -9
- package/lib/id3v2/ID3v22TagMapper.js +55 -55
- package/lib/id3v2/ID3v24TagMapper.d.ts +14 -14
- package/lib/id3v2/ID3v24TagMapper.js +193 -193
- package/lib/id3v2/ID3v2Parser.d.ts +28 -28
- package/lib/id3v2/ID3v2Parser.js +182 -182
- package/lib/id3v2/ID3v2Token.d.ts +73 -73
- package/lib/id3v2/ID3v2Token.js +106 -106
- package/lib/iff/index.d.ts +33 -33
- package/lib/iff/index.js +19 -19
- package/lib/index.d.ts +45 -45
- package/lib/index.js +74 -74
- package/lib/lyrics3/Lyrics3.d.ts +3 -3
- package/lib/lyrics3/Lyrics3.js +17 -17
- package/lib/matroska/MatroskaDtd.d.ts +8 -8
- package/lib/matroska/MatroskaDtd.js +279 -279
- package/lib/matroska/MatroskaParser.d.ts +37 -37
- package/lib/matroska/MatroskaParser.js +235 -235
- package/lib/matroska/MatroskaTagMapper.d.ts +4 -4
- package/lib/matroska/MatroskaTagMapper.js +35 -35
- package/lib/matroska/types.d.ts +175 -175
- package/lib/matroska/types.js +32 -32
- package/lib/mp4/Atom.d.ts +16 -16
- package/lib/mp4/Atom.js +70 -70
- package/lib/mp4/AtomToken.d.ts +395 -395
- package/lib/mp4/AtomToken.js +406 -406
- package/lib/mp4/MP4Parser.d.ts +30 -30
- package/lib/mp4/MP4Parser.js +511 -511
- package/lib/mp4/MP4TagMapper.d.ts +5 -5
- package/lib/mp4/MP4TagMapper.js +115 -115
- package/lib/mpeg/ExtendedLameHeader.d.ts +27 -27
- package/lib/mpeg/ExtendedLameHeader.js +31 -31
- package/lib/mpeg/MpegParser.d.ts +49 -49
- package/lib/mpeg/MpegParser.js +524 -524
- package/lib/mpeg/ReplayGainDataFormat.d.ts +55 -55
- package/lib/mpeg/ReplayGainDataFormat.js +69 -69
- package/lib/mpeg/XingTag.d.ts +45 -45
- package/lib/mpeg/XingTag.js +69 -69
- package/lib/musepack/index.d.ts +5 -5
- package/lib/musepack/index.js +32 -32
- package/lib/musepack/sv7/BitReader.d.ts +13 -13
- package/lib/musepack/sv7/BitReader.js +54 -54
- package/lib/musepack/sv7/MpcSv7Parser.d.ts +8 -8
- package/lib/musepack/sv7/MpcSv7Parser.js +46 -46
- package/lib/musepack/sv7/StreamVersion7.d.ts +28 -28
- package/lib/musepack/sv7/StreamVersion7.js +41 -41
- package/lib/musepack/sv8/MpcSv8Parser.d.ts +6 -6
- package/lib/musepack/sv8/MpcSv8Parser.js +55 -55
- package/lib/musepack/sv8/StreamVersion8.d.ts +40 -40
- package/lib/musepack/sv8/StreamVersion8.js +80 -80
- package/lib/ogg/Ogg.d.ts +72 -72
- package/lib/ogg/Ogg.js +2 -2
- package/lib/ogg/OggParser.d.ts +23 -23
- package/lib/ogg/OggParser.js +126 -126
- package/lib/ogg/opus/Opus.d.ts +48 -48
- package/lib/ogg/opus/Opus.js +28 -28
- package/lib/ogg/opus/OpusParser.d.ts +25 -25
- package/lib/ogg/opus/OpusParser.js +56 -56
- package/lib/ogg/speex/Speex.d.ts +36 -36
- package/lib/ogg/speex/Speex.js +31 -31
- package/lib/ogg/speex/SpeexParser.d.ts +22 -22
- package/lib/ogg/speex/SpeexParser.js +35 -35
- package/lib/ogg/theora/Theora.d.ts +20 -20
- package/lib/ogg/theora/Theora.js +23 -23
- package/lib/ogg/theora/TheoraParser.d.ts +28 -28
- package/lib/ogg/theora/TheoraParser.js +44 -44
- package/lib/ogg/vorbis/Vorbis.d.ts +69 -69
- package/lib/ogg/vorbis/Vorbis.js +78 -78
- package/lib/ogg/vorbis/VorbisDecoder.d.ts +12 -12
- package/lib/ogg/vorbis/VorbisDecoder.js +32 -32
- package/lib/ogg/vorbis/VorbisParser.d.ts +36 -36
- package/lib/ogg/vorbis/VorbisParser.js +128 -128
- package/lib/ogg/vorbis/VorbisTagMapper.d.ts +7 -7
- package/lib/ogg/vorbis/VorbisTagMapper.js +132 -132
- package/lib/riff/RiffChunk.d.ts +16 -16
- package/lib/riff/RiffChunk.js +32 -32
- package/lib/riff/RiffInfoTagMap.d.ts +10 -10
- package/lib/riff/RiffInfoTagMap.js +37 -37
- package/lib/type.d.ts +592 -592
- package/lib/type.js +5 -5
- package/lib/wav/BwfChunk.d.ts +17 -17
- package/lib/wav/BwfChunk.js +29 -28
- package/lib/wav/WaveChunk.d.ts +64 -64
- package/lib/wav/WaveChunk.js +65 -65
- package/lib/wav/WaveParser.d.ts +24 -24
- package/lib/wav/WaveParser.js +158 -156
- package/lib/wavpack/WavPackParser.d.ts +14 -14
- package/lib/wavpack/WavPackParser.js +99 -99
- package/lib/wavpack/WavPackToken.d.ts +64 -64
- package/lib/wavpack/WavPackToken.js +76 -76
- package/package.json +150 -150
package/lib/mpeg/MpegParser.js
CHANGED
|
@@ -1,524 +1,524 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MpegParser = void 0;
|
|
4
|
-
const Token = require("token-types");
|
|
5
|
-
const core_1 = require("strtok3/lib/core");
|
|
6
|
-
const debug_1 = require("debug");
|
|
7
|
-
const common = require("../common/Util");
|
|
8
|
-
const AbstractID3Parser_1 = require("../id3v2/AbstractID3Parser");
|
|
9
|
-
const XingTag_1 = require("./XingTag");
|
|
10
|
-
const debug = (0, debug_1.default)('music-metadata:parser:mpeg');
|
|
11
|
-
/**
|
|
12
|
-
* Cache buffer size used for searching synchronization preabmle
|
|
13
|
-
*/
|
|
14
|
-
const maxPeekLen = 1024;
|
|
15
|
-
/**
|
|
16
|
-
* MPEG-4 Audio definitions
|
|
17
|
-
* Ref: https://wiki.multimedia.cx/index.php/MPEG-4_Audio
|
|
18
|
-
*/
|
|
19
|
-
const MPEG4 = {
|
|
20
|
-
/**
|
|
21
|
-
* Audio Object Types
|
|
22
|
-
*/
|
|
23
|
-
AudioObjectTypes: [
|
|
24
|
-
'AAC Main',
|
|
25
|
-
'AAC LC',
|
|
26
|
-
'AAC SSR',
|
|
27
|
-
'AAC LTP' // Long Term Prediction
|
|
28
|
-
],
|
|
29
|
-
/**
|
|
30
|
-
* Sampling Frequencies
|
|
31
|
-
* https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Sampling_Frequencies
|
|
32
|
-
*/
|
|
33
|
-
SamplingFrequencies: [
|
|
34
|
-
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, undefined, undefined, -1
|
|
35
|
-
]
|
|
36
|
-
/**
|
|
37
|
-
* Channel Configurations
|
|
38
|
-
*/
|
|
39
|
-
};
|
|
40
|
-
const MPEG4_ChannelConfigurations = [
|
|
41
|
-
undefined,
|
|
42
|
-
['front-center'],
|
|
43
|
-
['front-left', 'front-right'],
|
|
44
|
-
['front-center', 'front-left', 'front-right'],
|
|
45
|
-
['front-center', 'front-left', 'front-right', 'back-center'],
|
|
46
|
-
['front-center', 'front-left', 'front-right', 'back-left', 'back-right'],
|
|
47
|
-
['front-center', 'front-left', 'front-right', 'back-left', 'back-right', 'LFE-channel'],
|
|
48
|
-
['front-center', 'front-left', 'front-right', 'side-left', 'side-right', 'back-left', 'back-right', 'LFE-channel']
|
|
49
|
-
];
|
|
50
|
-
/**
|
|
51
|
-
* MPEG Audio Layer I/II/III frame header
|
|
52
|
-
* Ref: https://www.mp3-tech.org/programmer/frame_header.html
|
|
53
|
-
* Bit layout: AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
|
|
54
|
-
* Ref: https://wiki.multimedia.cx/index.php/ADTS
|
|
55
|
-
*/
|
|
56
|
-
class MpegFrameHeader {
|
|
57
|
-
constructor(buf, off) {
|
|
58
|
-
// B(20,19): MPEG Audio versionIndex ID
|
|
59
|
-
this.versionIndex = common.getBitAllignedNumber(buf, off + 1, 3, 2);
|
|
60
|
-
// C(18,17): Layer description
|
|
61
|
-
this.layer = MpegFrameHeader.LayerDescription[common.getBitAllignedNumber(buf, off + 1, 5, 2)];
|
|
62
|
-
if (this.versionIndex > 1 && this.layer === 0) {
|
|
63
|
-
this.parseAdtsHeader(buf, off); // Audio Data Transport Stream (ADTS)
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
this.parseMpegHeader(buf, off); // Conventional MPEG header
|
|
67
|
-
}
|
|
68
|
-
// D(16): Protection bit (if true 16-bit CRC follows header)
|
|
69
|
-
this.isProtectedByCRC = !common.isBitSet(buf, off + 1, 7);
|
|
70
|
-
}
|
|
71
|
-
calcDuration(numFrames) {
|
|
72
|
-
return numFrames * this.calcSamplesPerFrame() / this.samplingRate;
|
|
73
|
-
}
|
|
74
|
-
calcSamplesPerFrame() {
|
|
75
|
-
return MpegFrameHeader.samplesInFrameTable[this.version === 1 ? 0 : 1][this.layer];
|
|
76
|
-
}
|
|
77
|
-
calculateSideInfoLength() {
|
|
78
|
-
if (this.layer !== 3)
|
|
79
|
-
return 2;
|
|
80
|
-
if (this.channelModeIndex === 3) {
|
|
81
|
-
// mono
|
|
82
|
-
if (this.version === 1) {
|
|
83
|
-
return 17;
|
|
84
|
-
}
|
|
85
|
-
else if (this.version === 2 || this.version === 2.5) {
|
|
86
|
-
return 9;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
if (this.version === 1) {
|
|
91
|
-
return 32;
|
|
92
|
-
}
|
|
93
|
-
else if (this.version === 2 || this.version === 2.5) {
|
|
94
|
-
return 17;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
calcSlotSize() {
|
|
99
|
-
return [null, 4, 1, 1][this.layer];
|
|
100
|
-
}
|
|
101
|
-
parseMpegHeader(buf, off) {
|
|
102
|
-
this.container = 'MPEG';
|
|
103
|
-
// E(15,12): Bitrate index
|
|
104
|
-
this.bitrateIndex = common.getBitAllignedNumber(buf, off + 2, 0, 4);
|
|
105
|
-
// F(11,10): Sampling rate frequency index
|
|
106
|
-
this.sampRateFreqIndex = common.getBitAllignedNumber(buf, off + 2, 4, 2);
|
|
107
|
-
// G(9): Padding bit
|
|
108
|
-
this.padding = common.isBitSet(buf, off + 2, 6);
|
|
109
|
-
// H(8): Private bit
|
|
110
|
-
this.privateBit = common.isBitSet(buf, off + 2, 7);
|
|
111
|
-
// I(7,6): Channel Mode
|
|
112
|
-
this.channelModeIndex = common.getBitAllignedNumber(buf, off + 3, 0, 2);
|
|
113
|
-
// J(5,4): Mode extension (Only used in Joint stereo)
|
|
114
|
-
this.modeExtension = common.getBitAllignedNumber(buf, off + 3, 2, 2);
|
|
115
|
-
// K(3): Copyright
|
|
116
|
-
this.isCopyrighted = common.isBitSet(buf, off + 3, 4);
|
|
117
|
-
// L(2): Original
|
|
118
|
-
this.isOriginalMedia = common.isBitSet(buf, off + 3, 5);
|
|
119
|
-
// M(3): The original bit indicates, if it is set, that the frame is located on its original media.
|
|
120
|
-
this.emphasis = common.getBitAllignedNumber(buf, off + 3, 7, 2);
|
|
121
|
-
this.version = MpegFrameHeader.VersionID[this.versionIndex];
|
|
122
|
-
this.channelMode = MpegFrameHeader.ChannelMode[this.channelModeIndex];
|
|
123
|
-
this.codec = `MPEG ${this.version} Layer ${this.layer}`;
|
|
124
|
-
// Calculate bitrate
|
|
125
|
-
const bitrateInKbps = this.calcBitrate();
|
|
126
|
-
if (!bitrateInKbps) {
|
|
127
|
-
throw new Error('Cannot determine bit-rate');
|
|
128
|
-
}
|
|
129
|
-
this.bitrate = bitrateInKbps * 1000;
|
|
130
|
-
// Calculate sampling rate
|
|
131
|
-
this.samplingRate = this.calcSamplingRate();
|
|
132
|
-
if (this.samplingRate == null) {
|
|
133
|
-
throw new Error('Cannot determine sampling-rate');
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
parseAdtsHeader(buf, off) {
|
|
137
|
-
debug(`layer=0 => ADTS`);
|
|
138
|
-
this.version = this.versionIndex === 2 ? 4 : 2;
|
|
139
|
-
this.container = 'ADTS/MPEG-' + this.version;
|
|
140
|
-
const profileIndex = common.getBitAllignedNumber(buf, off + 2, 0, 2);
|
|
141
|
-
this.codec = 'AAC';
|
|
142
|
-
this.codecProfile = MPEG4.AudioObjectTypes[profileIndex];
|
|
143
|
-
debug(`MPEG-4 audio-codec=${this.codec}`);
|
|
144
|
-
const samplingFrequencyIndex = common.getBitAllignedNumber(buf, off + 2, 2, 4);
|
|
145
|
-
this.samplingRate = MPEG4.SamplingFrequencies[samplingFrequencyIndex];
|
|
146
|
-
debug(`sampling-rate=${this.samplingRate}`);
|
|
147
|
-
const channelIndex = common.getBitAllignedNumber(buf, off + 2, 7, 3);
|
|
148
|
-
this.mp4ChannelConfig = MPEG4_ChannelConfigurations[channelIndex];
|
|
149
|
-
debug(`channel-config=${this.mp4ChannelConfig.join('+')}`);
|
|
150
|
-
this.frameLength = common.getBitAllignedNumber(buf, off + 3, 6, 2) << 11;
|
|
151
|
-
}
|
|
152
|
-
calcBitrate() {
|
|
153
|
-
if (this.bitrateIndex === 0x00 || // free
|
|
154
|
-
this.bitrateIndex === 0x0F) { // reserved
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
const codecIndex = `${Math.floor(this.version)}${this.layer}`;
|
|
158
|
-
return MpegFrameHeader.bitrate_index[this.bitrateIndex][codecIndex];
|
|
159
|
-
}
|
|
160
|
-
calcSamplingRate() {
|
|
161
|
-
if (this.sampRateFreqIndex === 0x03)
|
|
162
|
-
return null; // 'reserved'
|
|
163
|
-
return MpegFrameHeader.sampling_rate_freq_index[this.version][this.sampRateFreqIndex];
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
MpegFrameHeader.SyncByte1 = 0xFF;
|
|
167
|
-
MpegFrameHeader.SyncByte2 = 0xE0;
|
|
168
|
-
MpegFrameHeader.VersionID = [2.5, null, 2, 1];
|
|
169
|
-
MpegFrameHeader.LayerDescription = [0, 3, 2, 1];
|
|
170
|
-
MpegFrameHeader.ChannelMode = ['stereo', 'joint_stereo', 'dual_channel', 'mono'];
|
|
171
|
-
MpegFrameHeader.bitrate_index = {
|
|
172
|
-
0x01: { 11: 32, 12: 32, 13: 32, 21: 32, 22: 8, 23: 8 },
|
|
173
|
-
0x02: { 11: 64, 12: 48, 13: 40, 21: 48, 22: 16, 23: 16 },
|
|
174
|
-
0x03: { 11: 96, 12: 56, 13: 48, 21: 56, 22: 24, 23: 24 },
|
|
175
|
-
0x04: { 11: 128, 12: 64, 13: 56, 21: 64, 22: 32, 23: 32 },
|
|
176
|
-
0x05: { 11: 160, 12: 80, 13: 64, 21: 80, 22: 40, 23: 40 },
|
|
177
|
-
0x06: { 11: 192, 12: 96, 13: 80, 21: 96, 22: 48, 23: 48 },
|
|
178
|
-
0x07: { 11: 224, 12: 112, 13: 96, 21: 112, 22: 56, 23: 56 },
|
|
179
|
-
0x08: { 11: 256, 12: 128, 13: 112, 21: 128, 22: 64, 23: 64 },
|
|
180
|
-
0x09: { 11: 288, 12: 160, 13: 128, 21: 144, 22: 80, 23: 80 },
|
|
181
|
-
0x0A: { 11: 320, 12: 192, 13: 160, 21: 160, 22: 96, 23: 96 },
|
|
182
|
-
0x0B: { 11: 352, 12: 224, 13: 192, 21: 176, 22: 112, 23: 112 },
|
|
183
|
-
0x0C: { 11: 384, 12: 256, 13: 224, 21: 192, 22: 128, 23: 128 },
|
|
184
|
-
0x0D: { 11: 416, 12: 320, 13: 256, 21: 224, 22: 144, 23: 144 },
|
|
185
|
-
0x0E: { 11: 448, 12: 384, 13: 320, 21: 256, 22: 160, 23: 160 }
|
|
186
|
-
};
|
|
187
|
-
MpegFrameHeader.sampling_rate_freq_index = {
|
|
188
|
-
1: { 0x00: 44100, 0x01: 48000, 0x02: 32000 },
|
|
189
|
-
2: { 0x00: 22050, 0x01: 24000, 0x02: 16000 },
|
|
190
|
-
2.5: { 0x00: 11025, 0x01: 12000, 0x02: 8000 }
|
|
191
|
-
};
|
|
192
|
-
MpegFrameHeader.samplesInFrameTable = [
|
|
193
|
-
/* Layer I II III */
|
|
194
|
-
[0, 384, 1152, 1152],
|
|
195
|
-
[0, 384, 1152, 576] // MPEG-2(.5
|
|
196
|
-
];
|
|
197
|
-
/**
|
|
198
|
-
* MPEG Audio Layer I/II/III
|
|
199
|
-
*/
|
|
200
|
-
const FrameHeader = {
|
|
201
|
-
len: 4,
|
|
202
|
-
get: (buf, off) => {
|
|
203
|
-
return new MpegFrameHeader(buf, off);
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
function getVbrCodecProfile(vbrScale) {
|
|
207
|
-
return 'V' + Math.floor((100 - vbrScale) / 10);
|
|
208
|
-
}
|
|
209
|
-
class MpegParser extends AbstractID3Parser_1.AbstractID3Parser {
|
|
210
|
-
constructor() {
|
|
211
|
-
super(...arguments);
|
|
212
|
-
this.frameCount = 0;
|
|
213
|
-
this.syncFrameCount = -1;
|
|
214
|
-
this.countSkipFrameData = 0;
|
|
215
|
-
this.totalDataLength = 0;
|
|
216
|
-
this.bitrates = [];
|
|
217
|
-
this.calculateEofDuration = false;
|
|
218
|
-
this.buf_frame_header = Buffer.alloc(4);
|
|
219
|
-
this.syncPeek = {
|
|
220
|
-
buf: Buffer.alloc(maxPeekLen),
|
|
221
|
-
len: 0
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Called after ID3 headers have been parsed
|
|
226
|
-
*/
|
|
227
|
-
async postId3v2Parse() {
|
|
228
|
-
this.metadata.setFormat('lossless', false);
|
|
229
|
-
try {
|
|
230
|
-
let quit = false;
|
|
231
|
-
while (!quit) {
|
|
232
|
-
await this.sync();
|
|
233
|
-
quit = await this.parseCommonMpegHeader();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
catch (err) {
|
|
237
|
-
if (err instanceof core_1.EndOfStreamError) {
|
|
238
|
-
debug(`End-of-stream`);
|
|
239
|
-
if (this.calculateEofDuration) {
|
|
240
|
-
const numberOfSamples = this.frameCount * this.samplesPerFrame;
|
|
241
|
-
this.metadata.setFormat('numberOfSamples', numberOfSamples);
|
|
242
|
-
const duration = numberOfSamples / this.metadata.format.sampleRate;
|
|
243
|
-
debug(`Calculate duration at EOF: ${duration} sec.`, duration);
|
|
244
|
-
this.metadata.setFormat('duration', duration);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
throw err;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Called after file has been fully parsed, this allows, if present, to exclude the ID3v1.1 header length
|
|
254
|
-
*/
|
|
255
|
-
finalize() {
|
|
256
|
-
const format = this.metadata.format;
|
|
257
|
-
const hasID3v1 = this.metadata.native.hasOwnProperty('ID3v1');
|
|
258
|
-
if (format.duration && this.tokenizer.fileInfo.size) {
|
|
259
|
-
const mpegSize = this.tokenizer.fileInfo.size - this.mpegOffset - (hasID3v1 ? 128 : 0);
|
|
260
|
-
if (format.codecProfile && format.codecProfile[0] === 'V') {
|
|
261
|
-
this.metadata.setFormat('bitrate', mpegSize * 8 / format.duration);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
else if (this.tokenizer.fileInfo.size && format.codecProfile === 'CBR') {
|
|
265
|
-
const mpegSize = this.tokenizer.fileInfo.size - this.mpegOffset - (hasID3v1 ? 128 : 0);
|
|
266
|
-
const numberOfSamples = Math.round(mpegSize / this.frame_size) * this.samplesPerFrame;
|
|
267
|
-
this.metadata.setFormat('numberOfSamples', numberOfSamples);
|
|
268
|
-
const duration = numberOfSamples / format.sampleRate;
|
|
269
|
-
debug("Calculate CBR duration based on file size: %s", duration);
|
|
270
|
-
this.metadata.setFormat('duration', duration);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
async sync() {
|
|
274
|
-
let gotFirstSync = false;
|
|
275
|
-
while (true) {
|
|
276
|
-
let bo = 0;
|
|
277
|
-
this.syncPeek.len = await this.tokenizer.peekBuffer(this.syncPeek.buf, { length: maxPeekLen, mayBeLess: true });
|
|
278
|
-
if (this.syncPeek.len <= 163) {
|
|
279
|
-
throw new core_1.EndOfStreamError();
|
|
280
|
-
}
|
|
281
|
-
while (true) {
|
|
282
|
-
if (gotFirstSync && (this.syncPeek.buf[bo] & 0xE0) === 0xE0) {
|
|
283
|
-
this.buf_frame_header[0] = MpegFrameHeader.SyncByte1;
|
|
284
|
-
this.buf_frame_header[1] = this.syncPeek.buf[bo];
|
|
285
|
-
await this.tokenizer.ignore(bo);
|
|
286
|
-
debug(`Sync at offset=${this.tokenizer.position - 1}, frameCount=${this.frameCount}`);
|
|
287
|
-
if (this.syncFrameCount === this.frameCount) {
|
|
288
|
-
debug(`Re-synced MPEG stream, frameCount=${this.frameCount}`);
|
|
289
|
-
this.frameCount = 0;
|
|
290
|
-
this.frame_size = 0;
|
|
291
|
-
}
|
|
292
|
-
this.syncFrameCount = this.frameCount;
|
|
293
|
-
return; // sync
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
gotFirstSync = false;
|
|
297
|
-
bo = this.syncPeek.buf.indexOf(MpegFrameHeader.SyncByte1, bo);
|
|
298
|
-
if (bo === -1) {
|
|
299
|
-
if (this.syncPeek.len < this.syncPeek.buf.length) {
|
|
300
|
-
throw new core_1.EndOfStreamError();
|
|
301
|
-
}
|
|
302
|
-
await this.tokenizer.ignore(this.syncPeek.len);
|
|
303
|
-
break; // continue with next buffer
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
++bo;
|
|
307
|
-
gotFirstSync = true;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Combined ADTS & MPEG (MP2 & MP3) header handling
|
|
315
|
-
* @return {Promise<boolean>} true if parser should quit
|
|
316
|
-
*/
|
|
317
|
-
async parseCommonMpegHeader() {
|
|
318
|
-
if (this.frameCount === 0) {
|
|
319
|
-
this.mpegOffset = this.tokenizer.position - 1;
|
|
320
|
-
}
|
|
321
|
-
await this.tokenizer.peekBuffer(this.buf_frame_header, { offset: 1, length: 3 });
|
|
322
|
-
let header;
|
|
323
|
-
try {
|
|
324
|
-
header = FrameHeader.get(this.buf_frame_header, 0);
|
|
325
|
-
}
|
|
326
|
-
catch (err) {
|
|
327
|
-
await this.tokenizer.ignore(1);
|
|
328
|
-
this.metadata.addWarning('Parse error: ' + err.message);
|
|
329
|
-
return false; // sync
|
|
330
|
-
}
|
|
331
|
-
await this.tokenizer.ignore(3);
|
|
332
|
-
this.metadata.setFormat('container', header.container);
|
|
333
|
-
this.metadata.setFormat('codec', header.codec);
|
|
334
|
-
this.metadata.setFormat('lossless', false);
|
|
335
|
-
this.metadata.setFormat('sampleRate', header.samplingRate);
|
|
336
|
-
this.frameCount++;
|
|
337
|
-
return header.version >= 2 && header.layer === 0 ? this.parseAdts(header) : this.parseAudioFrameHeader(header);
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* @return {Promise<boolean>} true if parser should quit
|
|
341
|
-
*/
|
|
342
|
-
async parseAudioFrameHeader(header) {
|
|
343
|
-
this.metadata.setFormat('numberOfChannels', header.channelMode === 'mono' ? 1 : 2);
|
|
344
|
-
this.metadata.setFormat('bitrate', header.bitrate);
|
|
345
|
-
if (this.frameCount < 20 * 10000) {
|
|
346
|
-
debug('offset=%s MP%s bitrate=%s sample-rate=%s', this.tokenizer.position - 4, header.layer, header.bitrate, header.samplingRate);
|
|
347
|
-
}
|
|
348
|
-
const slot_size = header.calcSlotSize();
|
|
349
|
-
if (slot_size === null) {
|
|
350
|
-
throw new Error('invalid slot_size');
|
|
351
|
-
}
|
|
352
|
-
const samples_per_frame = header.calcSamplesPerFrame();
|
|
353
|
-
debug(`samples_per_frame=${samples_per_frame}`);
|
|
354
|
-
const bps = samples_per_frame / 8.0;
|
|
355
|
-
const fsize = (bps * header.bitrate / header.samplingRate) +
|
|
356
|
-
((header.padding) ? slot_size : 0);
|
|
357
|
-
this.frame_size = Math.floor(fsize);
|
|
358
|
-
this.audioFrameHeader = header;
|
|
359
|
-
this.bitrates.push(header.bitrate);
|
|
360
|
-
// xtra header only exists in first frame
|
|
361
|
-
if (this.frameCount === 1) {
|
|
362
|
-
this.offset = FrameHeader.len;
|
|
363
|
-
await this.skipSideInformation();
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
if (this.frameCount === 3) {
|
|
367
|
-
// the stream is CBR if the first 3 frame bitrates are the same
|
|
368
|
-
if (this.areAllSame(this.bitrates)) {
|
|
369
|
-
// Actual calculation will be done in finalize
|
|
370
|
-
this.samplesPerFrame = samples_per_frame;
|
|
371
|
-
this.metadata.setFormat('codecProfile', 'CBR');
|
|
372
|
-
if (this.tokenizer.fileInfo.size)
|
|
373
|
-
return true; // Will calculate duration based on the file size
|
|
374
|
-
}
|
|
375
|
-
else if (this.metadata.format.duration) {
|
|
376
|
-
return true; // We already got the duration, stop processing MPEG stream any further
|
|
377
|
-
}
|
|
378
|
-
if (!this.options.duration) {
|
|
379
|
-
return true; // Enforce duration not enabled, stop processing entire stream
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
// once we know the file is VBR attach listener to end of
|
|
383
|
-
// stream so we can do the duration calculation when we
|
|
384
|
-
// have counted all the frames
|
|
385
|
-
if (this.options.duration && this.frameCount === 4) {
|
|
386
|
-
this.samplesPerFrame = samples_per_frame;
|
|
387
|
-
this.calculateEofDuration = true;
|
|
388
|
-
}
|
|
389
|
-
this.offset = 4;
|
|
390
|
-
if (header.isProtectedByCRC) {
|
|
391
|
-
await this.parseCrc();
|
|
392
|
-
return false;
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
await this.skipSideInformation();
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
async parseAdts(header) {
|
|
400
|
-
const buf = Buffer.alloc(3);
|
|
401
|
-
await this.tokenizer.readBuffer(buf);
|
|
402
|
-
header.frameLength += common.getBitAllignedNumber(buf, 0, 0, 11);
|
|
403
|
-
this.totalDataLength += header.frameLength;
|
|
404
|
-
this.samplesPerFrame = 1024;
|
|
405
|
-
const framesPerSec = header.samplingRate / this.samplesPerFrame;
|
|
406
|
-
const bytesPerFrame = this.frameCount === 0 ? 0 : this.totalDataLength / this.frameCount;
|
|
407
|
-
const bitrate = 8 * bytesPerFrame * framesPerSec + 0.5;
|
|
408
|
-
this.metadata.setFormat('bitrate', bitrate);
|
|
409
|
-
debug(`frame-count=${this.frameCount}, size=${header.frameLength} bytes, bit-rate=${bitrate}`);
|
|
410
|
-
await this.tokenizer.ignore(header.frameLength > 7 ? header.frameLength - 7 : 1);
|
|
411
|
-
// Consume remaining header and frame data
|
|
412
|
-
if (this.frameCount === 3) {
|
|
413
|
-
this.metadata.setFormat('codecProfile', header.codecProfile);
|
|
414
|
-
if (header.mp4ChannelConfig) {
|
|
415
|
-
this.metadata.setFormat('numberOfChannels', header.mp4ChannelConfig.length);
|
|
416
|
-
}
|
|
417
|
-
if (this.options.duration) {
|
|
418
|
-
this.calculateEofDuration = true;
|
|
419
|
-
}
|
|
420
|
-
else {
|
|
421
|
-
return true; // Stop parsing after the third frame
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return false;
|
|
425
|
-
}
|
|
426
|
-
async parseCrc() {
|
|
427
|
-
this.crc = await this.tokenizer.readNumber(Token.INT16_BE);
|
|
428
|
-
this.offset += 2;
|
|
429
|
-
return this.skipSideInformation();
|
|
430
|
-
}
|
|
431
|
-
async skipSideInformation() {
|
|
432
|
-
const sideinfo_length = this.audioFrameHeader.calculateSideInfoLength();
|
|
433
|
-
// side information
|
|
434
|
-
await this.tokenizer.readToken(new Token.Uint8ArrayType(sideinfo_length));
|
|
435
|
-
this.offset += sideinfo_length;
|
|
436
|
-
await this.readXtraInfoHeader();
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
async readXtraInfoHeader() {
|
|
440
|
-
const headerTag = await this.tokenizer.readToken(XingTag_1.InfoTagHeaderTag);
|
|
441
|
-
this.offset += XingTag_1.InfoTagHeaderTag.len; // 12
|
|
442
|
-
switch (headerTag) {
|
|
443
|
-
case 'Info':
|
|
444
|
-
this.metadata.setFormat('codecProfile', 'CBR');
|
|
445
|
-
return this.readXingInfoHeader();
|
|
446
|
-
case 'Xing':
|
|
447
|
-
const infoTag = await this.readXingInfoHeader();
|
|
448
|
-
const codecProfile = getVbrCodecProfile(infoTag.vbrScale);
|
|
449
|
-
this.metadata.setFormat('codecProfile', codecProfile);
|
|
450
|
-
return null;
|
|
451
|
-
case 'Xtra':
|
|
452
|
-
// ToDo: ???
|
|
453
|
-
break;
|
|
454
|
-
case 'LAME':
|
|
455
|
-
const version = await this.tokenizer.readToken(XingTag_1.LameEncoderVersion);
|
|
456
|
-
if (this.frame_size >= this.offset + XingTag_1.LameEncoderVersion.len) {
|
|
457
|
-
this.offset += XingTag_1.LameEncoderVersion.len;
|
|
458
|
-
this.metadata.setFormat('tool', 'LAME ' + version);
|
|
459
|
-
await this.skipFrameData(this.frame_size - this.offset);
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
else {
|
|
463
|
-
this.metadata.addWarning('Corrupt LAME header');
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
// ToDo: ???
|
|
467
|
-
}
|
|
468
|
-
// ToDo: promise duration???
|
|
469
|
-
const frameDataLeft = this.frame_size - this.offset;
|
|
470
|
-
if (frameDataLeft < 0) {
|
|
471
|
-
this.metadata.addWarning('Frame ' + this.frameCount + 'corrupt: negative frameDataLeft');
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
await this.skipFrameData(frameDataLeft);
|
|
475
|
-
}
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Ref: http://gabriel.mp3-tech.org/mp3infotag.html
|
|
480
|
-
* @returns {Promise<string>}
|
|
481
|
-
*/
|
|
482
|
-
async readXingInfoHeader() {
|
|
483
|
-
const offset = this.tokenizer.position;
|
|
484
|
-
const infoTag = await (0, XingTag_1.readXingHeader)(this.tokenizer);
|
|
485
|
-
this.offset += this.tokenizer.position - offset;
|
|
486
|
-
if (infoTag.lame) {
|
|
487
|
-
this.metadata.setFormat('tool', 'LAME ' + common.stripNulls(infoTag.lame.version));
|
|
488
|
-
if (infoTag.lame.extended) {
|
|
489
|
-
// this.metadata.setFormat('trackGain', infoTag.lame.extended.track_gain);
|
|
490
|
-
this.metadata.setFormat('trackPeakLevel', infoTag.lame.extended.track_peak);
|
|
491
|
-
if (infoTag.lame.extended.track_gain) {
|
|
492
|
-
this.metadata.setFormat('trackGain', infoTag.lame.extended.track_gain.adjustment);
|
|
493
|
-
}
|
|
494
|
-
if (infoTag.lame.extended.album_gain) {
|
|
495
|
-
this.metadata.setFormat('albumGain', infoTag.lame.extended.album_gain.adjustment);
|
|
496
|
-
}
|
|
497
|
-
this.metadata.setFormat('duration', infoTag.lame.extended.music_length / 1000);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
if (infoTag.streamSize) {
|
|
501
|
-
const duration = this.audioFrameHeader.calcDuration(infoTag.numFrames);
|
|
502
|
-
this.metadata.setFormat('duration', duration);
|
|
503
|
-
debug('Get duration from Xing header: %s', this.metadata.format.duration);
|
|
504
|
-
return infoTag;
|
|
505
|
-
}
|
|
506
|
-
// frames field is not present
|
|
507
|
-
const frameDataLeft = this.frame_size - this.offset;
|
|
508
|
-
await this.skipFrameData(frameDataLeft);
|
|
509
|
-
return infoTag;
|
|
510
|
-
}
|
|
511
|
-
async skipFrameData(frameDataLeft) {
|
|
512
|
-
if (frameDataLeft < 0)
|
|
513
|
-
throw new Error('frame-data-left cannot be negative');
|
|
514
|
-
await this.tokenizer.ignore(frameDataLeft);
|
|
515
|
-
this.countSkipFrameData += frameDataLeft;
|
|
516
|
-
}
|
|
517
|
-
areAllSame(array) {
|
|
518
|
-
const first = array[0];
|
|
519
|
-
return array.every(element => {
|
|
520
|
-
return element === first;
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
exports.MpegParser = MpegParser;
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MpegParser = void 0;
|
|
4
|
+
const Token = require("token-types");
|
|
5
|
+
const core_1 = require("strtok3/lib/core");
|
|
6
|
+
const debug_1 = require("debug");
|
|
7
|
+
const common = require("../common/Util");
|
|
8
|
+
const AbstractID3Parser_1 = require("../id3v2/AbstractID3Parser");
|
|
9
|
+
const XingTag_1 = require("./XingTag");
|
|
10
|
+
const debug = (0, debug_1.default)('music-metadata:parser:mpeg');
|
|
11
|
+
/**
|
|
12
|
+
* Cache buffer size used for searching synchronization preabmle
|
|
13
|
+
*/
|
|
14
|
+
const maxPeekLen = 1024;
|
|
15
|
+
/**
|
|
16
|
+
* MPEG-4 Audio definitions
|
|
17
|
+
* Ref: https://wiki.multimedia.cx/index.php/MPEG-4_Audio
|
|
18
|
+
*/
|
|
19
|
+
const MPEG4 = {
|
|
20
|
+
/**
|
|
21
|
+
* Audio Object Types
|
|
22
|
+
*/
|
|
23
|
+
AudioObjectTypes: [
|
|
24
|
+
'AAC Main',
|
|
25
|
+
'AAC LC',
|
|
26
|
+
'AAC SSR',
|
|
27
|
+
'AAC LTP' // Long Term Prediction
|
|
28
|
+
],
|
|
29
|
+
/**
|
|
30
|
+
* Sampling Frequencies
|
|
31
|
+
* https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Sampling_Frequencies
|
|
32
|
+
*/
|
|
33
|
+
SamplingFrequencies: [
|
|
34
|
+
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, undefined, undefined, -1
|
|
35
|
+
]
|
|
36
|
+
/**
|
|
37
|
+
* Channel Configurations
|
|
38
|
+
*/
|
|
39
|
+
};
|
|
40
|
+
const MPEG4_ChannelConfigurations = [
|
|
41
|
+
undefined,
|
|
42
|
+
['front-center'],
|
|
43
|
+
['front-left', 'front-right'],
|
|
44
|
+
['front-center', 'front-left', 'front-right'],
|
|
45
|
+
['front-center', 'front-left', 'front-right', 'back-center'],
|
|
46
|
+
['front-center', 'front-left', 'front-right', 'back-left', 'back-right'],
|
|
47
|
+
['front-center', 'front-left', 'front-right', 'back-left', 'back-right', 'LFE-channel'],
|
|
48
|
+
['front-center', 'front-left', 'front-right', 'side-left', 'side-right', 'back-left', 'back-right', 'LFE-channel']
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* MPEG Audio Layer I/II/III frame header
|
|
52
|
+
* Ref: https://www.mp3-tech.org/programmer/frame_header.html
|
|
53
|
+
* Bit layout: AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
|
|
54
|
+
* Ref: https://wiki.multimedia.cx/index.php/ADTS
|
|
55
|
+
*/
|
|
56
|
+
class MpegFrameHeader {
|
|
57
|
+
constructor(buf, off) {
|
|
58
|
+
// B(20,19): MPEG Audio versionIndex ID
|
|
59
|
+
this.versionIndex = common.getBitAllignedNumber(buf, off + 1, 3, 2);
|
|
60
|
+
// C(18,17): Layer description
|
|
61
|
+
this.layer = MpegFrameHeader.LayerDescription[common.getBitAllignedNumber(buf, off + 1, 5, 2)];
|
|
62
|
+
if (this.versionIndex > 1 && this.layer === 0) {
|
|
63
|
+
this.parseAdtsHeader(buf, off); // Audio Data Transport Stream (ADTS)
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.parseMpegHeader(buf, off); // Conventional MPEG header
|
|
67
|
+
}
|
|
68
|
+
// D(16): Protection bit (if true 16-bit CRC follows header)
|
|
69
|
+
this.isProtectedByCRC = !common.isBitSet(buf, off + 1, 7);
|
|
70
|
+
}
|
|
71
|
+
calcDuration(numFrames) {
|
|
72
|
+
return numFrames * this.calcSamplesPerFrame() / this.samplingRate;
|
|
73
|
+
}
|
|
74
|
+
calcSamplesPerFrame() {
|
|
75
|
+
return MpegFrameHeader.samplesInFrameTable[this.version === 1 ? 0 : 1][this.layer];
|
|
76
|
+
}
|
|
77
|
+
calculateSideInfoLength() {
|
|
78
|
+
if (this.layer !== 3)
|
|
79
|
+
return 2;
|
|
80
|
+
if (this.channelModeIndex === 3) {
|
|
81
|
+
// mono
|
|
82
|
+
if (this.version === 1) {
|
|
83
|
+
return 17;
|
|
84
|
+
}
|
|
85
|
+
else if (this.version === 2 || this.version === 2.5) {
|
|
86
|
+
return 9;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
if (this.version === 1) {
|
|
91
|
+
return 32;
|
|
92
|
+
}
|
|
93
|
+
else if (this.version === 2 || this.version === 2.5) {
|
|
94
|
+
return 17;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
calcSlotSize() {
|
|
99
|
+
return [null, 4, 1, 1][this.layer];
|
|
100
|
+
}
|
|
101
|
+
parseMpegHeader(buf, off) {
|
|
102
|
+
this.container = 'MPEG';
|
|
103
|
+
// E(15,12): Bitrate index
|
|
104
|
+
this.bitrateIndex = common.getBitAllignedNumber(buf, off + 2, 0, 4);
|
|
105
|
+
// F(11,10): Sampling rate frequency index
|
|
106
|
+
this.sampRateFreqIndex = common.getBitAllignedNumber(buf, off + 2, 4, 2);
|
|
107
|
+
// G(9): Padding bit
|
|
108
|
+
this.padding = common.isBitSet(buf, off + 2, 6);
|
|
109
|
+
// H(8): Private bit
|
|
110
|
+
this.privateBit = common.isBitSet(buf, off + 2, 7);
|
|
111
|
+
// I(7,6): Channel Mode
|
|
112
|
+
this.channelModeIndex = common.getBitAllignedNumber(buf, off + 3, 0, 2);
|
|
113
|
+
// J(5,4): Mode extension (Only used in Joint stereo)
|
|
114
|
+
this.modeExtension = common.getBitAllignedNumber(buf, off + 3, 2, 2);
|
|
115
|
+
// K(3): Copyright
|
|
116
|
+
this.isCopyrighted = common.isBitSet(buf, off + 3, 4);
|
|
117
|
+
// L(2): Original
|
|
118
|
+
this.isOriginalMedia = common.isBitSet(buf, off + 3, 5);
|
|
119
|
+
// M(3): The original bit indicates, if it is set, that the frame is located on its original media.
|
|
120
|
+
this.emphasis = common.getBitAllignedNumber(buf, off + 3, 7, 2);
|
|
121
|
+
this.version = MpegFrameHeader.VersionID[this.versionIndex];
|
|
122
|
+
this.channelMode = MpegFrameHeader.ChannelMode[this.channelModeIndex];
|
|
123
|
+
this.codec = `MPEG ${this.version} Layer ${this.layer}`;
|
|
124
|
+
// Calculate bitrate
|
|
125
|
+
const bitrateInKbps = this.calcBitrate();
|
|
126
|
+
if (!bitrateInKbps) {
|
|
127
|
+
throw new Error('Cannot determine bit-rate');
|
|
128
|
+
}
|
|
129
|
+
this.bitrate = bitrateInKbps * 1000;
|
|
130
|
+
// Calculate sampling rate
|
|
131
|
+
this.samplingRate = this.calcSamplingRate();
|
|
132
|
+
if (this.samplingRate == null) {
|
|
133
|
+
throw new Error('Cannot determine sampling-rate');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
parseAdtsHeader(buf, off) {
|
|
137
|
+
debug(`layer=0 => ADTS`);
|
|
138
|
+
this.version = this.versionIndex === 2 ? 4 : 2;
|
|
139
|
+
this.container = 'ADTS/MPEG-' + this.version;
|
|
140
|
+
const profileIndex = common.getBitAllignedNumber(buf, off + 2, 0, 2);
|
|
141
|
+
this.codec = 'AAC';
|
|
142
|
+
this.codecProfile = MPEG4.AudioObjectTypes[profileIndex];
|
|
143
|
+
debug(`MPEG-4 audio-codec=${this.codec}`);
|
|
144
|
+
const samplingFrequencyIndex = common.getBitAllignedNumber(buf, off + 2, 2, 4);
|
|
145
|
+
this.samplingRate = MPEG4.SamplingFrequencies[samplingFrequencyIndex];
|
|
146
|
+
debug(`sampling-rate=${this.samplingRate}`);
|
|
147
|
+
const channelIndex = common.getBitAllignedNumber(buf, off + 2, 7, 3);
|
|
148
|
+
this.mp4ChannelConfig = MPEG4_ChannelConfigurations[channelIndex];
|
|
149
|
+
debug(`channel-config=${this.mp4ChannelConfig.join('+')}`);
|
|
150
|
+
this.frameLength = common.getBitAllignedNumber(buf, off + 3, 6, 2) << 11;
|
|
151
|
+
}
|
|
152
|
+
calcBitrate() {
|
|
153
|
+
if (this.bitrateIndex === 0x00 || // free
|
|
154
|
+
this.bitrateIndex === 0x0F) { // reserved
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const codecIndex = `${Math.floor(this.version)}${this.layer}`;
|
|
158
|
+
return MpegFrameHeader.bitrate_index[this.bitrateIndex][codecIndex];
|
|
159
|
+
}
|
|
160
|
+
calcSamplingRate() {
|
|
161
|
+
if (this.sampRateFreqIndex === 0x03)
|
|
162
|
+
return null; // 'reserved'
|
|
163
|
+
return MpegFrameHeader.sampling_rate_freq_index[this.version][this.sampRateFreqIndex];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
MpegFrameHeader.SyncByte1 = 0xFF;
|
|
167
|
+
MpegFrameHeader.SyncByte2 = 0xE0;
|
|
168
|
+
MpegFrameHeader.VersionID = [2.5, null, 2, 1];
|
|
169
|
+
MpegFrameHeader.LayerDescription = [0, 3, 2, 1];
|
|
170
|
+
MpegFrameHeader.ChannelMode = ['stereo', 'joint_stereo', 'dual_channel', 'mono'];
|
|
171
|
+
MpegFrameHeader.bitrate_index = {
|
|
172
|
+
0x01: { 11: 32, 12: 32, 13: 32, 21: 32, 22: 8, 23: 8 },
|
|
173
|
+
0x02: { 11: 64, 12: 48, 13: 40, 21: 48, 22: 16, 23: 16 },
|
|
174
|
+
0x03: { 11: 96, 12: 56, 13: 48, 21: 56, 22: 24, 23: 24 },
|
|
175
|
+
0x04: { 11: 128, 12: 64, 13: 56, 21: 64, 22: 32, 23: 32 },
|
|
176
|
+
0x05: { 11: 160, 12: 80, 13: 64, 21: 80, 22: 40, 23: 40 },
|
|
177
|
+
0x06: { 11: 192, 12: 96, 13: 80, 21: 96, 22: 48, 23: 48 },
|
|
178
|
+
0x07: { 11: 224, 12: 112, 13: 96, 21: 112, 22: 56, 23: 56 },
|
|
179
|
+
0x08: { 11: 256, 12: 128, 13: 112, 21: 128, 22: 64, 23: 64 },
|
|
180
|
+
0x09: { 11: 288, 12: 160, 13: 128, 21: 144, 22: 80, 23: 80 },
|
|
181
|
+
0x0A: { 11: 320, 12: 192, 13: 160, 21: 160, 22: 96, 23: 96 },
|
|
182
|
+
0x0B: { 11: 352, 12: 224, 13: 192, 21: 176, 22: 112, 23: 112 },
|
|
183
|
+
0x0C: { 11: 384, 12: 256, 13: 224, 21: 192, 22: 128, 23: 128 },
|
|
184
|
+
0x0D: { 11: 416, 12: 320, 13: 256, 21: 224, 22: 144, 23: 144 },
|
|
185
|
+
0x0E: { 11: 448, 12: 384, 13: 320, 21: 256, 22: 160, 23: 160 }
|
|
186
|
+
};
|
|
187
|
+
MpegFrameHeader.sampling_rate_freq_index = {
|
|
188
|
+
1: { 0x00: 44100, 0x01: 48000, 0x02: 32000 },
|
|
189
|
+
2: { 0x00: 22050, 0x01: 24000, 0x02: 16000 },
|
|
190
|
+
2.5: { 0x00: 11025, 0x01: 12000, 0x02: 8000 }
|
|
191
|
+
};
|
|
192
|
+
MpegFrameHeader.samplesInFrameTable = [
|
|
193
|
+
/* Layer I II III */
|
|
194
|
+
[0, 384, 1152, 1152],
|
|
195
|
+
[0, 384, 1152, 576] // MPEG-2(.5
|
|
196
|
+
];
|
|
197
|
+
/**
|
|
198
|
+
* MPEG Audio Layer I/II/III
|
|
199
|
+
*/
|
|
200
|
+
const FrameHeader = {
|
|
201
|
+
len: 4,
|
|
202
|
+
get: (buf, off) => {
|
|
203
|
+
return new MpegFrameHeader(buf, off);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
function getVbrCodecProfile(vbrScale) {
|
|
207
|
+
return 'V' + Math.floor((100 - vbrScale) / 10);
|
|
208
|
+
}
|
|
209
|
+
class MpegParser extends AbstractID3Parser_1.AbstractID3Parser {
|
|
210
|
+
constructor() {
|
|
211
|
+
super(...arguments);
|
|
212
|
+
this.frameCount = 0;
|
|
213
|
+
this.syncFrameCount = -1;
|
|
214
|
+
this.countSkipFrameData = 0;
|
|
215
|
+
this.totalDataLength = 0;
|
|
216
|
+
this.bitrates = [];
|
|
217
|
+
this.calculateEofDuration = false;
|
|
218
|
+
this.buf_frame_header = Buffer.alloc(4);
|
|
219
|
+
this.syncPeek = {
|
|
220
|
+
buf: Buffer.alloc(maxPeekLen),
|
|
221
|
+
len: 0
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Called after ID3 headers have been parsed
|
|
226
|
+
*/
|
|
227
|
+
async postId3v2Parse() {
|
|
228
|
+
this.metadata.setFormat('lossless', false);
|
|
229
|
+
try {
|
|
230
|
+
let quit = false;
|
|
231
|
+
while (!quit) {
|
|
232
|
+
await this.sync();
|
|
233
|
+
quit = await this.parseCommonMpegHeader();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
if (err instanceof core_1.EndOfStreamError) {
|
|
238
|
+
debug(`End-of-stream`);
|
|
239
|
+
if (this.calculateEofDuration) {
|
|
240
|
+
const numberOfSamples = this.frameCount * this.samplesPerFrame;
|
|
241
|
+
this.metadata.setFormat('numberOfSamples', numberOfSamples);
|
|
242
|
+
const duration = numberOfSamples / this.metadata.format.sampleRate;
|
|
243
|
+
debug(`Calculate duration at EOF: ${duration} sec.`, duration);
|
|
244
|
+
this.metadata.setFormat('duration', duration);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Called after file has been fully parsed, this allows, if present, to exclude the ID3v1.1 header length
|
|
254
|
+
*/
|
|
255
|
+
finalize() {
|
|
256
|
+
const format = this.metadata.format;
|
|
257
|
+
const hasID3v1 = this.metadata.native.hasOwnProperty('ID3v1');
|
|
258
|
+
if (format.duration && this.tokenizer.fileInfo.size) {
|
|
259
|
+
const mpegSize = this.tokenizer.fileInfo.size - this.mpegOffset - (hasID3v1 ? 128 : 0);
|
|
260
|
+
if (format.codecProfile && format.codecProfile[0] === 'V') {
|
|
261
|
+
this.metadata.setFormat('bitrate', mpegSize * 8 / format.duration);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (this.tokenizer.fileInfo.size && format.codecProfile === 'CBR') {
|
|
265
|
+
const mpegSize = this.tokenizer.fileInfo.size - this.mpegOffset - (hasID3v1 ? 128 : 0);
|
|
266
|
+
const numberOfSamples = Math.round(mpegSize / this.frame_size) * this.samplesPerFrame;
|
|
267
|
+
this.metadata.setFormat('numberOfSamples', numberOfSamples);
|
|
268
|
+
const duration = numberOfSamples / format.sampleRate;
|
|
269
|
+
debug("Calculate CBR duration based on file size: %s", duration);
|
|
270
|
+
this.metadata.setFormat('duration', duration);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async sync() {
|
|
274
|
+
let gotFirstSync = false;
|
|
275
|
+
while (true) {
|
|
276
|
+
let bo = 0;
|
|
277
|
+
this.syncPeek.len = await this.tokenizer.peekBuffer(this.syncPeek.buf, { length: maxPeekLen, mayBeLess: true });
|
|
278
|
+
if (this.syncPeek.len <= 163) {
|
|
279
|
+
throw new core_1.EndOfStreamError();
|
|
280
|
+
}
|
|
281
|
+
while (true) {
|
|
282
|
+
if (gotFirstSync && (this.syncPeek.buf[bo] & 0xE0) === 0xE0) {
|
|
283
|
+
this.buf_frame_header[0] = MpegFrameHeader.SyncByte1;
|
|
284
|
+
this.buf_frame_header[1] = this.syncPeek.buf[bo];
|
|
285
|
+
await this.tokenizer.ignore(bo);
|
|
286
|
+
debug(`Sync at offset=${this.tokenizer.position - 1}, frameCount=${this.frameCount}`);
|
|
287
|
+
if (this.syncFrameCount === this.frameCount) {
|
|
288
|
+
debug(`Re-synced MPEG stream, frameCount=${this.frameCount}`);
|
|
289
|
+
this.frameCount = 0;
|
|
290
|
+
this.frame_size = 0;
|
|
291
|
+
}
|
|
292
|
+
this.syncFrameCount = this.frameCount;
|
|
293
|
+
return; // sync
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
gotFirstSync = false;
|
|
297
|
+
bo = this.syncPeek.buf.indexOf(MpegFrameHeader.SyncByte1, bo);
|
|
298
|
+
if (bo === -1) {
|
|
299
|
+
if (this.syncPeek.len < this.syncPeek.buf.length) {
|
|
300
|
+
throw new core_1.EndOfStreamError();
|
|
301
|
+
}
|
|
302
|
+
await this.tokenizer.ignore(this.syncPeek.len);
|
|
303
|
+
break; // continue with next buffer
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
++bo;
|
|
307
|
+
gotFirstSync = true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Combined ADTS & MPEG (MP2 & MP3) header handling
|
|
315
|
+
* @return {Promise<boolean>} true if parser should quit
|
|
316
|
+
*/
|
|
317
|
+
async parseCommonMpegHeader() {
|
|
318
|
+
if (this.frameCount === 0) {
|
|
319
|
+
this.mpegOffset = this.tokenizer.position - 1;
|
|
320
|
+
}
|
|
321
|
+
await this.tokenizer.peekBuffer(this.buf_frame_header, { offset: 1, length: 3 });
|
|
322
|
+
let header;
|
|
323
|
+
try {
|
|
324
|
+
header = FrameHeader.get(this.buf_frame_header, 0);
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
await this.tokenizer.ignore(1);
|
|
328
|
+
this.metadata.addWarning('Parse error: ' + err.message);
|
|
329
|
+
return false; // sync
|
|
330
|
+
}
|
|
331
|
+
await this.tokenizer.ignore(3);
|
|
332
|
+
this.metadata.setFormat('container', header.container);
|
|
333
|
+
this.metadata.setFormat('codec', header.codec);
|
|
334
|
+
this.metadata.setFormat('lossless', false);
|
|
335
|
+
this.metadata.setFormat('sampleRate', header.samplingRate);
|
|
336
|
+
this.frameCount++;
|
|
337
|
+
return header.version >= 2 && header.layer === 0 ? this.parseAdts(header) : this.parseAudioFrameHeader(header);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* @return {Promise<boolean>} true if parser should quit
|
|
341
|
+
*/
|
|
342
|
+
async parseAudioFrameHeader(header) {
|
|
343
|
+
this.metadata.setFormat('numberOfChannels', header.channelMode === 'mono' ? 1 : 2);
|
|
344
|
+
this.metadata.setFormat('bitrate', header.bitrate);
|
|
345
|
+
if (this.frameCount < 20 * 10000) {
|
|
346
|
+
debug('offset=%s MP%s bitrate=%s sample-rate=%s', this.tokenizer.position - 4, header.layer, header.bitrate, header.samplingRate);
|
|
347
|
+
}
|
|
348
|
+
const slot_size = header.calcSlotSize();
|
|
349
|
+
if (slot_size === null) {
|
|
350
|
+
throw new Error('invalid slot_size');
|
|
351
|
+
}
|
|
352
|
+
const samples_per_frame = header.calcSamplesPerFrame();
|
|
353
|
+
debug(`samples_per_frame=${samples_per_frame}`);
|
|
354
|
+
const bps = samples_per_frame / 8.0;
|
|
355
|
+
const fsize = (bps * header.bitrate / header.samplingRate) +
|
|
356
|
+
((header.padding) ? slot_size : 0);
|
|
357
|
+
this.frame_size = Math.floor(fsize);
|
|
358
|
+
this.audioFrameHeader = header;
|
|
359
|
+
this.bitrates.push(header.bitrate);
|
|
360
|
+
// xtra header only exists in first frame
|
|
361
|
+
if (this.frameCount === 1) {
|
|
362
|
+
this.offset = FrameHeader.len;
|
|
363
|
+
await this.skipSideInformation();
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
if (this.frameCount === 3) {
|
|
367
|
+
// the stream is CBR if the first 3 frame bitrates are the same
|
|
368
|
+
if (this.areAllSame(this.bitrates)) {
|
|
369
|
+
// Actual calculation will be done in finalize
|
|
370
|
+
this.samplesPerFrame = samples_per_frame;
|
|
371
|
+
this.metadata.setFormat('codecProfile', 'CBR');
|
|
372
|
+
if (this.tokenizer.fileInfo.size)
|
|
373
|
+
return true; // Will calculate duration based on the file size
|
|
374
|
+
}
|
|
375
|
+
else if (this.metadata.format.duration) {
|
|
376
|
+
return true; // We already got the duration, stop processing MPEG stream any further
|
|
377
|
+
}
|
|
378
|
+
if (!this.options.duration) {
|
|
379
|
+
return true; // Enforce duration not enabled, stop processing entire stream
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// once we know the file is VBR attach listener to end of
|
|
383
|
+
// stream so we can do the duration calculation when we
|
|
384
|
+
// have counted all the frames
|
|
385
|
+
if (this.options.duration && this.frameCount === 4) {
|
|
386
|
+
this.samplesPerFrame = samples_per_frame;
|
|
387
|
+
this.calculateEofDuration = true;
|
|
388
|
+
}
|
|
389
|
+
this.offset = 4;
|
|
390
|
+
if (header.isProtectedByCRC) {
|
|
391
|
+
await this.parseCrc();
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
await this.skipSideInformation();
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async parseAdts(header) {
|
|
400
|
+
const buf = Buffer.alloc(3);
|
|
401
|
+
await this.tokenizer.readBuffer(buf);
|
|
402
|
+
header.frameLength += common.getBitAllignedNumber(buf, 0, 0, 11);
|
|
403
|
+
this.totalDataLength += header.frameLength;
|
|
404
|
+
this.samplesPerFrame = 1024;
|
|
405
|
+
const framesPerSec = header.samplingRate / this.samplesPerFrame;
|
|
406
|
+
const bytesPerFrame = this.frameCount === 0 ? 0 : this.totalDataLength / this.frameCount;
|
|
407
|
+
const bitrate = 8 * bytesPerFrame * framesPerSec + 0.5;
|
|
408
|
+
this.metadata.setFormat('bitrate', bitrate);
|
|
409
|
+
debug(`frame-count=${this.frameCount}, size=${header.frameLength} bytes, bit-rate=${bitrate}`);
|
|
410
|
+
await this.tokenizer.ignore(header.frameLength > 7 ? header.frameLength - 7 : 1);
|
|
411
|
+
// Consume remaining header and frame data
|
|
412
|
+
if (this.frameCount === 3) {
|
|
413
|
+
this.metadata.setFormat('codecProfile', header.codecProfile);
|
|
414
|
+
if (header.mp4ChannelConfig) {
|
|
415
|
+
this.metadata.setFormat('numberOfChannels', header.mp4ChannelConfig.length);
|
|
416
|
+
}
|
|
417
|
+
if (this.options.duration) {
|
|
418
|
+
this.calculateEofDuration = true;
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
return true; // Stop parsing after the third frame
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
async parseCrc() {
|
|
427
|
+
this.crc = await this.tokenizer.readNumber(Token.INT16_BE);
|
|
428
|
+
this.offset += 2;
|
|
429
|
+
return this.skipSideInformation();
|
|
430
|
+
}
|
|
431
|
+
async skipSideInformation() {
|
|
432
|
+
const sideinfo_length = this.audioFrameHeader.calculateSideInfoLength();
|
|
433
|
+
// side information
|
|
434
|
+
await this.tokenizer.readToken(new Token.Uint8ArrayType(sideinfo_length));
|
|
435
|
+
this.offset += sideinfo_length;
|
|
436
|
+
await this.readXtraInfoHeader();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
async readXtraInfoHeader() {
|
|
440
|
+
const headerTag = await this.tokenizer.readToken(XingTag_1.InfoTagHeaderTag);
|
|
441
|
+
this.offset += XingTag_1.InfoTagHeaderTag.len; // 12
|
|
442
|
+
switch (headerTag) {
|
|
443
|
+
case 'Info':
|
|
444
|
+
this.metadata.setFormat('codecProfile', 'CBR');
|
|
445
|
+
return this.readXingInfoHeader();
|
|
446
|
+
case 'Xing':
|
|
447
|
+
const infoTag = await this.readXingInfoHeader();
|
|
448
|
+
const codecProfile = getVbrCodecProfile(infoTag.vbrScale);
|
|
449
|
+
this.metadata.setFormat('codecProfile', codecProfile);
|
|
450
|
+
return null;
|
|
451
|
+
case 'Xtra':
|
|
452
|
+
// ToDo: ???
|
|
453
|
+
break;
|
|
454
|
+
case 'LAME':
|
|
455
|
+
const version = await this.tokenizer.readToken(XingTag_1.LameEncoderVersion);
|
|
456
|
+
if (this.frame_size >= this.offset + XingTag_1.LameEncoderVersion.len) {
|
|
457
|
+
this.offset += XingTag_1.LameEncoderVersion.len;
|
|
458
|
+
this.metadata.setFormat('tool', 'LAME ' + version);
|
|
459
|
+
await this.skipFrameData(this.frame_size - this.offset);
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
this.metadata.addWarning('Corrupt LAME header');
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
// ToDo: ???
|
|
467
|
+
}
|
|
468
|
+
// ToDo: promise duration???
|
|
469
|
+
const frameDataLeft = this.frame_size - this.offset;
|
|
470
|
+
if (frameDataLeft < 0) {
|
|
471
|
+
this.metadata.addWarning('Frame ' + this.frameCount + 'corrupt: negative frameDataLeft');
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
await this.skipFrameData(frameDataLeft);
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Ref: http://gabriel.mp3-tech.org/mp3infotag.html
|
|
480
|
+
* @returns {Promise<string>}
|
|
481
|
+
*/
|
|
482
|
+
async readXingInfoHeader() {
|
|
483
|
+
const offset = this.tokenizer.position;
|
|
484
|
+
const infoTag = await (0, XingTag_1.readXingHeader)(this.tokenizer);
|
|
485
|
+
this.offset += this.tokenizer.position - offset;
|
|
486
|
+
if (infoTag.lame) {
|
|
487
|
+
this.metadata.setFormat('tool', 'LAME ' + common.stripNulls(infoTag.lame.version));
|
|
488
|
+
if (infoTag.lame.extended) {
|
|
489
|
+
// this.metadata.setFormat('trackGain', infoTag.lame.extended.track_gain);
|
|
490
|
+
this.metadata.setFormat('trackPeakLevel', infoTag.lame.extended.track_peak);
|
|
491
|
+
if (infoTag.lame.extended.track_gain) {
|
|
492
|
+
this.metadata.setFormat('trackGain', infoTag.lame.extended.track_gain.adjustment);
|
|
493
|
+
}
|
|
494
|
+
if (infoTag.lame.extended.album_gain) {
|
|
495
|
+
this.metadata.setFormat('albumGain', infoTag.lame.extended.album_gain.adjustment);
|
|
496
|
+
}
|
|
497
|
+
this.metadata.setFormat('duration', infoTag.lame.extended.music_length / 1000);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (infoTag.streamSize) {
|
|
501
|
+
const duration = this.audioFrameHeader.calcDuration(infoTag.numFrames);
|
|
502
|
+
this.metadata.setFormat('duration', duration);
|
|
503
|
+
debug('Get duration from Xing header: %s', this.metadata.format.duration);
|
|
504
|
+
return infoTag;
|
|
505
|
+
}
|
|
506
|
+
// frames field is not present
|
|
507
|
+
const frameDataLeft = this.frame_size - this.offset;
|
|
508
|
+
await this.skipFrameData(frameDataLeft);
|
|
509
|
+
return infoTag;
|
|
510
|
+
}
|
|
511
|
+
async skipFrameData(frameDataLeft) {
|
|
512
|
+
if (frameDataLeft < 0)
|
|
513
|
+
throw new Error('frame-data-left cannot be negative');
|
|
514
|
+
await this.tokenizer.ignore(frameDataLeft);
|
|
515
|
+
this.countSkipFrameData += frameDataLeft;
|
|
516
|
+
}
|
|
517
|
+
areAllSame(array) {
|
|
518
|
+
const first = array[0];
|
|
519
|
+
return array.every(element => {
|
|
520
|
+
return element === first;
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
exports.MpegParser = MpegParser;
|