Initial working version
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
/// <reference types="node" />
|
||||
import { IChunkHeader } from '../iff';
|
||||
import { IGetToken } from 'strtok3/lib/core';
|
||||
/**
|
||||
* Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/dd317599(v=vs.85).aspx
|
||||
*/
|
||||
export declare enum WaveFormat {
|
||||
PCM = 1,
|
||||
ADPCM = 2,
|
||||
IEEE_FLOAT = 3,
|
||||
MPEG_ADTS_AAC = 5632,
|
||||
MPEG_LOAS = 5634,
|
||||
RAW_AAC1 = 255,
|
||||
DOLBY_AC3_SPDIF = 146,
|
||||
DVM = 8192,
|
||||
RAW_SPORT = 576,
|
||||
ESST_AC3 = 577,
|
||||
DRM = 9,
|
||||
DTS2 = 8193,
|
||||
MPEG = 80
|
||||
}
|
||||
/**
|
||||
* "fmt" sub-chunk describes the sound data's format
|
||||
* Ref: http://soundfile.sapp.org/doc/WaveFormat
|
||||
*/
|
||||
export interface IWaveFormat {
|
||||
/**
|
||||
* PCM = 1 (i.e. Linear quantization). Values other than 1 indicate some form of compression.
|
||||
*/
|
||||
wFormatTag: WaveFormat;
|
||||
/**
|
||||
* Mono = 1, Stereo = 2, etc.
|
||||
*/
|
||||
nChannels: number;
|
||||
/**
|
||||
* 8000, 44100, etc.
|
||||
*/
|
||||
nSamplesPerSec: number;
|
||||
nAvgBytesPerSec: number;
|
||||
nBlockAlign: number;
|
||||
wBitsPerSample: number;
|
||||
}
|
||||
/**
|
||||
* format chunk; chunk-id is "fmt "
|
||||
* http://soundfile.sapp.org/doc/WaveFormat/
|
||||
*/
|
||||
export declare class Format implements IGetToken<IWaveFormat> {
|
||||
len: number;
|
||||
constructor(header: IChunkHeader);
|
||||
get(buf: Buffer, off: number): IWaveFormat;
|
||||
}
|
||||
export interface IFactChunk {
|
||||
dwSampleLength: number;
|
||||
}
|
||||
/**
|
||||
* Fact chunk; chunk-id is "fact"
|
||||
* http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
|
||||
* http://www.recordingblogs.com/wiki/fact-chunk-of-a-wave-file
|
||||
*/
|
||||
export declare class FactChunk implements IGetToken<IFactChunk> {
|
||||
len: number;
|
||||
constructor(header: IChunkHeader);
|
||||
get(buf: Buffer, off: number): IFactChunk;
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.FactChunk = exports.Format = exports.WaveFormat = void 0;
|
||||
/**
|
||||
* Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/dd317599(v=vs.85).aspx
|
||||
*/
|
||||
var WaveFormat;
|
||||
(function (WaveFormat) {
|
||||
WaveFormat[WaveFormat["PCM"] = 1] = "PCM";
|
||||
// MPEG-4 and AAC Audio Types
|
||||
WaveFormat[WaveFormat["ADPCM"] = 2] = "ADPCM";
|
||||
WaveFormat[WaveFormat["IEEE_FLOAT"] = 3] = "IEEE_FLOAT";
|
||||
WaveFormat[WaveFormat["MPEG_ADTS_AAC"] = 5632] = "MPEG_ADTS_AAC";
|
||||
WaveFormat[WaveFormat["MPEG_LOAS"] = 5634] = "MPEG_LOAS";
|
||||
WaveFormat[WaveFormat["RAW_AAC1"] = 255] = "RAW_AAC1";
|
||||
// Dolby Audio Types
|
||||
WaveFormat[WaveFormat["DOLBY_AC3_SPDIF"] = 146] = "DOLBY_AC3_SPDIF";
|
||||
WaveFormat[WaveFormat["DVM"] = 8192] = "DVM";
|
||||
WaveFormat[WaveFormat["RAW_SPORT"] = 576] = "RAW_SPORT";
|
||||
WaveFormat[WaveFormat["ESST_AC3"] = 577] = "ESST_AC3";
|
||||
WaveFormat[WaveFormat["DRM"] = 9] = "DRM";
|
||||
WaveFormat[WaveFormat["DTS2"] = 8193] = "DTS2";
|
||||
WaveFormat[WaveFormat["MPEG"] = 80] = "MPEG";
|
||||
})(WaveFormat = exports.WaveFormat || (exports.WaveFormat = {}));
|
||||
/**
|
||||
* format chunk; chunk-id is "fmt "
|
||||
* http://soundfile.sapp.org/doc/WaveFormat/
|
||||
*/
|
||||
class Format {
|
||||
constructor(header) {
|
||||
if (header.chunkSize < 16)
|
||||
throw new Error('Invalid chunk size');
|
||||
this.len = header.chunkSize;
|
||||
}
|
||||
get(buf, off) {
|
||||
return {
|
||||
wFormatTag: buf.readUInt16LE(off),
|
||||
nChannels: buf.readUInt16LE(off + 2),
|
||||
nSamplesPerSec: buf.readUInt32LE(off + 4),
|
||||
nAvgBytesPerSec: buf.readUInt32LE(off + 8),
|
||||
nBlockAlign: buf.readUInt16LE(off + 12),
|
||||
wBitsPerSample: buf.readUInt16LE(off + 14)
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.Format = Format;
|
||||
/**
|
||||
* Fact chunk; chunk-id is "fact"
|
||||
* http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
|
||||
* http://www.recordingblogs.com/wiki/fact-chunk-of-a-wave-file
|
||||
*/
|
||||
class FactChunk {
|
||||
constructor(header) {
|
||||
if (header.chunkSize < 4) {
|
||||
throw new Error('Invalid fact chunk size.');
|
||||
}
|
||||
this.len = header.chunkSize;
|
||||
}
|
||||
get(buf, off) {
|
||||
return {
|
||||
dwSampleLength: buf.readUInt32LE(off)
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.FactChunk = FactChunk;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import * as riff from '../riff/RiffChunk';
|
||||
import { BasicParser } from '../common/BasicParser';
|
||||
/**
|
||||
* Resource Interchange File Format (RIFF) Parser
|
||||
*
|
||||
* WAVE PCM soundfile format
|
||||
*
|
||||
* Ref:
|
||||
* http://www.johnloomis.org/cpe102/asgn/asgn1/riff.html
|
||||
* http://soundfile.sapp.org/doc/WaveFormat
|
||||
*
|
||||
* ToDo: Split WAVE part from RIFF parser
|
||||
*/
|
||||
export declare class WaveParser extends BasicParser {
|
||||
private fact;
|
||||
private blockAlign;
|
||||
private header;
|
||||
parse(): Promise<void>;
|
||||
parseRiffChunk(chunkSize: number): Promise<void>;
|
||||
readWaveChunk(remaining: number): Promise<void>;
|
||||
parseListTag(listHeader: riff.IChunkHeader): Promise<void>;
|
||||
private parseRiffInfoTags;
|
||||
private addTag;
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.WaveParser = void 0;
|
||||
const strtok3 = require("strtok3/lib/core");
|
||||
const Token = require("token-types");
|
||||
const initDebug = require("debug");
|
||||
const riff = require("../riff/RiffChunk");
|
||||
const WaveChunk = require("./../wav/WaveChunk");
|
||||
const ID3v2Parser_1 = require("../id3v2/ID3v2Parser");
|
||||
const util = require("../common/Util");
|
||||
const FourCC_1 = require("../common/FourCC");
|
||||
const BasicParser_1 = require("../common/BasicParser");
|
||||
const debug = initDebug('music-metadata:parser:RIFF');
|
||||
/**
|
||||
* Resource Interchange File Format (RIFF) Parser
|
||||
*
|
||||
* WAVE PCM soundfile format
|
||||
*
|
||||
* Ref:
|
||||
* http://www.johnloomis.org/cpe102/asgn/asgn1/riff.html
|
||||
* http://soundfile.sapp.org/doc/WaveFormat
|
||||
*
|
||||
* ToDo: Split WAVE part from RIFF parser
|
||||
*/
|
||||
class WaveParser extends BasicParser_1.BasicParser {
|
||||
async parse() {
|
||||
const riffHeader = await this.tokenizer.readToken(riff.Header);
|
||||
debug(`pos=${this.tokenizer.position}, parse: chunkID=${riffHeader.chunkID}`);
|
||||
if (riffHeader.chunkID !== 'RIFF')
|
||||
return; // Not RIFF format
|
||||
return this.parseRiffChunk(riffHeader.chunkSize).catch(err => {
|
||||
if (!(err instanceof strtok3.EndOfStreamError)) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
async parseRiffChunk(chunkSize) {
|
||||
const type = await this.tokenizer.readToken(FourCC_1.FourCcToken);
|
||||
this.metadata.setFormat('container', type);
|
||||
switch (type) {
|
||||
case 'WAVE':
|
||||
return this.readWaveChunk(chunkSize - FourCC_1.FourCcToken.len);
|
||||
default:
|
||||
throw new Error(`Unsupported RIFF format: RIFF/${type}`);
|
||||
}
|
||||
}
|
||||
async readWaveChunk(remaining) {
|
||||
while (remaining >= riff.Header.len) {
|
||||
const header = await this.tokenizer.readToken(riff.Header);
|
||||
remaining -= riff.Header.len + header.chunkSize;
|
||||
if (header.chunkSize > remaining) {
|
||||
this.metadata.addWarning('Data chunk size exceeds file size');
|
||||
}
|
||||
this.header = header;
|
||||
debug(`pos=${this.tokenizer.position}, readChunk: chunkID=RIFF/WAVE/${header.chunkID}`);
|
||||
switch (header.chunkID) {
|
||||
case 'LIST':
|
||||
await this.parseListTag(header);
|
||||
break;
|
||||
case 'fact': // extended Format chunk,
|
||||
this.metadata.setFormat('lossless', false);
|
||||
this.fact = await this.tokenizer.readToken(new WaveChunk.FactChunk(header));
|
||||
break;
|
||||
case 'fmt ': // The Util Chunk, non-PCM Formats
|
||||
const fmt = await this.tokenizer.readToken(new WaveChunk.Format(header));
|
||||
let subFormat = WaveChunk.WaveFormat[fmt.wFormatTag];
|
||||
if (!subFormat) {
|
||||
debug('WAVE/non-PCM format=' + fmt.wFormatTag);
|
||||
subFormat = 'non-PCM (' + fmt.wFormatTag + ')';
|
||||
}
|
||||
this.metadata.setFormat('codec', subFormat);
|
||||
this.metadata.setFormat('bitsPerSample', fmt.wBitsPerSample);
|
||||
this.metadata.setFormat('sampleRate', fmt.nSamplesPerSec);
|
||||
this.metadata.setFormat('numberOfChannels', fmt.nChannels);
|
||||
this.metadata.setFormat('bitrate', fmt.nBlockAlign * fmt.nSamplesPerSec * 8);
|
||||
this.blockAlign = fmt.nBlockAlign;
|
||||
break;
|
||||
case 'id3 ': // The way Picard, FooBar currently stores, ID3 meta-data
|
||||
case 'ID3 ': // The way Mp3Tags stores ID3 meta-data
|
||||
const id3_data = await this.tokenizer.readToken(new Token.Uint8ArrayType(header.chunkSize));
|
||||
const rst = strtok3.fromBuffer(id3_data);
|
||||
await new ID3v2Parser_1.ID3v2Parser().parse(this.metadata, rst, this.options);
|
||||
break;
|
||||
case 'data': // PCM-data
|
||||
if (this.metadata.format.lossless !== false) {
|
||||
this.metadata.setFormat('lossless', true);
|
||||
}
|
||||
let chunkSize = header.chunkSize;
|
||||
if (this.tokenizer.fileInfo.size) {
|
||||
const calcRemaining = this.tokenizer.fileInfo.size - this.tokenizer.position;
|
||||
if (calcRemaining < chunkSize) {
|
||||
this.metadata.addWarning('data chunk length exceeding file length');
|
||||
chunkSize = calcRemaining;
|
||||
}
|
||||
}
|
||||
const numberOfSamples = this.fact ? this.fact.dwSampleLength : (chunkSize === 0xffffffff ? undefined : chunkSize / this.blockAlign);
|
||||
if (numberOfSamples) {
|
||||
this.metadata.setFormat('numberOfSamples', numberOfSamples);
|
||||
this.metadata.setFormat('duration', numberOfSamples / this.metadata.format.sampleRate);
|
||||
}
|
||||
this.metadata.setFormat('bitrate', this.metadata.format.numberOfChannels * this.blockAlign * this.metadata.format.sampleRate); // ToDo: check me
|
||||
await this.tokenizer.ignore(header.chunkSize);
|
||||
break;
|
||||
default:
|
||||
debug(`Ignore chunk: RIFF/${header.chunkID} of ${header.chunkSize} bytes`);
|
||||
this.metadata.addWarning('Ignore chunk: RIFF/' + header.chunkID);
|
||||
await this.tokenizer.ignore(header.chunkSize);
|
||||
}
|
||||
if (this.header.chunkSize % 2 === 1) {
|
||||
debug('Read odd padding byte'); // https://wiki.multimedia.cx/index.php/RIFF
|
||||
await this.tokenizer.ignore(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
async parseListTag(listHeader) {
|
||||
const listType = await this.tokenizer.readToken(new Token.StringType(4, 'binary'));
|
||||
debug('pos=%s, parseListTag: chunkID=RIFF/WAVE/LIST/%s', this.tokenizer.position, listType);
|
||||
switch (listType) {
|
||||
case 'INFO':
|
||||
return this.parseRiffInfoTags(listHeader.chunkSize - 4);
|
||||
case 'adtl':
|
||||
default:
|
||||
this.metadata.addWarning('Ignore chunk: RIFF/WAVE/LIST/' + listType);
|
||||
debug('Ignoring chunkID=RIFF/WAVE/LIST/' + listType);
|
||||
return this.tokenizer.ignore(listHeader.chunkSize - 4).then();
|
||||
}
|
||||
}
|
||||
async parseRiffInfoTags(chunkSize) {
|
||||
while (chunkSize >= 8) {
|
||||
const header = await this.tokenizer.readToken(riff.Header);
|
||||
const valueToken = new riff.ListInfoTagValue(header);
|
||||
const value = await this.tokenizer.readToken(valueToken);
|
||||
this.addTag(header.chunkID, util.stripNulls(value));
|
||||
chunkSize -= (8 + valueToken.len);
|
||||
}
|
||||
if (chunkSize !== 0) {
|
||||
throw Error('Illegal remaining size: ' + chunkSize);
|
||||
}
|
||||
}
|
||||
addTag(id, value) {
|
||||
this.metadata.addTag('exif', id, value);
|
||||
}
|
||||
}
|
||||
exports.WaveParser = WaveParser;
|
||||
Reference in New Issue
Block a user