Merge branch 'master' into master

This commit is contained in:
Christophe Jacquet 2024-02-24 22:46:05 +01:00 committed by GitHub
commit a3e3068376
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 512 additions and 54 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
src/waveform_*.wav
src/pydemod
src/test
src/pi_fm_rds
src/*_test
*.o

View File

@ -6,7 +6,7 @@ Pi-FM-RDS
This program generates an FM modulation, with RDS (Radio Data System) data generated in real time. It can include monophonic or stereophonic audio.
It is based on the FM transmitter created by [Oliver Mattos and Oskar Weigl](http://www.icrobotics.co.uk/wiki/index.php/Turning_the_Raspberry_Pi_Into_an_FM_Transmitter), and later adapted to using DMA by [Richard Hirst](https://github.com/richardghirst). Christophe Jacquet adapted it and added the RDS data generator and modulator. The transmitter uses the Raspberry Pi's PWM generator to produce VHF signals.
It is based on the FM transmitter created by Oliver Mattos and Oskar Weigl, and later adapted to using DMA by [Richard Hirst](https://github.com/richardghirst). Christophe Jacquet adapted it and added the RDS data generator and modulator. The transmitter uses the Raspberry Pi's PWM generator to produce VHF signals.
It is compatible with both the Raspberry Pi 1 (the original one) and the Raspberry Pi 2, 3 and 4.
@ -16,7 +16,7 @@ PiFmRds has been developed for experimentation only. It is not a media center, i
## How to use it?
Pi-FM-RDS, depends on the `sndfile` library. To install this library on Debian-like distributions, for instance Raspbian, run `sudo apt-get install libsndfile1-dev`.
Pi-FM-RDS, depends on the `sndfile` library. To install this library on Debian-like distributions, for instance Raspbian, run `sudo apt install libsndfile1-dev`.
Pi-FM-RDS also depends on the Linux `rpi-mailbox` driver, so you need a recent Linux kernel. The Raspbian releases have this starting from August 2015.
@ -75,7 +75,7 @@ The RDS standards states that the error for the 57 kHz subcarrier must be less t
In practice, I found that Pi-FM-RDS works okay even without using the `-ppm` parameter. I suppose the receivers are more tolerant than stated in the RDS spec.
One way to measure the ppm error is to play the `pulses.wav` file: it will play a pulse for precisely 1 second, then play a 1-second silence, and so on. Record the audio output from a radio with a good audio card. Say you sample at 44.1 kHz. Measure 10 intervals. Using [Audacity](http://audacity.sourceforge.net/) for example determine the number of samples of these 10 intervals: in the absence of clock error, it should be 441,000 samples. With my Pi, I found 441,132 samples. Therefore, my ppm error is (441132-441000)/441000 * 1e6 = 299 ppm, **assuming that my sampling device (audio card) has no clock error...**
One way to measure the ppm error is to play the `pulses.wav` file: it will play a pulse for precisely 1 second, then play a 1-second silence, and so on. Record the audio output from a radio with a good audio card. Say you sample at 44.1 kHz. Measure 10 intervals. Using [Audacity](https://www.audacityteam.org/) for example determine the number of samples of these 10 intervals: in the absence of clock error, it should be 441,000 samples. With my Pi, I found 441,132 samples. Therefore, my ppm error is (441132-441000)/441000 * 1e6 = 299 ppm, **assuming that my sampling device (audio card) has no clock error...**
### Piping audio into Pi-FM-RDS
@ -104,7 +104,11 @@ mkfifo rds_ctl
sudo ./pi_fm_rds -ctl rds_ctl
```
Then you can send “commands” to change PS, RT and TA:
At this point, Pi-FM-RDS waits until another program opens the named pipe in write mode
(for example `cat >rds_ctl` in the example below) before it starts transmitting.
You can use the named pipe to send “commands” to change PS, RT and TA. For instance, in
another terminal:
```
cat >rds_ctl
@ -116,9 +120,32 @@ TA OFF
...
```
> [!TIP]
> The program that opens the named pipe in write mode can be started after Pi-FM-RDS
> (like above) or before (in which case Pi-FM-RDS does not have to wait at startup).
Every line must start with either `PS`, `RT` or `TA`, followed by one space character, and the desired value. Any other line format is silently ignored. `TA ON` switches the Traffic Announcement flag to *on*, any other value switches it to *off*.
### Non-ASCII characters
You can use the full range of characters supported by the RDS protocol. Pi-FM-RDS decodes
the input strings based on the system's locale variables. As of early 2024, Raspberry Pi
OS uses by default UTF-8 and the `LANG` variable is set to `en_GB.UTF-8`. With this setup,
it should work out of the box.
If it does not work, look at the first message that Pi-FM-RDS prints out. It should be
something sensible, like:
```
Locale set to en_GB.UTF-8.
```
If it is not consistent with your setup, or if the locale appears to be set to `(null)`,
then your locale variables are not set correctly and Pi-FM-RDS is incapable of working
with non-ASCII characters.
## Warning and Disclaimer
PiFmRds is an **experimental** program, designed **only for experimentation**. It is in no way intended to become a personal *media center* or a tool to operate a *radio station*, or even broadcast sound to one's own stereo system.
@ -197,4 +224,4 @@ The samples are played by `pi_fm_rds.c` that is adapted from Richard Hirst's [Pi
--------
© [Christophe Jacquet](http://www.jacquet80.eu/) (F8FTK), 2014-2019. Released under the GNU GPL v3.
© [Christophe Jacquet](https://jacquet.xyz/en/) (F8FTK), 2014-2024. Released under the GNU GPL v3.

View File

@ -30,16 +30,23 @@ CFLAGS = $(STD_CFLAGS) $(ARCH_CFLAGS) -DRASPI=$(TARGET)
ifneq ($(TARGET), other)
app: rds.o waveforms.o pi_fm_rds.o fm_mpx.o control_pipe.o mailbox.o
$(CC) -o pi_fm_rds rds.o waveforms.o mailbox.o pi_fm_rds.o fm_mpx.o control_pipe.o -lsndfile -lm
app: rds.o waveforms.o pi_fm_rds.o rds_strings.o fm_mpx.o control_pipe.o mailbox.o
$(CC) -o pi_fm_rds rds.o rds_strings.o waveforms.o mailbox.o pi_fm_rds.o fm_mpx.o control_pipe.o -lsndfile -lm
endif
rds_wav: rds.o waveforms.o rds_wav.o fm_mpx.o
$(CC) -o rds_wav rds_wav.o rds.o waveforms.o fm_mpx.o -lsndfile -lm
rds_wav: rds.o rds_strings.o waveforms.o rds_wav.o fm_mpx.o
$(CC) -o rds_wav rds_wav.o rds.o rds_strings.o waveforms.o fm_mpx.o -lsndfile -lm
rds.o: rds.c waveforms.h
rds_strings.o: rds_strings.c rds_strings.h
$(CC) $(CFLAGS) rds_strings.c
rds_strings_test: rds_strings.o rds_strings_test.c
$(CC) -Wall -std=gnu99 -o rds_strings_test rds_strings.o rds_strings_test.c
./rds_strings_test
rds.o: rds.c waveforms.h rds_strings.o
$(CC) $(CFLAGS) rds.c
control_pipe.o: control_pipe.c control_pipe.h rds.h
@ -61,4 +68,4 @@ fm_mpx.o: fm_mpx.c fm_mpx.h
$(CC) $(CFLAGS) fm_mpx.c
clean:
rm -f *.o
rm -f *.o *_test

View File

@ -1,11 +1,8 @@
/*
PiFmRds - FM/RDS transmitter for the Raspberry Pi
Copyright (C) 2014 Christophe Jacquet, F8FTK
See https://github.com/ChristopheJacquet/PiFmRds
rds_wav.c is a test program that writes a RDS baseband signal to a WAV
file. It requires libsndfile.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -19,7 +16,7 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
control_pipe.c: handles command written to a non-blocking control pipe,
in order to change RDS PS and RT at runtime.
*/
@ -42,8 +39,8 @@ FILE *f_ctl;
* Opens a file (pipe) to be used to control the RDS coder, in non-blocking mode.
*/
int open_control_pipe(char *filename) {
int fd = open(filename, O_RDONLY | O_NONBLOCK);
if(fd == -1) return -1;
int fd = open(filename, O_RDONLY);
if(fd < 0) return -1;
int flags;
flags = fcntl(fd, F_GETFL, 0);
@ -89,7 +86,7 @@ int poll_control_pipe() {
return CONTROL_PIPE_TA_SET;
}
}
return -1;
}

View File

@ -6,7 +6,7 @@
*
* See https://github.com/ChristopheJacquet/PiFmRds
*
* PI-FM-RDS: RaspberryPi FM transmitter, with RDS.
* PI-FM-RDS: RaspberryPi FM transmitter, with RDS.
*
* This file contains the VHF FM modulator. All credit goes to the original
* authors, Oliver Mattos and Oskar Weigl for the original idea, and to
@ -34,7 +34,7 @@
* http://www.icrobotics.co.uk/wiki/index.php/Turning_the_Raspberry_Pi_Into_an_FM_Transmitter
*
* All credit to Oliver Mattos and Oskar Weigl for creating the original code.
*
*
* I have taken their idea and reworked it to use the Pi DMA engine, so
* reducing the CPU overhead for playing a .wav file from 100% to about 1.6%.
*
@ -88,6 +88,7 @@
* Richard Hirst <richardghirst@gmail.com> December 2012
*/
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
@ -212,7 +213,7 @@ static struct {
unsigned bus_addr; /* From mem_lock() */
uint8_t *virt_addr; /* From mapmem() */
} mbox;
static volatile uint32_t *pwm_reg;
@ -255,7 +256,7 @@ terminate(int num)
dma_reg[DMA_CS] = BCM2708_DMA_RESET;
udelay(10);
}
fm_mpx_close();
close_control_pipe();
@ -266,7 +267,7 @@ terminate(int num)
}
printf("Terminating: cleanly deactivated the DMA engine and killed the carrier.\n");
exit(num);
}
@ -327,7 +328,7 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
sa.sa_handler = terminate;
sigaction(i, &sa, NULL);
}
dma_reg = map_peripheral(DMA_VIRT_BASE, DMA_LEN);
pwm_reg = map_peripheral(PWM_VIRT_BASE, PWM_LEN);
clk_reg = map_peripheral(CLK_VIRT_BASE, CLK_LEN);
@ -351,7 +352,7 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
fatal("Could not map memory.\n");
}
printf("virt_addr = %p\n", mbox.virt_addr);
// GPIO4 needs to be ALT FUNC 0 to output the clock
gpio_reg[GPFSEL0] = (gpio_reg[GPFSEL0] & ~(7 << 12)) | (4 << 12);
@ -394,7 +395,7 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
cbp--;
cbp->next = mem_virt_to_phys(mbox.virt_addr);
// Here we define the rate at which we want to update the GPCLK control
// Here we define the rate at which we want to update the GPCLK control
// register.
//
// Set the range to 2 bits. PLLD is at 500 MHz, therefore to get 228 kHz
@ -404,7 +405,7 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
//
// However the fractional part may have to be adjusted to take the actual
// frequency of your Pi's oscillator into account. For example on my Pi,
// the fractional part should be 1916 instead of 2012 to get exactly
// the fractional part should be 1916 instead of 2012 to get exactly
// 228 kHz. However RDS decoding is still okay even at 2012.
//
// So we use the 'ppm' parameter to compensate for the oscillator error
@ -412,8 +413,8 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
float divider = (PLLFREQ/(2000*228*(1.+ppm/1.e6)));
uint32_t idivider = (uint32_t) divider;
uint32_t fdivider = (uint32_t) ((divider - idivider)*pow(2, 12));
printf("ppm corr is %.4f, divider is %.4f (%d + %d*2^-12) [nominal 1096.4912].\n",
printf("ppm corr is %.4f, divider is %.4f (%d + %d*2^-12) [nominal 1096.4912].\n",
ppm, divider, idivider, fdivider);
pwm_reg[PWM_CTL] = 0;
@ -433,7 +434,7 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
udelay(10);
pwm_reg[PWM_CTL] = PWMCTL_USEF1 | PWMCTL_PWEN1;
udelay(10);
// Initialise the DMA
dma_reg[DMA_CS] = BCM2708_DMA_RESET;
@ -443,7 +444,7 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
dma_reg[DMA_DEBUG] = 7; // clear debug error flags
dma_reg[DMA_CS] = 0x10880001; // go, mid priority, wait for outstanding writes
uint32_t last_cb = (uint32_t)ctl->cb;
// Data structures for baseband data
@ -453,7 +454,7 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
// Initialize the baseband generator
if(fm_mpx_open(audio_file, DATA_SIZE) < 0) return 1;
// Initialize the RDS modulator
char myps[9] = {0};
set_rds_pi(pi);
@ -461,7 +462,7 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
uint16_t count = 0;
uint16_t count2 = 0;
int varying_ps = 0;
if(ps) {
set_rds_ps(ps);
printf("PI: %04X, PS: \"%s\".\n", pi, ps);
@ -470,9 +471,11 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
varying_ps = 1;
}
printf("RT: \"%s\"\n", rt);
// Initialize the control pipe reader
if(control_pipe) {
printf("Waiting for control pipe `%s` to be opened by the writer, e.g. "
"by running `cat >%s`.\n", control_pipe, control_pipe);
if(open_control_pipe(control_pipe) == 0) {
printf("Reading control commands on %s.\n", control_pipe);
} else {
@ -480,8 +483,8 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
control_pipe = NULL;
}
}
printf("Starting to transmit on %3.1f MHz.\n", carrier_freq/1e6);
for (;;) {
@ -498,11 +501,11 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
}
count++;
}
if(control_pipe && poll_control_pipe() == CONTROL_PIPE_PS_SET) {
varying_ps = 0;
}
usleep(5000);
uint32_t cur_cb = mem_phys_to_virt(dma_reg[DMA_CONBLK_AD]);
@ -522,7 +525,7 @@ int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt,
data_len = DATA_SIZE;
data_index = 0;
}
float dval = data[data_index] * (DEVIATION / 10.);
data_index++;
data_len--;
@ -552,15 +555,15 @@ int main(int argc, char **argv) {
char *rt = "PiFmRds: live FM-RDS transmission from the RaspberryPi";
uint16_t pi = 0x1234;
float ppm = 0;
// Parse command-line arguments
for(int i=1; i<argc; i++) {
char *arg = argv[i];
char *param = NULL;
if(arg[0] == '-' && i+1 < argc) param = argv[i+1];
if((strcmp("-wav", arg)==0 || strcmp("-audio", arg)==0) && param != NULL) {
i++;
audio_file = param;
@ -590,8 +593,13 @@ int main(int argc, char **argv) {
" [-ps ps_text] [-rt rt_text] [-ctl control_pipe]\n", arg);
}
}
// Set locale based on the environment variables. This is necessary to decode
// non-ASCII characters using mbtowc() in rds_strings.c.
char* locale = setlocale(LC_ALL, "");
printf("Locale set to %s.\n", locale);
int errcode = tx(carrier_freq, audio_file, pi, ps, rt, ppm, control_pipe);
terminate(errcode);
}

View File

@ -23,6 +23,8 @@
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include "rds_strings.h"
#include "waveforms.h"
#define RT_LENGTH 64
@ -237,17 +239,11 @@ void set_rds_pi(uint16_t pi_code) {
}
void set_rds_rt(char *rt) {
strncpy(rds_params.rt, rt, 64);
for(int i=0; i<64; i++) {
if(rds_params.rt[i] == 0) rds_params.rt[i] = 32;
}
fill_rds_string(rds_params.rt, rt, 64);
}
void set_rds_ps(char *ps) {
strncpy(rds_params.ps, ps, 8);
for(int i=0; i<8; i++) {
if(rds_params.ps[i] == 0) rds_params.ps[i] = 32;
}
fill_rds_string(rds_params.ps, ps, 8);
}
void set_rds_ta(int ta) {

287
src/rds_strings.c Normal file
View File

@ -0,0 +1,287 @@
/*
PiFmRds - FM/RDS transmitter for the Raspberry Pi
Copyright (C) 2024 Christophe Jacquet, F8FTK
See https://github.com/ChristopheJacquet/PiFmRds
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
// Convert an Unicode code point to a character encoded using the RDS character set.
char codepoint_to_rds_char(wchar_t codepoint) {
// The following table is sorted by ascending RDS character code.
switch (codepoint) {
case 0x000A: return 0x0A; // LINE FEED
case 0x000B: return 0x0B; // END OF HEADLINE
case 0x000D: return 0x0D; // CARRIAGE RETURN
case 0x001F: return 0x1F; // WORD BREAK - SOFT HIPHEN
case 0x0020: return 0x20; // SPACE
case 0x0021: return 0x21; // EXCLAMATION MARK
case 0x0022: return 0x22; // QUOTATION MARK
case 0x0023: return 0x23; // NUMBER SIGN
case 0x00A4: return 0x24; // CURRENCY SIGN
case 0x0025: return 0x25; // PERCENT SIGN
case 0x0026: return 0x26; // AMPERSAND
case 0x0027: return 0x27; // APOSTOPHE
case 0x0028: return 0x28; // LEFT PARENTHESIS
case 0x0029: return 0x29; // RIGHT PARENTHESIS
case 0x002A: return 0x2A; // ASTERISK
case 0x002B: return 0x2B; // PLUS SIGN
case 0x002C: return 0x2C; // COMMA
case 0x002D: return 0x2D; // HYPHEN-MINUS
case 0x002E: return 0x2E; // FULL STOP
case 0x002F: return 0x2F; // SOLIDUS
case 0x0030: return 0x30; // DIGIT ZERO
case 0x0031: return 0x31; // DIGIT ONE
case 0x0032: return 0x32; // DIGIT TWO
case 0x0033: return 0x33; // DIGIT THREE
case 0x0034: return 0x34; // DIGIT FOUR
case 0x0035: return 0x35; // DIGIT FIVE
case 0x0036: return 0x36; // DIGIT SIX
case 0x0037: return 0x37; // DIGIT SEVEN
case 0x0038: return 0x38; // DIGIT EIGHT
case 0x0039: return 0x39; // DIGIT NINE
case 0x003A: return 0x3A; // COLON
case 0x003B: return 0x3B; // SEMICOLON
case 0x003C: return 0x3C; // LESS-THAN SIGN
case 0x003D: return 0x3D; // EQUALS SIGN
case 0x003E: return 0x3E; // GREATER-THAN SIGN
case 0x003F: return 0x3F; // QUESTION MARK
case 0x0040: return 0x40; // COMMERCIAL AT
case 0x0041: return 0x41; // LATIN CAPITAL LETTER A
case 0x0042: return 0x42; // LATIN CAPITAL LETTER B
case 0x0043: return 0x43; // LATIN CAPITAL LETTER C
case 0x0044: return 0x44; // LATIN CAPITAL LETTER D
case 0x0045: return 0x45; // LATIN CAPITAL LETTER E
case 0x0046: return 0x46; // LATIN CAPITAL LETTER F
case 0x0047: return 0x47; // LATIN CAPITAL LETTER G
case 0x0048: return 0x48; // LATIN CAPITAL LETTER H
case 0x0049: return 0x49; // LATIN CAPITAL LETTER I
case 0x004A: return 0x4A; // LATIN CAPITAL LETTER J
case 0x004B: return 0x4B; // LATIN CAPITAL LETTER K
case 0x004C: return 0x4C; // LATIN CAPITAL LETTER L
case 0x004D: return 0x4D; // LATIN CAPITAL LETTER M
case 0x004E: return 0x4E; // LATIN CAPITAL LETTER N
case 0x004F: return 0x4F; // LATIN CAPITAL LETTER O
case 0x0050: return 0x50; // LATIN CAPITAL LETTER P
case 0x0051: return 0x51; // LATIN CAPITAL LETTER Q
case 0x0052: return 0x52; // LATIN CAPITAL LETTER R
case 0x0053: return 0x53; // LATIN CAPITAL LETTER S
case 0x0054: return 0x54; // LATIN CAPITAL LETTER T
case 0x0055: return 0x55; // LATIN CAPITAL LETTER U
case 0x0056: return 0x56; // LATIN CAPITAL LETTER V
case 0x0057: return 0x57; // LATIN CAPITAL LETTER W
case 0x0058: return 0x58; // LATIN CAPITAL LETTER X
case 0x0059: return 0x59; // LATIN CAPITAL LETTER Y
case 0x005A: return 0x5A; // LATIN CAPITAL LETTER Z
case 0x005B: return 0x5B; // LEFT SQUARE BRACKET
case 0x005C: return 0x5C; // REVERSE SOLIDUS
case 0x005D: return 0x5D; // RIGHT SQUARE BRACKET
case 0x2015: return 0x5E; // HORIZONTAL BAR
case 0x005F: return 0x5F; // LOW LINE
case 0x2551: return 0x60; // BOX DRAWINGS DOUBLE VERTICAL
case 0x0061: return 0x61; // LATIN SMALL LETTER A
case 0x0062: return 0x62; // LATIN SMALL LETTER B
case 0x0063: return 0x63; // LATIN SMALL LETTER C
case 0x0064: return 0x64; // LATIN SMALL LETTER D
case 0x0065: return 0x65; // LATIN SMALL LETTER E
case 0x0066: return 0x66; // LATIN SMALL LETTER F
case 0x0067: return 0x67; // LATIN SMALL LETTER G
case 0x0068: return 0x68; // LATIN SMALL LETTER H
case 0x0069: return 0x69; // LATIN SMALL LETTER I
case 0x006A: return 0x6A; // LATIN SMALL LETTER J
case 0x006B: return 0x6B; // LATIN SMALL LETTER K
case 0x006C: return 0x6C; // LATIN SMALL LETTER L
case 0x006D: return 0x6D; // LATIN SMALL LETTER M
case 0x006E: return 0x6E; // LATIN SMALL LETTER N
case 0x006F: return 0x6F; // LATIN SMALL LETTER O
case 0x0070: return 0x70; // LATIN SMALL LETTER P
case 0x0071: return 0x71; // LATIN SMALL LETTER Q
case 0x0072: return 0x72; // LATIN SMALL LETTER R
case 0x0073: return 0x73; // LATIN SMALL LETTER S
case 0x0074: return 0x74; // LATIN SMALL LETTER T
case 0x0075: return 0x75; // LATIN SMALL LETTER U
case 0x0076: return 0x76; // LATIN SMALL LETTER V
case 0x0077: return 0x77; // LATIN SMALL LETTER W
case 0x0078: return 0x78; // LATIN SMALL LETTER X
case 0x0079: return 0x79; // LATIN SMALL LETTER Y
case 0x007A: return 0x7A; // LATIN SMALL LETTER Z
case 0x007B: return 0x7B; // LEFT CURLY BRACKET
case 0x007C: return 0x7C; // VERTICAL LINE
case 0x007D: return 0x7D; // RIGHT CURLY BRACKET
case 0x00AF: return 0x7E; // MACRON
case 0x00E1: return 0x80; // LATIN SMALL LETTER A WITH ACUTE
case 0x00E0: return 0x81; // LATIN SMALL LETTER A WITH GRAVE
case 0x00E9: return 0x82; // LATIN SMALL LETTER E WITH ACUTE
case 0x00E8: return 0x83; // LATIN SMALL LETTER E WITH GRAVE
case 0x00ED: return 0x84; // LATIN SMALL LETTER I WITH ACUTE
case 0x00EC: return 0x85; // LATIN SMALL LETTER I WITH GRAVE
case 0x00F3: return 0x86; // LATIN SMALL LETTER O WITH ACUTE
case 0x00F2: return 0x87; // LATIN SMALL LETTER O WITH GRAVE
case 0x00FA: return 0x88; // LATIN SMALL LETTER U WITH ACUTE
case 0x00F9: return 0x89; // LATIN SMALL LETTER U WITH GRAVE
case 0x00D1: return 0x8A; // LATIN CAPITAL LETTER N WITH TILDE
case 0x00C7: return 0x8B; // LATIN CAPITAL LETTER C WITH CEDILLA
case 0x015E: return 0x8C; // LATIN CAPITAL LETTER S WITH CEDILLA
case 0x00DF: return 0x8D; // LATIN SMALL LETTER SHARP S (German)
case 0x00A1: return 0x8E; // INVERTED EXCLAMATION MARK
case 0x0132: return 0x8F; // LATIN CAPITAL LIGATURE IJ
case 0x00E2: return 0x90; // LATIN SMALL LETTER A WITH CIRCUMFLEX
case 0x00E4: return 0x91; // LATIN SMALL LETTER A WITH DIAERESIS
case 0x00EA: return 0x92; // LATIN SMALL LETTER E WITH CIRCUMFLEX
case 0x00EB: return 0x93; // LATIN SMALL LETTER E WITH DIAERESIS
case 0x00EE: return 0x94; // LATIN SMALL LETTER I WITH CIRCUMFLEX
case 0x00EF: return 0x95; // LATIN SMALL LETTER I WITH DIAERESIS
case 0x00F4: return 0x96; // LATIN SMALL LETTER O WITH CIRCUMFLEX
case 0x00F6: return 0x97; // LATIN SMALL LETTER O WITH DIAERESIS
case 0x00FB: return 0x98; // LATIN SMALL LETTER U WITH CIRCUMFLEX
case 0x00FC: return 0x99; // LATIN SMALL LETTER U WITH DIAERESIS
case 0x00F1: return 0x9A; // LATIN SMALL LETTER N WITH TILDE
case 0x00E7: return 0x9B; // LATIN SMALL LETTER C WITH CEDILLA
case 0x015F: return 0x9C; // LATIN SMALL LETTER S WITH CEDILLA
case 0x011F: return 0x9D; // LATIN SMALL LETTER G WITH BREVE
case 0x0131: return 0x9E; // LATIN SMALL LETTER DOTLESS I
case 0x0133: return 0x9F; // LATIN SMALL LIGATURE IJ
case 0x00AA: return 0xA0; // FEMININE ORDINAL INDICATOR
case 0x03B1: return 0xA1; // GREEK SMALL LETTER ALPHA
case 0x00A9: return 0xA2; // COPYRIGHT SIGN
case 0x2030: return 0xA3; // PER MILLE SIGN
case 0x011E: return 0xA4; // LATIN CAPITAL LETTER G WITH BREVE
case 0x011B: return 0xA5; // LATIN SMALL LETTER E WITH CARON
case 0x0148: return 0xA6; // LATIN SMALL LETTER N WITH CARON
case 0x0151: return 0xA7; // LATIN SMALL LETTER O WITH DOUBLE ACUTE
case 0x03C0: return 0xA8; // GREEK SMALL LETTER PI
case 0x20AC: return 0xA9; // EURO SIGN
case 0x00A3: return 0xAA; // POUND SIGN
case 0x0024: return 0xAB; // DOLLAR SIGN
case 0x2190: return 0xAC; // LEFTWARDS ARROW
case 0x2191: return 0xAD; // UPWARDS ARROW
case 0x2192: return 0xAE; // RIGHTWARDS ARROW
case 0x2193: return 0xAF; // DOWNWARDS ARROW
case 0x00BA: return 0xB0; // MASCULIN ORDINAL INDICATOR
case 0x00B9: return 0xB1; // SUPERSCRIPT ONE
case 0x00B2: return 0xB2; // SUPERSCRIPT TWO
case 0x00B3: return 0xB3; // SUPERSCRIPT THREE
case 0x00B1: return 0xB4; // PLUS-MINUS SIGN
case 0x0130: return 0xB5; // LATIN CAPITAL LETTER I WITH DOT ABOVE
case 0x0144: return 0xB6; // LATIN SMALL LETTER N WITH ACUTE
case 0x0171: return 0xB7; // LATIN SMALL LETTER U WITH DOUBLE ACUTE
case 0x00B5: return 0xB8; // MIKRO SIGN
case 0x00BF: return 0xB9; // INVERTED QUESTION MARK
case 0x00F7: return 0xBA; // DIVISION SIGN
case 0x00B0: return 0xBB; // DEGREE SIGN
case 0x00BC: return 0xBC; // VULGAR FRACTION ONE QUARTER
case 0x00BD: return 0xBD; // VULGAR FRACTION ONE HALF
case 0x00BE: return 0xBE; // VULGAR FRACTION THREE QUARTERS
case 0x00A7: return 0xBF; // SECTION SIGN
case 0x00C1: return 0xC0; // LATIN CAPITAL LETTER A WITH ACUTE
case 0x00C0: return 0xC1; // LATIN CAPITAL LETTER A WITH GRAVE
case 0x00C9: return 0xC2; // LATIN CAPITAL LETTER E WITH ACUTE
case 0x00C8: return 0xC3; // LATIN CAPITAL LETTER E WITH GRAVE
case 0x00CD: return 0xC4; // LATIN CAPITAL LETTER I WITH ACUTE
case 0x00CC: return 0xC5; // LATIN CAPITAL LETTER I WITH GRAVE
case 0x00D3: return 0xC6; // LATIN CAPITAL LETTER O WITH ACUTE
case 0x00D2: return 0xC7; // LATIN CAPITAL LETTER O WITH GRAVE
case 0x00DA: return 0xC8; // LATIN CAPITAL LETTER U WITH ACUTE
case 0x00D9: return 0xC9; // LATIN CAPITAL LETTER U WITH GRAVE
case 0x0158: return 0xCA; // LATIN CAPITAL LETTER R WITH CARON
case 0x010C: return 0xCB; // LATIN CAPITAL LETTER C WITH CARON
case 0x0160: return 0xCC; // LATIN CAPITAL LETTER S WITH CARON
case 0x017D: return 0xCD; // LATIN CAPITAL LETTER Z WITH CARON
case 0x00D0: return 0xCE; // LATIN CAPITAL LETTER ETH
case 0x013F: return 0xCF; // LATIN CAPITAL LETTER L WITH MIDDLE DOT
case 0x00C2: return 0xD0; // LATIN CAPITAL LETTER A WITH CIRCUMFLEX
case 0x00C4: return 0xD1; // LATIN CAPITAL LETTER A WITH DIAERRESIS
case 0x00CA: return 0xD2; // LATIN CAPITAL LETTER E WITH CIRCUMFLEX
case 0x00CB: return 0xD3; // LATIN CAPITAL LETTER A WITH DIAERESIS
case 0x00CE: return 0xD4; // LATIN CAPITAL LETTER I WITH CIRCUMFLEX
case 0x00CF: return 0xD5; // LATIN CAPITAL LETTER I WITH DIAERESIS
case 0x00D4: return 0xD6; // LATIN CAPITAL LETTER O WITH CIRCUMFLEX
case 0x00D6: return 0xD7; // LATIN CAPITAL LETTER O WITH DIAERRESIS
case 0x00DB: return 0xD8; // LATIN CAPITAL LETTER U WITH CIRCUMFLEX
case 0x00DC: return 0xD9; // LATIN CAPITAL LETTER U WITH DIAERESIS
case 0x0159: return 0xDA; // LATIN SMALL LETTER R WITH CARON
case 0x010D: return 0xDB; // LATIN SMALL LETTER C WITH CARON
case 0x0161: return 0xDC; // LATIN SMALL LETTER S WITH CARON
case 0x017E: return 0xDD; // LATIN SMALL LETTER Z WITH CARON
case 0x0111: return 0xDE; // LATIN SMALL LETTER D WITH STROKE
case 0x0140: return 0xDF; // LATIN SMALL LETTER L WITH MIDDLE DOT
case 0x00C3: return 0xE0; // LATIN CAPITAL LETTER A WITH TILDE
case 0x00C5: return 0xE1; // LATIN CAPITAL LETTER A WITH RING ABOVE
case 0x00C6: return 0xE2; // LATIN CAPITAL LETTER AE
case 0x0152: return 0xE3; // LATIN CAPITAL LIGATURE OE
case 0x0177: return 0xE4; // LATIN SMALL LETTER Y WITH CIRCUMFLEX
case 0x00DD: return 0xE5; // LATIN CAPITAL LETTER Y WITH ACUTE
case 0x00D5: return 0xE6; // LATIN CAPITAL LETTER O WITH TILDE
case 0x00D8: return 0xE7; // LATIN CAPITAL LETTER O WITH STROKE
case 0x00DE: return 0xE8; // LATIN CAPITAL LETTER THORN
case 0x014A: return 0xE9; // LATIN CAPITAL LETTER ENG
case 0x0154: return 0xEA; // LATIN CAPITAL LETTER R WITH ACUTE
case 0x0106: return 0xEB; // LATIN CAPITAL LETTER C WITH ACUTE
case 0x015A: return 0xEC; // LATIN CAPITAL LETTER S WITH ACUTE
case 0x0179: return 0xED; // LATIN CAPITAL LETTER Z WITH ACUTE
case 0x0166: return 0xEE; // LATIN CAPITAL LETTER T WITH STROKE
case 0x00F0: return 0xEF; // LATIN SMALL LETTER ETH
case 0x00E3: return 0xF0; // LATIN SMALL LETTER A WITH TILDE
case 0x00E5: return 0xF1; // LATIN SMALL LETTER A WITH RING
case 0x00E6: return 0xF2; // LATIN SMALL LETTER AE
case 0x0153: return 0xF3; // LATIN SMALL LIGATURE OE
case 0x0175: return 0xF4; // LATIN SMALL LETTER W WITH CIRCUMFLEX
case 0x00FD: return 0xF5; // LATIN SMALL LETTER Y WITH ACUTE
case 0x00F5: return 0xF6; // LATIN SMALL LETTER O WITH TILDE
case 0x00F8: return 0xF7; // LATIN SMALL LETTER O WITH STROKE
case 0x00FE: return 0xF8; // LATIN SMALL LETTER THORN
case 0x014B: return 0xF9; // LATIN SMALL LETTER ENG
case 0x0155: return 0xFA; // LATIN SMALL LETTER R WITH ACUTE
case 0x0107: return 0xFB; // LATIN SMALL LETTER C WITH ACUTE
case 0x015B: return 0xFC; // LATIN SMALL LETTER S WITH ACUTE
case 0x017A: return 0xFD; // LATIN SMALL LETTER Z WITH ACUTE
case 0x0167: return 0xFE; // LATIN SMALL LETTER T WITH STROKE
default: return 0x20; // Return a SPACE character for all other code points.
}
}
void fill_rds_string(char* rds_string, char* src_string, size_t rds_string_size) {
mbtowc(NULL, 0, 0); // Reset decoder.
// First try to copy the source string.
size_t remaining_src_size = strlen(src_string);
size_t remaining_rds_size = rds_string_size;
wchar_t codepoint;
while (remaining_src_size > 0 && remaining_rds_size > 0) {
int size = mbtowc(&codepoint, src_string, remaining_src_size);
if (size == 0) break; // End of source string.
if (size < 0) { // Decode error. Try to skip 1 byte and resync.
src_string++;
remaining_src_size--;
continue;
}
*rds_string = codepoint_to_rds_char(codepoint);
rds_string++;
remaining_rds_size--;
src_string += size;
remaining_src_size -= size;
}
// Pad the RDS string with SPACE characters.
while (remaining_rds_size > 0) {
*rds_string = 0x20;
rds_string++;
remaining_rds_size--;
}
}

30
src/rds_strings.h Normal file
View File

@ -0,0 +1,30 @@
/*
PiFmRds - FM/RDS transmitter for the Raspberry Pi
Copyright (C) 2024 Christophe Jacquet, F8FTK
See https://github.com/ChristopheJacquet/PiFmRds
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef RDS_H
#define RDS_H
#include <stdlib.h>
extern void fill_rds_string(char* rds_string, char* src_string, size_t rds_string_size);
#endif /* RDS_H */

104
src/rds_strings_test.c Normal file
View File

@ -0,0 +1,104 @@
/*
PiFmRds - FM/RDS transmitter for the Raspberry Pi
Copyright (C) 2024 Christophe Jacquet, F8FTK
See https://github.com/ChristopheJacquet/PiFmRds
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <locale.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "rds_strings.h"
void print_hex_bytes(char* s, size_t size) {
for (int i=0; i < size; i++) {
printf("%02X ", s[i]);
}
printf("\n");
}
void assert_string(char* test_name, char* actual, char* expected, size_t size) {
bool equal = true;
for (int i=0; i < size; i++) {
if (actual[i] != expected[i]) {
equal = false;
break;
}
}
printf("Test: %s -> %s\n", test_name, equal ? "PASS" : "FAIL");
if (equal) return;
printf("Actual: "); print_hex_bytes(actual, size);
printf("Expected: "); print_hex_bytes(expected, size);
}
void test_src_shorter() {
const size_t dst_size = 7;
char dst[] = {0, 1, 2, 3, 4, 5, 6, 7};
char dst_ref[] = {'A', 'B', 'C', 'D', ' ', ' ', ' ', 7};
fill_rds_string(dst, "ABCD", dst_size);
assert_string("Copy shorter string", dst, dst_ref, dst_size+1);
}
void test_src_longer() {
const size_t dst_size = 7;
char dst[] = {0, 1, 2, 3, 4, 5, 6, 7};
char dst_ref[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 7};
fill_rds_string(dst, "ABCDEFGHI", dst_size);
assert_string("Copy longer string", dst, dst_ref, dst_size+1);
}
void test_same_sizes() {
const size_t dst_size = 7;
char dst[] = {0, 1, 2, 3, 4, 5, 6, 7};
char dst_ref[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 7};
fill_rds_string(dst, "ABCDEFG", dst_size);
assert_string("Copy same-size string", dst, dst_ref, dst_size+1);
}
void test_non_ascii() {
size_t dst_size = 20;
char dst[dst_size];
char* dst_ref = "M\x97""beltr\x91""gerf\x99\x8d""e d\x82\x9b""u";
fill_rds_string(dst, "M\xc3\xb6""beltr\xc3\xa4""gerf\xc3\xbc\xc3\x9f""e "
"d\xc3\xa9\xc3\xa7""u", dst_size);
assert_string("Convert non-ASCII characters", dst, dst_ref, dst_size);
}
void test_skip_invalid() {
size_t dst_size = 6;
char dst[dst_size];
char dst_ref[] = {'A', 'B', 'C', ' ', ' ', ' '};
fill_rds_string(dst, "A\xc0""B\xc1\xc2""C\xc3\xc4", dst_size);
assert_string("Skip invalid bytes", dst, dst_ref, dst_size);
}
int main() {
const char* locale = "C.UTF-8";
const char* locale_set = setlocale(LC_ALL, locale);
if (locale_set == NULL || strcmp(locale, locale_set) != 0) {
printf("Failed to set locale. Locale is '%s', while it should be '%s'. "
"Tests will probably fail.\n", locale_set, locale);
}
test_src_shorter();
test_src_longer();
test_same_sizes();
test_non_ascii();
test_skip_invalid();
}