Skip to content

Commit

Permalink
#28: Add initial support for RIFF-LIST-INFO tag.
Browse files Browse the repository at this point in the history
  • Loading branch information
Borewit committed Aug 19, 2017
1 parent 958b50e commit 5bcbfed
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 15 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ npm install music-metadata
| m4a, m4b, m4p, m4v, m4r, 3gp, mp4, aac | audio/aac, audio/aacp | QTFF |
| mp2, mp3, m2a | audio/mpeg | ID3v1.1, ID3v2 |
| ogv, oga, ogx, ogg | audio/ogg, application/ogg | Vorbis |
| wav | audio/wav, audio/wave | ID3v2 |
| wav | audio/wav, audio/wave | ID3v2, RIFF/INFO (EXIF 2.3) |
| wv, wvp | audio/x-wavpack | APEv2 |


Expand Down
19 changes: 18 additions & 1 deletion src/riff/RiffChunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as Token from "token-types";
export interface IChunkHeader {

/**
* A chunk ID (ie, 4 ASCII bytes)
* A chunk ID (ie, 4 ASCII bytes)
*/
chunkID: string,
/**
Expand All @@ -27,3 +27,20 @@ export const Header: Token.IGetToken<IChunkHeader> = {
};
}
};

/**
* Token to parse RIFF-INFO tag value
*/
export class ListInfoTagValue implements Token.IGetToken<string> {

public len: number;

public constructor(private tagHeader: IChunkHeader) {
this.len = tagHeader.size;
this.len += this.len & 1; // if it an odd length, round up to even
}

public get(buf, off): string {
return new Token.StringType(this.tagHeader.size, 'ascii').get(buf, off);
}
}
13 changes: 13 additions & 0 deletions src/riff/RiffInfoTagMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {INativeTagMap} from "../tagmap";

/**
* RIFF Info Tags; part of the EXIF 2.3
* Ref: http://owl.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html#Info
*/
export const RiffInfoTagMap: INativeTagMap = {
IART: 'artist', // Artist
ICRD: 'date', // DateCreated
INAM: 'title', // Title
IPRD: 'album', // Product
ITRK: 'track'
};
63 changes: 52 additions & 11 deletions src/riff/RiffParser.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {ITokenParser} from "../ParserFactory";
import * as strtok3 from "strtok3";
import {IOptions, INativeAudioMetadata} from "../";
import {IOptions, INativeAudioMetadata, ITag} 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";
import {IChunkHeader} from "../aiff/Chunk";
import Common from "../common";

/**
* Resource Interchange File Format (RIFF) Parser
Expand All @@ -27,12 +29,16 @@ export class WavePcmParser implements ITokenParser {
},
native: {}
};

/**
* RIFF/ILIST-INFO tag stored in EXIF
*/
private riffInfoTags: ITag[] = [];

private warnings: string[] = [];

private blockAlign: number;

private native: INativeAudioMetadata;

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

this.tokenizer = tokenizer;
Expand All @@ -41,12 +47,12 @@ export class WavePcmParser implements ITokenParser {
return this.tokenizer.readToken<RiffChunk.IChunkHeader>(RiffChunk.Header)
.then((header) => {
if (header.chunkID !== 'RIFF')
return null; // Not AIFF format
return null; // Not RIFF format

return this.tokenizer.readToken<string>(new Token.StringType(4, 'ascii')).then((type) => {
this.metadata.format.dataformat = type;
}).then(() => {
return this.readChunk().then(() => {
return this.readChunk(header).then(() => {
return null;
});
});
Expand All @@ -57,14 +63,31 @@ export class WavePcmParser implements ITokenParser {
} else {
throw err;
}
}).then((metadata) => {
if (this.riffInfoTags.length > 0) {
metadata.native.exif = this.riffInfoTags;
}
return metadata;
});
}

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

case "LIST":
return this.tokenizer.readToken<string>(new Token.StringType(4, 'ascii')).then((listTypes) => {
switch (listTypes) {
case 'INFO':
return this.parseRiffInfoTags(header.size - 4).then(() => header.size);

default:
this.warnings.push("Ignore chunk: RIFF/LIST." + listTypes);
return this.tokenizer.ignore(header.size).then(() => header.size);
}
});

case "fmt ": // The Common Chunk
return this.tokenizer.readToken<WaveChunk.IFormat>(new WaveChunk.Format(header))
.then((common) => {
Expand All @@ -73,9 +96,9 @@ export class WavePcmParser implements ITokenParser {
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 Picard, FooBar 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) => {
Expand All @@ -91,15 +114,33 @@ export class WavePcmParser implements ITokenParser {
case 'data': // PCM-data
this.metadata.format.numberOfSamples = header.size / this.blockAlign;
this.metadata.format.duration = this.metadata.format.numberOfSamples / this.metadata.format.sampleRate;
this.metadata.format.bitrate = this.metadata.format.numberOfChannels * this.blockAlign * this.metadata.format.sampleRate; // ToDo: check me
return this.tokenizer.ignore(header.size);

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

private parseRiffInfoTags(chunkSize): Promise<void> {
return this.tokenizer.readToken<RiffChunk.IChunkHeader>(WaveChunk.Header)
.then((header) => {
const valueToken = new RiffChunk.ListInfoTagValue(header);
return this.tokenizer.readToken(valueToken).then((value) => {
this.riffInfoTags.push({id: header.chunkID, value: Common.stripNulls(value)});
chunkSize -= (8 + valueToken.len);
if (chunkSize > 8) {
return this.parseRiffInfoTags(chunkSize);
} else if (chunkSize === 0) {
return Promise.resolve<void>();
} else {
throw Error("Illegal remaining size: " + chunkSize);
}
});
});
}

Expand Down
5 changes: 4 additions & 1 deletion src/tagmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {ID3v24TagMap} from "./id3v2/ID3v24TagMap";
import {MP4TagMap} from "./mp4/MP4TagMap";
import {VorbisTagMap} from "./vorbis/VorbisTagMap";
import {APEv2TagMap} from "./apev2/APEv2TagMap";
import {RiffInfoTagMap} from "./riff/RiffInfoTagMap";
export type HeaderType = 'vorbis' | 'id3v1.1'| 'id3v2.2' | 'id3v2.3' | 'id3v2.4' | 'APEv2' | 'asf' | 'iTunes MP4';

export type CommonTag = 'track' | 'disk' | 'year' | 'title' | 'artist' | 'artists' | 'albumartist' | 'album' | 'date' | 'originaldate' |
Expand Down Expand Up @@ -41,7 +42,8 @@ interface INativeTagMappings {
'id3v2.3': INativeTagMap,
'id3v2.4': INativeTagMap,
'iTunes MP4': INativeTagMap,
vorbis: INativeTagMap
vorbis: INativeTagMap,
exif: INativeTagMap
}

/**
Expand Down Expand Up @@ -175,6 +177,7 @@ export default class TagMap {
'id3v2.3': ID3v24TagMap,
'id3v2.4': ID3v24TagMap,
'iTunes MP4': MP4TagMap,
exif: RiffInfoTagMap,
vorbis: VorbisTagMap
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/wav/WaveChunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface IFormat {
}

/**
* "fmt " chunk
* format chunk; chunk-id is "fmt "
* http://soundfile.sapp.org/doc/WaveFormat/
*/
export class Format implements Token.IGetToken<IFormat> {
Expand Down
52 changes: 52 additions & 0 deletions test/test_riff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {} from "mocha";
import {assert} from 'chai';
import * as mm from '../src';
import * as path from 'path';

const t = assert;

describe("Extract metadata from RIFF (Resource Interchange File Format)", () => {

describe("Parse RIFF/WAVE audio format", () => {

function checkExifTags(exif: mm.INativeTagDict) {

t.deepEqual(exif.IART, ["Beth Hart & Joe Bonamassa"], "exif.IART");
t.deepEqual(exif.ICRD, ["2011"], "exif.ICRD");
t.deepEqual(exif.INAM, ["Sinner's Prayer"], "exif.INAM");
t.deepEqual(exif.IPRD, ["Don't Explain"], "exif.IPRD");
t.deepEqual(exif.ITRK, ["1/10"], "exif.ITRK");
}

/**
* Looks like RIFF/WAV not fully supported yet in MusicBrainz Picard: https://tickets.metabrainz.org/browse/PICARD-653?jql=text%20~%20%22RIFF%22.
* This file has been fixed with Mp3Tag to have a valid ID3v2.3 tag
*/
it("should parse LIST-INFO (EXIF)", () => {

const filename = "MusicBrainz - Beth Hart - Sinner's Prayer [id3v2.3].wav";
const filePath = path.join(__dirname, 'samples', filename);

function checkFormat(format: mm.IFormat) {
t.strictEqual(format.dataformat, "WAVE", "format.dataformat = WAVE");
// t.strictEqual(format.headerType, "id3v2.4", "format.headerType = 'id3v2.4'"); // ToDo
t.strictEqual(format.sampleRate, 44100, 'format.sampleRate = 44.1 kHz');
t.strictEqual(format.bitsPerSample, 16, 'format.bitsPerSample = 16 bits');
t.strictEqual(format.numberOfChannels, 2, 'format.numberOfChannels = 2 channels');
t.strictEqual(format.numberOfSamples, 93624, 'format.numberOfSamples = 93624');
t.strictEqual(format.duration, 2.1229931972789116, 'format.duration = ~2.123');
}

// Parse wma/asf file
return mm.parseFile(filePath, {native: true}).then((result) => {
// Check wma format
checkFormat(result.format);
// Check native tags
checkExifTags(mm.orderTags(result.native.exif));
});

});

});

});

0 comments on commit 5bcbfed

Please sign in to comment.