0
1
mirror of https://github.com/radio95-rnt/fm95.git synced 2026-02-26 19:23:51 +01:00
Files
fm95/src/chimer95.c
2025-09-27 15:23:52 +02:00

316 lines
8.5 KiB
C

#include <stdio.h>
#include <getopt.h>
#include <time.h>
#include <signal.h>
#include <string.h>
#include <stdbool.h>
#include "../inih/ini.h"
#define DEFAULT_CONFIG_PATH "/etc/chimer95.conf"
#define buffer_maxlength 2048
#define buffer_tlength_fragsize 2048
#define buffer_prebuf 0
#include "../dsp/oscillator.h"
#define DEFAULT_FREQ 1000.0f
#define DEFAULT_SAMPLE_RATE 8000
#define OUTPUT_DEVICE "FM_MPX"
#define BUFFER_SIZE 1024
#include "../io/audio.h"
#define DEFAULT_MASTER_VOLUME 0.5f
#define DEFAULT_OFFSET 0
#define PIP_DURATION 100
#define PIP_PAUSE 900
#define BEEP_DURATION 500
#define SEQ_NONE 0
#define SEQ_29_56 1
#define SEQ_59_55 2
#define SEQ_TEST_HOUR 3
volatile sig_atomic_t to_run = 1;
volatile sig_atomic_t playing_sequence = 0;
volatile int sequence_position = 0;
volatile int sequence_type = SEQ_NONE;
volatile time_t last_sequence_time = 0;
static void stop(int signum) {
(void)signum;
printf("\nReceived stop signal.\n");
to_run = 0;
}
void show_help(char *name) {
printf(
"Usage:\t%s\n"
"\t-c,--config\tSets the config path [default: %s]\n"
,name
,DEFAULT_CONFIG_PATH
);
}
void generate_signal(float *output, Oscillator *osc, float volume, int *elapsed_samples, int total_samples, int pip_samples, int pause_samples, int beep_samples, int num_pips) {
for (int i = 0; i < BUFFER_SIZE; i++) {
if (*elapsed_samples >= total_samples) {
output[i] = 0;
playing_sequence = 0;
} else {
int cycle_position = *elapsed_samples;
int pip_cycle = pip_samples + pause_samples;
if (cycle_position < num_pips * pip_cycle) {
if ((cycle_position % pip_cycle) < pip_samples) output[i] = get_oscillator_sin_sample(osc) * volume;
else output[i] = 0;
} else if (cycle_position < num_pips * pip_cycle + beep_samples) output[i] = get_oscillator_sin_sample(osc) * volume;
else output[i] = 0;
(*elapsed_samples)++;
}
}
}
int check_time_for_sequence(int test_mode, int16_t offset) {
static time_t last_check = 0;
static int last_minute = -1;
time_t now = time(NULL);
if (now == last_check) return SEQ_NONE;
last_check = now;
struct tm *utc_time = gmtime(&now);
int minute = utc_time->tm_min;
int second = utc_time->tm_sec;
if (difftime(now, last_sequence_time) < 1.0) return SEQ_NONE;
last_sequence_time = now;
if (minute == 29 && second == (56 + offset)) return SEQ_29_56;
if (minute == 59 && second == (55 + offset)) return SEQ_59_55;
if (test_mode && second == (55 + offset) && minute != last_minute) {
last_minute = minute;
return SEQ_TEST_HOUR;
}
return SEQ_NONE;
}
typedef struct
{
float master_volume;
float freq;
uint32_t sample_rate;
int16_t offset;
bool test_mode;
char ini_config_path[64];
} Chimer95_Config;
typedef struct
{
PulseOutputDevice output_device;
} Chimer95_Runtime;
typedef struct {
char output[64];
} Chimer95_DeviceNames;
typedef struct {
Chimer95_Config* config;
Chimer95_DeviceNames* devices;
} Chimer95_SetupContext;
int run_chimer95(const Chimer95_Config config, Chimer95_Runtime* runtime) {
int pulse_error;
Oscillator osc;
init_oscillator(&osc, config.freq, config.sample_rate);
float output[BUFFER_SIZE];
int pip_samples = (int)((PIP_DURATION / 1000.0) * config.sample_rate);
int pause_samples = (int)((PIP_PAUSE / 1000.0) * config.sample_rate);
int beep_samples = (int)((BEEP_DURATION / 1000.0) * config.sample_rate);
int samples_29_56 = 4 * (pip_samples + pause_samples) + beep_samples;
int samples_59_55 = 5 * (pip_samples + pause_samples) + beep_samples;
printf("Ready to play time signals.\n");
printf("Will trigger at XX:29:%02d and XX:59:%02d\n", 56+config.offset, 55+config.offset);
if (config.test_mode) printf("TEST MODE: Will also play full hour signal at the end of every minute\n");
int elapsed_samples = 0;
int total_sequence_samples = 0;
int sequence_completed = 0;
while (to_run) {
if (!playing_sequence) {
int new_sequence = check_time_for_sequence(config.test_mode, config.offset);
if (new_sequence != SEQ_NONE) {
playing_sequence = 1;
sequence_type = new_sequence;
elapsed_samples = 0;
sequence_completed = 0;
if (new_sequence == SEQ_29_56) total_sequence_samples = samples_29_56;
else total_sequence_samples = samples_59_55;
memset(output, 0, sizeof(output));
} else {
static int idle_counter = 0;
if (idle_counter++ % 10 == 0) {
memset(output, 0, sizeof(output));
if((pulse_error = write_PulseOutputDevice(&runtime->output_device, output, sizeof(output)))) {
fprintf(stderr, "Error writing to output device: %s\n", pa_strerror(pulse_error));
to_run = 0;
break;
}
}
struct timespec ts = {0, 5000000}; // 5ms sleep
nanosleep(&ts, NULL);
continue;
}
}
int num_pips = (sequence_type == SEQ_29_56) ? 4 : 5;
generate_signal(output, &osc, config.master_volume,
&elapsed_samples, total_sequence_samples,
pip_samples, pause_samples, beep_samples, num_pips);
if (!playing_sequence && !sequence_completed) sequence_completed = 1;
if((pulse_error = write_PulseOutputDevice(&runtime->output_device, output, sizeof(output)))) {
fprintf(stderr, "Error writing to output device: %s\n", pa_strerror(pulse_error));
to_run = 0;
break;
}
}
return 0;
}
int parse_arguments(int argc, char **argv, Chimer95_Config* config) {
int opt;
const char *short_opt = "c:h";
struct option long_opt[] =
{
{"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 'c':
memcpy(config->ini_config_path, optarg, 63);
break;
case 'h':
show_help(argv[0]);
return 1;
}
}
return 0;
}
static int config_handler(void* user, const char* section, const char* name, const char* value) {
Chimer95_SetupContext* ctx = (Chimer95_SetupContext*)user;
Chimer95_Config* pconfig = ctx->config;
Chimer95_DeviceNames* dv = ctx->devices;
#define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0
if (MATCH("chimer95", "freq")) {
pconfig->freq = strtof(value, NULL);
} else if(MATCH("chimer95", "volume")) {
pconfig->master_volume = strtof(value, NULL);
} else if(MATCH("chimer95", "offset")) {
pconfig->offset = strtoul(value, NULL, 10);
} else if(MATCH("chimer95", "sample_rate")) {
pconfig->sample_rate = atoi(value);
} else if(MATCH("chimer95", "test_mode")) {
pconfig->test_mode = atoi(value);
} else if(MATCH("devices", "chimer")) {
strncpy(dv->output, value, 63);
dv->output[63] = '\0';
} else {
return 0; // Unknown section/name
}
return 1;
}
int parse_config(Chimer95_Config* config, Chimer95_DeviceNames* dv) {
Chimer95_SetupContext ctx = {
.config = config,
.devices = dv
};
return ini_parse(config->ini_config_path, &config_handler, &ctx);
}
int main(int argc, char **argv) {
printf("chimer95 (GTS time signal encoder by radio95) version 1.3\n");
Chimer95_Config config = {
.master_volume = DEFAULT_MASTER_VOLUME,
.freq = DEFAULT_FREQ,
.sample_rate = DEFAULT_SAMPLE_RATE,
.offset = DEFAULT_OFFSET,
.test_mode = 0,
.ini_config_path = DEFAULT_CONFIG_PATH
};
int err;
err = parse_arguments(argc, argv, &config);
if(err != 0) return err;
Chimer95_DeviceNames dv_names = {
.output = OUTPUT_DEVICE,
};
err = parse_config(&config, &dv_names);
if(err != 0) {
printf("Could not parse the config file. (error code as return code)\n");
return err;
}
printf("Configuration:\n");
printf("\tOutput device: %s\n", dv_names.output);
printf("\tFrequency: %.1f Hz\n", config.freq);
printf("\tSample rate: %d Hz\n", config.sample_rate);
printf("\tVolume: %.2f\n", config.master_volume);
printf("\tTime offset: %d seconds\n", config.offset);
printf("\tTest mode: %s\n", config.test_mode ? "Enabled" : "Disabled");
// Setup PulseAudio
pa_buffer_attr output_buffer_atr = {
.maxlength = buffer_maxlength,
.tlength = buffer_tlength_fragsize,
.prebuf = buffer_prebuf
};
Chimer95_Runtime runtime;
memset(&runtime, 0, sizeof(runtime));
printf("Connecting to output device... (%s)\n", dv_names.output);
int pulse_error = init_PulseOutputDevice(&runtime.output_device, config.sample_rate, 1, "chimer95", "Main Audio Output", dv_names.output, &output_buffer_atr, PA_SAMPLE_FLOAT32NE);
if (pulse_error) {
fprintf(stderr, "Error: cannot open output device: %s\n", pa_strerror(pulse_error));
return 1;
}
signal(SIGINT, stop);
signal(SIGTERM, stop);
int ret = run_chimer95(config, &runtime);
printf("Cleaning up...\n");
free_PulseDevice(&runtime.output_device);
return ret;
}