Experiment: Sydney WFM Broadcast RDS (TMC, RT+) & ACS Audio

Since my last posting about subcarriers, I decided to do a little more digging around and try to understand more about the Open-Application RDS data and try a better approach at recovering the audio from the ACS (otherwise known as SCA) service.


The data carried through RDS on Group 8A with Application ID CD46 is known as TMC data where TMC stands for Traffic Message Channel. This is used by many higher-end standalone GPS units, as well as in-dash car-integrated GPS units where they advertise “live traffic” capabilities. The one-way broadcast is carried by FM radio on the RDS subcarrier to these GPS units to inform them where traffic jams, congestion, roadworks, accidents and other routing challenges may be present.

The protocol is called “ALERT-C” and the data-stream is specified in ISO 14819 series of documents. Most important is that the service merely sends messages containing “bit-mapped” information including an event description identifier, the location identifier, location/extent/direction, duration and whether a diversion is recommended. More complex multi-group messages can also have additional information such as control codes, length of route affected, speed limit, start-stop times, multi-event messages, detailed diversion instructions, destinations, precision location reference, source of problem and telephone services. In this paradigm, a lot of the data including the actual location co-ordinates are not sent through the air. Instead, a licensed receiver will contain the necessary algorithms or look-up tables to translate the codes back into “natural language” prompts on the screen (or in some case, spoken prompts) and take the necessary routing actions. The system can also utilize encryption to make things even less exciting …

This was somewhat expected, as RDS is a very low-bitrate subcarrier (~1kbit/s), and is required to carry station name, radiotext, time and date data as well. An efficient coding scheme is necessary as a result, minimising the transmission of “fixed” data. This does have a side benefit that the “phrases” for events can be stored in multiple languages to be triggered by the same code, although the system is not so flexible as to be able to represent arbitrary events. The list of phrases is maintained by TISA.

Additional to the TMC data itself, a tuning information block is sent periodically to let receivers know of the service name they are tuned to, and adjacent frequencies carrying the same service, and less frequently, of other services. To help TMC-receivers tune in, Group 3A blocks are used to let receivers know that the TMC application is being carried on this frequency and of the necessary information to receive it.

As the system works by one way broadcast, it’s not possible to know if receivers have managed to receive the data. This is especially true in automotive contexts where signal “fading” due to multipath and obstacles on the side of the road can take out the signal. Two strategies are used to try and minimise data loss – the first is immediate data repetition where the same packets are sent several times in sequence, and the second is data repetition in general. As receivers can be turned on “at any time”, the system works more like a bulletin board where each packet is examined as to whether it contains new data, and if so, is then cached locally in the receiver for use. A maximum of 300 “messages” can be active at any time, with the receivers responsible for decrementing times as necessary. As a result, sending the same event content over-and-over is not going to cause any confusion, and thus the TMC channel is just a stream of event updates with higher priority events being broadcasted more frequently – so if your receiver misses one round of transmissions, it only has to wait another few seconds or so for it to be resent assuming there isn’t a colossal amount of events.


The encryption used is apparently fairly “simple” and has been broken with some careful observation. The encryption is specified in ISO 14819-6 and was designed to limit any overhead and simply encrypts the location ID with any one of eight encryption tables.. Encrypted TMC has LTN value of 0, where non-zero LTN values represent non-encrypted services, which can be read from variant 0 Group 3A blocks.

As I had identified three stations carrying CD46 streams, I decided to take a look at their Group 3A blocks – below is the hex full-frame, followed by the last two groups decomposed into binary.

2DAY FM 104.1Mhz
2041 3150 0006 CD46 00000000 00000110  11001101 01000110 (Variant 0 Encrypted)
2041 3150 41C2 CD46 01000001 11000010  11001101 01000110 (Variant 1)

TRIPLEM 104.9Mhz
2049 3570 0007 CD46 00000000 00000111  11001101 01000110 (Variant 0 Encrypted)
2049 3570 4040 CD46 01000000 01000000  11001101 01000110 (Variant 1)

KIIS1065 106.5Mhz
2A1A 3550 0007 CD46 00000000 00000111  11001101 01000110 (Variant 0 Encrypted)
2A1A 3550 4040 CD46 01000000 01000000  11001101 01000110 (Variant 1)

As we can see, all the services on the air are encrypted here in Sydney. This is hardly surprising I suppose – notice the 2-bytes in Group 3 of Variant 0 (LTN) are all zeroes, a reserved value. As for the Variant 1 data, both the latter services have 0x4040, whereas the 2DAY FM carrier has 0x41C2. This represents system timing parameters.

Tuning Information

Part of the “system information” that is carried is the tuning information where the provider name can be carried (eight bytes). This is transmitted relatively infrequently – sometimes it takes a minute before you see it. But at least, it will let us know who is putting the services on the air.

Because I was a complete RDS novice, I didn’t realize the ODA AID tab only provided limited information, and the Group Content Full tab provided the full RDS frame. As a result, I came to the wrong conclusion that the data was alternating in the previous post – only parts of the RDS frame were alternating …


On the 2DAY FM 104.1Mhz carrier, the tuning information packets give the name “HERE SYD” as the provider. Three immediate repetitions are seen for every packet.


On TRIPLE M 104.9, the provider is “SUNA TMC“. Two repetitions are sent.


KIIS1065 shows the same provider and repetition rate. Why they chose to be carried by two relatively popular Sydney area stations is not certain – maybe the two stations have dramatically different coverage towards the fringes. From checking the ACMA licenses for both these stations, it seems their main and backup transmitters are co-sited at Gore Hill/Artarmon and both are 150kW maximum EIRP at 141/170m respectively. Maybe the antenna patterns differ slightly but it seems interesting that they are being carried by two stations.

Decoding the Data

I suppose it’s important to first note that I’m not decrypting the encrypted location IDs in any way – I’m just merely interested in looking at the messages being sent. In theory, I could spend a lot of time writing my own parser to look at the data logged by RDS Spy, but why do this when it has been done before in various ways?

In the end, I settled on gr-rds and GNU Radio as the preferred method. This particular one is a fork of balint256 (Balint Seeber’s) gr-rds and has a parser for RDS-TMC implemented. This also has all of the natural-language phrases which I couldn’t get access to otherwise. Getting it to work wasn’t too much of a chore, which was nice, and I got a mishmash of all RDS messages parsed in the console window.


What you are seeing is a tail of the log, as I diverted all of the console output to file for later analysis. Because the file is a mess with a lot of groups which I’m not interested in, I started by filtering just Group 8A packets using grep –no-group-separator -A 1 08A <inputfile> > <outputfile>. But since the first line wasn’t very useful, and all RDS parser lines started with a #, I decided to change this to grep –no-group-separator -A 0 \# <inputfile> > <outputfile>.


As a result, you get a list of messages which aren’t particularly meaningful on their own, but it’s interesting to see. The above is an excerpt of HERE’s data and it seems every event is diversion-bit-set with no duration (hence receivers don’t need to manage decrementing timers). The types of phrases didn’t seem to be too varied, and the triplicate message sending is seen.


SUNA on the other hand, has duplicate messages and a wider “vocabulary” of traffic issues. The same behaviour of not using duration and setting diversion bit on every message is seen.

Location ID Frequency

I observed 2DAY FM 104.1 between 5/10/2016 13:45 to 6/10/2016 12:12, TRIPLE M 104.9 between 6/10/2016 12:10 to 7/10/2016 08:34 and KIIS1065 106.5 between 7/10/2016 08:44 to 8/10/2016 11:48, all local (UTC+11) time. I didn’t observe the encryption bit prior to the experiment, and thus didn’t realize the locations would have “rotated” at midnight.

Regardless, I wanted to plot the frequency of “events” versus the location ID, so I first had to filter out all duplicate messages. A crude program was developed (in the appendix) which basically created a file for every location ID, and appended the “event” if it was different from the last line in the file. Unfortunately, this isn’t quite sufficient, as you will get massive duplication where interleaved events with opposite directions are sent which should be considered separate areas.


Anyhow, the resulting plot as a stemplot looks like this:


Note the log scale on the y-axis. Whatever encryption and location-ID mapping “HERE” users seems to result in “clumps” of activity across the location IDs, whereas the “SUNA” products seem to show a more “spread” activity. Whether the key rotated is a question, as the high spike in data coincided between both SUNA broadcasts over two different days at location 49634 because of the “interleaved-opposite-direction-events” I mentioned earlier. This suggests the key wasn’t rotated, although the distribution of events seems to cluster more to the left on the KIIS1065 day, and more to the right on the TRIPLE M day. This might just reflect the varying propensity for having an accident in Sydney … but it’s a pretty messy plot … I like it.

Funnily, all of this monitoring suggests that Wikipedia is out of date – their TMC page suggests only SUNA operates in Sydney, which is untrue, and their list of Sydney stations with RDS data only lists KIIS1065 as the only station with TMC, and ignores all the other stations with RDS as found in the previous post. Always good to find something “new”.

RDS-RT+ Data

Another common service carried by three stations is RadioText Plus (4BD7). This seems to put out occasional packets on some stations which coincide with changes in songs, as does the iTunes tagging (C3B0).


The exception to this is Hope1032 which sends the Group 12A packets continually.


Interestingly, after finding a short resource about RT+, it seems that RT+ is a very “efficient” protocol that works with the traditional RT sent in Group 2A/B and merely carries pointers and lengths referencing the strings sent as RT to “carve” out the Artist and Title information. That’s pretty efficient!

ACS Service on 2MBS Fine Music 102.5Mhz

After facing lots of difficulties in trying to decode the ACS service on a 92khz carrier on 102.5Mhz, I decided to do it properly and use the right tools, namely GNU Radio. I already went to the lengths of building and installing it, so why not?

As a result, I implemented this flow-graph of my own design to do the decoding. It’s not nicely abstracted, contains hard-coded values all over the place, etc. But at least, it works.


Initially, I had the audio sent to an Audio Sink for playing out of the sound card, but because what seems to be an audio subsystem “bug”, it always stuttered and complained of underruns. Sending it to a WAV file sink had no such issues, and playing it back resulted in smooth audio at the correct speed with no interruptions, so what happened is probably just peculiar to the VM I am running it in.

Anyway, the station is tuned with the RTL-SDR, offset 250kHz below to avoid the centre spike (I’m using an E4000 tuner). This is then passed through a frequency translating filter to bring the offset to zero. For debugging, lets just chuck in an FFT sink. Do we see the FM carrier? Yes we do!


This is then passed to a WBFM receive block which demodulates the signal and recovers the baseband, sampled at 250khz (due to decimation by 4). Do we now see the baseband signal? Yes we do! We can see the 19khz stereo pilot, the stereo imaging data, the RDS data near 57khz and the ACS at 92khz.


So far so good, of course, now the fun is to shift down the signal at 92khz so we can then receive the “inner FM”. Throw in another frequency translating filter, and we get the signal (albeit relatively weak, due to the lower transmission power).


Then, we pass this to an NBFM receiver to recover the audio. Being a little optimistic, I tried to use a wider 16khz filter and have a maximum deviation of 8khz, but it seems the audio is well band limited at 3khz for a very “telephone quality” result.


Of course, with that, there is no need to sample at 48khz, but it was a “hangover” of being optimized for sending to the sound card which is natively 48khz to try and avoid clocking differences/buffer under/overflows.

The recovered audio was much more stable and cleaner than using rtl_fm and then a second instance of SDR# through a virtual audio card. It clearly identifies the station as Indian Link. Their website claims that they are broadcast from the sidebands, so I suppose they mean subcarriers.


After some playing with more “heavy duty” tools, I managed to understand more about the RDS-TMC, RDS-RT+ services and develop an ACS decoding workflow that actually results in good quality reception. In the process, I satisfied my curiosity about the subcarrier services, and realized that the encrypted TMC services aren’t really of much use to those without the keys and the database of locations. But merely finding the services and the providers seems to have shown that some of the information online is out of date.

Appendix: Filtering Program

This is another quick hacky program that basically reads the logged stream from STDIN and looks for #u in the stream for user messages. Then, it looks for a file with the location ID – if there isn’t one already, it creates one and dumps the line into the file, otherwise it opens the existing file, reads iteratively until it reaches the last line, then compares the contents of that message with the one in the buffer. If it matches, no action is taken, thus filtering immediate transmission duplicates. Otherwise, it appends the line to the file. It’s not efficient, it’s not elegant, and it’s technically not spec compliant because it doesn’t check other properties of the message (e.g. so you can end up with two messages for the same location ID but with opposite directions causing long logs with duplicates especially where they are sent interleaved, where a proper receiver would consider them “separate” messages).

#include <stdio.h>
#include <string.h>
#include <assert.h>

int main (void) {
  char inputline[256] = {0};
  int inputcount = 0;
  char fileline[256] = {0};
  int filecount = 0;
  char locationid[6] = {0};
  int locationcount = 0;
  int tempchar = 0;
  int ftempchar = 0;
  FILE *fptr;

  while(tempchar!=EOF) {
    inputcount = 0;
    while (tempchar!='\n' && tempchar!=EOF) {
    if (tempchar!=EOF && inputline[0]='#' && inputline[1]=='u') {
      while (inputline[inputcount]!=':') {
      while (inputline[inputcount]!='\0') {
      if((fptr=fopen(locationid,"r"))) {
          filecount = 0;
          while (ftempchar!='\n' && ftempchar!=EOF) {
        if(strcmp(inputline,fileline)) {
      } else {

About lui_gough

I'm a bit of a nut for electronics, computing, photography, radio, satellite and other technical hobbies. Click for more about me!
This entry was posted in Computing, Radio and tagged , , , , , . Bookmark the permalink.

3 Responses to Experiment: Sydney WFM Broadcast RDS (TMC, RT+) & ACS Audio

  1. Gerry says:

    Thanks for your in-depth look into RDS :-). I was always curious about the details of the system, especially when I saw radiotext for the first time some years ago.

  2. Jim B says:

    Gough, thanks for taking the time to write this up!

    I found my way here via a link in this blog entry:

    I was surprised to learn that the auxiliary channels are fm modulated within an fm signal. Not that I ever looked into it, but I had just assumed that they were essentially independent frequency bands that were just modulated as fm at their own carrier offset.

    Is there some technical reason why it makes sense to do it that way, vs independently (de)modulating the FM portion and the auxiliary channel separately?

    Anyway, I’m off to explore your older blog posts.

    • lui_gough says:

      I suppose the issue boils down to bandwidth utilization, regulatory approval and a secondary “benefit” of being “less visible”.

      The first thing is that the FM Broadcast Band (e.g. CCIR 87.5Mhz to 108Mhz) is denoted for use by WFM signals of approximately 180 to 250khz width. There’s no technical reason why you couldn’t shove a NFM 8khz or 16khz wide station into the band, but for some reason, the radio planning guys decided that all the stations should use the same bandwidth and mode to avoid any alarm and confusion over strange signals, and prevent interference should two “incompatible” modulations or signals mix together (as some can be jammed or degraded at relatively low levels). In fact, having such “extra” information outside the FM carrier has been used by various in-band on-channel modulation schemes (IBOC), predominantly HD Radio (https://en.wikipedia.org/wiki/HD_Radio) in the US, but haven’t really taken off anywhere overseas.

      Of course, if you transmit a NFM station “in between” stations, you are now also openly visible, and potentially can cause interference to (or more likely, will receive interference from) other nearby WFM transmissions. The reason is because no “physical” filter is perfectly selective and steep, so where two FM signals are of similar strengths within the passband, the capture effect can result in the circuit demodulating a “nearby rival signal”. Not an ideal case, and the main reason why strong stations are usually spread across the dial and not “jam packed” side by side. This allows the receivers to be cheaper as well. Of course, with SDR technology, this is less of an issue as our filters can be very steep and tunable.

      The next thing is of course, spectrum utilization. Unfortunately, in many city areas, FM bands are extremely crowded and space is at a premium. Such “additional” bandwidth utilization can cause channel planning problems and interference with reception in adjacent coverage areas. The Wide-FM signal is already much wider than absolutely necessary to carry audible information (lets say 0-15khz audio, so ~30khz bandwidth is necessary for monaural NFM to carry the information). As a result, the actual WFM signal can carry anywhere up to 90-150khz of “audio” where the ultrasonic frequencies above 15khz aren’t audible to most. They already used the range up to ~53khz to implement stereo, but that still leaves some more “space” which can be used *without taking up any more precious bandwidth*. I think this is the main reason why they use “carrier inside a carrier” to do things – it’s a way of being bandwidth efficient, backwards compatible, etc.

      There are probably more factors than this – I’m no broadcast engineer myself, but it seems like a “decent” way to do things and a formerly more popular way to do things especially when everything had to be done with analog circuitry.

      – Gough

Error: Comment is Missing!