InterLude is a small macOS menu bar app that plays BBC radio about a minute behind live and quietly mutes the news bulletins. It plays on the Mac itself, on Chromecast speakers and groups, or on Sonos speakers and groups. It also runs as a web app on a Linux server (see Running the server).
I wanted 6 Music in the background all day without the hourly news, so it is tuned for music stations. Speech stations, and streams with no news such as Radio 1 Dance, play normally.
On the Mac: playing live, muting a bulletin, and an earlier programme with the news cut out. Phones and the server's web app look the same.
Unofficial. InterLude is not affiliated with the BBC. It relies on stream addresses the BBC does not document and can change at any time. It sends no telemetry: its only network traffic is to BBC servers, and to speakers and browsers on your own network.
- Plays any of 64 BBC stations (national, nations and local) a fixed time behind live (60 seconds by default).
- Recognises each bulletin as it arrives and replaces it with silence, or with ocean waves, a woodland stream or birdsong if you prefer. When the bulletin ends, the station comes back.
- Plays on the Mac, on a Chromecast or Cast group, or on a Sonos room or group, and you can switch speaker while it plays. Speakers appear and disappear in the list as they join and leave the network, and Sonos groups follow what you set up in the Sonos app.
- Earlier plays a programme from the last 30 days from its start, with the bulletins cut out rather than muted. The last 11 hours come from the live stream; anything older comes from the programme's BBC Sounds episode.
- One interface everywhere: the Mac's menu bar panel, the phone remote and the server's web app are the same page, so they look and work alike. On a wide window, Earlier and the news sit beside the player.
- A phone remote: scan a QR code and control InterLude from a phone on the same Wi-Fi, or from anywhere on your tailnet if the Mac runs Tailscale.
- Learns a station's news theme from the last few hours of broadcast, the first time you play it (so far proven on 6 Music).
- Keeps a log of every decision, so a wrong mute can be explained and the rules improved.
- Runs as a server too: a Docker image for Linux that does all of the above with no Mac, played and controlled from any browser, and playing on Chromecast and Sonos speakers itself.
The BBC's live streams are HLS: a playlist of 6.4 second audio segments. InterLude downloads the segments itself, analyses them, and serves its own playlist from the Mac to the player. Because it runs about a minute behind live, it has heard each bulletin begin before you do.
A bulletin is found in a few ways, all running on the machine itself:
- Speech near a bulletin slot. Apple's built-in sound classifier scores speech and music every 1.5 seconds. Twenty seconds of sustained speech close to a bulletin slot starts a mute. Slots start at :00 and :30; once a station has a dozen confirmed bulletins, its slots follow where they actually start, so a station with news only on the hour stops listening at :30, and one with bulletins at :45 starts listening there after News got through has marked a few. Setting the bulletin minutes in Settings fixes them instead. A station can also list long bulletins at fixed UK times, such as Newsbeat's 15 minutes at 12:45 on Radio 1, 1Xtra and Asian Network: InterLude listens around them too, lets the mute run their length, and needs 30 seconds of music, not 5, before it ends, since they play clips of songs between reports.
- The news theme. InterLude learns the station's news jingle from recent bulletins and starts the mute when it hears it, so the presenter's handover still plays.
- The newsreader's words. From a minute before to three minutes after each bulletin slot, on-device speech recognition transcribes the stream as it arrives. The newsreader's opening ("BBC News at five thirty", "Newsbeat, I'm …") starts a mute straight away. A presenter's handover ("here's the news with …") starts one too, but only if speech carries on for ten seconds after it, and never when it's a trail such as "after the news" or "the news is next". While a bulletin is muted, it listens for the sign-off ("…your next update's at six thirty", "That's 6 Music News for tonight") and ends the mute there, so trails and the presenter's chat afterwards are not lost. If no sign-off is heard, the mute ends when music returns.
Wording used across the BBC, such as "BBC News at" and "your next update", works on any station. What a station calls its own news ("Newsbeat", "6 Music News") is listed as newsNames in stations.json, so a station is added or corrected there rather than in code. Sign-offs are only looked for well into a bulletin: 45 seconds at first, then 60 % of the station's shortest usual bulletin once it has learned a few.
The server version does the same with open models in place of Apple's: YAMNet for speech and music, and whisper.cpp for the newsreader. YAMNet's scores are mapped onto Apple's, so the same rules apply to both.
Muted segments are replaced one for one with segments carrying the same timestamps, so the player sees one continuous stream and the delay never drifts. If a mute is wrong, Not news brings the sound back within a few seconds, and News got through teaches it from a bulletin it missed.
Switch the panel from Live to Earlier, step back through the days, and pick a programme. Looking through Earlier, or another station's programmes, doesn't stop what's playing; back in Live, Play … live instead switches to the station live. A picked programme plays from its start:
- From the live stream if it began in the last 11 hours or so. The BBC's live origin lists six hours of segments but keeps about eleven, so InterLude fetches them by index.
- On demand if it is older (up to 30 days) and has finished. InterLude asks the BBC's media selector for the episode, opens the HLS stream it offers, and lines the episode's segments up with the time they were broadcast, so the detector works exactly as it does live.
Because the whole programme already exists, the analysis runs about ten minutes ahead of you, faster than real time. That leaves time for one more check: InterLude transcribes from a minute before to two minutes after each of the station's bulletin minutes, and the newsreader's opening starts a bulletin there even when the presenter talked straight into the news. A bulletin is then left out rather than silenced, and the segments after it are given timestamps that carry straight on, so the programme simply continues. The panel shows how far through you are, where the news was, and how much has been skipped. Seeking within a programme isn't supported yet.
The details are in docs/on-demand.md.
Sonos speakers can't play the HLS playlist a Chromecast or a browser plays, so InterLude gives them one continuous AAC stream made from it, live.aac beside live.m3u8, and starts it on the speaker as an internet radio station. The stream reads InterLude's own playlist as any player does, so the delay and the muted or skipped news are the same on every speaker. Volume is the group's volume, and a room grouped in the Sonos app plays through its group.
The speaker's own buttons and the Sonos app work alongside InterLude, which asks the speaker every second what it is doing:
- Volume changed there moves InterLude's slider within a second or two, and the slider sets the speaker's volume as usual.
- Pause or stop there stops InterLude, as its own Stop would.
- Play there starts InterLude again, if that speaker is the one chosen. It plays what InterLude's Play would, starting afresh about a minute behind live rather than where it was paused, so it takes a few seconds.
- Playing something else there, or regrouping the room away, ends InterLude's playback and says so.
If a Sonos speaker is on another network whose Bonjour announcements don't reach the Mac, such as a separate VLAN for smart devices, give the Mac one speaker's address and it finds the rest from that one.
Turn on Settings › Phone remote and point a phone's camera at the QR code. The remote opens in the phone's browser, with nothing to install, and is the same as the Mac's panel: play and stop, volume, station, speaker, favourites, whether a bulletin is muted, recent mutes, and the Not news and News got through buttons. Its Settings has the station's news options; the Mac's own (start at login, port, the phone remote itself) stay in the Mac's panel. Its Earlier switch lists each day's programmes from the last 30 days; tap one to play it from the start, and the phone shows how far in you are and how much news was skipped. On an iPhone, Share › Add to Home Screen keeps it as an icon.
The remote is served by the same small web server the speakers use. Its address carries a long random code, so other people on the network can't guess it; Reset link makes a new one and the old link stops working. The Mac has to be awake with InterLude open. In Live, Play on the phone plays live; in Earlier it plays the programme last picked from its start, or live if none was. If the Mac's firewall is on, macOS asks once whether InterLude may accept incoming connections.
The design, the measurements behind each rule and the mistakes along the way are written up in docs/archive/findings.md.
- For the Mac app, macOS 26 or later on Apple silicon
- For the server, an x86-64 Linux machine with Docker and about 1 GB of memory free
- The BBC's UK stream addresses, which may not play outside the UK
Get the zip from the latest release, unzip it and move InterLude.app to Applications.
InterLude is signed but not notarised by Apple (that needs a paid developer account), so macOS blocks it the first time:
- Double-click InterLude. macOS says it can't be opened; click Done.
- Open System Settings › Privacy & Security and scroll down to the message about InterLude.
- Click Open Anyway, confirm with your password or Touch ID, then click Open.
You only need to do this once. The first time you look for speakers, macOS also asks to let InterLude find devices on your local network; allow it, or Chromecasts and Sonos speakers won't appear.
If you'd rather not run an app downloaded from the internet, build it yourself instead.
For the server, see Running the server.
You'll need Xcode.
./scripts/build-app.sh
open build/InterLude.app
The script builds build/InterLude.app and signs it with your Apple Development certificate if you have one (a free Apple ID is enough), or ad hoc otherwise. A stable signature stops macOS asking for local network permission after every rebuild. SANDBOX=0 ./scripts/build-app.sh builds without the App Sandbox.
Builds go to ~/Library/Caches/InterLudeBuild, because codesign rejects files with the extended attributes that iCloud-synced folders such as Documents add.
./scripts/package-release.sh builds the app and zips it into dist/ for a release.
To run the tests you also need ffmpeg and the libraries the server's speech models use, from Homebrew:
brew install ffmpeg onnxruntime whisper.cpp
Then:
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test --scratch-path ~/Library/Caches/InterLudeBuild
The Mac's panel is the web app in Web/, shown in a web view and served by the app's own local server, so the panel, the phone remote and the server's page are one set of files. To see the panel's states without playing anything, launch with --demo=playing, --demo=muted, --demo=upcoming, --demo=error, --demo=earlier, --demo=earlier-past, --demo=replay, --demo=settings or --demo=settings-end. Add --snapshot=<file.png> to save the window as a picture and quit, as for the screenshots here, and --port <number> to run beside an installed copy that already uses its port.
InterLude also runs as a web app on a Linux machine, so it works with no Mac involved: any phone, tablet or computer plays and controls it in a browser, and it can play on Chromecast and Sonos speakers itself. It uses open models in place of Apple's: YAMNet for speech and music and whisper.cpp for the newsreader, both running on the server's CPU. Nothing is sent to a speech service.
In a browser: on a phone, and on a computer with today's programmes beside an earlier one.
It ships as a Docker image for x86-64 Linux. You need Docker with Compose, and about 1 GB of memory free.
mkdir -p interlude/config && cd interlude
curl -fsSLO https://raw.githubusercontent.com/MstrThePlague/InterLude/main/compose.yaml
docker compose pull && docker compose up -d
docker compose logs | grep "Open the app"
The log gives the app's address, http://<server>:8642/app/<code>/. The long code keeps other people on your network out; Settings › Reset link makes a new one. Open it on a phone and add it to the Home Screen to keep it as an app. Settings also shows a QR code for opening it on another device.
The server starts playing when a browser asks for it and stops 10 minutes after the last one stops listening. Play and Stop act on the browser you press them in. With a speaker chosen, they start and stop the speaker instead, and a Sonos speaker's own Play and Pause buttons and volume work too (see Sonos speakers).
Your network. Browsers on your LAN reach the server on TCP 8642. Speakers need two more things:
- The stream: the speakers must be allowed to fetch it from the server on TCP 8642.
- Discovery: the server must see the speakers' multicast DNS, port 5353. On a network split into VLANs, add the server's network to the gateway's mDNS proxy or reflector. The server listens all the time, so speakers appear as they join the network and go when they leave.
- Sonos control: the server talks to Sonos speakers on their TCP port 1400. If multicast DNS can't reach a Sonos speaker, name one with
INTERLUDE_SONOS_HOSTSincompose.yaml(or--sonos-host): any one speaker describes all the household's rooms and groups.
If the server runs a firewall such as ufw, allow those ports in from your networks.
Away from home. With Tailscale on the server, serve the app over HTTPS on your tailnet:
sudo tailscale serve --bg --https=8443 http://127.0.0.1:8642
Then open https://<server's tailnet name>:8443/app/<code>/. Speakers still use the LAN address.
Updating: docker compose pull && docker compose up -d. Each release tag publishes the image as ghcr.io/mstrtheplague/interlude-server. Each change to the server on main publishes :edge too; set INTERLUDE_TAG=edge to follow it. To build from a checkout instead: docker compose up -d --build. Only the web app's files sit after the Swift build, so a change to the page alone rebuilds in seconds.
Changing the page or stations without a rebuild. The config folder beside compose.yaml can hold a stations.json and a web folder with index.html, app.css and app.js. Each one there is used in place of the image's own. The server reads them again within a few seconds of a change, carries on playing (unless the playing station's stream address changed, when it starts again), and open pages reload themselves. From a checkout, scripts/push-config.sh <host> copies Web/ and Data/stations.json across. Delete a file from config to go back to the image's copy.
Backing up. Settings, station profiles and learned news jingles live in the data volume. To copy them out:
docker run --rm -v interlude_data:/data -v "$PWD":/backup alpine tar czf /backup/interlude-data.tgz -C /data .
The volume is named after the folder compose.yaml is in, so for a folder called interlude it's interlude_data. Run docker volume ls to check. A jingle the Mac app has learned can be copied across: put its profiles/<station>.json and .candidates files into the volume's profiles folder and restart.
| File | What it holds |
|---|---|
Application Support/InterLude/stations.json |
The station list and stream address template. Edit a station's pool ID here if the BBC changes it, its newsNames to teach it what the station calls its news, set "news": false for a stream with no bulletins, "longBulletins": [{"at": "12:45", "minutes": 15}] for bulletins longer than the longest mute (UK time), or "untested": true for one where the speech rule hasn't been checked (new profiles then start in shadow mode and mute only on the jingle, as Radio 2 and Radio 3 do); the change takes effect within a few seconds. |
Application Support/InterLude/web/ |
Optional index.html, app.css or app.js to use in place of the app's own, for trying changes to the panel and phone remote without a rebuild. |
Application Support/InterLude/profiles/ |
Per-station settings, bulletin minutes and the learned news theme. |
Application Support/InterLude/events.jsonl |
One line per detected bulletin and per correction. Settings › Export diagnostics zips these up. |
Inside the App Sandbox, Application Support is the app's own container under ~/Library/Containers.
The server keeps the same files in its data volume (/var/lib/interlude in the container), plus settings.json, which holds its settings and the code in the app's link. A station list or web app in its config folder (/etc/interlude) is used in place of these and the image's own.
| Folder | Contents |
|---|---|
Sources/InterLudeCore |
Everything that isn't user interface or tied to Apple's frameworks: stream handling, MPEG-TS, detection, playout, the phone remote's routes and state, the Cast protocol, and Sonos control and its AAC stream. Builds and tests on Linux too. |
Sources/InterLudeApple |
The Mac's implementations of what the core asks the platform for: AAC decoding and encoding, the sound classifier, speech recognition, signal maths, the local server, and Cast and Sonos discovery. |
Sources/InterLudePortable |
Implementations that work away from the Mac, on Linux and Windows: decoding and encoding audio with ffmpeg. |
Sources/InterLudeHTTP |
The web server for macOS and Linux, on Hummingbird. |
Sources/InterLudeML |
The server's speech models: YAMNet on ONNX Runtime for speech and music, whisper.cpp for the newsreader. scripts/yamnet converts and calibrates YAMNet. |
Sources/InterLudeCast |
Casting from Linux: a Chromecast client on SwiftNIO, and a multicast DNS watcher that follows Chromecasts and Sonos speakers as they come and go. |
Sources/COnnxRuntime, CWhisper, CGGML |
Module maps that let Swift call the ONNX Runtime, whisper.cpp and ggml C libraries. |
Sources/InterLudeServer |
interlude-server, InterLude as a web app on a Linux host. Dockerfile and compose.yaml build and run it; interlude-server --check <audio> runs its models on a file. |
Sources/InterLudeApp |
The Mac app: the menu bar item and window, which show the web app in a web view, and the model behind it. |
Web |
The web app, InterLude's one interface: index.html, app.css and app.js, served from disk as the Mac's panel, its phone remote and the server's page, plus the server's icons. |
Tests |
Unit tests for the parts that can be tested without a network or speakers. |
Tools |
Command-line programs used to measure and tune detection (below). |
docs |
Notes on on-demand playback, release notes and screenshots. docs/archive holds the measurements and reasoning from early development. |
scripts |
Building and packaging the Mac app, installing the server's native libraries on Linux, preparing YAMNet, and push-config.sh, which puts the web app and station list on a running server without rebuilding its image. |
Run with swift run --scratch-path ~/Library/Caches/InterLudeBuild <name> from the checkout. Each tool's source begins with its full usage. Recordings are for personal testing and are kept out of git.
| Tool | Use |
|---|---|
record <serviceId> <hoursAgo> <minutes> <dir> |
Save segments from the BBC's six hour rewind window. |
classify <dir> <csv> |
Speech and music scores for a recording. CLASSIFIER=yamnet:<models dir> scores with the server's YAMNet instead (YAMNET_RAW=1 before calibration). |
evaluate <csv> [labels.json] [profile.json] |
Replay the speech rule over scores and report accuracy against labelled bulletins. |
ident-check <dir>:<start>,… … |
Find and test a station's news theme in recordings, given bulletin start times from the event log. PLATFORM=portable decodes with ffmpeg and uses the server's signal maths. |
signoff-check <dir>:<start>,<end> … |
Transcribe bulletins and report where the newsreader opens and signs off. <dir>:slots instead transcribes around every quarter hour of a recording, for finding a new station's bulletins and wording. STATION=<serviceId> uses that station's news names; WHISPER=<model file> uses whisper.cpp, as the server does. |
The sounds offered in place of bulletins are trimmed from recordings by Joseph Sardin at BigSoundBank, released into the public domain (CC0): sea waves #0698, forest and stream #2 #2714, and forest #3 #2715.
The server is built with Hummingbird and SwiftNIO (Apache 2.0). Its image also includes:
- YAMNet, Google's AudioSet classifier, converted to ONNX (Apache 2.0)
- whisper.cpp and its base.en model (MIT)
- ONNX Runtime (MIT)
- ffmpeg, from Ubuntu (LGPL and GPL)
- hls.js (Apache 2.0) and qrcode-generator (MIT), for the web page
MIT. See LICENSE.
BBC audio, artwork and logos are the BBC's and are never stored in this repository. Album art and track names shown in the panel and the web app are fetched from the BBC while they run.




