# Liquidsoap

## Audio Stream Generation

### users_radiopi.liq

    #!/usr/bin/liquidsoap

    # Standard settings
    set("log.file.path","/var/log/liquidsoap/pi.log")
    set("init.daemon",true)
    set("log.stdout",false)
    set("log.file",true)
    set("init.daemon.pidfile.path","/var/run/liquidsoap/pi.pid")

    # Enable telnet server
    set("server.telnet",true)

    # Enable harbor for any external
    # connection
    set("harbor.bind_addr","0.0.0.0")

    # Verbose logs
    set("log.level",4)

    # We use the scheduler intensively,
    # therefore we create many queues.
    set("scheduler.generic_queues",5)
    set("scheduler.fast_queues",3)
    set("scheduler.non_blocking_queues",3)

    # === Settings ===

    # The host to request files
    stream = "XXXxXXXx"
    # The command to request files
    scripts = "ssh XXxxxXXX@#{stream} '/path/to/scripts/"
    # A substitution on the returned path
    sed = " | sed -e s#/path/to/files/#ftp://user:password@#{stream}/#'"

    # Enable replay gain
    enable_replaygain_metadata ()

    pass = "XXxXXXXx"
    ice_host = "localhost"

    descr = "RadioPi"
    url = "http://radiopi.org"

    # === Live ===

    # A live source, on which we stip blank (make the source
    # unavailable when streaming blank)
    live =
      strip_blank(
        input.harbor(id="live", port=8000, password=pass,
                     buffer=8.,max=20.,"live.ogg"),
        length=10., threshold=-50.)

    # This source relays the live data, when available,
    # to the other streamer, in uncompressed format (WAV)
    output.icecast(%wav, host=stream,
                   port=8005, password=pass,
                   mount="live.ogg", fallible=true,
                   live)

    # This source relays the live source to "live.ogg". This
    # is used for debugging purposes, to see what is sent
    # to the harbor source.
    output.icecast(%vorbis, host="127.0.0.1",
                   port=8080, password=pass,
                   mount="live.ogg", fallible=true,
                   live)

    # This source starts an archive of the live stream
    # when available
    title = '$(if $(title),"$(title)",\
             "Emission inconnue")$(if $(artist), \
             " par  $(artist)") - %m-%d-%Y, %H:%M:%S'
    output.file(%vorbis, reopen_on_metadata=true,
                fallible=true,
                "/data/archives/brutes/" ^ title ^ ".ogg",
                live)

    # === Channels ===

    # Specialize the output functions by partial application
    output.icecast = output.icecast(description=descr, url=url)
    out = output.icecast(host=ice_host,port=8080,password=pass,fallible=true)
    out_aac32 = out(%aacplus(bitrate=32))
    out_aac = out(%aacplus(bitrate=64))
    out = out(%mp3)

    # A file for playing during failures
    interlude =
      single("/home/radiopi/fallback.mp3")

    # Lastfm submission
    def lastfm (m) =
      if (m["type"] == "chansons") then
        if (m["canal"] == "reggae" or m["canal"] == "Jazz" or m["canal"] == "That70Sound") then
          canal =
            if (m["canal"] == "That70Sound") then
    	   "70sound"
    	else
    	   m["canal"]
    	end
          user = "radiopi-" ^ canal
          lastfm.submit(user=user,password="xXXxx",m)
        end
      end
    end

    # === Basic sources ===

    # Custom crossfade to deal with jingles..
    def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3.,
                         ~default=(fun (a,b) -> sequence([a, b])),
                         ~high=-15., ~medium=-32., ~margin=4.,
                         ~width=2.,~conservative=false,s)
      fade.out = fade.out(type="sin",duration=fade_out)
      fade.in  = fade.in(type="sin",duration=fade_in)
      add = fun (a,b) -> add(normalize=false,[b, a])
      log = log(label="smart_crossfade")

      def transition(a,b,ma,mb,sa,sb)

        list.iter(fun(x)-> log(level=4,"Before: #{x}"),ma)
        list.iter(fun(x)-> log(level=4,"After : #{x}"),mb)

        if ma["type"] == "jingles" or mb["type"] == "jingles" then
          log("Old or new file is a jingle: sequenced transition.")
          sequence([sa, sb])
        elsif
          # If A and B are not too loud and close, fully cross-fade them.
          a <= medium and b <= medium and abs(a - b) <= margin
        then
          log("Old <= medium, new <= medium and |old-new| <= margin.")
          log("Old and new source are not too loud and close.")
          log("Transition: crossed, fade-in, fade-out.")
          add(fade.out(sa),fade.in(sb))

        elsif
          # If B is significantly louder than A, only fade-out A.
          # We don't want to fade almost silent things, ask for >medium.
          b >= a + margin and a >= medium and b <= high
        then
          log("new >= old + margin, old >= medium and new <= high.")
          log("New source is significantly louder than old one.")
          log("Transition: crossed, fade-out.")
          add(fade.out(sa),sb)

        elsif
          # Opposite as the previous one.
          a >= b + margin and b >= medium and a <= high
        then
          log("old >= new + margin, new >= medium and old <= high")
          log("Old source is significantly louder than new one.")
          log("Transition: crossed, fade-in.")
          add(sa,fade.in(sb))

        elsif
          # Do not fade if it's already very low.
          b >= a + margin and a <= medium and b <= high
        then
          log("new >= old + margin, old <= medium and new <= high.")
          log("Do not fade if it's already very low.")
          log("Transition: crossed, no fade.")
          add(sa,sb)

        # What to do with a loud end and a quiet beginning ?
        # A good idea is to use a jingle to separate the two tracks,
        # but that's another story.

        else
          # Otherwise, A and B are just too loud to overlap nicely,
          # or the difference between them is too large and overlapping would
          # completely mask one of them.
          log("No transition: using default.")
          default(sa, sb)
        end
      end

      smart_cross(width=width, duration=start_next, conservative=conservative,
                  transition,s)
    end

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

      # Request function
      def request () =
        log("Request for #{name}")
        ret = list.hd(get_process_lines(scripts^"radiopilote-getnext "^quote(name)^sed))
        log("Got answer: #{ret} for #{name}")
        request.create(ret)
      end

      # Create the request.dynamic source
      # Set conservative to true to queue
      # several songs in advance
      source =
        request.dynamic(conservative=true, length=400.,
                        id="dyn_"^name,request,
                        timeout=60.)

      # Apply normalization using replaygain
      # information
      source =   amplify(1.,override="replay_gain", source)

      # Skip blank when asked to
      source =
        if skip then
          skip_blank(source, length=10., threshold=-40.)
        else
          source
        end

      # Submit new tracks on lastfm
      source = on_metadata(lastfm,source)

      # Tell the system when a new track
      # is played
      source = on_metadata(fun (meta) ->
                        system(scripts ^ "radiopilote-feedback "
                               ^quote(meta["canal"])^" "
                               ^quote(meta["file_id"]) ^ "'"), source)

      # Rewrite metadata
      source =
        rewrite_metadata(
         [("artist",'$(if $(display_artist),"$(display_artist)","$(artist)")'),
          ("comment",""),
          ("title", '$(if $(display_title),"$(display_title)","$(title)")'),
          ("album", '$(if $(display_album),"$(display_album)","$(album)")')],
         source)

      # Finally apply a smart crossfading
      smart_crossfade(source)
    end

    # Basic source
    jazz = channel_radiopilote("jazz")
    discoqueen = channel_radiopilote("discoqueen")
    # Avoid skiping blank with classic music !!
    classique = channel_radiopilote(skip=false,"classique")
    That70Sound = channel_radiopilote("That70Sound")
    metal = channel_radiopilote("metal")
    reggae = channel_radiopilote("reggae")
    Rock = channel_radiopilote("Rock")

    # Group those sources in a seperate
    # clock (good for multithreading/multicore)
    clock.assign_new([jazz,That70Sound,metal,reggae])

    # === Mixing live ===

    # 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) =
      # Transition function: if transitioning
      # to the live, fade out the old source
      # if transitioning from live, fade.in
      # the new source. NOTE: We cannot
      # skip the current song because
      # reloading new songs for all the
      # sources when live starts costs too much
      # CPU.
      def trans(old,new) =
        if source.id(new) == source.id(live) then
          log("Transition to live!")
          add([new,fade.final(old)])
        elsif source.id(old) == source.id(live) then
          log("Transitioning from live!")
          add([fade.initial(new),old])
        else
          log("Dummy transition")
          new
        end
      end
      fallback(track_sensitive=false,
        transitions=[trans,trans,trans],
        [live,source,interlude])
    end

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

    # === Outputs ===

    mkoutput("jazz", jazz, "RadioPi - Canal Jazz","jazz")
    mkoutput("discoqueen", discoqueen, "RadioPi - Canal DiscoQueen","discoqueen")
    mkoutput("classique", classique, "RadioPi - Canal Classique","classique")
    mkoutput("That70Sound", That70Sound,
                           "RadioPi - Canal That70Sound","That70Sound")
    mkoutput("metal", metal, "RadioPi - Canal Metal","metal")
    mkoutput("reggae", reggae, "RadioPi - Canal Reggae","reggae")
    mkoutput("Rock", Rock, "RadioPi - Canal Rock","Rock")

    # Test outouts
    mkoutput(out=out_aac,"reggae.aacp", reggae, "RadioPi - Canal Reggae \
                                         (64 kbits AAC+ test stream)","reggae")
    mkoutput(out=out_aac32,"reggae.aacp32", reggae, "RadioPi - Canal Reggae \
                                          (32 kbits AAC+ test stream)","reggae")

Download