#include #include #include #include #include #include #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; }