Hi,
Most current Linux distros ship PipeWire as the system audio server. When you run scsynth with AUDIOAPI=jack on Ubuntu 22.10+, Fedora 34+, Arch, etc., libjack.so actually resolves to PipeWire’s libjack compatibility shim, and every audio callback is translated through it. The shim works, but it adds overhead.
I’ve written an experimental native PipeWire backend for scsynth that talks to PipeWire directly through pw_stream / pw_thread_loop, no shim. It’s full-duplex, uses -H for device targeting, -S for sample rate, -Z for buffer size, and integrates with the PipeWire graph the same way pw-cat or any other native client does. I’ve been using it daily for over a month.
Performance
Measured on my system with AMD Ryzen AI 9 365 with a Yamaha DM3 USB interface, PipeWire 1.0.5, 48 kHz / 1024-sample quantum. Both binaries built from the same scsynth source, only the audio backend differs.
| N voices | PipeWire CPU | JACK-shim CPU | savings |
|---|---|---|---|
| 500 | 23% | 37% | −37% |
| 1000 | 36% | 69% | −48% |
| 1500 | 55% | 90% | −39% |
| 2000 | 72% | 128% | −43% |
(Default synthdef, N voices at distributed frequencies. The “JACK” baseline is the libjack-on-PipeWire shim, so what most Linux desktops actually run today, not jack2 with a real jackd.)
Practical effect: roughly 40–50 % less CPU at the same DSP load, or equivalently ~50 % more sustainable voices before deadline misses. On this machine sustainable voice count goes from ~1500 to ~2500.
Try it
git clone --recurse-submodules -b pipewire-backend-experiment https://github.com/lucdoebereiner/supercollider.git
cd supercollider
mkdir build && cd build
cmake -DAUDIOAPI=pipewire -DSUPERNOVA=OFF ..
make -j$(nproc) scsynth
# verify the binary links pipewire and not libjack:
ldd server/scsynth/scsynth | grep -E 'pipewire|jack'
# expected: libpipewire-0.3.so.0 (no libjack line)
./server/scsynth/scsynth -u 57110
Build dependency on Debian/Ubuntu: sudo apt install libpipewire-0.3-dev
I believe on Fedora that is: sudo dnf install pipewire-devel
SUPERNOVA=OFF is only because supernova isn’t ported yet.
To switch back to the JACK build, cmake -DAUDIOAPI=jack . and rebuild.
Quick test
After building, start the new pw server:
./build/server/scsynth/scsynth -u 57110
You should see boot output ending with:
PipeWireDriver: negotiated 1024 samples @ 48000.0 Hz, 8 out / 8 in channel(s)
SC_AudioDriver: sample rate = 48000.000000, driver's block size = 1024
SuperCollider 3 server ready.
For an audio test, point sclang at the same port and play a something:
Server.default.options.numOutputBusChannels = 8;
Server.default.options.numInputBusChannels = 8;
Server.default.addr = NetAddr("127.0.0.1", 57110);
{ SinOsc.ar(440, 0, 0.2) ! 2 }.play;
Performance tests on your hardware
If you’d like to check whether the performance improvments hold on your machine:
git clone --recurse-submodules -b pipewire-backend-experiment \
https://github.com/lucdoebereiner/supercollider.git
cd supercollider
mkdir build && cd build
# 1) Build the JACK backend variant and save the binary
cmake -DAUDIOAPI=jack -DSUPERNOVA=OFF ..
make -j$(nproc) scsynth
cp server/scsynth/scsynth ../tools/jack_signal_bench/scsynth_jack
# 2) Switch to the PipeWire backend and build again
cmake -DAUDIOAPI=pipewire .
make -j$(nproc) scsynth
cp server/scsynth/scsynth ../tools/jack_signal_bench/scsynth_pw
# 3) Run the sweep on both. Each cell takes ~10 s.
cd ../tools/jack_signal_bench
: > results.jsonl
for bin in scsynth_jack scsynth_pw; do
for n in 100 500 1000 1500 2000; do
python3 scsynth_sweep.py --binary ./$bin --synths $n --seconds 8 \
--label "${bin}_n${n}" >> results.jsonl
sleep 2
done
done
# 4) Read the table
python3 -c "
import json
rows = [json.loads(l) for l in open('results.jsonl')]
for r in sorted(rows, key=lambda x: (x['binary'], x['synths_requested'])):
print(f\"{r['binary']:<14} n={r['synths_requested']:>5} avg={r['avg_mean']:>6.1f}% peak_max={r['peak_max']:>6.1f}% xruns={r['xruns']}\")"
This launches a fresh scsynth, loads the default synthdef, spawns N voices at
distributed frequencies, polls /status at 50 Hz for 8 seconds,
records avgCPU / peakCPU and any xrun lines from stderr, quits,
and emits one JSON summary line per cell. Hardware reports, lscpu,
audio interface model, PipeWire version, plus the resulting table,
would be very welcome in this thread.
Status. What works, what doesn’t
Works:
- Full-duplex playback and capture, autoconnects to default sink / source
-H "outSink:inSource"device targeting (usepw-cli ls Nodeorwpctl statusto find names)-Sfor sample rate (any rate with a clean integer ratio to the PipeWire graph rate, 24k / 48k / 96k / 192k / 384k all confirmed working). You can set it vias.options.sampleRate.-Zfor buffer size (256, 512, 1024, 2048, …)- Format / latency reporting via PipeWire’s
param_changedevent - Adaptive quantum-change handling (PipeWire renegotiating the graph mid-session no longer breaks the driver)
- Number of input/ouput channels via server options
Known gaps:
supernovanot ported, still goes through the libjack shimSoundInend-to-end not verified yet, capture connects and reachesstreamingstate but I haven’t run a real passthrough synth- Cross-stream scheduling order between in/out streams not explicitly enforced (up to one quantum of capture-to-output latency in the worst case)
- Rates with no clean integer ratio to the PipeWire graph rate (e.g. 44.1k against a 48k graph) abort cleanly with a diagnostic rather than working through internal re-blocking
Discussion
I’d love feedback, especially:
- Reports from different hardware / interfaces, does the speedup hold?
- Anyone who can test with real jack2 (no PipeWire) and see whether the gap is similar or smaller
- Opinions on whether this is worth a PR
Issue on GitHub: https://github.com/supercollider/supercollider/issues/7499
Branch: https://github.com/lucdoebereiner/supercollider/tree/pipewire-backend-experiment