diff --git a/CMakeLists.txt b/CMakeLists.txt index 9775257..fd6acb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,12 +18,17 @@ file(GLOB MODULATION_FILES "modulation/*.c") file(GLOB DSP_FILES "dsp/*.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(libfmfilter OBJECT ${FILTER_FILES}) add_library(libfmmodulation OBJECT ${MODULATION_FILES}) add_library(libfmdsp OBJECT ${DSP_FILES}) add_library(libfmio OBJECT ${IO_FILES}) + # Define DEBUG macro for Debug builds on libraries if(CMAKE_BUILD_TYPE STREQUAL "Debug") # target_compile_definitions(libfm PRIVATE DEBUG=1) @@ -33,7 +38,7 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_definitions(libfmio PRIVATE DEBUG=1) 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}) get_filename_component(EXEC_NAME ${SRC_FILE} NAME_WE) diff --git a/README.md b/README.md index 4f582d7..03441d4 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ and one output: ## 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: ```bash diff --git a/fm95.md b/fm95.md new file mode 100644 index 0000000..2dd3ed9 --- /dev/null +++ b/fm95.md @@ -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 diff --git a/src/fm95.c b/src/fm95.c index 9d6fbec..46973b8 100644 --- a/src/fm95.c +++ b/src/fm95.c @@ -1,6 +1,9 @@ #include #include #include +#include "../inih/ini.h" + +#define DEFAULT_INI_PATH "/etc/fm95.conf" #define LPF_ORDER 17 @@ -64,6 +67,9 @@ typedef struct float audio_volume; uint32_t sample_rate; + + // ini dont edit + char ini_config_path[64]; } FM95_Config; typedef struct @@ -78,6 +84,10 @@ typedef struct { char mpx[64]; char rds[64]; } FM95_DeviceNames; +typedef struct { + FM95_Config* config; + FM95_DeviceNames* devices; +} FM95_SetupContext; static void stop(int signum) { (void)signum; @@ -88,36 +98,9 @@ static void stop(int signum) { void show_help(char *name) { printf( "Usage: \t%s\n" - "\t-s,--stereo\tForce Stereo [default: %d]\n" - "\t-i,--input\tOverride input device [default: %s]\n" - "\t-o,--output\tOverride output device [default: %s]\n" - "\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 + "\t-c,--config\tOverride the default config path (%s)\n", + name, + DEFAULT_INI_PATH ); } @@ -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; - 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[] = { - {"stereo", optional_argument, NULL, 's'}, - {"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'}, - + {"config", required_argument, NULL, 'c'}, {"help", no_argument, NULL, 'h'}, {0, 0, 0, 0} }; while((opt = getopt_long(argc, argv, short_opt, long_opt, NULL)) != -1) { switch(opt) { - case 's': // Stereo - if(optarg) config->stereo = atoi(optarg); - 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); + case 'c': + memcpy(config->ini_config_path, optarg, 63); break; case 'h': show_help(argv[0]); @@ -356,6 +275,66 @@ int parse_arguments(int argc, char **argv, FM95_Config* config, FM95_DeviceNames 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) { pa_buffer_attr input_buffer_atr = { .maxlength = buffer_maxlength, @@ -428,7 +407,9 @@ int main(int argc, char **argv) { .master_volume = DEFAULT_MASTER_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 = { @@ -439,9 +420,15 @@ int main(int argc, char **argv) { }; int err; - err = parse_arguments(argc, argv, &config, &dv_names); + err = parse_arguments(argc, argv, &config); 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; memset(&runtime, 0, sizeof(runtime));