UNPKG

music-metadata

Version:

Music metadata parser for Node.js, supporting virtual any audio and tag format.

109 lines 5.9 kB
import * as Token from 'token-types'; import { APEv2Parser } from '../apev2/APEv2Parser.js'; import { FourCcToken } from '../common/FourCC.js'; import { BasicParser } from '../common/BasicParser.js'; import { BlockHeaderToken, MetadataIdToken } from './WavPackToken.js'; import initDebug from 'debug'; import { uint8ArrayToHex } from 'uint8array-extras'; import { makeUnexpectedFileContentError } from '../ParseError.js'; const debug = initDebug('music-metadata:parser:WavPack'); export class WavPackContentError extends makeUnexpectedFileContentError('WavPack') { } /** * WavPack Parser */ export class WavPackParser extends BasicParser { constructor() { super(...arguments); this.audioDataSize = 0; } async parse() { this.audioDataSize = 0; // First parse all WavPack blocks await this.parseWavPackBlocks(); // try to parse APEv2 header return APEv2Parser.tryParseApeHeader(this.metadata, this.tokenizer, this.options); } async parseWavPackBlocks() { do { const blockId = await this.tokenizer.peekToken(FourCcToken); if (blockId !== 'wvpk') break; const header = await this.tokenizer.readToken(BlockHeaderToken); if (header.BlockID !== 'wvpk') throw new WavPackContentError('Invalid WavPack Block-ID'); debug(`WavPack header blockIndex=${header.blockIndex}, len=${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 - (BlockHeaderToken.len - 8); await (header.blockIndex === 0 ? this.parseMetadataSubBlock(header, ignoreBytes) : 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 >= BlockHeaderToken.len); if (this.metadata.format.duration) { this.metadata.setFormat('bitrate', this.audioDataSize * 8 / this.metadata.format.duration); } } /** * Ref: http://www.wavpack.com/WavPack5FileFormat.pdf, 3.0 Metadata Sub-blocks * @param header Header * @param remainingLength Remaining length */ async parseMetadataSubBlock(header, remainingLength) { let remaining = remainingLength; while (remaining > MetadataIdToken.len) { const id = await this.tokenizer.readToken(MetadataIdToken); const dataSizeInWords = await this.tokenizer.readNumber(id.largeBlock ? Token.UINT24_LE : Token.UINT8); const data = new Uint8Array(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 << Token.UINT8.get(data, 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 WavPackContentError('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=${uint8ArrayToHex(data)}`); break; default: debug(`Ignore unsupported meta-sub-block-id functionId=0x${id.functionId.toString(16)}`); break; } remaining -= MetadataIdToken.len + (id.largeBlock ? Token.UINT24_LE.len : Token.UINT8.len) + dataSizeInWords * 2; debug(`remainingLength=${remaining}`); if (id.isOddSize) this.tokenizer.ignore(1); } if (remaining !== 0) throw new WavPackContentError('metadata-sub-block should fit it remaining length'); } } //# sourceMappingURL=WavPackParser.js.map