Before getting into the more advanced radio setups which can be achieved with Liquidsoap, we need to understand the language and the general concepts behind it. If you are eager to start your radio, it might be a good idea to at least skim though this chapter quickly at a first reading, and come back later to it when a deeper knowledge about a specific point is required.
Liquidsoap is a novel language which was designed from scratch. We present the generic constructions, feature specifically related to streaming are illustrated in next chapter and further detailed in this chapter.
One of the main features of the language is that it is typed\index{type}. This means that
every expression belongs to some type which indicates what it is. For instance,
"hello"
is a string whereas 23
is an integer, and, when presenting a
construction of the language, we will always indicate the associated
type. Liquidsoap implements a typechecking algorithm which ensures that
whenever a string is expected a string will actually be given, and similarly for
other types. This is done without running the program, so that it does not
depend on some dynamic tests, but is rather enforced by theoretical
considerations. Another distinguishing feature of this algorithm is that it also
performs type inference: you never actually have to write a type, those are
guessed automatically by Liquidsoap. This makes the language very safe, while
remaining very easy to use. For curious people reading French, the algorithm and
the associated theory are described in a publication [@baelde2008webradio].
Incidentally, apart from the usual type information which can be found in many languages, Liquidsoap also uses typing to check the coherence of parameters which are specific to streaming. For instance, the number of audio channels of streams is also present in their type, and it ensures that operators always get the right number of channels.
The language is functional, which means that you can very easily define functions, and that functions can be passed as arguments of other functions. This might look like a crazy thing at first, but it is actually quite common in some language communities (such as OCaml). It also might look quite useless: why should we need such functions when describing webradios? You will soon discover that it happens to be quite convenient in many places: for handlers (we can specify the function which describes what to do when some event occurs such as when a DJ connects to the radio), for transitions (we pass a function which describes the shape we want for the transition) and so on.
The unique feature of Liquidsoap is that it allows the manipulation of sources which are functions which will generate streams. These streams typically consist of stereo audio data, but we do not restrict to this: they can contain audio with arbitrary number of channels, they can also contain an arbitrary number of video channels, and also MIDI channels (there is limited support for sound synthesis).
When running a Liquidsoap program, the compiler goes through these four phases:
- lexical analysis and parsing: Liquidsoap ingests your program and ensures that its syntax follows the rules,
- type inference and type checking: Liquidsoap checks that your program does not contain basic errors and that types are correct,
- compilation of the program: this produces a new program which will generate the stream (a stream generator),
- instantiation: the sources are created and checked to be infallible where required,
- execution: we run the stream generator to actually produce audio.
The two last phases can be resumed by the following fact: Liquidsoap is a stream generator generator, it generates stream generators (sic).
In order to illustrate this fact, consider the following script (don't worry if you don't understand all the details for now, it uses concepts which will be detailed below):
Let us explain how this script should be thought of as a way of describing how
to generate a stream generator. In order to construct the stream generator,
Liquidsoap will execute the function list.map
{.liquidsoap} which will produce
the list obtained by applying the function note
{.liquidsoap} on each element
of the list and, in turn, this function will be replaced by its definition,
which consists of a sine
generator. The execution of the script will act as if
Liquidsoap successively replaced the second line by
s = add([note(0.), note(3.), note(7.)])
and then by
s = add([sine(440. * pow(2., 0. / 12.)),
sine(440. * pow(2., 3. / 12.)),
sine(440. * pow(2., 7. / 12.))])
and finally by
s = add([sine(440.), sine(523.25), sine(659.26)])
which is the actual stream generator. We see that running the script has
generated the three sine
stream generators!
\index{standard library}
Although the core of Liquidsoap is written in OCaml, many of the functions of
Liquidsoap are written in the Liquidsoap language itself. Those are defined in
the stdlib.liq
script, which is loaded by default and includes all the
libraries. You should not be frightened to have a look at the standard library,
it is often useful to better grasp the language, learn design patterns and
tricks, and add functionalities. Its location on your system is indicated in the
variable configure.libdir
and can be obtained by typing
liquidsoap --check "print(configure.libdir)"
Scripts in Liquidsoap can be written in any text editor\index{editor}, but things are more convenient if there is some specific support. We have developed a mode for the Emacs editor which adds syntax coloration and indentation when editing Liquidsoap files. User-contributed support for Liquidsoap is also available for popular editors such as Visual Studio Code or vim.
When writing scripts you will often need details about a particular operator and
its arguments. We recall from earlier that the
documentation\index{documentation} of an operator operator
, including its type
and a description of its arguments, can be obtained by typing
liquidsoap -h operator
This documentation is also available on the website.
In order to test the functions that will be introduced in this section, it can be convenient to use the interactive mode\index{interactive mode} of Liquidsoap which can be used to type small expressions and immediately see their result. This interactive mode is rarely used in practice, but is useful to learn the language and do small experiments. It can be started with
liquidsoap --interactive
It will display a "#
", meaning it is waiting for expressions, which are
programs in the language. They have to be ended by ";;
" in order to indicate
that Liquidsoap should evaluate them. For instance, if we type
name = "Sam";;
it answers
name : string = "Sam"
which means that we have defined a variable name
whose type is string
and
whose value is "Sam"
. It can be handy as a calculator:
2*3;;
results in
- : int = 6
("-
" means that we did not define a variable, that the type of the expression
is int
and that it evaluates to 6). Also, variables can be reused: if we type
print("Hello #{name} and welcome!");;
it will answer
Hello Sam and welcome!
- : unit = ()
The command print
{.liquidsoap} was evaluated and displays its argument and
then the result is shown, in the same format as above: -
means that we did not
define a variable, the type of the result is unit
and its value is ()
. The
meaning of these is detailed below. In the following, all examples starting by
#
indicate that they are being entered in the interactive mode.
Another useful feature is the -i
option of Liquidsoap which displays the types
of variables in a file. For instance, if we have a file test.liq
containing
and we run
liquidsoap -i test.liq
it will display the types for x
and f
:
x : float
f : (int) -> int
meaning that x
is a floating point number and f
is a function taking an
integer as argument and returning an integer.
We begin by describing the values one usually manipulates in Liquidsoap.
The integers\index{integer}, such as 3
or 42
, are of type
int
\indexop{int}. Depending on the current architecture of the
computer on which we are executing the script (32 or 64 bits, the latter being
the most common nowadays) they are stored on 31 or 63 bits. The minimal
(resp. maximal) representable integer can be obtained as the constant
min_int
(resp. max_int
); typically, on a 64 bits
architecture, they range from -4611686018427387904 to 4611686018427387903.
The floating point numbers, such as 2.45
, are of type
float
\indexop{float}, and are in double precision, meaning that
they are always stored on 64 bits. We always write a decimal point in them,
so that 3
and 3.
are not the same thing: the former is an integer and the
latter is a float. This is a source of errors for beginners, but is necessary for
typing to work well. For instance, if we try to execute a program containing the
instruction
it will raise the error
At line 1, char 9:
Error 5: this value has type int but it should be a subtype of float
which means that the sine function expects a float as argument, but an integer
is provided. The fix here obviously consists in replacing "500
" by "500.
"
(beware of the dot).
The usual arithmetic operations are available (+
, -
, *
, /
), and work for
both integers and floats. For floats, traditional arithmetic functions are
available such as sqrt
(square root), exp
(exponential), sin
(sine), cos
(cosine) and so on. Random integers and floats can be generated with the
random.int
\index{random} and random.float
functions.
Strings\index{string} are written between double or single quotes,
e.g. "hello!"
or 'hello!'
, and are of type string
.
The function to output strings on the standard output is print
\indexop{print}, as
in
print("Hello, world!")
Incidentally, this function can also be used to display values of any type, so that
print(3+2)
will display 5
, as expected. In practice, one rarely does use this functions,
which displays on the standard output, but rather the logging\index{log} functions
log.critical
, log.severe
, log.important
, log.info
and log.debug
which
write strings of various importance in the logs, so that it is easier to keep
track of them: they are timestamped, they can easily be stored in files, etc.
In order to write the character ""
" in a string, one cannot simply type ""
"
since this is already used to indicate the boundaries of a string: this
character should be escaped\index{escaping}\index{string!escaping}, which
means that the character "\
" should be typed first so that
will actually display "My name is "Sam"!
". Other commonly used escaped
characters are "\\
" for backslash and "\n
" for new line. Alternatively, one
can use the single quote notation, so that previous example can also be written
as
This is most often used when testing JSON data which can contain many quotes or for
command line arguments when calling external scripts. The character "\
" can also
be used at the end of the string to break long strings in scripts without
actually inserting newlines in the strings. For instance, the script
will actually print
His name is Romain.
Note that there is no line change between "is" and "Romain", and the indentation before "Romain" is not shown either.
The concatenation of two strings is achieved by the infix operator "^
", as in
user = "dj"
print("Current user is " ^ user)
Instead of using concatenation, it is often rather convenient to use string
interpolation\index{string!interpolation}: in a string, #{e}
is replaced by the string representation of
the result of the evaluation of the expression e
:
user = "admin"
print("The user #{user} has just logged.")
will print The user admin has just logged.
or
print("The number #{random.float()} is random.")
will print The number 0.663455738438 is random.
(at least it did last time I
tried).
The string representation of any value in Liquidsoap can be obtained using the
function string
\indexop{string}, e.g. string(5)
{.liquidsoap} is "5"
. Some
other useful string-related function are
string.length
: compute the length of a string# string.length("abc");; - : int = 3
string.sub
: extract a substring# string.sub("hello world!", start=6, length=5);; - : string = "world"
string.split
\indexop{string.split}: split a string on a given character# string.split(separator=":", "a:42:hello");; - : [string] = ["a", "42", "hello"]
string.contains
: test whether a string contains (or begins or ends with) a particular substring,string.quote
: escape shell special characters (you should always use this when passing strings to external programs).
Finally, some functions operate on regular expressions\index{regular expression}, which describe some shapes for strings:
string.match
: test whether a string matches a regular expression,string.replace
: replace substrings matching a regular expression.
A regular expression R
or S
is itself a string where some characters have a
particular meaning:
.
means "any character",R*
means "any number of times something of the formR
",R|S
means "something of the formR
or of the forS
",
other characters represent themselves (and special characters such as .
, *
or .
have to be escaped, which means that \.
represents the character
.
). An example is worth a thousand words: we can test whether a string fname
corresponds to the name of an image file with
Namely, this function will test if fname
matches the regular expression
.*\.png|.*\.jpg
which means "any number of any character followed by .png
or
any number of any character followed by .jpg
".
The booleans\index{boolean} are either true
{.liquidsoap}\indexop{true} or
false
{.liquidsoap}\indexop{false} and are of type bool
\indexop{bool}. They can be combined
using the usual boolean operations
and
\indexop{and}: conjunction,or
\indexop{or}: disjunction,not
\indexop{not}: negation.
Booleans typically originate from comparison operators, which take two values and return booleans:
==
\indexop{==}: compares for equality,!=
\indexop{!=}: compares for inequality,<=
: compares for inequality,
and so on (<
, >=
, >
). For instance, the following is a boolean expression:
(n < 3) and not (s == "hello")
The time predicates such as 10h-15h
are also booleans, which are true or false
depending on the current time, see there.
Conditional branchings\indexop{if} execute code depending on whether a condition is true or not. For instance, the code
will print that the condition is satisfied when either x
is between 1 and 12
or the current time is not between 10h and 15h. A conditional branching might
return a value, which is the last computed value in the chosen branch. For
instance,
will assign "A"
or "B"
to y
depending on whether x
is below 3 or
not. The two branches of a conditional should always have the same return type:
x = if 1 == 2 then "A" else 5 end
will result in
At line 1, char 19-21:
Error 5: this value has type (...) -> string
but it should be a subtype of (...) -> int
meaning that "A"
is a string but is expected to be an integer because the
second branch returns an integer, and the two should be of same nature. The
else
branch is optional, in which case the then
branch should be of type
unit
:
if x == "admin" then print("Welcome admin") end
In the case where you want to perform a conditional branching in the
else
{.liquidsoap} branch, the elsif
{.liquidsoap} keyword should be used, as
in the following example, which assigns 0, 1, 2 or 3 to s
depending on whether
x
is "a"
, "b"
, "c"
or something else:
This is equivalent (but shorter to write) to the following sequence of imbricated conditional branchings:
Finally, we should mention that the notation c?a:b
is also available as a
shorthand for if c then a else b end
{.liquidsoap}, so that the expression
can be shortened to
(and people will think that you are a cool guy).
Some functions, such as print
, do not return a meaningful value: we are
interested in what they are doing (e.g. printing on the standard output) and not
in their result. However, since typing requires that everything returns
something of some type, there is a particular type for the return of such
functions: unit
\indexop{unit}. Just as there are only two values in the booleans (true
and
false
), there is only one value in the unit type, which is written ()
. This
value can be thought of as the result of the expression saying "I'm done".
In sequences of instructions, all the instructions but the last should be of type unit. For instance, the following function is fine:
This is a function printing "hello" and then returning 5, see
below for details about functions. Sequences of instructions
are delimited by newlines, but can also be separated by ;
in order to have
them fit on one line, i.e., the above can equivalently be written
However, the code
gives rise to the following warning
At line 2, char 2-4:
Warning 3: This expression should have type unit.
The reason is that this function is first computing the result of 3+5 and then
returning 2 without doing anything with the result of the addition, and the fact
that the type of 3+5
is not unit (it is int
) allows to detect that. It is
often the sign of a mistake when one computes something without using it; if
however it is on purpose, you should use the ignore
function to explicitly
ignore the result:
Some more elaborate values can be constructed by combining the previous ones. A first kind is lists\index{list} which are finite sequences of values, being all of the same type. They are constructed by square bracketing the sequence whose elements are separated by commas. For instance, the list
[1, 4, 5]
is a list of three integers (1, 4 and 5), and its type is [int]
, and the type
of ["A", "B"]
{.liquidsoap} would obviously be [string]
. Note that a list can
be empty: []
. The function list.hd
returns the head of the list, that is its
first element:
# list.hd([1, 4, 5]);;
- : int = 1
This function also takes an optional argument default
which is the value which
is returned on the empty list, which does not have a first element:
# list.hd(default=0, []);;
- : int = 0
Similarly, the list.tl
function returns the tail of the list, i.e. the list
without its first element (by convention, the tail of the empty list is the
empty list). Other useful functions are
-
list.add
: add an element at the top of the list# list.add(5, [1, 3]);; - : [int] = [5, 1, 3]
-
list.length
: compute the length of a list# list.length([5, 1, 3]);; - : int = 3
-
list.mem
: check whether an element belongs to a list# list.mem(2, [1, 2, 3]);; - : bool = true
-
list.map
: apply a function to all the elements of a list# list.map(fun(n) -> 2*n, [1, 3, 5]);; - : [int] = [2, 6, 10]
-
list.iter
: execute a function on all the elements of a list# list.iter(fun(n) -> print(newline=false, n), [1, 3, 5]);; 135- : unit = ()
-
list.nth
: return the n-th element of a list# list.nth([5, 1, 3], 2);; - : int = 3
(note that the first element is the one at index n=0).
-
list.append
: construct a list by taking the elements of the first list and then those of the second list# list.append([1, 3], [2, 4, 5]);; - : [int] = [1, 3, 2, 4, 5]
Another construction present in Liquidsoap is tuples\index{tuple} of values, which are finite sequences of values which, contrarily to lists, might have different types. For instance,
(3, 4.2, "hello")
is a triple (a tuple with three elements) of type
int * float * string
which indicate that the first element is an integer, the second a float and the
third a string. In particular, a pair\index{pair} is a tuple with two elements. For those,
the first and second element can be retrieved with the functions fst
\indexop{fst} and
snd
\indexop{snd}:
# p = (3, "a");;
p : int * string = (3, "a")
# fst(p);;
- : int = 3
# snd(p);;
- : string = "a"
For general tuples, there is a special syntax in order to access
their elements. For instance, if t
is the above tuple (3, 4.2, "hello")
{.liquidsoap}, we can write
let (n, x, s) = t
which will assign the first element to the variable n
, the second element to
the variable x
and the third element to the variable s
:
# t = (3, 4.2, "hello");;
t : int * float * string = (3, 4.2, "hello")
# let (n, x, s) = t;;
(n, x, s) : int * float * string = (3, 4.2, "hello")
# n;;
- : int = 3
# x;;
- : float = 4.2
# s;;
- : string = "hello"
A quite useful combination of the two previous data structures is association lists\index{association list}\index{list!association}, which are lists of pairs. Those can be thought of as some kind of dictionary: each pair is an entry whose first component is its key and second component is its value. These are the way metadata\index{metadata} are represented for instance: they are lists of pairs of strings, the first string being the name of the metadata, and the second its value. For instance, a metadata would be the association list
m = [("artist", "Frank Sinatra"), ("title", "Fly me to the moon")]
indicating that the artist of the song is "Frank Sinatra" and the title is "Fly
me to the moon". For such an association list, one can obtain the value
associated to a given key using the list.assoc
function:
list.assoc("title", m)
will return "Fly me to the moon"
, i.e. the value associated to
"title"
. Since this is so useful, we have a special notation for the above
function, and it is equivalent to write
m["title"]
to obtain the "title"
metadata. Other useful functions are
list.assoc.mem
: determine whether there is an entry with a given key,list.assoc.remove
: remove all entries with given key.
Apart from metadata, association lists are also used to store http headers
(e.g. in http.get
).
In passing, you should note the importance of parenthesis when defining pairs. For instance
["a", "b"]
is a list of strings, whereas
[("a", "b")]
is a list of pairs of strings, i.e. an association list.
We have already seen many examples of uses of variables\index{variable}: we use
x = e
in order to assign the result of evaluating an expression e
to a
variable x
, which can later on be referred to as x
. Variables can be masked:
we can define two variables with the same name, and at any point in the program the
last defined value for the variable is used:
will print 3
and 5
. Contrarily to most languages, the value for a variable
cannot be changed (unless we explicitly require this by using references, see
below), so the above program does not modify the value of n
, it is simply that
a new n
is defined.
There is an alternative syntax for declaring variables which is\indexop{def}
def x =
e
end
It has the advantage that the expression e
can spread over multiple lines and
thus consist of multiple expressions, in which case the value of the last one
will be assigned to x
, see also next section. This is
particularly useful to use local variables when defining a value. For instance,
we can assign to x
the square of sin(2) by
Note that we first compute sin(2) in a variable y
and then multiply y
by
itself, which avoids computing sin(2) twice. Also, the variable y
is local:
it is defined only until the next end
, so that
will print 5
: outside the definition of x
, the definition of y
one on the
first line is not affected by the local redefinition.
When we define a variable, it is generally to use its value: otherwise, why bothering defining it? For this reason, Liquidsoap issues a warning when an unused variable\index{variable!unused} is found, since it is likely to be a bug. For instance, on
Liquidsoap will output
Line 1, character 1:
Warning 4: Unused variable n
If this situation is really wanted, you should use ignore
\indexop{ignore} in order to fake a
use of the variable n
by writing
ignore(n)
Another possibility is to assign the special variable _
\indexop{_}, whose purpose is to
store results which are not going to be used afterwards:
_ = 2 + 2
As indicated above, by default, the value of a variable cannot be
changed. However, one can use a reference\index{reference} in order to be able to do this.
Those can be seen as memory cells, containing values of a given fixed type,
which can be modified during the execution of the program. They are created with
the ref
\indexop{ref} function, with the initial value of the cell as argument. For instance,
declares that r
is a reference which contains 5
as initial value. Since 5
is an integer (of type int
), the type of the reference r
will be
(() -> int).{set : (int) -> unit}
It might be difficult for you to read right now (the syntax for curly brackets will be explained in functions' section and records' section below), but all you need to know is that it indicates that, on such a reference, two operations are available:
-
one can obtain the value of the reference
r
by writingr()
, for instancex = r() + 4
declares the variable
x
as being 9 (which is 5+4), -
one can change the value of the reference by using the
:=
\indexop{:=} keyword, e.g.r := 2
will assign the value 2 to
r
.
The behavior of references can be illustrated by the following simple interactive session:
# r = ref(5);;
r : (() -> int).{set : (int) -> unit} = <fun>.{set=<fun>}
# r();;
- : int = 5
# r := 2;;
- : unit = ()
# r();;
- : int = 2
Note that the type of a reference is fixed: once r
is declared to be a
reference to an integer, as above, one can only put integers into it, so that
the script
will raise the error
Error 5: this value has type string
but it should be a subtype of int
which can be explained as follows. On the first line, the declaration r = ref(5)
{.liquidsoap} implies that r
is a reference to an int
since it
initially contains 5
which is an integer. However, on the second line, we try
to assign a string to r
, which would only be possible if r
was a reference
to a string.
The usual looping\index{loop} constructions are available in Liquidsoap. The for
\indexop{for} loop
repeatedly executes a portion of code with an integer variable varying between
two bounds, being increased by one each time. For instance, the following code
will print the integers 1
, 2
, 3
, 4
and 5
, which are the values
successively taken by the variable i
:
In practice, such loops could be used to add a bunch of numbered files
(e.g. music1.mp3
, music2.mp3
, music3.mp3
, etc.) in a request queue for
instance.
The while
\indexop{while} loop repeatedly executes a portion of code, as long a condition is
satisfied. For instance, the following code doubles the contents of the
reference n
as long as its value is below 10
:
The variable n
will thus successively take the values 1
, 2
, 4
, 8
and
16
, at which point the looping condition n() < 10
is not satisfied anymore
and the loop is exited. The printed value is thus 16
.
Liquidsoap is built around the notion of function\index{function}: most operations are performed
by those. For some reason, we sometimes call operators\index{operator} the functions acting on
sources. Liquidsoap includes a standard library which consists of functions
defined in the Liquidsoap language, including fairly complex operators such as
playlist
which plays a playlist or crossfade
which takes care of fading
between songs.
A function is a construction which takes a bunch of arguments and produces a
result. For instance, we can define a function f
taking two float arguments,
prints the first and returns the result of adding twice the first to the second:\indexop{def}
This function can also be written on one line if we use semicolons (;
) to
separate the instructions instead of changing line:
The type of this function is
(int, int) -> int
The arrow ->
\indexop{->} means that it is a function, on the left are the types of the
arguments (here, two arguments of type int
) and on the right is the type of
the returned value of the function (here, int
). In order to use this function,
we have to apply it to arguments, as in
f (3, 4)
This will trigger the evaluation of the function, where the argument x
(resp. y
) is replaced by 3
(resp. 4
), i.e., it will print 3
and return
the evaluation of 2*3+4
, which is 10
. Of course, generally, there is no
reason why all arguments and the result should have the same type as in the
above example, for instance:
# def f(s, x) = string.length(s) + int_of_float(x) end;;
f : (string, float) -> int = <fun>
As explained earlier, declarations of variables made inside the definition of a
function are local: they are only valid within this definition (i.e., until
the next end
). For instance, in the definition
the variable y
is not available after the definition.
A typical use of functions in Liquidsoap is for handlers\index{handler}, which are functions
to be called when a particular event occurs, specifying the actions to be taken
when it occurs. For instance, the source.on_metadata
\indexop{source.on_metadata} operator allows
registering a handler when metadata occurs in a stream. Its type is
(source('a), (([string * string]) -> unit)) -> unit
and it thus takes two arguments:
-
the source, of type
source('a)
, see below, whose metadata are to be watched, -
the handler, which is a function of type
([string * string]) -> unit
which takes as argument an association list (of type
[string * string]
) encoding the metadata and returns nothing meaningful (unit
).
When some metadata occur in the source, the handler is called with the metadata
as argument. For instance, we can print the title of every song being played on
our radio (a source named radio
) with
The handler is here the function handle_metadata
, which prints the field
associated to "title"
in the association list given in the argument m
.
Other useful operators allow the registration of handlers for the following situations:
blank.detect
: when a source is streaming blank (no sound has been streamed for some period of time),source.on_track
: when a new track is played,source.on_end
: when a track is about to end,on_start
andon_shutdown
: when Liquidsoap is starting or stopping.
Many other operators also take more specific handlers as arguments. For
instance, the operator input.harbor
, which allows users to connect to a
Liquidsoap instance and send streams, has on_connect
and on_disconnect
arguments which allow the registration of handlers for the connection and
disconnection of users.
For concision in scripts, it is possible define a function\indexop{fun}\indexop{->} without giving it a name, using the syntax
fun (x) -> ...
This is called an anonymous function\index{function!anonymous}, and it is typically used in order to specify short handlers in arguments. For instance, the above example for printing the title in metadatas could equivalently be rewritten as
where we define the function directly in the argument.
As a side note, this means that a definition of a function of the form
def f(x) =
...
end
could equivalently be written
f = fun (x) -> ...
When using this syntax, on the right hand of ->
Liquidsoap expects exactly one
expression. If you intend to use multiple ones (for instance, in order to
perform a sequence of actions), you can use the begin ... end
{.liquidsoap}\indexop{begin}
syntax, which allows grouping multiple expressions as one. For instance,
\index{argument!labeled}
A function can have an arbitrary number of arguments, and when there are many of them it becomes difficult to keep track of their order and their order matter! For instance, the following function computes the sample rate given a number of samples in a given period of time:
which is of type
(float, float) -> float
For instance, if you have 110250 samples over 2.5 seconds the samplerate will be
samplerate(110250., 2.5)
{.liquidsoap} which is 44100. However, if you mix the
order of the arguments and type samplerate(2.5, 110250.)
{.liquidsoap}, you
will get quite a different result (2.27×10^-5^) and this will not be detected by
the typing system because both arguments have the same type. Fortunately, we can
give labels to arguments in order to prevent this, which forces explicitly
naming the arguments. This is indicated by prefixing the arguments with a tilde
"~
":
The labels will be indicated as follows in the type:
(samples : float, duration : float) -> float
Namely, in the above type, we read that the argument labeled samples
is a
float and similarly for the one labeled duration
. For those arguments, we have
to give the name of the argument when calling the function:
samplerate(samples=110250., duration=2.5)
The nice byproduct is that the order of the arguments does not matter anymore, the following will give the same result:
samplerate(duration=2.5, samples=110250.)
Of course, a function can have both labeled and non-labeled arguments.
Another useful feature is that we can give default values to arguments, which
thus become optional\index{argument!optional}: if, when calling the function, a value is not specified
for such arguments, the default value will be used. For instance, if for some
reason we tend to generally measure samples over a period of 2.5 seconds, we can
make this become the value for the duration
parameter:
In this way, if we do not specify a value for the duration, its value will implicitly be assumed to be 2.5, so that the expression:
samplerate(samples=110250.)
will still evaluate to 44100. Of course, if we want to use another value for the duration, we can still specify it, in which case the default value will be ignored:
samplerate(samples=132300., duration=3.)
The presence of an optional argument is indicated in the type by prefixing the
corresponding label with "?
"\indexop{?}, so that the type of the above function is
(samples : float, ?duration : float) -> float
As a more concrete example of labeled arguments, we can see that the
type of the operator output.youtube.live
, which outputs a video stream to
YouTube\index{YouTube}, is
(?id : string, ?video_bitrate : int, ?audio_encoder : string, ?audio_bitrate : int, ?url : string, key : string, source) -> source
(we have only slightly simplified the type source
, which will only be detailed
in a next section). Even if we have not read the
documentation of this function, we can still guess what it is doing:
- there are 5 optional arguments that we should be able to ignore because they
have reasonable default values (although we can guess the use of most of them
from the label, e.g.
video_bitrate
should specify the bitrate we want to encode video, etc.), - there is 1 mandatory argument which is labeled
key
of typestring
: it must be the secret key we need in order to broadcast on our YouTube account, - there is 1 mandatory argument, unlabeled, of type
source
: this is clearly the source that we are going to broadcast to YouTube.
As we can see the types and labels of arguments already provide us with much information about the functions and prevent many mistakes.
If you want a more full-fledged example, have a look at the type of
output.icecast
:
(?id : string, ?chunked : bool, ?connection_timeout : float, ?description : string, ?dumpfile : string, ?encoding : string, ?fallible : bool, ?format : string, ?genre : string, ?headers : [string * string], ?host : string, ?icy_id : int, ?icy_metadata : string, ?mount : string, ?name : string, ?on_connect : (() -> unit), ?on_disconnect : (() -> unit), ?on_error : ((string) -> float), ?on_start : (() -> unit), ?on_stop : (() -> unit), ?password : string, ?port : int, ?protocol : string, ?public : bool, ?start : bool, ?timeout : float, ?url : string, ?user : string, ?verb : string, format('a), source) -> source
Although the function has 31 arguments, it is still usable because most of them
are optional so that they are not usually specified. In passing, we recognize
some of the concepts introduced earlier: the headers (header
parameter) are
coded as an association list, and there are quite few handlers (on_connect
,
on_disconnect
, etc.).
Some functions can operate on values of many possible types. For instance, the
function list.tl
\indexop{list.tl}, which returns the tail of the list (the list without its
first element), works on lists of integers so that it can have the type
([int]) -> [int]
but it also works on lists of strings so that it can also have the type
([string]) -> [string]
and so on. In fact, this would work for any type, which is why in Liquidsoap the
function list.tl
is actually given the type
(['a]) -> ['a]
which means: "for whichever type you replace 'a
with, the resulting type is a
valid type for the function". Such a function is called polymorphic\index{polymorphism}, in the
sense that it can be given multiple types: here, 'a
is not a type but rather a
"meta-type" (the proper terminology is a type variable) which can be replaced
by any regular type. Similarly, the empty list []
is of type ['a]
: it is a
valid list of whatever type. More interestingly, the function fst
which returns the
first element of a pair has the type
('a * 'b) -> 'a
which means that it takes as argument a pair of a something ('a
) and a
something else ('b
) and returns a something ('a
). For instance, the type
(string * int) -> string
is valid for fst
. In general, a type can involve an arbitrary number of type
variables which are labeled 'a
, 'b
, 'c
and so on.
In Liquidsoap, some type variables can also be constrained so that they cannot
be replaced by any type, but only specific types. A typical example is the
multiplication function *
, which operates on both integers and floats, and can therefore
be given both the types
(int, int) -> int
and
(float, float) -> float
but not the type
(string, string) -> string
If you have a look at the type of *
in Liquidsoap, it is
('a, 'a) -> 'a where 'a is a number type
which means that it has type ('a, 'a) -> 'a
where 'a
can only be replaced by
a type that represents a number (i.e., int
or float
). Similarly, the
comparison function <=
has type
('a, 'a) -> bool where 'a is an orderable type
which means that it has the type ('a, 'a) -> bool
for any type 'a
on which
there is a canonical order (which is the case of all usual types, excepting for
function types and source types).
We often want to be able to dynamically modify some parameters in a script. For
instance, consider the operator amplify
\indexop{amplify}, which takes a float and an audio
source and returns the audio amplified by the given volume factor: we can expect
its type to be
(float, source('a)) -> source('a)
so that we can use it to have a radio consisting of a microphone input amplified by a factor 1.2 by
mic = input.alsa()
radio = amplify(1.2, mic)
In the above example, the volume 1.2 was supposedly chosen because the sound
delivered by the microphone is not loud enough, but this loudness can vary from
time to time, depending on the speaker for instance, and we would like to be
able to dynamically update it. The problem with the current operator is that the
volume is of type float
and a float cannot change over time: it has a fixed
value.
In order for the volume to have the possibility to vary over time, instead of
having a float
argument for amplify
, we have decided to have instead an
argument of type
() -> float
This is a function which takes no argument and returns a float (remember that a function can take an arbitrary number of arguments, which includes zero arguments). It is very close to a float excepting that each time it is called the returned value can change: we now have the possibility of having something like a float which varies over time. We like to call such a function a float getter\index{getter}, since it can be seen as some kind of object on which the only operation we can perform is get the value. For instance, we can define a float getter by
Each time we call f
, by writing f()
in our script, the resulting float
will be increased by one compared to the previous one: if we try it in an
interactive session, we obtain
# f();;
- : float = 1.0
# f();;
- : float = 2.0
# f();;
- : float = 3.0
Since defining such arguments often involves expressions of the form
fun () -> e
which is somewhat heavy, we have introduced the alternative syntax
{e}
for it. It can be thought of as a variant of the expression e
which will be
evaluated each time it is used instead of being evaluated once. Its use is
illustrated below.
Finally, we should mention an important fact. Since the value of a reference r
can be queried by writing r()
, any reference can be considered as a getter:
this is the function which, when queried, will return the contents of the
reference!
The type of amplify
is thus actually
(() -> float, source('a)) -> source('a)
and the operator will regularly call the volume function in order to have the current value for the volume before applying it. To be precise, it is actually called before each frame, which means roughly every 0.04 second. Let's see how we can use this in scripts. We can, of course, still apply a constant factor with
def volume () = 1.2 end
radio = amplify(volume, mic)
or, using anonymous functions,
radio = amplify(fun () -> 1.2, mic)
which we generally write, using the alternative syntax,
radio = amplify({1.2}, mic)
More interestingly, we can use the value of a float reference v
for
amplification:
radio = amplify({v()}, mic)
when the value of the reference gets changed, the amplification will get changed too. Moreover, since any reference can be considered as a getter, as mentioned above, this can be written in an even simpler way:
radio = amplify(v, mic)
However, we need to use the above syntax if we want to manipulate the value of the reference. For instance,
radio = amplify({2 * v()}, mic)
will amplify by twice the value of v
.
In practice, float getters are often created using interactive.float
\index{interactive variable}\index{variable!interactive} which
creates a float value which can be modified on the telnet server (this is an
internal server provided by Liquidsoap on which other applications can connect
to interact with it, as detailed in a later section), or
osc.float
which reads a float value from an external controller using the OSC
library. For instance, with the script
the volume can be modified by issuing the telnet command
var.set volume = 0.5
You should remember that getters are regular functions. For instance, if we expect that the volume on telnet to be expressed in decibels, we can convert it to an actual amplification coefficient as follows:
As a more elaborate variation on this, let's program a fade in: the volume
progressively increases from 0 to 1 in fade_duration
seconds (here, 5
seconds). We recall that the volume function will be called before each frame,
which is a buffer whose duration is called here frame_duration
and can be
obtained by querying the appropriate configuration parameter: in order to have
the volume raise from 0 to 1, we should increase it by frame_duration / fade_duration
at each call. If you execute the following script, you should
thus hear a sine which is getting louder and louder during the 5 first seconds:
Of course, this is for educational purposes only, and the actual way one would usually perform a fade in Liquidsoap is detailed in an ulterior section.
Let us give another advanced example, which uses many of the above
constructions. The standard library defines a function
metadata.getter.float
\index{metadata!getter}, whose type is
(float, string, source('a)) -> source('a) * (() -> float)
which creates a float getter with given initial value (the first argument), which can be updated by reading a given metadata (the second argument) on a given source (the third argument). Its code is
You can see that it create a reference x
, which contains the current value,
and registers a handler for metadata, which updates the value when the metadata
is present, i.e. m[metadata]
is different from the empty string ""
, which is
the default value. Given a radio
source which contains metadata labeled
"liq_amplify
", we can actually change the volume of the source according to the
metadata with
Finally, in order to simplify things a bit, you will see that the type of amplify is actually
({float}, source('a)) -> source('a)
where the type {float}
means that both float
and () -> float
are accepted,
so that you can still write constant floats where float getters are
expected. What we actually call a getter is generally an element of such a
type, which is either a constant or a function with no argument.
In order to work with such types, the standard library often uses the following functions:
getter
, of type({'a}) -> {'a}
, creates a getter,getter.get
, of type({'a}) -> 'a
, retrieves the current value of a getter,getter.function
, of type({'a}) -> () -> 'a
, creates a function from a getter.
Liquidsoap supports functions which are recursive\index{recursive function}\index{function!recursive}, i.e., that can call themselves. For instance, in mathematics, the factorial\index{factorial} function on natural numbers is defined as fact(n)=1×2×3×...×n, but it can also be defined recursively as the function such that fact(0)=1 and fact(n)=n×fact(n-1) when n>0: you can easily check by hand that the two functions agree on small values of n (and prove that they agree on all values of n). This last formulation has the advantage of immediately translating to the following implementation of factorial:
for which you can check that fact(5)
gives 120, the expected result. As
another example, the list.length
function, which computes the length of a
list, can be programmed in the following way in Liquidsoap:
We do not detail much further this trait since it is unlikely to be used for radios, but you can see a few occurrences of it in the standard library.
Suppose that we want to store and manipulate structured data. For instance, a
list of songs together with their duration and tempo. One way to store each song
is as a tuple of type string * float * float
, but there is a risk of confusion
between the duration and the length which are both floats, and the situation
would of course be worse if there were more fields. In order to overcome this,
one can use a record\index{record} which is basically the same as a tuple, excepting that
fields are named. In our case, we can store a song as
which is a record with three fields respectively named filename
, duration
and bpm
. The type of such a record is
{filename : string, duration : float, bpm : float}
which indicates the fields and their respective type. In order to access a field
of a record, we can use the syntax record.field
. For instance, we can print
the duration with
print("The duration of the song is #{song.duration} seconds")
Records are heavily used in Liquidsoap in order to structure the functions of
the standard library. We tend to call module\index{module} a record with only functions, but
this is really the same as a record. For instance, all the functions related to
lists are in the list
module and functions such as list.hd
are fields of
this record. For this reason, the def
{.liquidsoap}\indexop{def} construction allows adding
fields in record. For instance, the definition
adds, in the module list
, a new field named last
, which is a function which
computes the last element of a list. Another shorter syntax to perform
definitions consists in using the let
\indexop{let} keyword which allows assigning a value
to a field, so that the previous example can be rewritten as
If you often use the functions of a specific module, the open
\indexop{open} keyword allows
using its fields without having to prefix them by the module name. For instance,
in the following example
the open list
directive allows directly using the functions in this module: we
can simply write nth
and length
instead of list.nth
and list.length
.
A unique feature of the Liquidsoap language is that it allows adding fields to any value. We also call them methods\indexop{method} by analogy with object-oriented programming. For instance, we can write
which defines a string ("test.mp3"
) with two methods (duration
and
bpm
). This value has type
string.{duration : float, bpm : float}
and behaves like a string, e.g. we can concatenate it with other strings:
but we can also invoke its methods like a record or a module:
The construction def replaces
{.liquidsoap} allows changing the main value
while keeping the methods unchanged, so that
will print
"newfile.mp3".{duration = 123., bpm = 120.}
(note that the string is modified but not the fields duration
and bpm
).
The http.get
\indexop{http.get} function, which retrieves a webpage over http, has the type:
(?headers : [string * string], ?timeout : float, string) ->
string.{headers : [string * string],
status_message : string,
status_code : int,
protocol_version : string}
It returns a string (the contents of the webpage) with fields specifying the returned headers, the status message and the version used by the protocol. A typical use is
Another typical example is the rms
\index{RMS} operator, which takes a source as argument,
and returns the same source together with an added method named rms
which
allows retrieving the current value for the RMS (which is a measure of sound
intensity). The RMS of a source can thus be logged every second in a file as
follows (functions concerning files and threads are explained in
there):
When the return type of a function has methods, the help of Liquidsoap displays
them in a dedicated section. For instance, every function returning a source,
also returns methods associated to this source, such as the skip
\indexop{skip} function
which allows skipping the current track (those methods are detailed in a
section below). If we ask for help about the playlist
operator by typing
$ liquidsoap -h playlist
we can observe this: the help displays, among other,
Methods:
* reload : (?uri : string) -> unit
Reload the playlist.
* skip : () -> unit
Skip to the next track.
This indicates that the returned source has a reload
method, which allows
reloading the playlist, possibly specifying a new file, as well as the skip
method described above. If you try at home, you will see that they are actually
many more methods.
You should now be able to fully understand the type given to references. We
recall for instance that the type of ref(5)
is
(() -> int).{set : (int) -> unit}
This means that such a reference consists of a function of type () -> int
,
taking no argument and returning an integer (the current value of the
reference), together with a method set
of type (int) -> unit
, which takes as
argument an integer (and, when called, modifies the value of the reference
according to the argument). Since a reference r
can be considered as a
function, this explains why we have been writing r()
to get its value. In
order to modify its value, say set it to 7, we can call the method set
and
write r.set(7)
{.liquidsoap}. In fact, the syntax r := 7
{.liquidsoap} is
simply a shorthand for this.
In this section, we detail some more advanced values than the ones presented in previous sections. You are not expected to be understanding those in details for basic uses of Liquidsoap.
In the case where a function does not have a sensible result to return, it can raise an
error\index{error}. Typically, if we try to take the head of the empty list without
specifying a default value (with the optional parameter default
), an error will be raised.
By default, this error will stop the script, which is usually not a desirable
behavior. For instance, if you try to run a script containing
the program will exit printing
Error 14: Uncaught runtime error:
type: not_found, message: "no default value for list.hd"
This means that the error named "not_found
"\indexop{not_found} was raised, with a message
explaining that the function did not have a reasonable default value of the head
to provide.
In order to avoid this, one can catch exceptions with the syntax
try
code
catch err do
handler
end
This will execute the instructions code
: if an error is raised at some point
during this, the code handler
is executed, with err
being the error. For
instance, instead of writing
we could equivalently write
The name and message associated to an error can respectively be retrieved using
the functions error.kind
and error.message
, e.g. we can write
try
...
catch err do
print("the error #{error.kind(err)} was raised")
print("the error message is #{error.message(err)}")
end
Typically, when reading from or writing to a file, errors will be raised when a problem occurs (such as reading from a non-existent file or writing a file in a non-existent directory) and one should always check for those and log the corresponding message:
Specific errors can be catched with the syntax
try
...
catch err in l do
...
end
where l
is a list of error names that we want to handle here.
Errors can be raised from Liquidsoap with the function error.raise
, which
takes as arguments the error to raise and the error message. For instance:
Finally, we should mention that all the errors should be declared in advance
with the function error.register
, which takes as argument the name of the new
error to register:
It is sometimes useful to have a default value for a type. In Liquidsoap, there
is a special value for this, which is called null
\indexop{null}. Given a type t
, we write
t?
for the type of values which can be either of type t
or be null
: such a
value is said to be nullable. For instance, we could redefine the list.hd
function in order to return null (instead of raising an error) when the list is
empty:
whose type would be
(['a]) -> 'a?
since it takes as argument a list whose elements are of type 'a
and returns a
list whose elements are 'a
or null
. As it can be observed above, the null
value is created with null()
.
In order to use a nullable value, one typically uses the construction x ?? d
which is the value x
excepting when it is null, in which case it is the
default value d
. For instance, with the above head function:
Some other useful functions include
null.defined
: test whether a value is null or not,null.get
: obtain the value of a nullable value supposed to be distinct fromnull
,null.case
: execute a function or another, depending on whether a value is null or not.
Liquidsoap has a number of features (such as its preprocessor) which allow useful operations on the scripts, but cannot really be considered as part of the core language itself. Those are presented below.
The main configuration\index{configuration} options are accessed through
functions whose name are prefixed by settings
. These settings affect the
overall behavior of Liquidsoap. Each setting is a reference: this means that,
given a setting, we can obtain its value by applying it to ()
and we can
change its value by using the :=
syntax. For instance, the samplerate used for
audio in Liquidsoap is controlled by the settings.frame.audio.samplerate
setting. We can thus display its current value with
and change its value to 48kHz (the default being 44.1kHz) by adding the following command at the beginning of our script:
Or we can increase the verbosity of the log messages with
which sets the maximum level of shown log messages to 4, the default being 3. We recall that the log levels are 1 for critical messages, 2 for severe issues, 3 for important messages, 4 for information and 5 for debug messages.
You can obtain the list of all available settings, as well as their default value with the command
liquidsoap --list-settings
For instance, the documentation about the frame.duration
setting is
### Tentative frame duration in seconds
Audio samplerate and video frame rate constrain the possible frame
durations.This setting is used as a hint for the duration, when
'frame.audio.size'is not provided.Tweaking frame duration is tricky but
needed when dealing with latencyor getting soundcard I/O correctly
synchronized with liquidsoap.
settings.frame.duration := 0.04
The value 0.04
at the bottom indicates the default value.
It is often useful to split your script over multiple files, either because it
has become quite large, or because you want to be able to reuse common functions
between different scripts. You can include a file file.liq
in a script by
writing
%include "file.liq"
which will be evaluated as if you had pasted the contents of the file in place of the command.
For instance, this is useful in order to store passwords out of the main file, in
order to avoid risking leaking those when handing the script to some other
people. Typically, one would have a file passwords.liq
defining the passwords
in variables, e.g.
radio_pass = "secretpassword"
and would then use it by including it:
%include "passwords.liq"
radio = ...
output.icecast(%mp3, host="localhost", port=8000,
password=radio_pass, mount="my-radio.mp3", radio)
so that passwords are not shown in the main script.
Liquidsoap embeds a preprocessor which allows including or not part of the code
depending on some conditions. For instance, the following script will print
something depending on whether the function input.alsa
is defined or not:
This is useful in order to have some code being executed depending on the
compilation options of Liquidsoap (the above code will be run only when
Liquidsoap has the support for the ALSA library) and is used intensively in the
standard library. The command %ifndef
can similarly be used to execute code
when a function is not defined. We can also execute a portion of code whenever
an encoder is present using %ifencoder
(or %ifnencoder
when an encoder is
not present), the end of the code in question being delimited with %endif
as
above. For instance, suppose that we want to encode a file in mp3, if Liquidsoap
was compiled with support for it, and otherwise default to wave. This can be
achieved with
Finally, the command %ifversion
can be used to execute some code
conditionally, depending on the version of Liquidsoap:
This is quite useful in order to provide a script which is compatible with multiple versions of Liquidsoap (note that this functionality was only introduced in version 2.0, and thus unfortunately cannot be used in order to ensure backward compatibility with versions earlier than this).
In this section, we detail some of the most useful general purpose functions present in the standard library. The functions related to sound and streaming are mentioned in next section and detailed in subsequent chapters.
The string representation of any value can be obtained with the
string
\indexop{string} function:
print(string([1,2,3]))
Most expected type conversion function are implemented with names of the form
A_of_B
. For instance, we can convert a string to an integer with
int_of_string
:
print(1 + int_of_string("2"))
The\index{file} whole contents of a file can be obtained with the function file.contents
:
In the case where the file is big, it is advisable to use file.read
, whose type is
(string) -> () -> string
and returns a function which successively reads chunks of the file until the end, in which case the empty string is returned. The contents of a file can be dumped using it by
Other useful functions are
file.exists
: test whether a file exists,file.write
: write in a file,file.remove
: remove a file,file.ls
: list the files present in a directory.
Also, convenient functions for working on paths\index{path} are present in the file
and path
module:
file.extension
: get the extension of a file,file.temp
: generate a fresh temporary filename,path.dirname
: get the directory of a path,path.basename
: get the file name without the directory from a path,path.home
: home directory of user,
and so on.
\index{HTTP}
Distant files can be retrieved over http using http.get
. For instance, the
following script will fetch and display the list of changes in Liquidsoap:
Other useful functions are
http.post
: to send data, typically on forms,http.put
: to upload data,http.delete
: to delete resources.
Liquidsoap also features an internal web server called harbor, which allows serving web pages directly from Liquidsoap, which can be handy to present some data related to your script or implement some form of advanced interaction. This is described on details in there.
\index{argument!commandline}
The arguments passed on the command line to the current script can be retrieved
using the argv
\indexop{argv} function. Its use is illustrated in
there.
The current script can be stopped using the shutdown
\indexop{shutdown} function which cleanly
stops all the sources, and so on. In case of emergency, the application can be
immediately stopped with the exit
\indexop{exit} function, which allows specifying an exit
code (the convention is that a non-zero code means that an error occurred). The
current script can also be restarted using restart
\indexop{restart}.
In order to execute other programs\index{process} from Liquidsoap, you can use the function
process.read
which executes a command and returns the text it wrote in the
standard output. For instance, in the script
we use the find
command to find files in the ~/Music
directory and pipe it
through wc -l
which will count the number of printed lines, and thus the
number of files. In passing, in practice you would do this in pure Liquidsoap
with
There is also the quite useful variant called process.read.lines
, which
returns the list of lines written on the standard output. Typically, suppose
that we have a script generate-playlist
which outputs a list of files to play,
one per line. We can play it by feeding it to playlist.list
which plays a list
of files:
The more elaborate variant process.run
allows retrieving the return code of
the program, set a maximal time for the execution of the program and sandbox
its execution, i.e. restrict the directories it has access to in order to
improve security (remember that executing programs is dangerous, especially if
some user-contributed data is used). This is further detailed in
there.
\index{thread}
The function thread.run
\indexop{thread.run} can be used to run a function asynchronously in a thread, meaning
that the function will be executed in parallel to the main program and will not
block other computations if it takes time. It takes two optional arguments:
delay
: if specified, the function will not be run immediately, but after the specified number of seconds,every
: if specified, the function will be run regularly, every given number of seconds.
For instance, we can simulate the sound of a hanged phone by playing a sine and switching the volume on and off every second. This is easily achieved as follows:
Here, we amplify the sine by the contents of a reference volume
(or, more
precisely, by a getter which returns the value of the reference). Its value is
switched between 0.
and 1.
every second by the function change
.
A perhaps more useful variant of this is auto-gain control\index{automatic gain control}. We want to adjust the volume so that the output volume is always roughly -14 LUFS, which is a standard sound loudness measure. One way to do this is to regularly check its value and increase or lower the volume depending on whether we are below or above the threshold:
Here, we have a source pre
which we amplify by the value of the reference
volume
in order to define a source post
. On both sources, the lufs
function instructs that we should measure the LUFS\index{LUFS}, which value can be obtained
by calling the lufs
and lufs_momentary
methods attached to the
sources. Regularly (10 times per second), we run the function adjust
which
multiplies the volume by the coefficient needed to reach -14 LUFS (to be
precise, we actually divide the distance to -14 by 20 in order not to change the
volume too abruptly, and we constrain the volume in the interval [0.01,10] in
order to keep sane values).
Of course, in practice, you do not need to implement this by hand: the operator
normalize
\indexop{normalize} does this for you, and more efficiently than in the above
example. But it is nice to see that you could if you needed, to experiment with
new strategies for managing the gain for instance.
Another useful function is thread.when
\indexop{thread.when}, which executes a
function when a predicate (a boolean getter, of type {bool}
) becomes true. By
default, the value of the predicate is checked every half second, this can be
changed with the every
parameter. For instance, suppose that we have a file
named "song
" containing the path to a song, and we want that each time we
change the contents of this file, the new song is played. This can be achieved
as follows:
We begin by creating q
which is a request queue, i.e. some source on which we
can push new songs (those are detailed in there) and
song
which is a getter which returns the contents of the file. We then use
thread.when
on the predicate getter.changes(song)
(which is true when the
contents of song
changes) in order to detect changes in song
and, when this
is the case, actually push the song on the request queue.
As a variation on previous example, we can program a clock which will read the time at the beginning of every hour as follows:
Namely, the condition 0m
is true when the minute of the current time is zero,
i.e. we are the beginning of the hour: when this is the case we push in the
queue a request to say the current time. Note that even though the condition is
checked very regularly, the function read_time
is called only once at the
beginning of every hour: this is because, by default, thread.when
waits for
the condition to become false before executing the function again (this can be
altered with the changed
parameter of thread.when
).
In case you need it, the current time\index{time} can be retrieved using the
time
function. This function returns the number of seconds since the 1st of
January 1970, which is mostly useful to measure duration by considering the
difference between two points in time. For instance, we can compute the time
taken by the execution of a function f
with
which stores the time before and after the execution of f
and displays the
difference. As a useful variant, the function time.up
\indexop{time.up} returns the
uptime\index{uptime} of the script, i.e. the number of seconds since the
beginning of its execution.
In order to retrieve time in more usual notations, you can use the functions
time.local
and time.utc
which return a record containing the usual
information (year, month, day, hour, etc.), respectively according to the
current time zone and the Greenwich median time. For instance, we can print the
current date with
If you do not need to manipulate time components and only print time, this can
also be more conveniently done with the time.string
\indexop{time.string} function which takes a
string as argument and replaces %Y
by the year, %m
by the month and so on,
so that we can do
Finally, we mention here that the time zone can be retrieved and changed with
the time.zone
\indexop{time.zone} function:
Apart from the general-purpose constructions of the language described above, Liquidsoap also has constructions dedicated to building streams: after all this is what we are all here for. Those are put to practice in the next chapter and described in details in the chapter after. We however quickly recap here the main concepts and operators.
An operator producing a stream is called a source\index{source} and has a type of the form
source(audio=..., video=..., midi=...)
where the "...
" indicate the contents\index{contents} that the source can generate, i.e. the
number of channels, and their nature, for audio, video and midi data, that the
source can generate. For instance, the playlist
operator has (simplified) type
(?id : string, string) -> source(audio='a, video='b, midi='c)
we see that it takes as parameters an optional string labeled id
(most
operators take such an argument which indicates its name, and is used in the
logs or the telnet) as well as a string (the playlist to play) and returns a
source (which plays the playlist...).
Some sources are fallible\index{fallibility}\index{source!fallible}, which means that they are not always available. For
instance, the sound input from a DJ over the internet is only available when the
DJ connects. We recall from there that a source can be made
infallible with the mksafe
operator or by using a fallback to an infallible
source.
Some outputs need to send data encoded in some particular format. For instance,
the operator which records a stream into a file, output.file
, needs to know in
which format we want to store the file in, such MP3, AAC, etc. This is
specified by passing special parameters called encoders\index{encoder}. For instance, the
(simplified) type of output.file
is
(?id : string, format('a), string, source('a)) -> unit
We see that it takes the id
parameter (a string identifying the operator), an
encoder (the type of encoders is format(...)
), a string (the file where we
should save data) and a source. This means that we can play our playlist and
record it into an mp3 file as follows:
Here, %mp3
is an encoder specifying that we want to encode into the mp3
formats. Encoders for most usual formats are available (%wav
for wav,
%fdkaac
for aac, %opus
for opus, etc.) and are detailed later
on.
Internally, Liquidsoap does not directly deal with a file, but rather with an abstraction of it called a request\index{request}. The reason is that some files require some processing before being accessible. For instance, we cannot directly access a distant mp3 file: we first need to download it and make sure that it has the right format.
This is the reason why most low-level operators do not take files as arguments,
but requests. The main thing you need to know in practice is that you can create
a request from a file location, using the request.create
function. For
instance, in the following example, we create a request queue q
, on which we
can add requests to play in it using q.push
. We define a function play
,
which adds the file on the queue, by first creating a request from it. We then
use list.iter
to apply this function play
on all the mp3 files of the
current directory. The following script will thus play all the mp3 files in the
current directory:\indexop{request.queue}
The main functions in order to create and manipulate audio streams are
playlist
: plays a playlist,fallback
: plays the first available source in a list,switch
: plays a source depending on a condition,crossfade
: fade successive tracks,output.icecast
,output.hls
,output.file
: output on Icecast, an HLS playlist, or in a file,request.queue
: create a queue that can be dynamically be fed with user's requests and will play them in the order they were received.
Their use is detailed in next chapter.