/usr/share/doc/liquidsoap/html/radiopi.html is in liquidsoap 1.1.1-7.1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
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 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | <?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML \
1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" />
<title>Liquidsoap :: Liquidsoap on RadioPi</title>
<link href="css/style.css" type="text/css" rel="stylesheet" />
</head>
<body>
<div id="wrapper">
<div id="header">
<div id="logo">
<h1>Liquidsoap</h1>
<h2>audio stream generation</h2>
</div>
<div>
<ul id="menu">
<li id="menu-about">
<a href="index.html">about</a></li>
<li id="menu-doc-index">
<a href="documentation.html">documentation</a></li>
<li id="menu-doc-api">
<a href="reference.html">API</a></li>
<li id="menu-doc-snippets">
<a href="scripts/index.html">snippets</a></li>
<li id="menu-developers">
<a href="https://github.com/savonet/liquidsoap/issues">developers</a></li>
</ul>
</div>
</div>
<div id="content"><div>
<h3>RadioPi</h3>
<p>
<a href="http://www.radiopi.org" target="_blank">RadioPi</a> is the web radio of the ECP (Ecole Centrale de Paris). RadioPi runs many channels.
There are topical channels (Reggae, Hip-Hop, Jazz, ...). On top of that, they periodically broadcast live shows,
which are relayed on all channels.
</p>
<p>
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.
</p>
<h5>The migration process</h5>
<p>
Quite easy actually. They used to have many instances Ices2, each of these calling a Perl script to get the next song.
Other scripts were used for switching channels to live shows.
</p>
<p>
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.
</p>
<p>
The first machine gets its files from a ftp server opened on the second machine.
Liquidsoap handles download automatically.
</p>
<p>
Each file is given by an external script, <code>radiopilote-getnext</code>,
whose answer looks as follows (except that it's on a single line):
</p>
<pre class="syntax ">
annotate:file_id="3541",length="400.613877551",\
type="chansons",title="John Holt - Holigan",\
artist="RadioPi - Canal reggae",\
album="Studio One SeleKta! - Album Studio 1 12",\
canal="reggae":ftp://***:***@host/files/3541.mp3
</pre>
<p>
Note that we use annotate to pass some variables to liquidsoap...
</p>
<pre class="syntax 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)
# 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")
</pre>
<div align="right">
<a href="scripts/users_radiopi.liq">
<img class="grab" src="./images/grab.png" alt="Grab the code!">
</a>
</div></p>
<p>
The other machine has a similar configuration exept that files are local, but this is exactly the same for liquidsoap !
</p>
<p>
Using harbor, the live connects directly to liquidsoap, using port <code>8000</code> (icecast runs on port <code>8080</code>).
Then, liquidsoap starts a relay to the other encoder, and both switch their channels to the new live.
</p>
<p>
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 by the harbor.
</p>
</div></div>
<div>
<div id="footer"> 2003-2013 Savonet team</div>
</div>
</div>
</body></html>
|