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) | 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) | FLAC logo | +| DSF | Sony's DSD Stream File | [:link:](https://en.wikipedia.org/wiki/Direct_Stream_Digital) | 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) | MP3 logo | | MPC | Musepack SV7 | [:link:](https://wikipedia.org/wiki/Musepack) | musepack logo | 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}); + }); + +});