music-metadata 7.11.1 → 7.11.5

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.
Files changed (152) hide show
  1. package/README.md +432 -432
  2. package/lib/ParserFactory.d.ts +49 -49
  3. package/lib/ParserFactory.js +251 -251
  4. package/lib/aiff/AiffParser.d.ts +15 -15
  5. package/lib/aiff/AiffParser.js +85 -85
  6. package/lib/aiff/AiffToken.d.ts +22 -22
  7. package/lib/aiff/AiffToken.js +43 -43
  8. package/lib/apev2/APEv2Parser.d.ts +30 -30
  9. package/lib/apev2/APEv2Parser.js +162 -162
  10. package/lib/apev2/APEv2TagMapper.d.ts +4 -4
  11. package/lib/apev2/APEv2TagMapper.js +87 -86
  12. package/lib/apev2/APEv2Token.d.ts +100 -100
  13. package/lib/apev2/APEv2Token.js +127 -126
  14. package/lib/asf/AsfObject.d.ts +319 -319
  15. package/lib/asf/AsfObject.js +384 -384
  16. package/lib/asf/AsfParser.d.ts +17 -17
  17. package/lib/asf/AsfParser.js +135 -135
  18. package/lib/asf/AsfTagMapper.d.ts +7 -7
  19. package/lib/asf/AsfTagMapper.js +96 -95
  20. package/lib/asf/AsfUtil.d.ts +13 -13
  21. package/lib/asf/AsfUtil.js +40 -40
  22. package/lib/asf/GUID.d.ts +86 -86
  23. package/lib/asf/GUID.js +123 -123
  24. package/lib/common/BasicParser.d.ts +17 -17
  25. package/lib/common/BasicParser.js +18 -18
  26. package/lib/common/CaseInsensitiveTagMap.d.ts +10 -10
  27. package/lib/common/CaseInsensitiveTagMap.js +22 -21
  28. package/lib/common/CombinedTagMapper.d.ts +19 -19
  29. package/lib/common/CombinedTagMapper.js +52 -51
  30. package/lib/common/FourCC.d.ts +6 -6
  31. package/lib/common/FourCC.js +29 -28
  32. package/lib/common/GenericTagMapper.d.ts +51 -51
  33. package/lib/common/GenericTagMapper.js +56 -55
  34. package/lib/common/GenericTagTypes.d.ts +33 -33
  35. package/lib/common/GenericTagTypes.js +132 -131
  36. package/lib/common/MetadataCollector.d.ts +76 -76
  37. package/lib/common/MetadataCollector.js +276 -274
  38. package/lib/common/RandomFileReader.d.ts +20 -20
  39. package/lib/common/RandomFileReader.js +37 -37
  40. package/lib/common/RandomUint8ArrayReader.d.ts +18 -18
  41. package/lib/common/RandomUint8ArrayReader.js +25 -25
  42. package/lib/common/Util.d.ts +57 -57
  43. package/lib/common/Util.js +170 -169
  44. package/lib/core.d.ts +48 -48
  45. package/lib/core.js +90 -90
  46. package/lib/dsdiff/DsdiffParser.d.ts +14 -14
  47. package/lib/dsdiff/DsdiffParser.js +143 -143
  48. package/lib/dsdiff/DsdiffToken.d.ts +9 -9
  49. package/lib/dsdiff/DsdiffToken.js +21 -21
  50. package/lib/dsf/DsfChunk.d.ts +86 -86
  51. package/lib/dsf/DsfChunk.js +54 -54
  52. package/lib/dsf/DsfParser.d.ts +9 -9
  53. package/lib/dsf/DsfParser.js +56 -56
  54. package/lib/flac/FlacParser.d.ts +28 -28
  55. package/lib/flac/FlacParser.js +175 -175
  56. package/lib/id3v1/ID3v1Parser.d.ts +13 -13
  57. package/lib/id3v1/ID3v1Parser.js +134 -134
  58. package/lib/id3v1/ID3v1TagMap.d.ts +4 -4
  59. package/lib/id3v1/ID3v1TagMap.js +23 -22
  60. package/lib/id3v2/AbstractID3Parser.d.ts +17 -17
  61. package/lib/id3v2/AbstractID3Parser.js +60 -60
  62. package/lib/id3v2/FrameParser.d.ts +32 -32
  63. package/lib/id3v2/FrameParser.js +323 -323
  64. package/lib/id3v2/ID3v22TagMapper.d.ts +9 -9
  65. package/lib/id3v2/ID3v22TagMapper.js +56 -54
  66. package/lib/id3v2/ID3v24TagMapper.d.ts +14 -14
  67. package/lib/id3v2/ID3v24TagMapper.js +194 -193
  68. package/lib/id3v2/ID3v2Parser.d.ts +29 -29
  69. package/lib/id3v2/ID3v2Parser.js +194 -194
  70. package/lib/id3v2/ID3v2Token.d.ts +73 -73
  71. package/lib/id3v2/ID3v2Token.js +106 -106
  72. package/lib/iff/index.d.ts +33 -33
  73. package/lib/iff/index.js +19 -19
  74. package/lib/index.d.ts +34 -34
  75. package/lib/index.js +64 -64
  76. package/lib/lyrics3/Lyrics3.d.ts +3 -3
  77. package/lib/lyrics3/Lyrics3.js +17 -17
  78. package/lib/matroska/MatroskaDtd.d.ts +8 -8
  79. package/lib/matroska/MatroskaDtd.js +279 -279
  80. package/lib/matroska/MatroskaParser.d.ts +37 -37
  81. package/lib/matroska/MatroskaParser.js +235 -234
  82. package/lib/matroska/MatroskaTagMapper.d.ts +4 -4
  83. package/lib/matroska/MatroskaTagMapper.js +36 -35
  84. package/lib/matroska/types.d.ts +175 -175
  85. package/lib/matroska/types.js +32 -32
  86. package/lib/mp4/Atom.d.ts +16 -16
  87. package/lib/mp4/Atom.js +70 -70
  88. package/lib/mp4/AtomToken.d.ts +395 -395
  89. package/lib/mp4/AtomToken.js +406 -406
  90. package/lib/mp4/MP4Parser.d.ts +30 -31
  91. package/lib/mp4/MP4Parser.js +511 -515
  92. package/lib/mp4/MP4TagMapper.d.ts +5 -5
  93. package/lib/mp4/MP4TagMapper.js +116 -115
  94. package/lib/mpeg/ExtendedLameHeader.d.ts +27 -27
  95. package/lib/mpeg/ExtendedLameHeader.js +31 -31
  96. package/lib/mpeg/MpegParser.d.ts +49 -49
  97. package/lib/mpeg/MpegParser.js +529 -529
  98. package/lib/mpeg/ReplayGainDataFormat.d.ts +55 -55
  99. package/lib/mpeg/ReplayGainDataFormat.js +69 -69
  100. package/lib/mpeg/XingTag.d.ts +45 -45
  101. package/lib/mpeg/XingTag.js +69 -69
  102. package/lib/musepack/index.d.ts +5 -5
  103. package/lib/musepack/index.js +32 -32
  104. package/lib/musepack/sv7/BitReader.d.ts +13 -13
  105. package/lib/musepack/sv7/BitReader.js +54 -54
  106. package/lib/musepack/sv7/MpcSv7Parser.d.ts +8 -8
  107. package/lib/musepack/sv7/MpcSv7Parser.js +46 -46
  108. package/lib/musepack/sv7/StreamVersion7.d.ts +28 -28
  109. package/lib/musepack/sv7/StreamVersion7.js +41 -41
  110. package/lib/musepack/sv8/MpcSv8Parser.d.ts +6 -6
  111. package/lib/musepack/sv8/MpcSv8Parser.js +55 -55
  112. package/lib/musepack/sv8/StreamVersion8.d.ts +40 -40
  113. package/lib/musepack/sv8/StreamVersion8.js +80 -80
  114. package/lib/ogg/Ogg.d.ts +72 -72
  115. package/lib/ogg/Ogg.js +2 -2
  116. package/lib/ogg/OggParser.d.ts +23 -23
  117. package/lib/ogg/OggParser.js +126 -126
  118. package/lib/ogg/opus/Opus.d.ts +48 -48
  119. package/lib/ogg/opus/Opus.js +28 -28
  120. package/lib/ogg/opus/OpusParser.d.ts +25 -25
  121. package/lib/ogg/opus/OpusParser.js +56 -56
  122. package/lib/ogg/speex/Speex.d.ts +36 -36
  123. package/lib/ogg/speex/Speex.js +31 -31
  124. package/lib/ogg/speex/SpeexParser.d.ts +22 -22
  125. package/lib/ogg/speex/SpeexParser.js +35 -35
  126. package/lib/ogg/theora/Theora.d.ts +20 -20
  127. package/lib/ogg/theora/Theora.js +23 -23
  128. package/lib/ogg/theora/TheoraParser.d.ts +28 -28
  129. package/lib/ogg/theora/TheoraParser.js +44 -44
  130. package/lib/ogg/vorbis/Vorbis.d.ts +79 -79
  131. package/lib/ogg/vorbis/Vorbis.js +78 -78
  132. package/lib/ogg/vorbis/VorbisDecoder.d.ts +12 -12
  133. package/lib/ogg/vorbis/VorbisDecoder.js +32 -32
  134. package/lib/ogg/vorbis/VorbisParser.d.ts +36 -36
  135. package/lib/ogg/vorbis/VorbisParser.js +128 -128
  136. package/lib/ogg/vorbis/VorbisTagMapper.d.ts +7 -7
  137. package/lib/ogg/vorbis/VorbisTagMapper.js +133 -132
  138. package/lib/riff/RiffChunk.d.ts +16 -16
  139. package/lib/riff/RiffChunk.js +32 -32
  140. package/lib/riff/RiffInfoTagMap.d.ts +10 -10
  141. package/lib/riff/RiffInfoTagMap.js +38 -37
  142. package/lib/type.d.ts +599 -599
  143. package/lib/type.js +14 -13
  144. package/lib/wav/WaveChunk.d.ts +64 -64
  145. package/lib/wav/WaveChunk.js +65 -65
  146. package/lib/wav/WaveParser.d.ts +24 -24
  147. package/lib/wav/WaveParser.js +144 -144
  148. package/lib/wavpack/WavPackParser.d.ts +14 -14
  149. package/lib/wavpack/WavPackParser.js +105 -105
  150. package/lib/wavpack/WavPackToken.d.ts +64 -64
  151. package/lib/wavpack/WavPackToken.js +76 -76
  152. package/package.json +142 -142
@@ -1,515 +1,511 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MP4Parser = void 0;
4
- const initDebug = require("debug");
5
- const Token = require("token-types");
6
- const BasicParser_1 = require("../common/BasicParser");
7
- const Atom_1 = require("./Atom");
8
- const AtomToken = require("./AtomToken");
9
- const ID3v1Parser_1 = require("../id3v1/ID3v1Parser");
10
- const type_1 = require("../type");
11
- const debug = initDebug('music-metadata:parser:MP4');
12
- const tagFormat = 'iTunes';
13
- const encoderDict = {
14
- raw: {
15
- lossy: false,
16
- format: 'raw'
17
- },
18
- MAC3: {
19
- lossy: true,
20
- format: 'MACE 3:1'
21
- },
22
- MAC6: {
23
- lossy: true,
24
- format: 'MACE 6:1'
25
- },
26
- ima4: {
27
- lossy: true,
28
- format: 'IMA 4:1'
29
- },
30
- ulaw: {
31
- lossy: true,
32
- format: 'uLaw 2:1'
33
- },
34
- alaw: {
35
- lossy: true,
36
- format: 'uLaw 2:1'
37
- },
38
- Qclp: {
39
- lossy: true,
40
- format: 'QUALCOMM PureVoice'
41
- },
42
- '.mp3': {
43
- lossy: true,
44
- format: 'MPEG-1 layer 3'
45
- },
46
- alac: {
47
- lossy: false,
48
- format: 'ALAC'
49
- },
50
- 'ac-3': {
51
- lossy: true,
52
- format: 'AC-3'
53
- },
54
- mp4a: {
55
- lossy: true,
56
- format: 'MPEG-4/AAC'
57
- },
58
- mp4s: {
59
- lossy: true,
60
- format: 'MP4S'
61
- },
62
- // Closed Captioning Media, https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-SW87
63
- c608: {
64
- lossy: true,
65
- format: 'CEA-608'
66
- },
67
- c708: {
68
- lossy: true,
69
- format: 'CEA-708'
70
- }
71
- };
72
- function distinct(value, index, self) {
73
- return self.indexOf(value) === index;
74
- }
75
- /*
76
- * Parser for the MP4 (MPEG-4 Part 14) container format
77
- * Standard: ISO/IEC 14496-14
78
- * supporting:
79
- * - QuickTime container
80
- * - MP4 File Format
81
- * - 3GPP file format
82
- * - 3GPP2 file format
83
- *
84
- * MPEG-4 Audio / Part 3 (.m4a)& MPEG 4 Video (m4v, mp4) extension.
85
- * Support for Apple iTunes tags as found in a M4A/M4V files.
86
- * Ref:
87
- * https://en.wikipedia.org/wiki/ISO_base_media_file_format
88
- * https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html
89
- * http://atomicparsley.sourceforge.net/mpeg-4files.html
90
- * https://github.com/sergiomb2/libmp4v2/wiki/iTunesMetadata
91
- * https://wiki.multimedia.cx/index.php/QuickTime_container
92
- */
93
- class MP4Parser extends BasicParser_1.BasicParser {
94
- constructor() {
95
- super(...arguments);
96
- this.atomParsers = {
97
- /**
98
- * Parse movie header (mvhd) atom
99
- * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-56313
100
- */
101
- mvhd: async (len) => {
102
- const _mvhd = await this.tokenizer.readToken(new AtomToken.MvhdAtom(len));
103
- this.metadata.setFormat('creationTime', _mvhd.creationTime);
104
- this.metadata.setFormat('modificationTime', _mvhd.modificationTime);
105
- },
106
- /**
107
- * Parse media header (mdhd) atom
108
- * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25615
109
- */
110
- mdhd: async (len) => {
111
- const mdhd_data = await this.tokenizer.readToken(new AtomToken.MdhdAtom(len));
112
- // this.parse_mxhd(mdhd_data, this.currentTrack);
113
- const td = this.getTrackDescription();
114
- td.creationTime = mdhd_data.creationTime;
115
- td.modificationTime = mdhd_data.modificationTime;
116
- td.timeScale = mdhd_data.timeScale;
117
- td.duration = mdhd_data.duration;
118
- },
119
- chap: async (len) => {
120
- const td = this.getTrackDescription();
121
- const trackIds = [];
122
- while (len >= Token.UINT32_BE.len) {
123
- trackIds.push(await this.tokenizer.readNumber(Token.UINT32_BE));
124
- len -= Token.UINT32_BE.len;
125
- }
126
- td.chapterList = trackIds;
127
- },
128
- tkhd: async (len) => {
129
- const track = (await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(len)));
130
- this.tracks.push(track);
131
- },
132
- /**
133
- * Parse mdat atom.
134
- * Will scan for chapters
135
- */
136
- mdat: async (len) => {
137
- this.audioLengthInBytes = len;
138
- this.calculateBitRate();
139
- if (this.options.includeChapters) {
140
- const trackWithChapters = this.tracks.filter(track => track.chapterList);
141
- if (trackWithChapters.length === 1) {
142
- const chapterTrackIds = trackWithChapters[0].chapterList;
143
- const chapterTracks = this.tracks.filter(track => chapterTrackIds.indexOf(track.trackId) !== -1);
144
- if (chapterTracks.length === 1) {
145
- return this.parseChapterTrack(chapterTracks[0], trackWithChapters[0], len);
146
- }
147
- }
148
- }
149
- await this.tokenizer.ignore(len);
150
- },
151
- ftyp: async (len) => {
152
- const types = [];
153
- while (len > 0) {
154
- const ftype = await this.tokenizer.readToken(AtomToken.ftyp);
155
- len -= AtomToken.ftyp.len;
156
- const value = ftype.type.replace(/\W/g, '');
157
- if (value.length > 0) {
158
- types.push(value); // unshift for backward compatibility
159
- }
160
- }
161
- debug(`ftyp: ${types.join('/')}`);
162
- const x = types.filter(distinct).join('/');
163
- this.metadata.setFormat('container', x);
164
- },
165
- /**
166
- * Parse sample description atom
167
- */
168
- stsd: async (len) => {
169
- const stsd = await this.tokenizer.readToken(new AtomToken.StsdAtom(len));
170
- const trackDescription = this.getTrackDescription();
171
- trackDescription.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
172
- },
173
- /**
174
- * sample-to-Chunk Atoms
175
- */
176
- stsc: async (len) => {
177
- const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(len));
178
- this.getTrackDescription().sampleToChunkTable = stsc.entries;
179
- },
180
- /**
181
- * time to sample
182
- */
183
- stts: async (len) => {
184
- const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(len));
185
- this.getTrackDescription().timeToSampleTable = stts.entries;
186
- },
187
- /**
188
- * Parse sample-sizes atom ('stsz')
189
- */
190
- stsz: async (len) => {
191
- const stsz = await this.tokenizer.readToken(new AtomToken.StszAtom(len));
192
- const td = this.getTrackDescription();
193
- td.sampleSize = stsz.sampleSize;
194
- td.sampleSizeTable = stsz.entries;
195
- },
196
- /**
197
- * Parse chunk-offset atom ('stco')
198
- */
199
- stco: async (len) => {
200
- const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(len));
201
- this.getTrackDescription().chunkOffsetTable = stco.entries; // remember chunk offsets
202
- },
203
- date: async (len) => {
204
- const date = await this.tokenizer.readToken(new Token.StringType(len, 'utf-8'));
205
- this.addTag('date', date);
206
- }
207
- };
208
- }
209
- static read_BE_Signed_Integer(value) {
210
- if (value.length === 8) {
211
- return Number(value.readBigInt64BE(0));
212
- }
213
- return value.readIntBE(0, value.length);
214
- }
215
- static read_BE_Unsigned_Integer(value) {
216
- if (value.length === 8) {
217
- return Number(value.readBigUInt64BE(0));
218
- }
219
- return value.readUIntBE(0, value.length);
220
- }
221
- async parse() {
222
- this.tracks = [];
223
- let remainingFileSize = this.tokenizer.fileInfo.size;
224
- while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
225
- try {
226
- const token = await this.tokenizer.peekToken(AtomToken.Header);
227
- if (token.name === '\0\0\0\0') {
228
- const errMsg = `Error at offset=${this.tokenizer.position}: box.id=0`;
229
- debug(errMsg);
230
- this.addWarning(errMsg);
231
- break;
232
- }
233
- }
234
- catch (error) {
235
- const errMsg = `Error at offset=${this.tokenizer.position}: ${error.message}`;
236
- debug(errMsg);
237
- this.addWarning(errMsg);
238
- break;
239
- }
240
- const rootAtom = await Atom_1.Atom.readAtom(this.tokenizer, (atom, remaining) => this.handleAtom(atom, remaining), null, remainingFileSize);
241
- remainingFileSize -= rootAtom.header.length === BigInt(0) ? remainingFileSize : Number(rootAtom.header.length);
242
- }
243
- // Post process metadata
244
- const formatList = [];
245
- this.tracks.forEach(track => {
246
- const trackFormats = [];
247
- track.soundSampleDescription.forEach(ssd => {
248
- const streamInfo = {};
249
- const encoderInfo = encoderDict[ssd.dataFormat];
250
- if (encoderInfo) {
251
- trackFormats.push(encoderInfo.format);
252
- streamInfo.codecName = encoderInfo.format;
253
- }
254
- else {
255
- streamInfo.codecName = `<${ssd.dataFormat}>`;
256
- }
257
- if (ssd.description) {
258
- const { description } = ssd;
259
- if (description.sampleRate > 0) {
260
- streamInfo.type = type_1.TrackType.audio;
261
- streamInfo.audio = {
262
- samplingFrequency: description.sampleRate,
263
- bitDepth: description.sampleSize,
264
- channels: description.numAudioChannels
265
- };
266
- }
267
- }
268
- this.metadata.addStreamInfo(streamInfo);
269
- });
270
- if (trackFormats.length >= 1) {
271
- formatList.push(trackFormats.join('/'));
272
- }
273
- });
274
- if (formatList.length > 0) {
275
- this.metadata.setFormat('codec', formatList.filter(distinct).join('+'));
276
- }
277
- const audioTracks = this.tracks.filter(track => {
278
- return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
279
- });
280
- if (audioTracks.length >= 1) {
281
- const audioTrack = audioTracks[0];
282
- const duration = audioTrack.duration / audioTrack.timeScale;
283
- this.metadata.setFormat('duration', duration); // calculate duration in seconds
284
- const ssd = audioTrack.soundSampleDescription[0];
285
- if (ssd.description) {
286
- this.metadata.setFormat('sampleRate', ssd.description.sampleRate);
287
- this.metadata.setFormat('bitsPerSample', ssd.description.sampleSize);
288
- this.metadata.setFormat('numberOfChannels', ssd.description.numAudioChannels);
289
- }
290
- const encoderInfo = encoderDict[ssd.dataFormat];
291
- if (encoderInfo) {
292
- this.metadata.setFormat('lossless', !encoderInfo.lossy);
293
- }
294
- this.calculateBitRate();
295
- }
296
- }
297
- async handleAtom(atom, remaining) {
298
- if (atom.parent) {
299
- switch (atom.parent.header.name) {
300
- case 'ilst':
301
- case '<id>':
302
- return this.parseMetadataItemData(atom);
303
- }
304
- }
305
- // const payloadLength = atom.getPayloadLength(remaining);
306
- if (this.atomParsers[atom.header.name]) {
307
- return this.atomParsers[atom.header.name](remaining);
308
- }
309
- else {
310
- debug(`No parser for atom path=${atom.atomPath}, payload-len=${remaining}, ignoring atom`);
311
- await this.tokenizer.ignore(remaining);
312
- }
313
- }
314
- getTrackDescription() {
315
- return this.tracks[this.tracks.length - 1];
316
- }
317
- calculateBitRate() {
318
- if (this.audioLengthInBytes && this.metadata.format.duration) {
319
- this.metadata.setFormat('bitrate', 8 * this.audioLengthInBytes / this.metadata.format.duration);
320
- }
321
- }
322
- addTag(id, value) {
323
- this.metadata.addTag(tagFormat, id, value);
324
- }
325
- addWarning(message) {
326
- debug('Warning: ' + message);
327
- this.metadata.addWarning(message);
328
- }
329
- /**
330
- * Parse data of Meta-item-list-atom (item of 'ilst' atom)
331
- * @param metaAtom
332
- * Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW8
333
- */
334
- parseMetadataItemData(metaAtom) {
335
- let tagKey = metaAtom.header.name;
336
- return metaAtom.readAtoms(this.tokenizer, async (child, remaining) => {
337
- const payLoadLength = child.getPayloadLength(remaining);
338
- switch (child.header.name) {
339
- case 'data': // value atom
340
- return this.parseValueAtom(tagKey, child);
341
- case 'name': // name atom (optional)
342
- const name = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength));
343
- tagKey += ':' + name.name;
344
- break;
345
- case 'mean': // name atom (optional)
346
- const mean = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength));
347
- // console.log(" %s[%s] = %s", tagKey, header.name, mean.name);
348
- tagKey += ':' + mean.name;
349
- break;
350
- default:
351
- const dataAtom = await this.tokenizer.readToken(new Token.BufferType(payLoadLength));
352
- this.addWarning('Unsupported meta-item: ' + tagKey + '[' + child.header.name + '] => value=' + dataAtom.toString('hex') + ' ascii=' + dataAtom.toString('ascii'));
353
- }
354
- }, metaAtom.getPayloadLength(0));
355
- }
356
- async parseValueAtom(tagKey, metaAtom) {
357
- const dataAtom = await this.tokenizer.readToken(new AtomToken.DataAtom(Number(metaAtom.header.length) - AtomToken.Header.len));
358
- if (dataAtom.type.set !== 0) {
359
- throw new Error('Unsupported type-set != 0: ' + dataAtom.type.set);
360
- }
361
- // Use well-known-type table
362
- // Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
363
- switch (dataAtom.type.type) {
364
- case 0: // reserved: Reserved for use where no type needs to be indicated
365
- switch (tagKey) {
366
- case 'trkn':
367
- case 'disk':
368
- const num = Token.UINT8.get(dataAtom.value, 3);
369
- const of = Token.UINT8.get(dataAtom.value, 5);
370
- // console.log(" %s[data] = %s/%s", tagKey, num, of);
371
- this.addTag(tagKey, num + '/' + of);
372
- break;
373
- case 'gnre':
374
- const genreInt = Token.UINT8.get(dataAtom.value, 1);
375
- const genreStr = ID3v1Parser_1.Genres[genreInt - 1];
376
- // console.log(" %s[data] = %s", tagKey, genreStr);
377
- this.addTag(tagKey, genreStr);
378
- break;
379
- default:
380
- // console.log(" reserved-data: name=%s, len=%s, set=%s, type=%s, locale=%s, value{ hex=%s, ascii=%s }",
381
- // header.name, header.length, dataAtom.type.set, dataAtom.type.type, dataAtom.locale, dataAtom.value.toString('hex'), dataAtom.value.toString('ascii'));
382
- }
383
- break;
384
- case 1: // UTF-8: Without any count or NULL terminator
385
- case 18: // Unknown: Found in m4b in combination with a '©gen' tag
386
- this.addTag(tagKey, dataAtom.value.toString('utf-8'));
387
- break;
388
- case 13: // JPEG
389
- if (this.options.skipCovers)
390
- break;
391
- this.addTag(tagKey, {
392
- format: 'image/jpeg',
393
- data: Buffer.from(dataAtom.value)
394
- });
395
- break;
396
- case 14: // PNG
397
- if (this.options.skipCovers)
398
- break;
399
- this.addTag(tagKey, {
400
- format: 'image/png',
401
- data: Buffer.from(dataAtom.value)
402
- });
403
- break;
404
- case 21: // BE Signed Integer
405
- this.addTag(tagKey, MP4Parser.read_BE_Signed_Integer(dataAtom.value));
406
- break;
407
- case 22: // BE Unsigned Integer
408
- this.addTag(tagKey, MP4Parser.read_BE_Unsigned_Integer(dataAtom.value));
409
- break;
410
- case 65: // An 8-bit signed integer
411
- this.addTag(tagKey, dataAtom.value.readInt8(0));
412
- break;
413
- case 66: // A big-endian 16-bit signed integer
414
- this.addTag(tagKey, dataAtom.value.readInt16BE(0));
415
- break;
416
- case 67: // A big-endian 32-bit signed integer
417
- this.addTag(tagKey, dataAtom.value.readInt32BE(0));
418
- break;
419
- default:
420
- this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`);
421
- }
422
- }
423
- /**
424
- * @param sampleDescription
425
- * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-128916
426
- */
427
- parseSoundSampleDescription(sampleDescription) {
428
- const ssd = {
429
- dataFormat: sampleDescription.dataFormat,
430
- dataReferenceIndex: sampleDescription.dataReferenceIndex
431
- };
432
- let offset = 0;
433
- const version = AtomToken.SoundSampleDescriptionVersion.get(sampleDescription.description, offset);
434
- offset += AtomToken.SoundSampleDescriptionVersion.len;
435
- if (version.version === 0 || version.version === 1) {
436
- // Sound Sample Description (Version 0)
437
- ssd.description = AtomToken.SoundSampleDescriptionV0.get(sampleDescription.description, offset);
438
- }
439
- else {
440
- debug(`Warning: sound-sample-description ${version} not implemented`);
441
- }
442
- return ssd;
443
- }
444
- async parseChapterTrack(chapterTrack, track, len) {
445
- if (!chapterTrack.sampleSize) {
446
- if (chapterTrack.chunkOffsetTable.length !== chapterTrack.sampleSizeTable.length)
447
- throw new Error('Expected equal chunk-offset-table & sample-size-table length.');
448
- }
449
- const chapters = [];
450
- for (let i = 0; i < chapterTrack.chunkOffsetTable.length && len > 0; ++i) {
451
- const chunkOffset = chapterTrack.chunkOffsetTable[i];
452
- const nextChunkLen = chunkOffset - this.tokenizer.position;
453
- const sampleSize = chapterTrack.sampleSize > 0 ? chapterTrack.sampleSize : chapterTrack.sampleSizeTable[i];
454
- len -= nextChunkLen + sampleSize;
455
- if (len < 0)
456
- throw new Error('Chapter chunk exceeding token length');
457
- await this.tokenizer.ignore(nextChunkLen);
458
- const title = await this.tokenizer.readToken(new AtomToken.ChapterText(sampleSize));
459
- debug(`Chapter ${i + 1}: ${title}`);
460
- const chapter = {
461
- title,
462
- sampleOffset: this.findSampleOffset(track, this.tokenizer.position)
463
- };
464
- debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${this.tracks[0].duration}`);
465
- chapters.push(chapter);
466
- }
467
- this.metadata.setFormat('chapters', chapters);
468
- await this.tokenizer.ignore(len);
469
- }
470
- findSampleOffset(track, chapterOffset) {
471
- let totalDuration = 0;
472
- track.timeToSampleTable.forEach(e => {
473
- totalDuration += e.count * e.duration;
474
- });
475
- debug(`Total duration=${totalDuration}`);
476
- let chunkIndex = 0;
477
- while (chunkIndex < track.chunkOffsetTable.length && track.chunkOffsetTable[chunkIndex] < chapterOffset) {
478
- ++chunkIndex;
479
- }
480
- return this.getChunkDuration(chunkIndex + 1, track);
481
- }
482
- getChunkDuration(chunkId, track) {
483
- let ttsi = 0;
484
- let ttsc = track.timeToSampleTable[ttsi].count;
485
- let ttsd = track.timeToSampleTable[ttsi].duration;
486
- let curChunkId = 1;
487
- let samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
488
- let totalDuration = 0;
489
- while (curChunkId < chunkId) {
490
- const nrOfSamples = Math.min(ttsc, samplesPerChunk);
491
- totalDuration += nrOfSamples * ttsd;
492
- ttsc -= nrOfSamples;
493
- samplesPerChunk -= nrOfSamples;
494
- if (samplesPerChunk === 0) {
495
- ++curChunkId;
496
- samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
497
- }
498
- else {
499
- ++ttsi;
500
- ttsc = track.timeToSampleTable[ttsi].count;
501
- ttsd = track.timeToSampleTable[ttsi].duration;
502
- }
503
- }
504
- return totalDuration;
505
- }
506
- getSamplesPerChunk(chunkId, stcTable) {
507
- for (let i = 0; i < stcTable.length - 1; ++i) {
508
- if (chunkId >= stcTable[i].firstChunk && chunkId < stcTable[i + 1].firstChunk) {
509
- return stcTable[i].samplesPerChunk;
510
- }
511
- }
512
- return stcTable[stcTable.length - 1].samplesPerChunk;
513
- }
514
- }
515
- exports.MP4Parser = MP4Parser;
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MP4Parser = void 0;
4
+ const initDebug = require("debug");
5
+ const Token = require("token-types");
6
+ const BasicParser_1 = require("../common/BasicParser");
7
+ const ID3v1Parser_1 = require("../id3v1/ID3v1Parser");
8
+ const type_1 = require("../type");
9
+ const Atom_1 = require("./Atom");
10
+ const AtomToken = require("./AtomToken");
11
+ const debug = initDebug('music-metadata:parser:MP4');
12
+ const tagFormat = 'iTunes';
13
+ const encoderDict = {
14
+ raw: {
15
+ lossy: false,
16
+ format: 'raw'
17
+ },
18
+ MAC3: {
19
+ lossy: true,
20
+ format: 'MACE 3:1'
21
+ },
22
+ MAC6: {
23
+ lossy: true,
24
+ format: 'MACE 6:1'
25
+ },
26
+ ima4: {
27
+ lossy: true,
28
+ format: 'IMA 4:1'
29
+ },
30
+ ulaw: {
31
+ lossy: true,
32
+ format: 'uLaw 2:1'
33
+ },
34
+ alaw: {
35
+ lossy: true,
36
+ format: 'uLaw 2:1'
37
+ },
38
+ Qclp: {
39
+ lossy: true,
40
+ format: 'QUALCOMM PureVoice'
41
+ },
42
+ '.mp3': {
43
+ lossy: true,
44
+ format: 'MPEG-1 layer 3'
45
+ },
46
+ alac: {
47
+ lossy: false,
48
+ format: 'ALAC'
49
+ },
50
+ 'ac-3': {
51
+ lossy: true,
52
+ format: 'AC-3'
53
+ },
54
+ mp4a: {
55
+ lossy: true,
56
+ format: 'MPEG-4/AAC'
57
+ },
58
+ mp4s: {
59
+ lossy: true,
60
+ format: 'MP4S'
61
+ },
62
+ // Closed Captioning Media, https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-SW87
63
+ c608: {
64
+ lossy: true,
65
+ format: 'CEA-608'
66
+ },
67
+ c708: {
68
+ lossy: true,
69
+ format: 'CEA-708'
70
+ }
71
+ };
72
+ function distinct(value, index, self) {
73
+ return self.indexOf(value) === index;
74
+ }
75
+ /*
76
+ * Parser for the MP4 (MPEG-4 Part 14) container format
77
+ * Standard: ISO/IEC 14496-14
78
+ * supporting:
79
+ * - QuickTime container
80
+ * - MP4 File Format
81
+ * - 3GPP file format
82
+ * - 3GPP2 file format
83
+ *
84
+ * MPEG-4 Audio / Part 3 (.m4a)& MPEG 4 Video (m4v, mp4) extension.
85
+ * Support for Apple iTunes tags as found in a M4A/M4V files.
86
+ * Ref:
87
+ * https://en.wikipedia.org/wiki/ISO_base_media_file_format
88
+ * https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html
89
+ * http://atomicparsley.sourceforge.net/mpeg-4files.html
90
+ * https://github.com/sergiomb2/libmp4v2/wiki/iTunesMetadata
91
+ * https://wiki.multimedia.cx/index.php/QuickTime_container
92
+ */
93
+ class MP4Parser extends BasicParser_1.BasicParser {
94
+ constructor() {
95
+ super(...arguments);
96
+ this.atomParsers = {
97
+ /**
98
+ * Parse movie header (mvhd) atom
99
+ * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-56313
100
+ */
101
+ mvhd: async (len) => {
102
+ const _mvhd = await this.tokenizer.readToken(new AtomToken.MvhdAtom(len));
103
+ this.metadata.setFormat('creationTime', _mvhd.creationTime);
104
+ this.metadata.setFormat('modificationTime', _mvhd.modificationTime);
105
+ },
106
+ /**
107
+ * Parse media header (mdhd) atom
108
+ * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25615
109
+ */
110
+ mdhd: async (len) => {
111
+ const mdhd_data = await this.tokenizer.readToken(new AtomToken.MdhdAtom(len));
112
+ // this.parse_mxhd(mdhd_data, this.currentTrack);
113
+ const td = this.getTrackDescription();
114
+ td.creationTime = mdhd_data.creationTime;
115
+ td.modificationTime = mdhd_data.modificationTime;
116
+ td.timeScale = mdhd_data.timeScale;
117
+ td.duration = mdhd_data.duration;
118
+ },
119
+ chap: async (len) => {
120
+ const td = this.getTrackDescription();
121
+ const trackIds = [];
122
+ while (len >= Token.UINT32_BE.len) {
123
+ trackIds.push(await this.tokenizer.readNumber(Token.UINT32_BE));
124
+ len -= Token.UINT32_BE.len;
125
+ }
126
+ td.chapterList = trackIds;
127
+ },
128
+ tkhd: async (len) => {
129
+ const track = (await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(len)));
130
+ this.tracks.push(track);
131
+ },
132
+ /**
133
+ * Parse mdat atom.
134
+ * Will scan for chapters
135
+ */
136
+ mdat: async (len) => {
137
+ this.audioLengthInBytes = len;
138
+ this.calculateBitRate();
139
+ if (this.options.includeChapters) {
140
+ const trackWithChapters = this.tracks.filter(track => track.chapterList);
141
+ if (trackWithChapters.length === 1) {
142
+ const chapterTrackIds = trackWithChapters[0].chapterList;
143
+ const chapterTracks = this.tracks.filter(track => chapterTrackIds.indexOf(track.trackId) !== -1);
144
+ if (chapterTracks.length === 1) {
145
+ return this.parseChapterTrack(chapterTracks[0], trackWithChapters[0], len);
146
+ }
147
+ }
148
+ }
149
+ await this.tokenizer.ignore(len);
150
+ },
151
+ ftyp: async (len) => {
152
+ const types = [];
153
+ while (len > 0) {
154
+ const ftype = await this.tokenizer.readToken(AtomToken.ftyp);
155
+ len -= AtomToken.ftyp.len;
156
+ const value = ftype.type.replace(/\W/g, '');
157
+ if (value.length > 0) {
158
+ types.push(value); // unshift for backward compatibility
159
+ }
160
+ }
161
+ debug(`ftyp: ${types.join('/')}`);
162
+ const x = types.filter(distinct).join('/');
163
+ this.metadata.setFormat('container', x);
164
+ },
165
+ /**
166
+ * Parse sample description atom
167
+ */
168
+ stsd: async (len) => {
169
+ const stsd = await this.tokenizer.readToken(new AtomToken.StsdAtom(len));
170
+ const trackDescription = this.getTrackDescription();
171
+ trackDescription.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
172
+ },
173
+ /**
174
+ * sample-to-Chunk Atoms
175
+ */
176
+ stsc: async (len) => {
177
+ const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(len));
178
+ this.getTrackDescription().sampleToChunkTable = stsc.entries;
179
+ },
180
+ /**
181
+ * time to sample
182
+ */
183
+ stts: async (len) => {
184
+ const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(len));
185
+ this.getTrackDescription().timeToSampleTable = stts.entries;
186
+ },
187
+ /**
188
+ * Parse sample-sizes atom ('stsz')
189
+ */
190
+ stsz: async (len) => {
191
+ const stsz = await this.tokenizer.readToken(new AtomToken.StszAtom(len));
192
+ const td = this.getTrackDescription();
193
+ td.sampleSize = stsz.sampleSize;
194
+ td.sampleSizeTable = stsz.entries;
195
+ },
196
+ /**
197
+ * Parse chunk-offset atom ('stco')
198
+ */
199
+ stco: async (len) => {
200
+ const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(len));
201
+ this.getTrackDescription().chunkOffsetTable = stco.entries; // remember chunk offsets
202
+ },
203
+ date: async (len) => {
204
+ const date = await this.tokenizer.readToken(new Token.StringType(len, 'utf-8'));
205
+ this.addTag('date', date);
206
+ }
207
+ };
208
+ }
209
+ static read_BE_Integer(array, signed) {
210
+ const integerType = (signed ? 'INT' : 'UINT') + array.length * 8 + (array.length > 1 ? '_BE' : '');
211
+ const token = Token[integerType];
212
+ if (!token) {
213
+ throw new Error('Token for integer type not found: "' + integerType + '"');
214
+ }
215
+ return Number(token.get(array, 0));
216
+ }
217
+ async parse() {
218
+ this.tracks = [];
219
+ let remainingFileSize = this.tokenizer.fileInfo.size;
220
+ while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
221
+ try {
222
+ const token = await this.tokenizer.peekToken(AtomToken.Header);
223
+ if (token.name === '\0\0\0\0') {
224
+ const errMsg = `Error at offset=${this.tokenizer.position}: box.id=0`;
225
+ debug(errMsg);
226
+ this.addWarning(errMsg);
227
+ break;
228
+ }
229
+ }
230
+ catch (error) {
231
+ const errMsg = `Error at offset=${this.tokenizer.position}: ${error.message}`;
232
+ debug(errMsg);
233
+ this.addWarning(errMsg);
234
+ break;
235
+ }
236
+ const rootAtom = await Atom_1.Atom.readAtom(this.tokenizer, (atom, remaining) => this.handleAtom(atom, remaining), null, remainingFileSize);
237
+ remainingFileSize -= rootAtom.header.length === BigInt(0) ? remainingFileSize : Number(rootAtom.header.length);
238
+ }
239
+ // Post process metadata
240
+ const formatList = [];
241
+ this.tracks.forEach(track => {
242
+ const trackFormats = [];
243
+ track.soundSampleDescription.forEach(ssd => {
244
+ const streamInfo = {};
245
+ const encoderInfo = encoderDict[ssd.dataFormat];
246
+ if (encoderInfo) {
247
+ trackFormats.push(encoderInfo.format);
248
+ streamInfo.codecName = encoderInfo.format;
249
+ }
250
+ else {
251
+ streamInfo.codecName = `<${ssd.dataFormat}>`;
252
+ }
253
+ if (ssd.description) {
254
+ const { description } = ssd;
255
+ if (description.sampleRate > 0) {
256
+ streamInfo.type = type_1.TrackType.audio;
257
+ streamInfo.audio = {
258
+ samplingFrequency: description.sampleRate,
259
+ bitDepth: description.sampleSize,
260
+ channels: description.numAudioChannels
261
+ };
262
+ }
263
+ }
264
+ this.metadata.addStreamInfo(streamInfo);
265
+ });
266
+ if (trackFormats.length >= 1) {
267
+ formatList.push(trackFormats.join('/'));
268
+ }
269
+ });
270
+ if (formatList.length > 0) {
271
+ this.metadata.setFormat('codec', formatList.filter(distinct).join('+'));
272
+ }
273
+ const audioTracks = this.tracks.filter(track => {
274
+ return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
275
+ });
276
+ if (audioTracks.length >= 1) {
277
+ const audioTrack = audioTracks[0];
278
+ const duration = audioTrack.duration / audioTrack.timeScale;
279
+ this.metadata.setFormat('duration', duration); // calculate duration in seconds
280
+ const ssd = audioTrack.soundSampleDescription[0];
281
+ if (ssd.description) {
282
+ this.metadata.setFormat('sampleRate', ssd.description.sampleRate);
283
+ this.metadata.setFormat('bitsPerSample', ssd.description.sampleSize);
284
+ this.metadata.setFormat('numberOfChannels', ssd.description.numAudioChannels);
285
+ }
286
+ const encoderInfo = encoderDict[ssd.dataFormat];
287
+ if (encoderInfo) {
288
+ this.metadata.setFormat('lossless', !encoderInfo.lossy);
289
+ }
290
+ this.calculateBitRate();
291
+ }
292
+ }
293
+ async handleAtom(atom, remaining) {
294
+ if (atom.parent) {
295
+ switch (atom.parent.header.name) {
296
+ case 'ilst':
297
+ case '<id>':
298
+ return this.parseMetadataItemData(atom);
299
+ }
300
+ }
301
+ // const payloadLength = atom.getPayloadLength(remaining);
302
+ if (this.atomParsers[atom.header.name]) {
303
+ return this.atomParsers[atom.header.name](remaining);
304
+ }
305
+ else {
306
+ debug(`No parser for atom path=${atom.atomPath}, payload-len=${remaining}, ignoring atom`);
307
+ await this.tokenizer.ignore(remaining);
308
+ }
309
+ }
310
+ getTrackDescription() {
311
+ return this.tracks[this.tracks.length - 1];
312
+ }
313
+ calculateBitRate() {
314
+ if (this.audioLengthInBytes && this.metadata.format.duration) {
315
+ this.metadata.setFormat('bitrate', 8 * this.audioLengthInBytes / this.metadata.format.duration);
316
+ }
317
+ }
318
+ addTag(id, value) {
319
+ this.metadata.addTag(tagFormat, id, value);
320
+ }
321
+ addWarning(message) {
322
+ debug('Warning: ' + message);
323
+ this.metadata.addWarning(message);
324
+ }
325
+ /**
326
+ * Parse data of Meta-item-list-atom (item of 'ilst' atom)
327
+ * @param metaAtom
328
+ * Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW8
329
+ */
330
+ parseMetadataItemData(metaAtom) {
331
+ let tagKey = metaAtom.header.name;
332
+ return metaAtom.readAtoms(this.tokenizer, async (child, remaining) => {
333
+ const payLoadLength = child.getPayloadLength(remaining);
334
+ switch (child.header.name) {
335
+ case 'data': // value atom
336
+ return this.parseValueAtom(tagKey, child);
337
+ case 'name': // name atom (optional)
338
+ const name = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength));
339
+ tagKey += ':' + name.name;
340
+ break;
341
+ case 'mean': // name atom (optional)
342
+ const mean = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength));
343
+ // console.log(" %s[%s] = %s", tagKey, header.name, mean.name);
344
+ tagKey += ':' + mean.name;
345
+ break;
346
+ default:
347
+ const dataAtom = await this.tokenizer.readToken(new Token.BufferType(payLoadLength));
348
+ this.addWarning('Unsupported meta-item: ' + tagKey + '[' + child.header.name + '] => value=' + dataAtom.toString('hex') + ' ascii=' + dataAtom.toString('ascii'));
349
+ }
350
+ }, metaAtom.getPayloadLength(0));
351
+ }
352
+ async parseValueAtom(tagKey, metaAtom) {
353
+ const dataAtom = await this.tokenizer.readToken(new AtomToken.DataAtom(Number(metaAtom.header.length) - AtomToken.Header.len));
354
+ if (dataAtom.type.set !== 0) {
355
+ throw new Error('Unsupported type-set != 0: ' + dataAtom.type.set);
356
+ }
357
+ // Use well-known-type table
358
+ // Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
359
+ switch (dataAtom.type.type) {
360
+ case 0: // reserved: Reserved for use where no type needs to be indicated
361
+ switch (tagKey) {
362
+ case 'trkn':
363
+ case 'disk':
364
+ const num = Token.UINT8.get(dataAtom.value, 3);
365
+ const of = Token.UINT8.get(dataAtom.value, 5);
366
+ // console.log(" %s[data] = %s/%s", tagKey, num, of);
367
+ this.addTag(tagKey, num + '/' + of);
368
+ break;
369
+ case 'gnre':
370
+ const genreInt = Token.UINT8.get(dataAtom.value, 1);
371
+ const genreStr = ID3v1Parser_1.Genres[genreInt - 1];
372
+ // console.log(" %s[data] = %s", tagKey, genreStr);
373
+ this.addTag(tagKey, genreStr);
374
+ break;
375
+ default:
376
+ // console.log(" reserved-data: name=%s, len=%s, set=%s, type=%s, locale=%s, value{ hex=%s, ascii=%s }",
377
+ // header.name, header.length, dataAtom.type.set, dataAtom.type.type, dataAtom.locale, dataAtom.value.toString('hex'), dataAtom.value.toString('ascii'));
378
+ }
379
+ break;
380
+ case 1: // UTF-8: Without any count or NULL terminator
381
+ case 18: // Unknown: Found in m4b in combination with a '©gen' tag
382
+ this.addTag(tagKey, dataAtom.value.toString('utf-8'));
383
+ break;
384
+ case 13: // JPEG
385
+ if (this.options.skipCovers)
386
+ break;
387
+ this.addTag(tagKey, {
388
+ format: 'image/jpeg',
389
+ data: Buffer.from(dataAtom.value)
390
+ });
391
+ break;
392
+ case 14: // PNG
393
+ if (this.options.skipCovers)
394
+ break;
395
+ this.addTag(tagKey, {
396
+ format: 'image/png',
397
+ data: Buffer.from(dataAtom.value)
398
+ });
399
+ break;
400
+ case 21: // BE Signed Integer
401
+ this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, true));
402
+ break;
403
+ case 22: // BE Unsigned Integer
404
+ this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, false));
405
+ break;
406
+ case 65: // An 8-bit signed integer
407
+ this.addTag(tagKey, dataAtom.value.readInt8(0));
408
+ break;
409
+ case 66: // A big-endian 16-bit signed integer
410
+ this.addTag(tagKey, dataAtom.value.readInt16BE(0));
411
+ break;
412
+ case 67: // A big-endian 32-bit signed integer
413
+ this.addTag(tagKey, dataAtom.value.readInt32BE(0));
414
+ break;
415
+ default:
416
+ this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`);
417
+ }
418
+ }
419
+ /**
420
+ * @param sampleDescription
421
+ * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-128916
422
+ */
423
+ parseSoundSampleDescription(sampleDescription) {
424
+ const ssd = {
425
+ dataFormat: sampleDescription.dataFormat,
426
+ dataReferenceIndex: sampleDescription.dataReferenceIndex
427
+ };
428
+ let offset = 0;
429
+ const version = AtomToken.SoundSampleDescriptionVersion.get(sampleDescription.description, offset);
430
+ offset += AtomToken.SoundSampleDescriptionVersion.len;
431
+ if (version.version === 0 || version.version === 1) {
432
+ // Sound Sample Description (Version 0)
433
+ ssd.description = AtomToken.SoundSampleDescriptionV0.get(sampleDescription.description, offset);
434
+ }
435
+ else {
436
+ debug(`Warning: sound-sample-description ${version} not implemented`);
437
+ }
438
+ return ssd;
439
+ }
440
+ async parseChapterTrack(chapterTrack, track, len) {
441
+ if (!chapterTrack.sampleSize) {
442
+ if (chapterTrack.chunkOffsetTable.length !== chapterTrack.sampleSizeTable.length)
443
+ throw new Error('Expected equal chunk-offset-table & sample-size-table length.');
444
+ }
445
+ const chapters = [];
446
+ for (let i = 0; i < chapterTrack.chunkOffsetTable.length && len > 0; ++i) {
447
+ const chunkOffset = chapterTrack.chunkOffsetTable[i];
448
+ const nextChunkLen = chunkOffset - this.tokenizer.position;
449
+ const sampleSize = chapterTrack.sampleSize > 0 ? chapterTrack.sampleSize : chapterTrack.sampleSizeTable[i];
450
+ len -= nextChunkLen + sampleSize;
451
+ if (len < 0)
452
+ throw new Error('Chapter chunk exceeding token length');
453
+ await this.tokenizer.ignore(nextChunkLen);
454
+ const title = await this.tokenizer.readToken(new AtomToken.ChapterText(sampleSize));
455
+ debug(`Chapter ${i + 1}: ${title}`);
456
+ const chapter = {
457
+ title,
458
+ sampleOffset: this.findSampleOffset(track, this.tokenizer.position)
459
+ };
460
+ debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${this.tracks[0].duration}`);
461
+ chapters.push(chapter);
462
+ }
463
+ this.metadata.setFormat('chapters', chapters);
464
+ await this.tokenizer.ignore(len);
465
+ }
466
+ findSampleOffset(track, chapterOffset) {
467
+ let totalDuration = 0;
468
+ track.timeToSampleTable.forEach(e => {
469
+ totalDuration += e.count * e.duration;
470
+ });
471
+ debug(`Total duration=${totalDuration}`);
472
+ let chunkIndex = 0;
473
+ while (chunkIndex < track.chunkOffsetTable.length && track.chunkOffsetTable[chunkIndex] < chapterOffset) {
474
+ ++chunkIndex;
475
+ }
476
+ return this.getChunkDuration(chunkIndex + 1, track);
477
+ }
478
+ getChunkDuration(chunkId, track) {
479
+ let ttsi = 0;
480
+ let ttsc = track.timeToSampleTable[ttsi].count;
481
+ let ttsd = track.timeToSampleTable[ttsi].duration;
482
+ let curChunkId = 1;
483
+ let samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
484
+ let totalDuration = 0;
485
+ while (curChunkId < chunkId) {
486
+ const nrOfSamples = Math.min(ttsc, samplesPerChunk);
487
+ totalDuration += nrOfSamples * ttsd;
488
+ ttsc -= nrOfSamples;
489
+ samplesPerChunk -= nrOfSamples;
490
+ if (samplesPerChunk === 0) {
491
+ ++curChunkId;
492
+ samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
493
+ }
494
+ else {
495
+ ++ttsi;
496
+ ttsc = track.timeToSampleTable[ttsi].count;
497
+ ttsd = track.timeToSampleTable[ttsi].duration;
498
+ }
499
+ }
500
+ return totalDuration;
501
+ }
502
+ getSamplesPerChunk(chunkId, stcTable) {
503
+ for (let i = 0; i < stcTable.length - 1; ++i) {
504
+ if (chunkId >= stcTable[i].firstChunk && chunkId < stcTable[i + 1].firstChunk) {
505
+ return stcTable[i].samplesPerChunk;
506
+ }
507
+ }
508
+ return stcTable[stcTable.length - 1].samplesPerChunk;
509
+ }
510
+ }
511
+ exports.MP4Parser = MP4Parser;