music-metadata 11.3.0 → 11.5.0
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 +859 -859
- package/lib/ParserFactory.d.ts +1 -1
- package/lib/ParserFactory.js +10 -1
- package/lib/aiff/AiffParser.js +1 -0
- package/lib/apev2/APEv2Parser.d.ts +1 -1
- package/lib/apev2/APEv2Parser.js +5 -4
- package/lib/asf/AsfObject.d.ts +1 -1
- package/lib/asf/AsfObject.js +1 -1
- package/lib/common/GenericTagMapper.d.ts +1 -1
- package/lib/common/GenericTagMapper.js +1 -1
- package/lib/common/MetadataCollector.d.ts +2 -0
- package/lib/common/MetadataCollector.js +5 -1
- package/lib/core.d.ts +2 -2
- package/lib/core.js +3 -6
- package/lib/dsdiff/DsdiffParser.js +1 -0
- package/lib/dsf/DsfParser.js +1 -0
- package/lib/ebml/EbmlIterator.js +1 -1
- package/lib/flac/FlacParser.js +1 -0
- package/lib/mp4/Atom.js +1 -1
- package/lib/mp4/AtomToken.d.ts +84 -2
- package/lib/mp4/AtomToken.js +141 -9
- package/lib/mp4/MP4Parser.d.ts +4 -0
- package/lib/mp4/MP4Parser.js +156 -57
- package/lib/mp4/MP4TagMapper.d.ts +1 -1
- package/lib/mp4/MP4TagMapper.js +1 -1
- package/lib/mpeg/MpegParser.js +1 -0
- package/lib/musepack/MusepackParser.js +1 -0
- package/lib/musepack/sv7/MpcSv7Parser.js +2 -2
- package/lib/musepack/sv8/MpcSv8Parser.js +2 -2
- package/lib/ogg/opus/OpusParser.d.ts +1 -1
- package/lib/ogg/opus/OpusParser.js +2 -1
- package/lib/ogg/speex/SpeexParser.d.ts +1 -1
- package/lib/ogg/speex/SpeexParser.js +2 -1
- package/lib/ogg/theora/TheoraParser.d.ts +3 -5
- package/lib/ogg/theora/TheoraParser.js +4 -5
- package/lib/ogg/vorbis/VorbisParser.d.ts +1 -1
- package/lib/ogg/vorbis/VorbisParser.js +2 -1
- package/lib/type.d.ts +9 -1
- package/lib/wav/WaveParser.js +1 -0
- package/lib/wavpack/WavPackParser.js +3 -2
- package/package.json +146 -146
package/lib/mp4/MP4Parser.js
CHANGED
|
@@ -4,7 +4,7 @@ import { BasicParser } from '../common/BasicParser.js';
|
|
|
4
4
|
import { Genres } from '../id3v1/ID3v1Parser.js';
|
|
5
5
|
import { Atom } from './Atom.js';
|
|
6
6
|
import * as AtomToken from './AtomToken.js';
|
|
7
|
-
import { Mp4ContentError } from './AtomToken.js';
|
|
7
|
+
import { ChapterTrackReferenceBox, Mp4ContentError, } from './AtomToken.js';
|
|
8
8
|
import { TrackType } from '../type.js';
|
|
9
9
|
import { uint8ArrayToHex, uint8ArrayToString } from 'uint8array-extras';
|
|
10
10
|
const debug = initDebug('music-metadata:parser:MP4');
|
|
@@ -92,7 +92,9 @@ function distinct(value, index, self) {
|
|
|
92
92
|
export class MP4Parser extends BasicParser {
|
|
93
93
|
constructor() {
|
|
94
94
|
super(...arguments);
|
|
95
|
-
this.tracks =
|
|
95
|
+
this.tracks = new Map();
|
|
96
|
+
this.hasVideoTrack = false;
|
|
97
|
+
this.hasAudioTrack = true;
|
|
96
98
|
this.atomParsers = {
|
|
97
99
|
/**
|
|
98
100
|
* Parse movie header (mvhd) atom
|
|
@@ -103,19 +105,6 @@ export class MP4Parser extends BasicParser {
|
|
|
103
105
|
this.metadata.setFormat('creationTime', mvhd.creationTime);
|
|
104
106
|
this.metadata.setFormat('modificationTime', mvhd.modificationTime);
|
|
105
107
|
},
|
|
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
108
|
chap: async (len) => {
|
|
120
109
|
const td = this.getTrackDescription();
|
|
121
110
|
const trackIds = [];
|
|
@@ -125,10 +114,6 @@ export class MP4Parser extends BasicParser {
|
|
|
125
114
|
}
|
|
126
115
|
td.chapterList = trackIds;
|
|
127
116
|
},
|
|
128
|
-
tkhd: async (len) => {
|
|
129
|
-
const track = (await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(len)));
|
|
130
|
-
this.tracks.push(track);
|
|
131
|
-
},
|
|
132
117
|
/**
|
|
133
118
|
* Parse mdat atom.
|
|
134
119
|
* Will scan for chapters
|
|
@@ -137,10 +122,10 @@ export class MP4Parser extends BasicParser {
|
|
|
137
122
|
this.audioLengthInBytes = len;
|
|
138
123
|
this.calculateBitRate();
|
|
139
124
|
if (this.options.includeChapters) {
|
|
140
|
-
const trackWithChapters = this.tracks.filter(track => track.chapterList);
|
|
125
|
+
const trackWithChapters = [...this.tracks.values()].filter(track => track.chapterList);
|
|
141
126
|
if (trackWithChapters.length === 1) {
|
|
142
127
|
const chapterTrackIds = trackWithChapters[0].chapterList;
|
|
143
|
-
const chapterTracks = this.tracks.filter(track => chapterTrackIds.indexOf(track.trackId) !== -1);
|
|
128
|
+
const chapterTracks = [...this.tracks.values()].filter(track => chapterTrackIds.indexOf(track.header.trackId) !== -1);
|
|
144
129
|
if (chapterTracks.length === 1) {
|
|
145
130
|
return this.parseChapterTrack(chapterTracks[0], trackWithChapters[0], len);
|
|
146
131
|
}
|
|
@@ -170,20 +155,6 @@ export class MP4Parser extends BasicParser {
|
|
|
170
155
|
const trackDescription = this.getTrackDescription();
|
|
171
156
|
trackDescription.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
|
|
172
157
|
},
|
|
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 table
|
|
182
|
-
*/
|
|
183
|
-
stts: async (len) => {
|
|
184
|
-
const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(len));
|
|
185
|
-
this.getTrackDescription().timeToSampleTable = stts.entries;
|
|
186
|
-
},
|
|
187
158
|
/**
|
|
188
159
|
* Parse sample-sizes atom ('stsz')
|
|
189
160
|
*/
|
|
@@ -193,13 +164,6 @@ export class MP4Parser extends BasicParser {
|
|
|
193
164
|
td.sampleSize = stsz.sampleSize;
|
|
194
165
|
td.sampleSizeTable = stsz.entries;
|
|
195
166
|
},
|
|
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
167
|
date: async (len) => {
|
|
204
168
|
const date = await this.tokenizer.readToken(new Token.StringType(len, 'utf-8'));
|
|
205
169
|
await this.addTag('date', date);
|
|
@@ -215,7 +179,9 @@ export class MP4Parser extends BasicParser {
|
|
|
215
179
|
return Number(token.get(array, 0));
|
|
216
180
|
}
|
|
217
181
|
async parse() {
|
|
218
|
-
this.
|
|
182
|
+
this.hasVideoTrack = false;
|
|
183
|
+
this.hasAudioTrack = true;
|
|
184
|
+
this.tracks.clear();
|
|
219
185
|
let remainingFileSize = this.tokenizer.fileInfo.size || 0;
|
|
220
186
|
while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
|
|
221
187
|
try {
|
|
@@ -274,21 +240,39 @@ export class MP4Parser extends BasicParser {
|
|
|
274
240
|
if (formatList.length > 0) {
|
|
275
241
|
this.metadata.setFormat('codec', formatList.filter(distinct).join('+'));
|
|
276
242
|
}
|
|
277
|
-
const audioTracks = this.tracks.filter(track => {
|
|
243
|
+
const audioTracks = [...this.tracks.values()].filter(track => {
|
|
278
244
|
return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
|
|
279
245
|
});
|
|
280
246
|
if (audioTracks.length >= 1) {
|
|
281
247
|
const audioTrack = audioTracks[0];
|
|
282
|
-
if (audioTrack.timeScale > 0) {
|
|
283
|
-
|
|
284
|
-
|
|
248
|
+
if (audioTrack.media.header && audioTrack.media.header.timeScale > 0) {
|
|
249
|
+
if (audioTrack.media.header.duration > 0) {
|
|
250
|
+
debug('Using duration defined on audio track');
|
|
251
|
+
const duration = audioTrack.media.header.duration / audioTrack.media.header.timeScale; // calculate duration in seconds
|
|
252
|
+
this.metadata.setFormat('duration', duration);
|
|
253
|
+
}
|
|
254
|
+
else if (audioTrack.fragments.length > 0) {
|
|
255
|
+
debug('Calculate duration defined in track fragments');
|
|
256
|
+
let totalTimeUnits = 0;
|
|
257
|
+
for (const fragment of audioTrack.fragments) {
|
|
258
|
+
const defaultDuration = fragment.header.defaultSampleDuration;
|
|
259
|
+
for (const sample of fragment.trackRun.samples) {
|
|
260
|
+
const dur = sample.sampleDuration ?? defaultDuration;
|
|
261
|
+
if (dur == null) {
|
|
262
|
+
throw new Error("Missing sampleDuration and no default_sample_duration in tfhd");
|
|
263
|
+
}
|
|
264
|
+
totalTimeUnits += dur;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this.metadata.setFormat('duration', totalTimeUnits / audioTrack.media.header.timeScale);
|
|
268
|
+
}
|
|
285
269
|
}
|
|
286
270
|
const ssd = audioTrack.soundSampleDescription[0];
|
|
287
|
-
if (ssd.description) {
|
|
271
|
+
if (ssd.description && audioTrack.media.header) {
|
|
288
272
|
this.metadata.setFormat('sampleRate', ssd.description.sampleRate);
|
|
289
273
|
this.metadata.setFormat('bitsPerSample', ssd.description.sampleSize);
|
|
290
274
|
this.metadata.setFormat('numberOfChannels', ssd.description.numAudioChannels);
|
|
291
|
-
if (audioTrack.timeScale === 0 && audioTrack.timeToSampleTable.length > 0) {
|
|
275
|
+
if (audioTrack.media.header.timeScale === 0 && audioTrack.timeToSampleTable.length > 0) {
|
|
292
276
|
const totalSampleSize = audioTrack.timeToSampleTable
|
|
293
277
|
.map(ttstEntry => ttstEntry.count * ttstEntry.duration)
|
|
294
278
|
.reduce((total, sampleSize) => total + sampleSize);
|
|
@@ -302,6 +286,8 @@ export class MP4Parser extends BasicParser {
|
|
|
302
286
|
}
|
|
303
287
|
this.calculateBitRate();
|
|
304
288
|
}
|
|
289
|
+
this.metadata.setFormat('hasAudio', this.hasAudioTrack);
|
|
290
|
+
this.metadata.setFormat('hasVideo', this.hasVideoTrack);
|
|
305
291
|
}
|
|
306
292
|
async handleAtom(atom, remaining) {
|
|
307
293
|
if (atom.parent) {
|
|
@@ -309,6 +295,17 @@ export class MP4Parser extends BasicParser {
|
|
|
309
295
|
case 'ilst':
|
|
310
296
|
case '<id>':
|
|
311
297
|
return this.parseMetadataItemData(atom);
|
|
298
|
+
case 'moov':
|
|
299
|
+
switch (atom.header.name) {
|
|
300
|
+
case 'trak':
|
|
301
|
+
return this.parseTrackBox(atom);
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
case 'moof':
|
|
305
|
+
switch (atom.header.name) {
|
|
306
|
+
case 'traf':
|
|
307
|
+
return this.parseTrackFragmentBox(atom);
|
|
308
|
+
}
|
|
312
309
|
}
|
|
313
310
|
}
|
|
314
311
|
// const payloadLength = atom.getPayloadLength(remaining);
|
|
@@ -319,7 +316,9 @@ export class MP4Parser extends BasicParser {
|
|
|
319
316
|
await this.tokenizer.ignore(remaining);
|
|
320
317
|
}
|
|
321
318
|
getTrackDescription() {
|
|
322
|
-
|
|
319
|
+
// ToDo: pick the right track, not the last track!!!!
|
|
320
|
+
const tracks = [...this.tracks.values()];
|
|
321
|
+
return tracks[tracks.length - 1];
|
|
323
322
|
}
|
|
324
323
|
calculateBitRate() {
|
|
325
324
|
if (this.audioLengthInBytes && this.metadata.format.duration) {
|
|
@@ -432,6 +431,111 @@ export class MP4Parser extends BasicParser {
|
|
|
432
431
|
this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`);
|
|
433
432
|
}
|
|
434
433
|
}
|
|
434
|
+
async parseTrackBox(trakBox) {
|
|
435
|
+
// @ts-ignore
|
|
436
|
+
const track = {
|
|
437
|
+
media: {},
|
|
438
|
+
fragments: []
|
|
439
|
+
};
|
|
440
|
+
await trakBox.readAtoms(this.tokenizer, async (child, remaining) => {
|
|
441
|
+
const payLoadLength = child.getPayloadLength(remaining);
|
|
442
|
+
switch (child.header.name) {
|
|
443
|
+
case 'chap': {
|
|
444
|
+
const chap = await this.tokenizer.readToken(new ChapterTrackReferenceBox(remaining));
|
|
445
|
+
track.chapterList = chap;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case 'tkhd': // TrackHeaderBox
|
|
449
|
+
track.header = await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(payLoadLength));
|
|
450
|
+
break;
|
|
451
|
+
case 'hdlr': // TrackHeaderBox
|
|
452
|
+
track.handler = await this.tokenizer.readToken(new AtomToken.HandlerBox(payLoadLength));
|
|
453
|
+
switch (track.handler.handlerType) {
|
|
454
|
+
case 'audi':
|
|
455
|
+
debug('Contains audio track');
|
|
456
|
+
this.hasAudioTrack = true;
|
|
457
|
+
break;
|
|
458
|
+
case 'vide':
|
|
459
|
+
debug('Contains video track');
|
|
460
|
+
this.hasVideoTrack = true;
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
case 'mdhd': { // Parse media header (mdhd) box
|
|
465
|
+
const mdhd_data = await this.tokenizer.readToken(new AtomToken.MdhdAtom(payLoadLength));
|
|
466
|
+
track.media.header = mdhd_data;
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
case 'stco': {
|
|
470
|
+
const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(payLoadLength));
|
|
471
|
+
track.chunkOffsetTable = stco.entries; // remember chunk offsets
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case 'stsc': { // sample-to-Chunk box
|
|
475
|
+
const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(payLoadLength));
|
|
476
|
+
track.sampleToChunkTable = stsc.entries;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case 'stsd': { // sample description box
|
|
480
|
+
const stsd = await this.tokenizer.readToken(new AtomToken.StsdAtom(payLoadLength));
|
|
481
|
+
track.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
case 'stts': { // time-to-sample table
|
|
485
|
+
const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(payLoadLength));
|
|
486
|
+
track.timeToSampleTable = stts.entries;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case 'stsz': {
|
|
490
|
+
const stsz = await this.tokenizer.readToken(new AtomToken.StszAtom(payLoadLength));
|
|
491
|
+
track.sampleSize = stsz.sampleSize;
|
|
492
|
+
track.sampleSizeTable = stsz.entries;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case 'dinf':
|
|
496
|
+
case 'vmhd':
|
|
497
|
+
case 'smhd':
|
|
498
|
+
debug(`Ignoring: ${child.header.name}`);
|
|
499
|
+
await this.tokenizer.ignore(payLoadLength);
|
|
500
|
+
break;
|
|
501
|
+
default: {
|
|
502
|
+
debug(`Unexpected track box: ${child.header.name}`);
|
|
503
|
+
await this.tokenizer.ignore(payLoadLength);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}, trakBox.getPayloadLength(0));
|
|
507
|
+
// Register track
|
|
508
|
+
this.tracks.set(track.header.trackId, track);
|
|
509
|
+
}
|
|
510
|
+
parseTrackFragmentBox(trafBox) {
|
|
511
|
+
let tfhd;
|
|
512
|
+
return trafBox.readAtoms(this.tokenizer, async (child, remaining) => {
|
|
513
|
+
const payLoadLength = child.getPayloadLength(remaining);
|
|
514
|
+
switch (child.header.name) {
|
|
515
|
+
case 'tfhd': { // TrackFragmentHeaderBox
|
|
516
|
+
const fragmentHeaderBox = new AtomToken.TrackFragmentHeaderBox(child.getPayloadLength(remaining));
|
|
517
|
+
tfhd = await this.tokenizer.readToken(fragmentHeaderBox);
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
case 'tfdt': // TrackFragmentBaseMediaDecodeTimeBo
|
|
521
|
+
await this.tokenizer.ignore(payLoadLength);
|
|
522
|
+
break;
|
|
523
|
+
case 'trun': { // TrackRunBox
|
|
524
|
+
const trackRunBox = new AtomToken.TrackRunBox(payLoadLength);
|
|
525
|
+
const trun = await this.tokenizer.readToken(trackRunBox);
|
|
526
|
+
if (tfhd) {
|
|
527
|
+
const track = this.tracks.get(tfhd.trackId);
|
|
528
|
+
track?.fragments.push({ header: tfhd, trackRun: trun });
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
default: {
|
|
533
|
+
debug(`Unexpected box: ${child.header.name}`);
|
|
534
|
+
await this.tokenizer.ignore(payLoadLength);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}, trafBox.getPayloadLength(0));
|
|
538
|
+
}
|
|
435
539
|
/**
|
|
436
540
|
* @param sampleDescription
|
|
437
541
|
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-128916
|
|
@@ -476,22 +580,17 @@ export class MP4Parser extends BasicParser {
|
|
|
476
580
|
debug(`Chapter ${i + 1}: ${title}`);
|
|
477
581
|
const chapter = {
|
|
478
582
|
title,
|
|
479
|
-
timeScale: chapterTrack.timeScale,
|
|
583
|
+
timeScale: chapterTrack.media.header ? chapterTrack.media.header.timeScale : 0,
|
|
480
584
|
start,
|
|
481
585
|
sampleOffset: this.findSampleOffset(track, this.tokenizer.position)
|
|
482
586
|
};
|
|
483
|
-
debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${
|
|
587
|
+
debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${track.header.duration}`); // ToDo, use media duration if required!!!
|
|
484
588
|
chapters.push(chapter);
|
|
485
589
|
}
|
|
486
590
|
this.metadata.setFormat('chapters', chapters);
|
|
487
591
|
await this.tokenizer.ignore(len);
|
|
488
592
|
}
|
|
489
593
|
findSampleOffset(track, chapterOffset) {
|
|
490
|
-
let totalDuration = 0;
|
|
491
|
-
track.timeToSampleTable.forEach(e => {
|
|
492
|
-
totalDuration += e.count * e.duration;
|
|
493
|
-
});
|
|
494
|
-
debug(`Total duration=${totalDuration}`);
|
|
495
594
|
let chunkIndex = 0;
|
|
496
595
|
while (chunkIndex < track.chunkOffsetTable.length && track.chunkOffsetTable[chunkIndex] < chapterOffset) {
|
|
497
596
|
++chunkIndex;
|
|
@@ -4,5 +4,5 @@ import type { INativeMetadataCollector } from "../common/MetadataCollector.js";
|
|
|
4
4
|
export declare const tagType = "iTunes";
|
|
5
5
|
export declare class MP4TagMapper extends CaseInsensitiveTagMap {
|
|
6
6
|
constructor();
|
|
7
|
-
protected postMap(tag: ITag,
|
|
7
|
+
protected postMap(tag: ITag, _warnings: INativeMetadataCollector): void;
|
|
8
8
|
}
|
package/lib/mp4/MP4TagMapper.js
CHANGED
package/lib/mpeg/MpegParser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
2
|
import { BasicParser } from '../../common/BasicParser.js';
|
|
3
|
-
import {
|
|
3
|
+
import { tryParseApeHeader } from '../../apev2/APEv2Parser.js';
|
|
4
4
|
import { BitReader } from './BitReader.js';
|
|
5
5
|
import * as SV7 from './StreamVersion7.js';
|
|
6
6
|
import { MusepackContentError } from '../MusepackConentError.js';
|
|
@@ -29,7 +29,7 @@ export class MpcSv7Parser extends BasicParser {
|
|
|
29
29
|
this.metadata.setFormat('codec', (version / 100).toFixed(2));
|
|
30
30
|
await this.skipAudioData(header.frameCount);
|
|
31
31
|
debug(`End of audio stream, switching to APEv2, offset=${this.tokenizer.position}`);
|
|
32
|
-
return
|
|
32
|
+
return tryParseApeHeader(this.metadata, this.tokenizer, this.options);
|
|
33
33
|
}
|
|
34
34
|
async skipAudioData(frameCount) {
|
|
35
35
|
while (frameCount-- > 0) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
2
|
import { BasicParser } from '../../common/BasicParser.js';
|
|
3
|
-
import {
|
|
3
|
+
import { tryParseApeHeader } from '../../apev2/APEv2Parser.js';
|
|
4
4
|
import { FourCcToken } from '../../common/FourCC.js';
|
|
5
5
|
import * as SV8 from './StreamVersion8.js';
|
|
6
6
|
import { MusepackContentError } from '../MusepackConentError.js';
|
|
@@ -46,7 +46,7 @@ export class MpcSv8Parser extends BasicParser {
|
|
|
46
46
|
if (this.metadata.format.duration) {
|
|
47
47
|
this.metadata.setFormat('bitrate', this.audioLength * 8 / this.metadata.format.duration);
|
|
48
48
|
}
|
|
49
|
-
return
|
|
49
|
+
return tryParseApeHeader(this.metadata, this.tokenizer, this.options);
|
|
50
50
|
default:
|
|
51
51
|
throw new MusepackContentError(`Unexpected header: ${header.key}`);
|
|
52
52
|
}
|
|
@@ -18,7 +18,7 @@ export declare class OpusParser extends VorbisParser {
|
|
|
18
18
|
* @param {IPageHeader} header
|
|
19
19
|
* @param {Uint8Array} pageData
|
|
20
20
|
*/
|
|
21
|
-
protected parseFirstPage(
|
|
21
|
+
protected parseFirstPage(_header: IPageHeader, pageData: Uint8Array): void;
|
|
22
22
|
protected parseFullPage(pageData: Uint8Array): Promise<void>;
|
|
23
23
|
calculateDuration(header: IPageHeader): void;
|
|
24
24
|
}
|
|
@@ -19,7 +19,7 @@ export class OpusParser extends VorbisParser {
|
|
|
19
19
|
* @param {IPageHeader} header
|
|
20
20
|
* @param {Uint8Array} pageData
|
|
21
21
|
*/
|
|
22
|
-
parseFirstPage(
|
|
22
|
+
parseFirstPage(_header, pageData) {
|
|
23
23
|
this.metadata.setFormat('codec', 'Opus');
|
|
24
24
|
// Parse Opus ID Header
|
|
25
25
|
this.idHeader = new Opus.IdHeader(pageData.length).get(pageData, 0);
|
|
@@ -27,6 +27,7 @@ export class OpusParser extends VorbisParser {
|
|
|
27
27
|
throw new OpusContentError("Illegal ogg/Opus magic-signature");
|
|
28
28
|
this.metadata.setFormat('sampleRate', this.idHeader.inputSampleRate);
|
|
29
29
|
this.metadata.setFormat('numberOfChannels', this.idHeader.channelCount);
|
|
30
|
+
this.metadata.setAudioOnly();
|
|
30
31
|
}
|
|
31
32
|
async parseFullPage(pageData) {
|
|
32
33
|
const magicSignature = new Token.StringType(8, 'ascii').get(pageData, 0);
|
|
@@ -17,5 +17,5 @@ export declare class SpeexParser extends VorbisParser {
|
|
|
17
17
|
* @param {IPageHeader} header
|
|
18
18
|
* @param {Uint8Array} pageData
|
|
19
19
|
*/
|
|
20
|
-
protected parseFirstPage(
|
|
20
|
+
protected parseFirstPage(_header: IPageHeader, pageData: Uint8Array): void;
|
|
21
21
|
}
|
|
@@ -18,7 +18,7 @@ export class SpeexParser extends VorbisParser {
|
|
|
18
18
|
* @param {IPageHeader} header
|
|
19
19
|
* @param {Uint8Array} pageData
|
|
20
20
|
*/
|
|
21
|
-
parseFirstPage(
|
|
21
|
+
parseFirstPage(_header, pageData) {
|
|
22
22
|
debug('First Ogg/Speex page');
|
|
23
23
|
const speexHeader = Speex.Header.get(pageData, 0);
|
|
24
24
|
this.metadata.setFormat('codec', `Speex ${speexHeader.version}`);
|
|
@@ -27,6 +27,7 @@ export class SpeexParser extends VorbisParser {
|
|
|
27
27
|
if (speexHeader.bitrate !== -1) {
|
|
28
28
|
this.metadata.setFormat('bitrate', speexHeader.bitrate);
|
|
29
29
|
}
|
|
30
|
+
this.metadata.setAudioOnly();
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
//# sourceMappingURL=SpeexParser.js.map
|
|
@@ -9,7 +9,7 @@ import type { INativeMetadataCollector } from '../../common/MetadataCollector.js
|
|
|
9
9
|
export declare class TheoraParser implements Ogg.IPageConsumer {
|
|
10
10
|
private metadata;
|
|
11
11
|
private tokenizer;
|
|
12
|
-
constructor(metadata: INativeMetadataCollector,
|
|
12
|
+
constructor(metadata: INativeMetadataCollector, _options: IOptions, tokenizer: ITokenizer);
|
|
13
13
|
/**
|
|
14
14
|
* Vorbis 1 parser
|
|
15
15
|
* @param header Ogg Page Header
|
|
@@ -17,11 +17,9 @@ export declare class TheoraParser implements Ogg.IPageConsumer {
|
|
|
17
17
|
*/
|
|
18
18
|
parsePage(header: Ogg.IPageHeader, pageData: Uint8Array): Promise<void>;
|
|
19
19
|
flush(): Promise<void>;
|
|
20
|
-
calculateDuration(
|
|
20
|
+
calculateDuration(_header: Ogg.IPageHeader): void;
|
|
21
21
|
/**
|
|
22
22
|
* Parse first Theora Ogg page. the initial identification header packet
|
|
23
|
-
* @param {IPageHeader} header
|
|
24
|
-
* @param {Buffer} pageData
|
|
25
23
|
*/
|
|
26
|
-
protected parseFirstPage(
|
|
24
|
+
protected parseFirstPage(_header: Ogg.IPageHeader, pageData: Uint8Array): Promise<void>;
|
|
27
25
|
}
|
|
@@ -6,7 +6,7 @@ const debug = initDebug('music-metadata:parser:ogg:theora');
|
|
|
6
6
|
* - https://theora.org/doc/Theora.pdf
|
|
7
7
|
*/
|
|
8
8
|
export class TheoraParser {
|
|
9
|
-
constructor(metadata,
|
|
9
|
+
constructor(metadata, _options, tokenizer) {
|
|
10
10
|
this.metadata = metadata;
|
|
11
11
|
this.tokenizer = tokenizer;
|
|
12
12
|
}
|
|
@@ -23,19 +23,18 @@ export class TheoraParser {
|
|
|
23
23
|
async flush() {
|
|
24
24
|
debug('flush');
|
|
25
25
|
}
|
|
26
|
-
calculateDuration(
|
|
26
|
+
calculateDuration(_header) {
|
|
27
27
|
debug('duration calculation not implemented');
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Parse first Theora Ogg page. the initial identification header packet
|
|
31
|
-
* @param {IPageHeader} header
|
|
32
|
-
* @param {Buffer} pageData
|
|
33
31
|
*/
|
|
34
|
-
async parseFirstPage(
|
|
32
|
+
async parseFirstPage(_header, pageData) {
|
|
35
33
|
debug('First Ogg/Theora page');
|
|
36
34
|
this.metadata.setFormat('codec', 'Theora');
|
|
37
35
|
const idHeader = IdentificationHeader.get(pageData, 0);
|
|
38
36
|
this.metadata.setFormat('bitrate', idHeader.nombr);
|
|
37
|
+
this.metadata.setAudioOnly();
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
//# sourceMappingURL=TheoraParser.js.map
|
|
@@ -41,7 +41,7 @@ export declare class VorbisParser implements IPageConsumer {
|
|
|
41
41
|
* @param header
|
|
42
42
|
* @param pageData
|
|
43
43
|
*/
|
|
44
|
-
protected parseFirstPage(
|
|
44
|
+
protected parseFirstPage(_header: IPageHeader, pageData: Uint8Array): void;
|
|
45
45
|
protected parseFullPage(pageData: Uint8Array): Promise<void>;
|
|
46
46
|
/**
|
|
47
47
|
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-840005.2
|
|
@@ -90,8 +90,9 @@ export class VorbisParser {
|
|
|
90
90
|
* @param header
|
|
91
91
|
* @param pageData
|
|
92
92
|
*/
|
|
93
|
-
parseFirstPage(
|
|
93
|
+
parseFirstPage(_header, pageData) {
|
|
94
94
|
this.metadata.setFormat('codec', 'Vorbis I');
|
|
95
|
+
this.metadata.setAudioOnly();
|
|
95
96
|
debug('Parse first page');
|
|
96
97
|
// Parse Vorbis common header
|
|
97
98
|
const commonHeader = CommonHeader.get(pageData, 0);
|
package/lib/type.d.ts
CHANGED
|
@@ -370,7 +370,7 @@ export interface IRatio {
|
|
|
370
370
|
*/
|
|
371
371
|
dB: number;
|
|
372
372
|
}
|
|
373
|
-
export type FormatId = 'container' | 'duration' | 'bitrate' | 'sampleRate' | 'bitsPerSample' | 'codec' | 'tool' | 'codecProfile' | 'lossless' | 'numberOfChannels' | 'numberOfSamples' | 'audioMD5' | 'chapters' | 'modificationTime' | 'creationTime' | 'trackPeakLevel' | 'trackGain' | 'albumGain';
|
|
373
|
+
export type FormatId = 'container' | 'duration' | 'bitrate' | 'sampleRate' | 'bitsPerSample' | 'codec' | 'tool' | 'codecProfile' | 'lossless' | 'numberOfChannels' | 'numberOfSamples' | 'audioMD5' | 'chapters' | 'modificationTime' | 'creationTime' | 'trackPeakLevel' | 'trackGain' | 'albumGain' | 'hasAudio' | 'hasVideo';
|
|
374
374
|
export interface IAudioTrack {
|
|
375
375
|
samplingFrequency?: number;
|
|
376
376
|
outputSamplingFrequency?: number;
|
|
@@ -470,6 +470,14 @@ export interface IFormat {
|
|
|
470
470
|
readonly trackGain?: number;
|
|
471
471
|
readonly trackPeakLevel?: number;
|
|
472
472
|
readonly albumGain?: number;
|
|
473
|
+
/**
|
|
474
|
+
* Indicates if the audio files contains an audio stream
|
|
475
|
+
*/
|
|
476
|
+
hasAudio?: boolean;
|
|
477
|
+
/**
|
|
478
|
+
* Indicates if the media files contains a video stream
|
|
479
|
+
*/
|
|
480
|
+
hasVideo?: boolean;
|
|
473
481
|
}
|
|
474
482
|
export interface ITag {
|
|
475
483
|
id: string;
|
package/lib/wav/WaveParser.js
CHANGED
|
@@ -31,6 +31,7 @@ export class WaveParser extends BasicParser {
|
|
|
31
31
|
debug(`pos=${this.tokenizer.position}, parse: chunkID=${riffHeader.chunkID}`);
|
|
32
32
|
if (riffHeader.chunkID !== 'RIFF')
|
|
33
33
|
return; // Not RIFF format
|
|
34
|
+
this.metadata.setAudioOnly();
|
|
34
35
|
return this.parseRiffChunk(riffHeader.chunkSize).catch(err => {
|
|
35
36
|
if (!(err instanceof strtok3.EndOfStreamError)) {
|
|
36
37
|
throw err;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Token from 'token-types';
|
|
2
|
-
import {
|
|
2
|
+
import { tryParseApeHeader } from '../apev2/APEv2Parser.js';
|
|
3
3
|
import { FourCcToken } from '../common/FourCC.js';
|
|
4
4
|
import { BasicParser } from '../common/BasicParser.js';
|
|
5
5
|
import { BlockHeaderToken, MetadataIdToken } from './WavPackToken.js';
|
|
@@ -18,11 +18,12 @@ export class WavPackParser extends BasicParser {
|
|
|
18
18
|
this.audioDataSize = 0;
|
|
19
19
|
}
|
|
20
20
|
async parse() {
|
|
21
|
+
this.metadata.setAudioOnly();
|
|
21
22
|
this.audioDataSize = 0;
|
|
22
23
|
// First parse all WavPack blocks
|
|
23
24
|
await this.parseWavPackBlocks();
|
|
24
25
|
// try to parse APEv2 header
|
|
25
|
-
return
|
|
26
|
+
return tryParseApeHeader(this.metadata, this.tokenizer, this.options);
|
|
26
27
|
}
|
|
27
28
|
async parseWavPackBlocks() {
|
|
28
29
|
do {
|