diff --git a/Makefile.am b/Makefile.am index 7fb6dc15f..38419275e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1074,6 +1074,8 @@ DECODER_LIBS = \ if ENABLE_DSD libdecoder_a_SOURCES += \ + src/decoder/plugins/HybridDsdDecoderPlugin.cxx \ + src/decoder/plugins/HybridDsdDecoderPlugin.hxx \ src/decoder/plugins/DsdiffDecoderPlugin.cxx \ src/decoder/plugins/DsdiffDecoderPlugin.hxx \ src/decoder/plugins/DsfDecoderPlugin.cxx \ diff --git a/NEWS b/NEWS index 01d477197..43f6b8382 100644 --- a/NEWS +++ b/NEWS @@ -12,6 +12,7 @@ ver 0.21 (not yet released) - new tags "OriginalDate", "MUSICBRAINZ_WORKID" * decoder - gme: try loading m3u sidecar files + - hybrid_dsd: new decoder plugin - pcm: support audio/L24 (RFC 3190) * resampler - soxr: flush resampler at end of song diff --git a/doc/user.xml b/doc/user.xml index bbe8f6cdb..a123a6abe 100644 --- a/doc/user.xml +++ b/doc/user.xml @@ -2762,6 +2762,19 @@ run +
+ <varname>hybrid_dsd</varname> + + + Hybrid-DSD is a MP4 container file + (*.m4a) which contains both ALAC and + DSD data. It is disabled by default, and works only if you + explicitly enable it. Without this plugin, the ALAC parts + gets handled by the FFmpeg + decoder plugin. + +
+
<varname>mad</varname> diff --git a/src/decoder/DecoderList.cxx b/src/decoder/DecoderList.cxx index 58c0f0973..9da62bcce 100644 --- a/src/decoder/DecoderList.cxx +++ b/src/decoder/DecoderList.cxx @@ -26,6 +26,7 @@ #include "plugins/PcmDecoderPlugin.hxx" #include "plugins/DsdiffDecoderPlugin.hxx" #include "plugins/DsfDecoderPlugin.hxx" +#include "plugins/HybridDsdDecoderPlugin.hxx" #include "plugins/FlacDecoderPlugin.h" #include "plugins/OpusDecoderPlugin.h" #include "plugins/VorbisDecoderPlugin.h" @@ -73,6 +74,7 @@ const struct DecoderPlugin *const decoder_plugins[] = { #ifdef ENABLE_DSD &dsdiff_decoder_plugin, &dsf_decoder_plugin, + &hybrid_dsd_decoder_plugin, #endif #ifdef ENABLE_FAAD &faad_decoder_plugin, diff --git a/src/decoder/plugins/HybridDsdDecoderPlugin.cxx b/src/decoder/plugins/HybridDsdDecoderPlugin.cxx new file mode 100644 index 000000000..d26a5c721 --- /dev/null +++ b/src/decoder/plugins/HybridDsdDecoderPlugin.cxx @@ -0,0 +1,240 @@ +/* + * Copyright 2003-2017 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 "HybridDsdDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "input/InputStream.hxx" +#include "system/ByteOrder.hxx" +#include "util/Domain.hxx" +#include "util/WritableBuffer.hxx" +#include "util/StaticFifoBuffer.hxx" +#include "Log.hxx" + +#include + +static constexpr Domain hybrid_dsd_domain("hybrid_dsd"); + +namespace { + +static bool +InitHybridDsdDecoder(const ConfigBlock &block) +{ + if (block.GetBlockParam("enabled") == nullptr) { + LogInfo(hybrid_dsd_domain, + "The Hybrid DSD decoder is disabled because it was not explicitly enabled"); + return false; + } + + return true; +} + +struct UnsupportedFile {}; + +struct Mp4ChunkHeader { + uint32_t size; + char type[4]; +}; + +void +ReadFull(DecoderClient &client, InputStream &input, + WritableBuffer dest) +{ + while (!dest.empty()) { + size_t nbytes = client.Read(input, dest.data, dest.size); + if (nbytes == 0) + throw UnsupportedFile(); + + dest.skip_front(nbytes); + } +} + +void +ReadFull(DecoderClient &client, InputStream &input, WritableBuffer dest) +{ + ReadFull(client, input, WritableBuffer::FromVoid(dest)); +} + +template +T +ReadFullT(DecoderClient &client, InputStream &input) +{ + T dest; + ReadFull(client, input, WritableBuffer(&dest, sizeof(dest))); + return dest; +} + +Mp4ChunkHeader +ReadHeader(DecoderClient &client, InputStream &input) +{ + return ReadFullT(client, input); +} + +uint32_t +ReadBE32(DecoderClient &client, InputStream &input) +{ + return FromBE32(ReadFullT(client, input)); +} + +} /* anonymous namespace */ + +static std::pair +FindHybridDsdData(DecoderClient &client, InputStream &input) +{ + auto audio_format = AudioFormat::Undefined(); + + while (true) { + auto header = ReadHeader(client, input); + const size_t header_size = FromBE32(header.size); + if (header_size < sizeof(header)) + throw UnsupportedFile(); + + size_t remaining = header_size - sizeof(header); + if (memcmp(header.type, "bphv", 4) == 0) { + /* ? */ + if (remaining != 4 || ReadBE32(client, input) != 1) + throw UnsupportedFile(); + remaining -= 4; + + audio_format.format = SampleFormat::DSD; + } else if (memcmp(header.type, "bphc", 4) == 0) { + /* channel count */ + if (remaining != 4) + throw UnsupportedFile(); + + auto channels = ReadBE32(client, input); + remaining -= 4; + + if (!audio_valid_channel_count(channels)) + throw UnsupportedFile(); + + audio_format.channels = channels; + } else if (memcmp(header.type, "bphr", 4) == 0) { + /* (bit) sample rate */ + + if (remaining != 4) + throw UnsupportedFile(); + + auto sample_rate = ReadBE32(client, input) / 8; + remaining -= 4; + + if (!audio_valid_sample_rate(sample_rate)) + throw UnsupportedFile(); + + audio_format.sample_rate = sample_rate; + } else if (memcmp(header.type, "bphf", 4) == 0) { + /* ? */ + if (remaining != 4 || ReadBE32(client, input) != 0) + throw UnsupportedFile(); + remaining -= 4; + } else if (memcmp(header.type, "bphd", 4) == 0) { + /* the actual DSD data */ + if (!audio_format.IsValid()) + throw UnsupportedFile(); + + return std::make_pair(audio_format, remaining); + } + + input.LockSkip(remaining); + } +} + +static void +HybridDsdDecode(DecoderClient &client, InputStream &input) +{ + if (!input.CheapSeeking()) + /* probe only if seeking is cheap, i.e. not for HTTP + streams */ + return; + + offset_type remaining_bytes; + size_t frame_size; + + try { + auto result = FindHybridDsdData(client, input); + auto duration = SignedSongTime::FromS(result.second / result.first.GetTimeToSize()); + client.Ready(result.first, + /* TODO: implement seeking */ false, + duration); + frame_size = result.first.GetFrameSize(); + remaining_bytes = result.second; + } catch (UnsupportedFile) { + return; + } + + StaticFifoBuffer buffer; + + auto cmd = client.GetCommand(); + while (remaining_bytes > 0) { + switch (cmd) { + case DecoderCommand::NONE: + case DecoderCommand::START: + break; + + case DecoderCommand::STOP: + return; + + case DecoderCommand::SEEK: + // TODO: implement seeking + break; + } + + auto w = buffer.Write(); + if (!w.empty()) { + if (remaining_bytes < (1<<30ull) && + w.size > size_t(remaining_bytes)) + w.size = remaining_bytes; + + const size_t nbytes = client.Read(input, + w.data, w.size); + if (nbytes == 0) + return; + + remaining_bytes -= nbytes; + buffer.Append(nbytes); + } + + auto r = buffer.Read(); + auto n_frames = r.size / frame_size; + if (n_frames > 0) { + cmd = client.SubmitData(input, r.data, + n_frames * frame_size, + 0); + buffer.Consume(n_frames * frame_size); + } + } +} + +static const char *const hybrid_dsd_suffixes[] = { + "m4a", + nullptr +}; + +const struct DecoderPlugin hybrid_dsd_decoder_plugin = { + "hybrid_dsd", + InitHybridDsdDecoder, + nullptr, + HybridDsdDecode, + nullptr, + nullptr, + nullptr, + nullptr, + hybrid_dsd_suffixes, + nullptr, +}; diff --git a/src/decoder/plugins/HybridDsdDecoderPlugin.hxx b/src/decoder/plugins/HybridDsdDecoderPlugin.hxx new file mode 100644 index 000000000..3c4cf9f70 --- /dev/null +++ b/src/decoder/plugins/HybridDsdDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright 2003-2018 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_HYBRID_DSD_HXX +#define MPD_DECODER_HYBRID_DSD_HXX + +extern const struct DecoderPlugin hybrid_dsd_decoder_plugin; + +#endif