Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Sony DSF (DSD Stream File) format #197

Merged
merged 7 commits into from Apr 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -24,6 +24,7 @@ Stream and file based music metadata parser for node.
| APE | Monkey's Audio | [:link:](https://wikipedia.org/wiki/Monkey's_Audio) | <img src="https://foreverhits.files.wordpress.com/2015/05/ape_audio.jpg" width="40" alt="Monkey's Audio logo"> |
| ASF | Advanced Systems Format | [:link:](https://wikipedia.org/wiki/Advanced_Systems_Format) | |
| FLAC | Free Lossless Audio Codec | [:link:](https://wikipedia.org/wiki/FLAC) | <img src="https://upload.wikimedia.org/wikipedia/commons/e/e0/Flac_logo_vector.svg" width="80" alt="FLAC logo"> |
| DSF | Sony's DSD Stream File | [:link:](https://en.wikipedia.org/wiki/Direct_Stream_Digital) | <img src="https://upload.wikimedia.org/wikipedia/commons/b/bc/DSDlogo.svg" width="80" alt="DSD logo"> |
| MP2 | MPEG-1 Audio Layer II | [:link:](https://wikipedia.org/wiki/MPEG-1_Audio_Layer_II) | |
| MP3 | MPEG-1 / MPEG-2 Audio Layer III | [:link:](https://wikipedia.org/wiki/MP3) | <img src="https://upload.wikimedia.org/wikipedia/commons/e/ea/Mp3.svg" width="80" alt="MP3 logo"> |
| MPC | Musepack SV7 | [:link:](https://wikipedia.org/wiki/Musepack) | <img src="https://www.musepack.net/pictures/musepack_logo.png" width="80" alt="musepack logo"> |
Expand Down
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -36,7 +36,9 @@
"Opus",
"speex",
"musepack",
"mpc"
"mpc",
"dsd",
"dsf"
],
"main": "lib",
"typings": "lib/index",
Expand Down
5 changes: 5 additions & 0 deletions src/ParserFactory.ts
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
@@ -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: 40,

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)
};
}
};
59 changes: 59 additions & 0 deletions src/dsf/DsfParser.ts
@@ -0,0 +1,59 @@
'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 p0 = this.tokenizer.position; // mark start position, normally 0
const chunkHeader = await this.tokenizer.readToken<IChunkHeader>(ChunkHeader);
assert.strictEqual(chunkHeader.id, 'DSD ');
this.metadata.setFormat('dataformat', 'DSF');
this.metadata.setFormat('lossless', true);
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(dsdChunk.fileSize - chunkHeader.size);
// Jump to ID3 header
this.tokenizer.ignore(dsdChunk.metadataPointer - this.tokenizer.position - p0);
return new ID3v2Parser().parse(this.metadata, this.tokenizer, this.options);
}
}

private async parseChunks(bytesRemaining: number) {
while (bytesRemaining >= ChunkHeader.len) {
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);
const bitrate = formatChunk.bitsPerSample * formatChunk.samplingFrequency * formatChunk.channelNum;
this.metadata.setFormat('bitrate', bitrate);
return; // We got what we want, stop further processing of chunks
default:
this.tokenizer.ignore(chunkHeader.size - ChunkHeader.len);
break;
}
bytesRemaining -= chunkHeader.size;
}
}
}
2 changes: 1 addition & 1 deletion src/type.ts
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.
32 changes: 32 additions & 0 deletions test/test-file-dsf.ts
@@ -0,0 +1,32 @@
import {assert} from 'chai';
import * as mm from '../src';
import * as path from 'path';

describe('Parse Sony 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});

// format chunk information
assert.strictEqual(metadata.format.dataformat, 'DSF');
assert.deepEqual(metadata.format.lossless, true);
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);
assert.deepEqual(metadata.format.bitrate, 11289600);
assert.deepEqual(metadata.format.tagTypes, ['ID3v2.3']);

// 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});
});

});