Initial working version

This commit is contained in:
Samuel Kent
2022-12-22 20:22:22 +11:00
parent ce9675a1cc
commit ced7fa5092
902 changed files with 150252 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
/**
* Page header
* Ref: https://www.xiph.org/ogg/doc/framing.html#page_header
*/
export interface IPageHeader {
/**
* capture_pattern
* A header begins with a capture pattern that simplifies identifying pages;
* once the decoder has found the capture pattern it can do a more intensive job of verifying that it has in fact found a page boundary (as opposed to an inadvertent coincidence in the byte stream).
*/
capturePattern: string;
/**
* stream_structure_version
*/
version: number;
/**
* header_type_flag
*/
headerType: {
/**
* True: continued packet;
* False: fresh packet
*/
continued: boolean;
/**
* True: first page of logical bitstream (bos)
* False: not first page of logical bitstream
*/
firstPage: boolean;
/**
* True: last page of logical bitstream (eos)
* False: not last page of logical bitstream
*/
lastPage: boolean;
};
/**
* The total samples encoded after including all packets finished on this page
* The position specified in the frame header of the last page tells how long the data coded by the bitstream is.
*/
absoluteGranulePosition: number;
streamSerialNumber: number;
pageSequenceNo: number;
pageChecksum: number;
/**
* The number of segment entries to appear in the segment table.
* The maximum number of 255 segments (255 bytes each) sets the maximum possible physical page size at 65307 bytes or
* just under 64kB (thus we know that a header corrupted so as destroy sizing/alignment information will not cause a
* runaway bitstream. We'll read in the page according to the corrupted size information that's guaranteed to be a
* reasonable size regardless, notice the checksum mismatch, drop sync and then look for recapture).
*/
page_segments: number;
}
export interface ISegmentTable {
totalPageSize: number;
}
export interface IPageConsumer {
/**
* Parse Ogg page
* @param {IPageHeader} header Ogg page header
* @param {Buffer} pageData Ogg page data
*/
parsePage(header: IPageHeader, pageData: Uint8Array): any;
/**
* Calculate duration of provided header
* @param header Ogg header
*/
calculateDuration(header: IPageHeader): any;
/**
* Force to parse pending segments
*/
flush(): any;
}
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+23
View File
@@ -0,0 +1,23 @@
import * as Ogg from './Ogg';
import { BasicParser } from '../common/BasicParser';
import { IGetToken } from 'strtok3/lib/core';
export declare class SegmentTable implements IGetToken<Ogg.ISegmentTable> {
private static sum;
len: number;
constructor(header: Ogg.IPageHeader);
get(buf: any, off: any): Ogg.ISegmentTable;
}
/**
* Parser for Ogg logical bitstream framing
*/
export declare class OggParser extends BasicParser {
private static Header;
private header;
private pageNumber;
private pageConsumer;
/**
* Parse page
* @returns {Promise<void>}
*/
parse(): Promise<void>;
}
+126
View File
@@ -0,0 +1,126 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OggParser = exports.SegmentTable = void 0;
const Token = require("token-types");
const initDebug = require("debug");
const util = require("../common/Util");
const FourCC_1 = require("../common/FourCC");
const VorbisParser_1 = require("./vorbis/VorbisParser");
const OpusParser_1 = require("./opus/OpusParser");
const SpeexParser_1 = require("./speex/SpeexParser");
const BasicParser_1 = require("../common/BasicParser");
const TheoraParser_1 = require("./theora/TheoraParser");
const core_1 = require("strtok3/lib/core");
const debug = initDebug('music-metadata:parser:ogg');
class SegmentTable {
constructor(header) {
this.len = header.page_segments;
}
static sum(buf, off, len) {
let s = 0;
for (let i = off; i < off + len; ++i) {
s += buf[i];
}
return s;
}
get(buf, off) {
return {
totalPageSize: SegmentTable.sum(buf, off, this.len)
};
}
}
exports.SegmentTable = SegmentTable;
/**
* Parser for Ogg logical bitstream framing
*/
class OggParser extends BasicParser_1.BasicParser {
/**
* Parse page
* @returns {Promise<void>}
*/
async parse() {
debug('pos=%s, parsePage()', this.tokenizer.position);
try {
let header;
do {
header = await this.tokenizer.readToken(OggParser.Header);
if (header.capturePattern !== 'OggS')
throw new Error('Invalid Ogg capture pattern');
this.metadata.setFormat('container', 'Ogg');
this.header = header;
this.pageNumber = header.pageSequenceNo;
debug('page#=%s, Ogg.id=%s', header.pageSequenceNo, header.capturePattern);
const segmentTable = await this.tokenizer.readToken(new SegmentTable(header));
debug('totalPageSize=%s', segmentTable.totalPageSize);
const pageData = await this.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) {
const id = new Token.StringType(7, 'ascii').get(Buffer.from(pageData), 0);
switch (id) {
case '\x01vorbis': // Ogg/Vorbis
debug('Set page consumer to Ogg/Vorbis');
this.pageConsumer = new VorbisParser_1.VorbisParser(this.metadata, this.options);
break;
case 'OpusHea': // Ogg/Opus
debug('Set page consumer to Ogg/Opus');
this.pageConsumer = new OpusParser_1.OpusParser(this.metadata, this.options, this.tokenizer);
break;
case 'Speex ': // Ogg/Speex
debug('Set page consumer to Ogg/Speex');
this.pageConsumer = new SpeexParser_1.SpeexParser(this.metadata, this.options, this.tokenizer);
break;
case 'fishead':
case '\x00theora': // Ogg/Theora
debug('Set page consumer to Ogg/Theora');
this.pageConsumer = new TheoraParser_1.TheoraParser(this.metadata, this.options, this.tokenizer);
break;
default:
throw new Error('gg audio-codec not recognized (id=' + id + ')');
}
}
this.pageConsumer.parsePage(header, pageData);
} while (!header.headerType.lastPage);
}
catch (err) {
if (err instanceof core_1.EndOfStreamError) {
this.metadata.addWarning('Last OGG-page is not marked with last-page flag');
debug(`End-of-stream`);
this.metadata.addWarning('Last OGG-page is not marked with last-page flag');
if (this.header) {
this.pageConsumer.calculateDuration(this.header);
}
}
else if (err.message.startsWith('FourCC')) {
if (this.pageNumber > 0) {
// ignore this error: work-around if last OGG-page is not marked with last-page flag
this.metadata.addWarning('Invalid FourCC ID, maybe last OGG-page is not marked with last-page flag');
this.pageConsumer.flush();
}
}
else {
throw err;
}
}
}
}
exports.OggParser = OggParser;
OggParser.Header = {
len: 27,
get: (buf, off) => {
return {
capturePattern: FourCC_1.FourCcToken.get(buf, off),
version: Token.UINT8.get(buf, off + 4),
headerType: {
continued: util.getBit(buf, off + 5, 0),
firstPage: util.getBit(buf, off + 5, 1),
lastPage: util.getBit(buf, off + 5, 2)
},
// packet_flag: buf.readUInt8(off + 5),
absoluteGranulePosition: Number(Token.UINT64_LE.get(buf, off + 6)),
streamSerialNumber: Token.UINT32_LE.get(buf, off + 14),
pageSequenceNo: Token.UINT32_LE.get(buf, off + 18),
pageChecksum: Token.UINT32_LE.get(buf, off + 22),
page_segments: Token.UINT8.get(buf, off + 26)
};
}
};
+48
View File
@@ -0,0 +1,48 @@
import { IGetToken } from 'strtok3/lib/core';
/**
* Opus ID Header interface
* Ref: https://wiki.xiph.org/OggOpus#ID_Header
*/
export interface IIdHeader {
/**
* Magic signature: "OpusHead" (64 bits)
*/
magicSignature: string;
/**
* Version number (8 bits unsigned): 0x01 for this spec
*/
version: number;
/**
* Channel count 'c' (8 bits unsigned): MUST be > 0
*/
channelCount: number;
/**
* Pre-skip (16 bits unsigned, little endian)
*/
preSkip: number;
/**
* Input sample rate (32 bits unsigned, little endian): informational only
*/
inputSampleRate: number;
/**
* Output gain (16 bits, little endian, signed Q7.8 in dB) to apply when decoding
*/
outputGain: number;
/**
* Channel mapping family (8 bits unsigned)
* - 0 = one stream: mono or L,R stereo
* - 1 = channels in vorbis spec order: mono or L,R stereo or ... or FL,C,FR,RL,RR,LFE, ...
* - 2..254 = reserved (treat as 255)
* - 255 = no defined channel meaning
*/
channelMapping: number;
}
/**
* Opus ID Header parser
* Ref: https://wiki.xiph.org/OggOpus#ID_Header
*/
export declare class IdHeader implements IGetToken<IIdHeader> {
len: number;
constructor(len: number);
get(buf: any, off: any): IIdHeader;
}
+28
View File
@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.IdHeader = void 0;
const Token = require("token-types");
/**
* Opus ID Header parser
* Ref: https://wiki.xiph.org/OggOpus#ID_Header
*/
class IdHeader {
constructor(len) {
this.len = len;
if (len < 19) {
throw new Error("ID-header-page 0 should be at least 19 bytes long");
}
}
get(buf, off) {
return {
magicSignature: new Token.StringType(8, 'ascii').get(buf, off + 0),
version: buf.readUInt8(off + 8),
channelCount: buf.readUInt8(off + 9),
preSkip: buf.readInt16LE(off + 10),
inputSampleRate: buf.readInt32LE(off + 12),
outputGain: buf.readInt16LE(off + 16),
channelMapping: buf.readUInt8(off + 18)
};
}
}
exports.IdHeader = IdHeader;
+25
View File
@@ -0,0 +1,25 @@
/// <reference types="node" />
import { ITokenizer } from 'strtok3/lib/core';
import { IPageHeader } from '../Ogg';
import { VorbisParser } from '../vorbis/VorbisParser';
import { IOptions } from '../../type';
import { INativeMetadataCollector } from '../../common/MetadataCollector';
/**
* Opus parser
* Internet Engineering Task Force (IETF) - RFC 6716
* Used by OggParser
*/
export declare class OpusParser extends VorbisParser {
private tokenizer;
private idHeader;
private lastPos;
constructor(metadata: INativeMetadataCollector, options: IOptions, tokenizer: ITokenizer);
/**
* Parse first Opus Ogg page
* @param {IPageHeader} header
* @param {Buffer} pageData
*/
protected parseFirstPage(header: IPageHeader, pageData: Buffer): void;
protected parseFullPage(pageData: Buffer): void;
calculateDuration(header: IPageHeader): void;
}
+56
View File
@@ -0,0 +1,56 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpusParser = void 0;
const Token = require("token-types");
const Opus = require("./Opus");
const VorbisParser_1 = require("../vorbis/VorbisParser");
/**
* Opus parser
* Internet Engineering Task Force (IETF) - RFC 6716
* Used by OggParser
*/
class OpusParser extends VorbisParser_1.VorbisParser {
constructor(metadata, options, tokenizer) {
super(metadata, options);
this.tokenizer = tokenizer;
this.lastPos = -1;
}
/**
* Parse first Opus Ogg page
* @param {IPageHeader} header
* @param {Buffer} pageData
*/
parseFirstPage(header, pageData) {
this.metadata.setFormat('codec', 'Opus');
// Parse Opus ID Header
this.idHeader = new Opus.IdHeader(pageData.length).get(pageData, 0);
if (this.idHeader.magicSignature !== "OpusHead")
throw new Error("Illegal ogg/Opus magic-signature");
this.metadata.setFormat('sampleRate', this.idHeader.inputSampleRate);
this.metadata.setFormat('numberOfChannels', this.idHeader.channelCount);
}
parseFullPage(pageData) {
const magicSignature = new Token.StringType(8, 'ascii').get(pageData, 0);
switch (magicSignature) {
case 'OpusTags':
this.parseUserCommentList(pageData, 8);
this.lastPos = this.tokenizer.position - pageData.length;
break;
default:
break;
}
}
calculateDuration(header) {
if (this.metadata.format.sampleRate && header.absoluteGranulePosition >= 0) {
// Calculate duration
const pos_48bit = header.absoluteGranulePosition - this.idHeader.preSkip;
this.metadata.setFormat('numberOfSamples', pos_48bit);
this.metadata.setFormat('duration', pos_48bit / 48000);
if (this.lastPos !== -1 && this.tokenizer.fileInfo.size && this.metadata.format.duration) {
const dataSize = this.tokenizer.fileInfo.size - this.lastPos;
this.metadata.setFormat('bitrate', 8 * dataSize / this.metadata.format.duration);
}
}
}
}
exports.OpusParser = OpusParser;
+36
View File
@@ -0,0 +1,36 @@
import { IGetToken } from 'strtok3/lib/core';
/**
* Speex Header Packet
* Ref: https://www.speex.org/docs/manual/speex-manual/node8.html#SECTION00830000000000000000
*/
export interface IHeader {
/**
* speex_string, char[] 8
*/
speex: string;
/**
* speex_version, char[] 20
*/
version: string;
/**
* Version id
*/
version_id: number;
header_size: number;
rate: number;
mode: number;
mode_bitstream_version: number;
nb_channels: number;
bitrate: number;
frame_size: number;
vbr: number;
frames_per_packet: number;
extra_headers: number;
reserved1: number;
reserved2: number;
}
/**
* Speex Header Packet
* Ref: https://www.speex.org/docs/manual/speex-manual/node8.html#SECTION00830000000000000000
*/
export declare const Header: IGetToken<IHeader>;
+31
View File
@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Header = void 0;
const Token = require("token-types");
const util = require("../../common/Util");
/**
* Speex Header Packet
* Ref: https://www.speex.org/docs/manual/speex-manual/node8.html#SECTION00830000000000000000
*/
exports.Header = {
len: 80,
get: (buf, off) => {
return {
speex: new Token.StringType(8, 'ascii').get(buf, off + 0),
version: util.trimRightNull(new Token.StringType(20, 'ascii').get(buf, off + 8)),
version_id: buf.readInt32LE(off + 28),
header_size: buf.readInt32LE(off + 32),
rate: buf.readInt32LE(off + 36),
mode: buf.readInt32LE(off + 40),
mode_bitstream_version: buf.readInt32LE(off + 44),
nb_channels: buf.readInt32LE(off + 48),
bitrate: buf.readInt32LE(off + 52),
frame_size: buf.readInt32LE(off + 56),
vbr: buf.readInt32LE(off + 60),
frames_per_packet: buf.readInt32LE(off + 64),
extra_headers: buf.readInt32LE(off + 68),
reserved1: buf.readInt32LE(off + 72),
reserved2: buf.readInt32LE(off + 76)
};
}
};
+22
View File
@@ -0,0 +1,22 @@
/// <reference types="node" />
import { ITokenizer } from 'strtok3/lib/core';
import { IPageHeader } from '../Ogg';
import { VorbisParser } from '../vorbis/VorbisParser';
import { IOptions } from '../../type';
import { INativeMetadataCollector } from '../../common/MetadataCollector';
/**
* Speex, RFC 5574
* Ref:
* https://www.speex.org/docs/manual/speex-manual/
* https://tools.ietf.org/html/rfc5574
*/
export declare class SpeexParser extends VorbisParser {
private tokenizer;
constructor(metadata: INativeMetadataCollector, options: IOptions, tokenizer: ITokenizer);
/**
* Parse first Speex Ogg page
* @param {IPageHeader} header
* @param {Buffer} pageData
*/
protected parseFirstPage(header: IPageHeader, pageData: Buffer): void;
}
+35
View File
@@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpeexParser = void 0;
const initDebug = require("debug");
const Speex = require("./Speex");
const VorbisParser_1 = require("../vorbis/VorbisParser");
const debug = initDebug('music-metadata:parser:ogg:speex');
/**
* Speex, RFC 5574
* Ref:
* https://www.speex.org/docs/manual/speex-manual/
* https://tools.ietf.org/html/rfc5574
*/
class SpeexParser extends VorbisParser_1.VorbisParser {
constructor(metadata, options, tokenizer) {
super(metadata, options);
this.tokenizer = tokenizer;
}
/**
* Parse first Speex Ogg page
* @param {IPageHeader} header
* @param {Buffer} pageData
*/
parseFirstPage(header, pageData) {
debug('First Ogg/Speex page');
const speexHeader = Speex.Header.get(pageData, 0);
this.metadata.setFormat('codec', `Speex ${speexHeader.version}`);
this.metadata.setFormat('numberOfChannels', speexHeader.nb_channels);
this.metadata.setFormat('sampleRate', speexHeader.rate);
if (speexHeader.bitrate !== -1) {
this.metadata.setFormat('bitrate', speexHeader.bitrate);
}
}
}
exports.SpeexParser = SpeexParser;
+20
View File
@@ -0,0 +1,20 @@
import { IGetToken } from 'strtok3/lib/core';
/**
* 6.2 Identification Header
* Ref: https://theora.org/doc/Theora.pdf: 6.2 Identification Header Decode
*/
export interface IIdentificationHeader {
id: string;
vmaj: number;
vmin: number;
vrev: number;
vmbw: number;
vmbh: number;
nombr: number;
nqual: number;
}
/**
* 6.2 Identification Header
* Ref: https://theora.org/doc/Theora.pdf: 6.2 Identification Header Decode
*/
export declare const IdentificationHeader: IGetToken<IIdentificationHeader>;
+23
View File
@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.IdentificationHeader = void 0;
const Token = require("token-types");
/**
* 6.2 Identification Header
* Ref: https://theora.org/doc/Theora.pdf: 6.2 Identification Header Decode
*/
exports.IdentificationHeader = {
len: 42,
get: (buf, off) => {
return {
id: new Token.StringType(7, 'ascii').get(buf, off),
vmaj: buf.readUInt8(off + 7),
vmin: buf.readUInt8(off + 8),
vrev: buf.readUInt8(off + 9),
vmbw: buf.readUInt16BE(off + 10),
vmbh: buf.readUInt16BE(off + 17),
nombr: Token.UINT24_BE.get(buf, off + 37),
nqual: buf.readUInt8(off + 40)
};
}
};
+28
View File
@@ -0,0 +1,28 @@
/// <reference types="node" />
import { ITokenizer } from 'strtok3/lib/core';
import { IOptions } from '../../type';
import { INativeMetadataCollector } from '../../common/MetadataCollector';
import * as Ogg from '../Ogg';
/**
* Ref:
* https://theora.org/doc/Theora.pdf
*/
export declare class TheoraParser implements Ogg.IPageConsumer {
private metadata;
private tokenizer;
constructor(metadata: INativeMetadataCollector, options: IOptions, tokenizer: ITokenizer);
/**
* Vorbis 1 parser
* @param header Ogg Page Header
* @param pageData Page data
*/
parsePage(header: Ogg.IPageHeader, pageData: Buffer): void;
flush(): void;
calculateDuration(header: Ogg.IPageHeader): void;
/**
* Parse first Theora Ogg page. the initial identification header packet
* @param {IPageHeader} header
* @param {Buffer} pageData
*/
protected parseFirstPage(header: Ogg.IPageHeader, pageData: Buffer): void;
}
+44
View File
@@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TheoraParser = void 0;
const initDebug = require("debug");
const Theora_1 = require("./Theora");
const debug = initDebug('music-metadata:parser:ogg:theora');
/**
* Ref:
* https://theora.org/doc/Theora.pdf
*/
class TheoraParser {
constructor(metadata, options, tokenizer) {
this.metadata = metadata;
this.tokenizer = tokenizer;
}
/**
* Vorbis 1 parser
* @param header Ogg Page Header
* @param pageData Page data
*/
parsePage(header, pageData) {
if (header.headerType.firstPage) {
this.parseFirstPage(header, pageData);
}
}
flush() {
debug('flush');
}
calculateDuration(header) {
debug('duration calculation not implemented');
}
/**
* Parse first Theora Ogg page. the initial identification header packet
* @param {IPageHeader} header
* @param {Buffer} pageData
*/
parseFirstPage(header, pageData) {
debug('First Ogg/Theora page');
this.metadata.setFormat('codec', 'Theora');
const idHeader = Theora_1.IdentificationHeader.get(pageData, 0);
this.metadata.setFormat('bitrate', idHeader.nombr);
}
}
exports.TheoraParser = TheoraParser;
+79
View File
@@ -0,0 +1,79 @@
/// <reference types="node" />
import { IPicture } from '../../type';
import { IGetToken } from 'strtok3/lib/core';
export interface IVorbisPicture extends IPicture {
type: string;
description: string;
width: number;
height: number;
colour_depth: number;
indexed_color: number;
}
/**
* Interface to parsed result of METADATA_BLOCK_PICTURE
* Ref: https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
* Ref: https://xiph.org/flac/format.html#metadata_block_picture
*/
export interface IVorbisPicture {
type: string;
format: string;
description: string;
width: number;
height: number;
colour_depth: number;
indexed_color: number;
data: Buffer;
}
/**
* Parse the METADATA_BLOCK_PICTURE
* Ref: https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
* Ref: https://xiph.org/flac/format.html#metadata_block_picture
* // ToDo: move to ID3 / APIC?
*/
export declare class VorbisPictureToken implements IGetToken<IVorbisPicture> {
len: any;
static fromBase64(base64str: string): IVorbisPicture;
static fromBuffer(buffer: Buffer): IVorbisPicture;
constructor(len: any);
get(buffer: Buffer, offset: number): IVorbisPicture;
}
/**
* Vorbis 1 decoding tokens
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-620004.2.1
*/
/**
* Comment header interface
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-620004.2.1
*/
export interface ICommonHeader {
/**
* Packet Type
*/
packetType: number;
/**
* Should be 'vorbis'
*/
vorbis: string;
}
/**
* Comment header decoder
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-620004.2.1
*/
export declare const CommonHeader: IGetToken<ICommonHeader>;
/**
* Identification header interface
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2
*/
export interface IFormatInfo {
version: number;
channelMode: number;
sampleRate: number;
bitrateMax: number;
bitrateNominal: number;
bitrateMin: number;
}
/**
* Identification header decoder
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2
*/
export declare const IdentificationHeader: IGetToken<IFormatInfo>;
+78
View File
@@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.IdentificationHeader = exports.CommonHeader = exports.VorbisPictureToken = void 0;
const Token = require("token-types");
const ID3v2Token_1 = require("../../id3v2/ID3v2Token");
/**
* Parse the METADATA_BLOCK_PICTURE
* Ref: https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
* Ref: https://xiph.org/flac/format.html#metadata_block_picture
* // ToDo: move to ID3 / APIC?
*/
class VorbisPictureToken {
constructor(len) {
this.len = len;
}
static fromBase64(base64str) {
return this.fromBuffer(Buffer.from(base64str, 'base64'));
}
static fromBuffer(buffer) {
const pic = new VorbisPictureToken(buffer.length);
return pic.get(buffer, 0);
}
get(buffer, offset) {
const type = ID3v2Token_1.AttachedPictureType[Token.UINT32_BE.get(buffer, offset)];
const mimeLen = Token.UINT32_BE.get(buffer, offset += 4);
const format = buffer.toString('utf-8', offset += 4, offset + mimeLen);
const descLen = Token.UINT32_BE.get(buffer, offset += mimeLen);
const description = buffer.toString('utf-8', offset += 4, offset + descLen);
const width = Token.UINT32_BE.get(buffer, offset += descLen);
const height = Token.UINT32_BE.get(buffer, offset += 4);
const colour_depth = Token.UINT32_BE.get(buffer, offset += 4);
const indexed_color = Token.UINT32_BE.get(buffer, offset += 4);
const picDataLen = Token.UINT32_BE.get(buffer, offset += 4);
const data = Buffer.from(buffer.slice(offset += 4, offset + picDataLen));
return {
type,
format,
description,
width,
height,
colour_depth,
indexed_color,
data
};
}
}
exports.VorbisPictureToken = VorbisPictureToken;
/**
* Comment header decoder
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-620004.2.1
*/
exports.CommonHeader = {
len: 7,
get: (buf, off) => {
return {
packetType: buf.readUInt8(off),
vorbis: new Token.StringType(6, 'ascii').get(buf, off + 1)
};
}
};
/**
* Identification header decoder
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2
*/
exports.IdentificationHeader = {
len: 23,
get: (uint8Array, off) => {
const dataView = new DataView(uint8Array.buffer, uint8Array.byteOffset);
return {
version: dataView.getUint32(off + 0, true),
channelMode: dataView.getUint8(off + 4),
sampleRate: dataView.getUint32(off + 5, true),
bitrateMax: dataView.getUint32(off + 9, true),
bitrateNominal: dataView.getUint32(off + 13, true),
bitrateMin: dataView.getUint32(off + 17, true)
};
}
};
+12
View File
@@ -0,0 +1,12 @@
export declare class VorbisDecoder {
private readonly data;
private offset;
constructor(data: Uint8Array, offset: any);
readInt32(): number;
readStringUtf8(): string;
parseUserComment(): {
key: string;
value: string;
len: number;
};
}
+32
View File
@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VorbisDecoder = void 0;
const Token = require("token-types");
class VorbisDecoder {
constructor(data, offset) {
this.data = data;
this.offset = offset;
}
readInt32() {
const value = Token.UINT32_LE.get(this.data, this.offset);
this.offset += 4;
return value;
}
readStringUtf8() {
const len = this.readInt32();
const value = Buffer.from(this.data).toString('utf-8', this.offset, this.offset + len);
this.offset += len;
return value;
}
parseUserComment() {
const offset0 = this.offset;
const v = this.readStringUtf8();
const idx = v.indexOf('=');
return {
key: v.slice(0, idx).toUpperCase(),
value: v.slice(idx + 1),
len: this.offset - offset0
};
}
}
exports.VorbisDecoder = VorbisDecoder;
+36
View File
@@ -0,0 +1,36 @@
/// <reference types="node" />
import { IOptions } from '../../type';
import { INativeMetadataCollector } from '../../common/MetadataCollector';
import * as Ogg from '../Ogg';
import { IVorbisPicture } from './Vorbis';
/**
* Vorbis 1 Parser.
* Used by OggParser
*/
export declare class VorbisParser implements Ogg.IPageConsumer {
protected metadata: INativeMetadataCollector;
protected options: IOptions;
private pageSegments;
constructor(metadata: INativeMetadataCollector, options: IOptions);
/**
* Vorbis 1 parser
* @param header Ogg Page Header
* @param pageData Page data
*/
parsePage(header: Ogg.IPageHeader, pageData: Buffer): void;
flush(): void;
parseUserComment(pageData: Buffer, offset: number): number;
addTag(id: string, value: string | IVorbisPicture): void;
calculateDuration(header: Ogg.IPageHeader): void;
/**
* Parse first Ogg/Vorbis page
* @param {IPageHeader} header
* @param {Buffer} pageData
*/
protected parseFirstPage(header: Ogg.IPageHeader, pageData: Buffer): void;
protected parseFullPage(pageData: Buffer): void;
/**
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-840005.2
*/
protected parseUserCommentList(pageData: Buffer, offset: number): void;
}
+128
View File
@@ -0,0 +1,128 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VorbisParser = void 0;
const Token = require("token-types");
const _debug = require("debug");
const VorbisDecoder_1 = require("./VorbisDecoder");
const Vorbis_1 = require("./Vorbis");
const debug = _debug('music-metadata:parser:ogg:vorbis1');
/**
* Vorbis 1 Parser.
* Used by OggParser
*/
class VorbisParser {
constructor(metadata, options) {
this.metadata = metadata;
this.options = options;
this.pageSegments = [];
}
/**
* Vorbis 1 parser
* @param header Ogg Page Header
* @param pageData Page data
*/
parsePage(header, pageData) {
if (header.headerType.firstPage) {
this.parseFirstPage(header, pageData);
}
else {
if (header.headerType.continued) {
if (this.pageSegments.length === 0) {
throw new Error("Cannot continue on previous page");
}
this.pageSegments.push(pageData);
}
if (header.headerType.lastPage || !header.headerType.continued) {
// Flush page segments
if (this.pageSegments.length > 0) {
const fullPage = Buffer.concat(this.pageSegments);
this.parseFullPage(fullPage);
}
// Reset page segments
this.pageSegments = header.headerType.lastPage ? [] : [pageData];
}
}
if (header.headerType.lastPage) {
this.calculateDuration(header);
}
}
flush() {
this.parseFullPage(Buffer.concat(this.pageSegments));
}
parseUserComment(pageData, offset) {
const decoder = new VorbisDecoder_1.VorbisDecoder(pageData, offset);
const tag = decoder.parseUserComment();
this.addTag(tag.key, tag.value);
return tag.len;
}
addTag(id, value) {
if (id === 'METADATA_BLOCK_PICTURE' && (typeof value === 'string')) {
if (this.options.skipCovers) {
debug(`Ignore picture`);
return;
}
value = Vorbis_1.VorbisPictureToken.fromBase64(value);
debug(`Push picture: id=${id}, format=${value.format}`);
}
else {
debug(`Push tag: id=${id}, value=${value}`);
}
this.metadata.addTag('vorbis', id, value);
}
calculateDuration(header) {
if (this.metadata.format.sampleRate && header.absoluteGranulePosition >= 0) {
// Calculate duration
this.metadata.setFormat('numberOfSamples', header.absoluteGranulePosition);
this.metadata.setFormat('duration', this.metadata.format.numberOfSamples / this.metadata.format.sampleRate);
}
}
/**
* Parse first Ogg/Vorbis page
* @param {IPageHeader} header
* @param {Buffer} pageData
*/
parseFirstPage(header, pageData) {
this.metadata.setFormat('codec', 'Vorbis I');
debug("Parse first page");
// Parse Vorbis common header
const commonHeader = Vorbis_1.CommonHeader.get(pageData, 0);
if (commonHeader.vorbis !== 'vorbis')
throw new Error('Metadata does not look like Vorbis');
if (commonHeader.packetType === 1) {
const idHeader = Vorbis_1.IdentificationHeader.get(pageData, Vorbis_1.CommonHeader.len);
this.metadata.setFormat('sampleRate', idHeader.sampleRate);
this.metadata.setFormat('bitrate', idHeader.bitrateNominal);
this.metadata.setFormat('numberOfChannels', idHeader.channelMode);
debug("sample-rate=%s[hz], bitrate=%s[b/s], channel-mode=%s", idHeader.sampleRate, idHeader.bitrateNominal, idHeader.channelMode);
}
else
throw new Error('First Ogg page should be type 1: the identification header');
}
parseFullPage(pageData) {
// New page
const commonHeader = Vorbis_1.CommonHeader.get(pageData, 0);
debug("Parse full page: type=%s, byteLength=%s", commonHeader.packetType, pageData.byteLength);
switch (commonHeader.packetType) {
case 3: // type 3: comment header
return this.parseUserCommentList(pageData, Vorbis_1.CommonHeader.len);
case 1: // type 1: the identification header
case 5: // type 5: setup header type
break; // ignore
}
}
/**
* Ref: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-840005.2
*/
parseUserCommentList(pageData, offset) {
const strLen = Token.UINT32_LE.get(pageData, offset);
offset += 4;
// const vendorString = new Token.StringType(strLen, 'utf-8').get(pageData, offset);
offset += strLen;
let userCommentListLength = Token.UINT32_LE.get(pageData, offset);
offset += 4;
while (userCommentListLength-- > 0) {
offset += this.parseUserComment(pageData, offset);
}
}
}
exports.VorbisParser = VorbisParser;
+7
View File
@@ -0,0 +1,7 @@
import { CommonTagMapper } from '../../common/GenericTagMapper';
import { IRating, ITag } from '../../type';
export declare class VorbisTagMapper extends CommonTagMapper {
static toRating(email: string, rating: string): IRating;
constructor();
protected postMap(tag: ITag): void;
}
+132
View File
@@ -0,0 +1,132 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VorbisTagMapper = void 0;
const GenericTagMapper_1 = require("../../common/GenericTagMapper");
/**
* Vorbis tag mappings
*
* Mapping from native header format to one or possibly more 'common' entries
* The common entries aim to read the same information from different media files
* independent of the underlying format
*/
const vorbisTagMap = {
TITLE: 'title',
ARTIST: 'artist',
ARTISTS: 'artists',
ALBUMARTIST: 'albumartist',
'ALBUM ARTIST': 'albumartist',
ALBUM: 'album',
DATE: 'date',
ORIGINALDATE: 'originaldate',
ORIGINALYEAR: 'originalyear',
COMMENT: 'comment',
TRACKNUMBER: 'track',
DISCNUMBER: 'disk',
GENRE: 'genre',
METADATA_BLOCK_PICTURE: 'picture',
COMPOSER: 'composer',
LYRICS: 'lyrics',
ALBUMSORT: 'albumsort',
TITLESORT: 'titlesort',
WORK: 'work',
ARTISTSORT: 'artistsort',
ALBUMARTISTSORT: 'albumartistsort',
COMPOSERSORT: 'composersort',
LYRICIST: 'lyricist',
WRITER: 'writer',
CONDUCTOR: 'conductor',
// 'PERFORMER=artist (instrument)': 'performer:instrument', // ToDo
REMIXER: 'remixer',
ARRANGER: 'arranger',
ENGINEER: 'engineer',
PRODUCER: 'producer',
DJMIXER: 'djmixer',
MIXER: 'mixer',
LABEL: 'label',
GROUPING: 'grouping',
SUBTITLE: 'subtitle',
DISCSUBTITLE: 'discsubtitle',
TRACKTOTAL: 'totaltracks',
DISCTOTAL: 'totaldiscs',
COMPILATION: 'compilation',
RATING: 'rating',
BPM: 'bpm',
KEY: 'key',
MOOD: 'mood',
MEDIA: 'media',
CATALOGNUMBER: 'catalognumber',
RELEASESTATUS: 'releasestatus',
RELEASETYPE: 'releasetype',
RELEASECOUNTRY: 'releasecountry',
SCRIPT: 'script',
LANGUAGE: 'language',
COPYRIGHT: 'copyright',
LICENSE: 'license',
ENCODEDBY: 'encodedby',
ENCODERSETTINGS: 'encodersettings',
BARCODE: 'barcode',
ISRC: 'isrc',
ASIN: 'asin',
MUSICBRAINZ_TRACKID: 'musicbrainz_recordingid',
MUSICBRAINZ_RELEASETRACKID: 'musicbrainz_trackid',
MUSICBRAINZ_ALBUMID: 'musicbrainz_albumid',
MUSICBRAINZ_ARTISTID: 'musicbrainz_artistid',
MUSICBRAINZ_ALBUMARTISTID: 'musicbrainz_albumartistid',
MUSICBRAINZ_RELEASEGROUPID: 'musicbrainz_releasegroupid',
MUSICBRAINZ_WORKID: 'musicbrainz_workid',
MUSICBRAINZ_TRMID: 'musicbrainz_trmid',
MUSICBRAINZ_DISCID: 'musicbrainz_discid',
ACOUSTID_ID: 'acoustid_id',
ACOUSTID_ID_FINGERPRINT: 'acoustid_fingerprint',
MUSICIP_PUID: 'musicip_puid',
// 'FINGERPRINT=MusicMagic Fingerprint {fingerprint}': 'musicip_fingerprint', // ToDo
WEBSITE: 'website',
NOTES: 'notes',
TOTALTRACKS: 'totaltracks',
TOTALDISCS: 'totaldiscs',
// Discogs
DISCOGS_ARTIST_ID: 'discogs_artist_id',
DISCOGS_ARTISTS: 'artists',
DISCOGS_ARTIST_NAME: 'artists',
DISCOGS_ALBUM_ARTISTS: 'albumartist',
DISCOGS_CATALOG: 'catalognumber',
DISCOGS_COUNTRY: 'releasecountry',
DISCOGS_DATE: 'originaldate',
DISCOGS_LABEL: 'label',
DISCOGS_LABEL_ID: 'discogs_label_id',
DISCOGS_MASTER_RELEASE_ID: 'discogs_master_release_id',
DISCOGS_RATING: 'discogs_rating',
DISCOGS_RELEASED: 'date',
DISCOGS_RELEASE_ID: 'discogs_release_id',
DISCOGS_VOTES: 'discogs_votes',
CATALOGID: 'catalognumber',
STYLE: 'genre',
//
REPLAYGAIN_TRACK_GAIN: 'replaygain_track_gain',
REPLAYGAIN_TRACK_PEAK: 'replaygain_track_peak',
REPLAYGAIN_ALBUM_GAIN: 'replaygain_album_gain',
REPLAYGAIN_ALBUM_PEAK: 'replaygain_album_peak',
// To Sure if these (REPLAYGAIN_MINMAX, REPLAYGAIN_ALBUM_MINMAX & REPLAYGAIN_UNDO) are used for Vorbis:
REPLAYGAIN_MINMAX: 'replaygain_track_minmax',
REPLAYGAIN_ALBUM_MINMAX: 'replaygain_album_minmax',
REPLAYGAIN_UNDO: 'replaygain_undo'
};
class VorbisTagMapper extends GenericTagMapper_1.CommonTagMapper {
static toRating(email, rating) {
return {
source: email ? email.toLowerCase() : email,
rating: parseFloat(rating) * GenericTagMapper_1.CommonTagMapper.maxRatingScore
};
}
constructor() {
super(['vorbis'], vorbisTagMap);
}
postMap(tag) {
if (tag.id.indexOf('RATING:') === 0) {
const keys = tag.id.split(':');
tag.value = VorbisTagMapper.toRating(keys[1], tag.value);
tag.id = keys[0];
}
}
}
exports.VorbisTagMapper = VorbisTagMapper;