diff --git a/README.md b/README.md
index 2c1431d35..9be751e9f 100644
--- a/README.md
+++ b/README.md
@@ -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) | |
| ASF | Advanced Systems Format | [:link:](https://wikipedia.org/wiki/Advanced_Systems_Format) | |
| FLAC | Free Lossless Audio Codec | [:link:](https://wikipedia.org/wiki/FLAC) | |
+| DSF | Sony's DSD Stream File | [:link:](https://en.wikipedia.org/wiki/Direct_Stream_Digital) | |
| 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) | |
| MPC | Musepack SV7 | [:link:](https://wikipedia.org/wiki/Musepack) | |
diff --git a/package.json b/package.json
index 04d68cd3e..cfd2e334d 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,9 @@
"Opus",
"speex",
"musepack",
- "mpc"
+ "mpc",
+ "dsd",
+ "dsf"
],
"main": "lib",
"typings": "lib/index",
diff --git a/src/ParserFactory.ts b/src/ParserFactory.ts
index 8e77d21ad..f2d35e44e 100644
--- a/src/ParserFactory.ts
+++ b/src/ParserFactory.ts
@@ -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");
@@ -131,6 +132,9 @@ export class ParserFactory {
case ".mpc":
return 'musepack';
+
+ case '.dsf':
+ return 'dsf';
}
}
@@ -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();
diff --git a/src/dsf/DsfChunk.ts b/src/dsf/DsfChunk.ts
new file mode 100644
index 000000000..518c4ea88
--- /dev/null
+++ b/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 = {
+ 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 = {
+ 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 = {
+ 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)
+ };
+ }
+};
diff --git a/src/dsf/DsfParser.ts b/src/dsf/DsfParser.ts
new file mode 100644
index 000000000..5303d751b
--- /dev/null
+++ b/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 {
+
+ const p0 = this.tokenizer.position; // mark start position, normally 0
+ const chunkHeader = await this.tokenizer.readToken(ChunkHeader);
+ assert.strictEqual(chunkHeader.id, 'DSD ');
+ this.metadata.setFormat('dataformat', 'DSF');
+ this.metadata.setFormat('lossless', true);
+ const dsdChunk = await this.tokenizer.readToken(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(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;
+ }
+ }
+}
diff --git a/src/type.ts b/src/type.ts
index 7012b911e..6fc4e8d15 100644
--- a/src/type.ts
+++ b/src/type.ts
@@ -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,
diff --git a/test/samples/dsf/2L-110_stereo-5644k-1b_04_0.1-sec.dsf b/test/samples/dsf/2L-110_stereo-5644k-1b_04_0.1-sec.dsf
new file mode 100644
index 000000000..800242784
Binary files /dev/null and b/test/samples/dsf/2L-110_stereo-5644k-1b_04_0.1-sec.dsf differ
diff --git a/test/test-file-dsf.ts b/test/test-file-dsf.ts
new file mode 100644
index 000000000..d8e722d1b
--- /dev/null
+++ b/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});
+ });
+
+});