diff --git a/website/Makefile b/website/Makefile index 32b67a59..12e2be76 100644 --- a/website/Makefile +++ b/website/Makefile @@ -6,8 +6,8 @@ GENERATED_INTERPRETER_DIR := $(PWD)/../liquidsoap/_build/default/src/js NODE := $(shell which node) # Versions for which documentation should be built and included -VERSIONS := dev 1.4.4 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.2.0 2.2.1 2.2.2 -DEFAULT_VERSION := 2.2.2 +VERSIONS := dev 1.4.4 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.2.0 2.2.1 2.2.2 2.2.3 +DEFAULT_VERSION := 2.2.3 # DEFAULT_VERSION := dev # Static content diff --git a/website/content/doc-2.2.3/.gitignore b/website/content/doc-2.2.3/.gitignore new file mode 100644 index 00000000..8636e652 --- /dev/null +++ b/website/content/doc-2.2.3/.gitignore @@ -0,0 +1,3 @@ +reference.md +protocols.md +settings.md diff --git a/website/content/doc-2.2.3/beets.md b/website/content/doc-2.2.3/beets.md new file mode 100644 index 00000000..51c32b2e --- /dev/null +++ b/website/content/doc-2.2.3/beets.md @@ -0,0 +1,179 @@ +# Integrating a music library: an example with Beets + +Liquidsoap's native sources can read from files and folders, +but if your radio uses an important music library +(more than a thousand tracks) +sorting by folders may not be enough. +You will also need to adjust the playout gain per track (ReplayGain). +In that case you would better have a music library +queried by Liquidsoap. +In this section we'll do this with [Beets](http://beets.io/). +Beets holds your music catalog, +cleans tracks' tags before importing, +can compute each track's ReplayGain, +and most importantly has a command-line interface we can leverage from Liquidsoap. +The following examples may also inspire you to integrate another library or your own scripts. + +After installing Beets, +enable the `random` plug-in +(see [Beets documentation on plug-ins](https://beets.readthedocs.io/en/stable/plugins/index.html#using-plugins)). +To enable gain normalization, install and configure the +[`replaygain`](https://beets.readthedocs.io/en/stable/plugins/replaygain.html) plug-in. +To easily add single tracks to you library, +you might also be interested in the +[drop2beets](https://github.com/martinkirch/drop2beets#drop2beets) plug-in. +The following examples suppose you defined a `BEET` constant, +which contains the complete path to your `beet` executable (on UNIX systems, find it with `which beet`). For example: + +``` +BEET = "/home/radio/.local/bin/beet" +``` + +Before creating a Liquidsoap source, +let's see why Beets queries are interesting for a radio. + +## Beets queries + +Queries are parameters that you usually provide to the `beet ls` command : +Beets will find matching tracks. +The `random` plug-in works the same, except that it returns only one track matching the query +(see [the plug-in's documentation](https://beets.readthedocs.io/en/stable/plugins/random.html)). +Once your library is imported, +you can try the following queries on the command line +by typing `beet ls [query]` or `beet random [query]`. +To test quickly, add the `-t 60` option to `beet random` +so it will select an hour worth of tracks matching your query. + +Without selectors, queries search in a track’s title, artist, album name, +album artist, genre and comments. Typing an artist name or a complete title +usually match the exact track, and you could do a lovely playlist just by querying `love`. + +But in a radio you'll usually query on other fields. +You can select tracks by genre with the `genre:` selector. +Be careful that `genre:Rock` also matches `Indie Rock`, `Punk Rock`, etc. +To select songs having english lyrics, use `language:eng`. +Or pick 80s songs with `year:1980..1990`. + +Beets also holds internal meta-data, like `added`: +the date and time when you imported each song. +You can use it to query tracks inserted over the past month with `added:-1m..`. +Or you can query track imported more than a year ago with `added:..-1y`. +Beets also lets you +[set your own tags](https://beets.readthedocs.io/en/stable/guides/advanced.html#store-any-data-you-like). + +You can use the `info` plug-in to see everything Beets knows about title(s) matching a query +by typing `beet info -l [query]`. +See also [the Beets' documentation](https://beets.readthedocs.io/en/stable/reference/query.html) +for more details on queries operators. +All these options should allow you to create both general and specialiazed Liquidsoap sources. + +## A source querying each next track from Beets + +As of Liquidsoap 2.x we can create a function that creates a dynamic source, +given its `id` and a Beet query. +We rely on `request.dynamic` to call `beet random` +(with `-f '$path'` option so beets only returns the matching track's path) +every time the source must prepare a new track: + +```liquidsoap +def beets(id, query) = + beets_src = + request.dynamic(id=id, retry_delay=1., { + request.create( + string.trim( + process.read("#{BEET} random -f '$path' #{query}") + ) + ) + }) + (beets_src:source) +end + +all_music = beets("all_music", "") +recent_music = beets("recent_music", "added:-1m..") +rock_music = beets("rock_music", "genre:Rock") +``` + +Note that + +- `query` can be empty, it will match all tracks in the library. +- we set `retry_delay` to a second, to avoid looping on `beet` calls if something goes wrong. +- The final type hint (`:source`) will avoid false typing errors when the source is integrated in complex operators. + +## Applying ReplayGain + +When the [`replaygain` plug-in](https://beets.readthedocs.io/en/stable/plugins/replaygain.html) +is enabled, all tracks will have an additional metadata field called `replaygain_track_gain`. +Check that Beet is configured to +[write ID3 tags](https://beets.readthedocs.io/en/stable/reference/config.html#importer-options) +so Liquidsoap will be able to read this metadata - +your Beet configuration should include something like: + +``` +import: + write: yes +``` + +Then we only need to add `amplify` to our source creation function. In the example below we also add `blank.eat`, to automatically cut silence at the beginning or end of tracks. + +```liquidsoap +def beets(id, query) = + beets_src = + blank.eat(id="#{id}_", start_blank=true, max_blank=1.0, threshold=-45.0, + amplify(override="replaygain_track_gain", 1.0, + request.dynamic(id=id, retry_delay=1., { + request.create( + string.trim( + process.read("#{BEET} random -f '$path' #{query}") + ) + ) + }) + ) + ) + (beets_src:source) +end +``` + +This is the recommended Beets integration ; +such source will provide music continuously, +at a regular volume. + +## Beets as a requests protocol + +If you're queueing tracks with `request.queue`, +you may prefer to integrate Beets as a protocol. +In that case, +the list of paths returned by `beet random -f '$path'` fits directly +what's needed by protocol resolution: + +```liquidsoap +def beets_protocol(~rlog,~maxtime,arg) = + timeout = maxtime - time() + command = "#{BEET} random -f '$path' #{arg}" + p = process.run(timeout=timeout, command) + if p.status == "exit" and p.status.code == 0 then + [string.trim(p.stdout)] + else + rlog("Failed to execute #{command}: #{p.status} (#{p.status.code}) #{p.stderr}") + [] + end +end +protocol.add("beets", beets_protocol, + syntax = "same arguments as beet's random module, see https://beets.readthedocs.io/en/stable/reference/query.html" +) +``` + +Once this is done, +you can push a beets query from [the telnet server](server.html): +if you created `request.queue(id="userrequested")`, +the server command +`userrequested.push beets:All along the watchtower` +will push the Jimi Hendrix's song. + +With this method, you can benefit from replay gain metadata too, by wrapping +the recipient queue in an `amplify` operator, like + +```liquidsoap +userrequested = amplify(override="replaygain_track_gain", 1.0, + request.queue(id="userrequested") +) +``` diff --git a/website/content/doc-2.2.3/blank.md b/website/content/doc-2.2.3/blank.md new file mode 100644 index 00000000..6a1f5a97 --- /dev/null +++ b/website/content/doc-2.2.3/blank.md @@ -0,0 +1,50 @@ +# Blank detection + +[Liquidsoap](index.html) has three operators for dealing with blanks. + +On GeekRadio, we play many files, some of which include bonus tracks, which +means that they end with a very long blank and then a little extra music. It's +annoying to get that on air. The `blank.skip` operator skips the +current track when a too long blank is detected, which avoids that. The typical +usage is simple: + +```liquidsoap +# Wrap it with a blank skipper +source = blank.skip(source) +``` + +At [RadioPi](http://www.radiopi.org/) they have another problem: sometimes they +have technical problems, and while they think they are doing a live show, +they're making noise only in the studio, while only blank is on air; sometimes, +the staff has so much fun (or is it something else ?) doing live shows that they +leave at the end of the show without thinking to turn off the live, and the +listeners get some silence again. To avoid that problem we made the +`blank.strip` operators which hides the stream when it's too blank +(i.e. declare it as unavailable), which perfectly suits the typical setup used +for live shows: + +```liquidsoap +interlude = single("/path/to/sorryfortheblank.ogg") +# After 5 sec of blank the microphone stream is ignored, +# which causes the stream to fallback to interlude. +# As soon as noise comes back to the microphone the stream comes +# back to the live -- thanks to track_sensitive=false. +stream = fallback(track_sensitive=false, + [ blank.strip(max_blank=5.,live) , interlude ]) + +# Put that stream to a local file +output.file(%vorbis, "/tmp/hop.ogg", stream) +``` + +If you don't get the difference between these two operators, you should learn +more about liquidsoap's notion of [source](sources.html). + +Finally, if you need to do some custom action when there's too much blank, we +have `blank.detect`: + +```liquidsoap +def handler() + system("/path/to/your/script to do whatever you want") +end +source = blank.detect(handler,source) +``` diff --git a/website/content/doc-2.2.3/book.md b/website/content/doc-2.2.3/book.md new file mode 100644 index 00000000..b9c4ab24 --- /dev/null +++ b/website/content/doc-2.2.3/book.md @@ -0,0 +1,11 @@ +# The Liquidsoap book + +Together with the release of Liquidsoap 2.0, we have written _the Liquidsoap +book_ which covers in details the language and the process of building a +radio. It complements the online documentation by providing a homogeneous and +progressive presentation of Liquidsoap. + +[![The Liquidsoap book](/assets/img/book.svg){height=600px}](https://www.amazon.com/dp/B095PVTYR3) + +It can be [ordered from Amazon](https://www.amazon.com/dp/B095PVTYR3) +(or [read online](http://www.liquidsoap.info/book/book.pdf)). diff --git a/website/content/doc-2.2.3/build.md b/website/content/doc-2.2.3/build.md new file mode 100644 index 00000000..e7a94c6a --- /dev/null +++ b/website/content/doc-2.2.3/build.md @@ -0,0 +1,112 @@ +# Building Liquidsoap + +## Forewords + +Installing liquidsoap can be a difficult task. The software relies on a up-to date +OCaml compiler, as well as a bunch of OCaml modules and, for most of them, corresponding +C library dependencies. + +Our recommended way of installing liquidsoap is via [opam](http://opam.ocaml.org/). `opam` can take +care of installing the correct OCaml compiler, optional and required dependencies as well as system-specific +package dependencies. + +The `opam` method is described in details in the [documentation](doc/content/install.md). +We recommend that any interested user head over to this link to install the software via `opam`. + +The remainder of this document describes how to compile liquidsoap locally for developers. + +## Overview + +Liquidsoap is compiled using [dune](https://dune.readthedocs.io/en/stable/), which is the most popular +OCaml build system at the moment. `dune` is tightly integrated with `opam` so, even if you are installing +from source using `dune`, `opam` remains an important tool. + +Generally speaking, compiling from source may require the latest version of the liquidsoap code as well as its +dependencies. Some of its dependencies are optional and can be ignored at first and some are not. + +Keep in mind that, although `opam` is generally aware of required minimal version for dependencies, `dune` is not. +If a dependency is outdated, `dune` compilation will simply fail, at which point your may have to figure out if +you need to update a dependency. + +Each branch of liquidsoap is compiled using [github actions](https://github.com/savonet/liquidsoap/actions). When trying +to build a specific branch, if the CI passes with it then, most likely, you are missing a dependency or it is not +the latest version. + +## `opam` pinning + +`opam` pinning is a mechanism to update `opam` with the latest version of a package, even before it is published to +the official opam repository. This is the easiest way to update a dependency to its latest version. + +You can pin directly from a local git repository checkout: + +```shell +git clone https://github.com/savonet/ocaml-metadata.git +cd ocaml-metadata +opam pin -ny . +``` + +You can also pin directly using a git url: + +```shell +opam pin -ny git+https://github.com/savonet/ocaml-cry +``` + +See `opam pin --help` for more defails about the available options. + +## Dependencies + +The best way to figure out what dependencies are required or optional and their versions is to use the latest `opam` +package. Since `liquidsoap` development is using `dune` and `opam`, the dependencies are kept in sync via the +local liquidsoap opam package(s) and this serves as the de-facto list of dependencies and their versions. + +First, you should pin the latest liquidsoap code: + +```shell +opam pin -ny git+https://github.com/savonet/liquidsoap +``` + +Then, ask `opam` to list all the dependencies for `liquidsoap`: + +```shell +opam info liquidsoap +opam info liquidsoap-core +opam info liquidsoa-lang +``` + +This should give you a (long!) list of all dependencies. Then, you can query `opam` to see +what each dependency does. This is particularly useful for optional dependencies on `liquidsoap-core` +which provide opt-in features. For instance `opam info soundtouch` will let you know that this +package provides functions for changing pitch and timestretching audio data. + +Lastly, there are two types of dependencies: + +- Dependencies maintained by us +- Dependencies not maintained by us + +For dependencies not maintained by us, most of the time, we rely on the latest published version. Very rarely should you +have to fetch/pin the latest version of these dependencies. + +For dependencies maintained by us, we may break their API during our development cycle and you maybe have to fetch/pin +the latest version when compilign the latest `liquidsoap` code. You may also have to check out a specific +branch when compiling `liquidsoap` from a specific development branch when the changes in the liquidsoap code are paired with +changes in one of our dependencies. Typically, this happens a lof with the `ffmpeg` binding. + +## Compiling + +Once you have all dependencies installed, you should be able to compile via: + +```shell +dune build +``` + +If an error occurs, you may need to see if you need to update a dependency. Hopefully, with a short iteration of this cycle, +you will end up with a successful build! + +Once you have a successful build, you can also use the top-level `liquidsoap` script. This script builds the latest code and +executes it right away. It works as if you were calling the `liquidsoap` binary after installing it: + +```shell +./liquidsoap -h output.ao +``` + +From here, you can start changing code, testing script etc. Happy hacking! diff --git a/website/content/doc-2.2.3/clocks.md b/website/content/doc-2.2.3/clocks.md new file mode 100644 index 00000000..4f5820dd --- /dev/null +++ b/website/content/doc-2.2.3/clocks.md @@ -0,0 +1,312 @@ +# Clocks + +In the [quickstart](quick_start.html) and in the introduction to liquidsoap +[sources](sources.html), 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`, `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, + +```liquidsoap +output.icecast(fallback([crossfade(playlist(...)),jingles])) +``` + +yields the following graph: + +![Graph representation with clocks](/assets/img/graph_clocks.png) + +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 CPU-based clock, 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`: + +```liquidsoap +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`: + +```liquidsoap +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: + +```liquidsoap +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: + +```liquidsoap +clock.assign_new(sync="none",[source]) +``` + +This will automatically attach the appropriate sources to that clock. + +Another important use case of this operator is if your script involves multiple sources from the same external clock, typically multiple ALSA input or output from the same sound card or multiple jack input and output. By default (the so-called `clock_safe` mode), liquidsoap will assign a dedicated clock to each of those sources, leading either to an error or forcing the use of an unnecessary `buffer` (see below). Instead, you can allocate each source with `clock_safe=false` and assign them a single clock: + +``` +s1 = input.jack(clock_safe=false, ...) +s2 = input.jack(clock_safe=false, ...) + +clock.assign_new([s1,s2]) +``` + +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): + +```liquidsoap +input = input.oss() + +# Icecast source, with its own clock: +icecast_source = mksafe(buffer(input)) +clock.assign_new(id="icecast", [icecast_source]) + +# Output to icecast: +output.icecast(%mp3,mount="blah",icecast_source) + +# File output: +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: + +```liquidsoap +def one() + s = single(argv(1)) + clock.assign_new(sync="none",[s]) + output.file(%mp3,"/dev/null",s) +end +one() +one() +``` diff --git a/website/content/doc-2.2.3/complete_case.md b/website/content/doc-2.2.3/complete_case.md new file mode 100644 index 00000000..ddc1b2c2 --- /dev/null +++ b/website/content/doc-2.2.3/complete_case.md @@ -0,0 +1,81 @@ +# A complete case analysis + +We will develop here a more complex example, according to the following specifications: + +- play different playlists during the day; +- play user requests -- done via the telnet server; +- insert about 1 jingle every 5 songs; +- add one special jingle at the beginning of every hour, mixed on top of the normal stream; +- relay live shows as soon as one is available; +- and set up several outputs. + +Once you've managed to describe what you want in such a modular way, you're half the way. More precisely, you should think of a diagram such as the following, through which the audio streams flow, following the arrows. The nodes can modify the stream using some basic operators: switching and mixing in our case. The final nodes, the ends of the paths, are outputs: they are in charge of pulling the data out of the graph and send it to the world. In our case, we only have outputs to icecast, using two different formats. + +![Graph for 'radio.liq'](/assets/img/liqgraph.png) + +Now here is how to write that in [Liquidsoap](index.html). + +```liquidsoap +#!/usr/bin/liquidsoap + +# Lines starting with # are comments, they are ignored. + +# Put the log file in some directory where +# you have permission to write. +log.file.path := "/tmp/