Repository navigation
Send DSD files to Sendspin players as DoP (DSD over PCM) - #7429
manuel-alcocer wants to merge 4 commits into
Conversation
Sendspin only carries PCM, FLAC and Opus, so a DSD file is always decoded to PCM. DoP packs the raw DSD bits into 24-bit PCM frames, which a DAC that supports DoP plays back as native DSD, as long as every sample arrives untouched. Add an advanced per-player option, "Send DSD as DoP" (Sendspin, automatic format). When it is on and the player plays on its own: - a flow that starts with a local DSF file runs at its DoP rate (176.4 kHz for DSD64, 352.8 kHz for DSD128) in F32, which holds every 24-bit DoP word exactly - the flow restarts whenever a DSD track needs a different DoP rate - DSD tracks are streamed straight from the file as DoP, bypassing the audio buffer, volume normalization, fades and the crossfade prefetch - crossfades into or out of a DSD track are disabled numpy is only imported when a DoP stream starts, so start-up is unaffected.
|
✅ The title and description are good to go. Thanks! |
Merging this PR will not alter performance
Comparing Footnotes
|
Bot-MArley
left a comment
There was a problem hiding this comment.
Note
This comment was written by Claude and posted by Bot-MArley on behalf of the Music Assistant maintainers.
Thanks for the DoP work. It sends DSD to a Sendspin player untouched and falls back to PCM otherwise, the new helper is tested, and nothing changes for players with the option off. One problem needs fixing before this can merge.
Requested changes
- [PROBLEM]
dop_enabled_for_playersturns DoP off for groups, but not for players where something else changes the samples: DSP enabled on the player, a non-stereooutput_channelssetting, or an active audio overlay. In those cases the stream goes through a filter or mix and the DAC plays the DoP data as full-scale noise. Please also require that none of these apply before enabling DoP, and add a test.
| return bool(players) and all( | ||
| self.mass.config.get_raw_player_config_value(player.player_id, CONF_DSD_DOP, False) | ||
| is True | ||
| and len(player.group_members) <= 1 |
There was a problem hiding this comment.
[PROBLEM] This only rules out groups. DoP also has to be off when DSP is enabled on the player, when output_channels is not stereo, and while an audio overlay is mixed into the flow stream, because each of those alters the samples and turns DoP into noise on the DAC. Please add those checks here and cover them with a test.
There was a problem hiding this comment.
Done in 46a511b: dop_enabled_for_players now also requires that DSP is off and output_channels is stereo for every player, and DoP is skipped while a queue audio overlay is active (in select_flow_pcm_format, in the flow stream and in queue_item_plays_as_dop). Covered by test_flow_format_ignores_dop_with_dsp, test_flow_format_ignores_dop_with_channel_mapping and test_flow_format_ignores_dop_with_an_overlay.
There was a problem hiding this comment.
Note
This comment was written by Claude and posted by Bot-MArley on behalf of the Music Assistant maintainers.
Thanks, this looks resolved on the current head. DoP is now skipped when DSP is enabled or output_channels is left/right/mono (_player_processes_audio via dop_enabled_for_players), and while an overlay is active in the format selection, the flow stream and queue_item_plays_as_dop. The three new tests cover each case.
|
Note This comment was written by Claude and posted by Bot-MArley on behalf of the Music Assistant maintainers. Changes in this area must be tested on real hardware before they can be merged. Have you tested this on a real DSD-capable Sendspin player? If so, please say what you tested. If you have one, the Music Assistant DEV SERVER app is an easy way to try this PR with it: stop your Music Assistant app, install the DEV app, set its server_repo option to pr-7429 and start it (see its README). |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical DoP flaws risk corrupted playback and loud DAC noise through incompatible output, audio processing, and unsafe group transitions, while direct file access bypasses source-folder boundary checks.
6 open issues
Reject DoP for clients limited to 16-bit output · New Restart DoP playback as PCM when adding a group member · New DoP must bypass DSP, channel mapping, and queue overlays · New Open DoP files through the validated filesystem descriptor · New Preserve DoP marker phase across gapless track boundaries · New Make track preparation DoP-aware to avoid unused PCM buffers · New
What changed in this PR
Adds opt-in DSD-over-PCM (DoP) playback for local DSF files on standalone Sendspin players, allowing compatible DACs to receive DSD without PCM decoding.
Changes:
- Adds an advanced per-player DoP setting and explanatory text.
- Implements DSF parsing and DoP packing, with flow-rate selection and processing bypasses.
- Adds tests for packing accuracy, seeking, format selection, and flow restarts.
| File | Description |
|---|---|
tests/helpers/test_dop.py |
Tests DSF parsing, markers, bit-exact packing, padding, and seeking. |
tests/controllers/streams/test_flow_dop.py |
Tests DoP rate selection, eligibility, and transitions. |
music_assistant/translations/en.json |
Adds English DoP configuration text. |
music_assistant/providers/sendspin/strings.json |
Defines the DoP setting label and safety guidance. |
music_assistant/providers/sendspin/player.py |
Exposes the advanced DoP option for automatic format selection. |
music_assistant/helpers/dop.py |
Implements local DSF reading and DoP encoding. |
music_assistant/controllers/streams/audio.py |
Integrates DoP into flow playback and crossfade handling. |
music_assistant/constants.py |
Defines the DoP configuration key. |
Review effort: Balanced
|
🚧 Automated critical-issue gate — this PR has an unresolved Maintainers: add the |
A DoP track is read straight from its DSF file, but the queue loader still created an AudioBuffer for the first track and the stream feeder warmed one for the next. That buffer holds decoded PCM at the probed DSD rate / 8 in 32-bit: 705.6 kHz for DSD128, about 5.6 MB/s, so up to ~1.7 GB for a five minute track that nothing ever reads. Two of them were enough to push the server past a 4 GiB memory limit. - add StreamsAudio.queue_item_plays_as_dop(), which checks the queue's active output protocol player (or a linked one before playback picks it) - skip the start buffer in the queue loader and the next-track warm-up in the stream feeder for DoP tracks; a track that ends up decoded creates its buffer on demand, as before - release any buffer already attached when the DoP stream starts
Review follow-up for the DoP path: - Require a lossless stereo output of at least 24 bits at the DoP rate. New Player.supports_dop_output() (False by default); Sendspin answers it from the format a session at that rate actually negotiates (automatic format, top codec PCM/FLAC, stereo, >= 24 bits). A 16-bit-only or Opus client now gets decoded PCM instead of a truncated DoP payload. - Never use DoP when the samples get processed: DSP enabled on the player, a non-stereo output_channels mapping, or an active queue audio overlay. - Restart as decoded PCM when a member joins a player that is streaming DoP. The leader stops its session before the member is attached, so no DoP audio is backfilled to it, then resumes the queue at the same position; the grouped player no longer qualifies for DoP. - Read the DSF through the descriptor the filesystem provider opened and checked (open_local_files), with positional reads only, so a path swapped after stream details were resolved can not lead outside the source folder. - Keep the DoP markers alternating across gapless tracks: the marker phase is carried from one DoP item into the next within a flow and reset after decoded PCM.
|
FYI, created an extended feature request discussion partially based on this pull request, asking for DSD pass-through support: Please note that the linked feature request has a broader scope, also asking for native DSD support as well for MA's providers. |


What does this implement/fix?
I have a DAC that plays DSD natively and a collection of DSD files (.dsf), mostly SACD and vinyl transfers. When I play them on a Sendspin player today, Music Assistant decodes them to PCM, because Sendspin only carries PCM, FLAC and Opus. So the DAC never gets to play the DSD itself.
DoP (DSD over PCM) is the usual way around this: the DSD bits are packed into 24-bit PCM frames with a marker byte, and a DAC that understands DoP recognises the markers and switches to native DSD playback. The catch is that it only works if every sample reaches the DAC exactly as it was sent. Any resampling, volume change, normalization or crossfade turns it into loud noise.
This PR adds an advanced, per-player option for Sendspin players, "Send DSD as DoP". It is off by default and only shown when the format is set to automatic. When it is on and the player is playing on its own (not in a group):
PCM tracks in the same queue keep playing exactly as before. numpy is only imported when a DoP stream actually starts, so server start-up is not affected.
On the player side, the Sendspin CLI needs to offer the DoP rates and leave DoP untouched by its software volume. That is in Sendspin/sendspin-python-cli#324. For playback to stay fully bit-exact over time, the client should also avoid drift correction, which is what
--clock-source dac(Sendspin/sendspin-python-cli#318) is for.Related issue (if applicable):
Types of changes
bugfixnew-featureenhancementnew-providerbreaking-changerefactordocumentationmaintenancecidependenciesChecklist
pre-commit run --all-filespasses.pytestpasses, and tests have been added/updated undertests/where applicable.music-assistant/modelsis linked.music-assistant/frontendis linked.