music-metadata 11.2.3 → 11.4.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 -861
- package/lib/ParserFactory.js +2 -2
- package/lib/apev2/APEv2Parser.d.ts +1 -1
- package/lib/apev2/APEv2Parser.js +4 -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/core.d.ts +2 -2
- package/lib/core.js +2 -2
- package/lib/ebml/EbmlIterator.js +1 -1
- package/lib/lrc/LyricsParser.js +13 -4
- package/lib/mp4/Atom.js +1 -0
- package/lib/mp4/AtomToken.d.ts +60 -0
- package/lib/mp4/AtomToken.js +104 -0
- package/lib/mp4/MP4Parser.d.ts +2 -0
- package/lib/mp4/MP4Parser.js +58 -7
- package/lib/mp4/MP4TagMapper.d.ts +1 -1
- package/lib/mp4/MP4TagMapper.js +1 -1
- 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 +1 -1
- package/lib/ogg/speex/SpeexParser.d.ts +1 -1
- package/lib/ogg/speex/SpeexParser.js +1 -1
- package/lib/ogg/theora/TheoraParser.d.ts +3 -5
- package/lib/ogg/theora/TheoraParser.js +3 -5
- package/lib/ogg/vorbis/VorbisParser.d.ts +1 -1
- package/lib/ogg/vorbis/VorbisParser.js +1 -1
- package/lib/wavpack/WavPackParser.js +2 -2
- package/package.json +146 -146
package/lib/ParserFactory.js
CHANGED
|
@@ -71,7 +71,7 @@ export class ParserFactory {
|
|
|
71
71
|
// Parser could not be determined on MIME-type or extension
|
|
72
72
|
debug('Guess parser on content...');
|
|
73
73
|
await tokenizer.peekBuffer(buf, { mayBeLess: true });
|
|
74
|
-
const guessedType = await fileTypeFromBuffer(buf);
|
|
74
|
+
const guessedType = await fileTypeFromBuffer(buf, { mpegOffsetTolerance: 10 });
|
|
75
75
|
if (!guessedType || !guessedType.mime) {
|
|
76
76
|
throw new CouldNotDetermineFileTypeError('Failed to determine audio format');
|
|
77
77
|
}
|
|
@@ -108,7 +108,7 @@ export class ParserFactory {
|
|
|
108
108
|
try {
|
|
109
109
|
mime = parseHttpContentType(httpContentType);
|
|
110
110
|
}
|
|
111
|
-
catch (
|
|
111
|
+
catch (_err) {
|
|
112
112
|
debug(`Invalid HTTP Content-Type header value: ${httpContentType}`);
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
@@ -18,8 +18,8 @@ declare const ApeContentError_base: {
|
|
|
18
18
|
};
|
|
19
19
|
export declare class ApeContentError extends ApeContentError_base {
|
|
20
20
|
}
|
|
21
|
+
export declare function tryParseApeHeader(metadata: INativeMetadataCollector, tokenizer: strtok3.ITokenizer, options: IOptions): Promise<void>;
|
|
21
22
|
export declare class APEv2Parser extends BasicParser {
|
|
22
|
-
static tryParseApeHeader(metadata: INativeMetadataCollector, tokenizer: strtok3.ITokenizer, options: IOptions): Promise<void>;
|
|
23
23
|
/**
|
|
24
24
|
* Calculate the media file duration
|
|
25
25
|
* @param ah ApeHeader
|
package/lib/apev2/APEv2Parser.js
CHANGED
|
@@ -11,15 +11,15 @@ const tagFormat = 'APEv2';
|
|
|
11
11
|
const preamble = 'APETAGEX';
|
|
12
12
|
export class ApeContentError extends makeUnexpectedFileContentError('APEv2') {
|
|
13
13
|
}
|
|
14
|
+
export function tryParseApeHeader(metadata, tokenizer, options) {
|
|
15
|
+
const apeParser = new APEv2Parser(metadata, tokenizer, options);
|
|
16
|
+
return apeParser.tryParseApeHeader();
|
|
17
|
+
}
|
|
14
18
|
export class APEv2Parser extends BasicParser {
|
|
15
19
|
constructor() {
|
|
16
20
|
super(...arguments);
|
|
17
21
|
this.ape = {};
|
|
18
22
|
}
|
|
19
|
-
static tryParseApeHeader(metadata, tokenizer, options) {
|
|
20
|
-
const apeParser = new APEv2Parser(metadata, tokenizer, options);
|
|
21
|
-
return apeParser.tryParseApeHeader();
|
|
22
|
-
}
|
|
23
23
|
/**
|
|
24
24
|
* Calculate the media file duration
|
|
25
25
|
* @param ah ApeHeader
|
package/lib/asf/AsfObject.d.ts
CHANGED
|
@@ -82,7 +82,7 @@ export declare abstract class State<T> implements IGetToken<T> {
|
|
|
82
82
|
protected postProcessTag(tags: ITag[], name: string, valueType: number, data: AnyTagValue): void;
|
|
83
83
|
}
|
|
84
84
|
export declare class IgnoreObjectState extends State<unknown> {
|
|
85
|
-
get(
|
|
85
|
+
get(_buf: Uint8Array, _off: number): null;
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
88
|
* Interface for: 3.2: File Properties Object (mandatory, one only)
|
package/lib/asf/AsfObject.js
CHANGED
|
@@ -47,5 +47,5 @@ export declare class CommonTagMapper implements IGenericTagMapper {
|
|
|
47
47
|
* @param tag Tag e.g. {"©alb", "Buena Vista Social Club")
|
|
48
48
|
* @param warnings Used to register warnings
|
|
49
49
|
*/
|
|
50
|
-
protected postMap(
|
|
50
|
+
protected postMap(_tag: ITag, _warnings: IWarningCollector): void;
|
|
51
51
|
}
|
package/lib/core.d.ts
CHANGED
|
@@ -63,12 +63,12 @@ export declare function scanAppendingHeaders(tokenizer: IRandomAccessTokenizer,
|
|
|
63
63
|
* Implementation only available when loaded as Node.js
|
|
64
64
|
* This method will throw an Error, always.
|
|
65
65
|
*/
|
|
66
|
-
export declare function parseFile(
|
|
66
|
+
export declare function parseFile(_filePath: string, _options?: IOptions): Promise<IAudioMetadata>;
|
|
67
67
|
/**
|
|
68
68
|
* Implementation only available when loaded as Node.js
|
|
69
69
|
* This method will throw an Error, always.
|
|
70
70
|
*/
|
|
71
|
-
export declare function parseStream(
|
|
71
|
+
export declare function parseStream(_stream: Readable, _fileInfo?: IFileInfo | string, _options?: IOptions): Promise<IAudioMetadata>;
|
|
72
72
|
/**
|
|
73
73
|
* Return a list of supported mime-types
|
|
74
74
|
*/
|
package/lib/core.js
CHANGED
|
@@ -109,14 +109,14 @@ export async function scanAppendingHeaders(tokenizer, options = {}) {
|
|
|
109
109
|
* Implementation only available when loaded as Node.js
|
|
110
110
|
* This method will throw an Error, always.
|
|
111
111
|
*/
|
|
112
|
-
export async function parseFile(
|
|
112
|
+
export async function parseFile(_filePath, _options = {}) {
|
|
113
113
|
throw new Error('This function require a Node engine. To load Web API File objects use parseBlob instead.');
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
116
|
* Implementation only available when loaded as Node.js
|
|
117
117
|
* This method will throw an Error, always.
|
|
118
118
|
*/
|
|
119
|
-
export async function parseStream(
|
|
119
|
+
export async function parseStream(_stream, _fileInfo, _options = {}) {
|
|
120
120
|
throw new Error('This function require a Node engine.');
|
|
121
121
|
}
|
|
122
122
|
/**
|
package/lib/ebml/EbmlIterator.js
CHANGED
package/lib/lrc/LyricsParser.js
CHANGED
|
@@ -7,16 +7,25 @@ import { LyricsContentType, TimestampFormat } from '../type.js';
|
|
|
7
7
|
export function parseLrc(lrcString) {
|
|
8
8
|
const lines = lrcString.split('\n');
|
|
9
9
|
const syncText = [];
|
|
10
|
-
// Regular expression to match LRC timestamps (e.g., [00:45.52])
|
|
11
|
-
const timestampRegex = /\[(\d{2}):(\d{2})\.(\d{2})\]/;
|
|
10
|
+
// Regular expression to match LRC timestamps (e.g., [00:45.52] or [00:45.520])
|
|
11
|
+
const timestampRegex = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/;
|
|
12
12
|
for (const line of lines) {
|
|
13
13
|
const match = line.match(timestampRegex);
|
|
14
14
|
if (match) {
|
|
15
15
|
const minutes = Number.parseInt(match[1], 10);
|
|
16
16
|
const seconds = Number.parseInt(match[2], 10);
|
|
17
|
-
const
|
|
17
|
+
const millisecondsStr = match[3];
|
|
18
|
+
let milliseconds;
|
|
19
|
+
if (millisecondsStr.length === 3) {
|
|
20
|
+
// (e.g., .521 = 521 millseconds)
|
|
21
|
+
milliseconds = Number.parseInt(millisecondsStr, 10);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// (e.g., .52 = 520 millseconds)
|
|
25
|
+
milliseconds = Number.parseInt(millisecondsStr, 10) * 10;
|
|
26
|
+
}
|
|
18
27
|
// Convert the timestamp to milliseconds, as per TimestampFormat.milliseconds
|
|
19
|
-
const timestamp = (minutes * 60 + seconds) * 1000 +
|
|
28
|
+
const timestamp = (minutes * 60 + seconds) * 1000 + milliseconds;
|
|
20
29
|
// Get the text portion of the line (e.g., "あの蝶は自由になれたかな")
|
|
21
30
|
const text = line.replace(timestampRegex, '').trim();
|
|
22
31
|
syncText.push({ timestamp, text });
|
package/lib/mp4/Atom.js
CHANGED
|
@@ -50,6 +50,7 @@ export class Atom {
|
|
|
50
50
|
case '<id>':
|
|
51
51
|
case 'ilst':
|
|
52
52
|
case 'tref':
|
|
53
|
+
case 'moof':
|
|
53
54
|
return this.readAtoms(tokenizer, dataHandler, this.getPayloadLength(remaining));
|
|
54
55
|
case 'meta': { // Metadata Atom, ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW8
|
|
55
56
|
// meta has 4 bytes of padding, ignore
|
package/lib/mp4/AtomToken.d.ts
CHANGED
|
@@ -402,4 +402,64 @@ export declare class ChapterText implements IGetToken<string> {
|
|
|
402
402
|
constructor(len: number);
|
|
403
403
|
get(buf: Uint8Array, off: number): string;
|
|
404
404
|
}
|
|
405
|
+
interface ITrackFragmentHeaderBoxFlags {
|
|
406
|
+
baseDataOffsetPresent: boolean;
|
|
407
|
+
sampleDescriptionIndexPresent: boolean;
|
|
408
|
+
defaultSampleDurationPresent: boolean;
|
|
409
|
+
defaultSampleSizePresent: boolean;
|
|
410
|
+
defaultSampleFlagsPresent: boolean;
|
|
411
|
+
defaultDurationIsEmpty: boolean;
|
|
412
|
+
defaultBaseIsMoof: boolean;
|
|
413
|
+
}
|
|
414
|
+
export interface ITrackFragmentHeaderBox {
|
|
415
|
+
version: number;
|
|
416
|
+
flags: ITrackFragmentHeaderBoxFlags;
|
|
417
|
+
trackId: number;
|
|
418
|
+
baseDataOffset?: bigint;
|
|
419
|
+
sampleDescriptionIndex?: number;
|
|
420
|
+
defaultSampleDuration?: number;
|
|
421
|
+
defaultSampleSize?: number;
|
|
422
|
+
defaultSampleFlags?: number;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Sample-size ('tfhd') TrackFragmentHeaderBox
|
|
426
|
+
*/
|
|
427
|
+
export declare class TrackFragmentHeaderBox implements IGetToken<ITrackFragmentHeaderBox> {
|
|
428
|
+
len: number;
|
|
429
|
+
constructor(len: number);
|
|
430
|
+
get(buf: Uint8Array, off: number): ITrackFragmentHeaderBox;
|
|
431
|
+
}
|
|
432
|
+
interface ITrackRunBoxFlags {
|
|
433
|
+
dataOffsetPresent: boolean;
|
|
434
|
+
firstSampleFlagsPresent: boolean;
|
|
435
|
+
sampleDurationPresent: boolean;
|
|
436
|
+
sampleSizePresent: boolean;
|
|
437
|
+
sampleFlagsPresent: boolean;
|
|
438
|
+
sampleCompositionTimeOffsetsPresent: boolean;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* trun atom
|
|
442
|
+
*/
|
|
443
|
+
export interface ITrackRunBox {
|
|
444
|
+
version: number;
|
|
445
|
+
flags: ITrackRunBoxFlags;
|
|
446
|
+
samples: ITrackRunBoxSample[];
|
|
447
|
+
sampleCount: number;
|
|
448
|
+
dataOffset?: number;
|
|
449
|
+
firstSampleFlags?: number;
|
|
450
|
+
}
|
|
451
|
+
interface ITrackRunBoxSample {
|
|
452
|
+
sampleDuration?: number;
|
|
453
|
+
sampleSize?: number;
|
|
454
|
+
sampleFlags?: number;
|
|
455
|
+
sampleCompositionTimeOffset?: number;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Sample-size ('trun') TrackRunBox
|
|
459
|
+
*/
|
|
460
|
+
export declare class TrackRunBox implements IGetToken<ITrackRunBox> {
|
|
461
|
+
len: number;
|
|
462
|
+
constructor(len: number);
|
|
463
|
+
get(buf: Uint8Array, off: number): ITrackRunBox;
|
|
464
|
+
}
|
|
405
465
|
export {};
|
package/lib/mp4/AtomToken.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as Token from 'token-types';
|
|
|
2
2
|
import initDebug from 'debug';
|
|
3
3
|
import { FourCcToken } from '../common/FourCC.js';
|
|
4
4
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
5
|
+
import * as util from '../common/Util.js';
|
|
5
6
|
const debug = initDebug('music-metadata:parser:MP4:atom');
|
|
6
7
|
export class Mp4ContentError extends makeUnexpectedFileContentError('MP4') {
|
|
7
8
|
}
|
|
@@ -390,4 +391,107 @@ function readTokenTable(buf, token, off, remainingLen, numberOfEntries) {
|
|
|
390
391
|
}
|
|
391
392
|
return entries;
|
|
392
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Sample-size ('tfhd') TrackFragmentHeaderBox
|
|
396
|
+
*/
|
|
397
|
+
export class TrackFragmentHeaderBox {
|
|
398
|
+
constructor(len) {
|
|
399
|
+
this.len = len;
|
|
400
|
+
}
|
|
401
|
+
get(buf, off) {
|
|
402
|
+
const flagOffset = off + 1;
|
|
403
|
+
const header = {
|
|
404
|
+
version: Token.INT8.get(buf, off),
|
|
405
|
+
flags: {
|
|
406
|
+
baseDataOffsetPresent: util.getBit(buf, flagOffset + 2, 0),
|
|
407
|
+
sampleDescriptionIndexPresent: util.getBit(buf, flagOffset + 2, 1),
|
|
408
|
+
defaultSampleDurationPresent: util.getBit(buf, flagOffset + 2, 3),
|
|
409
|
+
defaultSampleSizePresent: util.getBit(buf, flagOffset + 2, 4),
|
|
410
|
+
defaultSampleFlagsPresent: util.getBit(buf, flagOffset + 2, 5),
|
|
411
|
+
defaultDurationIsEmpty: util.getBit(buf, flagOffset, 0),
|
|
412
|
+
defaultBaseIsMoof: util.getBit(buf, flagOffset, 1)
|
|
413
|
+
},
|
|
414
|
+
trackId: Token.UINT32_BE.get(buf, 4)
|
|
415
|
+
};
|
|
416
|
+
let dynOffset = 8;
|
|
417
|
+
if (header.flags.baseDataOffsetPresent) {
|
|
418
|
+
header.baseDataOffset = Token.UINT64_BE.get(buf, dynOffset);
|
|
419
|
+
dynOffset += 8;
|
|
420
|
+
}
|
|
421
|
+
if (header.flags.sampleDescriptionIndexPresent) {
|
|
422
|
+
header.sampleDescriptionIndex = Token.UINT32_BE.get(buf, dynOffset);
|
|
423
|
+
dynOffset += 4;
|
|
424
|
+
}
|
|
425
|
+
if (header.flags.defaultSampleDurationPresent) {
|
|
426
|
+
header.defaultSampleDuration = Token.UINT32_BE.get(buf, dynOffset);
|
|
427
|
+
dynOffset += 4;
|
|
428
|
+
}
|
|
429
|
+
if (header.flags.defaultSampleSizePresent) {
|
|
430
|
+
header.defaultSampleSize = Token.UINT32_BE.get(buf, dynOffset);
|
|
431
|
+
dynOffset += 4;
|
|
432
|
+
}
|
|
433
|
+
if (header.flags.defaultSampleFlagsPresent) {
|
|
434
|
+
header.defaultSampleFlags = Token.UINT32_BE.get(buf, dynOffset);
|
|
435
|
+
}
|
|
436
|
+
return header;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Sample-size ('trun') TrackRunBox
|
|
441
|
+
*/
|
|
442
|
+
export class TrackRunBox {
|
|
443
|
+
constructor(len) {
|
|
444
|
+
this.len = len;
|
|
445
|
+
}
|
|
446
|
+
get(buf, off) {
|
|
447
|
+
const flagOffset = off + 1;
|
|
448
|
+
const trun = {
|
|
449
|
+
version: Token.INT8.get(buf, off),
|
|
450
|
+
flags: {
|
|
451
|
+
dataOffsetPresent: util.getBit(buf, flagOffset + 2, 0),
|
|
452
|
+
firstSampleFlagsPresent: util.getBit(buf, flagOffset + 2, 2),
|
|
453
|
+
sampleDurationPresent: util.getBit(buf, flagOffset + 1, 0),
|
|
454
|
+
sampleSizePresent: util.getBit(buf, flagOffset + 1, 1),
|
|
455
|
+
sampleFlagsPresent: util.getBit(buf, flagOffset + 1, 2),
|
|
456
|
+
sampleCompositionTimeOffsetsPresent: util.getBit(buf, flagOffset + 1, 3)
|
|
457
|
+
},
|
|
458
|
+
sampleCount: Token.UINT32_BE.get(buf, off + 4),
|
|
459
|
+
samples: []
|
|
460
|
+
};
|
|
461
|
+
let dynOffset = off + 8;
|
|
462
|
+
if (trun.flags.dataOffsetPresent) {
|
|
463
|
+
trun.dataOffset = Token.UINT32_BE.get(buf, dynOffset);
|
|
464
|
+
dynOffset += 4;
|
|
465
|
+
}
|
|
466
|
+
if (trun.flags.firstSampleFlagsPresent) {
|
|
467
|
+
trun.firstSampleFlags = Token.UINT32_BE.get(buf, dynOffset);
|
|
468
|
+
dynOffset += 4;
|
|
469
|
+
}
|
|
470
|
+
for (let n = 0; n < trun.sampleCount; ++n) {
|
|
471
|
+
if (dynOffset >= this.len) {
|
|
472
|
+
debug("TrackRunBox size mismatch");
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
const sample = {};
|
|
476
|
+
if (trun.flags.sampleDurationPresent) {
|
|
477
|
+
sample.sampleDuration = Token.UINT32_BE.get(buf, dynOffset);
|
|
478
|
+
dynOffset += 4;
|
|
479
|
+
}
|
|
480
|
+
if (trun.flags.sampleSizePresent) {
|
|
481
|
+
sample.sampleSize = Token.UINT32_BE.get(buf, dynOffset);
|
|
482
|
+
dynOffset += 4;
|
|
483
|
+
}
|
|
484
|
+
if (trun.flags.sampleFlagsPresent) {
|
|
485
|
+
sample.sampleFlags = Token.UINT32_BE.get(buf, dynOffset);
|
|
486
|
+
dynOffset += 4;
|
|
487
|
+
}
|
|
488
|
+
if (trun.flags.sampleCompositionTimeOffsetsPresent) {
|
|
489
|
+
sample.sampleCompositionTimeOffset = Token.UINT32_BE.get(buf, dynOffset);
|
|
490
|
+
dynOffset += 4;
|
|
491
|
+
}
|
|
492
|
+
trun.samples.push(sample);
|
|
493
|
+
}
|
|
494
|
+
return trun;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
393
497
|
//# sourceMappingURL=AtomToken.js.map
|
package/lib/mp4/MP4Parser.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare class MP4Parser extends BasicParser {
|
|
|
4
4
|
private static read_BE_Integer;
|
|
5
5
|
private audioLengthInBytes;
|
|
6
6
|
private tracks;
|
|
7
|
+
private getTrackById;
|
|
7
8
|
parse(): Promise<void>;
|
|
8
9
|
handleAtom(atom: Atom, remaining: number): Promise<void>;
|
|
9
10
|
private getTrackDescription;
|
|
@@ -17,6 +18,7 @@ export declare class MP4Parser extends BasicParser {
|
|
|
17
18
|
*/
|
|
18
19
|
private parseMetadataItemData;
|
|
19
20
|
private parseValueAtom;
|
|
21
|
+
private parseTrackFragmentBox;
|
|
20
22
|
private atomParsers;
|
|
21
23
|
/**
|
|
22
24
|
* @param sampleDescription
|
package/lib/mp4/MP4Parser.js
CHANGED
|
@@ -127,6 +127,7 @@ export class MP4Parser extends BasicParser {
|
|
|
127
127
|
},
|
|
128
128
|
tkhd: async (len) => {
|
|
129
129
|
const track = (await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(len)));
|
|
130
|
+
track.fragments = [];
|
|
130
131
|
this.tracks.push(track);
|
|
131
132
|
},
|
|
132
133
|
/**
|
|
@@ -214,6 +215,9 @@ export class MP4Parser extends BasicParser {
|
|
|
214
215
|
}
|
|
215
216
|
return Number(token.get(array, 0));
|
|
216
217
|
}
|
|
218
|
+
getTrackById(trackId) {
|
|
219
|
+
return this.tracks.find(track => track.trackId === trackId);
|
|
220
|
+
}
|
|
217
221
|
async parse() {
|
|
218
222
|
this.tracks = [];
|
|
219
223
|
let remainingFileSize = this.tokenizer.fileInfo.size || 0;
|
|
@@ -280,8 +284,26 @@ export class MP4Parser extends BasicParser {
|
|
|
280
284
|
if (audioTracks.length >= 1) {
|
|
281
285
|
const audioTrack = audioTracks[0];
|
|
282
286
|
if (audioTrack.timeScale > 0) {
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
if (audioTrack.duration > 0) {
|
|
288
|
+
debug('Using duration defined on audio track');
|
|
289
|
+
const duration = audioTrack.duration / audioTrack.timeScale; // calculate duration in seconds
|
|
290
|
+
this.metadata.setFormat('duration', duration);
|
|
291
|
+
}
|
|
292
|
+
else if (audioTrack.fragments.length > 0) {
|
|
293
|
+
debug('Calculate duration defined in track fragments');
|
|
294
|
+
let totalTimeUnits = 0;
|
|
295
|
+
for (const fragment of audioTrack.fragments) {
|
|
296
|
+
const defaultDuration = fragment.header.defaultSampleDuration;
|
|
297
|
+
for (const sample of fragment.trackRun.samples) {
|
|
298
|
+
const dur = sample.sampleDuration ?? defaultDuration;
|
|
299
|
+
if (dur == null) {
|
|
300
|
+
throw new Error("Missing sampleDuration and no default_sample_duration in tfhd");
|
|
301
|
+
}
|
|
302
|
+
totalTimeUnits += dur;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
this.metadata.setFormat('duration', totalTimeUnits / audioTrack.timeScale);
|
|
306
|
+
}
|
|
285
307
|
}
|
|
286
308
|
const ssd = audioTrack.soundSampleDescription[0];
|
|
287
309
|
if (ssd.description) {
|
|
@@ -309,6 +331,11 @@ export class MP4Parser extends BasicParser {
|
|
|
309
331
|
case 'ilst':
|
|
310
332
|
case '<id>':
|
|
311
333
|
return this.parseMetadataItemData(atom);
|
|
334
|
+
case 'moof':
|
|
335
|
+
switch (atom.header.name) {
|
|
336
|
+
case 'traf':
|
|
337
|
+
return this.parseTrackFragmentBox(atom);
|
|
338
|
+
}
|
|
312
339
|
}
|
|
313
340
|
}
|
|
314
341
|
// const payloadLength = atom.getPayloadLength(remaining);
|
|
@@ -432,6 +459,35 @@ export class MP4Parser extends BasicParser {
|
|
|
432
459
|
this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`);
|
|
433
460
|
}
|
|
434
461
|
}
|
|
462
|
+
parseTrackFragmentBox(trafBox) {
|
|
463
|
+
let tfhd;
|
|
464
|
+
return trafBox.readAtoms(this.tokenizer, async (child, remaining) => {
|
|
465
|
+
const payLoadLength = child.getPayloadLength(remaining);
|
|
466
|
+
switch (child.header.name) {
|
|
467
|
+
case 'tfhd': { // TrackFragmentHeaderBox
|
|
468
|
+
const fragmentHeaderBox = new AtomToken.TrackFragmentHeaderBox(child.getPayloadLength(remaining));
|
|
469
|
+
tfhd = await this.tokenizer.readToken(fragmentHeaderBox);
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
case 'tfdt': // TrackFragmentBaseMediaDecodeTimeBo
|
|
473
|
+
await this.tokenizer.ignore(payLoadLength);
|
|
474
|
+
break;
|
|
475
|
+
case 'trun': { // TrackRunBox
|
|
476
|
+
const trackRunBox = new AtomToken.TrackRunBox(payLoadLength);
|
|
477
|
+
const trun = await this.tokenizer.readToken(trackRunBox);
|
|
478
|
+
if (tfhd) {
|
|
479
|
+
const track = this.getTrackById(tfhd.trackId);
|
|
480
|
+
track?.fragments.push({ header: tfhd, trackRun: trun });
|
|
481
|
+
}
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
default: {
|
|
485
|
+
debug(`Unexpected box: ${child.header.name}`);
|
|
486
|
+
await this.tokenizer.ignore(payLoadLength);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}, trafBox.getPayloadLength(0));
|
|
490
|
+
}
|
|
435
491
|
/**
|
|
436
492
|
* @param sampleDescription
|
|
437
493
|
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-128916
|
|
@@ -487,11 +543,6 @@ export class MP4Parser extends BasicParser {
|
|
|
487
543
|
await this.tokenizer.ignore(len);
|
|
488
544
|
}
|
|
489
545
|
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
546
|
let chunkIndex = 0;
|
|
496
547
|
while (chunkIndex < track.chunkOffsetTable.length && track.chunkOffsetTable[chunkIndex] < chapterOffset) {
|
|
497
548
|
++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
|
@@ -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);
|
|
@@ -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}`);
|
|
@@ -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,15 +23,13 @@ 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);
|
|
@@ -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,7 +90,7 @@ 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
95
|
debug('Parse first page');
|
|
96
96
|
// Parse Vorbis common header
|
|
@@ -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';
|
|
@@ -22,7 +22,7 @@ export class WavPackParser extends BasicParser {
|
|
|
22
22
|
// First parse all WavPack blocks
|
|
23
23
|
await this.parseWavPackBlocks();
|
|
24
24
|
// try to parse APEv2 header
|
|
25
|
-
return
|
|
25
|
+
return tryParseApeHeader(this.metadata, this.tokenizer, this.options);
|
|
26
26
|
}
|
|
27
27
|
async parseWavPackBlocks() {
|
|
28
28
|
do {
|