Reading and decoding RDS (Radio Data System) in C#
RDS or Radio Data System is very common in US and many European countries. It is communication protocol used to send small amount of digital information using regular FM radio broadcast. This protocol is used to “tell” your receiver about alternative frequencies, time, program notifications, program types, traffic information and regular text (such as singer name or genre). Unfortunately in Israel RDS is not very common and there is very limited number of radio stations broadcasts RDS information.
How RDS works?
As mentioned earlier, it uses FM subcarrier to broadcast digital information. It was designed to support 10 and 18 characters numeric and 80 characters alphanumeric displays. RDS operates at 1187.5 bps and based on 26-bit word consisting of 16 data and 10 error detection bits. Due to the fact, that FM carrier is not very reliable, error code allows correct information to be received even if an error of 3-5 bits exists within 26 bit block. Each four data blocks interpreted as 104-bit signal and named “group”. Depending of the type of information, contained within the group, as different group type code is defined and transmitted within the group as upper five bits code. Even if more, then 104 bits required to completely send the information, there is no requirement that the next segment of the transmission be sent in the next group. There are 32 known groups types, defined by RFC:
private enum groupType : byte {
RDS_TYPE_0A = (0 * 2 + 0),
RDS_TYPE_0B = (0 * 2 + 1),
RDS_TYPE_1A = (1 * 2 + 0),
RDS_TYPE_1B = (1 * 2 + 1),
RDS_TYPE_2A = (2 * 2 + 0),
RDS_TYPE_2B = (2 * 2 + 1),
RDS_TYPE_3A = (3 * 2 + 0),
RDS_TYPE_3B = (3 * 2 + 1),
RDS_TYPE_4A = (4 * 2 + 0),
RDS_TYPE_4B = (4 * 2 + 1),
RDS_TYPE_5A = (5 * 2 + 0),
RDS_TYPE_5B = (5 * 2 + 1),
RDS_TYPE_6A = (6 * 2 + 0),
RDS_TYPE_6B = (6 * 2 + 1),
RDS_TYPE_7A = (7 * 2 + 0),
RDS_TYPE_7B = (7 * 2 + 1),
RDS_TYPE_8A = (8 * 2 + 0),
RDS_TYPE_8B = (8 * 2 + 1),
RDS_TYPE_9A = (9 * 2 + 0),
RDS_TYPE_9B = (9 * 2 + 1),
RDS_TYPE_10A = (10 * 2 + 0),
RDS_TYPE_10B = (10 * 2 + 1),
RDS_TYPE_11A = (11 * 2 + 0),
RDS_TYPE_11B = (11 * 2 + 1),
RDS_TYPE_12A = (12 * 2 + 0),
RDS_TYPE_12B = (12 * 2 + 1),
RDS_TYPE_13A = (13 * 2 + 0),
RDS_TYPE_13B = (13 * 2 + 1),
RDS_TYPE_14A = (14 * 2 + 0),
RDS_TYPE_14B = (14 * 2 + 1),
RDS_TYPE_15A = (15 * 2 + 0),
RDS_TYPE_15B = (15 * 2 + 1)
}
Not all groups are in use all the time. However, there are some commitments, defined by the protocol. For example, 1A have to be transmitted at least once a second. This group contains special information, required for receivers to be synchronized and locked into the transmitting channel.
Within the error correction information we also receive the direction to treat them.
private enum correctedType : byte {
NONE = 0,
ONE_TO_TWO = 1,
THREE_TO_FIVE = 2,
UNCORRECTABLE = 3
}
Also, each message type has it own limits. For example RT (Radio Text – 64 character text to display on your receiver) and PS (Programme Service – eight character station identification) message are limited to 2 groups, when PI (Programme Identification – unique code of the station) and PTY (Programme Type – one of 31 predefined program types – e.g. News, Drama, Music) are limited to 4.
In addition to those constraints, block types are also different. But in this case, there are only 4 kinds
private enum blockType : byte {
A = 6,
B = 4,
C = 2,
D = 0
}
So, what we’re waiting for? Let’s start working.
Handling errors
First of all we should take care on errors and fix them if possible. For this purpose, we should first count them and detect the way of fixing
var errorCount = (byte)((registers[0xa] & 0x0E00) >> 9);
var errorFlags = (byte)(registers[0x6] & 0xFF);
if (errorCount < 4) {
_blocksValid += (byte)(4 – errorCount);
} else { /*drop data on more errors*/ return; }
Once it done, we can try to fix them
//Also drop the data if more than two errors were corrected
if (_getErrorsCorrected(errorFlags, blockType.B) > correctedType.ONE_TO_TWO) return;private correctedType _getErrorsCorrected(byte data, blockType block) { return (correctedType)((data >> (byte)block) & 0×30); }
Now, our registers should be fine and we can start the detection of group type
Group Type Detection
This is very simple task, all we have to do is to get five upper bites to get a type and version.
var group_type = (groupType)(registers[0xD] >> 11);
Then we can handle PI and PTY, which we always have in RDS.
PI and PTY treatment
Now, let’s update pi code, due to the fact, that B format always have PI in words A and C
_updatePI(registers[0xC]);
if (((byte)group_type & 0×01) != 0) {
_updatePI(registers[0xE]);
}
To update PI, we should check whether the new value is different from the previous and update it only in case it changed.
private void _updatePI(byte pi) {
uint rds_pi_validate_count = 0;
uint rds_pi_nonvalidated = 0;// if the pi value is the same for a certain number of times, update a validated pi variable
if (rds_pi_nonvalidated != pi) {
rds_pi_nonvalidated = pi;
rds_pi_validate_count = 1;
} else {
rds_pi_validate_count++;
}if (rds_pi_validate_count > PI_VALIDATE_LIMIT) {
_piDisplay = rds_pi_nonvalidated;
}
}
Then we will update PTY
_updatePTY((byte)((registers[0xd] >> 5) & 0x1f));
PTY treatment is very similar to PI, however it can be multiplied.
private void _updatePTY(byte pty) {
uint rds_pty_validate_count = 0;
uint rds_pty_nonvalidated = 0;// if the pty value is the same for a certain number of times, update a validated pty variable
if (rds_pty_nonvalidated != pty) {
rds_pty_nonvalidated = pty;
rds_pty_validate_count = 1;
} else {
rds_pty_validate_count++;
}if (rds_pty_validate_count > PTY_VALIDATE_LIMIT) {
_ptyDisplay = rds_pty_nonvalidated;
}
}
When we done with those two groups, we can start handling another. Today, we’ll handle only 0B, 2A and 2B types (I have a good reason for it, due to the fact, that only those are supported in Israel by now
) So,
Handling PS and different RTs
Simple switch on those groups
switch (group_type) {
case groupType.RDS_TYPE_0B:
addr = (byte)((registers[0xd] & 0×3) * 2);
_updatePS((byte)(addr + 0), (byte)(registers[0xf] >> 8));
_updatePS((byte)(addr + 1), (byte)(registers[0xf] & 0xff));
break;
case groupType.RDS_TYPE_2A:
addr = (byte)((registers[0xd] & 0xf) * 4);
abflag = (byte)((registers[0xb] & 0×0010) >> 4);
_updateRT(abflag, 4, addr, (byte[])registers.Skip(0xe), errorFlags);
break;
case groupType.RDS_TYPE_2B:
addr = (byte)((registers[0xd] & 0xf) * 2);
abflag = (byte)((registers[0xb] & 0×0010) >> 4);
// The last 32 bytes are unused in this format
_rtTmp0[32] = 0x0d;
_rtTmp1[32] = 0x0d;
_rtCnt[32] = RT_VALIDATE_LIMIT;
_updateRT(abflag, 2, addr, (byte[])registers.Skip(0xe), errorFlags);
break;
}
and let’s dig into PS.
In PS, we have high and low probability bits. So, if new bit in sequence matches the high probability bite and we have recieved enough bytes to max out the counter, we’ll push it into the low probability array.
if (_psTmp0[idx] == default(byte)) {
if (_psCnt[idx] < PS_VALIDATE_LIMIT) {
_psCnt[idx]++;
} else {
_psCnt[idx] = PS_VALIDATE_LIMIT;
_psTmp1[idx] = default(byte);
}
}
Else, if new byte matches with the low probability byte, we should swap them and then reset the counter, by flagging the text as in transition.
else if (_psTmp1[idx] == default(byte)) {
if (_psCnt[idx] >= PS_VALIDATE_LIMIT) {
isTextChange = true;
}
_psCnt[idx] = PS_VALIDATE_LIMIT + 1;
_psTmp1[idx] = _psTmp0[idx];
_psTmp0[idx] = default(byte);
}
When we have an empty byte in high probability array or new bytes does not match anything we know, we should put it into low probability array.
else if (_psCnt[idx] == null) {
_psTmp0[idx] = default(byte);
_psCnt[idx] = 1;
} else {
_psTmp1[idx] = default(byte);
}
Now, if we marked our text as changed, we should decrement the count for all characters to prevent displaying of partical message, which in still in transition.
if (isTextChange) {
for (byte i = 0; i < _psCnt.Length; i++) {
if (_psCnt[i] > 1) {
_psCnt[i]–;
}
}
}
Then by checking PS text for incompetence, when there are characters in high probability array has been seen fewer times, that was limited by validation.
for (byte i = 0; i < _psCnt.Length; i++) {
if (_psCnt[i] < PS_VALIDATE_LIMIT) {
isComplete = false;
break;
}
}
Only if PS text in the high probability array is complete, we’ll copy it into display.
if (isComplete) {
for (byte i = 0; i < _psDisplay.Length; i++) {
_psDisplay[i] = _psTmp0[i];
}
}
It is not very hard to treat PS. Isn’t it? Let’s see what’s going on with RT.
If A and B message flag changes, we’ll try to force a display by increasing the validation count for each byte. Then, we’ll wipe any cached text.
if (abFlag != _rtFlag && _rtFlagValid) {
// If the A/B message flag changes, try to force a display
// by increasing the validation count of each byte
for (i = 0; i < _rtCnt.Length; i++) _rtCnt[addr + i]++;
_updateRTValue();// Wipe out the cached text
for (i = 0; i < _rtCnt.Length; i++) {
_rtCnt[i] = 0;
_rtTmp0[i] = 0;
_rtTmp1[i] = 0;
}
}
Now A and B flags are safe, sp we can start with message processing. First of all, NULL in RDS means space
_rtFlag = abFlag;
_rtFlagValid = true;for (i = 0; i < count; i++) {
if (p[i] == null) p[i] = (byte)’ ‘;
The new byte matches the high probability byte also in this case. We habe to recieve this bite enough to max out counters. Then we can push it into the low probability as well.
if (_rtTmp0[addr + i] == p[i]) {
if (_rtCnt[addr + i] < RT_VALIDATE_LIMIT) _rtCnt[addr + i]++;
else {
_rtCnt[addr + i] = RT_VALIDATE_LIMIT;
_rtTmp1[addr + i] = p[i];
}
}
When the new byte matches with low probability byte, we’ll swap them as well and reset counters to update text in transition flag. However in this case, our counter will go higher, then the validation limit. So we’ll have to remove it down later.
else if (_rtTmp1[addr + i] == p[i]) {
if (_rtCnt[addr + i] >= PS_VALIDATE_LIMIT) isChange = true;
_rtCnt[addr + i] = RT_VALIDATE_LIMIT + 1;
_rtTmp1[addr + i] = _rtTmp0[addr + i];
_rtTmp0[addr + i] = p[i];
}
Now, the new byte is replaced an empty byte in the high probability array. Also, if this byte does not match anything, we should move it into low probability.
else if (_rtCnt[addr + i] == null) {
_rtTmp0[addr + i] = p[i];
_rtCnt[addr + i] = 1;
} else _rtTmp1[addr + i] = p[i];}
Now when the text is changing, we’ll decrement the counter for all characters exactly as we did for PS.
for (i = 0; i < _rtCnt.Length; i++) {
if (_rtCnt[i] > 1) _rtCnt[i]–;
}
}
However, right after, we’ll update display.
_updateRTValue();
}
Displaying RT
But how to convert all those byte arrays into readable message? Simple
First of all if text is incomplete, we should keep loading it. Also it makes sense to check whether the target array is shorter then maximum allowed to prevent junk from being displayed.
for (i = 0; i < _rtTmp0.Length; i++) {
if (_rtCnt[i] < RT_VALIDATE_LIMIT) {
isComplete = false;
break;
}
if (_rtTmp0[i] == 0x0d) {
break;
}
}
Now, when our Radio Text is in the high probability and it complete, we should copy buffers.
if (isComplete) {
_Text = string.Empty;for (i = 0; i < _rtDisplay.Length; i += 2) {
if ((_rtDisplay[i] != 0x0d) && (_rtDisplay[i + 1] != 0x0d)) {
_rtDisplay[i] = _rtTmp0[i + 1];
_rtDisplay[i + 1] = _rtTmp0[i];
} else {
_rtDisplay[i] = _rtTmp0[i];
_rtDisplay[i + 1] = _rtTmp0[i + 1];
}if (_rtDisplay[i] != 0x0d)
_Text += _rtDisplay[i];if (_rtDisplay[i + 1] != 0x0d)
_Text += _rtDisplay[i + 1];if ((_rtDisplay[i] == 0x0d) || (_rtDisplay[i + 1] == 0x0d))
i = (byte)_rtDisplay.Length;
}
And not forget to wipe out everything after the end of the message
for (i++; i < _rtDisplay.Length; i++) {
_rtDisplay[i] = 0;
_rtCnt[i] = 0;
_rtTmp0[i] = 0;
_rtTmp1[i] = 0;
}
}
And finally update the text
Text = _Text;
We done. Now we can handle RDS digital messages, but what to do with analog data we get? Don’t you already know? I blogged about it here.
Have a nice day and be good people, because you know how to write client, knows to get and parse radio data in managed code.
![]()
You may also be interested with:
- Video encoder and metadata reading by using Windows Media Foundation
- RSA private key import from PEM format in C#
December 11th, 2008 · Comments (9)
9 Responses to “Reading and decoding RDS (Radio Data System) in C#”
Leave a Reply
Discover other tags
My tools
- .NET Framework Detector
- Duplicate images finder
- Exchange Security Policy for Windows Mobile Devices Fix
- Gas Price Windows Vista SideBar gadget
- Israel Traffic Information Windows Vista SideBar gadget
- Localization fix for SAP ES Explorer for Visual Studio
- LocTester
- RTL and LTR in Windows Live Writer
- Silverlight controls library
- Snipping tool integration plugin for WLW
- USB FM receiver library
- Vista Battery Saver
- WebCam control for WPF
- Windows Live SkyDrive attachment for Windows Live Writer
- Wireless Migrator
- WPF Virtual Keyboard





January 1st, 2009 at 12:59 am
Pingback from Capturing and streaming sound by using DirectSound with C# | Tamir Khason – Just code
January 1st, 2009 at 12:59 am
Pingback from Read and use FM radio (or any other USB HID device) from C# | Tamir Khason – Just code
January 1st, 2009 at 12:59 am
You’ve been kicked (a good thing) – Trackback from DotNetKicks.com
January 1st, 2009 at 1:00 am
[...] time we spoke about reading and decoding RDS information from FM receivers. Also we already know how to stream sound from DirectSound compatible devices. However, before we [...]
January 1st, 2009 at 1:01 am
[...] about how to get sound from your microphone or any other DirectSound capturing device (such as FM receiver) and stream it out to your PC speakers and any other DirectSound Output device. So, let’s start [...]
August 24th, 2009 at 1:29 am
Hi I have a question about how to make the streaming from right to left RDS info on the LCD be changed to instead scrolling upwards like movie credits especially when I have a big LCD screen, so I can cover it with the slow upward scrolling RDS info received.
Thank you
YK
May 4th, 2011 at 6:13 pm
In searching for coded RDS decoder examples written in C, I came across your blog. I’m not familar with programming in C#, but I am simi-familar with programming in C. I know they’re different programming languages, but they look very similar. Anyway, I was wondering if you could help me with my program written in C. I’ve read all the documentation for my chip (Si4703) and I’ve read the 1998 RBDS US Standard. I understand how to read raw RDS data, but for some reason I’m having a hard time decoding it. Do you think you can help me out?
August 5th, 2011 at 5:09 am
Why are “high/low probability” counters needed for decoding PS? The “U.S. RDBS Standard doc” chapter “3.1.5.1 Type 0 groups: Basic tuning and switching information” does not mention any probabilities at all?
January 25th, 2012 at 1:54 pm
[...] [...]