How to make your own Auto DJ Radio Robot

A guide by Andy Moore Published September 2020

A detailed guide to setting up Radio Automation in the Cloud

Have you decided you want to build your own radio automation in the cloud?
What is wrong with you? Are you a control freak or something? #metoo

This guide is for anyone wanting to build radio automation in the cloud, but mainly for those who have outgrown their cloud radio partner, and want to upgrade to something managed in-house, it's also for anyone wanting to automate their stream with Liquidsoap: I've included answers to common questions; code samples, and the theoeritical hacks you need to take things to the next level.

When I say next level, I mean being able to scale to a million listeners a day. Is that next level enough for you?

I first started playing with Cloud Radio Automation in 2016; this is 4 years worth of know-how condensed into a single page
It's over thirty years now since I first went on air: this isn't just a techie tutorial, there's much 'Moore' between the lines

This article is broken down into easy to digest chunks; nibble on each individually, and they should all add up to a veritable banquet:

Options for Radio Automation

Before we dive deep into the techie side, let's first take a look at the options for automating your radio station: you could have a computer running a playout system in a studio, or use cloud based software and stream providers, or you can install third party software packages to run it for you:

Playout Based Automation

Playout systems require a dedicated physical computer; they store your music, ads and can run your station on Auto DJ mode, but you'll also need a streaming server so folks can listen:

Cloud Based Services

There are several options for hosted automation; these providers don't just supply you with the Auto DJ, they'll also fix you up with Icecast or Shoutcast streaming too:

Cloud Automation Software

Providers like; Radio King and Radio Jar build their own in-house software, many others rely on third party packages, and they're often built around Liquidsoap:

(Pro) means I consider these items to be fit and proper tools for real radio stations

PlayoutONE: great for internet stations and certainly one to watch as it's hot and going places
Myriad: I remember playing with an early version years ago and I've never heard a bad thing about it

Streaming Server Setup

I'm going to assume you've already got a streaming server, if not I've written a detailed guide which will help you get setup with Icecast:

If you've followed that guide you'll already have your streaming server setup; and you'll know how to go live and stream to the world, but what about those times when there's nobody around to play songs? That's where automation comes in!

I highly recommend DigitalOcean if you don't yet have a cloud server.

Sign up to Digital Ocean and get $100 free credit to create a streaming server

Disclosure: Sponsored link.

A $5 cloud server is enough to run your website; host your stream and get your station on Alexa. If you want to scale to infinity and beyond we'll cover that later on the in guide when we get into HLS: HTTP Live Streaming and Content Distribution Networks.

In order to proceed, you're going to need to be able to spin up mountpoints on your streaming server. If you've not got a good understanding, and mastery, over Icecast or Shoutcast, you're going to want to address that before scrolling any further down this page.

Liquidsoap: a Swiss-Army knife for multimedia streaming

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.

Installing Liquidsoap

For this guide we're looking at installing Liquidsoap on an Ubunto virtual server in the cloud, I run Liquidsoap on my cloud servers, I've also got it installed on my Windows development machine so I can hack around with code locally.

If you've followed my guide to setting up an Icecast streaming server you'll have a cloud server with Icecast2; you'll know what you're doing with PuTTY and you'll be familiar with executing commands via SSH.

In order to conduct brain surgery on your server, you're going to need a scalpel: open PuTTY and login.

Install the repository signing key:

apt-key adv --keyserver --recv-keys 20D63CCDDD0F62C2

Add the repository:

add-apt-repository ppa:sergey-dryabzhinsky/ffmpeg

Add the source:

echo deb bionic main >> /etc/apt/sources.list.d/liquidsoap.list

Install Liquidsoap:

apt-get install liquidsoap-1.4.2

Check it's installed:

liquidsoap --version

That should return the following:

Liquidsoap 1.4.2
Copyright (c) 2003-2019 Savonet team
Liquidsoap is open-source software, released under GNU General Public License.
See for more information.
You have installed Liquidsoap: congratulations!

It's probably a good time to RTFM.

.liq files - Recipes for Radio

Liquidsoap runs small scripts called .liq files, they can be as simple as one or two lines, yet they can be complex and powerful enough to meet even the most exhausting demands. Consider each .liq file a radio station in code form, each one capable of managing multiple inputs, and sending different formats to different streams.

Liquidsoap can run multiple radio stations on the same server, each .liq file is a different station: this stuff is awesome!

There are two core aspects to Liquidsoap, sources and outputs:

  • Single Files
  • Directories
  • Playlists
  • Local Sources
  • Remote Sources
  • MP3 / AAC / AAC+ / Ogg Vorbis / FFMPEG / HLS
  • Icecast Servers
  • Shoutcast Server
  • Broadcast Audio Processing
  • Files

We're going to Install Liquidsoap, then get to grips with .liq files, and basics like playing a single file, then we'll move onto some more advanced setups with daypartings, static and dynamic playlists, and the ability to go live and take over from the automation.

If you want to be inspired by what is possible check out Radio France, they use Liquidsoap to power:

I tip my hat in respect to Maxime Bugeia, he's the guy who pulled that off, I'm in awe of his work. He even explains how he did it.

Example 1: Playing a Single File

It makes sense to start with playing a single file, in a real-world scenario the main case for using this would be as a last resort backup, a SHTF emergency feed which only gets broadcast when all other mountpoints have failed.

Dead air sucks! Build redundancy into your cloud automation: think about cascading fallback sources and mounts

Playing a single track isn't much practical use outside backup and SHTF scenarios, but playing one song on repeat is a publicity stunt, ideal for a station relaunch: Steve Orchard pulled that trick back in 2014:

Three radio stations owned by Quidem started playing Queen's Bohemian Rhapsody on Friday evening, claiming to have been seized

I used to work at one of those seized stations, 107 Oak FM. Was there pre-launch up to the point a newspaper group bought it, then sacked any face that didn't fit, mine included: newspapers and radio, a bit like oil and water in my opinion. Possibly accounting for why my face didn't fit.

If you followed my Icecast guide you might be wondering how you get your audio files onto the server.

I can't actually remember how I setup SFTP on my server, I use Keys to login and really should take better notes. Don't forget to take notes.

Let's begin by moving into the main liquidsoap directory where it reads the .liq files from:

cd /etc/liquidsoap

Open the Nano text editor and create your first .liq file:

nano /etc/liquidsoap/example-1.liq

Now paste all of this into the file:


audio = single( "your_emergency_backup_file.mp3" )

output.icecast( %fdkaac( channels=2, samplerate=44100, bitrate=192 ),
  host = "",
  port = 8000,
  user = "robo",
  password = "123456",
  mount = "robo",
  mksafe( audio )

#!/usr/bin/liquidsoap is the shebang line, it tells the server which software to execute the file with when you run it via the command line.

set("init.allow_root",true) permits root level access, as I only use my root security keys to login.

set("log.file.path","log.log") instructs it to log everything to the imaginatively named log.log file.

set("log.level",3) sets the log.level; the default is 3, the possible values are:

  1. Critical
  2. Important
  3. Normal
  4. Information
  5. Debug

As with all log files, keep an eye on their size: unmanaged log files can grow to eat all available space on a server, of course logging is optional, but how else are we meant to know how and why things haven't worked?

audio = single( "your_emergency_backup_file.mp3" ) tells Liquidsoap about the file we want to play, keep this in the same directory as your .liq files.

output.icecast( %fdkaac( channels=2, samplerate=44100, bitrate=192 ),...... is where Liquidsoap is instructed to encode a stream, as stereo AAC at 192 kbps, to an Icecast server. The other details are the credentials for the Icecast server.

Liquidsoap Encoding Formats

I know what it's like when you're cutting and pasting code from tutorials, and I appreciate you might not have a mountpoint setup to test the stuff on this page, that's why I've left in live Icecast credentials, feel free to test with them, but don't take the p*** ;)

mksafe( audio ) instructs Liquidsoap to fallback to silence in the event of not being able to play the specified file. Liquidsoap comes with fallbacks which will enable us to build a basic cascade of inputs, and add some redundancy to our automaton in following examples.

Make the file executable: You need to do this with all .liq files

chmod u+x example-1.liq

Check it:

liquidsoap --check example-1.liq

This will either return nothing to your screen in which case everything is good to go, or it'll highlight any errors you need to fix before you run it.

Execute it:



liquidsoap example-1.liq

If you didn't change those Icecast details you should now be able to tune in at or

If that does work, now is the time to sort out your own mountpoints.

Example 2: Playing from a Directory

Playing from a dictory uses the playlist() function; you can randomly select songs from a directory, or play each track in a folder in sequence.

Random selection of songs:


backup = single( "your_emergency_backup_file.mp3" )

directory = playlist( "path/to/your/music/folder" )

audio = fallback( track_sensitive=false, [ directory, backup ] )

output.icecast( %fdkaac( channels=2, samplerate=44100, bitrate=192 ),
  host = "",
  port = 8000,
  user = "robo",
  password = "123456",
  mount = "robo",
  mksafe( audio )

backup = single( "your_emergency_backup_file.mp3" ) sets a backup file we hope we never have to broadcast!

directory = playlist( "path/to/your/music/folder" ) sets the directory to play music from.

audio = fallback( track_sensitive=false, [ directory, backup ] ) tells Liquidsoap to play music from the directory specified, or to play the backup file: at an absolute worse case, it won't be able to find any music, or the backup file, and will just stream silence.

track_sensitive=false tells Liquidsoap not to wait till the end of a track before transitioning sources. There will be times when you need it to be true, and others where it must be false. Experiment with it to learn the behaviour of each, in different circumstances.

For sequential selection of songs add mode="normal", before the directory:

directory = playlist( mode="normal", "path/to/your/music/folder" )

Mode Values:

To reload the directory every X minutes add reload=600, before the directory path:

directory = playlist( reload=600, "path/to/your/music/folder" )

A note about reloads and reload modes:

Example 3: Injecting Audio and Remote Control with Telnet

Now we need to be able to control this thing, that's where Telnet comes in.

Add the following settings to your .liq file:

set("server.telnet", true)
set("server.telnet.port",  4242)

set("server.telnet", true) tells Liquidsoap to enable Telnet

set("server.telnet.port", 4242) tells Liquidsoap to listen out for Telnet requests on port 4242, change this to something only you know

Open up another instance of PuTTY, or run cmd.exe and enter the following to connect to Liquidsoap via Telnet:


telnet 123.345.567.789 1234 for example will connect to the given IP address on port 1234.

Once connected enter help for the full list of commands available to you:

Available commands:
| folder.reload
| folder.uri [<URI>]
| exit
| help [<command>]
| list
| quit
| request.alive
| request.all
| request.metadata <rid>
| request.on_air
| request.resolving
| request.trace <rid>
| robo.autostart
| robo.metadata
| robo.remaining
| robo.skip
| robo.start
| robo.status
| robo.stop
| uptime
| var.get <variable>
| var.list
| var.set <variable> = <value>
| version

It's worth spending a little time to get to know these commands, and how they relate to each other, to better understand your automation.

folder. is relating to the playlist, in this case it's the name of the directory we're playing from and gives us the following controls:

When you use folder.uri [<URI>] to inject a single track, it will play that track, then revert to playing from the directory it was before the request; if you use that command to play a directory, or playlist, Liquidsoap will keep on playing from that directory, or playlist, till you instruct it otherwise.

Before you push remote URLs into your output validate the contents first: I was experimenting and managed to crash Liquidsoap a number of times with remote addresses. It's best to WGET / CURL them first, then serve them up locally to avoid possible problems.

robo. is relating to the output mountpoint, in this case it's my robo mount, these controls are worthy of note:

I've got a button on my dashboard where I can .skip tracks in my stream's automation; but the command I use most is .remaining, this is queried by my playlist application in the run up to the news. Liquidsoap's response is used to work out how long a song I need to fill the gap between the end of the current song, and the start of the news. It isn't perfect but it backtimes better than the average community radio presenter.

Read the guide to Manual Interaction with the Liquidsoap Server to better understand how you can automate control with a PHP Telnet Script:


if( !isset( $_GET['telnet_command'] ) ){
  $command = 'help';
  $command = $_GET['telnet_command'];

$fp = @fsockopen( 'localhost', 2424 );
if ( $fp ) {
  fwrite( $fp, $command."\n" );
  $line = @fread( $fp, 2048 );
  echo '<pre>'.$line.'</pre>';
  echo 'Error: unable to open telnet connection to localhost - is Liquidsoap running?';

It's basic, and it's rough around the edges but the code above is enough for you to build a more secure version from.

Example 4: Playing Jingles, Bringing in Branding

This adds a directory of jingles to the mix, roughly one every three songs:

jingles = playlist( "path/to/your/jingles" )

mixed = rotate( weights=[ 1, 3 ], [ jingles, directory ] )

audio = fallback( track_sensitive=false, [ mixed, backup ] )

mixed = rotate( weights=[ 1, 3 ], [ jingles, directory ] ) rotates jingles and tracks from the directory, with a weighting of 1 to 3.

I said roughly above because sometimes it will play two jingles back to back, other times it might play two songs then a jingle, or four. It's not an exact rotation, really music and branding should be managed, not left to random rotation.

You may think scheduling jingles like commercials is overkill, I thought so at first years back when one station I managed the ads for wanted all their branding scheduling: it wasn't overkill, their ad inventory commanded good prices, they knew their audience. It worked for them.

Scheduling your branding brings a more consisent sound, but mainly it stops presenters from playing the same jingles all the time; and we've all heard those presenters who play the same three jingles over and over, and they're usually name drops which w*** off the presenter's ego.

Sorry, I can't mention name drops without including a couple of name drops:

The voiceover in both these cuts is Mitch Johnson, top bloke and voice legend.

I'm not certain but if my memory is correct, that sung jingle was a Widebuddah production.

Example 5: Playing from Different Sources at Different Times

It is an oversimplification to say "Different Times", there's 168 hours in a week, that's actually a lot of different times!

I didn't know what the weight of 168 hours felt like till I was managing the output of an internet station: we had a live breakfast show, voicetracked mid-mornings with Andy Crane, I was live in the afternoon, evenings and lates were voice tracked by Chris Marsden and Graham Torrington respectively. A great line up, minus myself as I was too busy producing the voicetracked shows to focus on my own live show!

I had 16 hours of voicetracked automation to make sound live and seamless each day, more at weekends. 168 hours is heavy, and as soon as one hour passes, there's another one which needs producing!

Cloud radio still needs managing and producing. There's more to it than throwing some songs in folders

In order to help us manage schedules Liquidsoap comes with some rather limited time intervals:

That's sufficient to manage all 168 hours in a week; but overlooking months and years is a bit of an oversight.

Basic Dayparting:

day = playlist( "path/to/your/music/day_folder" )
night = playlist( "path/to/your/music/night_folder" )

daypartings = switch( [ 
                        ( { 6h-18h }, day ), 
                        ( { 18h-6h }, night ) 
                      ] )

audio = fallback( track_sensitive=true, [ daypartings, backup ] )

daypartings = switch( [ ... sets out daypartings with a switch, if you've worked with JavaScript Switch Stattements or PHP Switches you should be cool with Liquidsoap switches as they're similar; as soon as it finds a match it executes it, and ignores everything else. A little more on that later.

Dayparted Music and Branding:

day = playlist( "path/to/your/music/day_folder" )
day_jingles = playlist( "path/to/your/day_jingles" )
mixed_day = rotate( weights=[ 1, 3 ], [ day_jingles, day ] )

night = playlist( "path/to/your/music/night_folder" )
night_jingles = playlist( "path/to/your/night_jingles" )
mixed_night = rotate( weights=[ 1, 3 ], [ night_jingles, night ] )

daypartings = switch( [ 
                        ( { 6h-18h }, mixed_day ), 
                        ( { 18h-6h }, mixed_night ) 
                      ] )

Standard Radio Dayparting:

This is a cliche of a schedule, but allows more flexibility than just switching between day and night music selections; it also has shows which span midnight, and ones which only appear at the weekend to highlight options of how to schedule via a switch.

LS Day
7 + 0

The same schedule expressed as a Liquidsoap Switch:

Great of manifestations, there's only one person on earth I know who would get this joke about Thelema. Hi.

Only joking, though some websites have Liquidsoap code samples that are about as reader friendly as hieroglyphics.

Here's that schedule as close to human readable as I can get it:

daypartings = switch( [

  #0000 - 0200
  ( { ( 1w ) and 0h-2h }, rock ),
  ( { ( 2w or 3w or 4w or 5w ) and 0h-2h }, late ),
  ( { ( 6w or 7w ) and 0h-2h }, dance ),

  # 0200 - 0600
  ( { ( 1w or 2w or 3w or 4w or 5w ) and 2h-6h }, night ),
  ( { ( 6w or 7w ) and 2h-6h }, chillout ),

  # 0600 - 1000 
  ( { ( 1w or 2w or 3w or 4w or 5w ) and 6h-10h }, breakfast ),
  ( { ( 6w or 7w ) and 6h-10h }, chillout ),

  # 1000 - 1400
  ( { 10h-14h }, day ), 

  # 1400 - 1800
  ( { ( 1w or 2w or 3w or 4w or 5w ) and 14h-18h }, drive ),
  ( { ( 6w or 7w ) and 14h-18h }, day ),

  # 1800 - 2200 
  ( { ( 1w or 2w or 3w or 4w or 5w ) and 18h-22h }, evening ),
  ( { ( 6w or 7w ) and 18h-22h }, party ),

  # 2200-0000
  ( { ( 1w or 2w or 3w or 4w ) and 22h-0h }, late ),
  ( { ( 5w or 6w ) and 22h-0h }, dance ),
  ( { ( 7w ) and 22h-0h }, rock )

] )

That switch is loaded in a manner which breaks the day into the relevent daypartings, it then goes through each day and sets the output for that time. If you look at the schedule above and pick an hour from it, you'll easily be able to match that hour to output in the switch.

Switches can be b*****s:

The Switch manual says "it will select the first source whose predicate is true"; as I said earlier, it will go through the list of possibilities, then as soon as it finds a match it will execute it, and forget the rest of the switch. This means that it's easy to load up the priorities in a switch in a way that will create matches when you don't want them.

Say for the sake of example that your breakfast playlist runs weekdays from 0600 to 1000, except on Friday when at 0900 it changes styles. There are two ways to do this, one will fail because the different hour is scheduled after breakfast, the other will work as it will evaluate as true before breakfast:

The wrong way:

  ( { ( 1w or 2w or 3w or 4w or 5w ) and 6h-10h }, breakfast ),
  ( { ( 5w ) and 9h-10h }, different ),

The right way:

  ( { ( 5w ) and 9h-10h }, different ),
  ( { ( 1w or 2w or 3w or 4w or 5w ) and 6h-10h }, breakfast ),

I believe loading your switch like in the dayparted schedule is the easiest way to manage, and check, what you should be broadcasting and when. It's a logical common sense approach that doesn't look like hieroglyphics.

Example 6: Playing from a Static Playlist (yyyy-mm-dd-hh.pls)

All the scheduling software I've used has defaulted to exporting logs in yyyy-mm-dd-hh format, but we've already established that Liquidsoap doesn't handle months and years in the time intervals: if we want to use datestamped playlists from scheduling software we need a quick fix to make Liquidsoap aware of the year and month.

PHP to get the name of the next hour's playlist:

date_default_timezone_set( 'Europe/London' );
echo '/path/to/your/playlists/folder/'.date( 'Y-m-d-h', time() + 3600 ).'.pls';

date_default_timezone_set( 'Europe/London' ); needs changing to your local timezone.

That PHP simply returns the datestamped filename of the playlist to use in the next hour. It takes the time right now, adds one hour, and returns the format we need. If we didn't add an extra hour it would always reply with the current hour and our playlists would be an hour behind on air.

The following script will first look for a playlist for the hour ahead, if one is found it will be played. If there's no playlist for that hour it will revert to random play from day and night directories, and mix in day or night branding. If none of the above works it will revert to the emergency backup file.

Select a Custom Playlist for the hour ahead, or play in random mode from the dayparting:


backup = single( "your_emergency_backup_file.mp3" )

day = playlist( "path/to/your/music/day_folder" )
day_jingles = playlist( "path/to/your/day_jingles" )

mixed_day = rotate( weights=[ 1, 3 ], [ day_jingles, day ] )

night = playlist( "path/to/your/music/night_folder" )
night_jingles = playlist( "path/to/your/night_jingles" )

mixed_night = rotate( weights=[ 1, 3 ], [ night_jingles, night ] )

static_playlist  = list.hd( default="", get_process_lines( "php -q /path/to/yyyy-mm-dd-hh.php" ) )
playlist = playlist( mode="normal", static_playlist )

daypartings = switch( [ 
                        ( { 6h-18h }, mixed_day ), 
                        ( { 18h-6h }, mixed_night ) 
                      ] )

cascade = fallback( track_sensitive=true, [ playlist, daypartings ] )
audio = fallback( track_sensitive=true, [ cascade, backup ] )

output.icecast( %fdkaac( channels=2, samplerate=44100, bitrate=192 ),
  host = "",
  port = 8000,
  user = "robo",
  password = "123456",
  mount = "robo",
  mksafe( audio )

list.hd( default="", get_process_lines( "php -q /path/to/script" ) ) is where Liquidsoap accesses the PHP script which returns the value we need, but this same method could be used to get any output from a script which you could then use in your .liq file. The possibilities are wide.

Liquidsoap Playlist Formats

Basic Playlist:

/path/to/music/Robert Palmer - I'll Be Your Baby Tonight.mp3
/path/to/music/Michael Bolton - Time, Love And Tenderness.mp3
/path/to/music/MC Hammer - U Can't Touch This.mp3

Playlists can be as simple as a single item per line, like the example above. or more advanced with instructions on how to crossfade each track.

Example 7: Crossfading and Smarter Sequences with Annotations

Anyone with ears will notice there's enough space to park a car between songs at the moment. We've have a 24/7 internet radio jukebox of automated music, but it's not sounding smooth like it should yet.

Liquidsoap comes with built in functions to handle cross fades; they can be used out of the box, or they can be fine tuned.

Add crossfades to transitions:

mixed_day = crossfade( rotate( weights=[ 1, 3 ], [ day_jingles, day ] ) )

mixed_night = crossfade( rotate( weights=[ 1, 3 ], [ night_jingles, night ] ) )

playlist = crossfade( playlist( mode="normal", static_playlist ) )

That's very simple, you just just wrap your playlist inside a crossfade function, then sit back and hope for the best.

The defaults on the crossfade function have limitations:

Personally I'd rather listen without the default crossfades, I prefer small gaps to crap sounding transitions.

If you want to sound amazing all the time, which I hope you do, it's going to take some serious work: each track in your library will need annotations which instruct Liquidsoap when to play it from, how long to play it for, when to start the next song and if it should apply a fade in, or a fade out.

Those at stations with a professionally managed library of music, already have much of the hard work done for them: I imagine you've all the cue points and even know when the vocals start. However, those with a few mp3 files, who want a stream to sound slick, with custom transitions on each track, well, sorry to say you've got your work cut out.

Disclaimer: I've got a library of music, only the smallest percentage is fully cue-pointed
Managing music, to broadcast standards, isn't easy. It takes work and dedication

If you're a Head of Music at a radio station, I tip my hat to you. You'll understand the above so much better than most. I've met too many people who think having a flash drive of stolen music from LimeWire, and rips from YouTube or Spotify, is all it takes to start a station.

"I've got 30,000 songs" is something I've heard before, how many did they have which were fit for broadcast? Not one song was radio ready! Different bitrates, different formats, different volume levels, different audio quality. The only consistency was the inconsistency.

Don't be scared of pulling an all nighter, or three, if you want to get your music up to scratch, it's a lot of work going through an entire library to add all the Annotational Cue Points:

I built myself a track editor so I can set cue points; but honestly, it's hard work when there's a lot of tracks!

My track editor

Music library management is a lot of effort, but it's worth it for being able to make Playlists with annotations:

annotate:liq_cue_in="1.5",liq_fade_in="0",liq_start_next="1.6":/path/to/music/Robert Palmer - I'll Be Your Baby Tonight.mp3
annotate:liq_cue_in="0.9",liq_fade_in="0",liq_start_next="5.5":/path/to/music/Michael Bolton - Time, Love And Tenderness.mp3
annotate:liq_cue_in="0",liq_fade_in="0",liq_start_next="3.5":/path/to/music/MC Hammer - U Can't Touch This.mp3

Just like regular playlists, playlists with annotations have one track per line, their magic happens thanks to the 'annotated' values but to apply these values to our crossfades, we need to Execute our Playlist with the Cue Cut Function:

playlist = crossfade( cue_cut( playlist( mode="normal", static_playlist ) ) )

By placing the cue_cut function inside the crossfade function, we're telling Liquidsoap to transition tracks with exact instructions. It doesn't work the other way around, cue_cut must be wrapped inside the crossfade function.

Concept: Playing from a Dynamic Playlist (MySQL and PHP Playlists)

I'm just throwing out a skeleton of a database and a concept with this rather than fully working samples. I use Icecast, Liqduisoap and MySQL to power my stream: you can have the theory, some suggested tables and code samples, but you have to write the rest yourself.

In order to build dynamic playlists you're going to need some database tables to store all the info: you'll need artists, tracks, and log tables.


CREATE TABLE 'artists' (
  'id' bigint(5) NOT NULL,
  'artist_name' varchar(160) NOT NULL,
  'total_plays' bigint(20) NOT NULL,
  'last_played' varchar(30) DEFAULT NULL,
  'gap' int(3) NOT NULL,
  'related' varchar(255) NOT NULL,
  'twitter' varchar(16) NOT NULL

ALTER TABLE 'artists'

ALTER TABLE 'artists'

That's more than you need, the interesting bits are:


CREATE TABLE 'tracks' (
  'id' bigint(5) NOT NULL,
  'artist_id' bigint(5) NOT NULL,
  'title' varchar(160) NOT NULL,
  'filename' varchar(160) NOT NULL,
  'tags' varchar(100) NOT NULL,
  'month' int(2) NOT NULL,
  'year' year(4) NOT NULL,
  'total_plays' bigint(5) NOT NULL,
  'last_played' varchar(32) NOT NULL,
  'likes' bigint(5) NOT NULL,
  'dislikes' bigint(5) NOT NULL,
  'requests' bigint(5) NOT NULL,
  'score' bigint(5) NOT NULL,
  'gap' int(3) NOT NULL,
  'suspended' enum('true','false') NOT NULL,
  'duration' float NOT NULL,
  'cue_in' float NOT NULL,
  'cue_cut' float NOT NULL,
  'fade_in' float NOT NULL,
  'fade_out' float NOT NULL,
  'start_next' float NOT NULL

ALTER TABLE 'tracks'
  ADD UNIQUE KEY 'id' ('id'),
  ADD KEY 'index' ('year'),
  ADD KEY 'title' ('title'),
  ADD KEY 'artist_id' ('artist_id');

ALTER TABLE 'tracks'

Again, there's more than you need there, the extras are:

If you want to sound tight, the most important values in the table relate to liq_cue_* points:

PHP Playlist Application:

$playlist['artist_hours']               = '7';
$playlist['track_hours']                = '52';

$sql = "SELECT tracks.*
  tracks, artists
  tracks.last_played < DATE_SUB( '$now', INTERVAL ".$playlist['track_hours']." HOUR )
  artists.last_played < DATE_SUB( '$now', INTERVAL ".$playlist['artist_hours']." HOUR )
  tracks.artist_id =
  tracks.suspended !=true
  tracks.score desc,
  tracks.requests desc, 
  tracks.likes desc,

$result = $conn->query( $sql );

if( $result->num_rows > 0 ) {
  while( $row = $result->fetch_assoc() ) {
    echo 'annotate:type="song",tid="'.$row['id'].'",liq_cue_in="'.$row['cue_in'].'",liq_cue_out="'.$row['cue_cut'].'",liq_fade_in="'.$row['fade_in'].'",liq_fade_out="'.( $row['cue_cut'] - $row['fade_out'] ).'",liq_start_next="'.( $row['cue_cut'] - $row['start_next'] ).'":'.$stream['music_path'].$row['filename']."\n";

There's much more that could be done in a playlisting application, it could check a different table before running to see if there are any jingles, or features that need broadcasting, and with a large enough database that's fully cue pointed, you should be able to time your news so it's on the hour.

Here are two ideas from the above code you could build in:

Bringing it all together in Liquidsoap:

def get_next_track_from_database() =
  request.create( list.hd( get_process_lines( "php /path/to/playlist.php" ) ) )

dynamic_playlist = crossfade( cue_cut( request.dynamic( get_next_track_from_database ) ) )

get_next_track_from_database contains the output from the playlist script, the request.dynamic function is then used add it to the request queue.

Telling Liquidsoap what to play is only half the deal, something as equally important is getting Liquidsoap to log what has been played:

def apply_metadata(m) =
  track_title = m["title"]
  server = "http://localhost/track_logger.php?"
    tid = "tid=" ^m["tid"]
    uri = (

audio = on_metadata( apply_metadata, audio )

on_metadata( is a function which is executed when Liquidsoap detects a change in the meta data from the stream, in this instance it'll send the track ID to a logging application which will update the artist and track tables, and write an entry to the log database:

  'id' bigint(5) NOT NULL,
  'artist_id' bigint(5) NOT NULL,
  'track_id' bigint(5) NOT NULL,
  'listeners_at_start' bigint(5) NOT NULL,
  'listeners_30_seconds_later' float NOT NULL

  ADD UNIQUE KEY 'date_time' ('date_time'),
  ADD KEY 'artist_id' ('artist_id','track_id'),
  ADD KEY 'track_id' ('track_id'),
  ADD KEY 'date_time_2' ('date_time');


My logging table doesn't just make a record of which artist and track has been played, it also accomodates a couple of audience measurement metrics:

I pull the info from Icecast when I need it, another option would be to adopt Icecast's Yellow Pages Protocol and have it push you the info.

By hosting your own 'directory' you can get Icecast to post you this information as often as every 20 seconds; in theory it's possible to keep a near constant monitor on when people tune out, and at what points, in a track.

Logging on the back end:

$playlist['related_artist_hours']       = '4';

$trackq = "
  tracks.artist_id as artist_id, as gap, 
  artists.related as related, as artist_gap
WHERE'".trim( addslashes( $_GET['tid'] ) )."' 

$trackq  = $conn->query($trackq);

$track_data       = $trackq->fetch_array();
$artist_id        = $track_data['artist_id'];
$artist_gap       = $track_data['artist_gap'];
$track_gap        = $track_data['gap'];
$related          = $track_data['related'];

if( $related != '' ){
  $related = trim( $related, ',' );
  $relatives = explode( ',', $related);

  $play_relative_next = new DateTime( date( "Y-m-d H:i:s" ) );
  $play_relative_next->modify( "+".$playlist['related_artist_hours']." hour" );
  $play_relative_next = $play_relative_next->format( "Y-m-d H:i:s" );

  foreach( $relatives as $relative ){
    echo "add ".$playlist['related_artist_hours']." to $relative = $play_relative_next";
    $update_related = "update artists set last_played = '$play_relative_next' where id='$relative' limit 1;";
    echo "$update_related";
    $update_related  = $conn->query( $update_related );

if( $artist_gap > 0 ){
  $date = new DateTime( date( 'Y-m-d H:i:s' ) );
  $date->modify( "+$artist_gap day" );
  $artist_last_played = $date->format( "Y-m-d H:i:s" );
  $artist_last_played = date( 'Y-m-d H:i:s' );

if( $track_gap > 0 ){
  $date = new DateTime( date( 'Y-m-d H:i:s' ) );
  $date->modify( "+$track_gap day" );
  $track_last_played = $date->format( "Y-m-d H:i:s" );
  $track_last_played = date( 'Y-m-d H:i:s' );

$sql = "
  tracks.last_played = '$track_last_played',
  tracks.total_plays = tracks.total_plays + 1 ,
  artists.last_played = '$artist_last_played',
  artists.total_plays = artists.total_plays + 1
WHERE = '".trim( addslashes( $_GET['tid'] ) )."'
AND = tracks.artist_id";

$result = $conn->query( $sql );

$sql = " 
    ( 'id', 'artist_id', 'track_id', 'date_time', 'listeners_at_start', 'listeners_30_seconds_later' )
    ( NULL, '$artist_id', '".$_GET['tid']."', '".date( 'Y-m-d H:i:s' )."', '$listeners', '0.1' )";

$result = $conn->query( $sql );
$last_id = $conn->insert_id;


This application is notified by Liquidsoap everytime a track changes. It gets the track ID and queries the database to get the required info: then it updates the artist, track and related artist 'last played' values, and logs how many people are listening.

By using the 'related artists' and 'last played' values we can exclude tracks from selection, example: everytime Phil Collins is played Genesis tracks, and Phil Collins and Phil Bailey are put back a few hours. Same with Stevie Nicks and Fleetwood Mac, it takes related artists out of rotation for the specified time.

This script is notified about track changes so you could in theory use it to post now playing data to Twitter, but please don't, it sucks and all you have to do validate that statement is look at the interaction on #nowplaying tweets.

One of the best uses for this feature is to send notifications to the TuneIn Air API for Broadcasters.

This whole concept is to get you thinking about what you could potentially build. Dynamic Playlists open many possibilities:

Example 8: How to give Live Broadcasts Priority

If you have a dedicated mountpoint that your studio or presenters connect to when going live, Liquidsoap can tell when it's broadcasting or not. If there's no live broadcast it'll play the specified fallback source, or it'll rebroadcast the stream on air:

audio = fallback.skip( input=input.http( "http://server:port/mount" ), dynamic_playlist)

input=input.http( "http://server:port/mount" ) can be any streaming source, if the mountpoint is live it'll go to air.

Warning: Anyone who can connect to that mountpoint as a broadcaster will be able to take over your output

With Icecast you can authenticate each broadcaster as they try to connect to your mountpoint. You need to use the mount_add authentication option and run the input through a script. This will let you control who can broadcast, and when: it also means you can lock out sacked presenters.

I knew how to hack the transmission chain at one FM which sacked me: taking them off air out of spite was easy. I could do it by dialing an ISDN codec which when called would cut the audio to the KiloStream and bingo: they were off air. I found that out one day when setting up an outside broadcast, we simply dialed the wrong codec by accident and knocked the station off air. As emergency engineering contact, billing £50 a callout, I milked that like a cow.

Ex-presenters can be real a*sholes, make sure all your systems lock them out, fully, that way they can't play any pranks on you, I'm speaking from experience on both sides of the fence here. Consider it a case of poacher turned gamekeeper.

Example 9: Save Output to a File (Cloud Audio Logging)

It's prudent to have a copy of whatever you've put out on air, and some legislators like OFCOM have requirements for you to keep a copy of whatever you've broadcast for 42 days; people mishear things and complain, having audio to backup your side of any potential arguments is essential.

Back in 1993 when I first started out in radio all the logging was done through three VHS video recorders rackmounted in cabinets; each would record in long-play mode so a four hour tape would cover eight hours, one machine started recording as the other stopped, you needed 126 video tapes, there would be tape changes twice every day, and you had to keep a tick sheet to log when tapes had been changed, and by whom: it was a pain in the a*se.

Video logging took 126 tapes to log 42 days, whilst digital logging is much easier, instead of having shelves of video cassettes you need to be aware of file sizes and have sufficient storage space on your server, or better still get a cloud server that's dedicated to logging.

Sign up with Digital Ocean for $100 free credit to build your own audio logger

Disclosure: Sponsored link.

How much disc space you need to store audio logs:

Duration / Bitrate
320 kbps
192 kbps
128 kbps
64 kbps
32 kbps
140.63 MB
84.38 MB
56.25 MB
28.13 MB
14.06 MB
3.30 GB
1.98 GB
1.32 GB
675.00 MB
337.50 MB
23.07 GB
13.84 GB
9.23 GB
4.61 GB
2.31 GB
28 Days
92.29 GB
55.37 GB
36.91 GB
18.46 GB
9.23 GB
42 Days
138.43 GB
83.06 GB
55.37 GB
27.69 GB
13.84 GB

See my Bandwidth Calculator for different bitrates and durations.

Logging each hour to a file:

output.file(%fdkaac( channels=2, samplerate=44100, bitrate=32 ), 'path/to/audio_logs/%Y-%m-%d-%H-%M-%S.aac', audio, reopen_when={ 0m } )

The same methodology applies when sending output to files as it does to outputting to an Icecast server.

Instead of output.icecast( we're using output.file( and %fdkaac( channels=2, samplerate=44100, bitrate=32 ) which instructs Liquidsoap to create an AAC formatted file at 32 kbps for every hour it's broadcasting, and to save these files in the audio_logs directory.

reopen_when={ 0m } gives the instruction to reload the contents to a new file at the start of each hour. It's cool to watch log files and monitor the directory when audio files are created. Did I just say it's cool to watch log files? I need to get out more.

Logging all output in 15 minute files:

output.file(%fdkaac( channels=2, samplerate=44100, bitrate=32 ), 'path/to/audio_logs/%Y-%m-%d-%H-%M-%S.aac', audio, reopen_when={ 0m or 15m or 30m or 45m } )

Like the example before this one, we're using the reopen_when paramater but this time we're passing instructions to save to a new file every fifteen minutes.

{ 0m or 15m or 30m or 45m } instructs at what minute intervals to create a new log file, the timings here work the same way as the time intervals in switches which allow us to be pretty specific about what output we log.

Logging only live broadcasts to file:

output.file(%fdkaac( channels=2, samplerate=44100, bitrate=32 ), '/path/to/audio_logs/%Y-%m-%d-%H-%M-%S-live.aac', input.http( "http://server:port/mount" ), fallible=true, reopen_when={ 0m } )

input.http( "http://server:port/mount" ) is pulling in the stream from the specified server; port and mountpoint, fallible=true is the instruction which says that source is fallable, in other words is prone to fail, or not be streaming as it is in this case: fallible=true simply tells Liquidsoap not to worry if there's no stream, but the moment there's output on that mountpoint output.file ensures it'll be logged.

In addition to logging your live broadcasts with Liquidsoap, you can also dump a broadcast to a file with Icecast:


You need to edit icecast.xml and add a <dump-file> tag to the mountpoint; this will make it record all incoming connections to the dump file set..

Icecast Mountpoint Documentation

Manually cleaning up old files

As we established earlier logging audio output can take up a lot of disc space, run this command to delete all AAC files older than 42 days:

find /path/to/audio_logs -name "*.aac" -mtime +42 -exec rm {} \;

exec and rm commands both deserve respect, you don't know how powerful they are till you've slipped up. The chown and rm -rf commands both scare the hell out of me. With great power comes great responsibility: if you want to murder a server play with those two commands and watch what happens.

Automating file cleanup with CRON

Manually performing techie admin tasks sucks, add the following to your CRONTAB:

0 4 * * * find /path/to/audio_logs -name "*.aac" -mtime +42 -exec rm {} \;

0 4 * * * instructs the CRONTAB to execute the given command at 04:00 every day.

If you don't know what CRON is you're probably doing too much mundane techie stuff manually, CRON is your best friend if you want to automate tasks on a server. You can make it execute every minute of every day, or at a time you dictate. Crontab Guru is pretty neat.

CRON + Icecast = Dynamic Stream Intros

CRON is powerful: a great idea for it is to manage which stream intro files are heard at what times:

  1. Make CRON write over the intro file set in icecast.xml with the new intro file for that mountpoint
  2. Make CRON reload Icecast

I could write up a small guide on how to do dynamic Icecast intros, but for now I'll just leave this idea here.

Example 10: Liquidsoap Output to HLS

HLS is HTTP Live Streaming and it's very different to regular streaming. Unlike Icecast and Shoutcast it's not broadcasting at a constant bitrate over a streaming protocol, it uses Adaptive Bitrate Streaming and serves files over a regular HTTP connection.

In the upcoming example we'll take our input audio; convert it into three different bitrates, create a live playlist and a playlist for each bitrate. Each playlist will contain the details of the timestamped files Liquidsoap will output and the media player does the rest.

The listener's device connects to the live playlist and picks a bitrate based on the quality of the user's connection, then gets the right files for that situation, example: high quality on broadband connections, medium quality on 3G and low for when the signal sucks

HLS is perfect for in car listening thanks to the dynamic bitrate switching. I hope to get a new car soon so will write the guide to automotive streaming once I can say Hey BMW, play XXX...

This is Maxime Bugeia's Liquidsoap to HLS example:

aac_lofi = %ffmpeg(format="mpegts",

aac_midfi = %ffmpeg(format="mpegts",

aac_hifi = %ffmpeg(format="mpegts",

streams_info = [("aac_lofi",(40000,"mp4a.40.29","ts")),
streams = [("aac_lofi",aac_lofi), 
           ("aac_midfi", aac_midfi), 
           ("aac_hifi", aac_hifi)]
def segment_name(~position,~extname,stream_name) =
  timestamp = int_of_float(gettimeofday())
  duration = 2


The only things I changed there are the paths to the files. It worked first run on my server, though my local installation of Liquidsoap didn't like it.

Get the link to your live playlist and open it in a media player of your choice: it should play

aac_lofi creates the 16k stream, aac_midfi makes the 48k feed, and aac_hifi is the 96k one, now we have the three different fidelity outputs we need the magic to switch between streams depending on the user's connection.

streams_info = [("aac_lofi",(40000.... is pairing a stream name with a connection rate of bits per second, if you want to go deeper down the Rabbit Hole read the RFC, which for those who don't know is a technical acronym of Really F*cking Complicated.

streams = [("aac_lofi"... pairs the stream names and audio sources together. I really like the rationale of how these values all relate, but the real wonder of how it all works becomes much clearer when we see the playlists we're making.

def segment_name(... tells Liquidsoap to break down the respective streams into two second segments that are named with the relevent timestamps.

Finally, output.file.hls(playlist="live.m3u8"... is the instruction to output a master live.m3u8 playlist, this is what end users access, it also applies the segmentation and source matching patterns, before outputting all the files in the /hls/files directory.

Liquidsoap HLS Ouput

Take a look at the live.m3u8 playlist:


Now we can see how the settings in the streams_info and streams sections have generated the playlist names, and we can see how different connection qualities relate to different playlists.

Inside the aac_hifi.m3u8 playlist it looks like this:


That playlist simply gives the user a list of timestamped files to access, inside the directory set you'll see ever changing files:

HLS timestamped AAC files for HLS delivery

Files are automatically purged once expired but if you stop Liquidsoap it will leave files in the directory which you may want to clean out.

You should now be listening to your stream through HLS, and have some understanding of how it all works: cool eh?

Supercharge your Stream: Scaling to infinity and beyond!

Now your stream is rocking with HLS you need a way to guarantee it can handle all the listeners you could ever throw at it.

What if you could serve virtually limitless listeners? You can!
You just need a Content Distribution Network to serve your HLS files

If you're streaming in HLS and doing it through a CDN the sky is the limit. You can go full Buzz Lightyear with this setup!

Rather than having each listener connect to your server you can use an intermediary called a Content Distribution Network, or CDN for short. A CDN takes your content and distributes it over a network of computers around the globe, it then connects your visitors to the files on the computer nearest to them.

Without a CDN your streaming server would have to accomodate each and every listener, whilst this is fine for delivering a few thousand hours of audio a month, it's not ideal for serving a hundred thousand listeners at once.

In the image below the 'Origin Server' is your streaming server and the users are your listeners; the top section shows how they'd all connect directly to your server without a CDN, and the lower section shows how the CDN does the heavy lifting, with listeners connecting to the nearest server:


Delivering your audio through a CDN takes a lot of weight off your server's shoulders. It also means you don't have to worry about getting a sudden spike of listeners, the CDN will handle it.

A CDN is designed to accomodate huge spikes in traffic

Requests to your server and overheads are minimised as you only need to give each timestamped audio file to the CDN once; the CDN will cache that file and serve it to your listeners. This means you can go big. Real big.

Mentally replace the static file foobar.css in this example with an audio file like aac_hifi_2_1599327821_4769.ts to see how it works, it doesn't matter how many people request the file from the CDN, the CDN will only request it once from your server:


Icecast is solid for me, it's been load tested to handle 14,000 concurrent listeners which is impressive but we're aiming to handle huge listener figures here, well beyond what Icecast can cope with. Stick a zero on the end, that's what we want: something that can handle 140,000 concurrent listeners.

DigitalOcean are a solid dependable infrastructure partner, however if we want to scale, and I mean scale big we only want to use the Icecast and DigitalOcean to manage our audio: we should let a dedicated CDN provider who is HLS optimised take over with the delivery aspect.

Scale your audience beyond your wildest dreams with KeyCDN:


You can use KeyCDN's Content Distribution Network to help supercharge your HLS live stream
KeyCDN offers an optimized HLS feature free for all customers which allows you to accelerate your HLS streams even faster

Sign up to KeyCDN and get 250 GB data free

Disclosure: Sponsored link.

The KeyCDN network has extensive coverage with 34 data centers in 25 countries spread across 6 continents:

Europe: Amsterdam, Bucharest, Frankfurt, Helsinki, London, Madrid, Milan, Moscow, Oslo, Paris, Stockholm, Vienna, Warsaw, Zurich
North America: Atlanta, Chicago, Dallas, Los Angeles, Miami, Montreal, New York, San Francisco, Seattle
Asia: Bangalore, Hong Kong, Istanbul, Singapore, Tokyo
Oceania: Auckland, Melbourne, Perth, Sydney (Oceana sounds a bit too 1984 for my liking)
South Africa: Johannesburg, South America: São Paulo

KeyCDN scale on demand enabling you to stream to one listener or one million listeners

How to Supercharge your Stream:

What's the cost? from

KeyCDN Pricing

Sign up to KeyCDN and get 250 GB data free

Running your HTTP Live Stream through a CDN is overkill if you only have a tiny audience; the real benefits come from larger numbers of listeners, and you need to be aware that there are certain drawbacks with HLS which you don't get when serving your listeners through Icecast:

Want dedicated hardware colocation instead of Cloud hosting?

If your audience is UK based and your station consumes a lot of data, I have an old friend from my mobile content days who now manages a state of the art datacenter and fibre network which supplies connectivity to one of the UK's leading mobile networks.

They have the infrastructure, right here in the UK, to deliver millions of hours of audio to your listeners.

If your station wants to colocate dedicated physical hardware in the UK, and have access to world class connectivity get in touch*.

* = Please note: I'm only going to introduce people who I know are serious and who play on a different level to the rest of us.


This the most autistic thing I've ever written, congratulations for getting to the end!

There are over 1,400 1,500 1,600 lines of HTML in this guide. It's taken me quite a lot of hyperfocus to write it. If it helps your station do more in-house, or has helped your audience grow to infinity and beyond, please consider contributing to my Neumann TLM 103 fund on Amazon:

Has this helped? Why not treat me to an Amazon Gift Card?

© 2020 Andy Moore Privacy Policy Terms and Conditions