0
1
mirror of https://github.com/radio95-rnt/rds95.git synced 2026-02-27 12:53:53 +01:00

first commit

This commit is contained in:
2025-03-10 16:50:29 +01:00
commit 800f3222ca
36 changed files with 5064 additions and 0 deletions

82
src/CMakeLists.txt Normal file
View File

@@ -0,0 +1,82 @@
# This is from kuba's version of MiniRDS
cmake_minimum_required(VERSION 3.10)
# Project name and version
project(minirds VERSION 1.0)
# Define options
option(ODA_RTP "Enable ODA (RT+)" ON)
option(RDS2 "Enable RDS2 capabilities" OFF)
option(RDS2_QUADRATURE_CARRIER "Shift RDS2 stream carriers by 90, 180, and 270 degrees" ON)
option(RDS2_SYMBOL_SHIFTING "Enable RDS2 symbol shifting" ON)
option(RDS2_DEBUG "Enable RDS2 debugging" OFF)
option(STATIC_LIBSAMPLERATE "Use a static libsamplerate library (.a)" OFF)
set(LIBSAMPLERATE_DIR "./libsamplerate" CACHE STRING "Directory for static libsamplerate")
# Set compiler and flags
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror -Wextra -pedantic -O2 -std=c18 -DVERSION=\"${PROJECT_VERSION}\"")
# Define sources
set(SOURCES
minirds.c
waveforms.c
rds.c
)
# Handle RDS2 options
if(RDS2)
add_definitions(-DRDS2)
set(SOURCES ${SOURCES} rds2.c)
if(RDS2_QUADRATURE_CARRIER)
add_definitions(-DRDS2_QUADRATURE_CARRIER)
endif()
if(RDS2_SYMBOL_SHIFTING)
add_definitions(-DRDS2_SYMBOL_SHIFTING)
endif()
endif()
if(RDS2_DEBUG)
add_definitions(-DRDS2_DEBUG)
endif()
if(ODA_RTP)
add_definitions(-DODA)
add_definitions(-DODA_RTP)
endif()
set(SOURCES
${SOURCES}
fm_mpx.c
control_pipe.c
osc.c
resampler.c
modulator.c
lib.c
ascii_cmd.c
)
# Define the executable
add_executable(minirds ${SOURCES})
# Handle libsamplerate linkage
if(STATIC_LIBSAMPLERATE)
target_include_directories(minirds PRIVATE ${LIBSAMPLERATE_DIR}/include)
target_link_libraries(minirds PRIVATE ${LIBSAMPLERATE_DIR}/lib/libsamplerate.a)
else()
find_library(SAMPLERATE_LIBRARY samplerate)
target_link_libraries(minirds PRIVATE ${SAMPLERATE_LIBRARY})
endif()
# Link additional libraries
target_link_libraries(minirds PRIVATE m pthread pulse pulse-simple)
# Install target
install(TARGETS minirds DESTINATION /usr/local/bin)
# Add profiling flags globally
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")

395
src/ascii_cmd.c Normal file
View File

@@ -0,0 +1,395 @@
/*
* MiniRDS - FM RDS Encoder
* contains code from:
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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 "common.h"
#include "rds.h"
#include "fm_mpx.h"
#include "lib.h"
#include "ascii_cmd.h"
#define CMD_MATCHES(a) (ustrcmp(cmd, (unsigned char *)a) == 0)
void process_ascii_cmd(unsigned char *str) {
unsigned char *cmd, *arg;
uint16_t cmd_len = 0;
cmd_len = _strnlen((const char*)str, CTL_BUFFER_SIZE);
if (cmd_len > 3 && str[2] == ' ') {
cmd = str;
cmd[2] = 0;
arg = str + 3;
if (CMD_MATCHES("CG")) {
/* this stays */
uint16_t blocks[4];
int count = sscanf((const char*)arg, "%hX %hX %hX %hX",
&blocks[0], &blocks[1],
&blocks[2], &blocks[3]);
if (count == 4) {
set_rds_cg(blocks);
}
return;
}
}
if (cmd_len > 4 && str[3] == ' ') {
cmd = str;
cmd[3] = 0;
arg = str + 4;
if (CMD_MATCHES("MPX")) {
#ifdef RDS2
float gains[5];
if (sscanf((char *)arg, "%f,%f,%f,%f,%f",
&gains[0], &gains[1], &gains[2], &gains[3],
&gains[4]) == 5) {
set_carrier_volume(0, gains[0]);
set_carrier_volume(1, gains[1]);
set_carrier_volume(2, gains[2]);
set_carrier_volume(3, gains[3]);
set_carrier_volume(4, gains[4]);
}
#else
float gains[2];
if (sscanf((char *)arg, "%f,%f",
&gains[0], &gains[1]) == 2)
{
set_carrier_volume(0, gains[0]);
set_carrier_volume(1, gains[1]);
}
#endif
return;
}
if (CMD_MATCHES("VOL")) {
arg[4] = 0;
set_output_volume(strtof((char *)arg, NULL));
return;
}
}
if(cmd_len == 5) {
cmd = str;
if(CMD_MATCHES("AFCH=")) {
clear_rds_af();
return;
}
}
if (cmd_len > 5 && str[4] == '=') {
/* compatibilty with existing (standarts)*/
cmd = str;
cmd[4] = 0;
arg = str + 5;
if (CMD_MATCHES("TEXT")) {
arg[RT_LENGTH * 2] = 0;
set_rds_rt1(xlat(arg));
return;
}
if (CMD_MATCHES("PTYN")) {
arg[PTYN_LENGTH] = 0;
set_rds_ptyn(xlat(arg));
return;
}
if (CMD_MATCHES("AFCH")) {
if(arg[0] == 'A' || arg[0] == 'B') {
return;
}
clear_rds_af();
uint8_t arg_count;
rds_af_t new_af;
uint8_t af[MAX_AFS], *af_iter;
arg_count = sscanf((char *)arg,
"%hhx,%hhx,%hhx,%hhx,%hhx," /* AF list */
"%hhx,%hhx,%hhx,%hhx,%hhx,"
"%hhx,%hhx,%hhx,%hhx,%hhx,"
"%hhx,%hhx,%hhx,%hhx,%hhx,"
"%hhx,%hhx,%hhx,%hhx,%hhx",
&af[0], &af[1], &af[2], &af[3], &af[4],
&af[5], &af[6], &af[7], &af[8], &af[9],
&af[10], &af[11], &af[12], &af[13], &af[14],
&af[15], &af[16], &af[17], &af[18], &af[19],
&af[20], &af[21], &af[22], &af[23], &af[24]);
af_iter = af;
memset(&new_af, 0, sizeof(struct rds_af_t));
while (arg_count-- != 0) {
uint8_t current_value = *af_iter;
float frequency = (875.0 + current_value) / 10.0;
add_rds_af(&new_af, frequency);
af_iter++;
}
set_rds_af(new_af);
return;
}
}
if(cmd_len == 4) {
cmd = str;
if(CMD_MATCHES("TPS=")) {
set_rds_tpson(0);
return;
}
if(CMD_MATCHES("LPS=")) {
set_rds_lpson(0);
return;
}
}
if (cmd_len > 4 && str[3] == '=') {
cmd = str;
cmd[3] = 0;
arg = str + 4;
if (CMD_MATCHES("TPS")) {
arg[PS_LENGTH * 2] = 0;
set_rds_tps(xlat(arg));
set_rds_tpson(1);
return;
}
if (CMD_MATCHES("RT1")) {
arg[RT_LENGTH * 2] = 0;
set_rds_rt1(xlat(arg));
return;
}
if (CMD_MATCHES("PTY")) {
arg[2] = 0;
set_rds_pty(strtoul((char *)arg, NULL, 10));
return;
}
if (CMD_MATCHES("ECC")) {
arg[2] = 0;
set_rds_ecc(strtoul((char *)arg, NULL, 16));
return;
}
if (CMD_MATCHES("LIC")) {
arg[2] = 0;
set_rds_lic(strtoul((char *)arg, NULL, 16));
return;
}
#ifdef ODA_RTP
if (CMD_MATCHES("RTP")) {
char tag_names[2][32];
uint8_t tags[6];
if (sscanf((char *)arg, "%hhu,%hhu,%hhu,%hhu,%hhu,%hhu",
&tags[0], &tags[1], &tags[2], &tags[3],
&tags[4], &tags[5]) == 6) {
set_rds_rtplus_tags(tags);
} else if (sscanf((char *)arg, "%31[^,],%hhu,%hhu,%31[^,],%hhu,%hhu",
tag_names[0], &tags[1], &tags[2],
tag_names[1], &tags[4], &tags[5]) == 6) {
tags[0] = get_rtp_tag_id(tag_names[0]);
tags[3] = get_rtp_tag_id(tag_names[1]);
set_rds_rtplus_tags(tags);
}
return;
}
#endif
if (CMD_MATCHES("LPS")) {
arg[LPS_LENGTH] = 0;
set_rds_lpson(1);
set_rds_lps(arg);
return;
}
if (CMD_MATCHES("PIN")) {
uint8_t pin[3];
if (sscanf((char *)arg, "%hhu,%hhu,%hhu",
&pin[0], &pin[1], &pin[2]) == 3) {
set_rds_pin(pin[0], pin[1], pin[2]);
}
return;
}
}
if(cmd_len == 3) {
cmd = str;
if(CMD_MATCHES("AF=")) {
clear_rds_af();
return;
}
}
if (cmd_len > 3 && str[2] == '=') {
cmd = str;
cmd[2] = 0;
arg = str + 3;
if (CMD_MATCHES("PS")) {
if(arg[0] == '\0') arg[0] = ' '; /* fix for strings that start with a space idk why but tps works fine with space started strings */
arg[PS_LENGTH * 2] = 0;
set_rds_ps(xlat(arg));
return;
}
if (CMD_MATCHES("CT")) {
set_rds_ct(arg[0]);
return;
}
if (CMD_MATCHES("DI")) {
arg[2] = 0;
set_rds_di(strtoul((char *)arg, NULL, 10));
return;
}
if (CMD_MATCHES("TP")) {
set_rds_tp(arg[0]);
return;
}
if (CMD_MATCHES("TA")) {
set_rds_ta(arg[0]);
return;
}
if (CMD_MATCHES("MS")) {
set_rds_ms(arg[0]);
return;
}
if (CMD_MATCHES("PI")) {
arg[4] = 0;
set_rds_pi(strtoul((char *)arg, NULL, 16));
return;
}
if (CMD_MATCHES("AF")) {
if(arg[0] == 'A' || arg[0] == 'B') {
return;
}
clear_rds_af();
uint8_t arg_count;
rds_af_t new_af;
float af[MAX_AFS], *af_iter;
arg_count = sscanf((char *)arg,
"%f,%f,%f,%f,%f," /* AF list */
"%f,%f,%f,%f,%f,"
"%f,%f,%f,%f,%f,"
"%f,%f,%f,%f,%f,"
"%f,%f,%f,%f,%f",
&af[0], &af[1], &af[2], &af[3], &af[4],
&af[5], &af[6], &af[7], &af[8], &af[9],
&af[10], &af[11], &af[12], &af[13], &af[14],
&af[15], &af[16], &af[17], &af[18], &af[19],
&af[20], &af[21], &af[22], &af[23], &af[24]);
af_iter = af;
memset(&new_af, 0, sizeof(struct rds_af_t));
while (arg_count-- != 0) {
add_rds_af(&new_af, *af_iter++);
}
set_rds_af(new_af);
return;
}
}
if (cmd_len > 2 && str[1] == '=') {
cmd = str;
cmd[1] = 0;
arg = str + 2;
if (CMD_MATCHES("G")) {
uint16_t blocks[4];
if(cmd_len == 18){
/* RDS2 Group*/
/* do a ifdef rds2 here when implementing*/
} else if(cmd_len == 14) {
/* RDS1 Group*/
blocks[0] = get_rds_pi();
int count = sscanf((char *)arg, "%4hx%4hx%4hx", &blocks[1], &blocks[2], &blocks[3]);
if(count == 3) {
set_rds_cg(blocks);
}
}
return;
}
}
if (cmd_len > 6 && str[5] == '=') {
cmd = str;
cmd[5] = 0;
arg = str + 6;
if (CMD_MATCHES("PINEN")) {
arg[1] = 0;
set_rds_pin_enabled(strtoul((char *)arg, NULL, 10));
return;
}
if (CMD_MATCHES("RT1EN")) {
set_rds_rt1_enabled(arg[0]);
return;
}
}
if (cmd_len > 6 && str[5] == '=') {
cmd = str;
cmd[5] = 0;
arg = str + 6;
if (CMD_MATCHES("LEVEL")) {
uint8_t val = strtoul((char *)arg, NULL, 10);
val /= 255;
val *= 15; /* max value*/
#ifdef RDS2
set_carrier_volume(1, val);
set_carrier_volume(2, val);
#else
set_carrier_volume(1, val);
#endif
return;
}
}
if (cmd_len > 7 && str[6] == '=') {
cmd = str;
cmd[6] = 0;
arg = str + 7;
if (CMD_MATCHES("RDSGEN")) {
arg[1] = 0;
set_rdsgen(strtoul((char *)arg, NULL, 10));
return;
}
if (CMD_MATCHES("PTYNEN")) {
arg[1] = 0;
set_rds_ptyn_enabled(strtoul((char *)arg, NULL, 10));
return;
}
#ifdef ODA_RTP
if (CMD_MATCHES("RTPRUN")) {
arg[1] = 0;
set_rds_rtplus_flags(strtoul((char *)arg, NULL, 10));
return;
}
#endif
}
if (cmd_len > 6 && str[5] == '=') {
cmd = str;
cmd[5] = 0;
arg = str + 6;
if (CMD_MATCHES("LEVEL")) {
uint8_t val = strtoul((char *)arg, NULL, 10);
val /= 255;
val *= 15; /* max value*/
#ifdef RDS2
set_carrier_volume(1, val);
set_carrier_volume(2, val);
#else
set_carrier_volume(1, val);
#endif
return;
}
if (CMD_MATCHES("ECCEN")) {
set_rds_ecclic_toggle(arg[0]);
return;
}
}
}

23
src/ascii_cmd.h Normal file
View File

@@ -0,0 +1,23 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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/>.
*/
#define CMD_BUFFER_SIZE 255
#define CTL_BUFFER_SIZE (CMD_BUFFER_SIZE * 2)
#define READ_TIMEOUT_MS 100
extern void process_ascii_cmd(unsigned char *cmd);

33
src/common.h Normal file
View File

@@ -0,0 +1,33 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019-2021 Anthony96922
*
* 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/>.
*/
/* common includes for mpxgen */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <math.h>
#include <stdbool.h>
/* workaround for missing pi definition */
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#define M_2PI (M_PI * 2.0)

84
src/control_pipe.c Normal file
View File

@@ -0,0 +1,84 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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 "common.h"
#include "ascii_cmd.h"
#include "control_pipe.h"
static int fd;
static struct pollfd poller;
/*
* Opens a file (pipe) to be used to control the RDS coder.
*/
int open_control_pipe(char *filename) {
fd = open(filename, O_RDONLY | O_NONBLOCK);
if (fd == -1) return -1;
/* setup the poller */
poller.fd = fd;
poller.events = POLLIN;
return 0;
}
/*
* Polls the control file (pipe), and if a command is received,
* calls process_ascii_cmd.
*/
void poll_control_pipe() {
static unsigned char pipe_buf[CTL_BUFFER_SIZE];
static unsigned char cmd_buf[CMD_BUFFER_SIZE];
struct timeval timeout;
int ret;
fd_set set;
char *token;
FD_ZERO(&set);
FD_SET(fd, &set);
timeout.tv_sec = 0;
timeout.tv_usec = READ_TIMEOUT_MS * 1000;
/* check for new commands */
if (poll(&poller, 1, READ_TIMEOUT_MS) <= 0) return;
/* return early if there are no new commands */
if (poller.revents == 0) return;
memset(pipe_buf, 0, CTL_BUFFER_SIZE);
ret = select(fd + 1, &set, NULL, NULL, &timeout);
if (ret == -1 || ret == 0) {
return;
} else {
read(fd, pipe_buf, CTL_BUFFER_SIZE - 1);
}
/* handle commands per line this is really good because if were sending text commands very quick after eachother then we can get a rt of for example 'Now its 12:00RT Now its 12:01' */
token = strtok((char *)pipe_buf, "\n");
while (token != NULL) {
memset(cmd_buf, 0, CMD_BUFFER_SIZE);
memcpy(cmd_buf, token, CMD_BUFFER_SIZE - 1);
token = strtok(NULL, "\n");
process_ascii_cmd(cmd_buf);
}
}
void close_control_pipe() {
if (fd > 0) close(fd);
}

25
src/control_pipe.h Normal file
View File

@@ -0,0 +1,25 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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 <fcntl.h>
#include <poll.h>
#include <sys/select.h>
extern int open_control_pipe(char *filename);
extern void close_control_pipe();
extern void poll_control_pipe();

178
src/fm_mpx.c Normal file
View File

@@ -0,0 +1,178 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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 "common.h"
#include "rds.h"
#ifdef RDS2
#include "rds2.h"
#endif
#include "fm_mpx.h"
#include "osc.h"
/*
* Local oscillator objects
* this is where the MPX waveforms are stored
*
*/
static struct osc_t osc_57k;
#ifdef RDS2
static struct osc_t osc_67k;
static struct osc_t osc_71k;
static struct osc_t osc_76k;
#endif
static float mpx_vol;
static uint8_t rdsgen;
void set_output_volume(float vol) {
if (vol > 100.0f) vol = 100.0f;
mpx_vol = vol / 100.0f;
}
void set_rdsgen(uint8_t gen) {
#ifdef RDS2
if (gen > 2) gen = 2;
#else
if (gen > 1) gen = 1;
#endif
rdsgen = gen;
}
/* subcarrier volumes */
static float volumes[MPX_SUBCARRIER_END] = {
0.0f, /* pilot tone: 0% */
1.0f, /* RDS: 7.5% modulation */
#ifdef RDS2
/* RDS2 */
0.05f, //66.5 khz 5%
0.05f, //71.25 khz 5%
0.0f //76 khz 0%
#endif
};
void set_carrier_volume(uint8_t carrier, float new_volume) {
/* check for valid index */
if (carrier >= MPX_SUBCARRIER_END) return;
/* don't allow levels over 15% */
if (new_volume >= 15.0f) new_volume = 15.0f;
volumes[carrier] = new_volume / 100.0f;
}
void fm_mpx_init(uint32_t sample_rate) {
/* initialize the subcarrier oscillators */
osc_init(&osc_57k, sample_rate, 57000.0f);
rdsgen = 1;
#ifdef RDS2
osc_init(&osc_67k, sample_rate, 66500.0f);
osc_init(&osc_71k, sample_rate, 71250.0f);
osc_init(&osc_76k, sample_rate, 76000.0f);
rdsgen = 2;
#endif
}
void fm_rds_get_frames(float *outbuf, size_t num_frames) {
size_t j = 0;
float out;
for (size_t i = 0; i < num_frames; i++) {
out = 0.0f;
#ifdef RDS2
out += osc_get_cos(&osc_57k)
* get_rds_sample(0, 0)
* volumes[MPX_SUBCARRIER_RDS_STREAM_0];
#else
out += get_rds_sample(0)
* volumes[MPX_SUBCARRIER_RDS_STREAM_0];
#endif
#ifdef RDS2
#ifdef RDS2_QUADRATURE_CARRIER
/* RDS2 is quadrature phase */
/* 90 degree shift */
if(rdsgen == 2 && volumes[MPX_SUBCARRIER_RDS2_STREAM_1] != 0) {
out += osc_get_sin(&osc_67k)
* get_rds_sample(1, 0)
* volumes[MPX_SUBCARRIER_RDS2_STREAM_1];
}
/* 180 degree shift */
if(rdsgen == 2 && volumes[MPX_SUBCARRIER_RDS2_STREAM_2] != 0) {
out += -osc_get_cos(&osc_71k)
* get_rds_sample(2, 0)
* volumes[MPX_SUBCARRIER_RDS2_STREAM_2];
}
/* 270 degree shift */
if(rdsgen == 2 && volumes[MPX_SUBCARRIER_RDS2_STREAM_3] != 0) {
out += -osc_get_sin(&osc_76k)
* get_rds_sample(3, 0)
* volumes[MPX_SUBCARRIER_RDS2_STREAM_3];
}
#else
if(rdsgen == 2 && volumes[MPX_SUBCARRIER_RDS2_STREAM_1] != 0) {
out += osc_get_cos(&osc_67k)
* get_rds_sample(1, 0)
* volumes[MPX_SUBCARRIER_RDS2_STREAM_1];
}
if(rdsgen == 2 && volumes[MPX_SUBCARRIER_RDS2_STREAM_1] != 0) {
out += osc_get_cos(&osc_71k)
* get_rds_sample(2, 0)
* volumes[MPX_SUBCARRIER_RDS2_STREAM_2];
}
if(rdsgen == 2 && volumes[MPX_SUBCARRIER_RDS2_STREAM_3] != 0) {
out += osc_get_cos(&osc_76k)
* get_rds_sample(3, 0)
* volumes[MPX_SUBCARRIER_RDS2_STREAM_3];
}
#endif
#endif
/* update oscillator */
osc_update_pos(&osc_57k);
#ifdef RDS2
osc_update_pos(&osc_67k);
osc_update_pos(&osc_71k);
osc_update_pos(&osc_76k);
#endif
/* declipper */
out = fminf(+1.0f, out);
out = fmaxf(-1.0f, out);
/* adjust volume and put into channel */
if(rdsgen != 0) outbuf[j] = out * mpx_vol;
j++;
}
}
void fm_mpx_exit() {
osc_exit(&osc_57k);
#ifdef RDS2
osc_exit(&osc_67k);
osc_exit(&osc_71k);
osc_exit(&osc_76k);
#endif
}

45
src/fm_mpx.h Normal file
View File

@@ -0,0 +1,45 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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/>.
*/
/* MPX */
#define NUM_MPX_FRAMES_IN 512
#define NUM_MPX_FRAMES_OUT (NUM_MPX_FRAMES_IN * 2)
/*
* The sample rate at which the MPX generation runs at
*/
#define MPX_SAMPLE_RATE RDS_SAMPLE_RATE
#define OUTPUT_SAMPLE_RATE 192000
enum mpx_subcarriers {
MPX_SUBCARRIER_ST_PILOT,
MPX_SUBCARRIER_RDS_STREAM_0,
#ifdef RDS2
MPX_SUBCARRIER_RDS2_STREAM_1,
MPX_SUBCARRIER_RDS2_STREAM_2,
MPX_SUBCARRIER_RDS2_STREAM_3,
#endif
MPX_SUBCARRIER_END
};
extern void fm_mpx_init(uint32_t sample_rate);
extern void fm_rds_get_frames(float *outbuf, size_t num_frames);
extern void fm_mpx_exit();
extern void set_output_volume(float vol);
extern void set_rdsgen(uint8_t gen);
extern void set_carrier_volume(uint8_t carrier, float new_volume);

519
src/lib.c Normal file
View File

@@ -0,0 +1,519 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2021 Anthony96922
*
* 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 "common.h"
#include "rds.h"
#include <time.h>
/*
* Stuff common for both RDS and RDS2
*
*/
/* needed workaround implicit declaration (edit: do we need this? ) */
extern int nanosleep(const struct timespec *req, struct timespec *rem);
/* millisecond sleep */
void msleep(unsigned long ms) {
struct timespec ts;
ts.tv_sec = ms / 1000ul; /* whole seconds */
ts.tv_nsec = (ms % 1000ul) * 1000; /* remainder, in nanoseconds */
nanosleep(&ts, NULL);
}
/* just like strlen */
size_t _strnlen(const char *s, size_t maxlen) {
size_t len = 0;
while (s[len] != 0 && len < maxlen)
len++;
return len;
}
/* unsigned equivalent of strcmp */
int ustrcmp(const unsigned char *s1, const unsigned char *s2) {
unsigned char c1, c2;
do {
c1 = *s1++;
c2 = *s2++;
if (c1 == '\0')
return c1 - c2;
} while (c1 == c2);
return c1 - c2;
}
#ifdef ODA_RTP
static char *rtp_content_types[64] = {
/* dummy */
"DUMMY_CLASS",
/* item */
"ITEM.TITLE",
"ITEM.ALBUM",
"ITEM.TRACKNUMBER",
"ITEM.ARTIST",
"ITEM.COMPOSITION",
"ITEM.MOVEMENT",
"ITEM.CONDUCTOR",
"ITEM.COMPOSER",
"ITEM.BAND",
"ITEM.COMMENT",
"ITEM.GENRE",
/* info */
"INFO.NEWS",
"INFO.NEWS.LOCAL",
"INFO.STOCKMARKET",
"INFO.SPORT",
"INFO.LOTTERY",
"INFO.HOROSCOPE",
"INFO.DAILY_DIVERSION",
"INFO.HEALTH",
"INFO.EVENT",
"INFO.SCENE",
"INFO.CINEMA",
"INFO.TV",
"INFO.DATE_TIME",
"INFO.WEATHER",
"INFO.TRAFFIC",
"INFO.ALARM",
"INFO.ADVERTISEMENT",
"INFO.URL",
"INFO.OTHER",
/* program */
"STATIONNAME.SHORT",
"STATIONNAME.LONG",
"PROGRAMME.NOW",
"PROGRAMME.NEXT",
"PROGRAMME.PART",
"PROGRAMME.HOST",
"PROGRAMME.EDITORIAL_STAFF",
"PROGRAMME.FREQUENCY",
"PROGRAMME.HOMEPAGE",
"PROGRAMME.SUBCHANNEL",
/* interactivity */
"PHONE.HOTLINE",
"PHONE.STUDIO",
"PHONE.OTHER",
"SMS.STUDIO",
"SMS.OTHER",
"EMAIL.HOTLINE",
"EMAIL.STUDIO",
"EMAIL.OTHER",
"MMS.OTHER",
"CHAT",
"CHAT.CENTRE",
"VOTE.QUESTION",
"VOTE.CENTRE",
/* rfu */
"RFU_1",
"RFU_2",
/* private classes */
"PRIVATE_1",
"PRIVATE_2",
"PRIVATE_3",
/* descriptor */
"PLACE",
"APPOINTMENT",
"IDENTIFIER",
"PURCHASE",
"GET_DATA"
};
uint8_t get_rtp_tag_id(char *rtp_tag_name) {
uint8_t tag_id = 0;
for (uint8_t i = 0; i < 64; i++) {
if (strcmp(rtp_tag_name, rtp_content_types[i]) == 0) {
tag_id = i;
break;
}
}
return tag_id;
}
char *get_rtp_tag_name(uint8_t rtp_tag) {
if (rtp_tag > 63) rtp_tag = 0;
return rtp_content_types[rtp_tag];
}
#endif
static uint16_t offset_words[] = {
0x0FC, /* A */
0x198, /* B */
0x168, /* C */
0x1B4, /* D */
0x350 /* C' */
};
/* CRC-16 ITU-T/CCITT checkword calculation */
/* wanna ask where is this used */
uint16_t crc16(uint8_t *data, size_t len) {
uint16_t crc = 0xffff;
for (size_t i = 0; i < len; i++) {
crc = (crc >> 8) | (crc << 8);
crc ^= data[i];
crc ^= (crc & 0xff) >> 4;
crc ^= (crc << 8) << 4;
crc ^= ((crc & 0xff) << 4) << 1;
}
return crc ^ 0xffff;
}
/* Calculate the checkword for each block and emit the bits */
#ifdef RDS2
void add_checkwords(uint16_t *blocks, uint8_t *bits, bool rds2)
#else
void add_checkwords(uint16_t *blocks, uint8_t *bits)
#endif
{
size_t i, j;
uint8_t bit, msb;
uint16_t block, block_crc, check, offset_word;
bool group_type_b = false;
#ifdef RDS2
bool tunneling_type_b = false;
#endif
/* if b11 is 1, then type B */
#ifdef RDS2
if (!rds2 && IS_TYPE_B(blocks))
#else
if (IS_TYPE_B(blocks))
#endif
group_type_b = true;
#ifdef RDS2
/* for when version B groups are coded in RDS2 */
if (rds2 && IS_TUNNELING(blocks) && IS_TYPE_B(blocks))
tunneling_type_b = true;
#endif
for (i = 0; i < GROUP_LENGTH; i++) {
#ifdef RDS2
/*
* If tunneling type B groups use offset
* word C' for block 3
*/
if (rds2 && i == 2 && tunneling_type_b) {
offset_word = offset_words[4];
} else
#endif
/* Group version B needs C' for block 3 */
if (i == 2 && group_type_b) {
offset_word = offset_words[4];
} else {
offset_word = offset_words[i];
}
block = blocks[i];
/* Classical CRC computation */
block_crc = 0;
for (j = 0; j < BLOCK_SIZE; j++) {
bit = (block & (INT16_15 >> j)) != 0;
msb = (block_crc >> (POLY_DEG - 1)) & 1;
block_crc <<= 1;
if (msb ^ bit) block_crc ^= POLY;
*bits++ = bit;
}
check = block_crc ^ offset_word;
for (j = 0; j < POLY_DEG; j++) {
*bits++ = (check & ((1 << (POLY_DEG - 1)) >> j)) != 0;
}
}
}
/*
* AF stuff
*/
uint8_t add_rds_af(struct rds_af_t *af_list, float freq) {
uint16_t af;
uint8_t entries_reqd = 1; /* default for FM */
/*
* check how many slots are required for the frequency
*
* LF/MF (AM) needs 2 entries: one for the
* AM follows code and the freq code itself
*/
if (freq < 87.6f || freq > 107.9f) { /* is LF/MF */
entries_reqd = 2;
}
/* check if the AF list is full */
if (af_list->num_afs + entries_reqd > MAX_AFS) {
/* Too many AF entries */
return 1;
}
/* check if new frequency is valid */
if (freq >= 87.6f && freq <= 107.9f) { /* FM */
af = (uint16_t)(freq * 10.0f) - 875;
af_list->afs[af_list->num_entries] = af;
af_list->num_entries += 1;
} else if (freq >= 153.0f && freq <= 279.0f) { /* LF */
af = (uint16_t)(freq - 153.0f) / 9 + 1;
af_list->afs[af_list->num_entries + 0] = AF_CODE_LFMF_FOLLOWS;
af_list->afs[af_list->num_entries + 1] = af;
af_list->num_entries += 2;
} else if (freq >= 531.0f && freq <= 1602.0f) { /* AM 9 kHz spacing */
af = (uint16_t)(freq - 531.0f) / 9 + 16;
af_list->afs[af_list->num_entries + 0] = AF_CODE_LFMF_FOLLOWS;
af_list->afs[af_list->num_entries + 1] = af;
af_list->num_entries += 2;
} else {
return 1;
}
af_list->num_afs++;
return 0;
}
char *show_af_list(struct rds_af_t af_list) {
float freq;
bool is_lfmf = false;
static char outstr[255];
uint8_t outstrlen = 0;
outstrlen += sprintf(outstr, "AF: %u,", af_list.num_afs);
for (uint8_t i = 0; i < af_list.num_entries; i++) {
if (af_list.afs[i] == AF_CODE_LFMF_FOLLOWS) {
/* The next AF will be for LF/MF */
is_lfmf = true;
continue;
}
if (is_lfmf) {
if (af_list.afs[i] >= 1 && af_list.afs[i] <= 15) { /* LF */
freq = 153.0f + ((float)(af_list.afs[i] - 1) * 9.0f);
outstrlen += sprintf(outstr + outstrlen, " (LF)%.0f", freq);
} else { /*if (af_list.afs[i] >= 16 && af_list.afs[i] <= 135) {*/ /* MF */
freq = 531.0f + ((float)(af_list.afs[i] - 16) * 9.0f);
outstrlen += sprintf(outstr + outstrlen, " (MF)%.0f", freq);
}
is_lfmf = false;
continue;
}
/* FM */
freq = (float)((uint16_t)af_list.afs[i] + 875) / 10.0f;
outstrlen += sprintf(outstr + outstrlen, " %.1f", freq);
}
return outstr;
}
/*
* UTF-8 to RDS char set converter
*
* Translates certain chars into their RDS equivalents
* NOTE!! Only applies to PS, RT and PTYN. LPS uses UTF-8 (SCB = 1)
*
*/
#define XLATSTRLEN 255
unsigned char *xlat(unsigned char *str) {
static unsigned char new_str[XLATSTRLEN];
uint8_t i = 0;
while (*str != 0 && i < XLATSTRLEN) {
switch (*str) {
case 0xc2:
str++;
switch (*str) {
case 0xa1: new_str[i] = 0x8e; break; /* INVERTED EXCLAMATION MARK */
case 0xa3: new_str[i] = 0xaa; break; /* POUND SIGN */
case 0xa7: new_str[i] = 0xbf; break; /* SECTION SIGN */
case 0xa9: new_str[i] = 0xa2; break; /* COPYRIGHT SIGN */
case 0xaa: new_str[i] = 0xa0; break; /* FEMININE ORDINAL INDICATOR */
case 0xb0: new_str[i] = 0xbb; break; /* DEGREE SIGN */
case 0xb1: new_str[i] = 0xb4; break; /* PLUS-MINUS SIGN */
case 0xb2: new_str[i] = 0xb2; break; /* SUPERSCRIPT TWO */
case 0xb3: new_str[i] = 0xb3; break; /* SUPERSCRIPT THREE */
case 0xb5: new_str[i] = 0xb8; break; /* MIKRO SIGN */
case 0xb9: new_str[i] = 0xb1; break; /* SUPERSCRIPT ONE */
case 0xba: new_str[i] = 0xb0; break; /* MASCULINE ORDINAL INDICATOR */
case 0xbc: new_str[i] = 0xbc; break; /* VULGAR FRACTION ONE QUARTER */
case 0xbd: new_str[i] = 0xbd; break; /* VULGAR FRACTION ONE HALF */
case 0xbe: new_str[i] = 0xbe; break; /* VULGAR FRACTION THREE QUARTERS */
case 0xbf: new_str[i] = 0xb9; break; /* INVERTED QUESTION MARK */
default: new_str[i] = ' '; break;
}
break;
case 0xc3:
str++;
switch (*str) {
case 0x80: new_str[i] = 0xc1; break; /* LATIN CAPITAL LETTER A WITH GRAVE */
case 0x81: new_str[i] = 0xc0; break; /* LATIN CAPITAL LETTER A WITH ACUTE */
case 0x82: new_str[i] = 0xd0; break; /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX */
case 0x83: new_str[i] = 0xe0; break; /* LATIN CAPITAL LETTER A WITH TILDE */
case 0x84: new_str[i] = 0xd1; break; /* LATIN CAPITAL LETTER A WITH DIAERESIS */
case 0x85: new_str[i] = 0xe1; break; /* LATIN CAPITAL LETTER A WITH RING ABOVE */
case 0x86: new_str[i] = 0xe2; break; /* LATIN CAPITAL LETTER AE */
case 0x87: new_str[i] = 0x8b; break; /* LATIN CAPITAL LETTER C WITH CEDILLA */
case 0x88: new_str[i] = 0xc3; break; /* LATIN CAPITAL LETTER E WITH GRAVE */
case 0x89: new_str[i] = 0xc2; break; /* LATIN CAPITAL LETTER E WITH ACUTE */
case 0x8a: new_str[i] = 0xd2; break; /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX */
case 0x8b: new_str[i] = 0xd3; break; /* LATIN CAPITAL LETTER E WITH DIAERESIS */
case 0x8c: new_str[i] = 0xc5; break; /* LATIN CAPITAL LETTER I WITH GRAVE */
case 0x8d: new_str[i] = 0xc4; break; /* LATIN CAPITAL LETTER I WITH ACUTE */
case 0x8e: new_str[i] = 0xd4; break; /* LATIN CAPITAL LETTER I WITH CIRCUMFLEX */
case 0x8f: new_str[i] = 0xd5; break; /* LATIN CAPITAL LETTER I WITH DIAERESIS */
case 0x90: new_str[i] = 0xce; break; /* LATIN CAPITAL LETTER ETH */
case 0x91: new_str[i] = 0x8a; break; /* LATIN CAPITAL LETTER N WITH TILDE */
case 0x92: new_str[i] = 0xc7; break; /* LATIN CAPITAL LETTER O WITH GRAVE */
case 0x93: new_str[i] = 0xc6; break; /* LATIN CAPITAL LETTER O WITH ACUTE */
case 0x94: new_str[i] = 0xd6; break; /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX */
case 0x95: new_str[i] = 0xe6; break; /* LATIN CAPITAL LETTER O WITH TILDE */
case 0x96: new_str[i] = 0xd7; break; /* LATIN CAPITAL LETTER O WITH DIAERESIS */
case 0x98: new_str[i] = 0xe7; break; /* LATIN CAPITAL LETTER O WITH STROKE */
case 0x99: new_str[i] = 0xc9; break; /* LATIN CAPITAL LETTER U WITH GRAVE */
case 0x9a: new_str[i] = 0xc8; break; /* LATIN CAPITAL LETTER U WITH ACUTE */
case 0x9b: new_str[i] = 0xd8; break; /* LATIN CAPITAL LETTER U WITH CIRCUMFLEX */
case 0x9c: new_str[i] = 0xd9; break; /* LATIN CAPITAL LETTER U WITH DIAERESIS */
case 0x9d: new_str[i] = 0xe5; break; /* LATIN CAPITAL LETTER Y WITH ACUTE */
case 0x9e: new_str[i] = 0xe8; break; /* LATIN CAPITAL LETTER THORN */
case 0xa0: new_str[i] = 0x81; break; /* LATIN SMALL LETTER A WITH GRAVE */
case 0xa1: new_str[i] = 0x80; break; /* LATIN SMALL LETTER A WITH ACUTE */
case 0xa2: new_str[i] = 0x90; break; /* LATIN SMALL LETTER A WITH CIRCUMFLEX */
case 0xa3: new_str[i] = 0xf0; break; /* LATIN SMALL LETTER A WITH TILDE */
case 0xa4: new_str[i] = 0x91; break; /* LATIN SMALL LETTER A WITH DIAERESIS */
case 0xa5: new_str[i] = 0xf1; break; /* LATIN SMALL LETTER A WITH RING ABOVE */
case 0xa6: new_str[i] = 0xf2; break; /* LATIN SMALL LETTER AE */
case 0xa7: new_str[i] = 0x9b; break; /* LATIN SMALL LETTER C WITH CEDILLA */
case 0xa8: new_str[i] = 0x83; break; /* LATIN SMALL LETTER E WITH GRAVE */
case 0xa9: new_str[i] = 0x82; break; /* LATIN SMALL LETTER E WITH ACUTE */
case 0xaa: new_str[i] = 0x92; break; /* LATIN SMALL LETTER E WITH CIRCUMFLEX */
case 0xab: new_str[i] = 0x93; break; /* LATIN SMALL LETTER E WITH DIAERESIS */
case 0xac: new_str[i] = 0x85; break; /* LATIN SMALL LETTER I WITH GRAVE */
case 0xad: new_str[i] = 0x84; break; /* LATIN SMALL LETTER I WITH ACUTE */
case 0xae: new_str[i] = 0x94; break; /* LATIN SMALL LETTER I WITH CIRCUMFLEX */
case 0xaf: new_str[i] = 0x95; break; /* LATIN SMALL LETTER I WITH DIAERESIS */
case 0xb0: new_str[i] = 0xef; break; /* LATIN SMALL LETTER ETH */
case 0xb1: new_str[i] = 0x9a; break; /* LATIN SMALL LETTER N WITH TILDE */
case 0xb2: new_str[i] = 0x87; break; /* LATIN SMALL LETTER O WITH GRAVE */
case 0xb3: new_str[i] = 0x86; break; /* LATIN SMALL LETTER O WITH ACUTE */
case 0xb4: new_str[i] = 0x96; break; /* LATIN SMALL LETTER O WITH CIRCUMFLEX */
case 0xb5: new_str[i] = 0xf6; break; /* LATIN SMALL LETTER O WITH TILDE */
case 0xb6: new_str[i] = 0x97; break; /* LATIN SMALL LETTER O WITH DIAERESIS */
case 0xb7: new_str[i] = 0xba; break; /* DIVISION SIGN */
case 0xb8: new_str[i] = 0xf7; break; /* LATIN SMALL LETTER O WITH STROKE */
case 0xb9: new_str[i] = 0x89; break; /* LATIN SMALL LETTER U WITH GRAVE */
case 0xba: new_str[i] = 0x88; break; /* LATIN SMALL LETTER U WITH ACUTE */
case 0xbb: new_str[i] = 0x98; break; /* LATIN SMALL LETTER U WITH CIRCUMFLEX */
case 0xbc: new_str[i] = 0x99; break; /* LATIN SMALL LETTER U WITH DIAERESIS */
case 0xbd: new_str[i] = 0xf5; break; /* LATIN SMALL LETTER Y WITH ACUTE */
case 0xbe: new_str[i] = 0xf8; break; /* LATIN SMALL LETTER THORN */
default: new_str[i] = ' '; break;
}
break;
case 0xc4:
str++;
switch (*str) {
case 0x87: new_str[i] = 0xfb; break; /* LATIN SMALL LETTER C WITH ACUTE */
case 0x8c: new_str[i] = 0xcb; break; /* LATIN CAPITAL LETTER C WITH CARON */
case 0x8d: new_str[i] = 0xdb; break; /* LATIN SMALL LETTER C WITH CARON */
case 0x91: new_str[i] = 0xde; break; /* LATIN SMALL LETTER D WITH STROKE */
case 0x9b: new_str[i] = 0xa5; break; /* LATIN SMALL LETTER E WITH CARON */
case 0xb0: new_str[i] = 0xb5; break; /* LATIN CAPITAL LETTER I WITH DOT ABOVE */
case 0xb1: new_str[i] = 0x9f; break; /* LATIN SMALL LETTER DOTLESS I */
case 0xb2: new_str[i] = 0x8f; break; /* LATIN CAPITAL LIGATURE IJ */
case 0xb3: new_str[i] = 0x9f; break; /* LATIN SMALL LIGATURE IJ */
case 0xbf: new_str[i] = 0xcf; break; /* LATIN CAPITAL LETTER L WITH MIDDLE DOT */
default: new_str[i] = ' '; break;
}
break;
case 0xc5:
str++;
switch (*str) {
case 0x80: new_str[i] = 0xdf; break; /* LATIN SMALL LETTER L WITH MIDDLE DOT */
case 0x84: new_str[i] = 0xb6; break; /* LATIN SMALL LETTER N WITH ACUTE */
case 0x88: new_str[i] = 0xa6; break; /* LATIN SMALL LETTER N WITH CARON */
case 0x8a: new_str[i] = 0xe9; break; /* LATIN CAPITAL LETTER ENG */
case 0x8b: new_str[i] = 0xf9; break; /* LATIN SMALL LETTER ENG */
case 0x91: new_str[i] = 0xa7; break; /* LATIN SMALL LETTER O WITH DOUBLE ACUTE */
case 0x92: new_str[i] = 0xe3; break; /* LATIN CAPITAL LIGATURE OE */
case 0x93: new_str[i] = 0xf3; break; /* LATIN SMALL LIGATURE OE */
case 0x94: new_str[i] = 0xea; break; /* LATIN CAPITAL LETTER R WITH ACUTE */
case 0x95: new_str[i] = 0xfa; break; /* LATIN SMALL LETTER R WITH ACUTE */
case 0x98: new_str[i] = 0xca; break; /* LATIN CAPITAL LETTER R WITH CARON */
case 0x99: new_str[i] = 0xda; break; /* LATIN SMALL LETTER R WITH CARON */
case 0x9a: new_str[i] = 0xec; break; /* LATIN CAPITAL LETTER S WITH ACUTE */
case 0x9b: new_str[i] = 0xfc; break; /* LATIN SMALL LETTER S WITH ACUTE */
case 0x9e: new_str[i] = 0x8c; break; /* LATIN CAPITAL LETTER S WITH CEDILLA */
case 0x9f: new_str[i] = 0x9c; break; /* LATIN SMALL LETTER S WITH CEDILLA */
case 0xa0: new_str[i] = 0xcc; break; /* LATIN CAPITAL LETTER S WITH CARON */
case 0xa1: new_str[i] = 0xdc; break; /* LATIN SMALL LETTER S WITH CARON */
case 0xa6: new_str[i] = 0xee; break; /* LATIN CAPITAL LETTER T WITH STROKE */
case 0xa7: new_str[i] = 0xfe; break; /* LATIN SMALL LETTER T WITH STROKE */
case 0xb1: new_str[i] = 0xb7; break; /* LATIN SMALL LETTER U WITH DOUBLE ACUTE */
case 0xb5: new_str[i] = 0xf4; break; /* LATIN SMALL LETTER W WITH CIRCUMFLEX */
case 0xb7: new_str[i] = 0xe4; break; /* LATIN SMALL LETTER Y WITH CIRCUMFLEX */
case 0xb9: new_str[i] = 0xed; break; /* LATIN CAPITAL LETTER Z WITH ACUTE */
case 0xba: new_str[i] = 0xfd; break; /* LATIN SMALL LETTER Z WITH ACUTE */
case 0xbd: new_str[i] = 0xcd; break; /* LATIN CAPITAL LETTER Z WITH CARON */
case 0xbe: new_str[i] = 0xdd; break; /* LATIN SMALL LETTER Z WITH CARON */
default: new_str[i] = ' '; break;
}
break;
case 0xc7:
str++;
switch (*str) {
case 0xa6: new_str[i] = 0xa4; break; /* LATIN CAPITAL LETTER G WITH CARON */
case 0xa7: new_str[i] = 0x9d; break; /* LATIN SMALL LETTER G WITH CARON */
default: new_str[i] = ' '; break;
}
break;
case 0xce:
str++;
switch (*str) {
case 0xb1: new_str[i] = 0xa1; break; /* GREEK SMALL LETTER ALPHA */
case 0xb2: new_str[i] = 0x8d; break; /* GREEK SMALL LETTER BETA */
default: new_str[i] = ' '; break;
}
break;
case 0xcf:
str++;
switch (*str) {
case 0x80: new_str[i] = 0xa8; break; /* GREEK SMALL LETTER PI */
default: new_str[i] = ' '; break;
}
break;
default: /* 0-127 or unknown */
switch (*str) {
case '$': new_str[i] = 0xab; break; /* DOLLAR SIGN */
default: new_str[i] = *str; break;
}
break;
}
i++;
str++;
}
new_str[i] = 0;
return new_str;
}

34
src/lib.h Normal file
View File

@@ -0,0 +1,34 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2021 Anthony96922
*
* 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/>.
*/
extern void msleep(unsigned long ms);
extern size_t _strnlen(const char *s, size_t maxlen);
extern int ustrcmp(const unsigned char *s1, const unsigned char *s2);
extern uint8_t get_rtp_tag_id(char *rtp_tag_name);
extern char *get_rtp_tag_name(uint8_t rtp_tag);
#ifdef RDS2
extern void add_checkwords(uint16_t *blocks, uint8_t *bits, bool rds2);
#else
extern void add_checkwords(uint16_t *blocks, uint8_t *bits);
#endif
extern uint8_t add_rds_af(struct rds_af_t *af_list, float freq);
extern char *show_af_list(struct rds_af_t af_list);
extern uint16_t crc16(uint8_t *data, size_t len);
extern unsigned char *xlat(unsigned char *str);

15
src/make-station-logo.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Create station logo data to build into the mpxgen executable for RDS2
if [ -z "$1" ]; then
echo "Usage: $0 <logo file>"
exit
fi
if [ -f "$1" ]; then
echo 'static const unsigned char station_logo[] = {' > rds2_image_data.c
xxd -i < "$1" >> rds2_image_data.c
echo '};' >> rds2_image_data.c
echo 'static const unsigned int station_logo_len = sizeof(station_logo);' >> rds2_image_data.c
fi

354
src/minirds.c Normal file
View File

@@ -0,0 +1,354 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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 "common.h"
#include <signal.h>
#include <getopt.h>
#include <pthread.h>
#include <pulse/simple.h>
#include "rds.h"
#include "fm_mpx.h"
#include "control_pipe.h"
#include "resampler.h"
#include "lib.h"
#include "ascii_cmd.h"
static uint8_t stop_rds;
static void stop() {
printf("Received an stopping signal\n");
stop_rds = 1;
}
static inline void float2char2channel(
float *inbuf, char *outbuf, size_t frames) {
uint16_t j = 0, k = 0;
for (uint16_t i = 0; i < frames; i++) {
int16_t sample = lroundf(inbuf[j++] * 16383.5f);
// Direct memory write is more efficient for byte conversion
outbuf[k++] = sample & 0xFF;
outbuf[k++] = (sample >> 8) & 0xFF;
}
}
/* threads */
static void *control_pipe_worker() {
while (!stop_rds) {
poll_control_pipe();
msleep(READ_TIMEOUT_MS);
}
close_control_pipe();
pthread_exit(NULL);
}
static void show_help(char *name) {
printf(
"This is MiniRDS, a lightweight RDS encoder.\n"
"Version %f\n"
"\n"
"Usage: %s [options]\n"
"\n"
" -i,--pi Program Identification code\n"
" [default: 3073]\n"
" -s,--ps Program Service name\n"
" [default: \"radio95\"]\n"
" -r,--rt1 Radio Text 1\n"
" [default: (nothing)]\n"
" -p,--pty Program Type\n"
" [default: 0]\n"
" -T,--tp Traffic Program\n"
" [default: 0]\n"
" -A,--af Alternative Frequency (FM/LF/MF)\n"
" (more than one AF may be passed)\n"
" -P,--ptyn Program Type Name\n"
" -l,--lps Long PS\n"
" -e,--ecc ECC code\n"
" -d,--di DI code\n"
" -C,--ctl FIFO control pipe\n"
#ifdef RDS2
" -I,--img RDS2 Logo path\n"
#endif
" -h,--help Show this help text and exit\n"
" -v,--version Show version and exit\n"
"\n",
VERSION,
name
);
}
static void show_version() {
printf("MiniRDS version (radio95 Edit) %f\n", VERSION);
}
int main(int argc, char **argv) {
int opt;
char control_pipe[51];
struct rds_params_t rds_params = {
.ps = "radio95",
.rt1 = "",
.pi = 0x305F,
.ecc = 0xE2,
.lps = "radio95 - Radio Nowotomyskie"
};
float volume = 100.0f;
/* buffers */
float *mpx_buffer;
float *out_buffer;
char *dev_out;
int8_t r;
size_t frames;
/* SRC */
SRC_STATE *src_state;
SRC_DATA src_data;
/* PASIMPLE */
pa_simple *device;
pa_sample_spec format;
/* pthread */
pthread_attr_t attr;
pthread_t control_pipe_thread;
pthread_mutex_t control_pipe_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t control_pipe_cond;
const char *short_opt = "R:i:s:r:p:T:A:P:l:e:L:d:C:"
#ifdef RDS2
"I:"
#endif
"hv";
struct option long_opt[] =
{
{"rds", required_argument, NULL, 'R'},
{"pi", required_argument, NULL, 'i'},
{"ps", required_argument, NULL, 's'},
{"rt1", required_argument, NULL, 'r'},
{"pty", required_argument, NULL, 'p'},
{"tp", required_argument, NULL, 'T'},
{"af", required_argument, NULL, 'A'},
{"ptyn", required_argument, NULL, 'P'},
{"lps", required_argument, NULL, 'l'},
{"ecc", required_argument, NULL, 'e'},
{"lic", required_argument, NULL, 'L'},
{"di", required_argument, NULL, 'd'},
{"ctl", required_argument, NULL, 'C'},
#ifdef RDS2
{"img", required_argument, NULL, 'I'},
#endif
{"help", no_argument, NULL, 'h'},
{"version", no_argument, NULL, 'v'},
{ 0, 0, 0, 0 }
};
memset(control_pipe, 0, 51);
keep_parsing_opts:
opt = getopt_long(argc, argv, short_opt, long_opt, NULL);
if (opt == -1) goto done_parsing_opts;
switch (opt) {
case 'i': /* pi */
rds_params.pi = strtoul(optarg, NULL, 16);
break;
case 's': /* ps */
memcpy(rds_params.ps, xlat((unsigned char *)optarg), PS_LENGTH);
break;
case 'r': /* rt1 */
memcpy(rds_params.rt1, xlat((unsigned char *)optarg), RT_LENGTH);
break;
case 'p': /* pty */
rds_params.pty = strtoul(optarg, NULL, 10);
break;
case 'T': /* tp */
rds_params.tp = strtoul(optarg, NULL, 10);
break;
case 'A': /* af */
if (add_rds_af(&rds_params.af, strtof(optarg, NULL)) == 1) return 1;
break;
case 'P': /* ptyn */
memcpy(rds_params.ptyn, xlat((unsigned char *)optarg), PTYN_LENGTH);
break;
case 'l': /* lps */
memcpy(rds_params.lps, (unsigned char *)optarg, LPS_LENGTH);
break;
case 'e': /* ecc */
rds_params.ecc = strtoul(optarg, NULL, 16);
break;
case 'L': /* lic */
rds_params.lic = strtoul(optarg, NULL, 16);
break;
case 'C': /* ctl */
memcpy(control_pipe, optarg, 50);
break;
#ifdef RDS2
case 'I': /* img */
memcpy(rds_params.rds2_image_path, optarg, 50);
break;
#endif
case 'v': /* version */
show_version();
return 0;
case 'h': /* help */
case '?':
default:
show_help(argv[0]);
return 1;
}
goto keep_parsing_opts;
done_parsing_opts:
/* Initialize pthread stuff */
pthread_mutex_init(&control_pipe_mutex, NULL);
pthread_cond_init(&control_pipe_cond, NULL);
pthread_attr_init(&attr);
/* Setup buffers */
mpx_buffer = malloc(NUM_MPX_FRAMES_IN * 2 * sizeof(float));
out_buffer = malloc(NUM_MPX_FRAMES_OUT * 2 * sizeof(float));
dev_out = malloc(NUM_MPX_FRAMES_OUT * 2 * sizeof(int16_t) * sizeof(char));
/* Gracefully stop the encoder on SIGINT or SIGTERM */
signal(SIGINT, stop);
signal(SIGTERM, stop);
/* Initialize the baseband generator */
fm_mpx_init(MPX_SAMPLE_RATE);
set_output_volume(volume);
/* Initialize the RDS modulator */
init_rds_encoder(rds_params);
/* PASIMPLE format */
format.format = PA_SAMPLE_S16LE;
format.channels = 1;
format.rate = OUTPUT_SAMPLE_RATE;
device = pa_simple_new(
NULL, // Default PulseAudio server
"MiniRDS", // Application name
PA_STREAM_PLAYBACK, // Direction (playback)
"RDS", // Default device
"RDS Generator", // Stream description
&format, // Sample format
NULL, // Default channel map
NULL, // Default buffering attributes
NULL // Error variable
);
if (device == NULL) {
fprintf(stderr, "Error: cannot open sound device.\n");
goto exit;
}
/* SRC out (MPX -> output) */
memset(&src_data, 0, sizeof(SRC_DATA));
src_data.input_frames = NUM_MPX_FRAMES_IN;
src_data.output_frames = NUM_MPX_FRAMES_OUT;
src_data.src_ratio =
(double)OUTPUT_SAMPLE_RATE / (double)MPX_SAMPLE_RATE;
src_data.data_in = mpx_buffer;
src_data.data_out = out_buffer;
r = resampler_init(&src_state, 1);
if (r < 0) {
fprintf(stderr, "Could not create output resampler.\n");
goto exit;
}
/* Initialize the control pipe reader */
if (control_pipe[0]) {
if (open_control_pipe(control_pipe) == 0) {
fprintf(stderr, "Reading control commands on %s.\n", control_pipe);
/* Create control pipe polling worker */
r = pthread_create(&control_pipe_thread, &attr, control_pipe_worker, NULL);
if (r < 0) {
fprintf(stderr, "Could not create control pipe thread.\n");
control_pipe[0] = 0;
goto exit;
} else {
fprintf(stderr, "Created control pipe thread.\n");
}
} else {
fprintf(stderr, "Failed to open control pipe: %s.\n", control_pipe);
control_pipe[0] = 0;
}
}
for (;;) {
fm_rds_get_frames(mpx_buffer, NUM_MPX_FRAMES_IN);
if (resample(src_state, src_data, &frames) < 0) break;
float2char2channel(out_buffer, dev_out, frames);
/* num_bytes = audio frames( * channels) * bytes per sample */
if (pa_simple_write(device, dev_out, frames * sizeof(int16_t), NULL) != 0) {
fprintf(stderr, "Error: could not play audio.\n");
break;
}
if (stop_rds) {
fprintf(stderr, "Stopping the loop...\n");
break;
}
}
resampler_exit(src_state);
exit:
if (control_pipe[0]) {
/* shut down threads */
fprintf(stderr, "Waiting for pipe thread to shut down.\n");
pthread_cond_signal(&control_pipe_cond);
pthread_join(control_pipe_thread, NULL);
}
pthread_attr_destroy(&attr);
pa_simple_free(device);
fm_mpx_exit();
exit_rds_encoder();
free(mpx_buffer);
free(out_buffer);
free(dev_out);
return 0;
}

202
src/modulator.c Normal file
View File

@@ -0,0 +1,202 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2021 Anthony96922
*
* 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 "common.h"
#include "rds.h"
#ifdef RDS2
#include "rds2.h"
#endif
#include "fm_mpx.h"
#include "waveforms.h"
#include "modulator.h"
static struct rds_t **rds_ctx;
static float **waveform;
/*
* Create the RDS objects
*
*/
void init_rds_objects() {
rds_ctx = malloc(NUM_STREAMS * sizeof(struct rds_t));
for (uint8_t i = 0; i < NUM_STREAMS; i++) {
rds_ctx[i] = malloc(sizeof(struct rds_t));
rds_ctx[i]->bit_buffer = malloc(BITS_PER_GROUP);
rds_ctx[i]->sample_buffer =
malloc(SAMPLE_BUFFER_SIZE * sizeof(float));
#ifdef RDS2_SYMBOL_SHIFTING
/*
* symbol shifting to reduce total power of aggregate carriers
*
* see:
* https://ietresearch.onlinelibrary.wiley.com/doi/pdf/10.1049/el.2019.0292
* for more information
*/
switch (i) {
case 1:
/* time offset of 1/2 */
rds_ctx[i]->symbol_shift = SAMPLES_PER_BIT / 2;
rds_ctx[i]->symbol_shift_buf = malloc(
rds_ctx[i]->symbol_shift * sizeof(float));
memset(rds_ctx[i]->symbol_shift_buf, 0,
rds_ctx[i]->symbol_shift * sizeof(float));
break;
case 2:
/* time offset of 1/4 */
rds_ctx[i]->symbol_shift = SAMPLES_PER_BIT / 4;
rds_ctx[i]->symbol_shift_buf = malloc(
rds_ctx[i]->symbol_shift * sizeof(float));
memset(rds_ctx[i]->symbol_shift_buf, 0,
rds_ctx[i]->symbol_shift * sizeof(float));
break;
case 3:
/* time offset of 3/4 */
rds_ctx[i]->symbol_shift = (SAMPLES_PER_BIT / 4) * 3;
rds_ctx[i]->symbol_shift_buf = malloc(
rds_ctx[i]->symbol_shift * sizeof(float));
memset(rds_ctx[i]->symbol_shift_buf, 0,
rds_ctx[i]->symbol_shift * sizeof(float));
break;
default: /* stream 0 */
/* no time offset */
rds_ctx[i]->symbol_shift = 0;
break;
}
#endif
rds_ctx[i]->symbol_shift_buf_idx = 0;
}
waveform = malloc(2 * sizeof(float));
for (uint8_t i = 0; i < 2; i++) {
waveform[i] = malloc(FILTER_SIZE * sizeof(float));
for (uint16_t j = 0; j < FILTER_SIZE; j++) {
waveform[i][j] = i ?
+waveform_biphase[j] : -waveform_biphase[j];
}
}
}
void exit_rds_objects() {
for (uint8_t i = 0; i < NUM_STREAMS; i++) {
int has_symbol_shift = rds_ctx[i]->symbol_shift;
if (has_symbol_shift) {
free(rds_ctx[i]->symbol_shift_buf);
}
free(rds_ctx[i]->sample_buffer);
free(rds_ctx[i]->bit_buffer);
free(rds_ctx[i]);
}
free(rds_ctx);
for (uint8_t i = 0; i < 2; i++) {
free(waveform[i]);
}
free(waveform);
}
/* Get an RDS sample. This generates the envelope of the waveform using
* pre-generated elementary waveform samples.
*/
#ifdef RDS2
float get_rds_sample(uint8_t stream_num, uint8_t rds2tunneling) {
#else
float get_rds_sample(uint8_t stream_num) {
#endif
struct rds_t *rds;
uint16_t idx;
float *cur_waveform;
float sample;
/* select context */
rds = rds_ctx[stream_num];
if (rds->sample_count == SAMPLES_PER_BIT) {
if (rds->bit_pos == BITS_PER_GROUP) {
#ifdef RDS2
if(!rds2tunneling) {
if (stream_num > 0) {
get_rds2_bits(stream_num, rds->bit_buffer);
} else {
get_rds_bits(rds->bit_buffer);
}
} else {
get_rds_bits(rds->bit_buffer);
}
#else
get_rds_bits(rds->bit_buffer);
#endif
rds->bit_pos = 0;
}
/* do differential encoding */
rds->cur_bit = rds->bit_buffer[rds->bit_pos++];
rds->prev_output = rds->cur_output;
rds->cur_output = rds->prev_output ^ rds->cur_bit;
idx = rds->in_sample_index;
cur_waveform = waveform[rds->cur_output];
uint16_t buffer_remaining = SAMPLE_BUFFER_SIZE - idx;
if (buffer_remaining >= FILTER_SIZE) {
// Process continuous chunk
for (uint16_t i = 0; i < FILTER_SIZE; i++) {
rds->sample_buffer[idx + i] += cur_waveform[i];
}
idx += FILTER_SIZE;
} else {
// Process in two parts to handle wrapping
for (uint16_t i = 0; i < buffer_remaining; i++) {
rds->sample_buffer[idx + i] += cur_waveform[i];
}
for (uint16_t i = 0; i < FILTER_SIZE - buffer_remaining; i++) {
rds->sample_buffer[i] += cur_waveform[buffer_remaining + i];
}
idx = FILTER_SIZE - buffer_remaining;
}
rds->in_sample_index += SAMPLES_PER_BIT;
if (rds->in_sample_index == SAMPLE_BUFFER_SIZE)
rds->in_sample_index = 0;
rds->sample_count = 0;
}
rds->sample_count++;
if (rds->symbol_shift) {
rds->symbol_shift_buf[rds->symbol_shift_buf_idx++] =
rds->sample_buffer[rds->out_sample_index];
if (rds->symbol_shift_buf_idx == rds->symbol_shift)
rds->symbol_shift_buf_idx = 0;
sample = rds->symbol_shift_buf[rds->symbol_shift_buf_idx];
goto done;
}
sample = rds->sample_buffer[rds->out_sample_index];
done:
rds->sample_buffer[rds->out_sample_index++] = 0;
if (rds->out_sample_index == SAMPLE_BUFFER_SIZE)
rds->out_sample_index = 0;
return sample;
}

43
src/modulator.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2021 Anthony96922
*
* 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/>.
*/
#ifdef RDS2
#define NUM_STREAMS 4
#else
#define NUM_STREAMS 1
#endif
/* RDS signal context */
typedef struct rds_t {
uint8_t *bit_buffer; /* BITS_PER_GROUP */
uint8_t bit_pos;
float *sample_buffer; /* SAMPLE_BUFFER_SIZE */
uint8_t prev_output;
uint8_t cur_output;
uint8_t cur_bit;
uint8_t sample_count;
uint16_t in_sample_index;
uint16_t out_sample_index;
uint8_t symbol_shift;
float *symbol_shift_buf;
uint8_t symbol_shift_buf_idx;
} rds_t;
extern void init_rds_objects();
extern void exit_rds_objects();

108
src/osc.c Normal file
View File

@@ -0,0 +1,108 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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 "common.h"
#include "osc.h"
/*
* Code for MPX oscillator
*
* This uses lookup tables to speed up the waveform generation
*
*/
/*
* DDS function generator
*
* Create wave constants for a given frequency
*/
static void create_wave(uint32_t rate, float freq,
float *sin_wave, float *cos_wave, uint16_t *max_phase) {
double sin_sample, cos_sample;
/* used to determine if we have completed a cycle */
uint16_t periods = round((double)rate/freq);
const double w = M_2PI * freq / rate;
/* First value of a sine wave is always 0 */
*sin_wave++ = 0.0f;
*cos_wave++ = 1.0f;
for(uint16_t i = 1; i < periods; i++) {
sin_sample = sin(w * i);
cos_sample = cos(w * i);
*sin_wave++ = (float)sin_sample;
*cos_wave++ = (float)cos_sample;
}
*max_phase = periods;
}
/*
* Oscillator object initialization
*
*/
void osc_init(struct osc_t *osc, uint32_t sample_rate, float freq) {
/* sample rate for the objects */
osc->sample_rate = sample_rate;
/* waveform tables */
osc->sin_wave = malloc(osc->sample_rate * sizeof(float));
osc->cos_wave = malloc(osc->sample_rate * sizeof(float));
/* set current position to 0 */
osc->cur = 0;
/* create waveform data and load into lookup tables */
create_wave(osc->sample_rate, freq,
osc->sin_wave, osc->cos_wave,
&osc->max);
}
/*
* Get a single waveform sample
*
* Cosine is needed for SSB generation
*
*/
float osc_get_cos(struct osc_t *osc) {
return osc->cos_wave[osc->cur];
}
float osc_get_sin(struct osc_t *osc) {
return osc->sin_wave[osc->cur];
}
/*
* Shift the oscillator to the next position
*
*/
void osc_update_pos(struct osc_t *osc) {
if (++osc->cur == osc->max) osc->cur = 0;
}
/*
* Unload waveform tables
*
*/
void osc_exit(struct osc_t *osc) {
free(osc->sin_wave);
free(osc->cos_wave);
osc->cur = 0;
osc->max = 0;
}

50
src/osc.h Normal file
View File

@@ -0,0 +1,50 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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/>.
*/
/* context for MPX oscillator */
typedef struct osc_t {
/* the sample rate at which the oscillator operates */
uint32_t sample_rate;
/*
* frequency for this instance
*
*/
float freq;
/*
* Arrays of carrier wave constants
*
*/
float *sin_wave_out_of_phase;
float *sin_wave;
float *cos_wave;
/*
* Wave phase
*
*/
uint16_t cur;
uint16_t max;
} osc_t;
extern void osc_init(struct osc_t *osc, uint32_t sample_rate, const float freq);
extern float osc_get_sin(struct osc_t *osc);
extern float osc_get_cos(struct osc_t *osc);
extern void osc_update_pos(struct osc_t *osc);
extern void osc_exit(struct osc_t *osc);

778
src/rds.c Normal file
View File

@@ -0,0 +1,778 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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 "common.h"
#include "rds.h"
#include "modulator.h"
#ifdef RDS2
#include "rds2.h"
#endif
#include "lib.h"
#include <time.h>
static struct rds_params_t rds_data;
/* RDS data controls */
static struct {
/* Slow Labeling Codes */
uint8_t ecclic_enabled;
uint8_t ecc_or_lic;
/* Program Item Number */
uint8_t pin_enabled;
/* Program Service */
uint8_t ps_update;
uint8_t tps_update;
/* Radio Text*/
uint8_t rt1_enabled;
uint8_t rt_update;
uint8_t rt_ab;
uint8_t rt_segments;
/* PTYN */
uint8_t ptyn_enabled;
uint8_t ptyn_update;
uint8_t ptyn_ab;
/* Long PS */
uint8_t lps_on;
uint8_t lps_update;
uint8_t lps_segments;
uint8_t custom_group_in;
uint16_t custom_group[GROUP_LENGTH];
/* TODO: add UDG */
} rds_state;
#ifdef ODA
/* ODA */
static struct rds_oda_t odas[MAX_ODAS];
static struct {
uint8_t current;
uint8_t count;
} oda_state;
#endif
#ifdef ODA_RTP
/* RT+ */
static struct {
uint8_t group;
uint8_t enabled;
uint8_t running;
uint8_t toggle;
uint8_t type[2];
uint8_t start[2];
uint8_t len[2];
} rtplus_cfg;
#endif
#ifdef ODA
static void register_oda(uint8_t group, uint16_t aid, uint16_t scb) {
if (oda_state.count >= MAX_ODAS) return; /* can't accept more ODAs */
odas[oda_state.count].group = group;
odas[oda_state.count].aid = aid;
odas[oda_state.count].scb = scb;
oda_state.count++;
}
#endif
/* Get the next AF entry
*/
static uint16_t get_next_af() {
static uint8_t af_state;
uint16_t out;
if (rds_data.af.num_afs) {
if (af_state == 0) {
out = (AF_CODE_NUM_AFS_BASE + rds_data.af.num_afs) << 8;
out |= rds_data.af.afs[0];
af_state += 1;
} else {
out = rds_data.af.afs[af_state] << 8;
if (rds_data.af.afs[af_state + 1])
out |= rds_data.af.afs[af_state + 1];
else
out |= AF_CODE_FILLER;
af_state += 2;
}
if (af_state >= rds_data.af.num_entries) af_state = 0;
} else {
out = AF_CODE_NO_AF << 8 | AF_CODE_FILLER;
}
return out;
}
/* PS group (0A) */
static void get_rds_ps_group(uint16_t *blocks) {
static unsigned char ps_text[PS_LENGTH];
static unsigned char tps_text[PS_LENGTH];
static uint8_t ps_csegment;
if (ps_csegment == 0 && rds_state.ps_update) {
memcpy(ps_text, rds_data.ps, PS_LENGTH);
rds_state.ps_update = 0; /* rewind */
}
if(ps_csegment == 0 && rds_state.tps_update) {
memcpy(tps_text, rds_data.tps, PS_LENGTH);
rds_state.tps_update = 0; /* rewind */
}
/* TA */
blocks[1] |= rds_data.ta << 4;
/* MS */
blocks[1] |= rds_data.ms << 3;
/* DI */
blocks[1] |= ((rds_data.di >> (3 - ps_csegment)) & INT8_0) << 2; /* DI is a 4 bit number, we also have 4 segments, so we send a bit of the di number depending on the segment, so 0b0101 would be like: 0 1 0 1*/
/* PS segment address */
blocks[1] |= ps_csegment;
/* AF */
blocks[2] = get_next_af();
/* PS */
if(rds_data.ta && rds_data.traffic_ps_on) {
blocks[3] = tps_text[ps_csegment * 2] << 8 | tps_text[ps_csegment * 2 + 1];
} else {
/* TODO: Add DPS */
blocks[3] = ps_text[ps_csegment * 2] << 8 | ps_text[ps_csegment * 2 + 1];
}
ps_csegment++;
if (ps_csegment >= 4) ps_csegment = 0;
}
/* RT group (2A) */
static uint8_t get_rds_rt_group(uint16_t *blocks) {
static unsigned char rt_text[RT_LENGTH];
static uint8_t rt_state;
if (rds_state.rt_update) {
memcpy(rt_text, rds_data.rt1, RT_LENGTH);
rds_state.rt_ab ^= 1;
rds_state.rt_update = 0;
rt_state = 0; /* rewind when new RT arrives */
}
if(rt_text[0] == '\r' || !rds_state.rt1_enabled) {
/* RT strings lesser than 64 in size should have been ended with a \r if a return is on [0] then that means that our string is empty and thus we can not generate this group*/
return 0;
}
blocks[1] |= 2 << 12;
blocks[1] |= rds_state.rt_ab << 4;
blocks[1] |= rt_state;
blocks[2] = rt_text[rt_state * 4 ] << 8;
blocks[2] |= rt_text[rt_state * 4 + 1];
blocks[3] = rt_text[rt_state * 4 + 2] << 8;
blocks[3] |= rt_text[rt_state * 4 + 3];
rt_state++;
if (rt_state >= rds_state.rt_segments) rt_state = 0;
return 1;
}
/* ODA group (3A)
*/
#ifdef ODA
static void get_rds_oda_group(uint16_t *blocks) {
/* select ODA */
struct rds_oda_t this_oda = odas[oda_state.current];
#ifdef ODA_RTP
if(this_oda.aid == ODA_AID_RTPLUS && rtplus_cfg.enabled == 0) {
return;
}
#endif
blocks[1] |= 3 << 12;
blocks[1] |= GET_GROUP_TYPE(this_oda.group) << 1;
blocks[1] |= GET_GROUP_VER(this_oda.group);
blocks[2] = this_oda.scb;
blocks[3] = this_oda.aid;
oda_state.current++;
if (oda_state.current >= oda_state.count) oda_state.current = 0;
}
#endif
static uint8_t get_rds_ct_group(uint16_t *blocks) {
static uint8_t latest_minutes;
struct tm *utc, *local_time;
time_t now;
uint8_t l;
uint32_t mjd;
int16_t offset;
/* Check time */
now = time(NULL);
utc = gmtime(&now);
if (utc->tm_min != latest_minutes) {
/* Generate CT group */
latest_minutes = utc->tm_min;
l = utc->tm_mon <= 1 ? 1 : 0;
mjd = 14956 + utc->tm_mday +
(uint32_t)((utc->tm_year - l) * 365.25f) +
(uint32_t)((utc->tm_mon + (1+1) + l * 12) * 30.6001f);
blocks[1] |= 4 << 12 | (mjd >> 15);
blocks[2] = (mjd << 1) | (utc->tm_hour >> 4);
blocks[3] = (utc->tm_hour & INT16_L4) << 12 | utc->tm_min << 6;
/* get local time (for the offset) */
local_time = localtime(&now);
/* tm_gmtoff doesn't exist in POSIX but __tm_gmtoff does */
offset = local_time->__tm_gmtoff / (30 * 60); /*__tm_gmtoff is time in seconds from utc, so utc + __tm_gmtoff = local time*/
if (offset < 0) blocks[3] |= 1 << 5; /* if less than 0 then set the flag*/
blocks[3] |= abs(offset); /* offset should be halfs of a hour, so offset 1 = 30 minutes, 2 = 1 hour*/
return 1;
}
return 0;
}
/* PTYN group (10A) */
static void get_rds_ptyn_group(uint16_t *blocks) {
static unsigned char ptyn_text[PTYN_LENGTH];
static uint8_t ptyn_state;
if (ptyn_state == 0 && rds_state.ptyn_update) {
memcpy(ptyn_text, rds_data.ptyn, PTYN_LENGTH);
rds_state.ptyn_ab ^= 1;
rds_state.ptyn_update = 0;
}
blocks[1] |= 10 << 12 | ptyn_state;
blocks[1] |= rds_state.ptyn_ab << 4; /* no one knew about ptyn's ab until i saw sdr++'s decoder, it had decoded ptyn but not show it? it also did decode the ab flag */
blocks[2] = ptyn_text[ptyn_state * 4 + 0] << 8;
blocks[2] |= ptyn_text[ptyn_state * 4 + 1];
blocks[3] = ptyn_text[ptyn_state * 4 + 2] << 8;
blocks[3] |= ptyn_text[ptyn_state * 4 + 3];
ptyn_state++;
if (ptyn_state == 2) ptyn_state = 0;
}
/* Long PS group (15A) */
static void get_rds_lps_group(uint16_t *blocks) {
static unsigned char lps_text[LPS_LENGTH];
static uint8_t lps_state;
if (lps_state == 0 && rds_state.lps_update) {
memcpy(lps_text, rds_data.lps, LPS_LENGTH);
rds_state.lps_update = 0;
}
blocks[1] |= 15 << 12 | lps_state;
/* does lps have an ab? */
blocks[2] = lps_text[lps_state * 4 ] << 8;
blocks[2] |= lps_text[lps_state * 4 + 1];
blocks[3] = lps_text[lps_state * 4 + 2] << 8;
blocks[3] |= lps_text[lps_state * 4 + 3];
lps_state++;
if (lps_state == rds_state.lps_segments) lps_state = 0;
}
static void get_rds_ecc_group(uint16_t *blocks) {
blocks[1] |= 1 << 12;
if(rds_state.ecclic_enabled) {
blocks[2] = rds_data.ecc;
}
if(rds_state.pin_enabled) {
blocks[3] = rds_data.pin_day << 11; /* first 5 bits */
blocks[3] |= rds_data.pin_hour << 6; /* 5-10 bits from start */
blocks[3] |= rds_data.pin_minute; /* 10-16 bits from start */
}
}
static void get_rds_lic_group(uint16_t *blocks) {
blocks[1] |= 1 << 12;
if(rds_state.ecclic_enabled) {
blocks[2] = 0x3000; /* 0b0011000000000000 first 1 bit is the linkage actuator, the 3 after are variant codes which is 3 in this case after theres space for data */
blocks[2] |= rds_data.lic;
}
if(rds_state.pin_enabled) {
/* pin is also in lic as it also is a 1a group*/
blocks[3] = rds_data.pin_day << 11;
blocks[3] |= rds_data.pin_hour << 6;
blocks[3] |= rds_data.pin_minute;
}
}
/* RT+ */
#ifdef ODA_RTP
static void init_rtplus(uint8_t group) {
register_oda(group, ODA_AID_RTPLUS, 0);
rtplus_cfg.group = group;
rtplus_cfg.enabled = 1;
}
#endif
#ifdef ODA_RTP
/* RT+ group */
static void get_rds_rtplus_group(uint16_t *blocks) {
/* RT+ block format */
blocks[1] |= GET_GROUP_TYPE(rtplus_cfg.group) << 12;
blocks[1] |= GET_GROUP_VER(rtplus_cfg.group) << 11;
blocks[1] |= rtplus_cfg.toggle << 4 | rtplus_cfg.running << 3;
blocks[1] |= (rtplus_cfg.type[0] & INT8_U5) >> 3;
blocks[2] = (rtplus_cfg.type[0] & INT8_L3) << 13;
blocks[2] |= (rtplus_cfg.start[0] & INT8_L6) << 7;
blocks[2] |= (rtplus_cfg.len[0] & INT8_L6) << 1;
blocks[2] |= (rtplus_cfg.type[1] & INT8_U3) >> 5;
blocks[3] = (rtplus_cfg.type[1] & INT8_L5) << 11;
blocks[3] |= (rtplus_cfg.start[1] & INT8_L6) << 5;
blocks[3] |= rtplus_cfg.len[1] & INT8_L5;
}
#endif
/* Lower priority groups are placed in a subsequence
*/
static uint8_t get_rds_other_groups(uint16_t *blocks) {
static uint8_t group_counter[GROUP_15B];
/* Type 1A groups */
if (rds_data.ecc || rds_data.lic) {
if (++group_counter[GROUP_1A] >= 22) {
group_counter[GROUP_1A] = 0;
if(rds_data.ecc && rds_data.lic) {
if(rds_state.ecc_or_lic == 0) {
get_rds_ecc_group(blocks);
} else {
get_rds_lic_group(blocks);
}
rds_state.ecc_or_lic ^= 1;
} else if(rds_data.ecc) {
get_rds_ecc_group(blocks);
} else if(rds_data.ecc) {
get_rds_lic_group(blocks);
}
return 1;
}
}
/* Type 3A groups */
#ifdef ODA
if (oda_state.count) {
if (++group_counter[GROUP_3A] >= 25) {
group_counter[GROUP_3A] = 0;
get_rds_oda_group(blocks);
return 1;
}
}
#endif
/* Type 10A groups */
if (rds_state.ptyn_enabled && rds_data.ptyn[0]) {
if (++group_counter[GROUP_10A] >= 13) {
group_counter[GROUP_10A] = 0;
/* Do not generate a 10A group if PTYN is off */
get_rds_ptyn_group(blocks);
return 1;
}
}
/* RT+ groups */
#ifdef ODA_RTP
if (++group_counter[rtplus_cfg.group] >= 30) {
group_counter[rtplus_cfg.group] = 0;
if(rtplus_cfg.enabled == 0) {
return 0;
}
get_rds_rtplus_group(blocks);
return 1;
}
#endif
return 0;
}
/*
* Codes a group once every 3 groups
* Ex: 0A, 2A, 0A, 12A, 2A, 0A, 2A, 12A, etc
*/
static uint8_t get_rds_long_text_groups(uint16_t *blocks) {
static uint8_t group_selector = 0;
static uint8_t group_slot_counter = 0;
/* exit early until the 4th call */
if (++group_slot_counter < 4)
return 0;
/* reset the slot counter */
group_slot_counter = 0;
/* 3:1 ratio */
switch (group_selector) {
case 0:
case 1:
case 2:
case 3: /* Long PS */
if (rds_data.lps[0] && rds_state.lps_on) {
get_rds_lps_group(blocks);
goto group_coded;
}
break;
}
/* if no group was coded */
if (++group_selector >= 8) group_selector = 0;
return 0;
group_coded:
if (++group_selector >= 8) group_selector = 0;
return 1;
}
/* Get a custom group if there is one*/
static uint8_t get_rds_custom_groups(uint16_t *blocks) {
if(rds_state.custom_group_in) {
rds_state.custom_group_in = 0;
blocks[0] = rds_state.custom_group[0];
blocks[1] = rds_state.custom_group[1];
blocks[2] = rds_state.custom_group[2];
blocks[3] = rds_state.custom_group[3];
return 1;
}
return 0;
}
/* Creates an RDS group.
* This generates sequences of the form 0A, 2A, 0A, 2A, 0A, 2A, etc.
*/
static void get_rds_group(uint16_t *blocks) {
static uint8_t state;
/* Basic block data */
blocks[0] = rds_data.pi;
blocks[1] = rds_data.tp << 10;
blocks[1] |= rds_data.pty << 5;
blocks[2] = 0;
blocks[3] = 0;
/* Generate block content */
/* CT (clock time) has priority over other group types */
if (rds_data.ct && get_rds_ct_group(blocks)) {
goto group_coded;
}
/* Next to top priority, if want top then just disable CT i guess */
if(get_rds_custom_groups(blocks)) {
goto group_coded;
}
/* Longer text groups get medium priority */
if (get_rds_long_text_groups(blocks)) {
goto group_coded;
}
/* Other groups */
if (get_rds_other_groups(blocks)) {
goto group_coded;
}
/* Standard group sequence
This group sequence is way better than the offcial one, official goes like: 0A 2A which is shitty here it is 0A 0A 0A 0A 2A 2A 2A 2A (2A) so we're in sequence and for example immiedietly get PS and quite fast get RT */
if(state < 6) {
/* 0A */
get_rds_ps_group(blocks);
state++;
} else if(state >= 6) {
/* 2A */
if(!get_rds_rt_group(blocks)) { /* try to generate 2A if can't generate 2A than that means our text is empty so we generate PS instead of wasting groups on nothing*/
get_rds_ps_group(blocks);
}
state++;
}
if (state >= 12) state = 0;
group_coded:
/* for version B groups. good for custom groups */
if (IS_TYPE_B(blocks)) {
blocks[2] = rds_data.pi;
}
}
void get_rds_bits(uint8_t *bits) {
static uint16_t out_blocks[GROUP_LENGTH];
get_rds_group(out_blocks);
#ifdef RDS2
add_checkwords(out_blocks, bits, false);
#else
add_checkwords(out_blocks, bits);
#endif
}
void init_rds_encoder(struct rds_params_t rds_params) {
/* AF */
if (rds_params.af.num_afs) {
set_rds_af(rds_params.af);
fprintf(stderr, show_af_list(rds_params.af));
}
set_rds_pi(rds_params.pi);
set_rds_ps(rds_params.ps);
rds_state.rt_ab = 1;
set_rds_rt1_enabled(1);
set_rds_rt1(rds_params.rt1);
set_rds_pty(rds_params.pty);
rds_state.ptyn_ab = 1;
set_rds_ptyn(rds_params.ptyn);
set_rds_lps(rds_params.lps);
set_rds_lpson(1);
set_rds_tp(rds_params.tp);
set_rds_ecc(rds_params.ecc);
set_rds_lic(rds_params.lic);
set_rds_ecclic_toggle(1);
set_rds_pin_enabled(1);
set_rds_ct(1);
set_rds_ms(1);
set_rds_di(DI_STEREO | DI_DPTY);
/* Assign the RT+ AID to group 11A */
#ifdef ODA_RTP
init_rtplus(GROUP_11A);
#endif
/* initialize modulator objects */
init_rds_objects();
#ifdef RDS2
init_rds2_encoder(rds_params.rds2_image_path);
#endif
}
void exit_rds_encoder() {
exit_rds_objects();
#ifdef RDS2
exit_rds2_encoder();
#endif
}
void set_rds_pi(uint16_t pi_code) {
rds_data.pi = pi_code;
}
void set_rds_ecc(uint8_t ecc) {
rds_data.ecc = ecc; /* ecc is 8 bits, so a byte */
}
void set_rds_lic(uint8_t lic) {
rds_data.lic = lic & INT16_L12; /* lic is 12 bits according to the docs, but the values can be less, for example english is 0x09 */
}
void set_rds_ecclic_toggle(uint8_t toggle) {
rds_state.ecclic_enabled = toggle & INT8_0;
}
void set_rds_pin_enabled(uint8_t enabled) {
rds_state.pin_enabled = enabled & INT8_0;
}
void set_rds_pin(uint8_t day, uint8_t hour, uint8_t minute) {
rds_data.pin_day = (day & INT8_L5);
rds_data.pin_hour = (hour & INT8_L5);
rds_data.pin_minute = (minute & INT8_L6);
}
void set_rds_rt1_enabled(uint8_t rt1en) {
rds_state.rt1_enabled = rt1en & INT8_0;
}
void set_rds_rt1(unsigned char *rt1) {
uint8_t i = 0, len = 0;
rds_state.rt_update = 1;
memset(rds_data.rt1, ' ', RT_LENGTH);
while (*rt1 != 0 && len < RT_LENGTH)
rds_data.rt1[len++] = *rt1++;
if (len < RT_LENGTH) {
rds_state.rt_segments = 0;
/* Terminate RT with '\r' (carriage return) if RT
* is < 64 characters long
*/
rds_data.rt1[len++] = '\r';
/* find out how many segments are needed */
while (i < len) {
i += 4;
rds_state.rt_segments++;
}
} else {
/* Default to 16 if RT is 64 characters long */
rds_state.rt_segments = 16;
}
}
void set_rds_ps(unsigned char *ps) {
uint8_t len = 0;
rds_state.ps_update = 1;
memset(rds_data.ps, ' ', PS_LENGTH);
while (*ps != 0 && len < PS_LENGTH)
rds_data.ps[len++] = *ps++;
}
void set_rds_tpson(uint8_t tpson) {
rds_data.traffic_ps_on = tpson & INT8_0;
}
void set_rds_tps(unsigned char *tps) {
uint8_t len = 0;
rds_state.tps_update = 1;
memset(rds_data.tps, ' ', PS_LENGTH);
while (*tps != 0 && len < PS_LENGTH)
rds_data.tps[len++] = *tps++;
}
void set_rds_lpson(uint8_t lpson) {
rds_state.lps_on = lpson & INT8_0;
}
void set_rds_lps(unsigned char *lps) {
uint8_t i = 0, len = 0;
if (!lps[0]) {
memset(rds_data.lps, 0, LPS_LENGTH);
return;
}
rds_state.lps_update = 1;
memset(rds_data.lps, '\r', LPS_LENGTH);
while (*lps != 0 && len < LPS_LENGTH)
rds_data.lps[len++] = *lps++;
if (len < LPS_LENGTH) {
rds_state.lps_segments = 0;
/* increment to allow adding an '\r' in all cases */
len++;
/* find out how many segments are needed */
while (i < len) {
i += 4;
rds_state.lps_segments++;
}
} else {
/* default to 8 if LPS is 32 characters long */
rds_state.lps_segments = 8;
}
}
#ifdef ODA_RTP
void set_rds_rtplus_flags(uint8_t flags) {
if(flags == 2) {
rtplus_cfg.enabled = 0;
} else {
rtplus_cfg.enabled = 1;
}
rtplus_cfg.running = flags & INT8_0;
}
void set_rds_rtplus_tags(uint8_t *tags) {
rtplus_cfg.type[0] = tags[0] & INT8_L6;
rtplus_cfg.start[0] = tags[1] & INT8_L6;
rtplus_cfg.len[0] = tags[2] & INT8_L6;
rtplus_cfg.type[1] = tags[3] & INT8_L6;
rtplus_cfg.start[1] = tags[4] & INT8_L6;
rtplus_cfg.len[1] = tags[5] & INT8_L5;
rtplus_cfg.toggle ^= 1;
rtplus_cfg.running = 1;
}
#endif
void set_rds_af(struct rds_af_t new_af_list) {
memcpy(&rds_data.af, &new_af_list, sizeof(struct rds_af_t));
}
void clear_rds_af() {
memset(&rds_data.af, 0, sizeof(struct rds_af_t));
}
void set_rds_pty(uint8_t pty) {
rds_data.pty = pty & INT8_L5;
}
void set_rds_ptyn_enabled(uint8_t enabled) {
rds_state.ptyn_enabled = enabled & INT8_0;
}
void set_rds_ptyn(unsigned char *ptyn) {
uint8_t len = 0;
if (!ptyn[0]) {
memset(rds_data.ptyn, 0, PTYN_LENGTH);
return;
}
rds_state.ptyn_update = 1;
memset(rds_data.ptyn, ' ', PTYN_LENGTH);
while (*ptyn != 0 && len < PTYN_LENGTH)
rds_data.ptyn[len++] = *ptyn++;
}
void set_rds_ta(uint8_t ta) {
rds_data.ta = ta & INT8_0;
}
void set_rds_tp(uint8_t tp) {
rds_data.tp = tp & INT8_0;
}
void set_rds_ms(uint8_t ms) {
rds_data.ms = ms & INT8_0;
}
void set_rds_di(uint8_t di) {
rds_data.di = di & INT8_L4;
}
void set_rds_ct(uint8_t ct) {
rds_data.ct = ct & INT8_0;
}
uint16_t get_rds_pi() {
/* this is for custom groups, specifically 'G=' */
return rds_data.pi;
}
void set_rds_cg(uint16_t* blocks) {
rds_state.custom_group[0] = blocks[0];
rds_state.custom_group[1] = blocks[1];
rds_state.custom_group[2] = blocks[2];
rds_state.custom_group[3] = blocks[3];
rds_state.custom_group_in = 1;
}

336
src/rds.h Normal file
View File

@@ -0,0 +1,336 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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
/* The RDS error-detection code generator polynomial is
* x^10 + x^8 + x^7 + x^5 + x^4 + x^3 + x^0
*/
#define POLY 0x1B9
#define POLY_DEG 10
#define BLOCK_SIZE 16
#define GROUP_LENGTH 4
#define BITS_PER_GROUP (GROUP_LENGTH * (BLOCK_SIZE + POLY_DEG))
#define RDS_SAMPLE_RATE 190000
#define SAMPLES_PER_BIT 160
#define FILTER_SIZE 1120
#define SAMPLE_BUFFER_SIZE (SAMPLES_PER_BIT + FILTER_SIZE)
/* Text items
*
*/
#define RT_LENGTH 64
#define PS_LENGTH 8
#define PTYN_LENGTH 8
#define LPS_LENGTH 32
/* AF list size
*
*/
#define MAX_AFS 25
typedef struct rds_af_t {
uint8_t num_entries;
uint8_t num_afs;
uint8_t afs[MAX_AFS];
} rds_af_t;
/* AF codes */
#define AF_CODE_FILLER 205
#define AF_CODE_NO_AF 224
#define AF_CODE_NUM_AFS_BASE AF_CODE_NO_AF
#define AF_CODE_LFMF_FOLLOWS 250
typedef struct rds_params_t {
uint16_t pi; /*Program Identification*/
uint16_t lic; /* Language Identification code. lic should have 12 bits, 16 is the closest */
uint8_t ecc; /* Extended Country Code*/
uint8_t ta; /* Traffic Annoucement*/
uint8_t pty; /* Program Type */
uint8_t tp; /* Traffic Program */
uint8_t ms; /* Music/Speech */
uint8_t di; /* Decoder ID */
/* Program Service */
unsigned char ps[PS_LENGTH];
/* Traffic PS */
uint8_t traffic_ps_on;
unsigned char tps[PS_LENGTH];
/* Radio Text */
unsigned char rt1[RT_LENGTH];
/* Program Type Name */
unsigned char ptyn[PTYN_LENGTH];
/* Alternative Frequencies */
struct rds_af_t af;
/* Clock-time */
uint8_t ct;
/* Long PS */
unsigned char lps[LPS_LENGTH];
/* Program Item Number */
uint8_t pin_day;
uint8_t pin_hour;
uint8_t pin_minute;
#ifdef RDS2
/* RDS2 image path*/
char rds2_image_path[51];
#endif
} rds_params_t;
/* Here, the first member of the struct must be a scalar to avoid a
warning on -Wmissing-braces with GCC < 4.8.3
(bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53119)
*/
/* Group type
*
* 0-15
*/
#define GROUP_TYPE_0 ( 0 << 4)
#define GROUP_TYPE_1 ( 1 << 4)
#define GROUP_TYPE_2 ( 2 << 4)
#define GROUP_TYPE_3 ( 3 << 4)
#define GROUP_TYPE_4 ( 4 << 4)
#define GROUP_TYPE_5 ( 5 << 4)
#define GROUP_TYPE_6 ( 6 << 4)
#define GROUP_TYPE_7 ( 7 << 4)
#define GROUP_TYPE_8 ( 8 << 4)
#define GROUP_TYPE_9 ( 9 << 4)
#define GROUP_TYPE_10 (10 << 4)
#define GROUP_TYPE_11 (11 << 4)
#define GROUP_TYPE_12 (12 << 4)
#define GROUP_TYPE_13 (13 << 4)
#define GROUP_TYPE_14 (14 << 4)
#define GROUP_TYPE_15 (15 << 4)
/* Group versions
*
* The first 4 bits are the group number and the remaining 4 are
* the group version
*/
#define GROUP_VER_A 0
#define GROUP_VER_B 1
/* Version A groups */
#define GROUP_0A (GROUP_TYPE_0 | GROUP_VER_A)
#define GROUP_1A (GROUP_TYPE_1 | GROUP_VER_A)
#define GROUP_2A (GROUP_TYPE_2 | GROUP_VER_A)
#define GROUP_3A (GROUP_TYPE_3 | GROUP_VER_A)
#define GROUP_4A (GROUP_TYPE_4 | GROUP_VER_A)
#define GROUP_5A (GROUP_TYPE_5 | GROUP_VER_A)
#define GROUP_6A (GROUP_TYPE_6 | GROUP_VER_A)
#define GROUP_7A (GROUP_TYPE_7 | GROUP_VER_A)
#define GROUP_8A (GROUP_TYPE_8 | GROUP_VER_A)
#define GROUP_9A (GROUP_TYPE_9 | GROUP_VER_A)
#define GROUP_10A (GROUP_TYPE_10 | GROUP_VER_A)
#define GROUP_11A (GROUP_TYPE_11 | GROUP_VER_A)
#define GROUP_12A (GROUP_TYPE_12 | GROUP_VER_A)
#define GROUP_13A (GROUP_TYPE_13 | GROUP_VER_A)
#define GROUP_14A (GROUP_TYPE_14 | GROUP_VER_A)
#define GROUP_15A (GROUP_TYPE_15 | GROUP_VER_A)
/* Version B groups */
#define GROUP_0B (GROUP_TYPE_0 | GROUP_VER_B)
#define GROUP_1B (GROUP_TYPE_1 | GROUP_VER_B)
#define GROUP_2B (GROUP_TYPE_2 | GROUP_VER_B)
#define GROUP_3B (GROUP_TYPE_3 | GROUP_VER_B)
#define GROUP_4B (GROUP_TYPE_4 | GROUP_VER_B)
#define GROUP_5B (GROUP_TYPE_5 | GROUP_VER_B)
#define GROUP_6B (GROUP_TYPE_6 | GROUP_VER_B)
#define GROUP_7B (GROUP_TYPE_7 | GROUP_VER_B)
#define GROUP_8B (GROUP_TYPE_8 | GROUP_VER_B)
#define GROUP_9B (GROUP_TYPE_9 | GROUP_VER_B)
#define GROUP_10B (GROUP_TYPE_10 | GROUP_VER_B)
#define GROUP_11B (GROUP_TYPE_11 | GROUP_VER_B)
#define GROUP_12B (GROUP_TYPE_12 | GROUP_VER_B)
#define GROUP_13B (GROUP_TYPE_13 | GROUP_VER_B)
#define GROUP_14B (GROUP_TYPE_14 | GROUP_VER_B)
#define GROUP_15B (GROUP_TYPE_15 | GROUP_VER_B)
#define GET_GROUP_TYPE(x) ((x >> 4) & 15)
#define GET_GROUP_VER(x) (x & 1) /* only check bit 0 */
#define DI_STEREO (1 << 0) /* 1 - Stereo */
#define DI_AH (1 << 1) /* 2 - Artificial Head */
#define DI_COMPRESSED (1 << 2) /* 4 - Compressed */
#define DI_DPTY (1 << 3) /* 8 - Dynamic PTY */
/* Bit mask */
/* 8 bit */
#define INT8_ALL 0xff
/* Lower */
#define INT8_L1 0x01
#define INT8_L2 0x03
#define INT8_L3 0x07
#define INT8_L4 0x0f
#define INT8_L5 0x1f
#define INT8_L6 0x3f
#define INT8_L7 0x7f
/* Upper */
#define INT8_U7 0xfe
#define INT8_U6 0xfc
#define INT8_U5 0xf8
#define INT8_U4 0xf0
#define INT8_U3 0xe0
#define INT8_U2 0xc0
#define INT8_U1 0x80
/* Single */
#define INT8_0 0x01
#define INT8_1 0x02
#define INT8_2 0x04
#define INT8_3 0x08
#define INT8_4 0x10
#define INT8_5 0x20
#define INT8_6 0x40
#define INT8_7 0x80
/* 16 bit */
#define INT16_ALL 0xffff
/* Lower */
#define INT16_L1 0x0001
#define INT16_L2 0x0003
#define INT16_L3 0x0007
#define INT16_L4 0x000f
#define INT16_L5 0x001f
#define INT16_L6 0x003f
#define INT16_L7 0x007f
#define INT16_L8 0x00ff
#define INT16_L9 0x01ff
#define INT16_L10 0x03ff
#define INT16_L11 0x07ff
#define INT16_L12 0x0fff
#define INT16_L13 0x1fff
#define INT16_L14 0x3fff
#define INT16_L15 0x7fff
/* Upper */
#define INT16_U15 0xfffe
#define INT16_U14 0xfffc
#define INT16_U13 0xfff8
#define INT16_U12 0xfff0
#define INT16_U11 0xffe0
#define INT16_U10 0xffc0
#define INT16_U9 0xff80
#define INT16_U8 0xff00
#define INT16_U7 0xfe00
#define INT16_U6 0xfc00
#define INT16_U5 0xf800
#define INT16_U4 0xf000
#define INT16_U3 0xe000
#define INT16_U2 0xc000
#define INT16_U1 0x8000
/* Single */
#define INT16_0 0x0001
#define INT16_1 0x0002
#define INT16_2 0x0004
#define INT16_3 0x0008
#define INT16_4 0x0010
#define INT16_5 0x0020
#define INT16_6 0x0040
#define INT16_7 0x0080
#define INT16_8 0x0100
#define INT16_9 0x0200
#define INT16_10 0x0400
#define INT16_11 0x0800
#define INT16_12 0x1000
#define INT16_13 0x2000
#define INT16_14 0x4000
#define INT16_15 0x8000
/* 18 bit (for RDS2) */
#define INT18_U2 0x30000
#define INT18_L4 0x0000f
#define IS_TYPE_B(a) (a[1] & INT16_11)
#ifdef RDS2
#define IS_TUNNELING(a) (a[0] == 0x0000)
#endif
#ifdef ODA
/*
* RDS ODA ID group
*
* This struct is for defining ODAs that will be transmitted
*
* Can signal version A or B data groups
*/
typedef struct rds_oda_t {
uint8_t group;
uint16_t aid;
uint16_t scb;
} rds_oda_t;
#define MAX_ODAS 8
/*
* ODA AID
*
* Extensive list: https://www.nrscstandards.org/committees/dsm/archive/rds-oda-aids.pdf
*/
#define ODA_AID_RTPLUS 0x4bd7
#endif
/* RDS2 */
#define ODA_AID_RFT 0xff7f
#define ODA_AID_RFTPLUS 0xff80
extern void init_rds_encoder(struct rds_params_t rds_params);
extern void exit_rds_encoder();
extern void get_rds_bits(uint8_t *bits);
extern void set_rds_pi(uint16_t pi_code);
extern void set_rds_ecc(uint8_t ecc);
extern void set_rds_lic(uint8_t lic);
extern void set_rds_ecclic_toggle(uint8_t toggle);
extern void set_rds_pin_enabled(uint8_t enabled);
extern void set_rds_pin(uint8_t day, uint8_t hour, uint8_t minute);
extern void set_rds_rt1_enabled(uint8_t rt1en);
extern void set_rds_rt1(unsigned char *rt1);
extern void set_rds_ps(unsigned char *ps);
extern void set_rds_tpson(uint8_t tpson);
extern void set_rds_tps(unsigned char *ps);
extern void set_rds_lpson(uint8_t lpson);
extern void set_rds_lps(unsigned char *lps);
#ifdef ODA_RTP
extern void set_rds_rtplus_flags(uint8_t flags);
extern void set_rds_rtplus_tags(uint8_t *tags);
#endif
extern void set_rds_ta(uint8_t ta);
extern void set_rds_pty(uint8_t pty);
extern void set_rds_ptyn_enabled(uint8_t enabled);
extern void set_rds_ptyn(unsigned char *ptyn);
extern void set_rds_af(struct rds_af_t new_af_list);
extern void clear_rds_af();
extern void set_rds_tp(uint8_t tp);
extern void set_rds_ms(uint8_t ms);
extern void set_rds_ct(uint8_t ct);
extern void set_rds_di(uint8_t di);
#ifdef RDS2
extern float get_rds_sample(uint8_t stream_num, uint8_t rds2tunneling);
#else
extern float get_rds_sample(uint8_t stream_num);
#endif
extern uint16_t get_rds_pi();
extern void set_rds_cg(uint16_t* blocks);
#endif /* RDS_H */

452
src/rds2.c Normal file
View File

@@ -0,0 +1,452 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019-2020 Anthony96922
*
* 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 "common.h"
#include "rds.h"
#include "rds2.h"
#include "lib.h"
/*
* RDS2-specific stuff
*/
/* RFT */
/* fallback station logo */
#include "rds2_image_data.c"
/* station logo */
static struct rft_t station_logo_stream;
static void init_rft(struct rft_t *rft, uint8_t file_id,
bool usecrc, uint8_t crc_mode, char *file_path) {
uint32_t seg_counter = 0;
uint32_t crc_chunk_counter = 0;
uint16_t crc_chunk_size = 0;
FILE *image_file_hdlr;
rft->file_path = malloc(MAX_FILE_NAME_LEN + 1);
rft->file_path[MAX_FILE_NAME_LEN] = 0;
rft->file_data = malloc(MAX_IMAGE_LEN);
rft->crcs = malloc(MAX_CRC_CHUNK_ADDR * sizeof(uint16_t));
/* unused data must be padded with zeroes */
memset(rft->file_data, 0, MAX_IMAGE_LEN);
strncpy(rft->file_path, file_path, MAX_FILE_NAME_LEN);
/* open file in binary mode */
image_file_hdlr = fopen(file_path, "rb");
if (image_file_hdlr != NULL) {
fseek(image_file_hdlr, 0, SEEK_END);
rft->file_len = ftell(image_file_hdlr);
rewind(image_file_hdlr);
/* image cannot be larger than 163840 bytes */
if (rft->file_len > MAX_IMAGE_LEN) {
#ifdef RDS2_DEBUG
fprintf(stderr, "W: Image too large!\n");
#endif
rft->file_len = MAX_IMAGE_LEN;
}
/* copy image data to local buffer */
fread(rft->file_data, rft->file_len, 1, image_file_hdlr);
fclose(image_file_hdlr);
} else { /* fallback in case file can't be read at startup */
rft->file_len = station_logo_len;
if (rft->file_len > MAX_IMAGE_LEN) {
rft->file_len = MAX_IMAGE_LEN;
}
memcpy(rft->file_data, station_logo, rft->file_len);
}
/* keep track of file lengths so we know when to toggle */
rft->prev_file_len = rft->file_len;
rft->channel = 0;
rft->file_id = file_id;
rft->toggle = 0;
rft->num_segs = 0;
/* determine how many segments we need */
while (seg_counter < rft->file_len) {
seg_counter += 5;
rft->num_segs++;
}
rft->use_crc = usecrc;
/* if CRC is disabled exit early */
if (!rft->use_crc) {
rft->crc_mode = 0;
rft->num_crc_chunks = 1; /* only 1 */
rft->crcs[0] = 0x0000;
return;
}
rft->crc_mode = crc_mode & 7;
switch (rft->crc_mode) {
case RFT_CRC_MODE_ENTIRE_FILE:
/* no-op */
break;
case RFT_CRC_MODE_16_GROUPS:
crc_chunk_size = 16;
break;
case RFT_CRC_MODE_32_GROUPS:
crc_chunk_size = 32;
break;
case RFT_CRC_MODE_64_GROUPS:
crc_chunk_size = 64;
break;
case RFT_CRC_MODE_128_GROUPS:
crc_chunk_size = 128;
break;
case RFT_CRC_MODE_256_GROUPS:
crc_chunk_size = 256;
break;
case RFT_CRC_MODE_RFU: /* reserved - use auto mode */
case RFT_CRC_MODE_AUTO:
if (rft->file_len >= 81920) {
crc_chunk_size = 64;
rft->crc_mode = RFT_CRC_MODE_64_GROUPS;
} else if (rft->file_len > 40960) {
crc_chunk_size = 32;
rft->crc_mode = RFT_CRC_MODE_32_GROUPS;
} else {
crc_chunk_size = 16;
rft->crc_mode = RFT_CRC_MODE_16_GROUPS;
}
break;
}
if (rft->crc_mode) { /* chunked modes */
/* RFT packet size = chunk size * 5 data bytes per group */
rft->pkt_size = crc_chunk_size * 5;
/* the remainder (if file size is not a multiple of pkt_size) */
rft->pkt_size_rem = rft->file_len % rft->pkt_size;
/* calculate number of CRC chunks */
while (crc_chunk_counter <= rft->file_len) {
crc_chunk_counter += rft->pkt_size;
rft->num_crc_chunks++;
}
rft->crc_chunks = malloc(rft->pkt_size);
/* calculate CRC's for chunks */
for (uint16_t i = 0; i < rft->num_crc_chunks; i++) {
memset(rft->crc_chunks, 0, rft->pkt_size);
memcpy(rft->crc_chunks,
rft->file_data + i * rft->pkt_size, rft->pkt_size);
if ((i == rft->num_crc_chunks - 1) && rft->pkt_size_rem) {
/* last chunk may have less bytes */
rft->crcs[i] = crc16(rft->crc_chunks, rft->pkt_size_rem);
} else {
rft->crcs[i] = crc16(rft->crc_chunks, rft->pkt_size);
}
}
free(rft->crc_chunks);
} else { /* CRC of entire file */
rft->num_crc_chunks = 1; /* only 1 */
rft->crcs[0] = crc16(rft->file_data, rft->file_len);
}
}
static void update_rft(struct rft_t *rft) {
uint32_t seg_counter = 0;
FILE *image_file_hdlr;
/* open file in binary mode */
image_file_hdlr = fopen(rft->file_path, "rb");
if (image_file_hdlr == NULL) {
return;
}
rft->prev_file_len = rft->file_len;
/* update size field */
fseek(image_file_hdlr, 0, SEEK_END);
rft->file_len = ftell(image_file_hdlr);
rewind(image_file_hdlr);
/* file didn't change so return early */
if (rft->file_len == rft->prev_file_len) {
fclose(image_file_hdlr);
return;
}
/* image cannot be larger than 163840 bytes */
if (rft->file_len > MAX_IMAGE_LEN) {
#ifdef RDS2_DEBUG
fprintf(stderr, "W: Image too large!\n");
#endif
rft->file_len = MAX_IMAGE_LEN;
}
/* copy image data to local buffer */
fread(rft->file_data, rft->file_len, 1, image_file_hdlr);
fclose(image_file_hdlr);
rft->num_segs = 0;
/* recalculate # of needed segments */
while (seg_counter < rft->file_len) {
seg_counter += 5;
rft->num_segs++;
}
if (rft->crc_mode) {
/* recalculate CRC's for chunks */
for (uint16_t i = 0; i < rft->num_crc_chunks; i++) {
memset(rft->crc_chunks, 0, rft->pkt_size);
memcpy(rft->crc_chunks,
rft->file_data + i * rft->pkt_size, rft->pkt_size);
if ((i == rft->num_crc_chunks - 1) && rft->pkt_size_rem) {
/* last chunk may have less bytes */
rft->crcs[i] = crc16(rft->crc_chunks, rft->pkt_size_rem);
} else {
rft->crcs[i] = crc16(rft->crc_chunks, rft->pkt_size);
}
}
free(rft->crc_chunks);
} else { /* CRC of entire file */
rft->num_crc_chunks = 1; /* only 1 */
rft->crcs[0] = crc16(rft->file_data, rft->file_len);
}
if (++rft->file_version == 8) {
rft->file_version = 0;
}
}
static void exit_rft(struct rft_t *rft) {
free(rft->file_data);
free(rft->file_path);
free(rft->crcs);
}
/*
* RFT variant 0 (file metadata)
*
* Carries:
* - Function Header
* - Pipe Number
* - ODA ID
* - Variant code (0)
* - CRC-16 Flag
* - File ID
* - File size (18 bits)
*/
static void get_rft_var_0_data_group(struct rft_t *rft, uint16_t *blocks) {
/* function header */
blocks[0] = 1 << 15;
blocks[0] |= (rft->channel & INT8_L4); /* pipe number */
/* ODA ID */
blocks[1] = ODA_AID_RFT;
blocks[2] = (0 & INT18_L4) << 12; /* variant code */
blocks[2] |= ((rft->use_crc ? 1 : 0) & INT8_L1) << 11; /* CRC */
blocks[2] |= (rft->file_version & INT8_L3) << 8; /* file version */
blocks[2] |= (rft->file_id & INT8_L6) << 2; /* file ID */
blocks[2] |= (rft->file_len & INT18_U2) >> 16;
blocks[3] = rft->file_len & INT16_ALL;
}
/*
* RFT variant 1 (chunk to CRC mappings)
*
* Carries:
* - Function Header
* - Pipe Number
* - ODA ID
* - Variant Code (1)
* - CRC Mode
* - Chunk Address (9 bits)
* - CRC
*/
static void get_rft_var_1_data_group(struct rft_t *rft, uint16_t *blocks) {
/* function header */
blocks[0] = 1 << 15;
blocks[0] |= (rft->channel & INT8_L4); /* pipe number */
/* ODA ID */
blocks[1] = ODA_AID_RFT;
blocks[2] = (1 & INT8_L4) << 12; /* variant code */
blocks[2] |= (rft->crc_mode & INT8_L3) << 9; /* CRC mode */
/* chunk address */
blocks[2] |= rft->crc_chunk_addr & INT16_L9;
/* CRC */
blocks[3] = rft->crcs[rft->crc_chunk_addr];
if (++rft->crc_chunk_addr > rft->num_crc_chunks) {
#ifdef RDS2_DEBUG
fprintf(stderr, "File CRC sending complete\n");
#endif
rft->crc_chunk_addr = 0;
}
}
/*
* RFT variants 2-15 (custom variants)
*
* Carries:
* - Function Header
* - Pipe Number
* - Variant code
* - Variant data:
* - 2-7 carries file-related data
* - 8-15 ODA data that is not related
*/
#if 0
static void get_rft_var_2_15_data_group(struct rft_t *rft, uint16_t *blocks) {
/* function header */
blocks[0] = (2 << 6) << 8;
blocks[0] |= (rft->channel & INT8_L4); /* pipe number */
/* ODA ID */
blocks[1] = ODA_AID_RFT;
blocks[2] = (8 & INT8_L4) << 12; /* variant code */
blocks[2] |= 0 & INT16_L12;
blocks[3] = 0 & INT16_ALL;
}
#endif
/*
* RFT data group (for file data TXing)
*
* Carries:
* - Function Header
* - Pipe Number
* - Toggle Bit
* - Segment address (15 bits)
* - File data (5 bytes per group)
*/
static void get_rft_file_data_group(struct rft_t *rft, uint16_t *blocks) {
/* function header */
blocks[0] = 2 << 12;
blocks[0] |= (rft->channel & INT8_L4) << 8; /* pipe number */
/* toggle bit */
blocks[0] |= (rft->toggle & INT16_L1) << 7;
/* segment address */
blocks[0] |= ((rft->seg_addr_img & INT16_U8) >> 8) & INT16_L7;
blocks[1] = (rft->seg_addr_img & INT16_L8) << 8;
/* image data */
blocks[1] |= rft->file_data[rft->seg_addr_img * 5 + 0];
blocks[2] = rft->file_data[rft->seg_addr_img * 5 + 1] << 8;
blocks[2] |= rft->file_data[rft->seg_addr_img * 5 + 2];
blocks[3] = rft->file_data[rft->seg_addr_img * 5 + 3] << 8;
blocks[3] |= rft->file_data[rft->seg_addr_img * 5 + 4];
if (++rft->seg_addr_img > rft->num_segs) {
#ifdef RDS2_DEBUG
fprintf(stderr, "File sending complete\n");
#endif
rft->seg_addr_img = 0;
rft->toggle ^= 1;
update_rft(rft);
}
}
static void get_rft_stream(uint16_t *blocks) {
static uint8_t rft_state;
switch (rft_state) {
case 0:
get_rft_var_0_data_group(&station_logo_stream, blocks);
break;
case 1:
get_rft_var_1_data_group(&station_logo_stream, blocks);
break;
case 2:
get_rft_var_0_data_group(&station_logo_stream, blocks);
break;
case 3:
get_rft_var_1_data_group(&station_logo_stream, blocks);
break;
default:
get_rft_file_data_group(&station_logo_stream, blocks);
break;
}
rft_state++;
if (rft_state == 50) rft_state = 0;
}
/*
* RDS 2 group sequence
*/
static void get_rds2_group(uint8_t stream_num, uint16_t *blocks) {
switch (stream_num) {
case 1:
case 2:
case 3:
default:
get_rft_stream(blocks);
break;
}
#ifdef RDS2_DEBUG
fprintf(stderr, "Stream %u: %04x %04x %04x %04x\n",
stream_num, blocks[0], blocks[1], blocks[2], blocks[3]);
#endif
}
void get_rds2_bits(uint8_t stream, uint8_t *bits) {
static uint16_t out_blocks[GROUP_LENGTH];
get_rds2_group(stream, out_blocks);
add_checkwords(out_blocks, bits, true);
}
void init_rds2_encoder(char *station_logo_path) {
/* create a new stream for the station logo */
init_rft(&station_logo_stream,
0 /* file ID */,
false /* don't use crc */,
RFT_CRC_MODE_AUTO,
station_logo_path
);
}
void exit_rds2_encoder() {
exit_rft(&station_logo_stream);
}

71
src/rds2.h Normal file
View File

@@ -0,0 +1,71 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019-2020 Anthony96922
*
* 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/>.
*/
typedef struct rds2_oda_t {
uint16_t aid;
uint8_t channel;
} rds2_oda_t;
#define GET_RDS2_ODA_CHANNEL(x) (x & INT8_L5)
#define MAX_FILE_NAME_LEN 64 /* does not include terminating null char */
#define MAX_IMAGE_LEN 163840
#define MAX_CRC_CHUNK_ADDR 511
/* RFT CRC mode */
enum rft_crc_mode {
RFT_CRC_MODE_ENTIRE_FILE, /* over entire file (universal) */
RFT_CRC_MODE_16_GROUPS, /* chunks of 16 groups (size <= 40,960) */
RFT_CRC_MODE_32_GROUPS, /* chunks of 32 groups (40,960 < size <= 81,920) */
RFT_CRC_MODE_64_GROUPS, /* chunks of 64 groups (size > 81,960) */
RFT_CRC_MODE_128_GROUPS, /* chunks of 128 groups (size > 81,960) */
RFT_CRC_MODE_256_GROUPS, /* chunks of 256 groups (size > 81,960) */
RFT_CRC_MODE_RFU, /* reserved */
RFT_CRC_MODE_AUTO /* automatic between 1-3 based on size */
};
/* RDS2 File Transfer */
typedef struct rft_t {
uint8_t channel;
char *file_path;
unsigned char *file_data;
size_t file_len;
size_t prev_file_len;
uint8_t file_version;
uint8_t file_id;
uint8_t variant_code;
uint8_t toggle;
uint16_t pkt_size;
uint16_t pkt_size_rem;
uint16_t seg_addr_img;
uint16_t num_segs;
/* CRC chunk map */
bool use_crc;
uint8_t crc_mode;
uint16_t crc_chunk_addr;
uint16_t num_crc_chunks;
uint8_t *crc_chunks;
uint16_t *crcs;
} rft_t;
extern void get_rds2_bits(uint8_t stream_num, uint8_t *bits);
extern void init_rds2_encoder(char *station_logo_path);
extern void exit_rds2_encoder();

181
src/rds2_image_data.c Normal file
View File

@@ -0,0 +1,181 @@
static const unsigned char station_logo[] = {
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xe2, 0x01, 0xd8,
0x49, 0x43, 0x43, 0x5f, 0x50, 0x52, 0x4f, 0x46, 0x49, 0x4c, 0x45, 0x00,
0x01, 0x01, 0x00, 0x00, 0x01, 0xc8, 0x00, 0x00, 0x00, 0x00, 0x04, 0x30,
0x00, 0x00, 0x6d, 0x6e, 0x74, 0x72, 0x52, 0x47, 0x42, 0x20, 0x58, 0x59,
0x5a, 0x20, 0x07, 0xe0, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x61, 0x63, 0x73, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0xf6, 0xd6, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x2d, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x09, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00,
0x00, 0x24, 0x72, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00,
0x00, 0x14, 0x67, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x01, 0x28, 0x00, 0x00,
0x00, 0x14, 0x62, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x01, 0x3c, 0x00, 0x00,
0x00, 0x14, 0x77, 0x74, 0x70, 0x74, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00,
0x00, 0x14, 0x72, 0x54, 0x52, 0x43, 0x00, 0x00, 0x01, 0x64, 0x00, 0x00,
0x00, 0x28, 0x67, 0x54, 0x52, 0x43, 0x00, 0x00, 0x01, 0x64, 0x00, 0x00,
0x00, 0x28, 0x62, 0x54, 0x52, 0x43, 0x00, 0x00, 0x01, 0x64, 0x00, 0x00,
0x00, 0x28, 0x63, 0x70, 0x72, 0x74, 0x00, 0x00, 0x01, 0x8c, 0x00, 0x00,
0x00, 0x3c, 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, 0x53, 0x00, 0x00,
0x00, 0x08, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x73, 0x00, 0x52, 0x00, 0x47,
0x00, 0x42, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x6f, 0xa2, 0x00, 0x00, 0x38, 0xf5, 0x00, 0x00, 0x03, 0x90, 0x58, 0x59,
0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x99, 0x00, 0x00,
0xb7, 0x85, 0x00, 0x00, 0x18, 0xda, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x24, 0xa0, 0x00, 0x00, 0x0f, 0x84, 0x00, 0x00,
0xb6, 0xcf, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xf6, 0xd6, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x2d, 0x70, 0x61,
0x72, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02,
0x66, 0x66, 0x00, 0x00, 0xf2, 0xa7, 0x00, 0x00, 0x0d, 0x59, 0x00, 0x00,
0x13, 0xd0, 0x00, 0x00, 0x0a, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, 0x53, 0x00, 0x00,
0x00, 0x20, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x47, 0x00, 0x6f, 0x00, 0x6f,
0x00, 0x67, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x20, 0x00, 0x49, 0x00, 0x6e,
0x00, 0x63, 0x00, 0x2e, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x31,
0x00, 0x36, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x1b, 0x12, 0x14, 0x17, 0x14,
0x11, 0x1b, 0x17, 0x16, 0x17, 0x1e, 0x1c, 0x1b, 0x20, 0x28, 0x42, 0x2b,
0x28, 0x25, 0x25, 0x28, 0x51, 0x3a, 0x3d, 0x30, 0x42, 0x60, 0x55, 0x65,
0x64, 0x5f, 0x55, 0x5d, 0x5b, 0x6a, 0x78, 0x99, 0x81, 0x6a, 0x71, 0x90,
0x73, 0x5b, 0x5d, 0x85, 0xb5, 0x86, 0x90, 0x9e, 0xa3, 0xab, 0xad, 0xab,
0x67, 0x80, 0xbc, 0xc9, 0xba, 0xa6, 0xc7, 0x99, 0xa8, 0xab, 0xa4, 0xff,
0xdb, 0x00, 0x43, 0x01, 0x1c, 0x1e, 0x1e, 0x28, 0x23, 0x28, 0x4e, 0x2b,
0x2b, 0x4e, 0xa4, 0x6e, 0x5d, 0x6e, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xff, 0xc0, 0x00, 0x11,
0x08, 0x00, 0xcd, 0x00, 0xcd, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01,
0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1a, 0x00, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x05, 0x04, 0x06, 0x02, 0x03, 0x01, 0xff, 0xc4, 0x00, 0x3c, 0x10,
0x00, 0x01, 0x03, 0x03, 0x02, 0x02, 0x05, 0x09, 0x05, 0x08, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x11, 0x12,
0x21, 0x06, 0x31, 0x13, 0x22, 0x41, 0x91, 0xa1, 0x14, 0x32, 0x51, 0x61,
0x71, 0x73, 0x81, 0xb1, 0xd1, 0x34, 0x36, 0x52, 0xc2, 0xe1, 0x16, 0x24,
0x33, 0x35, 0x42, 0x44, 0x82, 0xc1, 0x54, 0xb2, 0xf0, 0xff, 0xc4, 0x00,
0x14, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xc4, 0x00, 0x14, 0x11,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00,
0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xe6, 0x40, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x56,
0xc7, 0x68, 0x65, 0xd1, 0x26, 0x57, 0xcc, 0xe8, 0xfa, 0x3d, 0x3c, 0x93,
0x39, 0xce, 0x7e, 0x85, 0x2a, 0x7e, 0x14, 0x6a, 0xac, 0x8b, 0x51, 0x3b,
0x91, 0x35, 0x2a, 0x31, 0x18, 0x89, 0x9c, 0x76, 0x2a, 0xfd, 0x00, 0xe6,
0x01, 0x42, 0x9e, 0xd8, 0xda, 0xab, 0xa3, 0xa8, 0xa0, 0xaa, 0x8d, 0xcd,
0x6a, 0xff, 0x00, 0x15, 0x76, 0xd4, 0x89, 0xcf, 0x09, 0xdb, 0xdb, 0xdc,
0x5b, 0x4e, 0x13, 0xa7, 0xe8, 0xb0, 0xb5, 0x32, 0xf4, 0x9f, 0x8b, 0x09,
0x8e, 0xef, 0xd4, 0x0e, 0x50, 0x1b, 0x6e, 0xb6, 0xc9, 0x6d, 0x93, 0xa4,
0x72, 0x2a, 0x3d, 0x8e, 0x4c, 0xb1, 0xe8, 0x98, 0xcf, 0xea, 0x6d, 0xb2,
0xd8, 0x16, 0xe1, 0x0f, 0x94, 0x4d, 0x22, 0xc7, 0x12, 0xae, 0x1a, 0x8d,
0x4d, 0xdd, 0xe9, 0x5f, 0x56, 0xe0, 0x67, 0xb1, 0xdb, 0xa3, 0xb9, 0xd4,
0xbe, 0x29, 0x1e, 0xe6, 0x23, 0x59, 0xab, 0x2d, 0xf6, 0xa1, 0xf9, 0x55,
0x6f, 0x8e, 0x1b, 0xe3, 0x68, 0x1a, 0xf7, 0x2b, 0x16, 0x46, 0x33, 0x52,
0xf3, 0xeb, 0x63, 0xea, 0x74, 0x76, 0xab, 0x2a, 0xdb, 0x2e, 0x2f, 0x92,
0x39, 0x3a, 0x48, 0x5f, 0x1a, 0xa2, 0x2a, 0xf3, 0x6a, 0xe5, 0x36, 0x5f,
0x49, 0x12, 0xee, 0xc5, 0x97, 0x8a, 0x56, 0x36, 0xbd, 0x58, 0xaf, 0x92,
0x36, 0xa3, 0x9b, 0xcd, 0xb9, 0x46, 0xee, 0x80, 0x78, 0xbf, 0x5a, 0x62,
0xb5, 0xf4, 0x1d, 0x14, 0x8f, 0x7f, 0x49, 0xab, 0x3a, 0xb1, 0xb6, 0x31,
0xf5, 0x24, 0x96, 0xb8, 0x8a, 0x86, 0x5a, 0x3f, 0x27, 0xe9, 0x2b, 0x26,
0xa9, 0xd7, 0xab, 0x1d, 0x22, 0xaa, 0xe9, 0xc6, 0x39, 0x6f, 0xeb, 0x3f,
0x6d, 0x1c, 0x3d, 0x25, 0x74, 0x29, 0x51, 0x3c, 0x8b, 0x14, 0x4e, 0x5e,
0xaa, 0x22, 0x65, 0xce, 0x4f, 0x4f, 0xa8, 0x08, 0x87, 0xa8, 0xa3, 0x7c,
0xd2, 0xb2, 0x28, 0xd3, 0x53, 0xde, 0xa8, 0xd6, 0xa7, 0xa5, 0x54, 0xe9,
0x6a, 0xb8, 0x4d, 0xbd, 0x1a, 0xad, 0x25, 0x43, 0xb5, 0xa2, 0x79, 0xb2,
0x22, 0x6f, 0xf1, 0x4e, 0x44, 0xbb, 0x03, 0x7a, 0x2b, 0xf4, 0x0c, 0x95,
0x34, 0xb9, 0xae, 0x73, 0x55, 0x1d, 0xd8, 0xba, 0x57, 0x6e, 0xf0, 0x2b,
0xd3, 0x70, 0x9c, 0x29, 0x1f, 0xef, 0x35, 0x12, 0x39, 0xeb, 0xd9, 0x1e,
0x11, 0x13, 0xbd, 0x17, 0x3e, 0x04, 0xbb, 0xcd, 0x8e, 0x4b, 0x6b, 0x52,
0x68, 0xde, 0xb2, 0xc0, 0xab, 0x85, 0x5c, 0x61, 0x5a, 0xbe, 0xbf, 0xa9,
0x53, 0x8a, 0x22, 0xaf, 0x92, 0x68, 0x3c, 0x99, 0xb2, 0xbe, 0x14, 0x4c,
0xe2, 0x24, 0x55, 0xc3, 0xb3, 0xcd, 0x71, 0xf0, 0xc7, 0xc4, 0xd5, 0x77,
0x47, 0xb7, 0x86, 0x5c, 0x95, 0x19, 0x59, 0x12, 0x38, 0xd1, 0xfb, 0xe5,
0x75, 0x65, 0x3f, 0xd8, 0x11, 0x6c, 0x76, 0x58, 0x6e, 0x74, 0xf2, 0x49,
0x24, 0xaf, 0x62, 0xb5, 0xfa, 0x51, 0x1b, 0x8f, 0x41, 0x4b, 0xf6, 0x4e,
0x97, 0xfe, 0x44, 0xde, 0x07, 0xef, 0x07, 0x7d, 0x86, 0x7f, 0x7b, 0xfe,
0x90, 0xf3, 0x51, 0x64, 0xb9, 0x4b, 0x2c, 0xf2, 0xb2, 0xe0, 0xac, 0xd4,
0xf7, 0x39, 0x8c, 0xd6, 0xec, 0x22, 0x2a, 0xed, 0xbf, 0x67, 0x70, 0x10,
0xef, 0x54, 0x0c, 0xb7, 0x56, 0x24, 0x11, 0xbd, 0xcf, 0x45, 0x62, 0x3b,
0x2e, 0xe7, 0xda, 0x60, 0x3e, 0xf5, 0xac, 0xa8, 0x8a, 0xa5, 0xf1, 0xd5,
0xab, 0xd6, 0x56, 0x6c, 0xba, 0x9d, 0x93, 0xe0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0x4f, 0xc1, 0x9e, 0x6d, 0x5f, 0xb5, 0x9f,
0x98, 0x97, 0x7a, 0xad, 0xa9, 0x75, 0xda, 0xa3, 0x13, 0x48, 0xd4, 0x63,
0x95, 0x8d, 0x46, 0xb9, 0x51, 0x11, 0x39, 0x78, 0x95, 0x38, 0x33, 0xcd,
0xab, 0xf6, 0xb3, 0xf3, 0x10, 0xee, 0xff, 0x00, 0xcd, 0x6a, 0xfd, 0xeb,
0xbe, 0x60, 0x7d, 0x2d, 0x16, 0xea, 0x8b, 0x85, 0x4e, 0x20, 0x7f, 0x47,
0xd1, 0xe1, 0xce, 0x93, 0xf0, 0xef, 0xb6, 0x3d, 0x7f, 0x42, 0xe3, 0x38,
0x6a, 0x76, 0xd4, 0xa5, 0x42, 0x5c, 0xde, 0xb3, 0x22, 0xe7, 0x5a, 0xc7,
0x95, 0xff, 0x00, 0xb1, 0x97, 0x84, 0x2a, 0x62, 0x8a, 0x6a, 0x88, 0x64,
0x7a, 0x35, 0xf2, 0xa3, 0x74, 0x65, 0x71, 0x9c, 0x67, 0x6f, 0x6e, 0xe6,
0x9a, 0xae, 0x1e, 0xa5, 0x8e, 0xa2, 0x4a, 0xba, 0x9a, 0xd7, 0x36, 0x15,
0x72, 0xbd, 0xc8, 0xa9, 0xbe, 0xfb, 0xe3, 0x3f, 0xa0, 0x0e, 0x31, 0x6a,
0x79, 0x35, 0x3b, 0xb1, 0xba, 0x3d, 0x53, 0x3f, 0x0f, 0xd0, 0xd8, 0xd7,
0x2d, 0x3f, 0x0b, 0xb5, 0xd1, 0x75, 0x1c, 0x94, 0xc8, 0xa8, 0xa9, 0xb6,
0x15, 0x53, 0x9f, 0x89, 0x97, 0x8c, 0x7e, 0xc7, 0x07, 0xbc, 0x5f, 0x91,
0xf6, 0xb0, 0xd5, 0xc1, 0x5f, 0x6a, 0x4a, 0x39, 0x5c, 0x8b, 0x23, 0x18,
0xb1, 0xbd, 0x8b, 0xb2, 0xab, 0x79, 0x22, 0xa7, 0xc3, 0x00, 0x4f, 0xe0,
0xf9, 0xa4, 0x5a, 0xa9, 0xe2, 0x57, 0xaa, 0xb1, 0x59, 0xaf, 0x0a, 0xbd,
0xb9, 0x4d, 0xfc, 0x4f, 0x8d, 0xc7, 0xef, 0x7b, 0x3d, 0xf4, 0x5f, 0x26,
0x97, 0x6d, 0x96, 0xea, 0x2b, 0x6d, 0x53, 0xe2, 0x82, 0x47, 0x3e, 0x67,
0xb3, 0x2a, 0x8f, 0x54, 0x55, 0x6b, 0x51, 0x7d, 0x49, 0xb7, 0x3f, 0x02,
0x15, 0xc7, 0xef, 0x7b, 0x3d, 0xf4, 0x5f, 0x26, 0x81, 0xab, 0x8c, 0xff,
0x00, 0xb3, 0xff, 0x00, 0x3f, 0xca, 0x6f, 0xb8, 0x48, 0xfa, 0x5e, 0x19,
0x47, 0x40, 0xf5, 0x63, 0x9b, 0x14, 0x68, 0x8e, 0x45, 0xdd, 0x39, 0x27,
0xc8, 0xc1, 0xc6, 0x7f, 0xd9, 0xff, 0x00, 0x9f, 0xe5, 0x36, 0x5a, 0xea,
0xa9, 0x2e, 0xd6, 0x96, 0xd1, 0x4c, 0xa9, 0xad, 0x18, 0x8c, 0x7b, 0x35,
0x61, 0x57, 0x1c, 0x95, 0x3b, 0x91, 0x40, 0xc5, 0xc2, 0x35, 0x53, 0x3e,
0x79, 0xe1, 0x7c, 0x8e, 0x7b, 0x34, 0x23, 0x91, 0x1c, 0xaa, 0xb8, 0x5c,
0xf6, 0x77, 0x93, 0x6f, 0xeb, 0xd0, 0xdf, 0xa7, 0x74, 0x5d, 0x47, 0x35,
0xcd, 0x72, 0x2a, 0x6d, 0x85, 0xd2, 0x8b, 0x9e, 0xf3, 0xa6, 0xa5, 0xa3,
0xa0, 0xb1, 0x43, 0x24, 0x8b, 0x2a, 0xb5, 0x1d, 0xcd, 0xf2, 0xb9, 0x32,
0xb8, 0xec, 0x43, 0x8f, 0xb8, 0xd5, 0x2d, 0x6d, 0x74, 0xd5, 0x18, 0xc2,
0x3d, 0xdb, 0x22, 0xf6, 0x27, 0x24, 0xf0, 0xc0, 0x1d, 0x1d, 0xa3, 0x88,
0x27, 0xad, 0xa8, 0x8a, 0x95, 0xd4, 0xad, 0x73, 0xd7, 0xce, 0x91, 0x1d,
0x84, 0x44, 0x4e, 0xdc, 0x63, 0xff, 0x00, 0x29, 0xe3, 0x8b, 0xeb, 0x34,
0xc5, 0x15, 0x1b, 0x57, 0x77, 0x2f, 0x48, 0xff, 0x00, 0x67, 0x24, 0xf1,
0xcf, 0x71, 0xeb, 0x84, 0xa8, 0xd2, 0x2a, 0x69, 0x2b, 0x5f, 0xb2, 0xc9,
0xd5, 0x6a, 0xaf, 0x63, 0x53, 0x9a, 0xf7, 0xfc, 0x8e, 0x7a, 0xe7, 0x56,
0xb5, 0xd5, 0xf3, 0x54, 0x7f, 0x4b, 0x9d, 0xd5, 0x4f, 0x43, 0x53, 0x64,
0xf0, 0x03, 0x6d, 0x9a, 0xf0, 0xfb, 0x6c, 0x0f, 0x8d, 0xb4, 0xab, 0x32,
0x39, 0xda, 0xb3, 0xab, 0x18, 0xdb, 0xd8, 0x6c, 0xb3, 0x5e, 0xab, 0x6a,
0xef, 0x2c, 0x8e, 0x59, 0x11, 0x62, 0x97, 0x57, 0x53, 0x09, 0x86, 0xec,
0xaa, 0x98, 0x3c, 0xf0, 0xad, 0xce, 0x1a, 0x74, 0x92, 0x92, 0x77, 0xb6,
0x34, 0x7b, 0xb5, 0xb1, 0xce, 0xd9, 0x33, 0x8d, 0xd1, 0x57, 0xe0, 0x85,
0x88, 0x6d, 0x14, 0x14, 0x55, 0x6f, 0xae, 0x6a, 0xab, 0x15, 0x32, 0xbd,
0x67, 0x22, 0x31, 0x99, 0xe6, 0xa8, 0x04, 0x4e, 0x30, 0x6e, 0x2e, 0x31,
0x3b, 0xd3, 0x12, 0x27, 0x8a, 0x90, 0x4a, 0x7c, 0x43, 0x5d, 0x1d, 0x7d,
0xc5, 0x5f, 0x0e, 0xf1, 0xc6, 0xd4, 0x62, 0x3b, 0xf1, 0x73, 0x5c, 0xf8,
0x93, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xd9
};
static const unsigned int station_logo_len = sizeof(station_logo);

52
src/resampler.c Normal file
View File

@@ -0,0 +1,52 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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 "common.h"
#include "resampler.h"
int8_t resampler_init(SRC_STATE **src_state, uint8_t channels) {
int src_error;
*src_state = src_new(CONVERTER_TYPE, channels, &src_error);
if (*src_state == NULL) {
fprintf(stderr, "Error: src_new failed: %s\n", src_strerror(src_error));
return -1;
}
return 0;
}
int8_t resample(SRC_STATE *src_state, SRC_DATA src_data, size_t *frames_generated) {
int src_error;
src_error = src_process(src_state, &src_data);
if (src_error) {
fprintf(stderr, "Error: src_process failed: %s\n", src_strerror(src_error));
return -1;
}
*frames_generated = src_data.output_frames_gen;
return 0;
}
void resampler_exit(SRC_STATE *src_state) {
src_delete(src_state);
}

25
src/resampler.h Normal file
View File

@@ -0,0 +1,25 @@
/*
* mpxgen - FM multiplex encoder with Stereo and RDS
* Copyright (C) 2019 Anthony96922
*
* 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 <samplerate.h>
#define CONVERTER_TYPE SRC_SINC_MEDIUM_QUALITY
extern int8_t resampler_init(SRC_STATE **src_state, uint8_t channels);
extern int8_t resample(SRC_STATE *src_state, SRC_DATA src_data, size_t *frames_generated);
extern void resampler_exit(SRC_STATE *src_state);

BIN
src/station_logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

27
src/supported_cmds.txt Normal file
View File

@@ -0,0 +1,27 @@
AF - Mode A only
AFCH - Mode A only
DI
MS
PI
PS
TPS
PTY
PTYN
PTYNEN
TEXT | RT1
RT1EN
TA
TP
CT
LEVEL - Not redecommended, if possible use `MPX`
RDSGEN
ECC
ECCEN
G
LIC
PIN
PINEN
RTP
RTPRUN
(Magic RDS 3 'Program' page is fully supported.)

26
src/todo.txt Normal file
View File

@@ -0,0 +1,26 @@
Current TODO for RDS encoding:
- UECP Decoding
- Dynamic PS (may not be implemented)
- Bidirectionality
ASCII commands to implement:
- Group sequence
- Short RT
ASCII Group seqeunce:
0 - Four 0As (PS x 4)
1 - 1A (ECC/LIC, PIN)
2 - 2A (RT)
A - 10A (PTYN)
E - 14A 14B (EON, yet to implement)
X - UDG1 (yet to implement, max 8 groups per udg)
Y - UDG2 (yet to implement)
R - RT+ 3A 11A (do i switch between them like ecc lic?)
F - 15A (LPS)
Max 24 items in a sequence.
Note that most of this is takes out of pira.cz's P164 RDS Encoder's manual: https://pira.cz/rds/p164man.pdf
I plan for this's commands to be similiar to PIRA32's or P164's (both are made by pira.cz)
In theory, now you can use the MiniRDS as a PIRA32 type device in Magic RDS 4 (but not everything is supported)

21
src/uecp_rds_todo.txt Normal file
View File

@@ -0,0 +1,21 @@
This aims to have the around the same options as UECP should have, this is what is not yet implemented:
- PIN (low priority, how do i decode that?)
- EON AF (very low priority)
- Linkage information (very low priority)
- (ODA skipped)
- TDC (low priority)
- EWS (how do i even implement that? can someone give docs?)
- IH (low priority)
- (Paging skipped)
- (No CT time adjustments)
- RDS on/off (low priority)
- RDS phase (might be medium, 9.5 degree steps)
- (ARI skipped)
Others:
- Encoder identification (site and encoder address)
- (PSN skipped)
- (EON skipped)
- Data set select
- Group sequence
- (other sequence related comamnds skipped)

7
src/waveforms.c Normal file

File diff suppressed because one or more lines are too long

7
src/waveforms.h Normal file
View File

@@ -0,0 +1,7 @@
/* This file was automatically generated by "generate_waveforms.py".
(C) 2014 Christophe Jacquet.
Released under the GNU GPL v3 license.
*/
extern float waveform_biphase[1120];