Audio streaming software for ZuidWest FM (Linux), Radio Rucphen (Linux) and BredaNu (Windows, also the sole user of the Zabbix integration). Stream audio from a Raspberry Pi to remote SRT destinations, or expose local SRT pull listeners for multiple clients. Built for broadcast environments with real-time monitoring and web-based configuration.
- Multi-output streaming - Push to SRT servers or expose local endpoints for monitoring
- Multi-recorder support - Run multiple recording jobs with local and/or S3 storage, hourly rotation, retention cleanup and on-demand control
- Configurable VU meters - Peak/RMS metering with configurable peak hold and clip detection
- Silence detection - Alerts via webhook, email, file log, or Zabbix when audio drops below threshold
- Audio incident dumps - Capture audio around silence and channel imbalance events for troubleshooting
- Channel imbalance detection - Detects dead or mismatched L/R channels with webhook, email, file log, Zabbix, live UI, and readiness status
- Multiple codecs - MP3, Opus, or uncompressed PCM per output
- Single binary - Web interface embedded, minimal runtime dependencies
| Platform | Status | Audio Capture |
|---|---|---|
| Linux (Raspberry Pi) | Primary | arecord (ALSA) |
| macOS | Development only | FFmpeg (AVFoundation) |
| Windows | Experimental | FFmpeg (DirectShow) |
The Windows build launches as a systray application without a console window: right-click the tray icon for Open UI / Start / Stop / Show Logs / Quit. slog output is redirected to %PROGRAMDATA%\encoder\encoder.log when no console is attached, falling back to %LOCALAPPDATA%\encoder\encoder.log and then the system temp directory if that path is not writable; the operational event log stays at %PROGRAMDATA%\encoder\logs\<port>\encoder.jsonl.
This is bare-metal software for a Raspberry Pi with a HiFiBerry sound card. Audio capture goes directly through ALSA (arecord). Install via the curl script in Installation below. CI publishes the binary as a GitHub release asset; no container image is published.
- Raspberry Pi 4 or 5
- HiFiBerry Digi+ I/O or HiFiBerry DAC+ ADC
- Raspberry Pi OS Trixie Lite (64-bit)
ffmpeg(for encoding; SRT protocol support is only required for push/caller streams)alsa-utils(for audio capture viaarecord)
- Install Raspberry Pi OS Trixie Lite (64-bit)
- Configure HiFiBerry following the official guide
- Run the installer as root:
sudo su
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/oszuidwest/zwfm-encoder/main/deploy/install.sh)"The web interface will be available at http://<raspberry-pi-ip>:8080
Default credentials: admin / encoder
Run the same installer script to update to the latest version:
sudo su
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/oszuidwest/zwfm-encoder/main/deploy/install.sh)"The installer detects existing installations and asks whether to:
- Update only - Downloads the latest binary while preserving your configuration
- Fresh install - Overwrites configuration (existing config is backed up)
The web interface shows a notification when updates are available.
Connect the audio output of your audio processor to the HiFiBerry input.
Requirements:
- 48 kHz sample rate
- 16-bit depth
- Stereo (2 channels)
| Codec | Encoder | Bitrate | Notes |
|---|---|---|---|
| MP3 | libmp3lame | 320 kbit/s | - |
| Opus | libopus | 128 kbit/s (64-256 configurable) | MPEG-TS container, 10 ms frames |
| PCM | s302m | ~1.92 Mbit/s (16-bit) | MPEG-TS container, SMPTE 302M |
Monitors audio levels and sends alerts when silence is detected or recovered. Uses hysteresis to prevent alert flapping:
| Setting | Default | Range | Description |
|---|---|---|---|
| Threshold | -40 dB | -60 to -1 | Audio level below which silence is detected |
| Duration | 15 s | 0.5 to 300 | Seconds of silence before alerting |
| Recovery | 5 s | 0.5 to 60 | Seconds of audio before recovery |
Detects dead or mismatched stereo channels by comparing the left and right RMS levels. Imbalance is only evaluated while at least one channel is above the configured silence threshold, so normal silence is handled by silence detection instead of imbalance detection.
| Setting | Default | Range | Description |
|---|---|---|---|
| Threshold | 12 dB | 1 to 59 | L/R level difference above which imbalance is detected |
| Duration | 15 s | 0.5 to 300 | Seconds of imbalance before alerting |
| Recovery | 5 s | 0.5 to 60 | Seconds of balanced audio, or dropped-away audio, before recovery |
Channel imbalance events can trigger webhook, email, and Zabbix notifications, are written to the event log, and are exposed in the dashboard, GET /health, and GET /ready. A confirmed imbalance makes the channel_imbalance readiness component fail until the channels are balanced again or audio drops away. When audio incident dumps are enabled, recovery also produces an MP3 with 15 seconds of context before and after the imbalance; the existing audio_dump webhook/email subscription controls its delivery.
Audio alerting options (can use multiple simultaneously, each with per-event control):
- Webhook - POST request to a URL; independently enable
silence_start,silence_end,audio_dump,channel_imbalance_start, andchannel_imbalance_end. - Email - Microsoft Graph API notification; independently enable
silence_start,silence_end,audio_dump,channel_imbalance_start, andchannel_imbalance_end. - File Log - Appends JSON Lines for every audio event, including silence and channel imbalance events (always records all events, no per-event toggle).
- Zabbix - Send trapper items to a Zabbix server; independently enable
silence_start,silence_end,channel_imbalance_start, andchannel_imbalance_end(noaudio_dump- trapper items do not support file attachments).
silence_end and channel_imbalance_end are sent immediately when recovery is confirmed; audio_dump_ready is dispatched as a separate event once the MP3 context for either incident type finishes encoding. Its trigger field is silence or channel_imbalance. Abandoned S3 uploads always alert every configured channel, regardless of per-event toggles.
Configure silence and channel imbalance detection under Settings -> Audio. Configure audio notifications under Settings -> Notifications.
Email notifications use Microsoft Graph API with app-only authentication.
- Create an App Registration, then copy the Client ID and Tenant ID
- Add API permissions:
Mail.Send(required),Application.Read.All(optional, for secret expiry warnings) - Grant admin consent
- Create a client secret, then copy the value immediately (won't be shown again)
- Create a shared mailbox as the sender (no license required)
The encoder warns when the secret expires within 30 days.
- Import
zabbix/template.xmlin Zabbix (Data collection -> Templates -> Import) - Link the template to your encoder host
- Configure in the encoder: server, port (default 10051), host name (must match Zabbix exactly), silence key, imbalance key, and upload key
The template creates triggers for SILENCE (Disaster), RECOVERY (Info), CHANNEL_IMBALANCE (High), CHANNEL_BALANCED (Info), TEST (Info), and UPLOAD_ABANDONED (High) events. Channel imbalance uses a separate imbalance.alert item; Zabbix imbalance notifications only work when imbalance_key is configured and the matching trapper item exists.
On-demand recordings can be started and stopped via the REST API without going through the web interface. Useful for automation or external systems that need to trigger recordings.
# Start recording (recorder_id matches the ID in the web interface)
curl -X POST "http://<host>:8080/api/recordings/start?recorder_id=1" \
-H "X-API-Key: <your-api-key>"
# Stop recording
curl -X POST "http://<host>:8080/api/recordings/stop?recorder_id=1" \
-H "X-API-Key: <your-api-key>"Configure the API key and maximum recording duration under Settings -> Audio -> Recording API. The max_duration_minutes limit automatically stops a recording after the configured time (0 = no limit).
For local recording storage under the packaged systemd service, use /var/lib/encoder/recordings. The service owns this state directory even with ProtectSystem=strict. Custom archive paths need a systemd override that adds the path to ReadWritePaths=.
Configuration is stored in /etc/encoder/config.json on production systems. For development, use the -config flag to specify a custom path, or place config.json next to the binary.
{
"system": { "port": 8080, "username": "admin", "password": "encoder" },
"web": { "station_name": "ZuidWest FM" }
}The installer creates a minimal config file. All other settings are configured through the web interface.
Streams support two SRT modes:
- Push to server (
mode: "caller"): the encoder connects to a remote SRT listener using the configured host, port, codec, password, stream ID, and retry settings. - Local pull listener (
mode: "listener"): the encoder opens a local SRT port so clients can pull the stream, for examplesrt://<encoder-ip>:9000?mode=caller.
Listener streams bind to 0.0.0.0 by default, use a separate port per stream, and accept up to 16 clients. They do not use stream_id. Client disconnects do not restart the encoder. SRT encryption is optional. Empty passwords allow unencrypted connections; non-empty passwords must be 10-64 characters. Caller mode requires FFmpeg with srt protocol support, while listener mode uses GoSRT and does not.
The encoder logs all stream, audio, and recording events to a JSON Lines file for monitoring and debugging. Audio events include silence, audio dump, and channel imbalance events. Events are accessible via the web interface and REST API.
The active log rotates at 50 MiB and keeps one previous file. The web interface and REST API read recent events from both files without loading the full log into memory.
See docs/events.md for the complete event reference.
GET /health provides a public endpoint for monitoring tools (Kubernetes probes, load balancers, Prometheus, etc.).
Healthy (200 OK) requires both:
- Encoder state is
running(audio capture active) - FFmpeg binary is available on the system
Unhealthy (503 Service Unavailable) when either:
- Encoder state is
stopped,starting, orstopping - FFmpeg binary is not found
Note: Stream connection failures, silence detection, channel imbalance detection, and recorder errors do not affect health status. These are reported in the response body for informational purposes only.
Response example:
{
"status": "healthy",
"encoder_state": "running",
"stream_count": 2,
"streams_stable": 2,
"recorder_count": 1,
"recorders_running": 1,
"uptime_seconds": 9252,
"silence_detected": false,
"channel_imbalance_detected": false
}No authentication required.
GET /ready is a public production-readiness endpoint. It returns 200 only when the process is usable for broadcast monitoring: FFmpeg is available, the encoder is running, enabled caller streams are stable, no silence alarm is active, no channel imbalance alarm is active, enabled hourly recorders are running, no recorder is in error, and no recording uploads are pending retry. Local pull listener streams are excluded from production-readiness because a running listener means "waiting for clients", not "connected remote output".
The readiness rollup is intentionally strict: one pending recording upload makes the overall endpoint return 503 because the archive path is degraded. A confirmed channel imbalance (for example one dead channel) makes the channel_imbalance component not ready, since the broadcast is degraded even though the encoder keeps running. Monitoring that should only page for live broadcast impact should alert on the relevant components in the JSON body, such as process, streams, silence, and channel_imbalance, instead of the aggregate status. Planned off-air periods or other intentional silence also make the silence component not ready while the silence alarm is active.
When any component is not ready, the endpoint returns 503 with component details:
{
"status": "not_ready",
"components": {
"uploads": {
"ok": false,
"message": "recording uploads are pending retry",
"details": { "pending": 1, "max": 0 }
}
}
}flowchart LR
A[Audio Input]
subgraph Capture["Capture"]
B[arecord / FFmpeg]
end
subgraph Processing["Audio Processing"]
D[Distributor]
subgraph Metering["Metering"]
M[RMS/Peak<br>Calculator]
PH[Peak Hold<br>Configurable]
CD[Clip<br>Detect]
end
subgraph Silence["Audio Incident Detection"]
SD[Detector<br>Hysteresis]
SDM[Shared Dump Manager<br>Ring Buffer]
end
subgraph Imbalance["Channel Imbalance"]
ID[Detector<br>L/R Difference]
end
end
subgraph Outputs["Streaming Outputs"]
OM[Stream Manager<br>Retry + Backoff]
F1[FFmpeg caller output]
F2[FFmpeg listener encoder]
FO[GoSRT fan-out]
S1[Remote SRT listener]
S2[Local SRT clients]
end
subgraph Recording["Recording"]
RM[Recording Manager]
R1[Hourly]
R2[On-Demand]
ST[(Storage)]
end
subgraph Alerts["Alerting"]
SN[Alert Orchestrator]
N1[Webhook]
N2[Email]
N4[Zabbix]
end
subgraph UI["Web UI"]
WS[WebSocket<br>levels 10fps + status 3s]
end
subgraph EventLog["Event Log"]
EL[JSON Lines<br>Logger]
end
%% Main audio flow
A ==>|S/PDIF| B ==>|PCM| D
%% Metering branch
D ==>|PCM| M -->|dB| PH
M -->|samples| CD
PH -->|dB JSON| WS
CD -->|clip JSON| WS
%% Silence detection branch
D -->|dB| SD
D ==>|PCM| SDM
SD -->|events| SDM
SD -->|state JSON| WS
%% Channel imbalance branch
D -->|dB| ID
ID -->|state JSON| WS
ID -->|events| SDM
%% Alerting
SD -->|event| SN
SDM -->|MP3| SN
ID -->|event| SN
SN -->|HTTP| N1
SN -->|Graph API| N2
SN -->|trapper| N4
SN -->|audio events| EL
%% Streaming
D ==>|PCM| OM
OM ==>|PCM| F1 -->|SRT caller| S1
OM ==>|PCM| F2 -->|pipe| FO -->|SRT listener| S2
OM -->|events| EL
%% Recording
D ==>|PCM| RM
RM ==>|PCM| R1 -->|audio| ST
RM ==>|PCM| R2 -->|audio| ST
RM -->|events| EL
RM -->|upload abandoned| SN
- Capture:
arecord(Linux) or FFmpeg (macOS/Windows) captures 48kHz 16-bit stereo PCM - Distributor: Processes PCM in ~100ms chunks, fans out to all consumers
- Metering: Calculates RMS/peak levels in Go (no FFmpeg filters), holds peaks for a configurable duration (default 3000 ms), detects clipping at +/-32760
- Silence Detection: Hysteresis-based detection with configurable threshold/duration/recovery. The shared dump manager buffers 15s audio context before/after silence events.
- Channel Imbalance Detection: Hysteresis-based L/R balance detection with configurable threshold/duration/recovery. Reuses the silence threshold as a presence floor, reports live balance/imbalance values, and uses the shared dump manager for recovery context.
- Alerting: Silence and channel imbalance events trigger webhook, email (MS Graph), log (JSON Lines), and/or Zabbix. Webhook and email support per-event subscriptions for
silence_start,silence_end,audio_dump,channel_imbalance_start, andchannel_imbalance_end; Zabbix supports the same set exceptaudio_dump.silence_endandchannel_imbalance_endfire immediately when recovery is confirmed;audio_dump_readyfires separately once the MP3 is ready. Abandoned S3 uploads also trigger notifications. - Streaming: Per-output FFmpeg encoders; caller streams use FFmpeg SRT with retry/backoff, listener streams use a GoSRT multi-client fan-out around one FFmpeg stdout pipe
- Recording: Hourly rotation or on-demand, with optional S3 upload
- Event Log: All stream, audio, and recording events written to a JSON Lines file; accessible via web UI and REST API with pagination
Optional hardening:
echo "dtoverlay=disable-wifi" >> /boot/firmware/config.txt # Disable WiFi
apt remove bolt bluez ntfs-3g telnet # Remove unused packages- Liquidsoap Server - Companion server software for receiving SRT streams
MIT License - See LICENSE.md