diff --git a/.gitignore b/.gitignore index 2a98046..4235326 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ src/waveform_*.wav src/pydemod src/test +src/pi_fm_rds +src/*_test *.o diff --git a/README.md b/README.md index e62f176..21976e6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/Makefile b/src/Makefile index 9f0002f..21a3cbc 100644 --- a/src/Makefile +++ b/src/Makefile @@ -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 diff --git a/src/control_pipe.c b/src/control_pipe.c index 19e8943..baf3214 100644 --- a/src/control_pipe.c +++ b/src/control_pipe.c @@ -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 . - + 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; } diff --git a/src/pi_fm_rds.c b/src/pi_fm_rds.c index 7423869..5bc8eff 100644 --- a/src/pi_fm_rds.c +++ b/src/pi_fm_rds.c @@ -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 December 2012 */ +#include #include #include #include @@ -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 #include #include + +#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) { diff --git a/src/rds_strings.c b/src/rds_strings.c new file mode 100644 index 0000000..48c6535 --- /dev/null +++ b/src/rds_strings.c @@ -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 . +*/ + +#include +#include +#include + +// 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--; + } +} diff --git a/src/rds_strings.h b/src/rds_strings.h new file mode 100644 index 0000000..60f9630 --- /dev/null +++ b/src/rds_strings.h @@ -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 . +*/ + +#ifndef RDS_H +#define RDS_H + + +#include + +extern void fill_rds_string(char* rds_string, char* src_string, size_t rds_string_size); + + +#endif /* RDS_H */ \ No newline at end of file diff --git a/src/rds_strings_test.c b/src/rds_strings_test.c new file mode 100644 index 0000000..b97a1c6 --- /dev/null +++ b/src/rds_strings_test.c @@ -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 . +*/ + +#include +#include +#include +#include + +#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(); +} \ No newline at end of file