music-metadata 11.5.0 → 11.6.1
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/lib/flac/FlacParser.d.ts +15 -2
- package/lib/flac/FlacParser.js +40 -83
- package/lib/flac/FlacToken.d.ts +45 -0
- package/lib/flac/FlacToken.js +64 -0
- package/lib/ogg/OggParser.d.ts +1 -12
- package/lib/ogg/OggParser.js +82 -94
- package/lib/ogg/{Ogg.d.ts → OggToken.d.ts} +11 -4
- package/lib/ogg/OggToken.js +42 -0
- package/lib/ogg/flac/FlacStream.d.ts +28 -0
- package/lib/ogg/flac/FlacStream.js +74 -0
- package/lib/ogg/opus/{OpusParser.d.ts → OpusStream.d.ts} +5 -5
- package/lib/ogg/opus/{OpusParser.js → OpusStream.js} +7 -7
- package/lib/ogg/speex/{SpeexParser.d.ts → SpeexStream.d.ts} +3 -3
- package/lib/ogg/speex/{SpeexParser.js → SpeexStream.js} +3 -3
- package/lib/ogg/theora/{TheoraParser.d.ts → TheoraStream.d.ts} +4 -4
- package/lib/ogg/theora/{TheoraParser.js → TheoraStream.js} +7 -7
- package/lib/ogg/vorbis/{VorbisParser.d.ts → VorbisStream.d.ts} +6 -5
- package/lib/ogg/vorbis/{VorbisParser.js → VorbisStream.js} +12 -14
- package/package.json +2 -1
- package/lib/ogg/Ogg.js +0 -2
package/lib/flac/FlacParser.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type IVorbisPicture } from '../ogg/vorbis/Vorbis.js';
|
|
1
2
|
import { AbstractID3Parser } from '../id3v2/AbstractID3Parser.js';
|
|
3
|
+
import type { IBlockStreamInfo } from './FlacToken.js';
|
|
2
4
|
export declare class FlacParser extends AbstractID3Parser {
|
|
3
5
|
private vorbisParser;
|
|
4
6
|
private padding;
|
|
@@ -7,11 +9,22 @@ export declare class FlacParser extends AbstractID3Parser {
|
|
|
7
9
|
/**
|
|
8
10
|
* Parse STREAMINFO
|
|
9
11
|
*/
|
|
10
|
-
private
|
|
12
|
+
private readBlockStreamInfo;
|
|
13
|
+
/**
|
|
14
|
+
* Parse STREAMINFO
|
|
15
|
+
*/
|
|
16
|
+
processsStreamInfo(streamInfo: IBlockStreamInfo): void;
|
|
17
|
+
/**
|
|
18
|
+
* Read VORBIS_COMMENT from tokenizer
|
|
19
|
+
* Ref: https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3
|
|
20
|
+
*/
|
|
21
|
+
private readComment;
|
|
11
22
|
/**
|
|
12
23
|
* Parse VORBIS_COMMENT
|
|
13
24
|
* Ref: https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3
|
|
14
25
|
*/
|
|
15
|
-
|
|
26
|
+
parseComment(data: Uint8Array): Promise<void>;
|
|
16
27
|
private parsePicture;
|
|
28
|
+
addPictureTag(picture: IVorbisPicture): Promise<void>;
|
|
29
|
+
private addTag;
|
|
17
30
|
}
|
package/lib/flac/FlacParser.js
CHANGED
|
@@ -1,32 +1,19 @@
|
|
|
1
|
-
import { UINT16_BE, UINT24_BE, Uint8ArrayType } from 'token-types';
|
|
2
1
|
import initDebug from 'debug';
|
|
3
|
-
import
|
|
2
|
+
import { Uint8ArrayType } from 'token-types';
|
|
4
3
|
import { VorbisPictureToken } from '../ogg/vorbis/Vorbis.js';
|
|
5
4
|
import { AbstractID3Parser } from '../id3v2/AbstractID3Parser.js';
|
|
6
5
|
import { FourCcToken } from '../common/FourCC.js';
|
|
7
|
-
import {
|
|
6
|
+
import { VorbisStream } from '../ogg/vorbis/VorbisStream.js';
|
|
8
7
|
import { VorbisDecoder } from '../ogg/vorbis/VorbisDecoder.js';
|
|
9
8
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
9
|
+
import * as Flac from './FlacToken.js';
|
|
10
10
|
const debug = initDebug('music-metadata:parser:FLAC');
|
|
11
11
|
class FlacContentError extends makeUnexpectedFileContentError('FLAC') {
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
14
|
-
* FLAC supports up to 128 kinds of metadata blocks; currently the following are defined:
|
|
15
|
-
* ref: https://xiph.org/flac/format.html#metadata_block
|
|
16
|
-
*/
|
|
17
|
-
const BlockType = {
|
|
18
|
-
STREAMINFO: 0, // STREAMINFO
|
|
19
|
-
PADDING: 1, // PADDING
|
|
20
|
-
APPLICATION: 2, // APPLICATION
|
|
21
|
-
SEEKTABLE: 3, // SEEKTABLE
|
|
22
|
-
VORBIS_COMMENT: 4, // VORBIS_COMMENT
|
|
23
|
-
CUESHEET: 5, // CUESHEET
|
|
24
|
-
PICTURE: 6 // PICTURE
|
|
25
|
-
};
|
|
26
13
|
export class FlacParser extends AbstractID3Parser {
|
|
27
14
|
constructor() {
|
|
28
15
|
super(...arguments);
|
|
29
|
-
this.vorbisParser = new
|
|
16
|
+
this.vorbisParser = new VorbisStream(this.metadata, this.options);
|
|
30
17
|
this.padding = 0;
|
|
31
18
|
}
|
|
32
19
|
async postId3v2Parse() {
|
|
@@ -34,11 +21,10 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
34
21
|
if (fourCC.toString() !== 'fLaC') {
|
|
35
22
|
throw new FlacContentError('Invalid FLAC preamble');
|
|
36
23
|
}
|
|
37
|
-
this.metadata.setAudioOnly();
|
|
38
24
|
let blockHeader;
|
|
39
25
|
do {
|
|
40
26
|
// Read block header
|
|
41
|
-
blockHeader = await this.tokenizer.readToken(BlockHeader);
|
|
27
|
+
blockHeader = await this.tokenizer.readToken(Flac.BlockHeader);
|
|
42
28
|
// Parse block data
|
|
43
29
|
await this.parseDataBlock(blockHeader);
|
|
44
30
|
} while (!blockHeader.lastBlock);
|
|
@@ -50,20 +36,20 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
50
36
|
async parseDataBlock(blockHeader) {
|
|
51
37
|
debug(`blockHeader type=${blockHeader.type}, length=${blockHeader.length}`);
|
|
52
38
|
switch (blockHeader.type) {
|
|
53
|
-
case BlockType.STREAMINFO:
|
|
54
|
-
return this.
|
|
55
|
-
case BlockType.PADDING:
|
|
39
|
+
case Flac.BlockType.STREAMINFO:
|
|
40
|
+
return this.readBlockStreamInfo(blockHeader.length);
|
|
41
|
+
case Flac.BlockType.PADDING:
|
|
56
42
|
this.padding += blockHeader.length;
|
|
57
43
|
break;
|
|
58
|
-
case BlockType.APPLICATION:
|
|
44
|
+
case Flac.BlockType.APPLICATION:
|
|
59
45
|
break;
|
|
60
|
-
case BlockType.SEEKTABLE:
|
|
46
|
+
case Flac.BlockType.SEEKTABLE:
|
|
61
47
|
break;
|
|
62
|
-
case BlockType.VORBIS_COMMENT:
|
|
63
|
-
return this.
|
|
64
|
-
case BlockType.CUESHEET:
|
|
48
|
+
case Flac.BlockType.VORBIS_COMMENT:
|
|
49
|
+
return this.readComment(blockHeader.length);
|
|
50
|
+
case Flac.BlockType.CUESHEET:
|
|
65
51
|
break;
|
|
66
|
-
case BlockType.PICTURE:
|
|
52
|
+
case Flac.BlockType.PICTURE:
|
|
67
53
|
await this.parsePicture(blockHeader.length);
|
|
68
54
|
return;
|
|
69
55
|
default:
|
|
@@ -75,12 +61,19 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
75
61
|
/**
|
|
76
62
|
* Parse STREAMINFO
|
|
77
63
|
*/
|
|
78
|
-
async
|
|
79
|
-
if (dataLen !== BlockStreamInfo.len)
|
|
64
|
+
async readBlockStreamInfo(dataLen) {
|
|
65
|
+
if (dataLen !== Flac.BlockStreamInfo.len)
|
|
80
66
|
throw new FlacContentError('Unexpected block-stream-info length');
|
|
81
|
-
const streamInfo = await this.tokenizer.readToken(BlockStreamInfo);
|
|
67
|
+
const streamInfo = await this.tokenizer.readToken(Flac.BlockStreamInfo);
|
|
82
68
|
this.metadata.setFormat('container', 'FLAC');
|
|
69
|
+
this.processsStreamInfo(streamInfo);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse STREAMINFO
|
|
73
|
+
*/
|
|
74
|
+
processsStreamInfo(streamInfo) {
|
|
83
75
|
this.metadata.setFormat('codec', 'FLAC');
|
|
76
|
+
this.metadata.setFormat('hasAudio', true);
|
|
84
77
|
this.metadata.setFormat('lossless', true);
|
|
85
78
|
this.metadata.setFormat('numberOfChannels', streamInfo.channels);
|
|
86
79
|
this.metadata.setFormat('bitsPerSample', streamInfo.bitsPerSample);
|
|
@@ -90,11 +83,18 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
90
83
|
}
|
|
91
84
|
}
|
|
92
85
|
/**
|
|
93
|
-
*
|
|
86
|
+
* Read VORBIS_COMMENT from tokenizer
|
|
94
87
|
* Ref: https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3
|
|
95
88
|
*/
|
|
96
|
-
async
|
|
89
|
+
async readComment(dataLen) {
|
|
97
90
|
const data = await this.tokenizer.readToken(new Uint8ArrayType(dataLen));
|
|
91
|
+
return this.parseComment(data);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Parse VORBIS_COMMENT
|
|
95
|
+
* Ref: https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3
|
|
96
|
+
*/
|
|
97
|
+
async parseComment(data) {
|
|
98
98
|
const decoder = new VorbisDecoder(data, 0);
|
|
99
99
|
decoder.readStringUtf8(); // vendor (skip)
|
|
100
100
|
const commentListLength = decoder.readInt32();
|
|
@@ -102,62 +102,19 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
102
102
|
for (let i = 0; i < commentListLength; i++) {
|
|
103
103
|
tags[i] = decoder.parseUserComment();
|
|
104
104
|
}
|
|
105
|
-
await Promise.all(tags.map(tag => this.
|
|
105
|
+
await Promise.all(tags.map(tag => this.addTag(tag.key, tag.value)));
|
|
106
106
|
}
|
|
107
107
|
async parsePicture(dataLen) {
|
|
108
108
|
if (this.options.skipCovers) {
|
|
109
109
|
return this.tokenizer.ignore(dataLen);
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
this.vorbisParser.addTag('METADATA_BLOCK_PICTURE', picture);
|
|
111
|
+
return this.addPictureTag(await this.tokenizer.readToken(new VorbisPictureToken(dataLen)));
|
|
113
112
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
len: 4,
|
|
117
|
-
get: (buf, off) => {
|
|
118
|
-
return {
|
|
119
|
-
lastBlock: util.getBit(buf, off, 7),
|
|
120
|
-
type: util.getBitAllignedNumber(buf, off, 1, 7),
|
|
121
|
-
length: UINT24_BE.get(buf, off + 1)
|
|
122
|
-
};
|
|
113
|
+
addPictureTag(picture) {
|
|
114
|
+
return this.addTag('METADATA_BLOCK_PICTURE', picture);
|
|
123
115
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
* METADATA_BLOCK_DATA
|
|
127
|
-
* Ref: https://xiph.org/flac/format.html#metadata_block_streaminfo
|
|
128
|
-
*/
|
|
129
|
-
const BlockStreamInfo = {
|
|
130
|
-
len: 34,
|
|
131
|
-
get: (buf, off) => {
|
|
132
|
-
return {
|
|
133
|
-
// The minimum block size (in samples) used in the stream.
|
|
134
|
-
minimumBlockSize: UINT16_BE.get(buf, off),
|
|
135
|
-
// The maximum block size (in samples) used in the stream.
|
|
136
|
-
// (Minimum blocksize == maximum blocksize) implies a fixed-blocksize stream.
|
|
137
|
-
maximumBlockSize: UINT16_BE.get(buf, off + 2) / 1000,
|
|
138
|
-
// The minimum frame size (in bytes) used in the stream.
|
|
139
|
-
// May be 0 to imply the value is not known.
|
|
140
|
-
minimumFrameSize: UINT24_BE.get(buf, off + 4),
|
|
141
|
-
// The maximum frame size (in bytes) used in the stream.
|
|
142
|
-
// May be 0 to imply the value is not known.
|
|
143
|
-
maximumFrameSize: UINT24_BE.get(buf, off + 7),
|
|
144
|
-
// Sample rate in Hz. Though 20 bits are available,
|
|
145
|
-
// the maximum sample rate is limited by the structure of frame headers to 655350Hz.
|
|
146
|
-
// Also, a value of 0 is invalid.
|
|
147
|
-
sampleRate: UINT24_BE.get(buf, off + 10) >> 4,
|
|
148
|
-
// probably slower: sampleRate: common.getBitAllignedNumber(buf, off + 10, 0, 20),
|
|
149
|
-
// (number of channels)-1. FLAC supports from 1 to 8 channels
|
|
150
|
-
channels: util.getBitAllignedNumber(buf, off + 12, 4, 3) + 1,
|
|
151
|
-
// bits per sample)-1.
|
|
152
|
-
// FLAC supports from 4 to 32 bits per sample. Currently the reference encoder and decoders only support up to 24 bits per sample.
|
|
153
|
-
bitsPerSample: util.getBitAllignedNumber(buf, off + 12, 7, 5) + 1,
|
|
154
|
-
// Total samples in stream.
|
|
155
|
-
// 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio will have 44100 samples regardless of the number of channels.
|
|
156
|
-
// A value of zero here means the number of total samples is unknown.
|
|
157
|
-
totalSamples: util.getBitAllignedNumber(buf, off + 13, 4, 36),
|
|
158
|
-
// the MD5 hash of the file (see notes for usage... it's a littly tricky)
|
|
159
|
-
fileMD5: new Uint8ArrayType(16).get(buf, off + 18)
|
|
160
|
-
};
|
|
116
|
+
addTag(id, value) {
|
|
117
|
+
return this.vorbisParser.addTag(id, value);
|
|
161
118
|
}
|
|
162
|
-
}
|
|
119
|
+
}
|
|
163
120
|
//# sourceMappingURL=FlacParser.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { IGetToken } from 'strtok3';
|
|
2
|
+
/**
|
|
3
|
+
* FLAC supports up to 128 kinds of metadata blocks; currently the following are defined:
|
|
4
|
+
* ref: https://xiph.org/flac/format.html#metadata_block
|
|
5
|
+
*/
|
|
6
|
+
export declare const BlockType: {
|
|
7
|
+
STREAMINFO: number;
|
|
8
|
+
PADDING: number;
|
|
9
|
+
APPLICATION: number;
|
|
10
|
+
SEEKTABLE: number;
|
|
11
|
+
VORBIS_COMMENT: number;
|
|
12
|
+
CUESHEET: number;
|
|
13
|
+
PICTURE: number;
|
|
14
|
+
};
|
|
15
|
+
export type BlockType = typeof BlockType[keyof typeof BlockType];
|
|
16
|
+
/**
|
|
17
|
+
* METADATA_BLOCK_DATA
|
|
18
|
+
* Ref: https://xiph.org/flac/format.html#metadata_block_streaminfo
|
|
19
|
+
*/
|
|
20
|
+
export interface IBlockHeader {
|
|
21
|
+
lastBlock: boolean;
|
|
22
|
+
type: BlockType;
|
|
23
|
+
length: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const BlockHeader: IGetToken<IBlockHeader>;
|
|
26
|
+
/**
|
|
27
|
+
* METADATA_BLOCK_DATA
|
|
28
|
+
* Ref: https://xiph.org/flac/format.html#metadata_block_streaminfo
|
|
29
|
+
*/
|
|
30
|
+
export interface IBlockStreamInfo {
|
|
31
|
+
minimumBlockSize: number;
|
|
32
|
+
maximumBlockSize: number;
|
|
33
|
+
minimumFrameSize: number;
|
|
34
|
+
maximumFrameSize: number;
|
|
35
|
+
sampleRate: number;
|
|
36
|
+
channels: number;
|
|
37
|
+
bitsPerSample: number;
|
|
38
|
+
totalSamples: number;
|
|
39
|
+
fileMD5: Uint8Array;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* METADATA_BLOCK_DATA
|
|
43
|
+
* Ref: https://xiph.org/flac/format.html#metadata_block_streaminfo
|
|
44
|
+
*/
|
|
45
|
+
export declare const BlockStreamInfo: IGetToken<IBlockStreamInfo>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as util from '../common/Util.js';
|
|
2
|
+
import { UINT16_BE, UINT24_BE, Uint8ArrayType } from 'token-types';
|
|
3
|
+
/**
|
|
4
|
+
* FLAC supports up to 128 kinds of metadata blocks; currently the following are defined:
|
|
5
|
+
* ref: https://xiph.org/flac/format.html#metadata_block
|
|
6
|
+
*/
|
|
7
|
+
export const BlockType = {
|
|
8
|
+
STREAMINFO: 0, // STREAMINFO
|
|
9
|
+
PADDING: 1, // PADDING
|
|
10
|
+
APPLICATION: 2, // APPLICATION
|
|
11
|
+
SEEKTABLE: 3, // SEEKTABLE
|
|
12
|
+
VORBIS_COMMENT: 4, // VORBIS_COMMENT
|
|
13
|
+
CUESHEET: 5, // CUESHEET
|
|
14
|
+
PICTURE: 6 // PICTURE
|
|
15
|
+
};
|
|
16
|
+
export const BlockHeader = {
|
|
17
|
+
len: 4,
|
|
18
|
+
get: (buf, off) => {
|
|
19
|
+
return {
|
|
20
|
+
lastBlock: util.getBit(buf, off, 7),
|
|
21
|
+
type: util.getBitAllignedNumber(buf, off, 1, 7),
|
|
22
|
+
length: UINT24_BE.get(buf, off + 1)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* METADATA_BLOCK_DATA
|
|
28
|
+
* Ref: https://xiph.org/flac/format.html#metadata_block_streaminfo
|
|
29
|
+
*/
|
|
30
|
+
export const BlockStreamInfo = {
|
|
31
|
+
len: 34,
|
|
32
|
+
get: (buf, off) => {
|
|
33
|
+
return {
|
|
34
|
+
// The minimum block size (in samples) used in the stream.
|
|
35
|
+
minimumBlockSize: UINT16_BE.get(buf, off),
|
|
36
|
+
// The maximum block size (in samples) used in the stream.
|
|
37
|
+
// (Minimum blocksize == maximum blocksize) implies a fixed-blocksize stream.
|
|
38
|
+
maximumBlockSize: UINT16_BE.get(buf, off + 2) / 1000,
|
|
39
|
+
// The minimum frame size (in bytes) used in the stream.
|
|
40
|
+
// May be 0 to imply the value is not known.
|
|
41
|
+
minimumFrameSize: UINT24_BE.get(buf, off + 4),
|
|
42
|
+
// The maximum frame size (in bytes) used in the stream.
|
|
43
|
+
// May be 0 to imply the value is not known.
|
|
44
|
+
maximumFrameSize: UINT24_BE.get(buf, off + 7),
|
|
45
|
+
// Sample rate in Hz. Though 20 bits are available,
|
|
46
|
+
// the maximum sample rate is limited by the structure of frame headers to 655350Hz.
|
|
47
|
+
// Also, a value of 0 is invalid.
|
|
48
|
+
sampleRate: UINT24_BE.get(buf, off + 10) >> 4,
|
|
49
|
+
// probably slower: sampleRate: common.getBitAllignedNumber(buf, off + 10, 0, 20),
|
|
50
|
+
// (number of channels)-1. FLAC supports from 1 to 8 channels
|
|
51
|
+
channels: util.getBitAllignedNumber(buf, off + 12, 4, 3) + 1,
|
|
52
|
+
// bits per sample)-1.
|
|
53
|
+
// FLAC supports from 4 to 32 bits per sample. Currently the reference encoder and decoders only support up to 24 bits per sample.
|
|
54
|
+
bitsPerSample: util.getBitAllignedNumber(buf, off + 12, 7, 5) + 1,
|
|
55
|
+
// Total samples in stream.
|
|
56
|
+
// 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio will have 44100 samples regardless of the number of channels.
|
|
57
|
+
// A value of zero here means the number of total samples is unknown.
|
|
58
|
+
totalSamples: util.getBitAllignedNumber(buf, off + 13, 4, 36),
|
|
59
|
+
// the MD5 hash of the file (see notes for usage... it's a littly tricky)
|
|
60
|
+
fileMD5: new Uint8ArrayType(16).get(buf, off + 18)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=FlacToken.js.map
|
package/lib/ogg/OggParser.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { type IGetToken } from 'strtok3';
|
|
2
1
|
import { BasicParser } from '../common/BasicParser.js';
|
|
3
|
-
import type * as Ogg from './Ogg.js';
|
|
4
2
|
declare const OggContentError_base: {
|
|
5
3
|
new (message: string): {
|
|
6
4
|
readonly fileType: string;
|
|
@@ -15,20 +13,11 @@ declare const OggContentError_base: {
|
|
|
15
13
|
};
|
|
16
14
|
export declare class OggContentError extends OggContentError_base {
|
|
17
15
|
}
|
|
18
|
-
export declare class SegmentTable implements IGetToken<Ogg.ISegmentTable> {
|
|
19
|
-
private static sum;
|
|
20
|
-
len: number;
|
|
21
|
-
constructor(header: Ogg.IPageHeader);
|
|
22
|
-
get(buf: Uint8Array, off: number): Ogg.ISegmentTable;
|
|
23
|
-
}
|
|
24
16
|
/**
|
|
25
17
|
* Parser for Ogg logical bitstream framing
|
|
26
18
|
*/
|
|
27
19
|
export declare class OggParser extends BasicParser {
|
|
28
|
-
private
|
|
29
|
-
private header;
|
|
30
|
-
private pageNumber;
|
|
31
|
-
private pageConsumer;
|
|
20
|
+
private streams;
|
|
32
21
|
/**
|
|
33
22
|
* Parse page
|
|
34
23
|
* @returns {Promise<void>}
|
package/lib/ogg/OggParser.js
CHANGED
|
@@ -1,33 +1,72 @@
|
|
|
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';
|
|
11
|
+
import { FlacStream } from './flac/FlacStream.js';
|
|
12
12
|
export class OggContentError extends makeUnexpectedFileContentError('Ogg') {
|
|
13
13
|
}
|
|
14
14
|
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;
|
|
15
|
+
class OggStream {
|
|
16
|
+
constructor(metadata, streamSerial, options) {
|
|
17
|
+
this.pageNumber = 0;
|
|
18
|
+
this.closed = false;
|
|
19
|
+
this.metadata = metadata;
|
|
20
|
+
this.streamSerial = streamSerial;
|
|
21
|
+
this.options = options;
|
|
26
22
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
async parsePage(tokenizer, header) {
|
|
24
|
+
this.pageNumber = header.pageSequenceNo;
|
|
25
|
+
debug('serial=%s page#=%s, Ogg.id=%s', header.streamSerialNumber, header.pageSequenceNo, header.capturePattern);
|
|
26
|
+
const segmentTable = await tokenizer.readToken(new SegmentTable(header));
|
|
27
|
+
debug('totalPageSize=%s', segmentTable.totalPageSize);
|
|
28
|
+
const pageData = await tokenizer.readToken(new Token.Uint8ArrayType(segmentTable.totalPageSize));
|
|
29
|
+
debug('firstPage=%s, lastPage=%s, continued=%s', header.headerType.firstPage, header.headerType.lastPage, header.headerType.continued);
|
|
30
|
+
if (header.headerType.firstPage) {
|
|
31
|
+
const idData = pageData.slice(0, 7); // Copy this portion
|
|
32
|
+
const asciiId = Array.from(idData)
|
|
33
|
+
.filter(b => b >= 32 && b <= 126) // Keep only printable ASCII
|
|
34
|
+
.map(b => String.fromCharCode(b))
|
|
35
|
+
.join('');
|
|
36
|
+
switch (asciiId) {
|
|
37
|
+
case 'vorbis': // Ogg/Vorbis
|
|
38
|
+
debug(`Set Ogg stream serial ${header.streamSerialNumber}, codec=Vorbis`);
|
|
39
|
+
this.pageConsumer = new VorbisStream(this.metadata, this.options);
|
|
40
|
+
break;
|
|
41
|
+
case 'OpusHea': // Ogg/Opus
|
|
42
|
+
debug('Set page consumer to Ogg/Opus');
|
|
43
|
+
this.pageConsumer = new OpusStream(this.metadata, this.options, tokenizer);
|
|
44
|
+
break;
|
|
45
|
+
case 'Speex ': // Ogg/Speex
|
|
46
|
+
debug('Set page consumer to Ogg/Speex');
|
|
47
|
+
this.pageConsumer = new SpeexStream(this.metadata, this.options, tokenizer);
|
|
48
|
+
break;
|
|
49
|
+
case 'fishead':
|
|
50
|
+
case 'theora': // Ogg/Theora
|
|
51
|
+
debug('Set page consumer to Ogg/Theora');
|
|
52
|
+
this.pageConsumer = new TheoraStream(this.metadata, this.options, tokenizer);
|
|
53
|
+
break;
|
|
54
|
+
case 'FLAC': // Ogg/Theora
|
|
55
|
+
debug('Set page consumer to Vorbis');
|
|
56
|
+
this.pageConsumer = new FlacStream(this.metadata, this.options, tokenizer);
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
throw new OggContentError(`Ogg codec not recognized (id=${asciiId}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (header.headerType.lastPage) {
|
|
63
|
+
this.closed = true;
|
|
64
|
+
}
|
|
65
|
+
if (this.pageConsumer) {
|
|
66
|
+
await this.pageConsumer.parsePage(header, pageData);
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
throw new Error('pageConsumer should be initialized');
|
|
31
70
|
}
|
|
32
71
|
}
|
|
33
72
|
/**
|
|
@@ -36,98 +75,47 @@ export class SegmentTable {
|
|
|
36
75
|
export class OggParser extends BasicParser {
|
|
37
76
|
constructor() {
|
|
38
77
|
super(...arguments);
|
|
39
|
-
this.
|
|
40
|
-
this.pageNumber = 0;
|
|
41
|
-
this.pageConsumer = null;
|
|
78
|
+
this.streams = new Map();
|
|
42
79
|
}
|
|
43
80
|
/**
|
|
44
81
|
* Parse page
|
|
45
82
|
* @returns {Promise<void>}
|
|
46
83
|
*/
|
|
47
84
|
async parse() {
|
|
85
|
+
this.streams = new Map();
|
|
48
86
|
debug('pos=%s, parsePage()', this.tokenizer.position);
|
|
87
|
+
let header;
|
|
49
88
|
try {
|
|
50
|
-
let header;
|
|
51
89
|
do {
|
|
52
|
-
header = await this.tokenizer.readToken(
|
|
90
|
+
header = await this.tokenizer.readToken(PageHeader);
|
|
53
91
|
if (header.capturePattern !== 'OggS')
|
|
54
92
|
throw new OggContentError('Invalid Ogg capture pattern');
|
|
55
93
|
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
|
-
}
|
|
94
|
+
let stream = this.streams.get(header.streamSerialNumber);
|
|
95
|
+
if (!stream) {
|
|
96
|
+
stream = new OggStream(this.metadata, header.streamSerialNumber, this.options);
|
|
97
|
+
this.streams.set(header.streamSerialNumber, stream);
|
|
86
98
|
}
|
|
87
|
-
await
|
|
88
|
-
} while (!
|
|
99
|
+
await stream.parsePage(this.tokenizer, header);
|
|
100
|
+
} while (![...this.streams.values()].every(item => item.closed));
|
|
89
101
|
}
|
|
90
102
|
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
|
-
}
|
|
103
|
+
if (err instanceof EndOfStreamError) {
|
|
104
|
+
debug("Reached end-of-stream");
|
|
105
|
+
}
|
|
106
|
+
else if (err instanceof OggContentError) {
|
|
107
|
+
this.metadata.addWarning(`Corrupt Ogg content at ${this.tokenizer.position}`);
|
|
107
108
|
}
|
|
108
109
|
else
|
|
109
110
|
throw err;
|
|
110
111
|
}
|
|
112
|
+
for (const stream of this.streams.values()) {
|
|
113
|
+
if (!stream.closed) {
|
|
114
|
+
this.metadata.addWarning(`End-of-stream reached before reaching last page in Ogg stream serial=${stream.streamSerial}`);
|
|
115
|
+
await stream.pageConsumer?.flush();
|
|
116
|
+
}
|
|
117
|
+
stream.pageConsumer?.calculateDuration();
|
|
118
|
+
}
|
|
111
119
|
}
|
|
112
120
|
}
|
|
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
121
|
//# 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
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ITokenizer } from 'strtok3';
|
|
2
|
+
import type * as Ogg from '../OggToken.js';
|
|
3
|
+
import type { IOptions } from '../../type.js';
|
|
4
|
+
import type { INativeMetadataCollector } from '../../common/MetadataCollector.js';
|
|
5
|
+
/**
|
|
6
|
+
* Ref:
|
|
7
|
+
* - https://xiph.org/flac/ogg_mapping.html
|
|
8
|
+
*/
|
|
9
|
+
export declare class FlacStream implements Ogg.IPageConsumer {
|
|
10
|
+
private metadata;
|
|
11
|
+
private options;
|
|
12
|
+
private tokenizer;
|
|
13
|
+
private flacParser;
|
|
14
|
+
constructor(metadata: INativeMetadataCollector, options: IOptions, tokenizer: ITokenizer);
|
|
15
|
+
/**
|
|
16
|
+
* Vorbis 1 parser
|
|
17
|
+
* @param header Ogg Page Header
|
|
18
|
+
* @param pageData Page data
|
|
19
|
+
*/
|
|
20
|
+
parsePage(header: Ogg.IPageHeader, pageData: Uint8Array): Promise<void>;
|
|
21
|
+
calculateDuration(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Parse first Theora Ogg page. the initial identification header packet
|
|
24
|
+
*/
|
|
25
|
+
protected parseFirstPage(_header: Ogg.IPageHeader, pageData: Uint8Array): Promise<void>;
|
|
26
|
+
private parseDataBlock;
|
|
27
|
+
flush(): Promise<void>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import initDebug from 'debug';
|
|
2
|
+
import * as Flac from '../../flac/FlacToken.js';
|
|
3
|
+
import { FlacParser } from '../../flac/FlacParser.js';
|
|
4
|
+
import { FourCcToken } from '../../common/FourCC.js';
|
|
5
|
+
import { VorbisPictureToken } from '../vorbis/Vorbis.js';
|
|
6
|
+
const debug = initDebug('music-metadata:parser:ogg:theora');
|
|
7
|
+
/**
|
|
8
|
+
* Ref:
|
|
9
|
+
* - https://xiph.org/flac/ogg_mapping.html
|
|
10
|
+
*/
|
|
11
|
+
export class FlacStream {
|
|
12
|
+
constructor(metadata, options, tokenizer) {
|
|
13
|
+
this.metadata = metadata;
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.tokenizer = tokenizer;
|
|
16
|
+
this.flacParser = new FlacParser(this.metadata, this.tokenizer, options);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Vorbis 1 parser
|
|
20
|
+
* @param header Ogg Page Header
|
|
21
|
+
* @param pageData Page data
|
|
22
|
+
*/
|
|
23
|
+
async parsePage(header, pageData) {
|
|
24
|
+
if (header.headerType.firstPage) {
|
|
25
|
+
await this.parseFirstPage(header, pageData);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
calculateDuration() {
|
|
29
|
+
debug('duration calculation not implemented');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse first Theora Ogg page. the initial identification header packet
|
|
33
|
+
*/
|
|
34
|
+
async parseFirstPage(_header, pageData) {
|
|
35
|
+
debug('First Ogg/FLAC page');
|
|
36
|
+
const fourCC = await FourCcToken.get(pageData, 9);
|
|
37
|
+
if (fourCC.toString() !== 'fLaC') {
|
|
38
|
+
throw new Error('Invalid FLAC preamble');
|
|
39
|
+
}
|
|
40
|
+
const blockHeader = await Flac.BlockHeader.get(pageData, 13);
|
|
41
|
+
await this.parseDataBlock(blockHeader, pageData.subarray(13 + Flac.BlockHeader.len));
|
|
42
|
+
}
|
|
43
|
+
async parseDataBlock(blockHeader, pageData) {
|
|
44
|
+
debug(`blockHeader type=${blockHeader.type}, length=${blockHeader.length}`);
|
|
45
|
+
switch (blockHeader.type) {
|
|
46
|
+
case Flac.BlockType.STREAMINFO: {
|
|
47
|
+
const streamInfo = Flac.BlockStreamInfo.get(pageData, 0);
|
|
48
|
+
return this.flacParser.processsStreamInfo(streamInfo);
|
|
49
|
+
}
|
|
50
|
+
case Flac.BlockType.PADDING:
|
|
51
|
+
break;
|
|
52
|
+
case Flac.BlockType.APPLICATION:
|
|
53
|
+
break;
|
|
54
|
+
case Flac.BlockType.SEEKTABLE:
|
|
55
|
+
break;
|
|
56
|
+
case Flac.BlockType.VORBIS_COMMENT:
|
|
57
|
+
return this.flacParser.parseComment(pageData);
|
|
58
|
+
case Flac.BlockType.PICTURE:
|
|
59
|
+
if (!this.options.skipCovers) {
|
|
60
|
+
const picture = new VorbisPictureToken(pageData.length).get(pageData, 0);
|
|
61
|
+
return this.flacParser.addPictureTag(picture);
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
this.metadata.addWarning(`Unknown block type: ${blockHeader.type}`);
|
|
66
|
+
}
|
|
67
|
+
// Ignore data block
|
|
68
|
+
return this.tokenizer.ignore(blockHeader.length).then();
|
|
69
|
+
}
|
|
70
|
+
flush() {
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=FlacStream.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;
|
|
@@ -40,10 +40,10 @@ export class OpusParser extends VorbisParser {
|
|
|
40
40
|
break;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
calculateDuration(
|
|
44
|
-
if (this.metadata.format.sampleRate &&
|
|
43
|
+
calculateDuration() {
|
|
44
|
+
if (this.lastPageHeader && this.metadata.format.sampleRate && this.lastPageHeader.absoluteGranulePosition >= 0) {
|
|
45
45
|
// Calculate duration
|
|
46
|
-
const pos_48bit =
|
|
46
|
+
const pos_48bit = this.lastPageHeader.absoluteGranulePosition - this.idHeader.preSkip;
|
|
47
47
|
this.metadata.setFormat('numberOfSamples', pos_48bit);
|
|
48
48
|
this.metadata.setFormat('duration', pos_48bit / 48000);
|
|
49
49
|
if (this.lastPos !== -1 && this.tokenizer.fileInfo.size && this.metadata.format.duration) {
|
|
@@ -53,4 +53,4 @@ export class OpusParser extends VorbisParser {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
//# 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;
|
|
@@ -30,4 +30,4 @@ export class SpeexParser extends VorbisParser {
|
|
|
30
30
|
this.metadata.setAudioOnly();
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
//# 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,7 +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);
|
|
37
|
-
this.metadata.
|
|
34
|
+
this.metadata.setFormat('hasVideo', true);
|
|
35
|
+
}
|
|
36
|
+
flush() {
|
|
37
|
+
return Promise.resolve();
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
//# 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,10 +36,10 @@ 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
|
-
* @param
|
|
42
|
+
* @param _header
|
|
42
43
|
* @param pageData
|
|
43
44
|
*/
|
|
44
45
|
protected parseFirstPage(_header: IPageHeader, pageData: Uint8Array): void;
|
|
@@ -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,21 +76,21 @@ 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
|
/**
|
|
89
87
|
* Parse first Ogg/Vorbis page
|
|
90
|
-
* @param
|
|
88
|
+
* @param _header
|
|
91
89
|
* @param pageData
|
|
92
90
|
*/
|
|
93
91
|
parseFirstPage(_header, pageData) {
|
|
94
92
|
this.metadata.setFormat('codec', 'Vorbis I');
|
|
95
|
-
this.metadata.
|
|
93
|
+
this.metadata.setFormat('hasAudio', true);
|
|
96
94
|
debug('Parse first page');
|
|
97
95
|
// Parse Vorbis common header
|
|
98
96
|
const commonHeader = CommonHeader.get(pageData, 0);
|
|
@@ -135,4 +133,4 @@ export class VorbisParser {
|
|
|
135
133
|
}
|
|
136
134
|
}
|
|
137
135
|
}
|
|
138
|
-
//# sourceMappingURL=
|
|
136
|
+
//# sourceMappingURL=VorbisStream.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "music-metadata",
|
|
3
3
|
"description": "Music metadata parser for Node.js, supporting virtual any audio and tag format.",
|
|
4
|
-
"version": "11.
|
|
4
|
+
"version": "11.6.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Borewit",
|
|
7
7
|
"url": "https://github.com/Borewit"
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"lint": "yarn run lint:ts && yarn run lint:md",
|
|
99
99
|
"test": "mocha",
|
|
100
100
|
"build": "yarn run clean && yarn compile && yarn run doc-gen",
|
|
101
|
+
"prepublishOnly": "yarn run build",
|
|
101
102
|
"test-coverage": "c8 yarn run test",
|
|
102
103
|
"send-codacy": "c8 report --reporter=text-lcov | codacy-coverage",
|
|
103
104
|
"doc-gen": "yarn node doc-gen/gen.js",
|
package/lib/ogg/Ogg.js
DELETED