Skip to content

Commit

Permalink
#195 Add DSF audio format parser
Browse files Browse the repository at this point in the history
  • Loading branch information
Borewit committed Apr 6, 2019
1 parent 327e8e5 commit f4c92de
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 1 deletion.
5 changes: 5 additions & 0 deletions src/ParserFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import MusepackParser from './musepack';
import { OggParser } from './ogg/OggParser';
import { WaveParser } from './riff/WaveParser';
import { WavPackParser } from './wavpack/WavPackParser';
import {DsfParser} from "./dsf/DsfParser";

const debug = _debug("music-metadata:parser:factory");

Expand Down Expand Up @@ -131,6 +132,9 @@ export class ParserFactory {

case ".mpc":
return 'musepack';

case '.dsf':
return 'dsf';
}
}

Expand All @@ -139,6 +143,7 @@ export class ParserFactory {
case 'aiff': return new AIFFParser();
case 'apev2': return new APEv2Parser();
case 'asf': return new AsfParser();
case 'dsf': return new DsfParser();
case 'flac': return new FlacParser();
case 'mp4': return new MP4Parser();
case 'mpeg': return new MpegParser();
Expand Down
137 changes: 137 additions & 0 deletions src/dsf/DsfChunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import * as Token from 'token-types';
import {FourCcToken} from '../common/FourCC';

/**
* Common interface for the common chunk DSD header
*/
export interface IChunkHeader {

/**
* Chunk ID
*/
id: string;

/**
* Chunk size
*/
size: number;
}

/**
* Common chunk DSD header: the 'chunk name (Four-CC)' & chunk size
*/
export const ChunkHeader: Token.IGetToken<IChunkHeader> = {
len: 12,

get: (buf: Buffer, off: number): IChunkHeader => {
return {id: FourCcToken.get(buf, off), size: Token.UINT64_LE.get(buf, off + 4)};
}
};

/**
* Interface to DSD payload chunk
*/
export interface IDsdChunk {

/**
* Total file size
*/
fileSize: number;

/**
* If Metadata doesn’t exist, set 0. If the file has ID3v2 tag, then set the pointer to it.
* ID3v2 tag should be located in the end of the file.
*/
metadataPointer: number;
}

/**
* Common chunk DSD header: the 'chunk name (Four-CC)' & chunk size
*/
export const DsdChunk: Token.IGetToken<IDsdChunk> = {
len: 16,

get: (buf: Buffer, off: number): IDsdChunk => {
return {
fileSize: Token.INT64_LE.get(buf, off),
metadataPointer: Token.INT64_LE.get(buf, off + 8)
}
;
}
};

export enum ChannelType {
mono = 1,
stereo = 2,
channels = 3,
quad = 4,
'4 channels' = 5,
'5 channels' = 6,
'5.1 channels' = 7
}

/**
* Interface to format chunk payload chunk
*/
export interface IFormatChunk {

/**
* Version of this file format
*/
formatVersion: number;

/**
* Format ID
*/
formatID: number;

/**
* Channel Type
*/
channelType: ChannelType;

/**
* Channel num
*/
channelNum: number;

/**
* Sampling frequency
*/
samplingFrequency: number;

/**
* Bits per sample
*/
bitsPerSample: number;

/**
* Sample count
*/
sampleCount: number;

/**
* Block size per channel
*/
blockSizePerChannel: number;
}

/**
* Common chunk DSD header: the 'chunk name (Four-CC)' & chunk size
*/
export const FormatChunk: Token.IGetToken<IFormatChunk> = {
len: 44,

get: (buf: Buffer, off: number): IFormatChunk => {
return {
formatVersion: Token.INT32_LE.get(buf, off),
formatID: Token.INT32_LE.get(buf, off + 4),
channelType: Token.INT32_LE.get(buf, off + 8),
channelNum: Token.INT32_LE.get(buf, off + 12),
samplingFrequency: Token.INT32_LE.get(buf, off + 16),
bitsPerSample: Token.INT32_LE.get(buf, off + 20),
sampleCount: Token.INT64_LE.get(buf, off + 24),
blockSizePerChannel: Token.INT32_LE.get(buf, off + 32)
};
}
};
52 changes: 52 additions & 0 deletions src/dsf/DsfParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';

import { AbstractID3Parser } from '../id3v2/AbstractID3Parser';
import * as assert from 'assert';

import * as _debug from 'debug';
import {ChunkHeader, DsdChunk, FormatChunk, IChunkHeader, IDsdChunk} from "./DsfChunk";
import {ID3v2Parser} from "../id3v2/ID3v2Parser";

const debug = _debug('music-metadata:parser:DSF');

/**
* DSF (dsd stream file) File Parser
* Ref: https://dsd-guide.com/sites/default/files/white-papers/DSFFileFormatSpec_E.pdf
*/
export class DsfParser extends AbstractID3Parser {

public async _parse(): Promise<void> {

const chunkHeader = await this.tokenizer.readToken<IChunkHeader>(ChunkHeader);
assert.strictEqual(chunkHeader.id, 'DSD ');
this.metadata.setFormat('dataformat', 'DSF');
const dsdChunk = await this.tokenizer.readToken<IDsdChunk>(DsdChunk);
if (dsdChunk.metadataPointer === 0) {
debug(`No ID3v2 tag present`);
} else {
debug(`expect ID3v2 at offset=${dsdChunk.metadataPointer}`);
await this.parseChunks();
// Jump to ID3 header
this.tokenizer.ignore(dsdChunk.metadataPointer - this.tokenizer.position);
return new ID3v2Parser().parse(this.metadata, this.tokenizer, this.options);
}
}

private async parseChunks() {
const chunkHeader = await this.tokenizer.readToken<IChunkHeader>(ChunkHeader);
debug(`Parsing chunk name=${chunkHeader.id} size=${chunkHeader.size}`);
switch (chunkHeader.id) {
case 'fmt ':
const formatChunk = await this.tokenizer.readToken(FormatChunk);
this.metadata.setFormat('numberOfChannels', formatChunk.channelNum);
this.metadata.setFormat('sampleRate', formatChunk.samplingFrequency);
this.metadata.setFormat('bitsPerSample', formatChunk.bitsPerSample);
this.metadata.setFormat('numberOfSamples', formatChunk.sampleCount);
this.metadata.setFormat('duration', formatChunk.sampleCount / formatChunk.samplingFrequency);
return;
default:
this.tokenizer.ignore(chunkHeader.size - ChunkHeader.len);
break;
}
}
}
2 changes: 1 addition & 1 deletion src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ export interface IAudioMetadata extends INativeAudioMetadata {
/**
* Corresponds with parser module name
*/
export type ParserType = 'mpeg' | 'apev2' | 'mp4' | 'asf' | 'flac' | 'ogg' | 'aiff' | 'wavpack' | 'riff' | 'musepack';
export type ParserType = 'mpeg' | 'apev2' | 'mp4' | 'asf' | 'flac' | 'ogg' | 'aiff' | 'wavpack' | 'riff' | 'musepack' | 'dsf';

export interface IOptions {
path?: string,
Expand Down
Binary file not shown.
30 changes: 30 additions & 0 deletions test/test-file-dsf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {assert} from 'chai';
import * as mm from '../src';
import * as path from 'path';

describe('Parse DSF (dsd stream file)', () => {

const dsfSamplePath = path.join(__dirname, 'samples', 'dsf');

it('parse: 2L-110_stereo-5644k-1b_04.dsf', async () => {

const dsfFilePath = path.join(dsfSamplePath, '2L-110_stereo-5644k-1b_04_0.1-sec.dsf');

const metadata = await mm.parseFile(dsfFilePath, {duration: false});
assert.strictEqual(metadata.format.dataformat, 'DSF');
assert.deepEqual(metadata.format.tagTypes, ['ID3v2.3']);

// format chunk information
assert.deepEqual(metadata.format.numberOfChannels, 2);
assert.deepEqual(metadata.format.bitsPerSample, 1);
assert.deepEqual(metadata.format.sampleRate, 5644800);
assert.deepEqual(metadata.format.numberOfSamples, 564480);
assert.deepEqual(metadata.format.duration, 0.1);

// ID3v2 chunk information
assert.strictEqual(metadata.common.title, 'Kyrie');
assert.strictEqual(metadata.common.artist, 'CANTUS (Tove Ramlo-Ystad) & Frode Fjellheim');
assert.deepEqual(metadata.common.track, {no: 4, of: 12});
});

});

0 comments on commit f4c92de

Please sign in to comment.