fallback, switch, rotate and random all do the same thing: at any moment
they pick one of their sources and stream it. Picking is the easy part. The hard
part — and the one that has generated more mailing list threads than anything
else in Liquidsoap — is what happens between two sources, when one stops
being streamed and another takes over.
PR #5077 reworks that entirely.
The problem with switch-wide parameters
Until now, track_sensitive, transitions, transition_length, override and
replay_metadata were parameters of the switch: one value, applied to everyone.
That works right up until your sources disagree — which, on a real radio, they
always do:
radio = fallback(track_sensitive=false, [live, jingles, music])
You wanted “the live show interrupts whatever is playing”. You also got “a
jingle can chop a song in half”, because track_sensitive belonged to the
fallback, not to the sources. Set it back to true and now the DJ has to wait
for the current song to end before going on air.
There was no way to say live interrupts, jingles wait. The switch had exactly one opinion to give and three sources that needed different ones.
Composition as a handoff
In the new model, composition is not configured on the switch at all. It is negotiated between the leaving source and the entering one, and each source carries its own answer.
live = input.harbor("live")
music = playlist("~/music")
radio = fallback([live, music])
No parameters, and this already does the right thing: the DJ cuts in mid-song with the music faded out underneath, and when they disconnect the playlist comes back on a fresh track rather than resuming the one that was interrupted.
Add jingles, and they wait:
radio = rotate([music.{weight = 3}, jingles.{weight = 1}])
Both are file sources, so neither is willing to interrupt the other: three songs, then a jingle, each starting cleanly at a track boundary with no fade — nothing was interrupted, so there is nothing to fade. And crucially, putting a live input next to them changes none of that. Every handoff involves exactly two sources, and only those two have a say.
Sometimes a file source should interrupt. Emergency announcements pushed into a queue need to go out now, not after the current song:
announcements = request.queue()
radio = fallback([announcements.{track_sensitive = false}, music])
Other things a source can ask for have nothing to do with interrupting at all — never being picked twice in a row, for instance:
radio = rotate([music, jingles.{single = true}])
You did not configure any of this on the switch. input.harbor is a live source
and playlist is a file source, each with its own defaults; when a source does
not behave like its type suggests, you say so once, on the source, and every
switch it appears in follows.
From there it is per-source methods all the way down: track_sensitive,
replay_metadata, single, weight, composition_type, plus on_select and
on_leave for writing your own transitions and cleanup:
def my_transition({ending, starting, replay_metadata = _}) =
if null.defined(ending) then
old = max_duration(3., null.get(ending))
(add([fade.out(duration=3., old), fade.in(duration=3., starting)]) : source)
else
starting
end
end
radio = fallback([s1.{on_select = my_transition}, s2])
ending is the source being left, and it is null when nothing was interrupted
— so the else branch is the “started at a boundary” case, where there is
nothing to blend. That distinction was impossible to express with the old
transitions parameter.
Go read the docs
This is a genuinely different way of thinking about switching, and it is worth reading the whole thing rather than pattern-matching from a blog post:
- Source composition —
the concepts, walked through the setups you are likely to build: live shows,
jingles, scheduled shows, backup files, emergency announcements, relays and
custom transitions. It also covers the details this post glosses over — when a
fade actually applies, how metadata replay interacts with a source’s own
metadata, and why an
on_selectthat mixesendingneeds a duration bound. - Migration notes —
this is a breaking change.
transitions,transition_lengthandoverrideare gone from switch operators, andtrack_sensitive/replay_metadatamoved to the sources. The mapping is mechanical, andsource.composition.legacy_on_selectrestores the old no-fade behaviour where you want it. A handful of stdlib operators (append,prepend,fallback.skip,map_first_track,overlap_sources) also stopped pinningtrack_sensitiveand now inherit it — worth a look if you use them.
The short version of why it is worth the churn: the old API asked you to configure a switch. The new one asks each source to describe itself once, and then every switch it appears in does the right thing on its own.