This repository contains a professional-grade audio streaming solution originally built for ZuidWest FM, Radio Rucphen, and BredaNu in the Netherlands. Using Liquidsoap as its core, it provides:
- High-availability streaming with automatic failover between multiple inputs
- Professional audio processing via StereoTool (optional)
- Multiple output formats: Icecast streaming (MP3/AAC), DAB+ encoding, and MicroMPX for FM transmitters
- Docker-based deployment for easy installation and management
While originally designed for these three Dutch radio stations, the system is fully configurable for any radio station's needs.
The system delivers audio through dual redundant pathways. Liquidsoap prioritizes the main input (SRT 1). If it becomes unavailable or silent, the system automatically switches to SRT 2. Should both inputs fail, it falls back to an emergency audio file (configured via EMERGENCY_AUDIO_PATH
). For maximum reliability, both inputs should receive the same broadcast via separate network paths.
- Liquidsoap: Core audio processing engine - handles input switching, fallback logic, and encoding
- Icecast: Public streaming server for distributing MP3/AAC streams to listeners
- StereoTool: Professional audio processor and MicroMPX encoder for FM transmitters (optional, requires license)
- ODR-AudioEnc: DAB+ audio encoder for digital radio broadcasting (optional)
- rpi-audio-encoder: Turn a Raspberry Pi into a production-grade SRT audio encoder for studio connections
- rpi-umpx-decoder: Turn a Raspberry Pi into a μMPX decoder for FM transmitter sites
- ODR-PadEnc: Programme Associated Data encoder for DAB+ metadata
- padenc-api: REST API server for managing DAB+ metadata
- zwfm-metadata: Metadata routing middleware for now-playing information
- Linux server (Ubuntu 24.04 or Debian 12 recommended)
- Docker and Docker Compose installed
- x86_64 or ARM64 architecture
- At least 2GB RAM and 10GB disk space
- Network connectivity for SRT streams
# Install Liquidsoap
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/oszuidwest/zwfm-liquidsoap/main/install.sh)"
# Optional: Install Icecast server
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/oszuidwest/zwfm-liquidsoap/main/icecast2.sh)"
After installation, edit the environment file at /opt/liquidsoap/.env
to configure your station settings. Example configuration files are provided:
.env.zuidwest.example
- Basic configuration without DME.env.rucphen.example
- Configuration with DME output.env.bredanu.example
- Configuration with DME output
Copy the appropriate example file to .env
and customize it for your station. Most configuration variables are centralized in conf/lib/defaults.liq
. Station-specific files only contain DME configuration (for Rucphen/BredaNu).
This table lists ALL environment variables used in the system. Variables without defaults are required and will cause Liquidsoap to fail if not set.
Variable | Description | Default | Example | Used In | Station |
---|---|---|---|---|---|
Station Configuration | |||||
STATION_ID |
Unique station identifier (lowercase, no spaces) | (required) | zuidwest |
conf/lib/defaults.liq |
All |
STATION_NAME |
Full station name for metadata | (required) | ZuidWest FM |
conf/lib/defaults.liq |
All |
Icecast Configuration | |||||
ICECAST_HOST |
Icecast server hostname | (required) | icecast.zuidwest.cloud |
conf/lib/defaults.liq |
All |
ICECAST_PORT |
Icecast server port | (required) | 8000 |
conf/lib/defaults.liq |
All |
ICECAST_SOURCE_PASSWORD |
Icecast source password | (required) | s3cur3p4ss |
conf/lib/defaults.liq |
All |
ICECAST_MOUNT_BASE |
Base mount point name | STATION_ID |
zuidwest |
conf/lib/defaults.liq |
All |
Stream Mount Points | |||||
ICECAST_MOUNT_MP3 |
MP3 stream mount | /#{ICECAST_MOUNT_BASE}.mp3 |
/zuidwest.mp3 |
conf/lib/defaults.liq |
All |
ICECAST_MOUNT_AAC_LOW |
AAC mobile stream mount | /#{ICECAST_MOUNT_BASE}.aac |
/zuidwest.aac |
conf/lib/defaults.liq |
All |
ICECAST_MOUNT_AAC_HIGH |
AAC STL stream mount | /#{ICECAST_MOUNT_BASE}.stl |
/zuidwest.stl |
conf/lib/defaults.liq |
All |
Stream Bitrates | |||||
ICECAST_BITRATE_MP3 |
MP3 stream bitrate (kbps) | 192 |
256 |
conf/lib/defaults.liq |
All |
ICECAST_BITRATE_AAC_LOW |
Low AAC bitrate (kbps) | 96 |
64 |
conf/lib/defaults.liq |
All |
ICECAST_BITRATE_AAC_HIGH |
High AAC bitrate (kbps) | 576 |
320 |
conf/lib/defaults.liq |
All |
SRT Studio Inputs | |||||
SRT_PASSPHRASE |
SRT encryption passphrase | (required) | alpha-bravo-charlie-delta |
conf/lib/studio_inputs.liq |
All |
SRT_PORT_PRIMARY |
Primary SRT listening port | 8888 |
8888 |
conf/lib/defaults.liq |
All |
SRT_PORT_SECONDARY |
Secondary SRT listening port | 9999 |
9999 |
conf/lib/defaults.liq |
All |
Audio Processing | |||||
STEREOTOOL_LICENSE |
StereoTool license key | (none) | ABC123DEF456... |
conf/lib/stereotool.liq |
All |
STEREOTOOL_WEB_PORT |
StereoTool web interface port | 8080 |
8080 |
conf/lib/stereotool.liq |
All |
Fallback & Control | |||||
EMERGENCY_AUDIO_PATH |
Fallback audio file when all inputs fail | /audio/fallback.ogg |
/audio/noodband.mp3 |
conf/lib/defaults.liq |
All |
SILENCE_CONTROL_PATH |
Silence detection control file | /silence_detection.txt |
/opt/silence.txt |
conf/lib/defaults.liq |
All |
SILENCE_SWITCH_SECONDS |
Max silence duration (seconds) | 15.0 |
20.0 |
conf/lib/defaults.liq |
All |
AUDIO_VALID_SECONDS |
Min audio duration (seconds) | 15.0 |
10.0 |
conf/lib/defaults.liq |
All |
DAB+ Configuration (Optional) | |||||
DAB_BITRATE |
DAB+ encoder bitrate | (none) | 128 |
conf/lib/defaults.liq |
All |
DAB_EDI_DESTINATIONS |
DAB+ EDI destination(s) | (none) | tcp://dab-mux.local:9001 or tcp://dab1:9001,tcp://dab2:9002 |
conf/lib/defaults.liq |
All |
DAB_METADATA_SIZE |
PAD size in bytes (0-255) | 58 when socket is set |
128 |
conf/lib/defaults.liq |
All |
DAB_METADATA_SOCKET |
PAD metadata socket path | (none) | padenc.sock |
conf/lib/defaults.liq |
All |
DME Configuration | |||||
DME_PRIMARY_HOST |
Primary DME server | (required) | ingest1.dme.nl |
conf/rucphen.liq , conf/bredanu.liq |
Rucphen/BredaNu |
DME_PRIMARY_PORT |
Primary DME port | (required) | 8010 |
conf/rucphen.liq , conf/bredanu.liq |
Rucphen/BredaNu |
DME_PRIMARY_USER |
Primary DME username | (required) | rucphen-live |
conf/rucphen.liq , conf/bredanu.liq |
Rucphen/BredaNu |
DME_PRIMARY_PASSWORD |
Primary DME password | (required) | dme123pass |
conf/rucphen.liq , conf/bredanu.liq |
Rucphen/BredaNu |
DME_SECONDARY_HOST |
Secondary DME server | (required) | ingest2.dme.nl |
conf/rucphen.liq , conf/bredanu.liq |
Rucphen/BredaNu |
DME_SECONDARY_PORT |
Secondary DME port | (required) | 8020 |
conf/rucphen.liq , conf/bredanu.liq |
Rucphen/BredaNu |
DME_SECONDARY_USER |
Secondary DME username | (required) | bredanu-backup |
conf/rucphen.liq , conf/bredanu.liq |
Rucphen/BredaNu |
DME_SECONDARY_PASSWORD |
Secondary DME password | (required) | backup456pwd |
conf/rucphen.liq , conf/bredanu.liq |
Rucphen/BredaNu |
DME_MOUNT_POINT |
DME mount point | (required) | /live-stream |
conf/rucphen.liq , conf/bredanu.liq |
Rucphen/BredaNu |
Docker Configuration | |||||
CONTAINER_TIMEZONE |
Container timezone | Europe/Amsterdam |
Europe/Amsterdam |
docker-compose.yml |
All |
- Required variables: Must be set in
.env
file or Liquidsoap will fail to start - Optional features: DAB+ output is optional - set both
DAB_BITRATE
andDAB_EDI_DESTINATIONS
to enable. PAD metadata requiresDAB_METADATA_SOCKET
- Multiple EDI outputs:
DAB_EDI_DESTINATIONS
supports comma-separated values for sending to multiple DAB+ destinations simultaneously - Station column: "All" means used by all stations, "Rucphen/BredaNu" means used only by stations with DME
- Default conventions:
#{VARIABLE}
means the value is interpolated from another variable - PAD sizes: Valid range 0-255 bytes. Common values: 16 (text only), 58 (text + logo), 128 (text + album art)
- File locations: Most configuration variables are centralized in
conf/lib/defaults.liq
- Station-specific files: Only contain DME configuration (for Rucphen/BredaNu) and station-specific logic
cd /opt/liquidsoap
# Start services
docker compose up -d
# View logs
docker compose logs -f
# Stop services
docker compose down
When StereoTool is enabled (by providing a STEREOTOOL_LICENSE
in the .env
file), access the web interface at: http://localhost:8080
StereoTool is always included in the installation. When enabled (by providing a STEREOTOOL_LICENSE
), the system creates two audio paths:
-
Unprocessed audio (
radio
): The raw combined audio from studios/fallback -
Processed audio (
radio_processed
): Audio processed by StereoTool- Audio processing (AGC, compression, limiting, EQ, etc.)
- MicroMPX encoding for FM transmitters (available via StereoTool's separate output)
Note: The output.dummy()
call is required to activate StereoTool's processing chain, even though this output isn't used directly.
The system includes automatic silence detection that monitors studio inputs and manages fallback behavior. This feature is enabled by default.
When silence detection is enabled (default):
- Studio inputs automatically switch away when silent for more than 15 seconds
- If both studios are silent/disconnected, the system plays the fallback file
- If no fallback file exists, the system plays silence
- Provides automatic redundancy for unattended operation
When silence detection is disabled:
- Studio inputs continue playing even when silent and only switch away when disconnected
- No automatic switching between sources
- Fallback file is never used
- Useful for testing or when manual control is preferred
Control silence detection via the control file:
# Enable silence detection (default)
echo '1' > /silence_detection.txt
# Disable silence detection
echo '0' > /silence_detection.txt
Note: The actual path depends on your container volume mapping. By default, this file is located at /silence_detection.txt
inside the container.
Changes take effect immediately without restarting the service.
The default silence detection parameters can be adjusted via environment variables:
SILENCE_SWITCH_SECONDS
: Maximum silence duration in seconds (default: 15.0)AUDIO_VALID_SECONDS
: The minimum duration of continuous audio required for an input to be considered valid (default: 15.0)
The system accepts two SRT input streams:
- Port 8888: Primary studio input (Studio A)
- Port 9999: Secondary studio input (Studio B)
All connections require encryption using the passphrase configured in SRT_PASSPHRASE
.
# Stream from ALSA audio device (Linux)
ffmpeg -f alsa -channels 2 -sample_rate 48000 -i hw:0 \
-codec:a pcm_s16le -vn -f matroska \
"srt://liquidsoap.example.com:8888?passphrase=your_passphrase&mode=caller&transtype=live&latency=10000"
# Stream from file (for testing)
ffmpeg -re -i input.mp3 -c copy -f mpegts \
"srt://liquidsoap.example.com:8888?passphrase=your_passphrase&mode=caller"
For production use, consider using rpi-audio-encoder for a dedicated hardware encoder.
The SRT listening ports can be customized via environment variables:
SRT_PORT_PRIMARY
: Primary studio input port (default: 8888)SRT_PORT_SECONDARY
: Secondary studio input port (default: 9999)
The system supports optional DAB+ output using ODR-AudioEnc. When configured, it encodes audio for digital radio transmission.
To enable DAB+ output, set these environment variables:
# Required to enable DAB+
DAB_BITRATE=128 # Encoder bitrate in kbps
DAB_EDI_DESTINATIONS=tcp://dab-mux.example.com:9001 # EDI output destination(s)
# Optional PAD metadata
DAB_METADATA_SOCKET=padenc.sock # Socket for PAD encoder
DAB_METADATA_SIZE=58 # PAD size (default: 58)
You can send DAB+ streams to multiple destinations by providing comma-separated URLs:
DAB_EDI_DESTINATIONS=tcp://primary.example.com:9001,tcp://backup.example.com:9002
PAD allows sending metadata like song titles and station logos alongside the audio:
- Size 16: Text only
- Size 58: Text + small logo (default)
- Size 128: Text + album artwork
Radio Rucphen and BredaNu require DME output for distribution through the Dutch public broadcasting system. DME configuration is handled in station-specific configuration files.
All DME variables must be set for these stations:
# Primary ingestion point
DME_PRIMARY_HOST=ingest1.dme.nl
DME_PRIMARY_PORT=8000
DME_PRIMARY_USER=station-live
DME_PRIMARY_PASSWORD=secret
# Secondary ingestion point
DME_SECONDARY_HOST=ingest2.dme.nl
DME_SECONDARY_PORT=8000
DME_SECONDARY_USER=station-backup
DME_SECONDARY_PASSWORD=secret
# Stream mount point
DME_MOUNT_POINT=/live
For now-playing information and metadata routing, see the zwfm-metadata project.
No audio output
- Check if SRT ports (8888/9999) are accessible through your firewall
- Verify the
SRT_PASSPHRASE
matches between encoder and Liquidsoap - Check Docker logs:
docker compose logs -f
Stream keeps switching sources
- Increase
SILENCE_SWITCH_SECONDS
for unstable connections - Check network stability between encoder and server
- Verify encoder is sending continuous audio
Icecast connection failed
- Verify
ICECAST_HOST
andICECAST_PORT
are correct - Check
ICECAST_SOURCE_PASSWORD
matches server configuration - Ensure Icecast server is running and accessible
StereoTool not processing
- Verify
STEREOTOOL_LICENSE
is set correctly - Check web interface at port 8080
- Review Docker logs for license validation errors
# View all logs
docker compose logs -f
# Check service status
docker compose ps
# Restart services
docker compose restart
# Validate configuration
docker run --rm -v "$PWD:/app" -w /app savonet/liquidsoap:latest liquidsoap -c conf/*.liq
├── conf/
│ ├── lib/ # Shared library modules
│ │ ├── defaults.liq # Environment variables and defaults
│ │ ├── studio_inputs.liq # SRT input handling
│ │ ├── icecast_outputs.liq # Streaming outputs
│ │ ├── stereotool.liq # Audio processing
│ │ └── dab_output.liq # DAB+ encoding
│ ├── zuidwest.liq # ZuidWest FM configuration
│ ├── rucphen.liq # Radio Rucphen configuration
│ └── bredanu.liq # BredaNu configuration
├── docker-compose.yml # Main Docker composition
├── docker-compose.*.yml # Station-specific compositions
└── Dockerfile # Multi-arch container image
# Clone repository
git clone https://github.com/oszuidwest/zwfm-liquidsoap.git
cd zwfm-liquidsoap
# Build Docker image
docker buildx build --platform linux/amd64,linux/arm64 -t zwfm-liquidsoap:local .
# Run with custom image
docker compose up -d
- Fork the repository
- Create a feature branch
- Make your changes
- Run syntax validation:
docker run --rm -v "$PWD:/app" -w /app savonet/liquidsoap:latest liquidsoap -c conf/*.liq
- Submit a pull request
Copyright 2025 Omroepstichting ZuidWest & Stichting BredaNu. This project is licensed under the MIT License. See LICENSE file for details.
- Liquidsoap - The amazing audio streaming language
- Icecast - Reliable streaming server
- StereoTool - Professional audio processing
- Opendigitalradio - DAB+ tools and community