0
1
mirror of https://github.com/radio95-rnt/fm95.git synced 2026-02-27 11:33:54 +01:00
This commit is contained in:
2025-06-28 18:15:58 +02:00
parent aaea2fbf65
commit 26f9ccc0c7
4 changed files with 227 additions and 102 deletions

View File

@@ -18,12 +18,17 @@ file(GLOB MODULATION_FILES "modulation/*.c")
file(GLOB DSP_FILES "dsp/*.c") file(GLOB DSP_FILES "dsp/*.c")
file(GLOB IO_FILES "io/*.c") file(GLOB IO_FILES "io/*.c")
file(GLOB INIH_FILES "inih/*.c")
add_library(inih OBJECT ${INIH_FILES})
# add_library(libfm OBJECT ${LIB_FILES}) # add_library(libfm OBJECT ${LIB_FILES})
add_library(libfmfilter OBJECT ${FILTER_FILES}) add_library(libfmfilter OBJECT ${FILTER_FILES})
add_library(libfmmodulation OBJECT ${MODULATION_FILES}) add_library(libfmmodulation OBJECT ${MODULATION_FILES})
add_library(libfmdsp OBJECT ${DSP_FILES}) add_library(libfmdsp OBJECT ${DSP_FILES})
add_library(libfmio OBJECT ${IO_FILES}) add_library(libfmio OBJECT ${IO_FILES})
# Define DEBUG macro for Debug builds on libraries # Define DEBUG macro for Debug builds on libraries
if(CMAKE_BUILD_TYPE STREQUAL "Debug") if(CMAKE_BUILD_TYPE STREQUAL "Debug")
# target_compile_definitions(libfm PRIVATE DEBUG=1) # target_compile_definitions(libfm PRIVATE DEBUG=1)
@@ -33,7 +38,7 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(libfmio PRIVATE DEBUG=1) target_compile_definitions(libfmio PRIVATE DEBUG=1)
endif() endif()
set(FM_LIBS libfmfilter libfmmodulation libfmio libfmdsp pulse pulse-simple m liquid) set(FM_LIBS inih libfmfilter libfmmodulation libfmio libfmdsp pulse pulse-simple m liquid)
foreach(SRC_FILE ${SRC_FILES}) foreach(SRC_FILE ${SRC_FILES})
get_filename_component(EXEC_NAME ${SRC_FILE} NAME_WE) get_filename_component(EXEC_NAME ${SRC_FILE} NAME_WE)

View File

@@ -22,6 +22,8 @@ and one output:
## How to compile? ## How to compile?
Note that you're required also to load submodules, if you don't know what that means then ask ChatGPT
To compile you need `cmake`, `liquid-dsp` and `libpulse-dev`, if you have those then do these commands: To compile you need `cmake`, `liquid-dsp` and `libpulse-dev`, if you have those then do these commands:
```bash ```bash

131
fm95.md Normal file
View File

@@ -0,0 +1,131 @@
# FM95 Configuration File
This document describes the configuration file format for the FM95 FM transmitter software.
## File Format
The configuration file uses standard INI format with sections and key-value pairs. Comments can be added using the `#` or `;` character at the beginning of a line.
## Configuration Sections
### [fm95] - Main Settings
This section contains the core FM95 transmission parameters.
#### Audio Processing
- **`stereo`** (0 or 1, default: 0)
Enable stereo transmission. Set to 1 for stereo, 0 for mono.
- **`audio_volume`** (float)
Audio input volume multiplier. 1.0 = unity gain, values > 1.0 may cause distortion.
- **`master_volume`** (float)
Master output volume control. Affects overall transmission level.
- **`clipper_threshold`** (float)
Audio clipper threshold to prevent overmodulation. Lower values = more aggressive clipping.
- **`preemphasis`** (float, microseconds)
Pre-emphasis time constant in microseconds. Common values: 50 (Europe), 75 (North America).
#### Stereo Processing
- **`polar_stereo`** (0 or 1, default: 0)
Enable polar stereo encoding.
#### RDS (Radio Data System)
- **`rds_streams`** (integer, range: 1-4)
Number of RDS data streams to process simultaneously. More streams allow richer RDS data but require more processing power.
#### MPX (Multiplex) Settings
- **`mpx_power`** (float)
MPX signal power level. For BS412.
- **`mpx_deviation`** (float, Hz)
Maximum frequency deviation for MPX signals. Standard value is 75000 Hz for FM broadcast.
#### Calibration
- **`calibration`** (0-2, default: 0)
Enable calibration mode for testing and adjustment. (2 enables 40 hz square wave instead of the 400 hz sine)
#### Frequency Deviation
- **`deviation`** (float, Hz)
Maximum frequency deviation. Standard FM broadcast uses 75000 Hz. This setting affects the master_volume automatically.
### [devices] - Audio Device Configuration
This section specifies the audio devices to use for input and output.
- **`input`** (string, max 63 chars)
Primary audio input device
- **`output`** (string, max 63 chars)
MPX input
- **`mpx`** (string, max 63 chars)
MPX (multiplex) input device for subcarrier input
- **`rds`** (string, max 63 chars)
RDS signal input
## Example Configuration
```ini
# FM95 Configuration File
# Lines starting with # are comments
[fm95]
# Enable stereo transmission
stereo=1
# Audio levels (1.0 = unity gain)
audio_volume=0.8
master_volume=1.0
# Prevent overmodulation
clipper_threshold=0.9
# Pre-emphasis for Europe (50µs) or North America (75µs)
preemphasis=50
# Enhanced stereo processing
polar_stereo=1
# RDS configuration
rds_streams=2
# MPX settings
mpx_power=0.1
mpx_deviation=75000
# Standard FM deviation
deviation=75000
# Enable calibration mode for testing
calibration=0
```
## Important Notes
1. **RDS Streams**: Maximum of 4 RDS streams are supported. Exceeding this limit will cause an error.
2. **String Length**: Device names are limited to 63 characters maximum.
3. **Deviation Interaction**: The `deviation` setting automatically adjusts `master_volume` using the formula: `master_volume *= (deviation / 75000.0)`. Set deviation last if you want to override master_volume.
4. **Audio Levels**: Keep audio levels reasonable to prevent distortion. Start with conservative values and adjust as needed.
5. **Preemphasis**: Use 50µs for European broadcast standards, 75µs for North American standards.
## Troubleshooting
- **No audio**: Check device names with system audio tools
- **Distorted audio**: Reduce audio_volume and clipper_threshold
- **Poor stereo separation**: Enable polar_stereo and adjust mpx_power
- **RDS not working**: Verify rds device and ensure rds_streams ≤ 4

View File

@@ -1,6 +1,9 @@
#include <getopt.h> #include <getopt.h>
#include <liquid/liquid.h> #include <liquid/liquid.h>
#include <stdbool.h> #include <stdbool.h>
#include "../inih/ini.h"
#define DEFAULT_INI_PATH "/etc/fm95.conf"
#define LPF_ORDER 17 #define LPF_ORDER 17
@@ -64,6 +67,9 @@ typedef struct
float audio_volume; float audio_volume;
uint32_t sample_rate; uint32_t sample_rate;
// ini dont edit
char ini_config_path[64];
} FM95_Config; } FM95_Config;
typedef struct typedef struct
@@ -78,6 +84,10 @@ typedef struct {
char mpx[64]; char mpx[64];
char rds[64]; char rds[64];
} FM95_DeviceNames; } FM95_DeviceNames;
typedef struct {
FM95_Config* config;
FM95_DeviceNames* devices;
} FM95_SetupContext;
static void stop(int signum) { static void stop(int signum) {
(void)signum; (void)signum;
@@ -88,36 +98,9 @@ static void stop(int signum) {
void show_help(char *name) { void show_help(char *name) {
printf( printf(
"Usage: \t%s\n" "Usage: \t%s\n"
"\t-s,--stereo\tForce Stereo [default: %d]\n" "\t-c,--config\tOverride the default config path (%s)\n",
"\t-i,--input\tOverride input device [default: %s]\n" name,
"\t-o,--output\tOverride output device [default: %s]\n" DEFAULT_INI_PATH
"\t-M,--mpx\tOverride MPX input device [default: %s]\n"
"\t-r,--rds\tOverride RDS95 input device [default: %s]\n"
"\t-R,--rds_strs\tSpecifies the number of the RDS streams provided by RDS95 [default: %d]\n"
"\t-c,--clipper\tOverride the clipper threshold [default: %.2f]\n"
"\t-O,--polar\tForce Polar Stereo (does not take effect with -s0%s)\n"
"\t-e,--preemp\tOverride preemphasis [default: %.2f µs]\n"
"\t-V,--calibrate\tEnable Calibration mode [default: off, option 2 enables a 60 hz square wave instead of the 400 hz sine wave]\n"
"\t-p,--power\tSet the MPX power [default: %.1f]\n"
"\t-P,--mpx_dev\tSet the MPX deviation [default: %.1f]\n"
"\t-A,--master_vol\tSet master volume [default: %.3f]\n"
"\t-v,--audio_vol\tSet audio volume [default: %.3f]\n"
"\t-D,--deviation\tSet audio volume, but with the deviation (100%% being 75000) [default: %.1f]\n"
,name
,DEFAULT_STEREO
,INPUT_DEVICE
,OUTPUT_DEVICE
,MPX_DEVICE
,RDS_DEVICE
,DEFAULT_RDS_STREAMS
,DEFAULT_CLIPPER_THRESHOLD
,(DEFAULT_STEREO_POLAR == 1) ? ", default" : ""
,DEFAULT_PREEMPHASIS_TAU/0.000001
,DEFAULT_MPX_POWER
,DEFAULT_MPX_DEVIATION
,DEFAULT_MASTER_VOLUME
,DEFAULT_AUDIO_VOLUME
,DEFAULT_DEVIATION
); );
} }
@@ -268,84 +251,20 @@ int run_fm95(const FM95_Config config, FM95_Runtime* runtime) {
} }
int parse_arguments(int argc, char **argv, FM95_Config* config, FM95_DeviceNames* dv) { int parse_arguments(int argc, char **argv, FM95_Config* config) {
int opt; int opt;
const char *short_opt = "s::i:o:M:r:R:c:O::e:V::p:P:A:v:D:h"; const char *short_opt = "c:h";
struct option long_opt[] = struct option long_opt[] =
{ {
{"stereo", optional_argument, NULL, 's'}, {"config", required_argument, NULL, 'c'},
{"input", required_argument, NULL, 'i'},
{"output", required_argument, NULL, 'o'},
{"mpx", required_argument, NULL, 'M'},
{"rds", required_argument, NULL, 'r'},
{"rds_strs", required_argument, NULL, 'R'},
{"clipper", required_argument, NULL, 'c'},
{"polar", optional_argument, NULL, 'O'},
{"preemp", required_argument, NULL, 'e'},
{"calibrate", optional_argument, NULL, 'V'},
{"power", required_argument, NULL, 'p'},
{"mpx_dev", required_argument, NULL, 'P'},
{"master_vol", required_argument, NULL, 'A'},
{"audio_vol", required_argument, NULL, 'v'},
{"deviation", required_argument, NULL, 'D'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{0, 0, 0, 0} {0, 0, 0, 0}
}; };
while((opt = getopt_long(argc, argv, short_opt, long_opt, NULL)) != -1) { while((opt = getopt_long(argc, argv, short_opt, long_opt, NULL)) != -1) {
switch(opt) { switch(opt) {
case 's': // Stereo case 'c':
if(optarg) config->stereo = atoi(optarg); memcpy(config->ini_config_path, optarg, 63);
else config->stereo = 1;
break;
case 'i': // Input Device
memcpy(dv->input, optarg, 63);
break;
case 'o': // Output Device
memcpy(dv->output, optarg, 63);
break;;
case 'M': //MPX in
memcpy(dv->mpx, optarg, 63);
break;
case 'r': // RDS in
memcpy(dv->rds, optarg, 63);
break;
case 'R': // RDS Streams
config->rds_streams = atoi(optarg);
if(config->rds_streams > 4) {
printf("RDS Streams more than 4? Nuh uh\n");
return 1;
}
break;
case 'c': //Clipper
config->clipper_threshold = strtof(optarg, NULL);
break;
case 'O': //Polar
if(optarg) config->polar_stereo = atoi(optarg);
else config->polar_stereo = 1;
break;
case 'e': // Preemp
config->preemphasis = strtof(optarg, NULL)*1.0e-6f;
break;
case 'V': // Calibration
if(optarg) config->calibration = atoi(optarg);
else config->calibration = 1;
break;
case 'p': // Power
config->mpx_power = strtof(optarg, NULL);
break;
case 'P': // MPX deviation
config->mpx_deviation = strtof(optarg, NULL);
break;
case 'A': // Master vol
config->master_volume = strtof(optarg, NULL);
break;
case 'v': // Audio Volume
config->audio_volume = strtof(optarg, NULL);
break;
case 'D': // Deviation
config->master_volume *= (strtof(optarg, NULL)/75000.0f);
break; break;
case 'h': case 'h':
show_help(argv[0]); show_help(argv[0]);
@@ -356,6 +275,66 @@ int parse_arguments(int argc, char **argv, FM95_Config* config, FM95_DeviceNames
return 0; return 0;
} }
static int config_handler(void* user, const char* section, const char* name, const char* value) {
FM95_SetupContext* ctx = (FM95_SetupContext*)user;
FM95_Config* pconfig = ctx->config;
FM95_DeviceNames* dv = ctx->devices;
#define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0
if (MATCH("fm95", "stereo")) {
pconfig->stereo = atoi(value);
} else if (MATCH("devices", "input")) {
strncpy(dv->input, value, 63);
dv->input[63] = '\0';
} else if (MATCH("devices", "output")) {
strncpy(dv->output, value, 63);
dv->output[63] = '\0';
} else if (MATCH("devices", "mpx")) {
strncpy(dv->mpx, value, 63);
dv->mpx[63] = '\0';
} else if (MATCH("devices", "rds")) {
strncpy(dv->rds, value, 63);
dv->rds[63] = '\0';
} else if (MATCH("fm95", "rds_streams")) {
pconfig->rds_streams = atoi(value);
if(pconfig->rds_streams > 4) {
printf("RDS Streams more than 4? Nuh uh\n");
return 0;
}
} else if (MATCH("fm95", "clipper_threshold")) {
pconfig->clipper_threshold = strtof(value, NULL);
} else if (MATCH("fm95", "polar_stereo")) {
pconfig->polar_stereo = atoi(value);
} else if (MATCH("fm95", "preemphasis")) {
pconfig->preemphasis = strtof(value, NULL) * 1.0e-6f;
} else if (MATCH("fm95", "calibration")) {
pconfig->calibration = atoi(value);
} else if (MATCH("fm95", "mpx_power")) {
pconfig->mpx_power = strtof(value, NULL);
} else if (MATCH("fm95", "mpx_deviation")) {
pconfig->mpx_deviation = strtof(value, NULL);
} else if (MATCH("fm95", "master_volume")) {
pconfig->master_volume = strtof(value, NULL);
} else if (MATCH("fm95", "audio_volume")) {
pconfig->audio_volume = strtof(value, NULL);
} else if (MATCH("fm95", "deviation")) {
pconfig->master_volume *= (strtof(value, NULL) / 75000.0f);
} else {
return 0; // Unknown section/name
}
return 1;
}
int parse_config(FM95_Config* config, FM95_DeviceNames* dv) {
FM95_SetupContext ctx = {
.config = config,
.devices = dv
};
return ini_parse(config->ini_config_path, &config_handler, &ctx);
}
int setup_audio(FM95_Runtime* runtime, const FM95_DeviceNames dv_names, const FM95_Config config, bool mpx_on, bool rds_on) { int setup_audio(FM95_Runtime* runtime, const FM95_DeviceNames dv_names, const FM95_Config config, bool mpx_on, bool rds_on) {
pa_buffer_attr input_buffer_atr = { pa_buffer_attr input_buffer_atr = {
.maxlength = buffer_maxlength, .maxlength = buffer_maxlength,
@@ -428,7 +407,9 @@ int main(int argc, char **argv) {
.master_volume = DEFAULT_MASTER_VOLUME, .master_volume = DEFAULT_MASTER_VOLUME,
.audio_volume = DEFAULT_AUDIO_VOLUME, .audio_volume = DEFAULT_AUDIO_VOLUME,
.sample_rate = DEFAULT_SAMPLE_RATE .sample_rate = DEFAULT_SAMPLE_RATE,
.ini_config_path = DEFAULT_INI_PATH
}; };
FM95_DeviceNames dv_names = { FM95_DeviceNames dv_names = {
@@ -439,9 +420,15 @@ int main(int argc, char **argv) {
}; };
int err; int err;
err = parse_arguments(argc, argv, &config, &dv_names); err = parse_arguments(argc, argv, &config);
if(err != 0) return err; if(err != 0) return err;
err = parse_config(&config, &dv_names);
if(err != 0) {
printf("Could not parse the config file. (error code as return code)\n");
return err;
}
FM95_Runtime runtime; FM95_Runtime runtime;
memset(&runtime, 0, sizeof(runtime)); memset(&runtime, 0, sizeof(runtime));