feat(audio): native DSD via DoP (DSD over PCM) on WASAPI Exclusive - #496
Draft
InstaZDLL wants to merge 12 commits into
Draft
feat(audio): native DSD via DoP (DSD over PCM) on WASAPI Exclusive#496InstaZDLL wants to merge 12 commits into
InstaZDLL wants to merge 12 commits into
Conversation
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).
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the βοΈ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #495 and #497.
Native DSD playback via DoP (DSD over PCM) on all three desktop OSes: a
.dsf/.dfftrack 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
hw:ALSA,S32_LEmarker-MSB-justifieddop_packtests green on Linux (alsa 0.10)AudioUnitWritten + 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_packpacking are one implementation; each OS only adds a thin output backend.I don't have a DoP-capable DAC, so two assumptions must be checked on real hardware:
0x69payload 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
DsdToDopβ pure, streamable: 1-bit DSD β 24-bit DoP words (marker0x05/0xFAper frame, payload MSB-first) atdsd_rate / 16(DSD64 β 176.4 kHz β¦). 8 unit tests.dop_packβ byte (WASAPI packed/padded) + MSB-justified i32 (ALSA/CoreAudio) word writers, DoP idle-frame generators, andfill_dop_period*(pull whole frames, silence-fill underrun). 8 unit tests.run_dop_event_loop. ALSA: rawhw:at the exact DoP rate inS32_LE, rate re-verified, XRUN-recovered. Both surface a device loss like the PCM path. A refused format errors β fallback.LoadAndPlay,maybe_switch_dop_outputparses the DSD header and callsAudioEngine::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 viaSwapProducer). No-op when the format already matches.play_dop_trackis the bit-perfect twin ofplay_track(no crossfade / gapless / EQ / RG / normalize / mono / speed).hw:path itself. Elsewhere: DoP stays OFF, no wasteful rebuilds.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, anddop_packunit tests compile (app-crate tests run in CI).bun run typecheck+bun run lintclean.cargo check+cargo clippyclean,dop_packtests green (alsa 0.10). macOScargo check+cargo clippyclean 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).