AoIP and Liquidsoap

Many modern broadcast facilities, like the one I work at, are fully AoIP enabled. AoIP gives stations great flexibility and availability with their audio sources, and enables engineers to rely less on end-point hardware, like sound cards and processors, that might create single points of failure and require problematic digital-to-analog conversions.

In order to fully reap the benefits of AoIP, audio must stay “on the network” for as many tasks as possible, including processing and encoding. This helps eliminate single points of failure and results in more flexibility with audio paths. To this end, I’ve implemented a simple encoding solution that utilizes existing hardware, open-source software, and an AoIP driver to create a more robust and flexible Internet streaming appliance.

The first step I took was identifying the proper device to be the audio encoder. I experimented exhaustively with this task to ensure latency was consistently low enough to process the AoIP streams reliably. I initially tried using a virtual machine hosted on our redundant virtualization infrastructure, but latency became a problem with the contentious nature of the platform. After lots of tweaking and experimenting, I gave up on the VM, and opted for installation on a physical Windows 10 machine in our automation network. We have about a half-dozen workstations with identical hardware, so I chose the most under-utilized machine. We reap two benefits from this option:

  • Using dedicated hardware means low latency is much easier to achieve and maintain
  • Using an automation machine means that I can easily migrate streaming operations to another nearly identical automation machine in the event of maintenance or failures.

Unfortunately, if something goes wrong with the physical machine, only administrator intervention or some creative scripting can restore streaming by migrating operations to a spare; this is what I hoped to avoid by using a redundant VM. That said, risk of failure is relatively low as the hardware has been historically reliable. It is pertinent to setup a “hot spare” that can be activated in the event of a failure, however.

Next, I installed Wheatstone’s WDM WNIP drivers which creates several virtual sound cards accessible on the WheatNET AoIP routing matrix. This driver is relatively pricey and requires a WheatNet network, but the driver itself is cheaper than a professional sound card. Once the driver was installed, I routed our program audio to the new destinations in the matrix.

Our ultimate destination for this audio stream is an Icecast service hosted on the public Internet, so I decided to use Liquidsoap as the encoder for several reasons: it’s open source, I’m already familiar with its feature set, it’s platform agnostic, and most importantly, it plays nicely with the WheatNET driver. I tried several different encoding applications, but liquidsoap was the right price (free) and has a ton of features- no need to buy a license or unlock functionality.

Installing Liquidsoap in Windows 10 is a breeze. Just grab the latest Windowns zip file and extract it to your destination of choice. I chose the “C:\liquidsoap” directory. Liquidsoap uses a scripting language to define encoders and delivery methods. It’s generally recommended to you create a script for every audio interface you plan on using, allowing more even CPU load distribution. You may also assign separate clocks to each output using the “clock.assign_new” method which will result in separate CPU threads for each output. Here’s the resulting (sanitized) script for our Icecast service:

#!/usr/bin/liquidsoap -v
# C:\liquidsoap\stl.liq

set("log.level", 3)
set("log.file.path", "log/stl.log")
set("log.stdout", true)

# The setenv line determines which audio device to use
# Experiment with the value of the variable to determine
# which PA device matches which audio interface.
# It may not be a 1-to-1, 2-to-2 match
setenv("PA_RECOMMENDED_INPUT_DEVICE","1")
# Despite the fact that this script utilizes the
# "input.portaudio" function, you do not have to
# install Portaudio; liquidsoap bundles it.
source = input.portaudio()

# Set some variables to be used in each output
genre = "News, Classical, Jazz"
description = "Radio you need to know."
name = "WYSU Main Channel"
host = "live.streamguys1.com"
port = 3180
url = "https://wysu.org"
password = "SANITIZED"

# HD1 128kbps MP3 Service
output.icecast(
	%mp3(bitrate=128, internal_quality=4),
	mount = "hd1",
	genre = genre,
	name = name,
	host = host,
	port = port,
	password = password,
	url = url,
	description = description,
	public = true,
	mksafe(buffer(source))
)

# HD1 256kbps AAC Service
output.icecast(
	%fdkaac(bitrate=256, channels=2, samplerate=44100, afterburner=false, aot="mpeg4_aac_lc", transmux="adts", sbr_mode=false),
	mount = "hd1-256",
	genre = genre,
	name = name,
	host = host,
	port = port,
	password = password,
	url = url,
	description = description,
	public = true,
	mksafe(buffer(source))
)

Once the scripts were in place and tested, I created a scheduled task that runs the following script when users logon:

cd C:\liquidsoap
start /realtime liquidsoap.exe stl.liq

Notice the “/realtime” argument which sets the CPU priority of the encoder. This will default to “below normal” which generally isn’t desirable for real-time streaming applications.

Now it’s a matter of wash, rinse, and repeat for each streaming source. We’re licensed for four stereo AoIP channels per workstation, so we could encode up-to four different audio sources to a theoretically unlimited number of destinations.