# Liquidsoap

## audio stream generation

### RadioPi

RadioPi is the web radio of the ECP (Ecole Centrale de Paris). RadioPi runs many channels. There are topical channels (Reggae, Hip-Hop, Jazz, ...) and a so-called Web channel which switches from one to another of the topical channels. On top of that, they periodically broadcast live shows, which are relayed on all channels.

We met a RadioPi manager right after having released Liquidsoap 0.2.0, and he was seduced by the system. They needed quite complex features, which they were at that time fulfilling using dirty tricks, loads of obfuscated scripts. Using Liquidsoap now allow them to do all they want in an integrated way, but also provided new features.

##### The migration process

Quite easy actually. They used to have many instances Ices2, each of these calling a Perl script to get the next song. The Web channel was an Icecast playback of one of the previous Ices topical streams. Other scripts were used for switching channels to live shows, switching the Web channel relay from one to another topical stream, turning on or off all these channels, etc.

Now they have this single Liquidsoap script, no more. It calls external scripts to interact with their web-based song scheduling system. And they won new features: blank detection and distributed encoding.

The first machine gets its files from a ftp server opened on the second machine. Liquidsoap handles download automatically.

Each file is given by an external script, `radiopilote-getnext`, whose answer looks as follows (except that it's on a single line):

    annotate:file_id="3541",length="400.613877551",\
      type="chansons",display_title="John Holt - Holigan",\
      display_artist="RadioPi - Canal reggae",\
      display_album="Studio One SeleKta! - Album Studio 1 12",\
      canal="reggae":ftp://***:***@host/files/3541.mp3

Note that we use annotate to pass some variables to liquidsoap...

On top of that, we build a common channel, that plays each channel using a special schedule.

    #!/usr/bin/liquidsoap

    # === Settings ===

    set("log.file.path","/var/log/liquidsoap/pi.log")
    set("init.daemon",true)
    set("init.daemon.pidfile.path","/var/run/liquidsoap/pi.pid")
    set("server.telnet",true)
    set("harbor.bind_addr","0.0.0.0")
    set("harbor.port",8000)
    set("harbor.password","xxxxxx")
    set("log.level",4)
    set("scheduler.event_queues",4)
    set("scheduler.log",false)

    scripts = "/path/to/scripts/"

    pass = "xxxxxx"
    ice_host = "host"

    descr = "Radio Piston"
    url = "http://radiopi.org"

    # === Live relays ===

    def live_start() =
      log("got live source, starting relays..")
      ignore(execute("stream_relay.start"))
      ignore(execute("archives.start"))
    end

    def live_stop() =
      log("live source has gone, stoping relays..")
      ignore(execute("stream_relay.stop"))
      ignore(execute("archives.stop"))
    end

    # Live source through harbor
    live = input.harbor(id="live",on_connect=live_start,on_disconnect=live_stop,"live.ogg")
    live_safe = mksafe(live)

    # Live relay to secondary encoder
    output.icecast.vorbis(id="stream_relay",start=false,restart=true,host="1.2.3.4",port=8005,password="xxxx",mount="live.ogg",live_safe)

    # File source for archiving
    title = '$(if $(title),"$(title)","Emission inconnue")$(if $(artist), " par  $(artist)") - %m-%d-%Y, %H:%M:%S'
    output.file.vorbis(id="archives",start=false,reopen_on_metadata=true,"/path/to/archives/" ^ title ^ ".ogg",live_safe)

    # === Main script ===

    # Specialize the output functions by partial application
    output.icecast     = output.icecast.mp3(restart=true,description=descr, url=url)
    out = output.icecast(host=ice_host,port=8080,password=pass)

    # A file for playing during failures
    interlude =
      single("/path/to/failure.mp3")

    def fallback.transition(previous,next)
      add([fade.in(next),fade.final(duration=5.,previous)])
    end

    # === Channels ===

    # Lastfm submission
    def lastfm (m) =
    # Only submit songs of type "chansons" (got type using annotate..)
      if (m["type"] == "chansons") then
        canal = m["canal"]
        user = "radiopi-" ^ canal
        lastfm.submit(user=user,password="xxxxxx",m)
      end
    end

    # To create a channel from a basic source, add:
    # - a new-track notification for radiopilote
    # - metadata rewriting
    # - the live shows
    # - the failsafe 'interlude' source to channels
    # - blank detection
    def mklive(source)
      # Add lastfm submission
      source = on_metadata(lastfm,source)

      # Feedback the system
      source = on_metadata(fun (meta) ->
                        system(scripts ^ "radiopilote-feedback "
                               ^quote(meta["canal"])^" "
                               ^quote(meta["file_id"])), source)

      # Rewrite metadata to add channel name and mode..
      # Got data from annotate..
      rewrite_metadata(
       [("artist",'$(if $(display_artist),"$(display_artist)","$(artist)")'),
        ("comment",""),
        ("title", '$(if $(display_title),"$(display_title)","$(title)")'),
        ("album", '$(if $(display_album),"$(display_album)","$(album)")')],

       # Default fallback
       fallback(track_sensitive=false,[
         # Strip blank to avoid live streaming only blank..
         strip_blank(live, length=10., threshold=-50.),
            source, interlude ]) )
    end

    # === Basic sources ===

    # Create a radiopilote-driven source
    def channel_radiopilote(~skip=true,name)
      log("Creating canal #{name}")

      # Request function
      def request () =
        log("Request for #{name}")
        request.create(audio=true,
            get_process_output(scripts ^ "radiopilote-getnext " ^ quote(name)))
      end

      # Basic source
      source =   request.dynamic(
              id="dyn_"^name,request)

      # Add smart crossfading
      source = smart_crossfade(source)

      # Only skip some channels
      if skip then
        skip_blank(source, length=10., threshold=-40.)
      else
        source
      end
    end

    # Channels encoded here
    reggae = channel_radiopilote("reggae")
    jazz = channel_radiopilote("jazz")
    discoqueen = channel_radiopilote("discoqueen")
    # Avoid skiping blank with classic music !!
    classique = channel_radiopilote(skip=false,"classique")
    That70Sound = channel_radiopilote("That70Sound")

    # Create a channel using mklive(), encode and output it to icecast.
    def mkoutput(mount,source,name,genre)
      out(mount=mount,name=name,genre=genre,
         mklive(source))
    end

    # === Outputs ===

    # These channels are encoded on this machine
    reggae  = mkoutput("reggae", reggae, "RadioPi - Canal Reggae","reggae")
    jazz    = mkoutput("jazz", jazz, "RadioPi - Canal Jazz","jazz")
    discoqueen  = mkoutput("discoqueen", discoqueen, "RadioPi - Canal DiscoQueen","discoqueen")
    classique = mkoutput("classique", classique, "RadioPi - Canal Classique","classique")
    That70Sound = mkoutput("That70Sound", That70Sound,
                           "RadioPi - Canal That70Sound","That70Sound")

    # Channels from the other encoder:
    def get_channel(mount)
     input.http(autostart=true,id="input_"^mount,"http://host:8080/"^mount)
    end
    metal = get_channel("metal")
    electro = get_channel("electro")
    hiphop = get_channel("hiphop")

    # Finally the web channel, and its mp3 version
    web =
      out(mount="radioPi",name="RadioPi - www.radiopi.org",
          fallback([ switch(track_sensitive=false,
                            [ ( { 6h-12h }, reggae  ),
                              ( { 12h-14h }, discoqueen ),
                              ( { 14h-16h }, electro ),
                              ( { 16h-19h }, reggae ),
                              ( { 19h-20h }, That70Sound ),
                              ( { 20h-22h }, metal ),
                              ( { 22h-0h }, reggae ),
                              ( {  0h-6h  }, jazz  ),
                             ]),
                     interlude ]))

The other machine has a similar configuration exept that files are local, but this is exactly the same for liquidsoap !

Using harbor, the live connects directly to liquidsoap, using port `8000` (icecast runs on port `8080`). Then, liquidsoap starts a relay to the other encoder, and both switch their channels to the new live.

Additionally, a file output is started upon live connection, in order to backup the stream. You could also add a relay to icecast in order to manually check what's received bythe harbor.