Skip to content

Commit

Permalink
#19: Added support for RIFF/WAVE metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
Borewit committed Aug 8, 2017
1 parent 2bb242e commit 255e175
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 33 deletions.
8 changes: 8 additions & 0 deletions lib/ParserFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {Promise} from "es6-promise";
import * as Stream from "stream";
import * as path from "path";
import {AIFFParser} from "./aiff/AiffParser";
import {WavePcmParser} from "./riff/RiffParser";

export interface ITokenParser {
parse(tokenizer: strtok3.ITokenizer, options: IOptions): Promise<INativeAudioMetadata>;
Expand Down Expand Up @@ -71,7 +72,9 @@ export class ParserFactory {
const extension = path.extname(filePath).toLocaleLowerCase();
switch (extension) {

case '.mp2':
case '.mp3':
case '.m2a':
return this.hasStartTag(filePath, 'ID3').then((hasID3) => {
return hasID3 ? new ID3v2Parser() : new ID3v1Parser();
});
Expand Down Expand Up @@ -103,9 +106,14 @@ export class ParserFactory {
case '.ogx':
return Promise.resolve<ITokenParser>(new OggParser());

case '.aif':
case '.aiff':
case '.aifc':
return Promise.resolve<ITokenParser>(new AIFFParser());

case '.wav':
return Promise.resolve<ITokenParser>(new WavePcmParser());

default:
throw new Error("Extension " + extension + " not supported.");
}
Expand Down
4 changes: 2 additions & 2 deletions lib/id3v2/ID3v24TagMap.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {INativeTagMap} from "../tagmap";

/**
* ID3v2.3 tag mappings
* ID3v2.3/ID3v2.4 tag mappings
*/
export const ID3v24TagMap: INativeTagMap = {
// id3v2.3
Expand Down Expand Up @@ -105,5 +105,5 @@ export const ID3v24TagMap: INativeTagMap = {

// Windows Media Player
'PRIV:AverageLevel' : 'averageLevel',
'PRIV:PeakLevel' : 'peakLevel'
'PRIV:PeakLevel' : 'peakLevel',
};
29 changes: 29 additions & 0 deletions lib/riff/RiffChunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as Token from "token-types";

export interface IChunkHeader {

/**
* A chunk ID (ie, 4 ASCII bytes)
*/
chunkID: string,
/**
* Number of data bytes following this data header
*/
size: number
}

/**
* Common RIFF chunk header
*/
export const Header: Token.IGetToken<IChunkHeader> = {
len: 8,

get: (buf, off): IChunkHeader => {
return {
// Group-ID
chunkID: new Token.StringType(4, 'ascii').get(buf, off),
// Size
size: buf.readUInt32BE(off + 4)
};
}
};
119 changes: 119 additions & 0 deletions lib/riff/RiffParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {ITokenParser} from "../ParserFactory";
import {EndOfFile, ITokenizer} from "strtok3";
import * as strtok3 from "strtok3";
import {IOptions, INativeAudioMetadata} from "../";
import * as Token from "token-types";
import * as RiffChunk from "./RiffChunk";
import * as WaveChunk from "./../wav/WaveChunk";
import {Readable} from "stream";
import {ID3v2Parser} from "../id3v2/ID3v2Parser";

/**
* 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
*/
export class WavePcmParser implements ITokenParser {

private tokenizer: ITokenizer;
private options: IOptions;

private metadata: INativeAudioMetadata = {
format: {
dataformat: "WAVE PCM"
},
native: {}
};
private warnings: string[] = [];

private blockAlign: number;

private native: INativeAudioMetadata;

public parse(tokenizer: ITokenizer, options: IOptions): Promise<INativeAudioMetadata> {

this.tokenizer = tokenizer;
this.options = options;

return this.tokenizer.readToken<RiffChunk.IChunkHeader>(RiffChunk.Header)
.then((header) => {
if (header.chunkID !== 'RIFF')
return null; // Not AIFF format

return this.tokenizer.readToken<string>(new Token.StringType(4, 'ascii')).then((type) => {
this.metadata.format.dataformat = type;
}).then(() => {
return this.readChunk().then(() => {
return null;
});
});
})
.catch((err) => {
if (err === EndOfFile) {
return this.metadata;
} else {
throw err;
}
});
}

public readChunk(): Promise<void> {
return this.tokenizer.readToken<RiffChunk.IChunkHeader>(WaveChunk.Header)
.then((header) => {
switch (header.chunkID) {

case "fmt ": // The Common Chunk
return this.tokenizer.readToken<WaveChunk.IFormat>(new WaveChunk.Format(header))
.then((common) => {
this.metadata.format.bitsPerSample = common.bitsPerSample;
this.metadata.format.sampleRate = common.sampleRate;
this.metadata.format.numberOfChannels = common.numChannels;
this.metadata.format.bitrate = common.blockAlign * common.sampleRate * 8;
this.blockAlign = common.blockAlign;
});

case "id3 ": // The way Picard currently stores, ID3 meta-data
case "ID3 ": // The way Mp3Tags stores ID3 meta-data
return this.tokenizer.readToken<Buffer>(new Token.BufferType(header.size))
.then((id3_data) => {
const id3stream = new ID3Stream(id3_data);
return strtok3.fromStream(id3stream).then((rst) => {
return ID3v2Parser.getInstance().parse(rst, this.options).then((id3) => {
this.metadata.format.headerType = id3.format.headerType;
this.metadata.native = id3.native;
});
});
});

case 'data': // PCM-data
this.metadata.format.numberOfSamples = header.size / this.blockAlign;
this.metadata.format.duration = this.metadata.format.numberOfSamples / this.metadata.format.sampleRate;
return this.tokenizer.ignore(header.size);

case "LIST": // LIST ToDo?
default:
this.warnings.push("Ignore chunk: " + header.chunkID);
return this.tokenizer.ignore(header.size);
}
}).then(() => {
return this.readChunk();
});
}

}

class ID3Stream extends Readable {

constructor(private buf: Buffer) {
super();
}

public _read() {
this.push(this.buf);
this.push(null); // push the EOF-signaling `null` chunk
}
}
67 changes: 67 additions & 0 deletions lib/wav/WaveChunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as Token from "token-types";
import * as assert from "assert";
import {IChunkHeader} from "../riff/RiffChunk";

/**
* WAVE header chunk
*/
export const Header: Token.IGetToken<IChunkHeader> = {
len: 8,

get: (buf, off): IChunkHeader => {
return {
// Group-ID
chunkID: new Token.StringType(4, 'ascii').get(buf, off),
// Size
size: buf.readUInt32LE(off + 4)
};
}
};

/**
* "fmt" sub-chunk describes the sound data's format
* Ref: http://soundfile.sapp.org/doc/WaveFormat
*/
export interface IFormat {
/**
* PCM = 1 (i.e. Linear quantization). Values other than 1 indicate some form of compression.
*/
audioFormat: number,
/**
* Mono = 1, Stereo = 2, etc.
*/
numChannels: number,
/**
* 8000, 44100, etc.
*/
sampleRate: number,
byteRate: number,
blockAlign: number,
bitsPerSample: number
}

/**
* "fmt " chunk
* http://soundfile.sapp.org/doc/WaveFormat/
*/
export class Format implements Token.IGetToken<IFormat> {

public len: number;

public constructor(header: IChunkHeader) {
assert.ok(header.size >= 16, "16 for PCM.");
this.len = header.size;
}

public get(buf: Buffer, off: number): IFormat {
return {
audioFormat: buf.readUInt16LE(off),
numChannels: buf.readUInt16LE(off + 2),
sampleRate: buf.readUInt32LE(off + 4),
byteRate: buf.readUInt32LE(off + 8),
blockAlign: buf.readUInt16LE(off + 12),
bitsPerSample: buf.readUInt16LE(off + 14)
};
}

}
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit 255e175

Please sign in to comment.