Runs the TNC itself against a radio attached to the default ALSA device. Command line and defaults are embedded in the unit's ExecStart — edit it to match your setup, then:
sudo systemctl daemon-reload
sudo systemctl enable --now miniwolfLoads the snd-aloop kernel module. It is a root-level Type=oneshot unit with RemainAfterExit=yes, so it runs once at boot and simply exits — the loopback device stays available for anything that wants it. modprobe is idempotent: if the module is already loaded (or built into the kernel), it succeeds without doing anything.
This exists as a separate unit because user-level services cannot load kernel modules and, being in a different systemd manager, cannot formally depend on system units. Enable it once as root, and both system and user bridges can consume the device at will:
sudo systemctl enable --now mw-loopbackTo skip the module load entirely at boot (preferred alternative), blacklist-style persistence works too: echo snd-aloop | sudo tee /etc/modules-load.d/snd-aloop.conf — the unit tolerates an already-loaded module either way.
miniwolf has no network audio input; it captures samples only from an ALSA device. This bridge feeds it from a TCP stream instead:
TCP server (raw mono s16le/f32le) -> nc -> ffmpeg/sox (resample) -> aplay -> snd-aloop playback end
miniwolf <- snd-aloop capture end
The script:
- Checks that a loopback device exists (exits if not — enable
mw-loopback.servicefirst) - Connects with
nc -w IDLE_TIMEOUT— the connection is dropped after that many seconds without data - Resamples the stream from
MW_AUDIO_IN_RATEtoMW_AUDIO_OUT_RATE. This must happen in userspace:snd-aloopis a dumb sample pipe, its two ends must run at the same rate or audio comes out pitch-shifted. WithMW_AUDIO_RESAMPLER=auto(default) sox is used when installed and ffmpeg otherwise; sox is the lighter choice for simple mono resampling, ffmpeg the more common one. Both expect the stream rate to be declared honestly — see the rate-mismatch note below - Writes raw
f32lesamples intoplughw:Loopback,0,0(the loopback's playback end); miniwolf captures from the pairedplughw:Loopback,1,0
set -euo pipefail means any stage failing (connection refused, reset, EOF, ffmpeg/aplay death, device gone) exits the script — and so does a clean disconnect. Combined with the unit's Restart=always / RestartSec=5, the bridge self-heals: it comes back within 5 seconds until the stream is usable again. This also covers the boot-order edge case: if the bridge starts before mw-loopback.service has run, it exits with a clear message and retries until the device appears.
Configuration via Environment= lines in the unit (or exported variables when run by hand):
| Variable | Default | Meaning |
|---|---|---|
MW_AUDIO_HOST |
127.0.0.1 | Audio server address |
MW_AUDIO_PORT |
7355 | Audio server port |
MW_AUDIO_IN_RATE |
16000 | Sample rate of the TCP stream |
MW_AUDIO_OUT_RATE |
48000 | Rate miniwolf captures at (both loopback ends) |
MW_AUDIO_IDLE_TIMEOUT |
30 | Seconds without data before exiting (nc -w) |
MW_AUDIO_RESAMPLER |
auto |
sox, ffmpeg, or auto (prefers sox) |
MW_AUDIO_FORMAT |
f32le |
Stream sample format: f32le or s16le |
Notes:
- If the server only streams while transmitting, raise
MW_AUDIO_IDLE_TIMEOUTor the bridge will restart continuously during silence - Input format is assumed raw mono
f32le; setMW_AUDIO_FORMAT=s16lefor 16-bit little-endian streams (a common server default). Wrong format both sounds wrong and consumes the stream at the wrong rate — symptom: constant underruns - The stream rate must be declared honestly: measure it with
nc HOST PORT | dd of=/dev/null bs=16k count=25 2>&1 | grep copied—kB/s ÷ 2(s16le) or÷ 4(f32le) = sample rate - Start miniwolf before the bridge, or the first moments of a stream may be lost while the loopback blocks
- Persistent
underrun!!!messages from aplay mean the playback buffer runs dry: usuallyMW_AUDIO_IN_RATEdoes not match the real stream rate, or the source is bursty. Measure the actual rate withtimeout 10 nc HOST PORT | wc -c— bytes/s ÷ 4 = sample rate. The default--buffer-size=32768(≈0.7 s at 48 kHz) absorbs normal jitter
Install (done automatically by make install):
sudo install -m 755 mw-audio-bridge.sh /usr/local/bin/
sudo cp *.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now mw-loopback
sudo systemctl enable --now mw-audio-bridgeThe bridge unit is written to work identically with systemctl --user — no root required for the bridge itself:
sudo systemctl enable --now mw-loopback # once, as root
mkdir -p ~/.config/systemd/user
cp mw-audio-bridge.service ~/.config/systemd/user/
# adjust Environment= lines and, if the script is not in /usr/local/bin, ExecStart=
systemctl --user daemon-reload
systemctl --user enable --now mw-audio-bridgeOne thing the user manager cannot do for you:
- Staying alive after logout — user services are stopped when the last session closes unless lingering is enabled:
sudo loginctl enable-linger $USER
If capture fails with a permission error, add yourself to the audio group (sudo usermod -aG audio $USER, re-login afterwards).