From 4e909f94113360933b77cf9e67ec54649d48665f Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@duempel.org>
Date: Mon, 3 Oct 2011 12:52:15 +0200
Subject: [PATCH] decoder/dsdiff: new decoder plugin

Doesn't seem to work yet, getting just noise from a test file.
Seeking isn't implemented yet.
---
 Makefile.am                         |   3 +
 NEWS                                |   1 +
 src/decoder/dsdiff_decoder_plugin.c | 485 ++++++++++++++++++++++++++++
 src/decoder/dsdiff_decoder_plugin.h |  25 ++
 src/decoder_list.c                  |   2 +
 5 files changed, 516 insertions(+)
 create mode 100644 src/decoder/dsdiff_decoder_plugin.c
 create mode 100644 src/decoder/dsdiff_decoder_plugin.h

diff --git a/Makefile.am b/Makefile.am
index 25ef552ce..54e03c277 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -490,6 +490,9 @@ DECODER_LIBS = \
 
 DECODER_SRC = \
 	src/decoder/pcm_decoder_plugin.c \
+	src/decoder/dsdiff_decoder_plugin.c \
+	src/decoder/dsdiff_decoder_plugin.h \
+	src/dsd2pcm/dsd2pcm.c src/dsd2pcm/dsd2pcm.h \
 	src/decoder_buffer.c \
 	src/decoder_plugin.c \
 	src/decoder_list.c
diff --git a/NEWS b/NEWS
index 6c060e758..a38f66e8b 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,7 @@ ver 0.17 (2011/??/??)
   - ffmpeg: drop support for pre-0.5 ffmpeg
   - ffmpeg: support libavformat 0.7
   - oggflac: delete this obsolete plugin
+  - dsdiff: new decoder plugin
 * output:
   - httpd: support for streaming to a DLNA client
   - osx: allow user to specify other audio devices
diff --git a/src/decoder/dsdiff_decoder_plugin.c b/src/decoder/dsdiff_decoder_plugin.c
new file mode 100644
index 000000000..d1b12e0a7
--- /dev/null
+++ b/src/decoder/dsdiff_decoder_plugin.c
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2003-2011 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "dsdiff_decoder_plugin.h"
+#include "decoder_api.h"
+#include "audio_check.h"
+#include "dsd2pcm/dsd2pcm.h"
+
+#include <unistd.h>
+
+#undef G_LOG_DOMAIN
+#define G_LOG_DOMAIN "dsdiff"
+
+struct dsdiff_id {
+	char value[4];
+};
+
+struct dsdiff_header {
+	struct dsdiff_id id;
+	uint32_t size_high, size_low;
+	struct dsdiff_id format;
+};
+
+struct dsdiff_chunk_header {
+	struct dsdiff_id id;
+	uint32_t size_high, size_low;
+};
+
+struct dsdiff_metadata {
+	unsigned sample_rate, channels;
+};
+
+static bool
+dsdiff_id_equals(const struct dsdiff_id *id, const char *s)
+{
+	assert(id != NULL);
+	assert(s != NULL);
+	assert(strlen(s) == sizeof(id->value));
+
+	return memcmp(id->value, s, sizeof(id->value)) == 0;
+}
+
+/**
+ * Read the "size" attribute from the specified header, converting it
+ * to the host byte order if needed.
+ */
+G_GNUC_CONST
+static uint64_t
+dsdiff_chunk_size(const struct dsdiff_chunk_header *header)
+{
+	return (((uint64_t)GUINT32_FROM_BE(header->size_high)) << 32) |
+		((uint64_t)GUINT32_FROM_BE(header->size_low));
+}
+
+static bool
+dsdiff_read(struct decoder *decoder, struct input_stream *is,
+	    void *data, size_t length)
+{
+	size_t nbytes = decoder_read(decoder, is, data, length);
+	return nbytes == length;
+}
+
+static bool
+dsdiff_read_id(struct decoder *decoder, struct input_stream *is,
+	       struct dsdiff_id *id)
+{
+	return dsdiff_read(decoder, is, id, sizeof(*id));
+}
+
+static bool
+dsdiff_read_chunk_header(struct decoder *decoder, struct input_stream *is,
+			 struct dsdiff_chunk_header *header)
+{
+	return dsdiff_read(decoder, is, header, sizeof(*header));
+}
+
+static bool
+dsdiff_read_payload(struct decoder *decoder, struct input_stream *is,
+		    const struct dsdiff_chunk_header *header,
+		    void *data, size_t length)
+{
+	uint64_t size = dsdiff_chunk_size(header);
+	if (size != (uint64_t)length)
+		return false;
+
+	size_t nbytes = decoder_read(decoder, is, data, length);
+	return nbytes == length;
+}
+
+/**
+ * Skip the #input_stream to the specified offset.
+ */
+static bool
+dsdiff_skip_to(struct decoder *decoder, struct input_stream *is,
+	       goffset offset)
+{
+	if (is->seekable)
+		return input_stream_seek(is, offset, SEEK_SET, NULL);
+
+	if (is->offset > offset)
+		return false;
+
+	char buffer[8192];
+	while (is->offset < offset) {
+		size_t length = sizeof(buffer);
+		if (offset - is->offset < (goffset)length)
+			length = offset - is->offset;
+
+		size_t nbytes = decoder_read(decoder, is, buffer, length);
+		if (nbytes == 0)
+			return false;
+	}
+
+	assert(is->offset == offset);
+	return true;
+}
+
+/**
+ * Skip some bytes from the #input_stream.
+ */
+static bool
+dsdiff_skip(struct decoder *decoder, struct input_stream *is,
+	    goffset delta)
+{
+	assert(delta >= 0);
+
+	if (delta == 0)
+		return true;
+
+	if (is->seekable)
+		return input_stream_seek(is, delta, SEEK_CUR, NULL);
+
+	char buffer[8192];
+	while (delta > 0) {
+		size_t length = sizeof(buffer);
+		if ((goffset)length > delta)
+			length = delta;
+
+		size_t nbytes = decoder_read(decoder, is, buffer, length);
+		if (nbytes == 0)
+			return false;
+
+		delta -= nbytes;
+	}
+
+	return true;
+}
+
+/**
+ * Read and parse a "SND" chunk inside "PROP".
+ */
+static bool
+dsdiff_read_prop_snd(struct decoder *decoder, struct input_stream *is,
+		     struct dsdiff_metadata *metadata,
+		     goffset end_offset)
+{
+	struct dsdiff_chunk_header header;
+	while ((goffset)(is->offset + sizeof(header)) <= end_offset) {
+		if (!dsdiff_read_chunk_header(decoder, is, &header))
+			return false;
+
+		goffset chunk_end_offset =
+			is->offset + dsdiff_chunk_size(&header);
+		if (chunk_end_offset > end_offset)
+			return false;
+
+		if (dsdiff_id_equals(&header.id, "FS  ")) {
+			uint32_t sample_rate;
+			if (!dsdiff_read_payload(decoder, is, &header,
+						 &sample_rate,
+						 sizeof(sample_rate)))
+				return false;
+
+			metadata->sample_rate = GUINT32_FROM_BE(sample_rate);
+		} else if (dsdiff_id_equals(&header.id, "CHNL")) {
+			uint16_t channels;
+			if (dsdiff_chunk_size(&header) < sizeof(channels) ||
+			    !dsdiff_read(decoder, is,
+					 &channels, sizeof(channels)) ||
+			    !dsdiff_skip_to(decoder, is, chunk_end_offset))
+				return false;
+
+			metadata->channels = GUINT16_FROM_BE(channels);
+		} else if (dsdiff_id_equals(&header.id, "CMPR")) {
+			struct dsdiff_id type;
+			if (dsdiff_chunk_size(&header) < sizeof(type) ||
+			    !dsdiff_read(decoder, is,
+					 &type, sizeof(type)) ||
+			    !dsdiff_skip_to(decoder, is, chunk_end_offset))
+				return false;
+
+			if (!dsdiff_id_equals(&type, "DSD "))
+				/* only uincompressed DSD audio data
+				   is implemented */
+				return false;
+		} else {
+			/* ignore unknown chunk */
+
+			if (!dsdiff_skip_to(decoder, is, chunk_end_offset))
+				return false;
+		}
+	}
+
+	return is->offset == end_offset;
+}
+
+/**
+ * Read and parse a "PROP" chunk.
+ */
+static bool
+dsdiff_read_prop(struct decoder *decoder, struct input_stream *is,
+		 struct dsdiff_metadata *metadata,
+		 const struct dsdiff_chunk_header *prop_header)
+{
+	uint64_t prop_size = dsdiff_chunk_size(prop_header);
+	goffset end_offset = is->offset + prop_size;
+
+	struct dsdiff_id prop_id;
+	if (prop_size < sizeof(prop_id) ||
+	    !dsdiff_read_id(decoder, is, &prop_id))
+		return false;
+
+	if (dsdiff_id_equals(&prop_id, "SND "))
+		return dsdiff_read_prop_snd(decoder, is, metadata, end_offset);
+	else
+		/* ignore unknown PROP chunk */
+		return dsdiff_skip_to(decoder, is, end_offset);
+}
+
+/**
+ * Read and parse all metadata chunks at the beginning.  Stop when the
+ * first "DSD" chunk is seen, and return its header in the
+ * "chunk_header" parameter.
+ */
+static bool
+dsdiff_read_metadata(struct decoder *decoder, struct input_stream *is,
+		     struct dsdiff_metadata *metadata,
+		     struct dsdiff_chunk_header *chunk_header)
+{
+	struct dsdiff_header header;
+	if (!dsdiff_read(decoder, is, &header, sizeof(header)) ||
+	    !dsdiff_id_equals(&header.id, "FRM8") ||
+	    !dsdiff_id_equals(&header.format, "DSD "))
+		return false;
+
+	while (true) {
+		if (!dsdiff_read_chunk_header(decoder, is, chunk_header))
+			return false;
+
+		if (dsdiff_id_equals(&chunk_header->id, "PROP")) {
+			if (!dsdiff_read_prop(decoder, is, metadata,
+					      chunk_header))
+				return false;
+		} else if (dsdiff_id_equals(&chunk_header->id, "DSD ")) {
+			/* done with metadata */
+			return true;
+		} else {
+			/* ignore unknown chunk */
+
+			uint64_t chunk_size = dsdiff_chunk_size(chunk_header);
+			goffset chunk_end_offset = is->offset + chunk_size;
+
+			if (!dsdiff_skip_to(decoder, is, chunk_end_offset))
+				return false;
+		}
+	}
+}
+
+G_GNUC_CONST
+static inline int32_t
+clip(int32_t min, int32_t value, int32_t max)
+{
+	if (G_UNLIKELY(value < min))
+		value = min;
+	else if (G_UNLIKELY(value > max))
+		value = max;
+	return value;
+}
+
+/**
+ * Convert an array of float samples [-1 .. +1] to padded 24 bit
+ * samples.
+ */
+static void
+float_to_uint24(int32_t *dest, const float *src, size_t length)
+{
+	const float *src_end = src + length;
+	while (src < src_end) {
+		float f_sample = *src++ * 8388608;
+		*dest++ = clip(-8388608, (int32_t)f_sample, 8388607);
+	}
+}
+
+/**
+ * Decode one "DSD" chunk.
+ */
+static bool
+dsdiff_decode_chunk(struct decoder *decoder, struct input_stream *is,
+		    unsigned channels,
+		    dsd2pcm_ctx **dsd2pcm, uint64_t chunk_size)
+{
+	uint8_t buffer[8192];
+	const size_t sample_size = sizeof(buffer[0]);
+	const size_t frame_size = channels * sample_size;
+	const unsigned buffer_frames = sizeof(buffer) / frame_size;
+	const unsigned buffer_samples = buffer_frames * frame_size;
+	const size_t buffer_size = buffer_samples * sample_size;
+	float f_buffer[G_N_ELEMENTS(buffer)];
+	int32_t i_buffer[G_N_ELEMENTS(buffer)];
+
+	while (chunk_size > 0) {
+		/* see how much aligned data from the remaining chunk
+		   fits into the local buffer */
+		unsigned now_frames = buffer_frames;
+		size_t now_size = buffer_size;
+		unsigned now_samples = buffer_samples;
+		if (chunk_size < (uint64_t)now_size) {
+			now_frames = (unsigned)chunk_size / frame_size;
+			now_size = now_frames * frame_size;
+			now_samples = now_frames * channels;
+		}
+
+		size_t nbytes = decoder_read(decoder, is, buffer, now_size);
+		if (nbytes != now_size)
+			return false;
+
+		chunk_size -= nbytes;
+
+		/* invoke the dsp2pcm library, once for each
+		   channel */
+
+		for (unsigned c = 0; c < channels; ++c)
+			dsd2pcm_translate(dsd2pcm[c], now_frames,
+					  buffer + c, channels,
+					  true, f_buffer + c, channels);
+
+		/* convert to integer and submit to the decoder API */
+
+		float_to_uint24(i_buffer, f_buffer, now_samples);
+		enum decoder_command cmd =
+			decoder_data(decoder, is, i_buffer,
+				     now_samples * sizeof(i_buffer[0]),
+				     0);
+		switch (cmd) {
+		case DECODE_COMMAND_NONE:
+			break;
+
+		case DECODE_COMMAND_START:
+		case DECODE_COMMAND_STOP:
+			return false;
+
+		case DECODE_COMMAND_SEEK:
+			/* not implemented yet */
+			decoder_seek_error(decoder);
+			break;
+		}
+	}
+
+	return dsdiff_skip(decoder, is, chunk_size);
+}
+
+static void
+dsdiff_stream_decode(struct decoder *decoder, struct input_stream *is)
+{
+	struct dsdiff_metadata metadata = {
+		.sample_rate = 0,
+		.channels = 0,
+	};
+
+	struct dsdiff_chunk_header chunk_header;
+	if (!dsdiff_read_metadata(decoder, is, &metadata, &chunk_header))
+		return;
+
+	GError *error = NULL;
+	struct audio_format audio_format;
+	if (!audio_format_init_checked(&audio_format, metadata.sample_rate / 8,
+				       SAMPLE_FORMAT_S24_P32,
+				       metadata.channels, &error)) {
+		g_warning("%s", error->message);
+		g_error_free(error);
+		return;
+	}
+
+	/* initialize the dsd2pcm library */
+
+	dsd2pcm_ctx *dsd2pcm[MAX_CHANNELS];
+	for (unsigned i = 0; i < metadata.channels; ++i) {
+		dsd2pcm[i] = dsd2pcm_init();
+		if (dsd2pcm[i] == NULL) {
+			for (unsigned j = 0; j < i; ++j)
+				dsd2pcm_destroy(dsd2pcm[j]);
+			return;
+		}
+	}
+
+	/* success: file was recognized */
+
+	decoder_initialized(decoder, &audio_format, false, -1);
+
+	/* every iteration of the following loop decodes one "DSD"
+	   chunk */
+
+	while (true) {
+		uint64_t chunk_size = dsdiff_chunk_size(&chunk_header);
+
+		if (dsdiff_id_equals(&chunk_header.id, "DSD ")) {
+			if (!dsdiff_decode_chunk(decoder, is,
+						 metadata.channels,
+						 dsd2pcm, chunk_size))
+				break;
+		} else {
+			/* ignore other chunks */
+
+			if (!dsdiff_skip(decoder, is, chunk_size))
+				break;
+		}
+
+		/* read next chunk header; the first one was read by
+		   dsdiff_read_metadata() */
+
+		if (!dsdiff_read_chunk_header(decoder, is, &chunk_header))
+			break;
+	}
+
+	for (unsigned i = 0; i < metadata.channels; ++i)
+		dsd2pcm_destroy(dsd2pcm[i]);
+}
+
+static struct tag *
+dsdiff_stream_tag(struct input_stream *is)
+{
+	struct dsdiff_metadata metadata = {
+		.sample_rate = 0,
+		.channels = 0,
+	};
+
+	struct dsdiff_chunk_header chunk_header;
+	if (!dsdiff_read_metadata(NULL, is, &metadata, &chunk_header))
+		return NULL;
+
+	struct audio_format audio_format;
+	if (!audio_format_init_checked(&audio_format, metadata.sample_rate / 8,
+				       SAMPLE_FORMAT_S24_P32,
+				       metadata.channels, NULL))
+		/* refuse to parse files which we cannot play anyway */
+		return NULL;
+
+	/* no total time estimate, no tags implemented yet */
+	return tag_new();
+}
+
+static const char *const dsdiff_suffixes[] = {
+	"dff",
+	NULL
+};
+
+static const char *const dsdiff_mime_types[] = {
+	"application/x-dff",
+	NULL
+};
+
+const struct decoder_plugin dsdiff_decoder_plugin = {
+	.name = "dsdiff",
+	.stream_decode = dsdiff_stream_decode,
+	.stream_tag = dsdiff_stream_tag,
+	.suffixes = dsdiff_suffixes,
+	.mime_types = dsdiff_mime_types,
+};
diff --git a/src/decoder/dsdiff_decoder_plugin.h b/src/decoder/dsdiff_decoder_plugin.h
new file mode 100644
index 000000000..34e1438de
--- /dev/null
+++ b/src/decoder/dsdiff_decoder_plugin.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2011 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_DECODER_DSDIFF_H
+#define MPD_DECODER_DSDIFF_H
+
+extern const struct decoder_plugin dsdiff_decoder_plugin;
+
+#endif
diff --git a/src/decoder_list.c b/src/decoder_list.c
index 11da3f63c..2029b3e62 100644
--- a/src/decoder_list.c
+++ b/src/decoder_list.c
@@ -24,6 +24,7 @@
 #include "conf.h"
 #include "mpd_error.h"
 #include "decoder/pcm_decoder_plugin.h"
+#include "decoder/dsdiff_decoder_plugin.h"
 
 #include <glib.h>
 
@@ -70,6 +71,7 @@ const struct decoder_plugin *const decoder_plugins[] = {
 #ifdef HAVE_AUDIOFILE
 	&audiofile_decoder_plugin,
 #endif
+	&dsdiff_decoder_plugin,
 #ifdef HAVE_FAAD
 	&faad_decoder_plugin,
 #endif