music-metadata 11.4.0 → 11.6.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 +9 -0
- package/lib/aiff/AiffParser.js +1 -0
- package/lib/apev2/APEv2Parser.js +1 -0
- package/lib/common/MetadataCollector.d.ts +2 -0
- package/lib/common/MetadataCollector.js +5 -1
- package/lib/core.js +1 -4
- package/lib/dsdiff/DsdiffParser.js +1 -0
- package/lib/dsf/DsfParser.js +1 -0
- package/lib/flac/FlacParser.js +3 -2
- package/lib/mp4/Atom.js +0 -1
- package/lib/mp4/AtomToken.d.ts +24 -2
- package/lib/mp4/AtomToken.js +37 -9
- package/lib/mp4/MP4Parser.d.ts +3 -1
- package/lib/mp4/MP4Parser.js +106 -58
- package/lib/mpeg/MpegParser.js +1 -0
- package/lib/musepack/MusepackParser.js +1 -0
- package/lib/ogg/OggParser.d.ts +1 -12
- package/lib/ogg/OggParser.js +80 -94
- package/lib/ogg/{Ogg.d.ts → OggToken.d.ts} +11 -4
- package/lib/ogg/OggToken.js +42 -0
- package/lib/ogg/opus/{OpusParser.d.ts → OpusStream.d.ts} +5 -5
- package/lib/ogg/opus/{OpusParser.js → OpusStream.js} +8 -7
- package/lib/ogg/speex/{SpeexParser.d.ts → SpeexStream.d.ts} +3 -3
- package/lib/ogg/speex/{SpeexParser.js → SpeexStream.js} +4 -3
- package/lib/ogg/theora/{TheoraParser.d.ts → TheoraStream.d.ts} +4 -4
- package/lib/ogg/theora/{TheoraParser.js → TheoraStream.js} +7 -6
- package/lib/ogg/vorbis/{VorbisParser.d.ts → VorbisStream.d.ts} +5 -4
- package/lib/ogg/vorbis/{VorbisParser.js → VorbisStream.js} +11 -12
- package/lib/type.d.ts +9 -1
- package/lib/wav/WaveParser.js +1 -0
- package/lib/wavpack/WavPackParser.js +1 -0
- package/package.json +146 -146
- package/lib/ogg/Ogg.js +0 -2
package/lib/ogg/OggParser.js
CHANGED
|
@@ -1,33 +1,70 @@
|
|
|
1
1
|
import * as Token from 'token-types';
|
|
2
2
|
import { EndOfStreamError } from 'strtok3';
|
|
3
3
|
import initDebug from 'debug';
|
|
4
|
-
import * as util from '../common/Util.js';
|
|
5
|
-
import { FourCcToken } from '../common/FourCC.js';
|
|
6
4
|
import { BasicParser } from '../common/BasicParser.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
5
|
+
import { VorbisStream } from './vorbis/VorbisStream.js';
|
|
6
|
+
import { OpusStream } from './opus/OpusStream.js';
|
|
7
|
+
import { SpeexStream } from './speex/SpeexStream.js';
|
|
8
|
+
import { TheoraStream } from './theora/TheoraStream.js';
|
|
11
9
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
10
|
+
import { PageHeader, SegmentTable } from './OggToken.js';
|
|
12
11
|
export class OggContentError extends makeUnexpectedFileContentError('Ogg') {
|
|
13
12
|
}
|
|
14
13
|
const debug = initDebug('music-metadata:parser:ogg');
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return s;
|
|
23
|
-
}
|
|
24
|
-
constructor(header) {
|
|
25
|
-
this.len = header.page_segments;
|
|
14
|
+
class OggStream {
|
|
15
|
+
constructor(metadata, streamSerial, options) {
|
|
16
|
+
this.pageNumber = 0;
|
|
17
|
+
this.closed = false;
|
|
18
|
+
this.metadata = metadata;
|
|
19
|
+
this.streamSerial = streamSerial;
|
|
20
|
+
this.options = options;
|
|
26
21
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
async parsePage(tokenizer, header) {
|
|
23
|
+
this.pageNumber = header.pageSequenceNo;
|
|
24
|
+
debug('serial=%s page#=%s, Ogg.id=%s', header.streamSerialNumber, header.pageSequenceNo, header.capturePattern);
|
|
25
|
+
const segmentTable = await tokenizer.readToken(new SegmentTable(header));
|
|
26
|
+
debug('totalPageSize=%s', segmentTable.totalPageSize);
|
|
27
|
+
const pageData = await tokenizer.readToken(new Token.Uint8ArrayType(segmentTable.totalPageSize));
|
|
28
|
+
debug('firstPage=%s, lastPage=%s, continued=%s', header.headerType.firstPage, header.headerType.lastPage, header.headerType.continued);
|
|
29
|
+
if (header.headerType.firstPage) {
|
|
30
|
+
const idData = pageData.slice(0, 7); // Copy this portion
|
|
31
|
+
switch (idData[0]) {
|
|
32
|
+
case 0x01:
|
|
33
|
+
case 0x80:
|
|
34
|
+
idData[0] = 0x5F; // underscore
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
const id = new TextDecoder('latin1').decode(idData);
|
|
38
|
+
switch (id) {
|
|
39
|
+
case '_vorbis': // Ogg/Vorbis
|
|
40
|
+
debug(`Set Ogg stream serial ${header.streamSerialNumber}, codec=Vorbis`);
|
|
41
|
+
this.pageConsumer = new VorbisStream(this.metadata, this.options);
|
|
42
|
+
break;
|
|
43
|
+
case 'OpusHea': // Ogg/Opus
|
|
44
|
+
debug('Set page consumer to Ogg/Opus');
|
|
45
|
+
this.pageConsumer = new OpusStream(this.metadata, this.options, tokenizer);
|
|
46
|
+
break;
|
|
47
|
+
case 'Speex ': // Ogg/Speex
|
|
48
|
+
debug('Set page consumer to Ogg/Speex');
|
|
49
|
+
this.pageConsumer = new SpeexStream(this.metadata, this.options, tokenizer);
|
|
50
|
+
break;
|
|
51
|
+
case 'fishead':
|
|
52
|
+
case '_theora': // Ogg/Theora
|
|
53
|
+
debug('Set page consumer to Ogg/Theora');
|
|
54
|
+
this.pageConsumer = new TheoraStream(this.metadata, this.options, tokenizer);
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
throw new OggContentError(`Ogg codec not recognized (id=${id})`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (header.headerType.lastPage) {
|
|
61
|
+
this.closed = true;
|
|
62
|
+
}
|
|
63
|
+
if (this.pageConsumer) {
|
|
64
|
+
await this.pageConsumer.parsePage(header, pageData);
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
throw new Error('pageConsumer should be initialized');
|
|
31
68
|
}
|
|
32
69
|
}
|
|
33
70
|
/**
|
|
@@ -36,98 +73,47 @@ export class SegmentTable {
|
|
|
36
73
|
export class OggParser extends BasicParser {
|
|
37
74
|
constructor() {
|
|
38
75
|
super(...arguments);
|
|
39
|
-
this.
|
|
40
|
-
this.pageNumber = 0;
|
|
41
|
-
this.pageConsumer = null;
|
|
76
|
+
this.streams = new Map();
|
|
42
77
|
}
|
|
43
78
|
/**
|
|
44
79
|
* Parse page
|
|
45
80
|
* @returns {Promise<void>}
|
|
46
81
|
*/
|
|
47
82
|
async parse() {
|
|
83
|
+
this.streams = new Map();
|
|
48
84
|
debug('pos=%s, parsePage()', this.tokenizer.position);
|
|
85
|
+
let header;
|
|
49
86
|
try {
|
|
50
|
-
let header;
|
|
51
87
|
do {
|
|
52
|
-
header = await this.tokenizer.readToken(
|
|
88
|
+
header = await this.tokenizer.readToken(PageHeader);
|
|
53
89
|
if (header.capturePattern !== 'OggS')
|
|
54
90
|
throw new OggContentError('Invalid Ogg capture pattern');
|
|
55
91
|
this.metadata.setFormat('container', 'Ogg');
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
debug('totalPageSize=%s', segmentTable.totalPageSize);
|
|
61
|
-
const pageData = await this.tokenizer.readToken(new Token.Uint8ArrayType(segmentTable.totalPageSize));
|
|
62
|
-
debug('firstPage=%s, lastPage=%s, continued=%s', header.headerType.firstPage, header.headerType.lastPage, header.headerType.continued);
|
|
63
|
-
if (header.headerType.firstPage) {
|
|
64
|
-
const id = new TextDecoder('ascii').decode(pageData.subarray(0, 7));
|
|
65
|
-
switch (id) {
|
|
66
|
-
case '\x01vorbis': // Ogg/Vorbis
|
|
67
|
-
debug('Set page consumer to Ogg/Vorbis');
|
|
68
|
-
this.pageConsumer = new VorbisParser(this.metadata, this.options);
|
|
69
|
-
break;
|
|
70
|
-
case 'OpusHea': // Ogg/Opus
|
|
71
|
-
debug('Set page consumer to Ogg/Opus');
|
|
72
|
-
this.pageConsumer = new OpusParser(this.metadata, this.options, this.tokenizer);
|
|
73
|
-
break;
|
|
74
|
-
case 'Speex ': // Ogg/Speex
|
|
75
|
-
debug('Set page consumer to Ogg/Speex');
|
|
76
|
-
this.pageConsumer = new SpeexParser(this.metadata, this.options, this.tokenizer);
|
|
77
|
-
break;
|
|
78
|
-
case 'fishead':
|
|
79
|
-
case '\x00theora': // Ogg/Theora
|
|
80
|
-
debug('Set page consumer to Ogg/Theora');
|
|
81
|
-
this.pageConsumer = new TheoraParser(this.metadata, this.options, this.tokenizer);
|
|
82
|
-
break;
|
|
83
|
-
default:
|
|
84
|
-
throw new OggContentError(`gg audio-codec not recognized (id=${id})`);
|
|
85
|
-
}
|
|
92
|
+
let stream = this.streams.get(header.streamSerialNumber);
|
|
93
|
+
if (!stream) {
|
|
94
|
+
stream = new OggStream(this.metadata, header.streamSerialNumber, this.options);
|
|
95
|
+
this.streams.set(header.streamSerialNumber, stream);
|
|
86
96
|
}
|
|
87
|
-
await
|
|
88
|
-
} while (!
|
|
97
|
+
await stream.parsePage(this.tokenizer, header);
|
|
98
|
+
} while (![...this.streams.values()].every(item => item.closed));
|
|
89
99
|
}
|
|
90
100
|
catch (err) {
|
|
91
|
-
if (err instanceof
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (this.header) {
|
|
97
|
-
this.pageConsumer.calculateDuration(this.header);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else if (err.message.startsWith('FourCC')) {
|
|
101
|
-
if (this.pageNumber > 0) {
|
|
102
|
-
// ignore this error: work-around if last OGG-page is not marked with last-page flag
|
|
103
|
-
this.metadata.addWarning('Invalid FourCC ID, maybe last OGG-page is not marked with last-page flag');
|
|
104
|
-
await this.pageConsumer.flush();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
101
|
+
if (err instanceof EndOfStreamError) {
|
|
102
|
+
debug("Reached end-of-stream");
|
|
103
|
+
}
|
|
104
|
+
else if (err instanceof OggContentError) {
|
|
105
|
+
this.metadata.addWarning(`Corrupt Ogg content at ${this.tokenizer.position}`);
|
|
107
106
|
}
|
|
108
107
|
else
|
|
109
108
|
throw err;
|
|
110
109
|
}
|
|
110
|
+
for (const stream of this.streams.values()) {
|
|
111
|
+
if (!stream.closed) {
|
|
112
|
+
this.metadata.addWarning(`End-of-stream reached before reaching last page in Ogg stream serial=${stream.streamSerial}`);
|
|
113
|
+
await stream.pageConsumer?.flush();
|
|
114
|
+
}
|
|
115
|
+
stream.pageConsumer?.calculateDuration();
|
|
116
|
+
}
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
|
-
OggParser.Header = {
|
|
114
|
-
len: 27,
|
|
115
|
-
get: (buf, off) => {
|
|
116
|
-
return {
|
|
117
|
-
capturePattern: FourCcToken.get(buf, off),
|
|
118
|
-
version: Token.UINT8.get(buf, off + 4),
|
|
119
|
-
headerType: {
|
|
120
|
-
continued: util.getBit(buf, off + 5, 0),
|
|
121
|
-
firstPage: util.getBit(buf, off + 5, 1),
|
|
122
|
-
lastPage: util.getBit(buf, off + 5, 2)
|
|
123
|
-
},
|
|
124
|
-
// packet_flag: Token.UINT8.get(buf, off + 5),
|
|
125
|
-
absoluteGranulePosition: Number(Token.UINT64_LE.get(buf, off + 6)),
|
|
126
|
-
streamSerialNumber: Token.UINT32_LE.get(buf, off + 14),
|
|
127
|
-
pageSequenceNo: Token.UINT32_LE.get(buf, off + 18),
|
|
128
|
-
pageChecksum: Token.UINT32_LE.get(buf, off + 22),
|
|
129
|
-
page_segments: Token.UINT8.get(buf, off + 26)
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
119
|
//# sourceMappingURL=OggParser.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IGetToken } from 'strtok3';
|
|
1
2
|
/**
|
|
2
3
|
* Page header
|
|
3
4
|
* Ref: https://www.xiph.org/ogg/doc/framing.html#page_header
|
|
@@ -50,21 +51,27 @@ export interface IPageHeader {
|
|
|
50
51
|
*/
|
|
51
52
|
page_segments: number;
|
|
52
53
|
}
|
|
54
|
+
export declare const PageHeader: IGetToken<IPageHeader>;
|
|
53
55
|
export interface ISegmentTable {
|
|
54
56
|
totalPageSize: number;
|
|
55
57
|
}
|
|
58
|
+
export declare class SegmentTable implements IGetToken<ISegmentTable> {
|
|
59
|
+
private static sum;
|
|
60
|
+
len: number;
|
|
61
|
+
constructor(header: IPageHeader);
|
|
62
|
+
get(buf: Uint8Array, off: number): ISegmentTable;
|
|
63
|
+
}
|
|
56
64
|
export interface IPageConsumer {
|
|
57
65
|
/**
|
|
58
66
|
* Parse Ogg page
|
|
59
|
-
* @param
|
|
60
|
-
* @param
|
|
67
|
+
* @param header Ogg page header
|
|
68
|
+
* @param pageData Ogg page data
|
|
61
69
|
*/
|
|
62
70
|
parsePage(header: IPageHeader, pageData: Uint8Array): Promise<void>;
|
|
63
71
|
/**
|
|
64
72
|
* Calculate duration of provided header
|
|
65
|
-
* @param header Ogg header
|
|
66
73
|
*/
|
|
67
|
-
calculateDuration(
|
|
74
|
+
calculateDuration(): void;
|
|
68
75
|
/**
|
|
69
76
|
* Force to parse pending segments
|
|
70
77
|
*/
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as Token from 'token-types';
|
|
2
|
+
import * as util from '../common/Util.js';
|
|
3
|
+
import { StringType } from 'token-types';
|
|
4
|
+
export const PageHeader = {
|
|
5
|
+
len: 27,
|
|
6
|
+
get: (buf, off) => {
|
|
7
|
+
return {
|
|
8
|
+
capturePattern: new StringType(4, 'latin1').get(buf, off),
|
|
9
|
+
version: Token.UINT8.get(buf, off + 4),
|
|
10
|
+
headerType: {
|
|
11
|
+
continued: util.getBit(buf, off + 5, 0),
|
|
12
|
+
firstPage: util.getBit(buf, off + 5, 1),
|
|
13
|
+
lastPage: util.getBit(buf, off + 5, 2)
|
|
14
|
+
},
|
|
15
|
+
// packet_flag: Token.UINT8.get(buf, off + 5),
|
|
16
|
+
absoluteGranulePosition: Number(Token.UINT64_LE.get(buf, off + 6)),
|
|
17
|
+
streamSerialNumber: Token.UINT32_LE.get(buf, off + 14),
|
|
18
|
+
pageSequenceNo: Token.UINT32_LE.get(buf, off + 18),
|
|
19
|
+
pageChecksum: Token.UINT32_LE.get(buf, off + 22),
|
|
20
|
+
page_segments: Token.UINT8.get(buf, off + 26)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export class SegmentTable {
|
|
25
|
+
static sum(buf, off, len) {
|
|
26
|
+
const dv = new DataView(buf.buffer, 0);
|
|
27
|
+
let s = 0;
|
|
28
|
+
for (let i = off; i < off + len; ++i) {
|
|
29
|
+
s += dv.getUint8(i);
|
|
30
|
+
}
|
|
31
|
+
return s;
|
|
32
|
+
}
|
|
33
|
+
constructor(header) {
|
|
34
|
+
this.len = header.page_segments;
|
|
35
|
+
}
|
|
36
|
+
get(buf, off) {
|
|
37
|
+
return {
|
|
38
|
+
totalPageSize: SegmentTable.sum(buf, off, this.len)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=OggToken.js.map
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { ITokenizer } from 'strtok3';
|
|
2
|
-
import type { IPageHeader } from '../
|
|
3
|
-
import {
|
|
2
|
+
import type { IPageHeader } from '../OggToken.js';
|
|
3
|
+
import { VorbisStream } from '../vorbis/VorbisStream.js';
|
|
4
4
|
import type { IOptions } from '../../type.js';
|
|
5
5
|
import type { INativeMetadataCollector } from '../../common/MetadataCollector.js';
|
|
6
6
|
/**
|
|
7
7
|
* Opus parser
|
|
8
8
|
* Internet Engineering Task Force (IETF) - RFC 6716
|
|
9
|
-
* Used by
|
|
9
|
+
* Used by OggStream
|
|
10
10
|
*/
|
|
11
|
-
export declare class
|
|
11
|
+
export declare class OpusStream extends VorbisStream {
|
|
12
12
|
private idHeader;
|
|
13
13
|
private lastPos;
|
|
14
14
|
private tokenizer;
|
|
@@ -20,5 +20,5 @@ export declare class OpusParser extends VorbisParser {
|
|
|
20
20
|
*/
|
|
21
21
|
protected parseFirstPage(_header: IPageHeader, pageData: Uint8Array): void;
|
|
22
22
|
protected parseFullPage(pageData: Uint8Array): Promise<void>;
|
|
23
|
-
calculateDuration(
|
|
23
|
+
calculateDuration(): void;
|
|
24
24
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as Token from 'token-types';
|
|
2
|
-
import {
|
|
2
|
+
import { VorbisStream } from '../vorbis/VorbisStream.js';
|
|
3
3
|
import * as Opus from './Opus.js';
|
|
4
4
|
import { OpusContentError } from './Opus.js';
|
|
5
5
|
/**
|
|
6
6
|
* Opus parser
|
|
7
7
|
* Internet Engineering Task Force (IETF) - RFC 6716
|
|
8
|
-
* Used by
|
|
8
|
+
* Used by OggStream
|
|
9
9
|
*/
|
|
10
|
-
export class
|
|
10
|
+
export class OpusStream extends VorbisStream {
|
|
11
11
|
constructor(metadata, options, tokenizer) {
|
|
12
12
|
super(metadata, options);
|
|
13
13
|
this.idHeader = null;
|
|
@@ -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);
|
|
@@ -39,10 +40,10 @@ export class OpusParser extends VorbisParser {
|
|
|
39
40
|
break;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
calculateDuration(
|
|
43
|
-
if (this.metadata.format.sampleRate &&
|
|
43
|
+
calculateDuration() {
|
|
44
|
+
if (this.lastPageHeader && this.metadata.format.sampleRate && this.lastPageHeader.absoluteGranulePosition >= 0) {
|
|
44
45
|
// Calculate duration
|
|
45
|
-
const pos_48bit =
|
|
46
|
+
const pos_48bit = this.lastPageHeader.absoluteGranulePosition - this.idHeader.preSkip;
|
|
46
47
|
this.metadata.setFormat('numberOfSamples', pos_48bit);
|
|
47
48
|
this.metadata.setFormat('duration', pos_48bit / 48000);
|
|
48
49
|
if (this.lastPos !== -1 && this.tokenizer.fileInfo.size && this.metadata.format.duration) {
|
|
@@ -52,4 +53,4 @@ export class OpusParser extends VorbisParser {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
//# sourceMappingURL=
|
|
56
|
+
//# sourceMappingURL=OpusStream.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ITokenizer } from 'strtok3';
|
|
2
|
-
import type { IPageHeader } from '../
|
|
3
|
-
import {
|
|
2
|
+
import type { IPageHeader } from '../OggToken.js';
|
|
3
|
+
import { VorbisStream } from '../vorbis/VorbisStream.js';
|
|
4
4
|
import type { IOptions } from '../../type.js';
|
|
5
5
|
import type { INativeMetadataCollector } from '../../common/MetadataCollector.js';
|
|
6
6
|
/**
|
|
@@ -9,7 +9,7 @@ import type { INativeMetadataCollector } from '../../common/MetadataCollector.js
|
|
|
9
9
|
* - https://www.speex.org/docs/manual/speex-manual/
|
|
10
10
|
* - https://tools.ietf.org/html/rfc5574
|
|
11
11
|
*/
|
|
12
|
-
export declare class
|
|
12
|
+
export declare class SpeexStream extends VorbisStream {
|
|
13
13
|
private tokenizer;
|
|
14
14
|
constructor(metadata: INativeMetadataCollector, options: IOptions, tokenizer: ITokenizer);
|
|
15
15
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
|
-
import {
|
|
2
|
+
import { VorbisStream } from '../vorbis/VorbisStream.js';
|
|
3
3
|
import * as Speex from './Speex.js';
|
|
4
4
|
const debug = initDebug('music-metadata:parser:ogg:speex');
|
|
5
5
|
/**
|
|
@@ -8,7 +8,7 @@ const debug = initDebug('music-metadata:parser:ogg:speex');
|
|
|
8
8
|
* - https://www.speex.org/docs/manual/speex-manual/
|
|
9
9
|
* - https://tools.ietf.org/html/rfc5574
|
|
10
10
|
*/
|
|
11
|
-
export class
|
|
11
|
+
export class SpeexStream extends VorbisStream {
|
|
12
12
|
constructor(metadata, options, tokenizer) {
|
|
13
13
|
super(metadata, options);
|
|
14
14
|
this.tokenizer = tokenizer;
|
|
@@ -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
|
-
//# sourceMappingURL=
|
|
33
|
+
//# sourceMappingURL=SpeexStream.js.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ITokenizer } from 'strtok3';
|
|
2
|
-
import type * as Ogg from '../
|
|
2
|
+
import type * as Ogg from '../OggToken.js';
|
|
3
3
|
import type { IOptions } from '../../type.js';
|
|
4
4
|
import type { INativeMetadataCollector } from '../../common/MetadataCollector.js';
|
|
5
5
|
/**
|
|
6
6
|
* Ref:
|
|
7
7
|
* - https://theora.org/doc/Theora.pdf
|
|
8
8
|
*/
|
|
9
|
-
export declare class
|
|
9
|
+
export declare class TheoraStream implements Ogg.IPageConsumer {
|
|
10
10
|
private metadata;
|
|
11
11
|
private tokenizer;
|
|
12
12
|
constructor(metadata: INativeMetadataCollector, _options: IOptions, tokenizer: ITokenizer);
|
|
@@ -16,10 +16,10 @@ export declare class TheoraParser implements Ogg.IPageConsumer {
|
|
|
16
16
|
* @param pageData Page data
|
|
17
17
|
*/
|
|
18
18
|
parsePage(header: Ogg.IPageHeader, pageData: Uint8Array): Promise<void>;
|
|
19
|
-
|
|
20
|
-
calculateDuration(_header: Ogg.IPageHeader): void;
|
|
19
|
+
calculateDuration(): void;
|
|
21
20
|
/**
|
|
22
21
|
* Parse first Theora Ogg page. the initial identification header packet
|
|
23
22
|
*/
|
|
24
23
|
protected parseFirstPage(_header: Ogg.IPageHeader, pageData: Uint8Array): Promise<void>;
|
|
24
|
+
flush(): Promise<void>;
|
|
25
25
|
}
|
|
@@ -5,7 +5,7 @@ const debug = initDebug('music-metadata:parser:ogg:theora');
|
|
|
5
5
|
* Ref:
|
|
6
6
|
* - https://theora.org/doc/Theora.pdf
|
|
7
7
|
*/
|
|
8
|
-
export class
|
|
8
|
+
export class TheoraStream {
|
|
9
9
|
constructor(metadata, _options, tokenizer) {
|
|
10
10
|
this.metadata = metadata;
|
|
11
11
|
this.tokenizer = tokenizer;
|
|
@@ -20,10 +20,7 @@ export class TheoraParser {
|
|
|
20
20
|
await this.parseFirstPage(header, pageData);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
debug('flush');
|
|
25
|
-
}
|
|
26
|
-
calculateDuration(_header) {
|
|
23
|
+
calculateDuration() {
|
|
27
24
|
debug('duration calculation not implemented');
|
|
28
25
|
}
|
|
29
26
|
/**
|
|
@@ -34,6 +31,10 @@ export class TheoraParser {
|
|
|
34
31
|
this.metadata.setFormat('codec', 'Theora');
|
|
35
32
|
const idHeader = IdentificationHeader.get(pageData, 0);
|
|
36
33
|
this.metadata.setFormat('bitrate', idHeader.nombr);
|
|
34
|
+
this.metadata.setFormat('hasVideo', true);
|
|
35
|
+
}
|
|
36
|
+
flush() {
|
|
37
|
+
return Promise.resolve();
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
//# sourceMappingURL=
|
|
40
|
+
//# sourceMappingURL=TheoraStream.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type IVorbisPicture } from './Vorbis.js';
|
|
2
|
-
import type { IPageConsumer, IPageHeader } from '../
|
|
2
|
+
import type { IPageConsumer, IPageHeader } from '../OggToken.js';
|
|
3
3
|
import type { IOptions } from '../../type.js';
|
|
4
4
|
import type { INativeMetadataCollector } from '../../common/MetadataCollector.js';
|
|
5
5
|
declare const VorbisContentError_base: {
|
|
@@ -18,12 +18,13 @@ export declare class VorbisContentError extends VorbisContentError_base {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Vorbis 1 Parser.
|
|
21
|
-
* Used by
|
|
21
|
+
* Used by OggStream
|
|
22
22
|
*/
|
|
23
|
-
export declare class
|
|
23
|
+
export declare class VorbisStream implements IPageConsumer {
|
|
24
24
|
private pageSegments;
|
|
25
25
|
protected metadata: INativeMetadataCollector;
|
|
26
26
|
protected options: IOptions;
|
|
27
|
+
protected lastPageHeader?: IPageHeader;
|
|
27
28
|
constructor(metadata: INativeMetadataCollector, options: IOptions);
|
|
28
29
|
/**
|
|
29
30
|
* Vorbis 1 parser
|
|
@@ -35,7 +36,7 @@ export declare class VorbisParser implements IPageConsumer {
|
|
|
35
36
|
flush(): Promise<void>;
|
|
36
37
|
parseUserComment(pageData: Uint8Array, offset: number): Promise<number>;
|
|
37
38
|
addTag(id: string, value: string | IVorbisPicture): Promise<void>;
|
|
38
|
-
calculateDuration(
|
|
39
|
+
calculateDuration(): void;
|
|
39
40
|
/**
|
|
40
41
|
* Parse first Ogg/Vorbis page
|
|
41
42
|
* @param header
|
|
@@ -8,9 +8,9 @@ export class VorbisContentError extends makeUnexpectedFileContentError('Vorbis')
|
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Vorbis 1 Parser.
|
|
11
|
-
* Used by
|
|
11
|
+
* Used by OggStream
|
|
12
12
|
*/
|
|
13
|
-
export class
|
|
13
|
+
export class VorbisStream {
|
|
14
14
|
constructor(metadata, options) {
|
|
15
15
|
this.pageSegments = [];
|
|
16
16
|
this.metadata = metadata;
|
|
@@ -22,6 +22,7 @@ export class VorbisParser {
|
|
|
22
22
|
* @param pageData Page data
|
|
23
23
|
*/
|
|
24
24
|
async parsePage(header, pageData) {
|
|
25
|
+
this.lastPageHeader = header;
|
|
25
26
|
if (header.headerType.firstPage) {
|
|
26
27
|
this.parseFirstPage(header, pageData);
|
|
27
28
|
}
|
|
@@ -35,16 +36,13 @@ export class VorbisParser {
|
|
|
35
36
|
if (header.headerType.lastPage || !header.headerType.continued) {
|
|
36
37
|
// Flush page segments
|
|
37
38
|
if (this.pageSegments.length > 0) {
|
|
38
|
-
const fullPage =
|
|
39
|
+
const fullPage = VorbisStream.mergeUint8Arrays(this.pageSegments);
|
|
39
40
|
await this.parseFullPage(fullPage);
|
|
40
41
|
}
|
|
41
42
|
// Reset page segments
|
|
42
43
|
this.pageSegments = header.headerType.lastPage ? [] : [pageData];
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
if (header.headerType.lastPage) {
|
|
46
|
-
this.calculateDuration(header);
|
|
47
|
-
}
|
|
48
46
|
}
|
|
49
47
|
static mergeUint8Arrays(arrays) {
|
|
50
48
|
const totalSize = arrays.reduce((acc, e) => acc + e.length, 0);
|
|
@@ -56,7 +54,7 @@ export class VorbisParser {
|
|
|
56
54
|
return merged;
|
|
57
55
|
}
|
|
58
56
|
async flush() {
|
|
59
|
-
await this.parseFullPage(
|
|
57
|
+
await this.parseFullPage(VorbisStream.mergeUint8Arrays(this.pageSegments));
|
|
60
58
|
}
|
|
61
59
|
async parseUserComment(pageData, offset) {
|
|
62
60
|
const decoder = new VorbisDecoder(pageData, offset);
|
|
@@ -78,11 +76,11 @@ export class VorbisParser {
|
|
|
78
76
|
}
|
|
79
77
|
await this.metadata.addTag('vorbis', id, value);
|
|
80
78
|
}
|
|
81
|
-
calculateDuration(
|
|
82
|
-
if (this.metadata.format.sampleRate &&
|
|
79
|
+
calculateDuration() {
|
|
80
|
+
if (this.lastPageHeader && this.metadata.format.sampleRate && this.lastPageHeader.absoluteGranulePosition >= 0) {
|
|
83
81
|
// Calculate duration
|
|
84
|
-
this.metadata.setFormat('numberOfSamples',
|
|
85
|
-
this.metadata.setFormat('duration',
|
|
82
|
+
this.metadata.setFormat('numberOfSamples', this.lastPageHeader.absoluteGranulePosition);
|
|
83
|
+
this.metadata.setFormat('duration', this.lastPageHeader.absoluteGranulePosition / this.metadata.format.sampleRate);
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
86
|
/**
|
|
@@ -92,6 +90,7 @@ export class VorbisParser {
|
|
|
92
90
|
*/
|
|
93
91
|
parseFirstPage(_header, pageData) {
|
|
94
92
|
this.metadata.setFormat('codec', 'Vorbis I');
|
|
93
|
+
this.metadata.setFormat('hasAudio', true);
|
|
95
94
|
debug('Parse first page');
|
|
96
95
|
// Parse Vorbis common header
|
|
97
96
|
const commonHeader = CommonHeader.get(pageData, 0);
|
|
@@ -134,4 +133,4 @@ export class VorbisParser {
|
|
|
134
133
|
}
|
|
135
134
|
}
|
|
136
135
|
}
|
|
137
|
-
//# sourceMappingURL=
|
|
136
|
+
//# sourceMappingURL=VorbisStream.js.map
|
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;
|