Snapcast client container direct to audio output in multi-room

I’m looking at the balenaSound architecture diagram here, and I had an idea I think might help with glitches and skips. I don’t know how feasible this is.

When running in any multi-room mode, route the multiroom-client container’s output directly to the sound output, rather than through the audio block. The audio block is a great management point for audio inputs. But there is only ever one audio output, and it doesn’t change during the life-cycle of a device, under normal usage patterns. Upon reboot or a sound-supervisor reconfig event, the containers are restarted anyway, so reconnecting the correct blocks to the sound output would happen at that time, without any expected conflicts.

This would provide reduced complexity in the audio routing, and it would open up the ability to run a dedicated multi-room client on only the multiroom-client container, which I expect would re-enable support all the way back to the RPi 1.x.

In standalone mode, audio would route through a plugin, then the audio block, then to the sound output, as it currently does.

In multi-room mode, audio would route through a plugin, then audio block, then multi-room server … received by multi-room client, then to sound output.

My rationale here is that I think pulseaudio is just trying to do too much, with how much compute and I/O the RPi has available (or maybe some kind of process or scheduling issue, but that’s beyond the scope of this post). I’ve noticed that even though a device like an RPi 4B has plenty of compute capability to keep up with balenaSound, it still has glitches and skips, which did not exist (as far as I can tell) in the 2.4.x code.



Hi Mark,

Thank you for this - you’ve clearly put a lot of time and effort into the idea and writing it up so succinctly. I took a look at the code to try and work out if this would work, and it looks plausible, but I don’t know the codebase well enough to consider all the angles. I’ve asked Tomas, who is the main dude for balenaSound, if he could come and have a look at your suggestion. I’ll gently prod him until he replies. :wink:

Thanks again,

1 Like

Hey Mark! Thank you for the detailed explanation, always nice to read well thought posts.

I agree with your overall assessment and to be honest I don’t have any concrete arguments against this idea. The only concern I have is purely philosophical as we do want to have a solid audio block that can handle everything audio related and this would detract us from getting to the bottom of the problem :stuck_out_tongue: However user experience should always go first, so if this provides a clear benefit I think we should go for it.

My gut feeling is that while handling output does add stuff to pulseaudio to do it shouldn’t be too significant, but yeah this is not based on science but rather a hunch/previous experience. Fortunately this should be easy enough to implement and test so I can take a stab at it in the coming days and let you know (maybe you can help test?).

For reference, the other avenues we need to explore are process scheduling, process priority and dealing with FIFO pipes (this problem does seem to get worse with multiroom), but this are more complex topics to debug and test.

Anyways, I’ll let you know when I get the time to try this out so you can also test (unless you want to go for it, obviously feel free to do so!).

If possible can you raise a GitHub issue in the sound repo for this? Thanks Mark!


I created feature request 354 for this.


Hey All,

I’ve modified my local code to bypass the audio block for output audio. The Snapcast client connects directly to the alsa sound card, and pulseaudio is used only for plugin input. This has dramatically improved performance, and made balenaSound work on my RPi Zero Ws without any further build customization (I.e. I don’t have to build the application with most of the components removed from docker-compose.yaml). I’ve disable unwanted services with environment variables. So far my only glitches are caused by a WiFi signal black hole I found right where I have my work desk. (of course)

I’m going to run the new code throughout the afternoon today, and see what I get for glitches on the old devices. I’m also retesting on my old RPi B+, just to see how well the application performs on the oldest and jankyest hardware I have.

The changes I made are a bit of a mess, and mixed in with some other customizations I’ve made. So before I provide any code, I’m going to make a new branch and clean things up.

EDIT: After a couple hours of playback, all devices, old and new are performing wonderfully. (I cleared up the WiFi issue I was having earlier.) I even got my old RPi B+ 1.2 back on the application and it’s running at or below 50% CPU, and hovering around +/- 1.0 load average. The RPi Zero Ws are also running at a nice low load - slightly lower than the B+. Even with an occasional spike to 100% CPU, the audio stays clean on the B+ and the Zero Ws. The B+ has no WiFi nor Bluetooth hardware.

This change does not have all the bases covered yet. I’m seeing errors from the audio block due to me rather ungracefully severing its access to the sound hardware. Also, the changes currently break stand-alone mode. I also have the sound card identity manually provided as an environment variable, which is not a user-friendly solution. I need to figure out how to automatically choose the right sound card


1 Like

Epic work Mark!

Hi Mark, would you care to share your work here or e.g. on github? I’m trying to set up a simplified version of balena sound (just as a snapcast client) on old Raspberry 1 B+ and would like to study it to minimize the load on the device.

Hi @tomasrollo,

I haven’t had a chance to go in and clean up the ungraceful disconnection I did on pulseaudio, and the application logs a few issues because of it.

But it works on my RPi Zero W and RPi B+ And is much better at staying in sync than stock, on all devices. The B+ does exhibit some issues now and again, because it lacks built-in WiFi and Bluetooth, and the USB bus glitches a little due to shuffling so many things across it when you use USB WiFi and BT. Also, avoid HDMI output on the B+ - it also causes glitchiness. You’re better off with either the built-in headphone port or a DAC.

You can further lighten the load by also eliminating the sound-supervisor and hard-coding some of the values the sound-supervisor normally manages.

It’s in a branch of my fork here:

Take a look at this file, in particular:

EDIT: You will need to watch log output to identify the value that must be set for this variable:


Thanks, @koyaanisqatsi,

will have a look. I started by removing all the unnecessary services from the docker-compose.yml and now I’m trying to bypass the PulseAudio and linking the snapclient directly to the ALSA backend - hense my interest :slight_smile:

I realized I actually have the oldest RPi 1 B, not B+ (26 pin header, not 40) so I’m curious if it will manage.

Once I conquer that the next thing will be to make HDMI output work (I need to output to HDMI for my soundbar where optical in is already occupied by my TV) - I’m struggling with that on both the RPi 1B and on my newer RPi 3B+ :frowning:

Quick update - I got rid of everything except for the multiroom-client container and made it use the ALSA backend directly w/o PulseAudio. Works great - no stutter on (the oldest) RPi 1 B (ethernet, not wifi).

Here’s my code: GitHub - tomasrollo/balena-sound at simplified_multiroom_client

Also conquered the HDMI output on RPi 1 B - works fine too.

For the RPi 3 B+ HDMI output - turns out that wasn’t in Raspberry, I had it connected to my soundbar that only supports ARC on HDMI port which (as I found out) doesn’t work w/ Raspberry - apparently ARC uses HDMI pins that are not even connected on RPi. Took me a while to figure this out :slight_smile:

1 Like