Liquidsoap 0.9.2 : Liquidsoap on RadioPi

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 ]))

Grab the code!

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.