Initial working version
This commit is contained in:
+5
@@ -0,0 +1,5 @@
|
||||
import { AbstractID3Parser } from '../id3v2/AbstractID3Parser';
|
||||
declare class MusepackParser extends AbstractID3Parser {
|
||||
_parse(): Promise<void>;
|
||||
}
|
||||
export default MusepackParser;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const initDebug = require("debug");
|
||||
const Token = require("token-types");
|
||||
const MpcSv8Parser_1 = require("./sv8/MpcSv8Parser");
|
||||
const MpcSv7Parser_1 = require("./sv7/MpcSv7Parser");
|
||||
const AbstractID3Parser_1 = require("../id3v2/AbstractID3Parser");
|
||||
const debug = initDebug('music-metadata:parser:musepack');
|
||||
class MusepackParser extends AbstractID3Parser_1.AbstractID3Parser {
|
||||
async _parse() {
|
||||
const signature = await this.tokenizer.peekToken(new Token.StringType(3, 'binary'));
|
||||
let mpcParser;
|
||||
switch (signature) {
|
||||
case 'MP+': {
|
||||
debug('Musepack stream-version 7');
|
||||
mpcParser = new MpcSv7Parser_1.MpcSv7Parser();
|
||||
break;
|
||||
}
|
||||
case 'MPC': {
|
||||
debug('Musepack stream-version 8');
|
||||
mpcParser = new MpcSv8Parser_1.MpcSv8Parser();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error('Invalid Musepack signature prefix');
|
||||
}
|
||||
}
|
||||
mpcParser.init(this.metadata, this.tokenizer, this.options);
|
||||
return mpcParser.parse();
|
||||
}
|
||||
}
|
||||
exports.default = MusepackParser;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { ITokenizer } from 'strtok3/lib/core';
|
||||
export declare class BitReader {
|
||||
private tokenizer;
|
||||
pos: number;
|
||||
private dword;
|
||||
constructor(tokenizer: ITokenizer);
|
||||
/**
|
||||
*
|
||||
* @param bits 1..30 bits
|
||||
*/
|
||||
read(bits: number): Promise<number>;
|
||||
ignore(bits: number): Promise<number>;
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.BitReader = void 0;
|
||||
const Token = require("token-types");
|
||||
class BitReader {
|
||||
constructor(tokenizer) {
|
||||
this.tokenizer = tokenizer;
|
||||
this.pos = 0;
|
||||
this.dword = undefined;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param bits 1..30 bits
|
||||
*/
|
||||
async read(bits) {
|
||||
while (this.dword === undefined) {
|
||||
this.dword = await this.tokenizer.readToken(Token.UINT32_LE);
|
||||
}
|
||||
let out = this.dword;
|
||||
this.pos += bits;
|
||||
if (this.pos < 32) {
|
||||
out >>>= (32 - this.pos);
|
||||
return out & ((1 << bits) - 1);
|
||||
}
|
||||
else {
|
||||
this.pos -= 32;
|
||||
if (this.pos === 0) {
|
||||
this.dword = undefined;
|
||||
return out & ((1 << bits) - 1);
|
||||
}
|
||||
else {
|
||||
this.dword = await this.tokenizer.readToken(Token.UINT32_LE);
|
||||
if (this.pos) {
|
||||
out <<= this.pos;
|
||||
out |= this.dword >>> (32 - this.pos);
|
||||
}
|
||||
return out & ((1 << bits) - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
async ignore(bits) {
|
||||
if (this.pos > 0) {
|
||||
const remaining = 32 - this.pos;
|
||||
this.dword = undefined;
|
||||
bits -= remaining;
|
||||
this.pos = 0;
|
||||
}
|
||||
const remainder = bits % 32;
|
||||
const numOfWords = (bits - remainder) / 32;
|
||||
await this.tokenizer.ignore(numOfWords * 4);
|
||||
return this.read(remainder);
|
||||
}
|
||||
}
|
||||
exports.BitReader = BitReader;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { BasicParser } from '../../common/BasicParser';
|
||||
export declare class MpcSv7Parser extends BasicParser {
|
||||
private bitreader;
|
||||
private audioLength;
|
||||
private duration;
|
||||
parse(): Promise<void>;
|
||||
private skipAudioData;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MpcSv7Parser = void 0;
|
||||
const initDebug = require("debug");
|
||||
const BasicParser_1 = require("../../common/BasicParser");
|
||||
const SV7 = require("./StreamVersion7");
|
||||
const APEv2Parser_1 = require("../../apev2/APEv2Parser");
|
||||
const BitReader_1 = require("./BitReader");
|
||||
const debug = initDebug('music-metadata:parser:musepack');
|
||||
class MpcSv7Parser extends BasicParser_1.BasicParser {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.audioLength = 0;
|
||||
}
|
||||
async parse() {
|
||||
const header = await this.tokenizer.readToken(SV7.Header);
|
||||
if (header.signature !== 'MP+')
|
||||
throw new Error('Unexpected magic number');
|
||||
debug(`stream-version=${header.streamMajorVersion}.${header.streamMinorVersion}`);
|
||||
this.metadata.setFormat('container', 'Musepack, SV7');
|
||||
this.metadata.setFormat('sampleRate', header.sampleFrequency);
|
||||
const numberOfSamples = 1152 * (header.frameCount - 1) + header.lastFrameLength;
|
||||
this.metadata.setFormat('numberOfSamples', numberOfSamples);
|
||||
this.duration = numberOfSamples / header.sampleFrequency;
|
||||
this.metadata.setFormat('duration', this.duration);
|
||||
this.bitreader = new BitReader_1.BitReader(this.tokenizer);
|
||||
this.metadata.setFormat('numberOfChannels', header.midSideStereo || header.intensityStereo ? 2 : 1);
|
||||
const version = await this.bitreader.read(8);
|
||||
this.metadata.setFormat('codec', (version / 100).toFixed(2));
|
||||
await this.skipAudioData(header.frameCount);
|
||||
debug(`End of audio stream, switching to APEv2, offset=${this.tokenizer.position}`);
|
||||
return APEv2Parser_1.APEv2Parser.tryParseApeHeader(this.metadata, this.tokenizer, this.options);
|
||||
}
|
||||
async skipAudioData(frameCount) {
|
||||
while (frameCount-- > 0) {
|
||||
const frameLength = await this.bitreader.read(20);
|
||||
this.audioLength += 20 + frameLength;
|
||||
await this.bitreader.ignore(frameLength);
|
||||
}
|
||||
// last frame
|
||||
const lastFrameLength = await this.bitreader.read(11);
|
||||
this.audioLength += lastFrameLength;
|
||||
this.metadata.setFormat('bitrate', this.audioLength / this.duration);
|
||||
}
|
||||
}
|
||||
exports.MpcSv7Parser = MpcSv7Parser;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { IGetToken } from 'strtok3/lib/core';
|
||||
/**
|
||||
* MusePack stream version 7 format specification
|
||||
* http://trac.musepack.net/musepack/wiki/SV7Specification
|
||||
*/
|
||||
export interface IHeader {
|
||||
signature: string;
|
||||
streamMinorVersion: number;
|
||||
streamMajorVersion: number;
|
||||
frameCount: number;
|
||||
intensityStereo: boolean;
|
||||
midSideStereo: boolean;
|
||||
maxBand: number;
|
||||
profile: number;
|
||||
link: number;
|
||||
sampleFrequency: number;
|
||||
maxLevel: number;
|
||||
titleGain: number;
|
||||
titlePeak: number;
|
||||
albumGain: number;
|
||||
albumPeak: number;
|
||||
trueGapless: boolean;
|
||||
lastFrameLength: number;
|
||||
}
|
||||
/**
|
||||
* BASIC STRUCTURE
|
||||
*/
|
||||
export declare const Header: IGetToken<IHeader>;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Header = void 0;
|
||||
const Token = require("token-types");
|
||||
const util = require("../../common/Util");
|
||||
/**
|
||||
* BASIC STRUCTURE
|
||||
*/
|
||||
exports.Header = {
|
||||
len: 6 * 4,
|
||||
get: (buf, off) => {
|
||||
const header = {
|
||||
// word 0
|
||||
signature: Buffer.from(buf).toString('latin1', off, off + 3),
|
||||
// versionIndex number * 1000 (3.81 = 3810) (remember that 4-byte alignment causes this to take 4-bytes)
|
||||
streamMinorVersion: util.getBitAllignedNumber(buf, off + 3, 0, 4),
|
||||
streamMajorVersion: util.getBitAllignedNumber(buf, off + 3, 4, 4),
|
||||
// word 1
|
||||
frameCount: Token.UINT32_LE.get(buf, off + 4),
|
||||
// word 2
|
||||
maxLevel: Token.UINT16_LE.get(buf, off + 8),
|
||||
sampleFrequency: [44100, 48000, 37800, 32000][util.getBitAllignedNumber(buf, off + 10, 0, 2)],
|
||||
link: util.getBitAllignedNumber(buf, off + 10, 2, 2),
|
||||
profile: util.getBitAllignedNumber(buf, off + 10, 4, 4),
|
||||
maxBand: util.getBitAllignedNumber(buf, off + 11, 0, 6),
|
||||
intensityStereo: util.isBitSet(buf, off + 11, 6),
|
||||
midSideStereo: util.isBitSet(buf, off + 11, 7),
|
||||
// word 3
|
||||
titlePeak: Token.UINT16_LE.get(buf, off + 12),
|
||||
titleGain: Token.UINT16_LE.get(buf, off + 14),
|
||||
// word 4
|
||||
albumPeak: Token.UINT16_LE.get(buf, off + 16),
|
||||
albumGain: Token.UINT16_LE.get(buf, off + 18),
|
||||
// word
|
||||
lastFrameLength: (Token.UINT32_LE.get(buf, off + 20) >>> 20) & 0x7FF,
|
||||
trueGapless: util.isBitSet(buf, off + 23, 0)
|
||||
};
|
||||
header.lastFrameLength = header.trueGapless ? (Token.UINT32_LE.get(buf, 20) >>> 20) & 0x7FF : 0;
|
||||
return header;
|
||||
}
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { BasicParser } from '../../common/BasicParser';
|
||||
export declare class MpcSv8Parser extends BasicParser {
|
||||
private audioLength;
|
||||
parse(): Promise<void>;
|
||||
private parsePacket;
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MpcSv8Parser = void 0;
|
||||
const initDebug = require("debug");
|
||||
const BasicParser_1 = require("../../common/BasicParser");
|
||||
const SV8 = require("./StreamVersion8");
|
||||
const APEv2Parser_1 = require("../../apev2/APEv2Parser");
|
||||
const FourCC_1 = require("../../common/FourCC");
|
||||
const debug = initDebug('music-metadata:parser:musepack');
|
||||
class MpcSv8Parser extends BasicParser_1.BasicParser {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.audioLength = 0;
|
||||
}
|
||||
async parse() {
|
||||
const signature = await this.tokenizer.readToken(FourCC_1.FourCcToken);
|
||||
if (signature !== 'MPCK')
|
||||
throw new Error('Invalid Magic number');
|
||||
this.metadata.setFormat('container', 'Musepack, SV8');
|
||||
return this.parsePacket();
|
||||
}
|
||||
async parsePacket() {
|
||||
const sv8reader = new SV8.StreamReader(this.tokenizer);
|
||||
do {
|
||||
const header = await sv8reader.readPacketHeader();
|
||||
debug(`packet-header key=${header.key}, payloadLength=${header.payloadLength}`);
|
||||
switch (header.key) {
|
||||
case 'SH': // Stream Header
|
||||
const sh = await sv8reader.readStreamHeader(header.payloadLength);
|
||||
this.metadata.setFormat('numberOfSamples', sh.sampleCount);
|
||||
this.metadata.setFormat('sampleRate', sh.sampleFrequency);
|
||||
this.metadata.setFormat('duration', sh.sampleCount / sh.sampleFrequency);
|
||||
this.metadata.setFormat('numberOfChannels', sh.channelCount);
|
||||
break;
|
||||
case 'AP': // Audio Packet
|
||||
this.audioLength += header.payloadLength;
|
||||
await this.tokenizer.ignore(header.payloadLength);
|
||||
break;
|
||||
case 'RG': // Replaygain
|
||||
case 'EI': // Encoder Info
|
||||
case 'SO': // Seek Table Offset
|
||||
case 'ST': // Seek Table
|
||||
case 'CT': // Chapter-Tag
|
||||
await this.tokenizer.ignore(header.payloadLength);
|
||||
break;
|
||||
case 'SE': // Stream End
|
||||
this.metadata.setFormat('bitrate', this.audioLength * 8 / this.metadata.format.duration);
|
||||
return APEv2Parser_1.APEv2Parser.tryParseApeHeader(this.metadata, this.tokenizer, this.options);
|
||||
default:
|
||||
throw new Error(`Unexpected header: ${header.key}`);
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
}
|
||||
exports.MpcSv8Parser = MpcSv8Parser;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { ITokenizer } from 'strtok3/lib/core';
|
||||
export interface IPacketHeader {
|
||||
key: string;
|
||||
payloadLength: number;
|
||||
}
|
||||
/**
|
||||
* Stream Header Packet
|
||||
* Ref: http://trac.musepack.net/musepack/wiki/SV8Specification#StreamHeaderPacket
|
||||
*/
|
||||
interface IStreamHeader1 {
|
||||
crc: number;
|
||||
streamVersion: number;
|
||||
}
|
||||
/**
|
||||
* Stream Header Packet
|
||||
* Ref: http://trac.musepack.net/musepack/wiki/SV8Specification#StreamHeaderPacket
|
||||
*/
|
||||
interface IStreamHeader3 {
|
||||
sampleFrequency: number;
|
||||
maxUsedBands: number;
|
||||
channelCount: number;
|
||||
msUsed: boolean;
|
||||
audioBlockFrames: number;
|
||||
}
|
||||
/**
|
||||
* Stream Header Packet
|
||||
* Ref: http://trac.musepack.net/musepack/wiki/SV8Specification#StreamHeaderPacket
|
||||
*/
|
||||
interface IStreamHeader extends IStreamHeader1, IStreamHeader3 {
|
||||
sampleCount: number;
|
||||
beginningOfSilence: number;
|
||||
}
|
||||
export declare class StreamReader {
|
||||
private tokenizer;
|
||||
constructor(tokenizer: ITokenizer);
|
||||
readPacketHeader(): Promise<IPacketHeader>;
|
||||
readStreamHeader(size: number): Promise<IStreamHeader>;
|
||||
private readVariableSizeField;
|
||||
}
|
||||
export {};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.StreamReader = void 0;
|
||||
const Token = require("token-types");
|
||||
const util = require("../../common/Util");
|
||||
const initDebug = require("debug");
|
||||
const debug = initDebug('music-metadata:parser:musepack:sv8');
|
||||
const PacketKey = new Token.StringType(2, 'binary');
|
||||
/**
|
||||
* Stream Header Packet part 1
|
||||
* Ref: http://trac.musepack.net/musepack/wiki/SV8Specification#StreamHeaderPacket
|
||||
*/
|
||||
const SH_part1 = {
|
||||
len: 5,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
crc: Token.UINT32_LE.get(buf, off),
|
||||
streamVersion: Token.UINT8.get(buf, off + 4)
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Stream Header Packet part 3
|
||||
* Ref: http://trac.musepack.net/musepack/wiki/SV8Specification#StreamHeaderPacket
|
||||
*/
|
||||
const SH_part3 = {
|
||||
len: 2,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
sampleFrequency: [44100, 48000, 37800, 32000][util.getBitAllignedNumber(buf, off, 0, 3)],
|
||||
maxUsedBands: util.getBitAllignedNumber(buf, off, 3, 5),
|
||||
channelCount: util.getBitAllignedNumber(buf, off + 1, 0, 4) + 1,
|
||||
msUsed: util.isBitSet(buf, off + 1, 4),
|
||||
audioBlockFrames: util.getBitAllignedNumber(buf, off + 1, 5, 3)
|
||||
};
|
||||
}
|
||||
};
|
||||
class StreamReader {
|
||||
constructor(tokenizer) {
|
||||
this.tokenizer = tokenizer;
|
||||
}
|
||||
async readPacketHeader() {
|
||||
const key = await this.tokenizer.readToken(PacketKey);
|
||||
const size = await this.readVariableSizeField();
|
||||
return {
|
||||
key,
|
||||
payloadLength: size.value - 2 - size.len
|
||||
};
|
||||
}
|
||||
async readStreamHeader(size) {
|
||||
const streamHeader = {};
|
||||
debug(`Reading SH at offset=${this.tokenizer.position}`);
|
||||
const part1 = await this.tokenizer.readToken(SH_part1);
|
||||
size -= SH_part1.len;
|
||||
Object.assign(streamHeader, part1);
|
||||
debug(`SH.streamVersion = ${part1.streamVersion}`);
|
||||
const sampleCount = await this.readVariableSizeField();
|
||||
size -= sampleCount.len;
|
||||
streamHeader.sampleCount = sampleCount.value;
|
||||
const bs = await this.readVariableSizeField();
|
||||
size -= bs.len;
|
||||
streamHeader.beginningOfSilence = bs.value;
|
||||
const part3 = await this.tokenizer.readToken(SH_part3);
|
||||
size -= SH_part3.len;
|
||||
Object.assign(streamHeader, part3);
|
||||
// assert.equal(size, 0);
|
||||
await this.tokenizer.ignore(size);
|
||||
return streamHeader;
|
||||
}
|
||||
async readVariableSizeField(len = 1, hb = 0) {
|
||||
let n = await this.tokenizer.readNumber(Token.UINT8);
|
||||
if ((n & 0x80) === 0) {
|
||||
return { len, value: hb + n };
|
||||
}
|
||||
n &= 0x7F;
|
||||
n += hb;
|
||||
return this.readVariableSizeField(len + 1, n << 7);
|
||||
}
|
||||
}
|
||||
exports.StreamReader = StreamReader;
|
||||
Reference in New Issue
Block a user