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
+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;