If you have ever tried to run an internet radio station, you have probably hit the same wall. You start with a playlist — maybe a folder of MP3s and a script that feeds them to Icecast. It works, until it does not. The stream dies at 3 AM and nobody notices. Two tracks play back-to-back with wildly different volumes. There is no crossfade, no jingles, no way to cut to a live input when a DJ shows up. You bolt on more scripts, more cron jobs, more glue. It gets ugly fast. This is the problem Liquidsoap solves.
What Liquidsoap is
Liquidsoap is a programming language designed specifically for audio and video streaming. Not a playlist manager. Not a config file for a streaming server. An actual statically typed language where sources of audio are first-class values that you can compose, transform, and route.
The idea started at the Ecole Normale Superieure de Lyon around 2004, when a group of students wanted a better way to run their campus radio. Twenty years later, it powers Radio France, AzuraCast, Radionomy, and countless community stations. The language has grown to handle video, HLS output, SRT ingest, speech synthesis, YouTube streaming, and more — but the core philosophy has not changed: simple things should be simple, complex things should be possible.
A basic Liquidsoap script reads like a description of what you want your radio to do:
music = playlist("~/Music")
jingles = playlist("~/Jingles")
s = rotate(weights=[1, 4], [jingles, music])
s = crossfade(s)
output.icecast(%mp3, host="localhost", port=8000, password="hackme", mount="/stream", s)
That is a radio station. Jingles every four tracks, crossfaded transitions, streaming to Icecast in MP3. If any of these sources fail — say the music directory gets unmounted — Liquidsoap detects it at type-check time, before the script even runs. If you want a live input that takes priority over the playlist, you add a fallback. If you want time-based scheduling, there is switch. The type system ensures you cannot accidentally create a stream that might go silent.
This is fundamentally different from chaining together ffmpeg commands, cron jobs, and shell scripts. Liquidsoap understands audio at a semantic level — clocks, synchronisation, fallibility, track boundaries — and handles the hard parts so you do not have to.
I have been building custom audio pipelines and wanted to iterate on .liq scripts locally — test the playlist logic, tweak fallback chains, verify encoding settings. The kind of thing you want a fast feedback loop for. So I reached for brew install and found nothing. No formula. Your options were wrestling with OPAM directly (slow, fragile, leaves a .opam directory in your home) or running the Docker image. Every friction point in installation is a potential community radio builder who gives up before writing their first script. I decided to fix that.
Bringing it to Homebrew
Liquidsoap is written in OCaml, which makes packaging non-trivial. It uses OPAM to pull in dozens of OCaml libraries at build time, dune as its build system, and has a complex relationship with system libraries like ffmpeg and libcurl. The build process is not a simple ./configure && make — it is closer to bootstrapping a small OCaml ecosystem inside a temporary directory, compiling everything, then extracting the binary and its runtime files. The binary also needs to find its standard library (.liq scripts) and unicode data at runtime, and the default build mode bakes in paths that assume a Linux FHS layout. Not exactly the kind of thing brew create handles for you.
The formula follows the same OPAM-based pattern that the handful of other OCaml formulas in homebrew-core (semgrep, flow, zero-install) use, with some liquidsoap-specific adaptations. Liquidsoap has three build modes for resolving runtime paths. The “posix” mode hardcodes paths like /usr/share/liquidsoap/libs — obviously wrong for Homebrew. The formula patches these at build time to point to Homebrew’s prefix, then builds with LIQUIDSOAP_BUILD_TARGET=posix. Instead of building the OCaml compiler from source (which takes 10+ minutes), the formula uses Homebrew’s OCaml package directly via --compiler=ocaml-system. OPAM gets a temporary root inside the build directory — nothing touches your home directory or persists after install.
The build steps boil down to:
- Patch runtime paths and a vendored library’s dune file (the
crymodule referencesbytes, a compat library removed in OCaml 5.x) - Initialize OPAM with the system compiler
- Install OCaml dependencies from the project’s opam files
- Install the ffmpeg OCaml bindings
- Build with dune
- Install, relocate man pages and stdlib files to Homebrew-conventional locations
- Copy camomile unicode data from the OPAM switch before it is cleaned up
The whole thing builds in about 2.5 minutes on an M1 Max.
Room to grow
The current formula is intentionally minimal — ffmpeg covers the most important use case. But there are bindings that ffmpeg does not replace: LADSPA/Lilv for external audio effect plugins, OSC for real-time control from hardware or apps, Prometheus for production monitoring, and sqlite3 for persistent metadata-driven playlists. These get compiled into the binary at build time — there is no runtime plugin system, and homebrew-core does not support --with-foo build options. If you need them today, install via OPAM directly or use a third-party tap. They are on my list to explore adding as default dependencies in future updates.
Note that the formula does not output audio to your speakers directly — output() falls back to output.dummy without a native audio backend. For local testing, write to a file and play it back:
liquidsoap 'output.file(%wav, fallible=true, "/tmp/test.wav", sine(duration=3.))'
afplay /tmp/test.wav
One thing that bothered me about the formula was the source patching. The posix build target hardcodes its runtime paths in a static OCaml file, so the formula had to inreplace six paths before building — fragile, version-sensitive, and the kind of thing that breaks silently on upgrades. The right fix was upstream: I sent a patch to Liquidsoap that replaces the static file with a dune generation rule, so packagers can set paths through environment variables instead. The defaults stay identical, but the Homebrew formula can now just set LIQUIDSOAP_LIBS_DIR and friends instead of rewriting source code. That PR has been accepted, so future formula updates will be considerably cleaner.
Three words from your own radio
The PR is up on homebrew-core. Once merged, anyone on macOS is three words from building their own radio. If you have been curious about internet radio, the Liquidsoap book and the community Discord are good places to start.
brew install liquidsoapgives you a working binary with ffmpeg support — encode, decode, and stream in virtually any format- The formula builds from source in about 2.5 minutes using your system OCaml — nothing persists in your home directory
- LADSPA, OSC, Prometheus, and sqlite3 support are candidates for future updates