AES67 Audio using GStreamer

Not long ago, I needed to send audio from a website’s streaming feed to some professional audio equipment that supports AES67 audio. The cross platform GStreamer suite can do this and more.

On Debian systems, you can install GStreamer and friends using:

sudo apt install gstreamer1.0-plugins-base-apps libgstreamer-plugins-* \
    gstreamer1.0-plugins-*

Once you have GStreamer, start a client listening for AES67 so you can test:

gst-launch-1.0 udpsrc address=239.69.161.58 port=5004 ! \
    application/x-rtp, clock-rate=48000, channels=2 ! \
    rtpjitterbuffer ! rtpL24depay ! audioconvert ! audioresample ! autoaudiosink
Setting pipeline to PAUSED ...
Pipeline is live and does not need PREROLL ...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock

Let’s start by sending a sine wave to yourself:

gst-launch-1.0 audiotestsrc freq=440 volume=0.1 ! \
    audioconvert ! audio/x-raw, format=S24BE, channels=2, rate=48000 ! \
    rtpL24pay name=rtppay min-ptime=1000000 max-ptime=1000000 ! \
    application/x-rtp, clock-rate=48000, channels=2, payload=98 ! \
    udpsink host=127.0.0.1 port=5004 qos=true qos-dscp=34
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
Redistribute latency...
New clock: GstSystemClock
^C
handling interrupt.
Interrupt: Stopping pipeline ...
Execution ended after 0:00:10.607570694
Setting pipeline to NULL ...
Freeing pipeline ...

You should hear a 440 Hz “A” note, which is apparently a subject of conspiracy and debate on the Internet. ^C when you’ve had enough. On the listener / player side, you should see a clock start running while it’s receiving audio. It will keep listening on the UDP socket, even after you terminate the sender.

GStreamer can handle just about any format you throw at it, and there are plugins for other things. Let’s play some arbitrary compressed audio file:

gst-launch-1.0 filesrc location=104\ UN\ Greetings\,\ Whale\ Greetings.ogg ! \
    oggdemux ! vorbisdec ! autoaudiosink
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Redistribute latency...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
Redistribute latency...
New clock: GstPulseSinkClock
Got EOS from element "pipeline0".
Execution ended after 0:04:05.048152802
Setting pipeline to NULL ...
Freeing pipeline ...

So if you happen to have a copy of the Voyager Golden Record in OGG format in the current directory, you should hear something. GStreamer works with pipelines of decoders, encoders, filters, etc. gst-launch-1.0 is a tool that lets you build and run these pipelines on the command line. Note the oggdemux ! vorbisdec steps. This pipeline is Ogg Vorbis specific, and will be unhappy if you pass it an .mp3:

gst-launch-1.0 filesrc location=01-super-mario-bros.mp3 ! oggdemux ! vorbisdec \
   ! autoaudiosink
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
ERROR: from element /GstPipeline:pipeline0/GstOggDemux:oggdemux0: Could not demultiplex stream.
Additional debug info:
../ext/ogg/gstoggdemux.c(4533): gst_ogg_demux_find_chains (): /GstPipeline:pipeline0/GstOggDemux:oggdemux0:
can't get first chain
ERROR: pipeline doesn't want to preroll.
ERROR: from element /GstPipeline:pipeline0/GstOggDemux:oggdemux0: Internal data stream error.
Additional debug info:
../ext/ogg/gstoggdemux.c(5013): gst_ogg_demux_loop (): /GstPipeline:pipeline0/GstOggDemux:oggdemux0:
streaming stopped, reason error (-5)
ERROR: pipeline doesn't want to preroll.
Setting pipeline to NULL ...
ERROR: from element /GstPipeline:pipeline0/GstOggDemux:oggdemux0: Could not demultiplex stream.
Additional debug info:
../ext/ogg/gstoggdemux.c(5031): gst_ogg_demux_loop (): /GstPipeline:pipeline0/GstOggDemux:oggdemux0:
EOS before finding a chain
ERROR: pipeline doesn't want to preroll.
Freeing pipeline ...

So for an MP3, you need to change your pipeline accordingly:

gst-launch-1.0 filesrc location=01-super-mario-bros.mp3 ! mpegaudioparse ! \
    mpg123audiodec ! autoaudiosink
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Redistribute latency...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
Redistribute latency...
New clock: GstPulseSinkClock
 ^C 
handling interrupt. (1.7 %)
Interrupt: Stopping pipeline ...
Execution ended after 0:00:02.019700038
Setting pipeline to NULL ...
Freeing pipeline ...

And for a FLAC:

gst-launch-1.0 filesrc location=Rob_Hubbard_-_Commando_C64-qrQuR1LHAVI.flac ! \
    flacparse ! flacdec ! autoaudiosink
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Redistribute latency...
Redistribute latency...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
Redistribute latency...
New clock: GstPulseSinkClock
^C
handling interrupt. (1.5 %)
Interrupt: Stopping pipeline ...
Execution ended after 0:00:04.260792135
Setting pipeline to NULL ...
Freeing pipeline ...

But, whoops! We were not sending these to our UDP listener. autoaudiosink will send the audio out the default sound card. We need to ship the audio out over AES67 UDP packets:

gst-launch-1.0 filesrc location=Rob_Hubbard_-_Commando_C64-qrQuR1LHAVI.flac ! \
    flacparse ! flacdec ! audioconvert ! audioresample ! \
    audio/x-raw, format=S24BE, channels=2, rate=48000 ! \
    rtpL24pay name=rtppay min-ptime=1000000 max-ptime=1000000 ! \
    application/x-rtp, clock-rate=48000, channels=2, payload=98 ! \
    udpsink host=127.0.0.1 port=5004 qos=true qos-dscp=34
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Redistribute latency...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
Redistribute latency...
New clock: GstSystemClock
^C
handling interrupt. (1.8 %)
Interrupt: Stopping pipeline ...
Execution ended after 0:00:04.960337718
Setting pipeline to NULL ...
Freeing pipeline ...

There’s some trial and error involved. Unlike with the sine wave, we had to add an audioresample step. Otherwise you get this:

gst-launch-1.0 filesrc location=Rob_Hubbard_-_Commando_C64-qrQuR1LHAVI.flac ! \
    flacparse ! flacdec !  audioconvert ! \
    audio/x-raw, format=S24BE, channels=2, rate=48000 ! \
    rtpL24pay name=rtppay min-ptime=1000000 max-ptime=1000000 ! \
    application/x-rtp, clock-rate=48000, channels=2, payload=98 ! \
    udpsink host=127.0.0.1 port=5004 qos=true qos-dscp=34
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Redistribute latency...
ERROR: from element /GstPipeline:pipeline0/GstFlacParse:flacparse0: Internal data stream error.
Additional debug info:
../libs/gst/base/gstbaseparse.c(3681): gst_base_parse_loop (): /GstPipeline:pipeline0/GstFlacParse:flacparse0:
streaming stopped, reason not-negotiated (-4)
ERROR: pipeline doesn''t want to preroll.
Setting pipeline to NULL ...
Freeing pipeline ...

While we’re here, there’s a different plugin that will auto-decode most formats so we don’t have to build a specific pipeline for each codec beforehand:

gst-launch-1.0 filesrc location=Rob_Hubbard_-_Commando_C64-qrQuR1LHAVI.flac ! \
    decodebin ! audioconvert ! audioresample ! \
    audio/x-raw, format=S24BE, channels=2, rate=48000 ! \
    rtpL24pay name=rtppay min-ptime=1000000 max-ptime=1000000 ! \
    application/x-rtp, clock-rate=48000, channels=2, payload=98 ! \
    udpsink host=127.0.0.1 port=5004 qos=true qos-dscp=34
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Redistribute latency...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
Redistribute latency...
New clock: GstSystemClock
^C
handling interrupt. (3.2 %)
Interrupt: Stopping pipeline ...
Execution ended after 0:00:08.821843086
Setting pipeline to NULL ...
Freeing pipeline ...

So with the above, you can provide pretty much any audio file and it will play it to the other instance of gst-launch-1.0 still running and listening.

Now if you want to push out a web audio stream, use the uridecodebin plugin:

URI=""

gst-launch-1.0 uridecodebin uri=$URI !
    audioconvert ! audioresample ! \
    audio/x-raw, format=S24BE, channels=2, rate=48000 ! \
    rtpL24pay name=rtppay min-ptime=1000000 max-ptime=1000000 ! \
    application/x-rtp, clock-rate=48000, channels=2, payload=98 ! \
    udpsink host=127.0.0.1 port=5004 qos=true qos-dscp=34

You’ll need to provide a URL for your feed, but otherwise, it should work. If the site you use returns a .m3u file, you’ll need to get the URL out of that.

Wrap Up

Listen for AES67 audio, by default on a multicast address, but it doesn’t have to be:

#!/bin/sh

LISTEN="239.69.161.58"
PORT="5004"
OPTIONS=""
#OPTIONS="multicast-iface=eth0"
# ^ You might need this in a true multicast situation with multiple NICs

gst-launch-1.0 udpsrc address=$LISTEN port=$PORT "$OPTIONS" ! \
    application/x-rtp, clock-rate=48000, channels=2 ! \
    rtpjitterbuffer ! rtpL24depay ! audioconvert ! \
    audioresample ! autoaudiosink

Play out a file:

#!/bin/sh

FILE="$1"
ADDRESS="127.0.0.1"
# ^ Note sending to multicast locally might not work, so we use localhost
# for testing.
PORT="5004"

gst-launch-1.0 filesrc location="$FILE" ! decodebin ! \
    audioconvert ! audioresample ! \
    audio/x-raw, format=S24BE, channels=2, rate=48000 ! \
    rtpL24pay name=rtppay min-ptime=1000000 max-ptime=1000000 ! \
    application/x-rtp, clock-rate=48000, channels=2, payload=98 ! \
    udpsink host="$ADDRESS" port="$PORT" qos=true qos-dscp=34

Play out a stream:

#!/bin/sh

URI="$1"
ADDRESS="127.0.0.1"
# ^ Note sending to multicast locally might not work, so we use localhost
# for testing.
PORT="5004"

gst-launch-1.0 uridecodebin uri="$URI" ! \
    audioconvert ! audioresample ! \
    audio/x-raw, format=S24BE, channels=2, rate=48000 ! \
    rtpL24pay name=rtppay min-ptime=1000000 max-ptime=1000000 ! \
    application/x-rtp, clock-rate=48000, channels=2, payload=98 ! \
    udpsink host="$ADDRESS" port="$PORT" qos=true qos-dscp=34

Bonus 1

You can do some other cool things with GStreamer. If you want to simulate a crappy cell phone connection, try this:

gst-launch-1.0 filesrc location=synth.wav ! decodebin ! audioconvert ! \
    audioresample !  gsmenc ! breakmydata probability=0.01 ! \
    gsmdec ! autoaudiosink

We decode, convert, and resample the audio file. We run it through the GSM audio codec Then, we use the breakmydata plugin to add 1% packet loss. Finally we decode and play out the default speakers. Increase the number for worse results.

Bonus 2

This works on Android. You can install GStreamer using termux and use your phone as a AES67 speaker. Multicast does not seem to work on Android, however.