It took a little effort, but now I found a working solution.
Since I saw that there are quite a few users looking for a similar, I'll write up the setup that I came up with (with a little help from <link>, <link>, <link> and <link>):
- Set up the desired cards as sinks in PulseAudio
- Create 'dummy' NULL sinks for all of them + 1 tmp
- Make the tmp dummy the default
- Create a combined sink for all relevant NULL sinks
- Route the audio with a delay from the NULL sinks to the desired cards
- Delay the video to match
First, we have to make the desired audio cards exist in ALSA as "Playback" device (not valid for Bluetooth devices):
LibreELEC:~ # aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: vc4hdmi0 [vc4-hdmi-0], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
Subdevices: 0/1
Subdevice #0: subdevice #0
card 1: vc4hdmi1 [vc4-hdmi-1], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 2: Speaker [Creative USB Speaker], device 0: USB Audio [USB Audio]
Subdevices: 0/1
Subdevice #0: subdevice #0
Display More
So I have 3 playback devices: both HDMIs and an USB speaker.
We'll need the desired devices to showing up as PulseAudio (PA) sinks, which means adding them as ALSA cards. Let's see which sinks we have by default:
LibreELEC:~ # pactl list short sinks
1 librespot module-null-sink.c s16le 2ch 44100Hz SUSPENDED
We see (in my case) only the sink created by the librespot plugin.
Let's use PA's ability to import and configure the available cards and look at what we have after:
LibreELEC:~ # pactl load-module module-udev-detect
16
LibreELEC:~ # pactl list short sinks
1 librespot module-null-sink.c s16le 2ch 44100Hz SUSPENDED
2 alsa_output.usb-Creative_Creative_USB_Speaker-00.analog-stereo module-alsa-card.c s16le 2ch 44100Hz SUSPENDED
3 alsa_output.0.hdmi-stereo module-alsa-card.c s16le 2ch 44100Hz SUSPENDED
Great! The connected HDMI and USB sound card are automatically configured as sinks. For Bluetooth devices, we follow the GUI of LibreELEC. When done, we check the result:
LibreELEC:~ # pactl list short sinks
1 librespot module-null-sink.c s16le 2ch 44100Hz SUSPENDED
2 alsa_output.usb-Creative_Creative_USB_Speaker-00.analog-stereo module-alsa-card.c s16le 2ch 44100Hz SUSPENDED
3 alsa_output.0.hdmi-stereo module-alsa-card.c s16le 2ch 44100Hz SUSPENDED
4 bluez_sink.C0_28_8D_9E_29_1B.a2dp_sink module-bluez5-device.c s16le 2ch 44100Hz SUSPENDED
Now we have all the cards and continue to the next step. Create dummys for all + 1
LibreELEC:~ # pactl load-module module-null-sink sink_name=LB_USB_speakers sink_properties=device.description=DelayedUSB
22
LibreELEC:~ # pactl load-module module-null-sink sink_name=LB_BT_speakers sink_properties=device.description=DelayedBT
23
LibreELEC:~ # pactl load-module module-null-sink sink_name=LB_HDMI_speakers sink_properties=device.description=DelayedHDMI
24
LibreELEC:~ # pactl load-module module-null-sink sink_name=tmp
25
LibreELEC:~ # pactl list short sinks
1 librespot module-null-sink.c s16le 2ch 44100Hz SUSPENDED
2 alsa_output.usb-Creative_Creative_USB_Speaker-00.analog-stereo module-alsa-card.c s16le 2ch 44100Hz SUSPENDED
3 alsa_output.0.hdmi-stereo module-alsa-card.c s16le 2ch 44100Hz SUSPENDED
4 bluez_sink.C0_28_8D_9E_29_1B.a2dp_sink module-bluez5-device.c s16le 2ch 44100Hz SUSPENDED
5 LB_USB_speakers module-null-sink.c s16le 2ch 44100Hz SUSPENDED
6 LB_BT_speakers module-null-sink.c s16le 2ch 44100Hz SUSPENDED
7 LB_HDMI_speakers module-null-sink.c s16le 2ch 44100Hz SUSPENDED
8 tmp module-null-sink.c s16le 2ch 44100Hz IDLE
Display More
We set the dummy ('tmp') as the default sink:
Create the combined sink:
LibreELEC:~ # pactl load-module module-combine-sink sink_name=combined sink_properties=device.description=CombinedSpeakers slaves=LB_BT_speakers,LB_USB_speakers,LB_HDMI_speakers
26
Now we can create the final routing, adding latency to compensate for the delays in audio output. Since we're adding latency we have to add these values 'in reverse': The slowest device gets no additional latency (the BT device gets 10ms in my example) while the others get 600ms and 700ms to delay them. As a result they will be in sync with the BT device:
LibreELEC:~ # pactl load-module module-loopback latency_msec=800 source=LB_USB_speakers.monitor sink=alsa_output.usb-Creative_Creative_USB_Speaker-00.analog-stereo
27
LibreELEC:~ # pactl load-module module-loopback latency_msec=700 source=LB_HDMI_speakers.monitor sink=alsa_output.0.hdmi-stereo
28
LibreELEC:~ # pactl load-module module-loopback latency_msec=10 source=LB_BT_speakers.monitor sink=bluez_sink.C0_28_8D_9E_29_1B.a2dp_sink
29
We can clean up the tmp sink and set the default sink to our newly created 'combined' sink:
Finally, we set up Kodi to account for the delays in audio by delaying the video with an equal amount: Create /storage/.kodi/userdata/advancedsettings.xml with this content:
<advancedsettings>
<video>
<latency>
<delay>-700</delay>
</latency>
</video>
</advancedsettings>
That's it!
Of course, since you probably won't want to do all these steps after each reboot, but how to fix that I have to figure out for myself still.