diff --git a/artifacts/lkma-193a/ground-truth/v21/inbound.wav b/artifacts/lkma-193a/ground-truth/v21/inbound.wav new file mode 100644 index 0000000..bd9d02c Binary files /dev/null and b/artifacts/lkma-193a/ground-truth/v21/inbound.wav differ diff --git a/artifacts/lkma-193a/ground-truth/v21/outbound.wav b/artifacts/lkma-193a/ground-truth/v21/outbound.wav new file mode 100644 index 0000000..c8b950c Binary files /dev/null and b/artifacts/lkma-193a/ground-truth/v21/outbound.wav differ diff --git a/artifacts/lkma-193a/ground-truth/v21/report.json b/artifacts/lkma-193a/ground-truth/v21/report.json new file mode 100644 index 0000000..0154c34 --- /dev/null +++ b/artifacts/lkma-193a/ground-truth/v21/report.json @@ -0,0 +1,8 @@ +{ + "source": "synthetic-spandsp", + "note": "Generated by spandsp FSK TX - not a real hardware capture. Replace with lab recording when USB FXO rig is available.", + "sampleRate": 8000, + "channels": 1, + "bitDepth": 16, + "durationSeconds": 30 +} diff --git a/artifacts/lkma-193a/ground-truth/v22bis/inbound.wav b/artifacts/lkma-193a/ground-truth/v22bis/inbound.wav new file mode 100644 index 0000000..c8b950c Binary files /dev/null and b/artifacts/lkma-193a/ground-truth/v22bis/inbound.wav differ diff --git a/artifacts/lkma-193a/ground-truth/v22bis/outbound.wav b/artifacts/lkma-193a/ground-truth/v22bis/outbound.wav new file mode 100644 index 0000000..c8b950c Binary files /dev/null and b/artifacts/lkma-193a/ground-truth/v22bis/outbound.wav differ diff --git a/artifacts/lkma-193a/ground-truth/v22bis/report.json b/artifacts/lkma-193a/ground-truth/v22bis/report.json new file mode 100644 index 0000000..dd67a29 --- /dev/null +++ b/artifacts/lkma-193a/ground-truth/v22bis/report.json @@ -0,0 +1,8 @@ +{ + "source": "synthetic-spandsp", + "note": "V.22bis placeholder silence. Generated by the synthetic spandsp artifact generator; replace with lab recording when USB FXO rig is available.", + "sampleRate": 8000, + "channels": 1, + "bitDepth": 16, + "durationSeconds": 30 +} diff --git a/artifacts/lkma-193a/groundtruth-v21.wav b/artifacts/lkma-193a/groundtruth-v21.wav new file mode 100644 index 0000000..bd9d02c Binary files /dev/null and b/artifacts/lkma-193a/groundtruth-v21.wav differ diff --git a/tools/generate-lkma-193a-groundtruth.c b/tools/generate-lkma-193a-groundtruth.c new file mode 100644 index 0000000..1f66b68 --- /dev/null +++ b/tools/generate-lkma-193a-groundtruth.c @@ -0,0 +1,257 @@ +#include +#include +#include +#include +#include +#include + +#include + +#define SAMPLE_RATE 8000 +#define CHANNELS 1 +#define BITS_PER_SAMPLE 16 +#define DURATION_SECONDS 30 +#define TOTAL_SAMPLES (SAMPLE_RATE * DURATION_SECONDS) +#define BLOCK_SAMPLES 160 +#define V21_MARK_BITS 150 + +typedef struct { + int bit_index; +} v21_bits_t; + +static int make_dirs(const char *path); +static int write_wav_header(FILE *file, uint32_t samples); +static int write_silence_wav(const char *path); +static int write_v21_wav(const char *path); +static int copy_file(const char *from, const char *to); +static int write_report(const char *path, const char *note); +static int get_v21_bit(void *user_data); + +int main(void) { + if (make_dirs("artifacts/lkma-193a/ground-truth/v21") != 0 || + make_dirs("artifacts/lkma-193a/ground-truth/v22bis") != 0) { + return 1; + } + + if (write_v21_wav("artifacts/lkma-193a/groundtruth-v21.wav") != 0 || + copy_file("artifacts/lkma-193a/groundtruth-v21.wav", + "artifacts/lkma-193a/ground-truth/v21/inbound.wav") != 0 || + write_silence_wav("artifacts/lkma-193a/ground-truth/v21/outbound.wav") != 0 || + write_report( + "artifacts/lkma-193a/ground-truth/v21/report.json", + "Generated by spandsp FSK TX - not a real hardware capture. Replace with lab recording when USB FXO rig is available." + ) != 0 || + write_silence_wav("artifacts/lkma-193a/ground-truth/v22bis/inbound.wav") != 0 || + write_silence_wav("artifacts/lkma-193a/ground-truth/v22bis/outbound.wav") != 0 || + write_report( + "artifacts/lkma-193a/ground-truth/v22bis/report.json", + "V.22bis placeholder silence. Generated by the synthetic spandsp artifact generator; replace with lab recording when USB FXO rig is available." + ) != 0) { + return 1; + } + + return 0; +} + +static int make_dirs(const char *path) { + char partial[512]; + size_t length = strlen(path); + + if (length >= sizeof(partial)) { + fprintf(stderr, "path too long: %s\n", path); + return -1; + } + + for (size_t index = 0; index <= length; index++) { + partial[index] = path[index]; + if (path[index] != '/' && path[index] != '\0') { + continue; + } + + partial[index] = '\0'; + if (partial[0] != '\0' && mkdir(partial, 0775) != 0 && errno != EEXIST) { + fprintf(stderr, "mkdir %s: %s\n", partial, strerror(errno)); + return -1; + } + partial[index] = path[index]; + } + return 0; +} + +static int write_u16_le(FILE *file, uint16_t value) { + uint8_t bytes[2] = { + (uint8_t) (value & 0xff), + (uint8_t) ((value >> 8) & 0xff) + }; + return fwrite(bytes, 1, sizeof(bytes), file) == sizeof(bytes) ? 0 : -1; +} + +static int write_u32_le(FILE *file, uint32_t value) { + uint8_t bytes[4] = { + (uint8_t) (value & 0xff), + (uint8_t) ((value >> 8) & 0xff), + (uint8_t) ((value >> 16) & 0xff), + (uint8_t) ((value >> 24) & 0xff) + }; + return fwrite(bytes, 1, sizeof(bytes), file) == sizeof(bytes) ? 0 : -1; +} + +static int write_i16_le(FILE *file, int16_t value) { + return write_u16_le(file, (uint16_t) value); +} + +static int write_wav_header(FILE *file, uint32_t samples) { + const uint16_t block_align = CHANNELS * (BITS_PER_SAMPLE / 8); + const uint32_t byte_rate = SAMPLE_RATE * block_align; + const uint32_t data_bytes = samples * block_align; + + return fwrite("RIFF", 1, 4, file) == 4 && + write_u32_le(file, 36 + data_bytes) == 0 && + fwrite("WAVEfmt ", 1, 8, file) == 8 && + write_u32_le(file, 16) == 0 && + write_u16_le(file, 1) == 0 && + write_u16_le(file, CHANNELS) == 0 && + write_u32_le(file, SAMPLE_RATE) == 0 && + write_u32_le(file, byte_rate) == 0 && + write_u16_le(file, block_align) == 0 && + write_u16_le(file, BITS_PER_SAMPLE) == 0 && + fwrite("data", 1, 4, file) == 4 && + write_u32_le(file, data_bytes) == 0 ? 0 : -1; +} + +static int write_silence_wav(const char *path) { + FILE *file = fopen(path, "wb"); + if (!file) { + fprintf(stderr, "open %s: %s\n", path, strerror(errno)); + return -1; + } + if (write_wav_header(file, TOTAL_SAMPLES) != 0) { + fclose(file); + fprintf(stderr, "write header %s failed\n", path); + return -1; + } + for (int sample = 0; sample < TOTAL_SAMPLES; sample++) { + if (write_i16_le(file, 0) != 0) { + fclose(file); + fprintf(stderr, "write sample %s failed\n", path); + return -1; + } + } + return fclose(file); +} + +static int write_v21_wav(const char *path) { + FILE *file = fopen(path, "wb"); + if (!file) { + fprintf(stderr, "open %s: %s\n", path, strerror(errno)); + return -1; + } + + if (write_wav_header(file, TOTAL_SAMPLES) != 0) { + fclose(file); + fprintf(stderr, "write header %s failed\n", path); + return -1; + } + + v21_bits_t bits = {0}; + fsk_tx_state_t *tx = fsk_tx_init(NULL, &preset_fsk_specs[FSK_V21CH1], get_v21_bit, &bits); + if (!tx) { + fclose(file); + fprintf(stderr, "fsk_tx_init failed\n"); + return -1; + } + + int16_t samples[BLOCK_SAMPLES]; + int remaining = TOTAL_SAMPLES; + while (remaining > 0) { + int count = remaining > BLOCK_SAMPLES ? BLOCK_SAMPLES : remaining; + int generated = fsk_tx(tx, samples, count); + if (generated != count) { + fsk_tx_free(tx); + fclose(file); + fprintf(stderr, "fsk_tx generated %d of %d samples\n", generated, count); + return -1; + } + for (int index = 0; index < generated; index++) { + if (write_i16_le(file, samples[index]) != 0) { + fsk_tx_free(tx); + fclose(file); + fprintf(stderr, "write sample %s failed\n", path); + return -1; + } + } + remaining -= generated; + } + + fsk_tx_free(tx); + return fclose(file); +} + +static int copy_file(const char *from, const char *to) { + FILE *source = fopen(from, "rb"); + if (!source) { + fprintf(stderr, "open %s: %s\n", from, strerror(errno)); + return -1; + } + FILE *target = fopen(to, "wb"); + if (!target) { + fclose(source); + fprintf(stderr, "open %s: %s\n", to, strerror(errno)); + return -1; + } + + uint8_t buffer[8192]; + size_t count; + while ((count = fread(buffer, 1, sizeof(buffer), source)) > 0) { + if (fwrite(buffer, 1, count, target) != count) { + fclose(source); + fclose(target); + fprintf(stderr, "write %s failed\n", to); + return -1; + } + } + + int read_error = ferror(source); + int close_source = fclose(source); + int close_target = fclose(target); + return read_error || close_source || close_target ? -1 : 0; +} + +static int write_report(const char *path, const char *note) { + FILE *file = fopen(path, "wb"); + if (!file) { + fprintf(stderr, "open %s: %s\n", path, strerror(errno)); + return -1; + } + + int ok = fprintf( + file, + "{\n" + " \"source\": \"synthetic-spandsp\",\n" + " \"note\": \"%s\",\n" + " \"sampleRate\": 8000,\n" + " \"channels\": 1,\n" + " \"bitDepth\": 16,\n" + " \"durationSeconds\": 30\n" + "}\n", + note + ) > 0; + + return fclose(file) == 0 && ok ? 0 : -1; +} + +static int get_v21_bit(void *user_data) { + static const uint8_t pattern[] = {0xff, 0x01, 0x7e}; + v21_bits_t *bits = (v21_bits_t *) user_data; + int bit; + + if (bits->bit_index < V21_MARK_BITS) { + bit = 1; + } else { + int payload_bit = bits->bit_index - V21_MARK_BITS; + uint8_t byte = pattern[(payload_bit / 8) % (int) (sizeof(pattern) / sizeof(pattern[0]))]; + bit = (byte >> (payload_bit % 8)) & 1; + } + bits->bit_index++; + return bit; +}