Skip to content

Repository files navigation

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 remote SRT destinations, or expose local SRT pull listeners for multiple clients. Built for broadcast environments with real-time monitoring and web-based configuration.

Raspberry Pi and SRT logo

Key features

  • 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 Support

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.

Deployment

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.

Requirements

  • 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 via arecord)

Installation

  1. Install Raspberry Pi OS Trixie Lite (64-bit)
  2. Configure HiFiBerry following the official guide
  3. 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.

Audio Input

Connect the audio output of your audio processor to the HiFiBerry input.

Requirements:

  • 48 kHz sample rate
  • 16-bit depth
  • Stereo (2 channels)

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 0.5 to 300 Seconds of silence before alerting
Recovery 5 s 0.5 to 60 Seconds of audio before recovery

Channel Imbalance Detection

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.

Alerting

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, and channel_imbalance_end.
  • Email - Microsoft Graph API notification; independently enable silence_start, silence_end, audio_dump, channel_imbalance_start, and channel_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, and channel_imbalance_end (no audio_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.

Microsoft 365 Email Setup

Email notifications use Microsoft Graph API with app-only authentication.

  1. Create an App Registration, then copy the Client ID and Tenant ID
  2. Add API permissions: Mail.Send (required), Application.Read.All (optional, for secret expiry warnings)
  3. Grant admin consent
  4. Create a client secret, then copy the value immediately (won't be shown again)
  5. Create a shared mailbox as the sender (no license required)

The encoder warns when the secret expires within 30 days.

Zabbix Setup

  1. Import zabbix/template.xml in Zabbix (Data collection -> Templates -> Import)
  2. Link the template to your encoder host
  3. 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.

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).

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

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.

Streaming Modes

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 example srt://<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.

Event Log

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.

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, 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.

Readiness Endpoint

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 }
    }
  }
}

Architecture

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
Loading

Audio Flow

  1. Capture: arecord (Linux) or FFmpeg (macOS/Windows) captures 48kHz 16-bit stereo PCM
  2. Distributor: Processes PCM in ~100ms chunks, fans out to all consumers
  3. Metering: Calculates RMS/peak levels in Go (no FFmpeg filters), holds peaks for a configurable duration (default 3000 ms), detects clipping at +/-32760
  4. Silence Detection: Hysteresis-based detection with configurable threshold/duration/recovery. The shared dump manager buffers 15s audio context before/after silence events.
  5. 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.
  6. 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, and channel_imbalance_end; Zabbix supports the same set except audio_dump. silence_end and channel_imbalance_end fire immediately when recovery is confirmed; audio_dump_ready fires separately once the MP3 is ready. Abandoned S3 uploads also trigger notifications.
  7. 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
  8. Recording: Hourly rotation or on-demand, with optional S3 upload
  9. Event Log: All stream, audio, 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

Related

License

MIT License - See LICENSE.md

About

Software to turn a Raspberry Pi into a production grade audio encoder

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages