Using Liquidsoap and scheduling

Liquidsoap On Ubuntu

Due to repeated technical issues I was experiencing with MIXXX in auto-dj mode and broadcasting to a Shoutcast server, I researched options for an alternate software solution which would reliably play a playlist of music for Hawkwynd Radio. Mixxx is a very powerful DJ software package, and I use it for my live broadcasts, but when left unattended in AUTO-DJ mode over time, it would simply crash, leaving no errors in the logs, or any way to tell what was causing it to fail. My only suspicion is that I’m running it on an old Dell PC I ressurected for the sole use of being the client to broadcast the audio. I tried several things short of formatting and re-installing Ubuntu OS itself.

Then I Found an alternative – Liquidsoap

Liquidsoap is a powerful and flexible language for describing audio and video streams. It offers a rich collection of operators that you can combine at will, giving you more power than you need for creating or transforming streams.

First, this solution is not for the faint of heart. You’ll need some ‘nix skills, and enjoy working in a command line environment because Liquidsoap has no GUI interface. Something I was drawn to from the start of reading about it, as I love working in the command line since the days of old when DOS was young. (1980)

My Wanted List

What I found in Liquidsoap

Install liquidsoap on Ubuntu 14.04

1
sudo apt-get install liquidsoap

If you want the bleeding edge latest version, you can build liquidsoap from source. I didn’t do this, but very well may in my next go-round on a new machine. For now, I’m sticking with the base version contained in the repository.

The Pc I am using is an old Dell Inspiron desktop with 3gb ram which was given to me by a friend who no longer wanted it. I installed Ubuntu 14.04 as my operating system, and discovered you don’t need an expensive computer to do what I wanted to do (run Hawkwynd Radio).

Here’s a peek at the schedule.liq script which currently powers hawkwynd radio:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/liquidsoap

# include code for crossfade
%include "crossfade.liq"

# enable liquidsoap to read mp3 metadata "REPLAY_GAIN" values
enable_replaygain_metadata ()

# turn on logging, and open connections to telnet 1234
set("log.file.path","scheduled-radio.log")
set("server.telnet", true)
set("server.telnet.port", 1234)


# Add a skip function to a source
# when it does not have one
# by default
def add_skip_command(~command,s)
    # Register the command:
    server.register(
        usage="skip",
        description="Skip the current song in source.",
        command,
        fun(_) -> begin source.skip(s) "Skipping current song." end
    )
end

# Define crossfade and smooth_add
def crossfade(s)
  #duration is automatically overwritten by metadata fields passed in
  #with audio
  s = fade.in(type="sin", duration=4., s)
  s = fade.out(type="sin", duration=5., s)
  fader = fun (a,b) -> add(normalize=false,[b,a])
  cross(fader,s)
end

def smooth_add(~delay=0.5,~p=0.2,~normal,~special)
  d = delay
  fade.final = fade.final(duration=d*2.)
  fade.initial = fade.initial(duration=d*2.)
  q = 1. - p
  c = amplify
  fallback(track_sensitive=false,
           [special,normal],
           transitions=[
             fun(normal,special)->
               add(normalize=false,
                   [c(p,normal),
                    c(q,fade.final(type="sin",normal)),
                    sequence([blank(duration=d),c(q,special)])]),
             fun(special,normal)->
               add(normalize=false,
                   [c(p,normal),
                    c(q,fade.initial(type="sin",normal))])
           ])
end


# MainStream Top40 List
# =====================
MainStream  = playlist(
     id="main",
     mode="randomize",
     reload_mode="rounds",
     reload=1,
     mime_type="audio/x-mpegurl",
     "~/playlist/a-z-bands-of-rock.pls"
)

# nrj: Compress and normalize, producing a more uniform and “full” sound.
jingles = nrj(
   audio_to_stereo(
     playlist(
       id="jingles",
       mode="randomize",
       reload_mode="rounds",
       reload=1,
       mime_type="audio/x-mpegurl",
       "~/playlist/jingles.m3u"
      )
    )
  )

# define our station identifier Top Of The Hour
# ============================================
clock =  single("~/music/jingles/hawkad1.mp3")

# set our requests id, to accept requests the LR
requests = request.queue( id="requested" )

# Band Playlists top-bottom play normal
# =====================================
mellow      = playlist( id="mellow",mode="randomize",
                       reload_mode="rounds",reload=1,mime_type="audio/x-mpegurl",
                       "~/playlist/mellow-mornings.m3u" )
ACDC        = playlist( id="main",mode="normal",
                        reload_mode="rounds",reload=1,mime_type="audio/x-mpegurl",
                        "~/playlist/AC-DC.m3u" )
Beatles     = playlist( id="main",mode="normal",
                        reload_mode="rounds",reload=1,mime_type="audio/x-mpegurl",
                        "~/playlist/Beatles.m3u" )
jambros     = playlist( id="main",mode="normal",reload_mode="rounds",
                        reload=1,mime_type="audio/x-mpegurl",
                        "~/playlist/jambros.m3u" )
LedZeppelin = playlist( id="main",mode="normal",reload_mode="rounds",
                        reload=1,mime_type="audio/x-mpegurl",
                        "~/playlist/Led-Zeppelin.m3u" )
PinkFloyd   = playlist( id="main",mode="normal",reload_mode="rounds",
                        reload=1, mime_type="audio/x-mpegurl",
                        "~/playlist/Pink-Floyd.m3u" )
Rush        = playlist( id="main",mode="normal",reload_mode="rounds",
                        reload=1,mime_type="audio/x-mpegurl",
                        "~/playlist/Rush.m3u" )
TheCrusaders= playlist( id="main",mode="normal",reload_mode="rounds",
                        reload=1,mime_type="audio/x-mpegurl",
                        "~/playlist/The-Crusaders.m3u" )
ThePolice   = playlist( id="main",mode="normal",reload_mode="rounds",
                        reload=1,mime_type="audio/x-mpegurl",
                        "~/playlist/The-Police.m3u" )
Yes         = playlist( id="main",mode="normal",reload_mode="rounds",
                        reload=1,mime_type="audio/x-mpegurl",
                        "~/playlist/Yes.m3u" )
FridayNightShow= playlist( id="main",mode="normal",reload_mode="rounds",
                           reload=1,mime_type="audio/x-mpegurl",
                           "~/playlist/1-3-19-liveshow.m3u" )

# Weekdays listings
# =================
sunday      = rotate( weights=[1,4], [jingles, Beatles])
monday      = rotate( weights=[1,4], [jingles, TheCrusaders])
tuesday     = rotate( weights=[1,4], [jingles, LedZeppelin])
wednesday   = rotate( weights=[1,4], [jingles, ThePolice])
thursday    = rotate( weights=[1,3,2],[jingles, Rush, LedZeppelin] )
friday      = rotate( weights=[1,4], [jingles, Yes])
Freplay     = rotate( weights=[1,4], [jingles, FridayNightShow ])

# Rotating Mainstream
# ===================================================
rotating    = rotate( weights=[ 1, 4 ], [ jingles, MainStream ] )

# set up play for radio, dont interupt tracks
# for requests, jingles.

radio = fallback( track_sensitive=true,
[ requests,
    switch( track_sensitive=true,
    [
        # midnight to 1am every night it's the Pink Floyd hour
        ({0h-1h}, rotate( weights=[1,4], [jingles, PinkFloyd])),

        #sunday 7a-noon Sunday morning with the Beatles
        ({ (0w) and 07h-12h }, sunday),
        #  12-1pm Jambros hour
        ({ (0w) and 12h-13h}, rotate( weights=[1,4],[jingles, jambros] )),
        # 1-2pm Zeppelin hour
        ({ (0w) and 13h-14h}, rotate( weights=[1,4],[jingles, LedZeppelin] )),
        #  2-3pm The Police Hour
        ({ (0w) and 14h-15h}, rotate( weights=[1,4],[jingles, ThePolice] )),
       
        #monday
        # 6-7am mellow out
        ({ (1w) and 06h-7h }, mellow ),
        #  3-4pm The monday
        ({ (1w) and 15h-16h}, rotate( weights=[1,4],[jingles, monday] )),
       
        # tuesday
        # 6-7 mellow out
        ({ (2w) and 6h-7h }, mellow ),
        #  3-4pm tuesday
        ({ (2w) and 15h-16h}, rotate( weights=[1,4],[jingles, tuesday] )),
       
        # wednesday
        # 6-7am mellow out
        ({ (3w) and 6h-7h }, mellow),
        #  3-4pm The wednesday
        ({ (3w) and 15h-16h}, rotate( weights=[1,4],[jingles, wednesday] )),
       
        ### Thursday ###

        # 6-7am mellow out
        ({ (4w) and 6h-7h }, mellow),
        #  3-4pm The Rush Zeppelin hour and police
        ({ (4w) and 15h-16h}, rotate( weights=[1,4],[jingles, thursday] )),
       
        ### Friday ###
       
        # 6-7am mellow out
        # 3-4pm ACDAC ATTACK
        # 7-9pm Crusaders
        ({ (5w) and 6h-7h }, mellow ),
        ({ (5w) and 18h-19h }, friday ),
        ({ (5w) and 19h-21h}, monday ),
        ({ (5w) and 15h-16h}, rotate( weights=[1,4],[jingles, ACDC] )),
       
        # Saturday
        # 6-7am mellow out
        # 7-9am Coffee with The Beatles (2hrs)
        ({ (6w) and 6h-7h }, mellow),
        ({ (6w) and 07h-09h }, rotate( weights=[1,4], [jingles, Beatles])),
        # 9-930 Zeppelin
        ({ (6w) and 9h-9h30}, rotate( weights=[1,4],[jingles, LedZeppelin] )),
        # 930-10 Police
        ({ (6w) and 9h30-10h}, rotate( weights=[1,4],[jingles, ThePolice])),      
        # 12 - 2pm Friday Night Replay with some MainStream mixed in
        ({ (6w) and 12h-14h}, rotate( weights=[1,4,2],[ jingles, Freplay, MainStream ] ))

    ]
    ),
        # All other times/days play rotating:
        rotating
])

# Normalize the playback of our stream
radio = normalize( gain_max=6.0,
                   gain_min=-6.0,
                   k_down=0.1,
                   k_up=0.005,
                   target=1.0,
                   threshold=-25.0,
                   window= 0.1,
                   radio )

# at top of the hour, play a specific radio identifier over the playing track
radio  = add([ radio, switch([ ( {0m0s}, clock ) ] ) ] )

# amplify our signal to level shit out based on replay_gain metadata in the track.
radio = amplify( 1.0, override = "replay_gain", radio )

# Add a skip command for use via telnet
add_skip_command(command="skip", radio)

# Send to Hawkwynd Radio station
output.shoutcast(
    %mp3,
    host        = "ip.address.hidden,
    name        = "
Hawkwynd Radio",
    url         = "
http://stream.hawkwynd.com",
    genre       = "
Classic Rock",  
    port        = 8000,
    password    = "
password-hidden",
    public      = true,
    fallible    = true,
    radio
)

# shoot it all out to the soundcard
out(radio)

crossfade.liq – which is included at the top of the script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# Crossfade between tracks,
# taking the respective volume levels
# into account in the choice of the
# transition.
# @category Source / Track Processing
# @param ~start_next   Crossing duration, if any.
# @param ~fade_in      Fade-in duration, if any.
# @param ~fade_out     Fade-out duration, if any.
# @param ~width        Width of the volume analysis window.
# @param ~conservative Always prepare for a premature end-of-track.
# @param s             The input source.

def smart_crossfade (~start_next=5.,~fade_in=3.,
                     ~fade_out=3., ~width=2.,
             ~conservative=false, s )
  high   = -20.
  medium = -32.
  margin = 4.
  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
      # If A and B and not too loud and close,
      # fully cross-fade them.
      a <= medium and
      b <= medium and
      abs(a - b) <= margin
    then
      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("Transition: crossed, fade-out.")
      add(fade.out(sa),sb)

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

    elsif
      # Opposite as the previous one.
      a >= b + margin and b >= medium and a <= high
    then
      log("Transition: crossed, fade-in.")
      add(sa,fade.in(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: just sequencing. --crossfade")
      sequence([sa, sb])
    end
  end

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

To run the script:

1
liquidsoap sechedule.liq

You can listen to it anytime at Hawkwynd Radio