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
+27
View File
@@ -0,0 +1,27 @@
/**
* Extended Lame Header
*/
import { IGetToken } from 'strtok3/lib/core';
import { IReplayGain } from './ReplayGainDataFormat';
/**
* LAME Tag, extends the Xing header format
* First added in LAME 3.12 for VBR
* The modified header is also included in CBR files (effective LAME 3.94), with "Info" instead of "XING" near the beginning.
*/
export interface IExtendedLameHeader {
revision: number;
vbr_method: number;
lowpass_filter: number;
track_peak?: number;
track_gain: IReplayGain;
album_gain: IReplayGain;
music_length: number;
music_crc: number;
header_crc: number;
}
/**
* Info Tag
* @link http://gabriel.mp3-tech.org/mp3infotag.html
* @link https://github.com/quodlibet/mutagen/blob/abd58ee58772224334a18817c3fb31103572f70e/mutagen/mp3/_util.py#L112
*/
export declare const ExtendedLameHeader: IGetToken<IExtendedLameHeader>;
+31
View File
@@ -0,0 +1,31 @@
"use strict";
/**
* Extended Lame Header
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExtendedLameHeader = void 0;
const Token = require("token-types");
const common = require("../common/Util");
const ReplayGainDataFormat_1 = require("./ReplayGainDataFormat");
/**
* Info Tag
* @link http://gabriel.mp3-tech.org/mp3infotag.html
* @link https://github.com/quodlibet/mutagen/blob/abd58ee58772224334a18817c3fb31103572f70e/mutagen/mp3/_util.py#L112
*/
exports.ExtendedLameHeader = {
len: 27,
get: (buf, off) => {
const track_peak = Token.UINT32_BE.get(buf, off + 2);
return {
revision: common.getBitAllignedNumber(buf, off, 0, 4),
vbr_method: common.getBitAllignedNumber(buf, off, 4, 4),
lowpass_filter: 100 * Token.UINT8.get(buf, off + 1),
track_peak: track_peak === 0 ? undefined : track_peak / Math.pow(2, 23),
track_gain: ReplayGainDataFormat_1.ReplayGain.get(buf, 6),
album_gain: ReplayGainDataFormat_1.ReplayGain.get(buf, 8),
music_length: Token.UINT32_BE.get(buf, off + 20),
music_crc: Token.UINT8.get(buf, off + 24),
header_crc: Token.UINT16_BE.get(buf, off + 24)
};
}
};
+49
View File
@@ -0,0 +1,49 @@
import { AbstractID3Parser } from '../id3v2/AbstractID3Parser';
export declare class MpegParser extends AbstractID3Parser {
private frameCount;
private syncFrameCount;
private countSkipFrameData;
private totalDataLength;
private audioFrameHeader;
private bitrates;
private offset;
private frame_size;
private crc;
private calculateEofDuration;
private samplesPerFrame;
private buf_frame_header;
/**
* Number of bytes already parsed since beginning of stream / file
*/
private mpegOffset;
private syncPeek;
/**
* Called after ID3 headers have been parsed
*/
_parse(): Promise<void>;
/**
* Called after file has been fully parsed, this allows, if present, to exclude the ID3v1.1 header length
*/
protected finalize(): void;
private sync;
/**
* Combined ADTS & MPEG (MP2 & MP3) header handling
* @return {Promise<boolean>} true if parser should quit
*/
private parseCommonMpegHeader;
/**
* @return {Promise<boolean>} true if parser should quit
*/
private parseAudioFrameHeader;
private parseAdts;
private parseCrc;
private skipSideInformation;
private readXtraInfoHeader;
/**
* Ref: http://gabriel.mp3-tech.org/mp3infotag.html
* @returns {Promise<string>}
*/
private readXingInfoHeader;
private skipFrameData;
private areAllSame;
}
+529
View File
@@ -0,0 +1,529 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MpegParser = void 0;
const Token = require("token-types");
const core_1 = require("strtok3/lib/core");
const initDebug = require("debug");
const common = require("../common/Util");
const AbstractID3Parser_1 = require("../id3v2/AbstractID3Parser");
const XingTag_1 = require("./XingTag");
const debug = initDebug('music-metadata:parser:mpeg');
/**
* Cache buffer size used for searching synchronization preabmle
*/
const maxPeekLen = 1024;
/**
* MPEG-4 Audio definitions
* Ref: https://wiki.multimedia.cx/index.php/MPEG-4_Audio
*/
const MPEG4 = {
/**
* Audio Object Types
*/
AudioObjectTypes: [
'AAC Main',
'AAC LC',
'AAC SSR',
'AAC LTP' // Long Term Prediction
],
/**
* Sampling Frequencies
* https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Sampling_Frequencies
*/
SamplingFrequencies: [
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, undefined, undefined, -1
]
/**
* Channel Configurations
*/
};
const MPEG4_ChannelConfigurations = [
undefined,
['front-center'],
['front-left', 'front-right'],
['front-center', 'front-left', 'front-right'],
['front-center', 'front-left', 'front-right', 'back-center'],
['front-center', 'front-left', 'front-right', 'back-left', 'back-right'],
['front-center', 'front-left', 'front-right', 'back-left', 'back-right', 'LFE-channel'],
['front-center', 'front-left', 'front-right', 'side-left', 'side-right', 'back-left', 'back-right', 'LFE-channel']
];
/**
* MPEG Audio Layer I/II/III frame header
* Ref: https://www.mp3-tech.org/programmer/frame_header.html
* Bit layout: AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
* Ref: https://wiki.multimedia.cx/index.php/ADTS
*/
class MpegFrameHeader {
constructor(buf, off) {
// B(20,19): MPEG Audio versionIndex ID
this.versionIndex = common.getBitAllignedNumber(buf, off + 1, 3, 2);
// C(18,17): Layer description
this.layer = MpegFrameHeader.LayerDescription[common.getBitAllignedNumber(buf, off + 1, 5, 2)];
if (this.versionIndex > 1 && this.layer === 0) {
this.parseAdtsHeader(buf, off); // Audio Data Transport Stream (ADTS)
}
else {
this.parseMpegHeader(buf, off); // Conventional MPEG header
}
// D(16): Protection bit (if true 16-bit CRC follows header)
this.isProtectedByCRC = !common.isBitSet(buf, off + 1, 7);
}
calcDuration(numFrames) {
return numFrames * this.calcSamplesPerFrame() / this.samplingRate;
}
calcSamplesPerFrame() {
return MpegFrameHeader.samplesInFrameTable[this.version === 1 ? 0 : 1][this.layer];
}
calculateSideInfoLength() {
if (this.layer !== 3)
return 2;
if (this.channelModeIndex === 3) {
// mono
if (this.version === 1) {
return 17;
}
else if (this.version === 2 || this.version === 2.5) {
return 9;
}
}
else {
if (this.version === 1) {
return 32;
}
else if (this.version === 2 || this.version === 2.5) {
return 17;
}
}
}
calcSlotSize() {
return [null, 4, 1, 1][this.layer];
}
parseMpegHeader(buf, off) {
this.container = 'MPEG';
// E(15,12): Bitrate index
this.bitrateIndex = common.getBitAllignedNumber(buf, off + 2, 0, 4);
// F(11,10): Sampling rate frequency index
this.sampRateFreqIndex = common.getBitAllignedNumber(buf, off + 2, 4, 2);
// G(9): Padding bit
this.padding = common.isBitSet(buf, off + 2, 6);
// H(8): Private bit
this.privateBit = common.isBitSet(buf, off + 2, 7);
// I(7,6): Channel Mode
this.channelModeIndex = common.getBitAllignedNumber(buf, off + 3, 0, 2);
// J(5,4): Mode extension (Only used in Joint stereo)
this.modeExtension = common.getBitAllignedNumber(buf, off + 3, 2, 2);
// K(3): Copyright
this.isCopyrighted = common.isBitSet(buf, off + 3, 4);
// L(2): Original
this.isOriginalMedia = common.isBitSet(buf, off + 3, 5);
// M(3): The original bit indicates, if it is set, that the frame is located on its original media.
this.emphasis = common.getBitAllignedNumber(buf, off + 3, 7, 2);
this.version = MpegFrameHeader.VersionID[this.versionIndex];
this.channelMode = MpegFrameHeader.ChannelMode[this.channelModeIndex];
this.codec = `MPEG ${this.version} Layer ${this.layer}`;
// Calculate bitrate
const bitrateInKbps = this.calcBitrate();
if (!bitrateInKbps) {
throw new Error('Cannot determine bit-rate');
}
this.bitrate = bitrateInKbps * 1000;
// Calculate sampling rate
this.samplingRate = this.calcSamplingRate();
if (this.samplingRate == null) {
throw new Error('Cannot determine sampling-rate');
}
}
parseAdtsHeader(buf, off) {
debug(`layer=0 => ADTS`);
this.version = this.versionIndex === 2 ? 4 : 2;
this.container = 'ADTS/MPEG-' + this.version;
const profileIndex = common.getBitAllignedNumber(buf, off + 2, 0, 2);
this.codec = 'AAC';
this.codecProfile = MPEG4.AudioObjectTypes[profileIndex];
debug(`MPEG-4 audio-codec=${this.codec}`);
const samplingFrequencyIndex = common.getBitAllignedNumber(buf, off + 2, 2, 4);
this.samplingRate = MPEG4.SamplingFrequencies[samplingFrequencyIndex];
debug(`sampling-rate=${this.samplingRate}`);
const channelIndex = common.getBitAllignedNumber(buf, off + 2, 7, 3);
this.mp4ChannelConfig = MPEG4_ChannelConfigurations[channelIndex];
debug(`channel-config=${this.mp4ChannelConfig.join('+')}`);
this.frameLength = common.getBitAllignedNumber(buf, off + 3, 6, 2) << 11;
}
calcBitrate() {
if (this.bitrateIndex === 0x00 || // free
this.bitrateIndex === 0x0F) { // reserved
return;
}
const codecIndex = `${Math.floor(this.version)}${this.layer}`;
return MpegFrameHeader.bitrate_index[this.bitrateIndex][codecIndex];
}
calcSamplingRate() {
if (this.sampRateFreqIndex === 0x03)
return null; // 'reserved'
return MpegFrameHeader.sampling_rate_freq_index[this.version][this.sampRateFreqIndex];
}
}
MpegFrameHeader.SyncByte1 = 0xFF;
MpegFrameHeader.SyncByte2 = 0xE0;
MpegFrameHeader.VersionID = [2.5, null, 2, 1];
MpegFrameHeader.LayerDescription = [0, 3, 2, 1];
MpegFrameHeader.ChannelMode = ['stereo', 'joint_stereo', 'dual_channel', 'mono'];
MpegFrameHeader.bitrate_index = {
0x01: { 11: 32, 12: 32, 13: 32, 21: 32, 22: 8, 23: 8 },
0x02: { 11: 64, 12: 48, 13: 40, 21: 48, 22: 16, 23: 16 },
0x03: { 11: 96, 12: 56, 13: 48, 21: 56, 22: 24, 23: 24 },
0x04: { 11: 128, 12: 64, 13: 56, 21: 64, 22: 32, 23: 32 },
0x05: { 11: 160, 12: 80, 13: 64, 21: 80, 22: 40, 23: 40 },
0x06: { 11: 192, 12: 96, 13: 80, 21: 96, 22: 48, 23: 48 },
0x07: { 11: 224, 12: 112, 13: 96, 21: 112, 22: 56, 23: 56 },
0x08: { 11: 256, 12: 128, 13: 112, 21: 128, 22: 64, 23: 64 },
0x09: { 11: 288, 12: 160, 13: 128, 21: 144, 22: 80, 23: 80 },
0x0A: { 11: 320, 12: 192, 13: 160, 21: 160, 22: 96, 23: 96 },
0x0B: { 11: 352, 12: 224, 13: 192, 21: 176, 22: 112, 23: 112 },
0x0C: { 11: 384, 12: 256, 13: 224, 21: 192, 22: 128, 23: 128 },
0x0D: { 11: 416, 12: 320, 13: 256, 21: 224, 22: 144, 23: 144 },
0x0E: { 11: 448, 12: 384, 13: 320, 21: 256, 22: 160, 23: 160 }
};
MpegFrameHeader.sampling_rate_freq_index = {
1: { 0x00: 44100, 0x01: 48000, 0x02: 32000 },
2: { 0x00: 22050, 0x01: 24000, 0x02: 16000 },
2.5: { 0x00: 11025, 0x01: 12000, 0x02: 8000 }
};
MpegFrameHeader.samplesInFrameTable = [
/* Layer I II III */
[0, 384, 1152, 1152],
[0, 384, 1152, 576] // MPEG-2(.5
];
/**
* MPEG Audio Layer I/II/III
*/
const FrameHeader = {
len: 4,
get: (buf, off) => {
return new MpegFrameHeader(buf, off);
}
};
function getVbrCodecProfile(vbrScale) {
return 'V' + Math.floor((100 - vbrScale) / 10);
}
class MpegParser extends AbstractID3Parser_1.AbstractID3Parser {
constructor() {
super(...arguments);
this.frameCount = 0;
this.syncFrameCount = -1;
this.countSkipFrameData = 0;
this.totalDataLength = 0;
this.bitrates = [];
this.calculateEofDuration = false;
this.buf_frame_header = Buffer.alloc(4);
this.syncPeek = {
buf: Buffer.alloc(maxPeekLen),
len: 0
};
}
/**
* Called after ID3 headers have been parsed
*/
async _parse() {
this.metadata.setFormat('lossless', false);
try {
let quit = false;
while (!quit) {
await this.sync();
quit = await this.parseCommonMpegHeader();
}
}
catch (err) {
if (err instanceof core_1.EndOfStreamError) {
debug(`End-of-stream`);
if (this.calculateEofDuration) {
const numberOfSamples = this.frameCount * this.samplesPerFrame;
this.metadata.setFormat('numberOfSamples', numberOfSamples);
const duration = numberOfSamples / this.metadata.format.sampleRate;
debug(`Calculate duration at EOF: ${duration} sec.`, duration);
this.metadata.setFormat('duration', duration);
}
}
else {
throw err;
}
}
}
/**
* Called after file has been fully parsed, this allows, if present, to exclude the ID3v1.1 header length
*/
finalize() {
const format = this.metadata.format;
const hasID3v1 = this.metadata.native.hasOwnProperty('ID3v1');
if (format.duration && this.tokenizer.fileInfo.size) {
const mpegSize = this.tokenizer.fileInfo.size - this.mpegOffset - (hasID3v1 ? 128 : 0);
if (format.codecProfile && format.codecProfile[0] === 'V') {
this.metadata.setFormat('bitrate', mpegSize * 8 / format.duration);
}
}
else if (this.tokenizer.fileInfo.size && format.codecProfile === 'CBR') {
const mpegSize = this.tokenizer.fileInfo.size - this.mpegOffset - (hasID3v1 ? 128 : 0);
const numberOfSamples = Math.round(mpegSize / this.frame_size) * this.samplesPerFrame;
this.metadata.setFormat('numberOfSamples', numberOfSamples);
const duration = numberOfSamples / format.sampleRate;
debug("Calculate CBR duration based on file size: %s", duration);
this.metadata.setFormat('duration', duration);
}
}
async sync() {
let gotFirstSync = false;
while (true) {
let bo = 0;
this.syncPeek.len = await this.tokenizer.peekBuffer(this.syncPeek.buf, { length: maxPeekLen, mayBeLess: true });
if (this.syncPeek.len <= 163) {
throw new core_1.EndOfStreamError();
}
while (true) {
if (gotFirstSync && (this.syncPeek.buf[bo] & 0xE0) === 0xE0) {
this.buf_frame_header[0] = MpegFrameHeader.SyncByte1;
this.buf_frame_header[1] = this.syncPeek.buf[bo];
await this.tokenizer.ignore(bo);
debug(`Sync at offset=${this.tokenizer.position - 1}, frameCount=${this.frameCount}`);
if (this.syncFrameCount === this.frameCount) {
debug(`Re-synced MPEG stream, frameCount=${this.frameCount}`);
this.frameCount = 0;
this.frame_size = 0;
}
this.syncFrameCount = this.frameCount;
return; // sync
}
else {
gotFirstSync = false;
bo = this.syncPeek.buf.indexOf(MpegFrameHeader.SyncByte1, bo);
if (bo === -1) {
if (this.syncPeek.len < this.syncPeek.buf.length) {
throw new core_1.EndOfStreamError();
}
await this.tokenizer.ignore(this.syncPeek.len);
break; // continue with next buffer
}
else {
++bo;
gotFirstSync = true;
}
}
}
}
}
/**
* Combined ADTS & MPEG (MP2 & MP3) header handling
* @return {Promise<boolean>} true if parser should quit
*/
async parseCommonMpegHeader() {
if (this.frameCount === 0) {
this.mpegOffset = this.tokenizer.position - 1;
}
await this.tokenizer.peekBuffer(this.buf_frame_header, { offset: 1, length: 3 });
let header;
try {
header = FrameHeader.get(this.buf_frame_header, 0);
}
catch (err) {
await this.tokenizer.ignore(1);
this.metadata.addWarning('Parse error: ' + err.message);
return false; // sync
}
await this.tokenizer.ignore(3);
this.metadata.setFormat('container', header.container);
this.metadata.setFormat('codec', header.codec);
this.metadata.setFormat('lossless', false);
this.metadata.setFormat('sampleRate', header.samplingRate);
this.frameCount++;
if (header.version >= 2 && header.layer === 0) {
return this.parseAdts(header); // ADTS, usually AAC
}
else {
return this.parseAudioFrameHeader(header); // MP3
}
}
/**
* @return {Promise<boolean>} true if parser should quit
*/
async parseAudioFrameHeader(header) {
this.metadata.setFormat('numberOfChannels', header.channelMode === 'mono' ? 1 : 2);
this.metadata.setFormat('bitrate', header.bitrate);
if (this.frameCount < 20 * 10000) {
debug('offset=%s MP%s bitrate=%s sample-rate=%s', this.tokenizer.position - 4, header.layer, header.bitrate, header.samplingRate);
}
const slot_size = header.calcSlotSize();
if (slot_size === null) {
throw new Error('invalid slot_size');
}
const samples_per_frame = header.calcSamplesPerFrame();
debug(`samples_per_frame=${samples_per_frame}`);
const bps = samples_per_frame / 8.0;
const fsize = (bps * header.bitrate / header.samplingRate) +
((header.padding) ? slot_size : 0);
this.frame_size = Math.floor(fsize);
this.audioFrameHeader = header;
this.bitrates.push(header.bitrate);
// xtra header only exists in first frame
if (this.frameCount === 1) {
this.offset = FrameHeader.len;
await this.skipSideInformation();
return false;
}
if (this.frameCount === 3) {
// the stream is CBR if the first 3 frame bitrates are the same
if (this.areAllSame(this.bitrates)) {
// Actual calculation will be done in finalize
this.samplesPerFrame = samples_per_frame;
this.metadata.setFormat('codecProfile', 'CBR');
if (this.tokenizer.fileInfo.size)
return true; // Will calculate duration based on the file size
}
else if (this.metadata.format.duration) {
return true; // We already got the duration, stop processing MPEG stream any further
}
if (!this.options.duration) {
return true; // Enforce duration not enabled, stop processing entire stream
}
}
// once we know the file is VBR attach listener to end of
// stream so we can do the duration calculation when we
// have counted all the frames
if (this.options.duration && this.frameCount === 4) {
this.samplesPerFrame = samples_per_frame;
this.calculateEofDuration = true;
}
this.offset = 4;
if (header.isProtectedByCRC) {
await this.parseCrc();
return false;
}
else {
await this.skipSideInformation();
return false;
}
}
async parseAdts(header) {
const buf = Buffer.alloc(3);
await this.tokenizer.readBuffer(buf);
header.frameLength += common.getBitAllignedNumber(buf, 0, 0, 11);
this.totalDataLength += header.frameLength;
this.samplesPerFrame = 1024;
const framesPerSec = header.samplingRate / this.samplesPerFrame;
const bytesPerFrame = this.frameCount === 0 ? 0 : this.totalDataLength / this.frameCount;
const bitrate = 8 * bytesPerFrame * framesPerSec + 0.5;
this.metadata.setFormat('bitrate', bitrate);
debug(`frame-count=${this.frameCount}, size=${header.frameLength} bytes, bit-rate=${bitrate}`);
await this.tokenizer.ignore(header.frameLength > 7 ? header.frameLength - 7 : 1);
// Consume remaining header and frame data
if (this.frameCount === 3) {
this.metadata.setFormat('codecProfile', header.codecProfile);
if (header.mp4ChannelConfig) {
this.metadata.setFormat('numberOfChannels', header.mp4ChannelConfig.length);
}
if (this.options.duration) {
this.calculateEofDuration = true;
}
else {
return true; // Stop parsing after the third frame
}
}
return false;
}
async parseCrc() {
this.crc = await this.tokenizer.readNumber(Token.INT16_BE);
this.offset += 2;
return this.skipSideInformation();
}
async skipSideInformation() {
const sideinfo_length = this.audioFrameHeader.calculateSideInfoLength();
// side information
await this.tokenizer.readToken(new Token.Uint8ArrayType(sideinfo_length));
this.offset += sideinfo_length;
await this.readXtraInfoHeader();
return;
}
async readXtraInfoHeader() {
const headerTag = await this.tokenizer.readToken(XingTag_1.InfoTagHeaderTag);
this.offset += XingTag_1.InfoTagHeaderTag.len; // 12
switch (headerTag) {
case 'Info':
this.metadata.setFormat('codecProfile', 'CBR');
return this.readXingInfoHeader();
case 'Xing':
const infoTag = await this.readXingInfoHeader();
const codecProfile = getVbrCodecProfile(infoTag.vbrScale);
this.metadata.setFormat('codecProfile', codecProfile);
return null;
case 'Xtra':
// ToDo: ???
break;
case 'LAME':
const version = await this.tokenizer.readToken(XingTag_1.LameEncoderVersion);
if (this.frame_size >= this.offset + XingTag_1.LameEncoderVersion.len) {
this.offset += XingTag_1.LameEncoderVersion.len;
this.metadata.setFormat('tool', 'LAME ' + version);
await this.skipFrameData(this.frame_size - this.offset);
return null;
}
else {
this.metadata.addWarning('Corrupt LAME header');
break;
}
// ToDo: ???
}
// ToDo: promise duration???
const frameDataLeft = this.frame_size - this.offset;
if (frameDataLeft < 0) {
this.metadata.addWarning('Frame ' + this.frameCount + 'corrupt: negative frameDataLeft');
}
else {
await this.skipFrameData(frameDataLeft);
}
return null;
}
/**
* Ref: http://gabriel.mp3-tech.org/mp3infotag.html
* @returns {Promise<string>}
*/
async readXingInfoHeader() {
const _offset = this.tokenizer.position;
const infoTag = await (0, XingTag_1.readXingHeader)(this.tokenizer);
this.offset += this.tokenizer.position - _offset;
if (infoTag.lame) {
this.metadata.setFormat('tool', 'LAME ' + common.stripNulls(infoTag.lame.version));
if (infoTag.lame.extended) {
// this.metadata.setFormat('trackGain', infoTag.lame.extended.track_gain);
this.metadata.setFormat('trackPeakLevel', infoTag.lame.extended.track_peak);
if (infoTag.lame.extended.track_gain) {
this.metadata.setFormat('trackGain', infoTag.lame.extended.track_gain.adjustment);
}
if (infoTag.lame.extended.album_gain) {
this.metadata.setFormat('albumGain', infoTag.lame.extended.album_gain.adjustment);
}
this.metadata.setFormat('duration', infoTag.lame.extended.music_length / 1000);
}
}
if (infoTag.streamSize) {
const duration = this.audioFrameHeader.calcDuration(infoTag.numFrames);
this.metadata.setFormat('duration', duration);
debug('Get duration from Xing header: %s', this.metadata.format.duration);
return infoTag;
}
// frames field is not present
const frameDataLeft = this.frame_size - this.offset;
await this.skipFrameData(frameDataLeft);
return infoTag;
}
async skipFrameData(frameDataLeft) {
if (frameDataLeft < 0)
throw new Error('frame-data-left cannot be negative');
await this.tokenizer.ignore(frameDataLeft);
this.countSkipFrameData += frameDataLeft;
}
areAllSame(array) {
const first = array[0];
return array.every(element => {
return element === first;
});
}
}
exports.MpegParser = MpegParser;
+55
View File
@@ -0,0 +1,55 @@
import { IGetToken } from 'strtok3/lib/core';
export interface IReplayGain {
type: NameCode;
origin: ReplayGainOriginator;
adjustment: number;
}
/**
* https://github.com/Borewit/music-metadata/wiki/Replay-Gain-Data-Format#name-code
*/
declare enum NameCode {
/**
* not set
*/
not_set = 0,
/**
* Radio Gain Adjustment
*/
radio = 1,
/**
* Audiophile Gain Adjustment
*/
audiophile = 2
}
/**
* https://github.com/Borewit/music-metadata/wiki/Replay-Gain-Data-Format#originator-code
*/
declare enum ReplayGainOriginator {
/**
* Replay Gain unspecified
*/
unspecified = 0,
/**
* Replay Gain pre-set by artist/producer/mastering engineer
*/
engineer = 1,
/**
* Replay Gain set by user
*/
user = 2,
/**
* Replay Gain determined automatically, as described on this site
*/
automatic = 3,
/**
* Set by simple RMS average
*/
rms_average = 4
}
/**
* Replay Gain Data Format
*
* https://github.com/Borewit/music-metadata/wiki/Replay-Gain-Data-Format
*/
export declare const ReplayGain: IGetToken<IReplayGain>;
export {};
+69
View File
@@ -0,0 +1,69 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReplayGain = void 0;
const common = require("../common/Util");
/**
* https://github.com/Borewit/music-metadata/wiki/Replay-Gain-Data-Format#name-code
*/
var NameCode;
(function (NameCode) {
/**
* not set
*/
NameCode[NameCode["not_set"] = 0] = "not_set";
/**
* Radio Gain Adjustment
*/
NameCode[NameCode["radio"] = 1] = "radio";
/**
* Audiophile Gain Adjustment
*/
NameCode[NameCode["audiophile"] = 2] = "audiophile";
})(NameCode || (NameCode = {}));
/**
* https://github.com/Borewit/music-metadata/wiki/Replay-Gain-Data-Format#originator-code
*/
var ReplayGainOriginator;
(function (ReplayGainOriginator) {
/**
* Replay Gain unspecified
*/
ReplayGainOriginator[ReplayGainOriginator["unspecified"] = 0] = "unspecified";
/**
* Replay Gain pre-set by artist/producer/mastering engineer
*/
ReplayGainOriginator[ReplayGainOriginator["engineer"] = 1] = "engineer";
/**
* Replay Gain set by user
*/
ReplayGainOriginator[ReplayGainOriginator["user"] = 2] = "user";
/**
* Replay Gain determined automatically, as described on this site
*/
ReplayGainOriginator[ReplayGainOriginator["automatic"] = 3] = "automatic";
/**
* Set by simple RMS average
*/
ReplayGainOriginator[ReplayGainOriginator["rms_average"] = 4] = "rms_average";
})(ReplayGainOriginator || (ReplayGainOriginator = {}));
/**
* Replay Gain Data Format
*
* https://github.com/Borewit/music-metadata/wiki/Replay-Gain-Data-Format
*/
exports.ReplayGain = {
len: 2,
get: (buf, off) => {
const gain_type = common.getBitAllignedNumber(buf, off, 0, 3);
const sign = common.getBitAllignedNumber(buf, off, 6, 1);
const gain_adj = common.getBitAllignedNumber(buf, off, 7, 9) / 10.0;
if (gain_type > 0) {
return {
type: common.getBitAllignedNumber(buf, off, 0, 3),
origin: common.getBitAllignedNumber(buf, off, 3, 3),
adjustment: (sign ? -gain_adj : gain_adj)
};
}
return undefined;
}
};
+45
View File
@@ -0,0 +1,45 @@
/// <reference types="node" />
import * as Token from "token-types";
import { IGetToken, ITokenizer } from 'strtok3/lib/core';
import { IExtendedLameHeader } from './ExtendedLameHeader';
export interface IXingHeaderFlags {
frames: boolean;
bytes: boolean;
toc: boolean;
vbrScale: boolean;
}
/**
* Info Tag: Xing, LAME
*/
export declare const InfoTagHeaderTag: Token.StringType;
/**
* LAME TAG value
* Did not find any official documentation for this
* Value e.g.: "3.98.4"
*/
export declare const LameEncoderVersion: Token.StringType;
export interface IXingInfoTag {
/**
* total bit stream frames from Vbr header data
*/
numFrames?: number;
/**
* Actual stream size = file size - header(s) size [bytes]
*/
streamSize?: number;
toc?: Buffer;
/**
* the number of header data bytes (from original file)
*/
vbrScale?: number;
lame?: {
version: string;
extended?: IExtendedLameHeader;
};
}
/**
* Info Tag
* Ref: http://gabriel.mp3-tech.org/mp3infotag.html
*/
export declare const XingHeaderFlags: IGetToken<IXingHeaderFlags>;
export declare function readXingHeader(tokenizer: ITokenizer): Promise<IXingInfoTag>;
+69
View File
@@ -0,0 +1,69 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.readXingHeader = exports.XingHeaderFlags = exports.LameEncoderVersion = exports.InfoTagHeaderTag = void 0;
const Token = require("token-types");
const util = require("../common/Util");
const ExtendedLameHeader_1 = require("./ExtendedLameHeader");
/**
* Info Tag: Xing, LAME
*/
exports.InfoTagHeaderTag = new Token.StringType(4, 'ascii');
/**
* LAME TAG value
* Did not find any official documentation for this
* Value e.g.: "3.98.4"
*/
exports.LameEncoderVersion = new Token.StringType(6, 'ascii');
/**
* Info Tag
* Ref: http://gabriel.mp3-tech.org/mp3infotag.html
*/
exports.XingHeaderFlags = {
len: 4,
get: (buf, off) => {
return {
frames: util.isBitSet(buf, off, 31),
bytes: util.isBitSet(buf, off, 30),
toc: util.isBitSet(buf, off, 29),
vbrScale: util.isBitSet(buf, off, 28)
};
}
};
// /**
// * XING Header Tag
// * Ref: http://gabriel.mp3-tech.org/mp3infotag.html
// */
async function readXingHeader(tokenizer) {
const flags = await tokenizer.readToken(exports.XingHeaderFlags);
const xingInfoTag = {};
if (flags.frames) {
xingInfoTag.numFrames = await tokenizer.readToken(Token.UINT32_BE);
}
if (flags.bytes) {
xingInfoTag.streamSize = await tokenizer.readToken(Token.UINT32_BE);
}
if (flags.toc) {
xingInfoTag.toc = Buffer.alloc(100);
await tokenizer.readBuffer(xingInfoTag.toc);
}
if (flags.vbrScale) {
xingInfoTag.vbrScale = await tokenizer.readToken(Token.UINT32_BE);
}
const lameTag = await tokenizer.peekToken(new Token.StringType(4, 'ascii'));
if (lameTag === 'LAME') {
await tokenizer.ignore(4);
xingInfoTag.lame = {
version: await tokenizer.readToken(new Token.StringType(5, 'ascii'))
};
const match = xingInfoTag.lame.version.match(/\d+.\d+/g);
if (match) {
const majorMinorVersion = xingInfoTag.lame.version.match(/\d+.\d+/g)[0]; // e.g. 3.97
const version = majorMinorVersion.split('.').map(n => parseInt(n, 10));
if (version[0] >= 3 && version[1] >= 90) {
xingInfoTag.lame.extended = await tokenizer.readToken(ExtendedLameHeader_1.ExtendedLameHeader);
}
}
}
return xingInfoTag;
}
exports.readXingHeader = readXingHeader;