Skip to content

feat(audio): native DSD via DoP (DSD over PCM) on WASAPI Exclusive - #496

Draft
InstaZDLL wants to merge 12 commits into
mainfrom
feat/dsd-dop-495
Draft

feat(audio): native DSD via DoP (DSD over PCM) on WASAPI Exclusive#496
InstaZDLL wants to merge 12 commits into
mainfrom
feat/dsd-dop-495

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Closes #495 and #497.

Native DSD playback via DoP (DSD over PCM) on all three desktop OSes: a .dsf / .dff track is shipped as raw 1-bit DoP frames to a DoP-capable DAC, which decodes the DSD in hardware β€” truly bit-perfect, no host-side FIR conversion.

Platform coverage

OS Exclusive backend Status
Windows WASAPI Exclusive done, compiled
Linux raw hw: ALSA, S32_LE marker-MSB-justified done β€” compiles + clippy clean + dop_pack tests green on Linux (alsa 0.10)
macOS CoreAudio hog mode + forced physical format via AudioUnit done β€” compiled + clippy-clean on a Mac, CoreAudio FFI runtime-probed (fallback confirmed)

Written + verified on real Linux and macOS boxes, not blind β€” only a borrow-scope fix (Linux) and two import fixes (macOS) were needed against the actual crate APIs.

The stack is platform-agnostic: the encoder, decoder path, engine switch, and the shared dop_pack packing are one implementation; each OS only adds a thin output backend.

⚠️ Requires on-hardware validation before merge

I don't have a DoP-capable DAC, so two assumptions must be checked on real hardware:

  • Bit order β€” DoP payload is MSB-first in time; DFF verbatim, DSF bit-reversed. Wrong β‡’ harsh noise.
  • DoP idle/silence β€” pause/underrun emit 0x69 payload frames with a live marker so the DAC keeps DoP lock. Wrong β‡’ clicks/dropout on pause.

Opt-in, default OFF, fully fail-soft. ⚠️ Playing DoP to a non-DoP DAC produces white noise β€” hence the explicit toggle for users who know their DAC supports it.

How it works

  • Core encoder DsdToDop β€” pure, streamable: 1-bit DSD β†’ 24-bit DoP words (marker 0x05/0xFA per frame, payload MSB-first) at dsd_rate / 16 (DSD64 β†’ 176.4 kHz …). 8 unit tests.
  • Shared packing dop_pack β€” byte (WASAPI packed/padded) + MSB-justified i32 (ALSA/CoreAudio) word writers, DoP idle-frame generators, and fill_dop_period* (pull whole frames, silence-fill underrun). 8 unit tests.
  • Backends β€” WASAPI: forced DoP format + a bit-exact run_dop_event_loop. ALSA: raw hw: at the exact DoP rate in S32_LE, rate re-verified, XRUN-recovered. Both surface a device loss like the PCM path. A refused format errors β†’ fallback.
  • Per-track output switch β€” on a cold LoadAndPlay, maybe_switch_dop_output parses the DSD header and calls AudioEngine::switch_output_for_track, which re-opens the exclusive output at the DoP rate and hands the fresh ring producer straight back to the decoder (not via SwapProducer). No-op when the format already matches. play_dop_track is the bit-perfect twin of play_track (no crossfade / gapless / EQ / RG / normalize / mono / speed).
  • Gating β€” Windows: DoP rides the WASAPI Exclusive opt-in. Linux: the DoP toggle engages the hw: path itself. Elsewhere: DoP stays OFF, no wasteful rebuilds.
  • UI β€” a "Native DSD (DoP)" toggle under Settings β†’ Playback (default OFF, shown on Windows + Linux); the pipeline popover shows a "Native DSD" pill from player_get_state.dop_active (what actually engaged). i18n across all 17 locales.

Validation done

  • cargo test -p waveflow-core β€” DSD tests green (8 DoP encoder + existing).
  • cargo check + cargo clippy (app + core) clean, and dop_pack unit tests compile (app-crate tests run in CI).
  • bun run typecheck + bun run lint clean.
  • Windows build green. Linux cargo check + cargo clippy clean, dop_pack tests green (alsa 0.10). macOS cargo check + cargo clippy clean and the CoreAudio FFI runtime-probed on a MacBook (device/hog/format-negotiation execute; built-in speakers offer no DoP rate β†’ clean fallback to DSD β†’ PCM, as designed).
  • Still needs on-hardware DoP validation with a real DoP-capable DAC on each OS (bit order + idle-frame lock) β€” none of the three dev boxes had one connected.

First layer of native DSD playback (#495): a pure, streamable encoder
that repackages the raw 1-bit DSD stream into 24-bit DoP frames
(marker 0x05/0xFA alternating per frame, payload MSB-first, output rate
= DSD bit rate / 16) for a bit-perfect transport to a DoP-capable DAC.

No wiring yet β€” this is the tested foundation. DFF bytes pass through
verbatim; DSF bytes are bit-reversed to honour the MSB-first payload
convention. Marker cadence and leftover-byte carry survive arbitrary
block chunking. 8 unit tests cover rate math, bit order, marker
alternation, streaming continuity and the zeroed top byte.
Second layer of native DSD: the exclusive backend can now open at a
forced DoP format and ship the stream bit-perfect.

- `DopFormat` (rate, channels) threaded through `spawn_output_with_mode`
  -> `spawn_exclusive_output_thread` -> `open_exclusive_session`. When
  set, the session pins the exact DoP layout (`dsd_rate / 16`, source
  channels) and a 24-bit-only format chain (packed then padded) β€” Float
  and 16-bit can't carry a DoP word. A refusal returns an error instead
  of dropping to shared mode, so the caller can fall back to DSD -> PCM.
- Dedicated `run_dop_event_loop`: bit-exact (no volume / mono / normalize
  / clamp), pulls the decoder's 24-bit DoP words from the ring and writes
  them little-endian. Pause / drain / underrun emit marker-carrying DoP
  idle frames (0x69 silence payload, alternating 0x05/0xFA marker) so the
  DAC keeps DoP lock instead of clicking. The PCM hot path is dispatched
  around untouched.
- `OutputHandle.dop_rate` records the active DoP rate so the engine can
  tell when the next track needs an output rebuild.
- `SharedPlayback.dsd_dop_enabled` carries the user opt-in (default OFF).

Not yet wired to track loading. Windows-only; requires on-hardware
validation with a DoP-capable DAC before shipping.
)

Third layer: a DSD track now plays as native DoP end to end when the
opt-in is on and the DAC accepts it.

crossfade.rs β€” new `StreamBackend::Dop`: `ActiveStream::open` gains a
`dop` flag that builds a `DsdToDop` encoder instead of `DsdToPcm`. A
dedicated `decode_dop_block` reads the raw bitstream and emits 24-bit DoP
words with no FIR / resampler / channel-convert; seek + reset drop the
encoder's marker phase so the DAC re-locks cleanly. Prefetch always opens
PCM (DoP never crossfades).

decoder.rs β€” on a cold LoadAndPlay, `maybe_switch_dop_output` parses the
DSD header for the DoP rate and asks the engine to re-open the output at
it. On success it swaps in the fresh producer and runs `play_dop_track`
(the bit-perfect twin of `play_track`: no crossfade / gapless / EQ /
ReplayGain / speed / A-B, reusing `drain_commands` + `push_samples` for
identical transport control). A refused DoP format falls back to the
DSD β†’ PCM path.

engine.rs β€” `switch_output_for_track` rebuilds the output at the DoP
format (or restores normal PCM after a DoP track), handing the new ring
producer straight back to the decoder instead of via the SwapProducer
channel. No-op when the format already matches, so PCM-to-PCM tracks pay
nothing.

Still no user setting to turn it on (`dsd_dop_enabled` stays false).
Windows-only; needs on-hardware validation with a DoP DAC.
`player_set_dsd_dop` persists the opt-in to `profile_setting['audio.dsd_dop']`
and mirrors it into `SharedPlayback.dsd_dop_enabled`, following the DSD
precision pattern. `player_get_state` hydration resolves the row to a
definite bool on every call (default OFF, reset on profile switch), and
`player_get_audio_settings` surfaces `dsd_dop` for the Settings view.
Handler registered in lib.rs. Frontend wiring follows.
- Settings -> Playback: a "Native DSD (DoP)" toggle next to DSD
  precision, wired through `playerSetDsdDop` / `player_get_audio_settings`
  (optimistic update + rollback, default OFF).
- `player_get_state` now reports `dop_active` β€” whether DoP *actually*
  engaged (the DAC accepted it), sourced from `AudioEngine::current_output_is_dop`,
  not just the opt-in. The AudioPipelinePopover shows a distinct
  "Native DSD" chip and counts the stream as bit-perfect when it's on,
  suppressing the spurious resample/downmix flags the nominal DoP rate
  would otherwise trip.
- New i18n keys `settings.dsdDop.*` and `playerBar.pipeline.chip.dopNative`
  across all 17 locales (DoP / DSD / DAC / WASAPI kept verbatim).
Update the DSD pipeline sections in CLAUDE.md and docs/features/playback.md:
the StreamBackend enum now has a Dop variant, the opt-in setting, the
per-track exclusive re-open + fallback, the bit-exact event loop and idle
frames, the bypass of every DSP stage, and the fail-soft behaviour
(refused format / non-exclusive / non-Windows all fall back to DSD -> PCM).
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a635e5a1-1a2d-4c44-b2b5-71d34d95ef98

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review

Comment @coderabbitai help to get the list of available commands.

@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) scope: i18n Translations (src/i18n/) scope: docs Docs, README, assets type: feat New feature size: xl > 500 lines labels Aug 9, 2026
@InstaZDLL InstaZDLL self-assigned this Aug 9, 2026
@InstaZDLL InstaZDLL added the status: stalled Inactive and awaiting renewed prioritization label Aug 9, 2026
DoP can only ride WASAPI Exclusive, so:

- `switch_output_for_track` now drops a DoP request when the exclusive
  preference isn't active (always the case on Linux / macOS, and on
  Windows without the opt-in) BEFORE tearing the output down. Previously
  a DSD track with the toggle on outside exclusive mode rebuilt the
  output on every load just to fall back to PCM β€” now it's a clean no-op.
  Also covers a synced profile carrying `audio.dsd_dop = true` from a
  Windows machine.
- The Settings toggle is hidden on non-Windows platforms (same UA sniff
  as the WASAPI Exclusive card), since DoP can't engage there.

Behaviour is unchanged where it mattered: Linux / macOS keep playing DSD
via DSD -> PCM exactly as before.
Pull the DoP word/silence/period-fill helpers out of wasapi_exclusive
into a platform-agnostic `audio/dop_pack` module so the coming ALSA
(Linux) and CoreAudio (macOS) exclusive backends reuse the exact same
bit-exact packing + marker-carrying idle logic instead of re-deriving it.
`fill_dop_period` consolidates the per-period hot loop (pull whole frames,
silence-fill the remainder on underrun). 6 unit tests. WASAPI now calls
into it; behaviour unchanged.
Extend DoP beyond Windows to Linux, reusing the whole platform-agnostic
stack (encoder / decoder / engine / dop_pack) unchanged β€” only a new
output backend was needed.

- `alsa_exclusive` (cfg linux): opens the DAC as a raw `hw:` device
  (never `default` / a Pulse / PipeWire alias, which would resample the
  marker into noise), at the exact DoP rate in `S32_LE` with the 24-bit
  word MSB-justified (marker in the top byte). Verifies the negotiated
  rate matches exactly, else errors β†’ fallback. XRUN-recovers via
  `try_recover`; a hard failure reports a device loss like the WASAPI
  backend.
- `spawn_output_with_mode` dispatches the DoP request per OS (WASAPI /
  ALSA); `switch_output_for_track` gates DoP on the exclusive preference
  on Windows and engages it directly on Linux (the raw `hw:` open IS the
  exclusive path there).
- Settings toggle now shows on Windows + Linux (hidden on macOS/mobile);
  i18n subtitle reworded platform-neutrally across all 17 locales.

macOS (CoreAudio hog mode) is deliberately deferred β€” the engine keeps
DoP OFF there so DSD plays via DSD -> PCM exactly as before, no wasteful
rebuilds. Linux ALSA can't be compiled on the Windows dev box, so its
compilation must be validated on Linux / CI; the Windows build stays
green.
The Linux DoP backend was written on a Windows box and never built:
`hw_params_current()` borrows the PCM, and the returned `HwParams` was
held in a function-scope binding, so the borrow was still live at the
`Ok((pcm, period_frames))` move β€” E0505.

Read the negotiated rate + period size inside a block so `HwParams`
drops before the move. No behaviour change: the exact-rate check still
rejects any device that didn't land on `dsd_rate / 16`, so a DAC that
can't do the DoP rate still falls back to DSD -> PCM.

Every other alsa API name in the file type-checked as written
(Format::S32LE, Access::RWInterleaved, State::Setup, ValueOr::Nearest,
io_i32/writei, try_recover) β€” this was the only build failure.
Complete DoP across all three desktop OSes β€” macOS was the last gap.
Written + compiled + clippy-clean directly on a Mac (Xcode toolchain),
so this is real, not a blind stub.

- `coreaudio_exclusive` (cfg macos): takes device hog mode (exclusive
  access), forces the physical stream format to the exact DoP rate in
  32-bit signed int, and feeds an `AudioUnit` render callback with DoP
  words MSB-justified (marker in the top byte) via the shared
  `dop_pack::fill_dop_period_i32`. Hog + physical format via
  `coreaudio-rs` 0.14 `macos_helpers`; the render callback is RT-safe
  (ring pops + atomics only). Releases hog on teardown. A busy device /
  unsupported DoP rate errors β†’ fallback to DSD -> PCM.
- Wiring: `spawn_output_with_mode` routes macOS DoP to it,
  `switch_output_for_track` engages DoP from the toggle on macOS too
  (the hog-mode open is the exclusive path), Settings toggle now shows on
  Windows + Linux + macOS, i18n subtitle lists all three mechanisms.
- `coreaudio-rs = "0.14"` added under a macOS-only target; Windows +
  Linux bundles untouched. Cargo.lock updated (transitive objc2 deps were
  already present via cpal).

Bit-perfect note: pinning the device *physical* rate to the DoP rate
means CoreAudio never resamples; any endian adjustment the AudioUnit
does is value-preserving, so the marker stays in the sample's MSB.

Still needs on-hardware validation with a DoP-capable DAC (bit order +
idle-frame lock), same as the other backends. Closes #497.
A read-only smoke test that exercises the CoreAudio FFI against the real
default output device (device id, hog pid, physical-format negotiation)
and reports which DoP rates it can do β€” proof the bindings actually run,
not just compile. `#[ignore]` (needs macOS + a device), so CI skips it.

Verified on a MacBook Air: FFI executes cleanly, built-in speakers offer
no DoP rate β†’ `find_matching_physical_format` returns None β†’ the backend
errors β†’ engine falls back to DSD -> PCM, exactly as designed.
@InstaZDLL InstaZDLL linked an issue Aug 9, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: xl > 500 lines status: stalled Inactive and awaiting renewed prioritization type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(audio): native DSD via DoP on macOS (CoreAudio hog mode) feat(audio): DSD natif via DoP (DSD over PCM) en WASAPI Exclusive

1 participant