music-metadata
Version:
Music metadata parser for Node.js, supporting virtual any audio and tag format.
126 lines (125 loc) • 5.63 kB
JavaScript
import * as Token from 'token-types';
import { EndOfStreamError } from 'strtok3';
import initDebug from 'debug';
import { BasicParser } from '../common/BasicParser.js';
import { VorbisStream } from './vorbis/VorbisStream.js';
import { OpusStream } from './opus/OpusStream.js';
import { SpeexStream } from './speex/SpeexStream.js';
import { TheoraStream } from './theora/TheoraStream.js';
import { makeUnexpectedFileContentError } from '../ParseError.js';
import { PageHeader, SegmentTable } from './OggToken.js';
import { FlacStream } from './flac/FlacStream.js';
export class OggContentError extends makeUnexpectedFileContentError('Ogg') {
}
const debug = initDebug('music-metadata:parser:ogg');
class OggStream {
constructor(metadata, streamSerial, options) {
this.pageNumber = 0;
this.closed = false;
this.metadata = metadata;
this.streamSerial = streamSerial;
this.options = options;
}
async parsePage(tokenizer, header) {
this.pageNumber = header.pageSequenceNo;
debug('serial=%s page#=%s, Ogg.id=%s', header.streamSerialNumber, header.pageSequenceNo, header.capturePattern);
const segmentTable = await tokenizer.readToken(new SegmentTable(header));
debug('totalPageSize=%s', segmentTable.totalPageSize);
const pageData = await tokenizer.readToken(new Token.Uint8ArrayType(segmentTable.totalPageSize));
debug('firstPage=%s, lastPage=%s, continued=%s', header.headerType.firstPage, header.headerType.lastPage, header.headerType.continued);
if (header.headerType.firstPage) {
this.metadata.setFormat('container', 'Ogg');
const idData = pageData.subarray(0, 7); // Copy this portion
const asciiId = Array.from(idData)
.filter(b => b >= 32 && b <= 126) // Keep only printable ASCII
.map(b => String.fromCharCode(b))
.join('');
switch (asciiId) {
case 'vorbis': // Ogg/Vorbis
debug(`Set Ogg stream serial ${header.streamSerialNumber}, codec=Vorbis`);
this.pageConsumer = new VorbisStream(this.metadata, this.options);
break;
case 'OpusHea': // Ogg/Opus
debug('Set page consumer to Ogg/Opus');
this.pageConsumer = new OpusStream(this.metadata, this.options, tokenizer);
break;
case 'Speex ': // Ogg/Speex
debug('Set page consumer to Ogg/Speex');
this.pageConsumer = new SpeexStream(this.metadata, this.options, tokenizer);
break;
case 'fishead':
case 'theora': // Ogg/Theora
debug('Set page consumer to Ogg/Theora');
this.pageConsumer = new TheoraStream(this.metadata, this.options, tokenizer);
break;
case 'FLAC': // Ogg/Theora
debug('Set page consumer to Vorbis');
this.pageConsumer = new FlacStream(this.metadata, this.options, tokenizer);
break;
default:
throw new OggContentError(`Ogg codec not recognized (id=${asciiId}`);
}
}
if (header.headerType.lastPage) {
this.closed = true;
}
if (this.pageConsumer) {
await this.pageConsumer.parsePage(header, pageData);
}
else
throw new Error('pageConsumer should be initialized');
}
}
/**
* Parser for Ogg logical bitstream framing
*/
export class OggParser extends BasicParser {
constructor() {
super(...arguments);
this.streams = new Map();
}
/**
* Parse page
* @returns {Promise<void>}
*/
async parse() {
this.streams = new Map();
let enfOfStream = false;
let header;
try {
do {
header = await this.tokenizer.readToken(PageHeader);
if (header.capturePattern !== 'OggS')
throw new OggContentError('Invalid Ogg capture pattern');
let stream = this.streams.get(header.streamSerialNumber);
if (!stream) {
stream = new OggStream(this.metadata, header.streamSerialNumber, this.options);
this.streams.set(header.streamSerialNumber, stream);
}
await stream.parsePage(this.tokenizer, header);
if (stream.pageNumber > 12 && !(this.options.duration && [...this.streams.values()].find(stream => stream.pageConsumer?.durationOnLastPage))) {
debug("Stop processing Ogg stream");
break;
}
} while (![...this.streams.values()].every(item => item.closed));
}
catch (err) {
if (err instanceof EndOfStreamError) {
debug("Reached end-of-stream");
enfOfStream = true;
}
else if (err instanceof OggContentError) {
this.metadata.addWarning(`Corrupt Ogg content at ${this.tokenizer.position}`);
}
else
throw err;
}
for (const stream of this.streams.values()) {
if (!stream.closed) {
this.metadata.addWarning(`End-of-stream reached before reaching last page in Ogg stream serial=${stream.streamSerial}`);
await stream.pageConsumer?.flush();
}
stream.pageConsumer?.calculateDuration(enfOfStream);
}
}
}