I’m working with an ES9018K2M DAC, using I2S for audio and I2C for DAC control.
This DAC requires explicit register writes to switch bit depth (16 / 24 / 32 bit).
To handle this, I wrote a small Python service that listens to the Volumio WebSocket (pushState) and updates DAC registers when parameters like volume or bit depth change.
However, I’m facing several issues related to timing and state consistency:
WebSocket bit depth reporting is unstable
Sometimes Volumio reports:
incorrect bit depth
or rapidly toggles values (e.g. 16 → 24 → 16) while playing a single 16-bit track
DAC register changes during active playback
Because the audio stream is already running, the DAC attempts to switch bit depth while audio is present, which causes audible pops and glitches.
No time window before playback starts
When switching tracks with different bit depths, the audio stream starts immediately, leaving no time for the DAC to:
receive the new format info
update registers
stabilize before playback
As a workaround, I enabled resampling in Volumio and fixed output to 32-bit, which stabilizes the DAC — but this only works for local files and not for streaming sources.
My questions:
Is there a recommended or supported way in Volumio to:
delay audio playback until hardware is ready?
or receive a reliable, pre-playback audio format notification?
Is relying on WebSocket pushState for bit depth detection considered unsafe by design?
From a Volumio architecture perspective, is it better to:
avoid DAC bit-depth switching entirely and rely on DAC auto-mode / internal oversampling? (my DAC didnt support this features)
or force a fixed format at ALSA level for all sources?
I’d appreciate any guidance on the cleanest and most correct approach here.
make use of a generic dt-overlay for the I2S signals that has a fixed 64FS output.
this will make the bit-depth transition not required anymore, because the I2S stream will always be 32bit per channel, with zero-padding when playing 16bit or 24bit content.
the hifiberry-dac overlay has 32FS output for 16bit audio and 64FS output for 24bit and 32bit audio, this is not good for your use-case.
if you use the “rpi-dac” overlay, the output is fixed at 64FS for all, please give it a try removing all the logic for handling bit-depth in your code
Good analysis of the problem. You have correctly identified the core issue: pushState reports format changes AFTER the audio stream has already started. The DAC receives audio data before your code can update the registers.
Suggested approach: Mute-Switch-Unmute workflow
Instead of relying on the unreliable bitdepth field, detect track changes via URI (which always changes on track switch) and use a mute window:
Detect track change: uri != last_uri
Immediately mute DAC
Wait for DAC to stabilize (~100-150ms)
Unmute
Example modification to on_message:
track_changed = (uri != last_uri) and (uri is not None)
last_uri = uri
if status != "play":
dac.set_mute(True)
dac.set_volume(0)
return
if track_changed:
dac.set_mute(True)
time.sleep(0.150) # DAC stabilization window
dac.set_mute(False)
# Continue with volume handling...
The mute masks the transition period so any format change happens while output is silent.
Timing may need adjustment for the specific board - start conservative (200ms) and reduce if the gap between tracks is noticeable.
To the specific questions:
Delaying playback until hardware ready: Not currently supported in Volumio architecture. The mute-window approach is the practical workaround.
Is pushState bitdepth unsafe by design: It is not designed for real-time hardware synchronization. It reports state, not predictive events.
Fixed format at ALSA level: The current resampling workaround remains the most reliable approach if dynamic switching proves too problematic. Some DACs handle auto-detection better than others - the ES9018K2M varies by board implementation.
One note: be cautious relying on AI assistants for Volumio-specific development - they lack understanding of the ecosystem and its nuances.
Yea i suspected that pushState arrives after the stream has already started, but I didn’t fully see a clean way to implement a safe workaround without turning the whole script upside down)
Your mute switch unmute idea using URI change detection makes a lot of sense thanks!
I’ll try this approach as an experiment and see how stable it is on my board…
That said, I managed to get the dt-overlay switching working, and for now I’ll probably stick with that. It solves the issue without editing python script, and I still have a few other unresolved problems in my code that I want to focus on first… (I’m working on a separate script that handles DAC input switching between I2S and SPDIF. So far the results are not great it mostly works, but there are still stability and timing issues that I haven’t fully solved yet )