diff --git a/DOCS/interface-changes/audio-spdif-dsd.txt b/DOCS/interface-changes/audio-spdif-dsd.txt new file mode 100644 index 0000000000000..4b1a90d72b268 --- /dev/null +++ b/DOCS/interface-changes/audio-spdif-dsd.txt @@ -0,0 +1 @@ +add `dsd` choice to `--audio-spdif` for DSD-over-PCM (DoP) passthrough diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 2a359d4692d5b..c74b192f83ef8 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -2121,11 +2121,16 @@ Audio List of codecs for which compressed audio passthrough should be used. This works for both classic S/PDIF and HDMI. - Possible codecs are ``ac3``, ``dts``, ``dts-hd``, ``eac3``, ``truehd``. - Multiple codecs can be specified by separating them with ``,``. ``dts`` - refers to low bitrate DTS core, while ``dts-hd`` refers to DTS MA (receiver - and OS support varies). If both ``dts`` and ``dts-hd`` are specified, it - behaves equivalent to specifying ``dts-hd`` only. + Possible codecs are ``ac3``, ``dts``, ``dts-hd``, ``eac3``, ``truehd``, + ``dsd``. Multiple codecs can be specified by separating them with ``,``. + ``dts`` refers to low bitrate DTS core, while ``dts-hd`` refers to DTS MA + (receiver and OS support varies). If both ``dts`` and ``dts-hd`` are + specified, it behaves equivalent to specifying ``dts-hd`` only. + + ``dsd`` enables bit-perfect passthrough of DSD audio, requires an audio + output with exclusive device access (currently ``wasapi``) and a DAC that + accepts DoP at the resulting PCM rate (176.4 kHz for DSD64, 352.8 kHz for + DSD128, and so on). In earlier mpv versions you could use ``--ad`` to force the spdif wrapper. This does not work anymore. diff --git a/audio/aframe.c b/audio/aframe.c index fbba7a3b10445..3c93f289610b6 100644 --- a/audio/aframe.c +++ b/audio/aframe.c @@ -32,7 +32,8 @@ struct mp_aframe { AVFrame *av_frame; // We support channel layouts different from AVFrame channel masks struct mp_chmap chmap; - // We support spdif formats, which are allocated as AV_SAMPLE_FMT_S16. + // We support bitstream formats, which are allocated as fake integer PCM + // of matching sample size. int format; double pts; double speed; @@ -306,9 +307,10 @@ bool mp_aframe_set_format(struct mp_aframe *frame, int format) return false; enum AVSampleFormat av_format = af_to_avformat(format); if (av_format == AV_SAMPLE_FMT_NONE && format) { - if (!af_fmt_is_spdif(format)) + if (af_fmt_is_pcm(format)) return false; - av_format = AV_SAMPLE_FMT_S16; + av_format = af_fmt_to_bytes(format) == 4 ? AV_SAMPLE_FMT_S32 + : AV_SAMPLE_FMT_S16; } frame->format = format; frame->av_frame->format = av_format; @@ -488,7 +490,7 @@ double mp_aframe_duration(struct mp_aframe *f) // Clip the given frame to the given timestamp range. Adjusts the frame size // and timestamp. -// Refuses to change spdif frames. +// Refuses to change bitstream (spdif/DoP) frames. void mp_aframe_clip_timestamps(struct mp_aframe *f, double start, double end) { double f_end = mp_aframe_end_pts(f); @@ -500,7 +502,7 @@ void mp_aframe_clip_timestamps(struct mp_aframe *f, double start, double end) if (f->pts >= end) { f->av_frame->nb_samples = 0; } else { - if (af_fmt_is_spdif(mp_aframe_get_format(f))) + if (!af_fmt_is_pcm(mp_aframe_get_format(f))) return; int new = (end - f->pts) * rate; f->av_frame->nb_samples = MPCLAMP(new, 0, f->av_frame->nb_samples); @@ -513,7 +515,7 @@ void mp_aframe_clip_timestamps(struct mp_aframe *f, double start, double end) f->av_frame->nb_samples = 0; f->pts = f_end; } else { - if (af_fmt_is_spdif(mp_aframe_get_format(f))) + if (!af_fmt_is_pcm(mp_aframe_get_format(f))) return; int skip = (start - f->pts) * rate; skip = MPCLAMP(skip, 0, f->av_frame->nb_samples); diff --git a/audio/decode/ad_dsd.c b/audio/decode/ad_dsd.c new file mode 100644 index 0000000000000..721e2678a025b --- /dev/null +++ b/audio/decode/ad_dsd.c @@ -0,0 +1,383 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include +#include +#include + +#include +#include + +#include "audio/aframe.h" +#include "audio/chmap.h" +#include "audio/chmap_avchannel.h" +#include "audio/format.h" +#include "common/av_common.h" +#include "common/codecs.h" +#include "common/common.h" +#include "common/msg.h" +#include "demux/packet.h" +#include "demux/stheader.h" +#include "filters/f_decoder_wrapper.h" +#include "filters/filter_internal.h" +#include "misc/bstr.h" +#include "osdep/compiler.h" + +// Each output sample carries one marker byte and 16 DSD bits per channel: +// (marker << 24) | (dsd0 << 16) | (dsd1 << 8), with the oldest DSD bit in +// the MSB and the marker alternating between 0x05 and 0xfa every sample. +// See + +struct priv { + bool planar; + int rate; // output PCM rate (DSD byte rate / 2) + struct mp_chmap chmap; + struct mp_aframe *fmt; + struct mp_aframe_pool *pool; + uint8_t bits[256]; // maps input byte to MSB-first DSD bits + bool marker; + bool have_hold; + uint8_t hold[MP_NUM_CHANNELS]; // odd trailing byte carried between packets + bool warned_align; + + // DST decompression (lossless DST -> DSD) + AVCodecContext *avctx; + AVPacket *avpkt; + AVFrame *avframe; + + struct mp_decoder public; +}; + +static uint8_t reverse_bits8(uint8_t v) +{ +#if __has_builtin(__builtin_bitreverse8) + return __builtin_bitreverse8(v); +#else + v = (v & 0xf0) >> 4 | (v & 0x0f) << 4; + v = (v & 0xcc) >> 2 | (v & 0x33) << 2; + v = (v & 0xaa) >> 1 | (v & 0x55) << 1; + return v; +#endif +} + +// Per-channel input byte stream: the held byte (if any), then packet data. +static inline uint8_t in_byte(struct priv *p, const uint8_t *plane, int step, + int c, int i) +{ + if (p->have_hold) { + if (i == 0) + return p->hold[c]; + i -= 1; + } + return p->bits[plane[(size_t)i * step]]; +} + +// Convert per_ch DSD bytes per channel to DoP and output the result. +// planes[c] points at the first byte of channel c, consecutive bytes of one +// channel are step bytes apart. +static void emit_frame(struct mp_filter *da, const uint8_t *const *planes, + int step, int per_ch, double pts) +{ + struct priv *p = da->priv; + int nch = p->chmap.num; + int total = per_ch + (p->have_hold ? 1 : 0); + int frames = total / 2; + + if (!frames) { + if (per_ch) { + for (int c = 0; c < nch; c++) + p->hold[c] = in_byte(p, planes[c], step, c, total - 1); + p->have_hold = true; + } + mp_filter_internal_mark_progress(da); + return; + } + + if (pts != MP_NOPTS_VALUE && p->have_hold) + pts -= 0.5 / p->rate; + + struct mp_aframe *out = mp_aframe_new_ref(p->fmt); + if (mp_aframe_pool_allocate(p->pool, out, frames) < 0) + goto fail; + + uint8_t **data = mp_aframe_get_data_rw(out); + if (!data) + goto fail; + uint32_t *dst = (uint32_t *)data[0]; + + for (int f = 0; f < frames; f++) { + uint32_t marker = p->marker ? 0xfa : 0x05; + p->marker = !p->marker; + for (int c = 0; c < nch; c++) { + uint32_t b0 = in_byte(p, planes[c], step, c, f * 2); + uint32_t b1 = in_byte(p, planes[c], step, c, f * 2 + 1); + *dst++ = marker << 24 | b0 << 16 | b1 << 8; + } + } + + bool hold = total & 1; + if (hold) { + uint8_t tmp[MP_NUM_CHANNELS]; + for (int c = 0; c < nch; c++) + tmp[c] = in_byte(p, planes[c], step, c, total - 1); + memcpy(p->hold, tmp, nch); + } + p->have_hold = hold; + + mp_aframe_set_pts(out, pts); + mp_pin_in_write(da->ppins[1], MAKE_FRAME(MP_FRAME_AUDIO, out)); + return; + +fail: + talloc_free(out); + mp_filter_internal_mark_failed(da); +} + +static void process_dst(struct mp_filter *da, struct demux_packet *mpkt) +{ + struct priv *p = da->priv; + int nch = p->chmap.num; + + mp_set_av_packet(p->avpkt, mpkt, NULL); + int ret = avcodec_send_packet(p->avctx, p->avpkt); + if (ret >= 0) + ret = avcodec_receive_frame(p->avctx, p->avframe); + if (ret < 0) { + MP_WARN(da, "Error decoding DST frame.\n"); + mp_filter_internal_mark_progress(da); + return; + } + + if (p->avframe->format != AV_SAMPLE_FMT_U8 || + p->avframe->ch_layout.nb_channels != nch) + { + MP_ERR(da, "Unexpected DST decoder output.\n"); + av_frame_unref(p->avframe); + mp_filter_internal_mark_failed(da); + return; + } + + const uint8_t *planes[MP_NUM_CHANNELS]; + for (int c = 0; c < nch; c++) + planes[c] = p->avframe->data[0] + c; + emit_frame(da, planes, nch, p->avframe->nb_samples, mpkt->pts); + + av_frame_unref(p->avframe); +} + +static void process(struct mp_filter *da) +{ + struct priv *p = da->priv; + + if (!mp_pin_can_transfer_data(da->ppins[1], da->ppins[0])) + return; + + struct mp_frame inframe = mp_pin_out_read(da->ppins[0]); + if (inframe.type == MP_FRAME_EOF) { + // A held byte is half a DoP frame; nothing useful can be output. + p->have_hold = false; + mp_pin_in_write(da->ppins[1], inframe); + return; + } else if (inframe.type != MP_FRAME_PACKET) { + if (inframe.type) { + MP_ERR(da, "unknown frame type\n"); + mp_filter_internal_mark_failed(da); + } + return; + } + + struct demux_packet *mpkt = inframe.data; + + if (p->avctx) { + process_dst(da, mpkt); + } else { + int nch = p->chmap.num; + int per_ch = (int)MPMIN(mpkt->len / nch, INT_MAX / 2); + + if (mpkt->len % nch && !p->warned_align) { + MP_WARN(da, "Packet not aligned to channel count.\n"); + p->warned_align = true; + } + + const uint8_t *planes[MP_NUM_CHANNELS]; + for (int c = 0; c < nch; c++) + planes[c] = mpkt->buffer + (p->planar ? c * (size_t)per_ch : c); + emit_frame(da, planes, p->planar ? 1 : nch, per_ch, mpkt->pts); + } + + talloc_free(mpkt); +} + +static void reset(struct mp_filter *da) +{ + struct priv *p = da->priv; + + p->have_hold = false; + p->marker = false; + if (p->avctx) + avcodec_flush_buffers(p->avctx); +} + +static void destroy(struct mp_filter *da) +{ + struct priv *p = da->priv; + + avcodec_free_context(&p->avctx); + mp_free_av_packet(&p->avpkt); + av_frame_free(&p->avframe); +} + +static const struct { + const char *codec; + bool lsbf, planar, dst; +} dsd_codecs[] = { + {"dsd_lsbf", true, false, false}, + {"dsd_lsbf_planar", true, true, false}, + {"dsd_msbf", false, false, false}, + {"dsd_msbf_planar", false, true, false}, + {"dst", false, false, true}, +}; + +// codec is the libavcodec name of the source audio codec. +// pref is the ","-separated list from --audio-spdif; the "dsd" entry enables +// DoP pass-through for all raw DSD and DST codecs. +struct mp_decoder_list *select_dsd_codec(const char *codec, const char *pref) +{ + struct mp_decoder_list *list = talloc_zero(NULL, struct mp_decoder_list); + + if (!codec) + return list; + + bool found = false; + for (int n = 0; n < MP_ARRAY_SIZE(dsd_codecs); n++) + found |= strcmp(dsd_codecs[n].codec, codec) == 0; + if (!found) + return list; + + bstr sel = bstr0(pref); + while (sel.len) { + bstr entry; + bstr_split_tok(sel, ",", &entry, &sel); + if (bstr_equals0(entry, "dsd")) { + mp_add_decoder(list, codec, "dop", + "DSD to DSD-over-PCM (DoP) pass-through decoder"); + break; + } + } + return list; +} + +static const struct mp_filter_info ad_dsd_filter = { + .name = "ad_dsd", + .priv_size = sizeof(struct priv), + .process = process, + .reset = reset, + .destroy = destroy, +}; + +static bool init_dst_decoder(struct mp_filter *da) +{ + struct priv *p = da->priv; + + const AVCodec *dec = avcodec_find_decoder(AV_CODEC_ID_DST); + if (!dec) + return false; + + p->avctx = avcodec_alloc_context3(dec); + p->avpkt = av_packet_alloc(); + p->avframe = av_frame_alloc(); + if (!p->avctx || !p->avpkt || !p->avframe) + return false; + + p->avctx->sample_rate = p->rate * 2; + mp_chmap_to_av_layout(&p->avctx->ch_layout, &p->chmap); + + if (av_opt_set_int(p->avctx, "raw_dsd", 1, AV_OPT_SEARCH_CHILDREN) < 0) { + MP_ERR(da, "FFmpeg DST decoder does not support raw DSD output.\n"); + return false; + } + + if (avcodec_open2(p->avctx, dec, NULL) < 0 || + p->avctx->sample_fmt != AV_SAMPLE_FMT_U8) + return false; + + return true; +} + +static struct mp_decoder *create(struct mp_filter *parent, + struct mp_codec_params *codec, + const char *decoder) +{ + struct mp_filter *da = mp_filter_create(parent, &ad_dsd_filter); + if (!da) + return NULL; + + mp_filter_add_pin(da, MP_PIN_IN, "in"); + mp_filter_add_pin(da, MP_PIN_OUT, "out"); + + da->log = mp_log_new(da, parent->log, NULL); + + struct priv *p = da->priv; + p->public.f = da; + + bool lsbf = false, dst = false, found = false; + for (int n = 0; n < MP_ARRAY_SIZE(dsd_codecs); n++) { + if (codec->codec && strcmp(dsd_codecs[n].codec, codec->codec) == 0) { + lsbf = dsd_codecs[n].lsbf; + p->planar = dsd_codecs[n].planar; + dst = dsd_codecs[n].dst; + found = true; + break; + } + } + + // codec->samplerate is the DSD byte rate. DSD64 (2.8224 MHz) is the + // lowest rate DoP is defined for. + if (!found || codec->samplerate < 352800 || codec->samplerate % 2 || + !mp_chmap_is_valid(&codec->channels)) + { + MP_ERR(da, "Codec parameters unsuitable for DoP.\n"); + goto fail; + } + + p->rate = codec->samplerate / 2; + p->chmap = codec->channels; + + if (dst && !init_dst_decoder(da)) + goto fail; + + for (int n = 0; n < 256; n++) + p->bits[n] = lsbf ? reverse_bits8(n) : n; + + p->fmt = mp_aframe_create(); + talloc_steal(p, p->fmt); + mp_aframe_set_chmap(p->fmt, &p->chmap); + mp_aframe_set_format(p->fmt, AF_FORMAT_S_DOP); + mp_aframe_set_rate(p->fmt, p->rate); + + p->pool = mp_aframe_pool_create(p); + + return &p->public; + +fail: + talloc_free(da); + return NULL; +} + +const struct mp_decoder_fns ad_dsd = { + .create = create, +}; diff --git a/audio/format.c b/audio/format.c index 4441456eded9c..efa22c8c35e42 100644 --- a/audio/format.c +++ b/audio/format.c @@ -33,6 +33,8 @@ int af_fmt_to_bytes(int format) case AF_FORMAT_FLOAT: return 4; case AF_FORMAT_DOUBLE: return 8; } + if (format == AF_FORMAT_S_DOP) + return 4; if (af_fmt_is_spdif(format)) return 2; return 0; @@ -53,9 +55,10 @@ bool af_fmt_is_float(int format) // true for both unsigned and signed ints bool af_fmt_is_int(int format) { - return format && !af_fmt_is_spdif(format) && !af_fmt_is_float(format); + return format && af_fmt_is_pcm(format) && !af_fmt_is_float(format); } +// true for IEC61937 wrapped formats only (not other bitstream formats) bool af_fmt_is_spdif(int format) { return af_format_sample_alignment(format) > 1; @@ -63,7 +66,8 @@ bool af_fmt_is_spdif(int format) bool af_fmt_is_pcm(int format) { - return af_fmt_is_valid(format) && !af_fmt_is_spdif(format); + return af_fmt_is_valid(format) && !af_fmt_is_spdif(format) && + format != AF_FORMAT_S_DOP; } static const int planar_formats[][2] = { @@ -135,6 +139,7 @@ const char *af_fmt_to_str(int format) case AF_FORMAT_S_EAC3: return "spdif-eac3"; case AF_FORMAT_S_MP3: return "spdif-mp3"; case AF_FORMAT_S_TRUEHD: return "spdif-truehd"; + case AF_FORMAT_S_DOP: return "dop"; } return "??"; } diff --git a/audio/format.h b/audio/format.h index bdd4744ae3aea..ddd6af1660277 100644 --- a/audio/format.h +++ b/audio/format.h @@ -48,6 +48,10 @@ enum af_format { AF_FORMAT_S_MP3, AF_FORMAT_S_TRUEHD, + // DSD-over-PCM (DoP): 16 DSD bits and a marker byte in the 24 MSBs of + // 32-bit samples. + AF_FORMAT_S_DOP, + AF_FORMAT_COUNT }; diff --git a/audio/out/ao_wasapi_utils.c b/audio/out/ao_wasapi_utils.c index a1e116b6c66bc..f17809c757ead 100644 --- a/audio/out/ao_wasapi_utils.c +++ b/audio/out/ao_wasapi_utils.c @@ -145,6 +145,8 @@ static const struct wasapi_sample_fmt wasapi_formats[] = { {AF_FORMAT_S_TRUEHD, 16, 16, &KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP}, {AF_FORMAT_S_EAC3, 16, 16, &KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS}, {AF_FORMAT_S_DTSHD, 16, 16, &KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD}, + // DSD-over-PCM + {AF_FORMAT_S_DOP, 32, 24, &KSDATAFORMAT_SUBTYPE_PCM}, {0}, }; @@ -328,8 +330,9 @@ static bool set_ao_format(struct ao *ao, WAVEFORMATEX *wf, } // Do not touch the ao for passthrough, just assume that we set WAVEFORMATEX - // correctly. - if (af_fmt_is_pcm(format.mp_format)) { + // correctly. Checked on both sides, since the DoP wire format is plain + // 24-in-32 PCM and reverse maps to a PCM sample format. + if (af_fmt_is_pcm(format.mp_format) && af_fmt_is_pcm(ao->format)) { struct mp_chmap channels; if (!chmap_from_waveformat(&channels, wf)) { MP_ERR(ao, "Unable to construct channel map from WAVEFORMAT %s\n", @@ -474,6 +477,10 @@ static bool find_formats_exclusive(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat) if (try_format_exclusive(ao, wformat)) return true; + // DoP must be played back bit-exactly at the requested rate. + if (ao->format == AF_FORMAT_S_DOP) + return false; + if (af_fmt_is_spdif(ao->format)) { if (ao->format != AF_FORMAT_S_AC3) { // If the requested format failed and it is passthrough, but not @@ -576,7 +583,7 @@ static bool find_formats(struct ao *ao) WAVEFORMATEXTENSIBLE wformat; set_waveformat(&wformat, &wasapi_format, ao->samplerate, &channels); - if (state->opt_exclusive || af_fmt_is_spdif(ao->format)) { + if (state->opt_exclusive || !af_fmt_is_pcm(ao->format)) { share_mode = AUDCLNT_SHAREMODE_EXCLUSIVE; if(!find_formats_exclusive(ao, &wformat)) return false; diff --git a/filters/f_decoder_wrapper.c b/filters/f_decoder_wrapper.c index 2137b4b0d2152..8418eef5970f3 100644 --- a/filters/f_decoder_wrapper.c +++ b/filters/f_decoder_wrapper.c @@ -261,7 +261,7 @@ static int decoder_list_help(struct mp_log *log, const m_option_t *opt, return M_OPT_EXIT; } if (strcmp(opt->name, "audio-spdif") == 0) { - mp_info(log, "Choices: ac3,dts-hd,dts (and possibly more)\n"); + mp_info(log, "Choices: ac3,dts-hd,dts,dsd (and possibly more)\n"); return M_OPT_EXIT; } return 1; @@ -453,6 +453,14 @@ static bool reinit_decoder(struct priv *p) list = spdif; } else { talloc_free(spdif); + struct mp_decoder_list *dsd = + select_dsd_codec(p->codec->codec, p->opts->audio_spdif); + if (dsd->num_entries) { + driver = &ad_dsd; + list = dsd; + } else { + talloc_free(dsd); + } } } } diff --git a/filters/f_decoder_wrapper.h b/filters/f_decoder_wrapper.h index a352dc0c4dd88..92718e26ab29f 100644 --- a/filters/f_decoder_wrapper.h +++ b/filters/f_decoder_wrapper.h @@ -106,6 +106,7 @@ struct mp_decoder_fns { extern const struct mp_decoder_fns vd_lavc; extern const struct mp_decoder_fns ad_lavc; extern const struct mp_decoder_fns ad_spdif; +extern const struct mp_decoder_fns ad_dsd; // Convenience wrapper for lavc based decoders. Treat lavc_state as private; // init to all-0 on init and resets. @@ -119,3 +120,6 @@ void lavc_process(struct mp_filter *f, struct lavc_state *state, // ad_spdif.c struct mp_decoder_list *select_spdif_codec(const char *codec, const char *pref); + +// ad_dsd.c +struct mp_decoder_list *select_dsd_codec(const char *codec, const char *pref); diff --git a/meson.build b/meson.build index 80bd39d7ec3bf..ec432b8e75fe7 100644 --- a/meson.build +++ b/meson.build @@ -60,6 +60,7 @@ sources = files( 'audio/chmap.c', 'audio/chmap_avchannel.c', 'audio/chmap_sel.c', + 'audio/decode/ad_dsd.c', 'audio/decode/ad_lavc.c', 'audio/decode/ad_spdif.c', 'audio/filter/af_drop.c', diff --git a/player/audio.c b/player/audio.c index b3e975dd2eda3..7dc19158c9340 100644 --- a/player/audio.c +++ b/player/audio.c @@ -416,7 +416,7 @@ static int reinit_audio_filters_and_output(struct MPContext *mpctx) mp_aframe_get_chmap(out_fmt, &out_channels); int ao_flags = 0; - bool spdif_fallback = af_fmt_is_spdif(out_format) && + bool spdif_fallback = !af_fmt_is_pcm(out_format) && ao_c->spdif_passthrough; if (opts->ao_null_fallback && !spdif_fallback) @@ -454,7 +454,7 @@ static int reinit_audio_filters_and_output(struct MPContext *mpctx) ao_get_format(mpctx->ao, &ao_rate, &ao_format, &ao_channels); // Verify passthrough format was not changed. - if (mpctx->ao && af_fmt_is_spdif(out_format)) { + if (mpctx->ao && !af_fmt_is_pcm(out_format)) { if (out_rate != ao_rate || out_format != ao_format || !mp_chmap_equals(&out_channels, &ao_channels)) {