ZuidWest FM Encoder
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 multiple SRT destinations simultaneously. Built for broadcast environments with real-time monitoring and web-based configuration.
Features
- Multi-output streaming - Send to multiple SRT servers with different codecs simultaneously
- Real-time VU meters - Configurable peak hold with peak/RMS toggle, clip detection, updated via WebSocket
- Silence detection - Alerts via webhook, email, file log, or Zabbix when audio drops below threshold
- Web interface - Configure outputs, select audio input, monitor levels
- Auto-recovery - Automatic reconnection with configurable retry limits per output
- Multiple codecs - MP3, Opus, or uncompressed PCM per output
- Update notifications - Alerts when new versions are available
- 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) |
Linux on Raspberry Pi is the primary target. macOS works for development. Windows is experimental.
Deployment Model
This is bare-metal software for a Raspberry Pi with a HiFiBerry sound card - there is no Docker target. Audio capture goes directly through ALSA (arecord) on the host, which needs the kernel sound device, the HiFiBerry overlay, and predictable real-time scheduling. Containerizing it would add a layer without solving anything for this hardware path.
Install via the curl script in Installation below. CI publishes the binary as a GitHub release asset; no container image is published.
Requirements
Installation
- 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
Updating
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 digital output of your audio processor to the HiFiBerry input.
Requirements:
- 48 kHz sample rate
- 16-bit depth
- Stereo (2 channels)
- S/PDIF format preferred (AES/EBU compatibility not guaranteed)
Codecs
| 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 |
Silence Detection
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 |
1 to 300 |
Seconds of silence before alerting |
| Recovery |
5 s |
1 to 60 |
Seconds of audio before recovery |
Alerting options (can use multiple simultaneously, each with per-event control):
- Webhook - POST request to a URL; independently enable
silence_start, silence_end, and audio_dump (MP3 attachment). Abandoned S3 uploads are always sent when the channel is configured.
- Email - Microsoft Graph API notification; independently enable
silence_start, silence_end, and audio_dump (MP3 attachment). Abandoned S3 uploads are always sent when the channel is configured.
- File Log - Appends JSON Lines for every silence event (always records all events, no per-event toggle)
- Zabbix - Send trapper items to a Zabbix server; independently enable
silence_start and silence_end (no audio_dump - trapper items do not support file attachments). Abandoned S3 uploads are always sent when the upload key is configured.
silence_end is sent immediately on recovery. audio_dump_ready is dispatched as a separate event once the MP3 encoding completes.
Configure via the web interface under Settings → Notifications.
Microsoft 365 Email Setup
Email notifications use Microsoft Graph API with app-only authentication.
- Create an App Registration → copy 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 → 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.
Zabbix Setup
- Import
zabbix/template.xml in 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, and upload key
The template creates triggers for SILENCE (Disaster), RECOVERY (Info), TEST (Info), and UPLOAD_ABANDONED (High) events.
Recording API
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).
Configuration
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.
Event Log
The encoder logs all stream, silence, and recording events to a JSON Lines file for monitoring and debugging. Events are accessible via the web interface and REST API.
See docs/events.md for the complete event reference.
Health Endpoint
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, or stopping
- FFmpeg binary is not found
Note: Stream connection failures, silence 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
}
No authentication required.
Architecture
flowchart LR
A[Audio Input]
subgraph Capture["Capture"]
B[arecord]
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["Silence Detection"]
SD[Detector<br>Hysteresis]
SDM[Dump Manager<br>Ring Buffer]
end
end
subgraph Outputs["Streaming Outputs"]
OM[Stream Manager<br>Retry + Backoff]
F1[FFmpeg MP3]
F2[FFmpeg Opus]
S1[SRT 1]
S2[SRT 2]
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]
N3[Log]
N4[Zabbix]
end
subgraph UI["Web UI"]
WS[WebSocket<br>10fps]
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
%% Alerting
SD -->|event| SN
SDM -->|MP3| SN
SN -->|HTTP| N1
SN -->|Graph API| N2
SN -->|file| N3
SN -->|trapper| N4
SN -->|events| EL
%% Streaming
D ==>|PCM| OM
OM ==>|PCM| F1 -->|SRT| S1
OM ==>|PCM| F2 -->|SRT| S2
OM -->|events| EL
%% Recording
D ==>|PCM| RM
RM ==>|PCM| R1 -->|audio| ST
RM ==>|PCM| R2 -->|audio| ST
RM -->|events| EL
Audio Flow
- 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. Buffers 15s audio context before/after silence events
- Alerting: Silence events trigger webhook, email (MS Graph), log (JSON Lines), and/or Zabbix. Each channel has per-event subscriptions for
silence_start, silence_end, and audio_dump_ready. silence_end fires immediately on recovery; audio_dump_ready fires separately once the MP3 is ready. Abandoned S3 uploads also trigger notifications.
- Streaming: Per-output FFmpeg processes with automatic retry and exponential backoff
- Recording: Hourly rotation or on-demand, with optional S3 upload
- Event Log: All stream, silence, and recording events written to a JSON Lines file; accessible via web UI and REST API with pagination
Post-installation
Optional hardening:
echo "dtoverlay=disable-wifi" >> /boot/firmware/config.txt # Disable WiFi
apt remove bolt bluez ntfs-3g telnet # Remove unused packages
SRT Resources
License
MIT License - See LICENSE.md