Initial working version
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
import { BasicParser } from '../common/BasicParser';
|
||||
/**
|
||||
* WavPack Parser
|
||||
*/
|
||||
export declare class WavPackParser extends BasicParser {
|
||||
private audioDataSize;
|
||||
parse(): Promise<void>;
|
||||
parseWavPackBlocks(): Promise<void>;
|
||||
/**
|
||||
* Ref: http://www.wavpack.com/WavPack5FileFormat.pdf, 3.0 Metadata Sub-blocks
|
||||
* @param remainingLength
|
||||
*/
|
||||
private parseMetadataSubBlock;
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.WavPackParser = void 0;
|
||||
const Token = require("token-types");
|
||||
const APEv2Parser_1 = require("../apev2/APEv2Parser");
|
||||
const FourCC_1 = require("../common/FourCC");
|
||||
const BasicParser_1 = require("../common/BasicParser");
|
||||
const WavPackToken_1 = require("./WavPackToken");
|
||||
const initDebug = require("debug");
|
||||
const debug = initDebug('music-metadata:parser:WavPack');
|
||||
/**
|
||||
* WavPack Parser
|
||||
*/
|
||||
class WavPackParser extends BasicParser_1.BasicParser {
|
||||
async parse() {
|
||||
this.audioDataSize = 0;
|
||||
// First parse all WavPack blocks
|
||||
await this.parseWavPackBlocks();
|
||||
// try to parse APEv2 header
|
||||
return APEv2Parser_1.APEv2Parser.tryParseApeHeader(this.metadata, this.tokenizer, this.options);
|
||||
}
|
||||
async parseWavPackBlocks() {
|
||||
do {
|
||||
const blockId = await this.tokenizer.peekToken(FourCC_1.FourCcToken);
|
||||
if (blockId !== 'wvpk')
|
||||
break;
|
||||
const header = await this.tokenizer.readToken(WavPackToken_1.WavPack.BlockHeaderToken);
|
||||
if (header.BlockID !== 'wvpk')
|
||||
throw new Error('Invalid WavPack Block-ID');
|
||||
debug(`WavPack header blockIndex=${header.blockIndex}, len=${WavPackToken_1.WavPack.BlockHeaderToken.len}`);
|
||||
if (header.blockIndex === 0 && !this.metadata.format.container) {
|
||||
this.metadata.setFormat('container', 'WavPack');
|
||||
this.metadata.setFormat('lossless', !header.flags.isHybrid);
|
||||
// tagTypes: this.type,
|
||||
this.metadata.setFormat('bitsPerSample', header.flags.bitsPerSample);
|
||||
if (!header.flags.isDSD) {
|
||||
// In case isDSD, these values will ne set in ID_DSD_BLOCK
|
||||
this.metadata.setFormat('sampleRate', header.flags.samplingRate);
|
||||
this.metadata.setFormat('duration', header.totalSamples / header.flags.samplingRate);
|
||||
}
|
||||
this.metadata.setFormat('numberOfChannels', header.flags.isMono ? 1 : 2);
|
||||
this.metadata.setFormat('numberOfSamples', header.totalSamples);
|
||||
this.metadata.setFormat('codec', header.flags.isDSD ? 'DSD' : 'PCM');
|
||||
}
|
||||
const ignoreBytes = header.blockSize - (WavPackToken_1.WavPack.BlockHeaderToken.len - 8);
|
||||
if (header.blockIndex === 0) {
|
||||
// Meta-data block
|
||||
await this.parseMetadataSubBlock(header, ignoreBytes);
|
||||
}
|
||||
else {
|
||||
await this.tokenizer.ignore(ignoreBytes);
|
||||
}
|
||||
if (header.blockSamples > 0) {
|
||||
this.audioDataSize += header.blockSize; // Count audio data for bit-rate calculation
|
||||
}
|
||||
} while (!this.tokenizer.fileInfo.size || this.tokenizer.fileInfo.size - this.tokenizer.position >= WavPackToken_1.WavPack.BlockHeaderToken.len);
|
||||
this.metadata.setFormat('bitrate', this.audioDataSize * 8 / this.metadata.format.duration);
|
||||
}
|
||||
/**
|
||||
* Ref: http://www.wavpack.com/WavPack5FileFormat.pdf, 3.0 Metadata Sub-blocks
|
||||
* @param remainingLength
|
||||
*/
|
||||
async parseMetadataSubBlock(header, remainingLength) {
|
||||
while (remainingLength > WavPackToken_1.WavPack.MetadataIdToken.len) {
|
||||
const id = await this.tokenizer.readToken(WavPackToken_1.WavPack.MetadataIdToken);
|
||||
const dataSizeInWords = await this.tokenizer.readNumber(id.largeBlock ? Token.UINT24_LE : Token.UINT8);
|
||||
const data = Buffer.alloc(dataSizeInWords * 2 - (id.isOddSize ? 1 : 0));
|
||||
await this.tokenizer.readBuffer(data);
|
||||
debug(`Metadata Sub-Blocks functionId=0x${id.functionId.toString(16)}, id.largeBlock=${id.largeBlock},data-size=${data.length}`);
|
||||
switch (id.functionId) {
|
||||
case 0x0: // ID_DUMMY: could be used to pad WavPack blocks
|
||||
break;
|
||||
case 0xe: // ID_DSD_BLOCK
|
||||
debug('ID_DSD_BLOCK');
|
||||
// https://github.com/dbry/WavPack/issues/71#issuecomment-483094813
|
||||
const mp = 1 << data.readUInt8(0);
|
||||
const samplingRate = header.flags.samplingRate * mp * 8; // ToDo: second factor should be read from DSD-metadata block https://github.com/dbry/WavPack/issues/71#issuecomment-483094813
|
||||
if (!header.flags.isDSD)
|
||||
throw new Error('Only expect DSD block if DSD-flag is set');
|
||||
this.metadata.setFormat('sampleRate', samplingRate);
|
||||
this.metadata.setFormat('duration', header.totalSamples / samplingRate);
|
||||
break;
|
||||
case 0x24: // ID_ALT_TRAILER: maybe used to embed original ID3 tag header
|
||||
debug('ID_ALT_TRAILER: trailer for non-wav files');
|
||||
break;
|
||||
case 0x26: // ID_MD5_CHECKSUM
|
||||
this.metadata.setFormat('audioMD5', data);
|
||||
break;
|
||||
case 0x2f: // ID_BLOCK_CHECKSUM
|
||||
debug(`ID_BLOCK_CHECKSUM: checksum=${data.toString('hex')}`);
|
||||
break;
|
||||
default:
|
||||
debug(`Ignore unsupported meta-sub-block-id functionId=0x${id.functionId.toString(16)}`);
|
||||
break;
|
||||
}
|
||||
remainingLength -= WavPackToken_1.WavPack.MetadataIdToken.len + (id.largeBlock ? Token.UINT24_LE.len : Token.UINT8.len) + dataSizeInWords * 2;
|
||||
debug(`remainingLength=${remainingLength}`);
|
||||
if (id.isOddSize)
|
||||
this.tokenizer.ignore(1);
|
||||
}
|
||||
if (remainingLength !== 0)
|
||||
throw new Error('metadata-sub-block should fit it remaining length');
|
||||
}
|
||||
}
|
||||
exports.WavPackParser = WavPackParser;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { IGetToken } from 'strtok3/lib/core';
|
||||
/**
|
||||
* WavPack Block Header
|
||||
*
|
||||
* 32-byte little-endian header at the front of every WavPack block
|
||||
*
|
||||
* Ref:
|
||||
* http://www.wavpack.com/WavPack5FileFormat.pdf (page 2/6: 2.0 "Block Header")
|
||||
*/
|
||||
export interface IBlockHeader {
|
||||
BlockID: string;
|
||||
blockSize: number;
|
||||
version: number;
|
||||
blockIndex: number;
|
||||
totalSamples: number;
|
||||
blockSamples: number;
|
||||
flags: {
|
||||
bitsPerSample: number;
|
||||
isMono: boolean;
|
||||
isHybrid: boolean;
|
||||
isJointStereo: boolean;
|
||||
crossChannel: boolean;
|
||||
hybridNoiseShaping: boolean;
|
||||
floatingPoint: boolean;
|
||||
samplingRate: number;
|
||||
isDSD: boolean;
|
||||
};
|
||||
crc: Uint8Array;
|
||||
}
|
||||
export interface IMetadataId {
|
||||
/**
|
||||
* metadata function id
|
||||
*/
|
||||
functionId: number;
|
||||
/**
|
||||
* If true, audio-decoder does not need to understand the metadata field
|
||||
*/
|
||||
isOptional: boolean;
|
||||
/**
|
||||
* actual data byte length is 1 less
|
||||
*/
|
||||
isOddSize: boolean;
|
||||
/**
|
||||
* large block (> 255 words)
|
||||
*/
|
||||
largeBlock: boolean;
|
||||
}
|
||||
export declare class WavPack {
|
||||
/**
|
||||
* WavPack Block Header
|
||||
*
|
||||
* 32-byte little-endian header at the front of every WavPack block
|
||||
*
|
||||
* Ref: http://www.wavpack.com/WavPack5FileFormat.pdf (page 2/6: 2.0 "Block Header")
|
||||
*/
|
||||
static BlockHeaderToken: IGetToken<IBlockHeader>;
|
||||
/**
|
||||
* 3.0 Metadata Sub-Blocks
|
||||
* Ref: http://www.wavpack.com/WavPack5FileFormat.pdf (page 4/6: 3.0 "Metadata Sub-Block")
|
||||
*/
|
||||
static MetadataIdToken: IGetToken<IMetadataId>;
|
||||
private static isBitSet;
|
||||
private static getBitAllignedNumber;
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.WavPack = void 0;
|
||||
const Token = require("token-types");
|
||||
const FourCC_1 = require("../common/FourCC");
|
||||
const SampleRates = [6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100,
|
||||
48000, 64000, 88200, 96000, 192000, -1];
|
||||
class WavPack {
|
||||
static isBitSet(flags, bitOffset) {
|
||||
return WavPack.getBitAllignedNumber(flags, bitOffset, 1) === 1;
|
||||
}
|
||||
static getBitAllignedNumber(flags, bitOffset, len) {
|
||||
return (flags >>> bitOffset) & (0xffffffff >>> (32 - len));
|
||||
}
|
||||
}
|
||||
exports.WavPack = WavPack;
|
||||
/**
|
||||
* WavPack Block Header
|
||||
*
|
||||
* 32-byte little-endian header at the front of every WavPack block
|
||||
*
|
||||
* Ref: http://www.wavpack.com/WavPack5FileFormat.pdf (page 2/6: 2.0 "Block Header")
|
||||
*/
|
||||
WavPack.BlockHeaderToken = {
|
||||
len: 32,
|
||||
get: (buf, off) => {
|
||||
const flags = Token.UINT32_LE.get(buf, off + 24);
|
||||
const res = {
|
||||
// should equal 'wvpk'
|
||||
BlockID: FourCC_1.FourCcToken.get(buf, off),
|
||||
// 0x402 to 0x410 are valid for decode
|
||||
blockSize: Token.UINT32_LE.get(buf, off + 4),
|
||||
// 0x402 (1026) to 0x410 are valid for decode
|
||||
version: Token.UINT16_LE.get(buf, off + 8),
|
||||
// 40-bit total samples for entire file (if block_index == 0 and a value of -1 indicates an unknown length)
|
||||
totalSamples: /* replace with bigint? (Token.UINT8.get(buf, off + 11) << 32) + */ Token.UINT32_LE.get(buf, off + 12),
|
||||
// 40-bit block_index
|
||||
blockIndex: /* replace with bigint? (Token.UINT8.get(buf, off + 10) << 32) + */ Token.UINT32_LE.get(buf, off + 16),
|
||||
// 40-bit total samples for entire file (if block_index == 0 and a value of -1 indicates an unknown length)
|
||||
blockSamples: Token.UINT32_LE.get(buf, off + 20),
|
||||
// various flags for id and decoding
|
||||
flags: {
|
||||
bitsPerSample: (1 + WavPack.getBitAllignedNumber(flags, 0, 2)) * 8,
|
||||
isMono: WavPack.isBitSet(flags, 2),
|
||||
isHybrid: WavPack.isBitSet(flags, 3),
|
||||
isJointStereo: WavPack.isBitSet(flags, 4),
|
||||
crossChannel: WavPack.isBitSet(flags, 5),
|
||||
hybridNoiseShaping: WavPack.isBitSet(flags, 6),
|
||||
floatingPoint: WavPack.isBitSet(flags, 7),
|
||||
samplingRate: SampleRates[WavPack.getBitAllignedNumber(flags, 23, 4)],
|
||||
isDSD: WavPack.isBitSet(flags, 31)
|
||||
},
|
||||
// crc for actual decoded data
|
||||
crc: new Token.Uint8ArrayType(4).get(buf, off + 28)
|
||||
};
|
||||
if (res.flags.isDSD) {
|
||||
res.totalSamples *= 8;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 3.0 Metadata Sub-Blocks
|
||||
* Ref: http://www.wavpack.com/WavPack5FileFormat.pdf (page 4/6: 3.0 "Metadata Sub-Block")
|
||||
*/
|
||||
WavPack.MetadataIdToken = {
|
||||
len: 1,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
functionId: WavPack.getBitAllignedNumber(buf[off], 0, 6),
|
||||
isOptional: WavPack.isBitSet(buf[off], 5),
|
||||
isOddSize: WavPack.isBitSet(buf[off], 6),
|
||||
largeBlock: WavPack.isBitSet(buf[off], 7)
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user