Clocks
In the quickstart and in the introduction to liquidsoap sources, we have described a simple world in which sources communicate with each other, creating and transforming data that composes multimedia streams. In this simple view, all sources produce data at the same rate, animated by a single clock: at every cycle of the clock, a fixed amount of data is produced.
While this simple picture is useful to get a fair idea of what's going on in liquidsoap, the full picture is more complex: in fact, a streaming system might involve multiple clocks, or in other words several time flows.
It is only in very particular cases that liquidsoap scripts need to mention clocks explicitly. Otherwise, you won't even notice how many clocks are involved in your setup: indeed, liquidsoap can figure out the clocks by itself, much like it infers types. Nevertheless, there will sometimes be cases where your script cannot be assigned clocks in a correct way, in which case liquidsoap will complain. For that reason, every user should eventually get a minimum understanding of clocks.
In the following, we first describe why we need clocks. Then we go through the possible errors that any user might encounter regarding clocks. Finally, we describe how to explicitly use clocks, and show a few striking examples of what can be achieved that way.
Why multiple clocks
The first reason is external to liquidsoap: there is simply not a unique notion of time in the real world. Your computer has an internal clock which indicates a slightly different time than your watch or another computer's clock. Moreover, when communicating with a remote computer, network latency causes extra time distortions. Even within a single computer there are several clocks: notably, each soundcard has its own clock, which will tick at a slightly different rate than the main clock of the computer. Since liquidsoap communicates with soundcards and remote computers, it has to take those mismatches into account.
There are also some reasons that are purely internal to liquidsoap:
in order to produce a stream at a given speed,
a source might need to obtain data from another source at
a different rate. This is obvious for an operator that speeds up or
slows down audio (stretch
). But it also holds more subtly
for cross
, smart_cross
as well as the
derived operators: during the lapse of time where the operator combines
data from an end of track with the beginning of the other other,
the crossing operator needs twice as much stream data. After ten tracks,
with a crossing duration of six seconds, one more minute will have
passed for the source compared to the time of the crossing operator.
In order to avoid inconsistencies caused by time differences, while maintaining a simple and efficient execution model for its sources, liquidsoap works under the restriction that one source belongs to a unique clock, fixed once for all when the source is created.
The graph representation of streaming systems can be adapted
into a good representation of what clocks mean.
One simply needs to add boxes representing clocks:
a source can belong to only one box,
and all sources of a box produce streams at the same rate.
For example,
output.icecast(fallback([crossfade(playlist(...)),jingles]))
yields the following graph:
Here, clock_2 was created specifically for the crossfading operator; the rate of that clock is controlled by that operator, which can hence accelerate it around track changes without any risk of inconsistency. The other clock is simply a wallclock, so that the main stream is produced following the “real” time rate.
Error messages
Most of the time you won't have to do anything special about clocks: operators that have special requirements regarding clocks will do what's necessary themselves, and liquidsoap will check that everything is fine. But if the check fails, you'll need to understand the error, which is what this section is for.
Disjoint clocks
On the following example, liquidsoap will issue the fatal error
a source cannot belong to two clocks
:
s = playlist("~/media/audio") output.alsa(s) # perhaps for monitoring output.icecast(mount="radio.ogg",%vorbis,crossfade(s))
Here, the source s
is first assigned the ALSA clock,
because it is tied to an ALSA output.
Then, we attempt to build a crossfade
over s
.
But this operator requires its source to belong to a dedicated
internal clock (because crossfading requires control over the flow
of the of the source, to accelerate it around track changes).
The error expresses this conflict:
s
must belong at the same time to the ALSA clock
and crossfade
's clock.
Nested clocks
On the following example, liquidsoap will issue the fatal error
cannot unify two nested clocks
:
jingles = playlist("jingles.lst") music = rotate([1,10],[jingles,playlist("remote.lst")]) safe = rotate([1,10],[jingles,single("local.ogg")]) q = fallback([crossfade(music),safe])
Let's see what happened.
The rotate
operator, like most operators, operates
within a single clock, which means that jingles
and our two playlist
instances must belong to the same clock.
Similarly, music
and safe
must belong to that
same clock.
When we applied crossfading to music
,
the crossfade
operator created its own internal clock,
call it cross_clock
,
to signify that it needs the ability to accelerate at will the
streaming of music
.
So, music
is attached to cross_clock
,
and all sources built above come along.
Finally, we build the fallback, which requires that all of its
sources belong to the same clock.
In other words, crossfade(music)
must belong
to cross_clock
just like safe
.
The error message simply says that this is forbidden: the internal
clock of our crossfade cannot be its external clock – otherwise
it would not have exclusive control over its internal flow of time.
The same error also occurs on add([crossfade(s),s])
,
the simplest example of conflicting time flows, described above.
However, you won't find yourself writing this obviously problematic
piece of code. On the other hand, one would sometimes like to
write things like our first example.
The key to the error with our first example is that the same
jingles
source is used in combination with music
and safe
. As a result, liquidsoap sees a potentially
nasty situation, which indeed could be turned into a real mess
by adding just a little more complexity. To obtain the desired effect
without requiring illegal clock assignments, it suffices to
create two jingle sources, one for each clock:
music = rotate([1,10],[playlist("jingles.lst"), playlist("remote.lst")]) safe = rotate([1,10],[playlist("jingles.lst"), single("local.ogg")]) q = fallback([crossfade(music),safe])
There is no problem anymore: music
belongs to
crossfade
's internal clock, and crossfade(music)
,
safe
and the fallback
belong to another clock.
The clock API
There are only a couple of operations dealing explicitly with clocks.
The function clock.assign_new(l)
creates a new clock
and assigns it to all sources from the list l
.
For convenience, we also provide a wrapper, clock(s)
which does the same with a single source instead of a list,
and returns that source.
With both functions, the new clock will follow (the computer's idea of)
real time, unless sync=false
is passed, in which case
it will run as fast as possible.
The old (pre-1.0.0) setting root.sync
is superseded
by clock.assign_new()
.
If you want to run an output as fast as your CPU allows,
just attach it to a new clock without synchronization:
clock.assign_new(sync=false,[output.file(%vorbis,"audio.ogg",source)])
This will automatically attach the appropriate sources to that clock. However, you may need to do it for other operators if they are totally unrelated to the first one.
The buffer()
operator can be used to communicate between
any two clocks: it takes a source in one clock and builds a source
in another. The trick is that it uses a buffer: if one clock
happens to run too fast or too slow, the buffer may empty or overflow.
Finally, get_clock_status
provides information on
existing clocks and track their respective times:
it returns a list containing for each clock a pair
(name,time)
indicating
the clock id its current time in clock cycles –
a cycle corresponds to the duration of a frame,
which is given in ticks, displayed on startup in the logs.
The helper function log_clocks
built
around get_clock_status
can be used to directly
obtain a simple log file, suitable for graphing with gnuplot.
Those functions are useful to debug latency issues.
External clocks: decoupling latencies
The first reason to explicitly assign clocks is to precisely handle the various latencies that might occur in your setup.
Most input/output operators (ALSA, AO, Jack, OSS, etc)
require their own clocks. Indeed, their processing rate is constrained
by external sound APIs or by the hardware itself.
Sometimes, it is too much of an inconvenience,
in which case one can set clock_safe=false
to allow
another clock assignment –
use at your own risk, as this might create bad latency interferences.
Currently, output.icecast
does not require to belong
to any particular clock. This allows to stream according to the
soundcard's internal clock, like in most other tools:
in output.icecast(%vorbis,mount="live.ogg",input.alsa())
,
the ALSA clock will drive the streaming of the soundcard input via
icecast.
Sometimes, the external factors tied to Icecast output cannot be
disregarded: the network may lag. If you stream a soundcard input
to Icecast and the network lags, there will be a glitch in the
soundcard input – a long enough lag will cause a disconnection.
This might be undesirable, and is certainly disappointing if you
are recording a backup of your precious soundcard input using
output.file
: by default it will suffer the same
latencies and glitches, while in theory it could be perfect.
To fix this you can explicitly separate Icecast (high latency,
low quality acceptable) from the backup and soundcard input (low latency,
high quality wanted):
input = input.oss() clock.assign_new(id="icecast", [output.icecast(%mp3,mount="blah",mksafe(buffer(input)))]) output.file( %mp3,"record-%Y-%m-%d-%H-%M-%S.mp3", input)
Here, the soundcard input and file output end up in the OSS
clock. The icecast output
goes to the explicitly created "icecast"
clock,
and a buffer is used to
connect it to the soundcard input. Small network lags will be
absorbed by the buffer. Important lags and possible disconnections
will result in an overflow of the buffer.
In any case, the OSS input and file output won't be affected
by those latencies, and the recording should be perfect.
The Icecast quality is also better with that setup,
since small lags are absorbed by the buffer and do not create
a glitch in the OSS capture, so that Icecast listeners won't
notice the lag at all.
Internal clocks: exploiting multiple cores
Clocks can also be useful even when external factors are not an issue. Indeed, several clocks run in several threads, which creates an opportunity to exploit multiple CPU cores. The story is a bit complex because OCaml has some limitations on exploiting multiple cores, but in many situations most of the computing is done in C code (typically decoding and encoding) so it parallelizes quite well.
Typically, if you run several outputs that do not share much (any) code,
you can put each of them in a separate clock.
For example the following script takes one file and encodes it as MP3
twice. You should run it as liquidsoap EXPR -- FILE
and observe that it fully exploits two cores:
def one() clock.assign_new(sync=false, [output.file(%mp3,"/dev/null",single(argv(1)))]) end one() one()