[feat](trx-rs): add ft8 decoder
Co-authored-by: Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
Vendored
+393
@@ -0,0 +1,393 @@
|
||||
#define _POSIX_C_SOURCE 199309L
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <ft8/decode.h>
|
||||
#include <ft8/encode.h>
|
||||
#include <ft8/message.h>
|
||||
|
||||
#include <common/common.h>
|
||||
#include <common/wave.h>
|
||||
#include <common/monitor.h>
|
||||
#include <common/audio.h>
|
||||
|
||||
#define LOG_LEVEL LOG_INFO
|
||||
#include <ft8/debug.h>
|
||||
|
||||
const int kMin_score = 10; // Minimum sync score threshold for candidates
|
||||
const int kMax_candidates = 140;
|
||||
const int kLDPC_iterations = 25;
|
||||
|
||||
const int kMax_decoded_messages = 50;
|
||||
|
||||
const int kFreq_osr = 2; // Frequency oversampling rate (bin subdivision)
|
||||
const int kTime_osr = 2; // Time oversampling rate (symbol subdivision)
|
||||
|
||||
void usage(const char* error_msg)
|
||||
{
|
||||
if (error_msg != NULL)
|
||||
{
|
||||
fprintf(stderr, "ERROR: %s\n", error_msg);
|
||||
}
|
||||
fprintf(stderr, "Usage: decode_ft8 [-list|([-ft4] [INPUT|-dev DEVICE])]\n\n");
|
||||
fprintf(stderr, "Decode a 15-second (or slighly shorter) WAV file.\n");
|
||||
}
|
||||
|
||||
#define CALLSIGN_HASHTABLE_SIZE 256
|
||||
|
||||
static struct
|
||||
{
|
||||
char callsign[12]; ///> Up to 11 symbols of callsign + trailing zeros (always filled)
|
||||
uint32_t hash; ///> 8 MSBs contain the age of callsign; 22 LSBs contain hash value
|
||||
} callsign_hashtable[CALLSIGN_HASHTABLE_SIZE];
|
||||
|
||||
static int callsign_hashtable_size;
|
||||
|
||||
void hashtable_init(void)
|
||||
{
|
||||
callsign_hashtable_size = 0;
|
||||
memset(callsign_hashtable, 0, sizeof(callsign_hashtable));
|
||||
}
|
||||
|
||||
void hashtable_cleanup(uint8_t max_age)
|
||||
{
|
||||
for (int idx_hash = 0; idx_hash < CALLSIGN_HASHTABLE_SIZE; ++idx_hash)
|
||||
{
|
||||
if (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||
{
|
||||
uint8_t age = (uint8_t)(callsign_hashtable[idx_hash].hash >> 24);
|
||||
if (age > max_age)
|
||||
{
|
||||
LOG(LOG_INFO, "Removing [%s] from hash table, age = %d\n", callsign_hashtable[idx_hash].callsign, age);
|
||||
// free the hash entry
|
||||
callsign_hashtable[idx_hash].callsign[0] = '\0';
|
||||
callsign_hashtable[idx_hash].hash = 0;
|
||||
callsign_hashtable_size--;
|
||||
}
|
||||
else
|
||||
{
|
||||
// increase callsign age
|
||||
callsign_hashtable[idx_hash].hash = (((uint32_t)age + 1u) << 24) | (callsign_hashtable[idx_hash].hash & 0x3FFFFFu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void hashtable_add(const char* callsign, uint32_t hash)
|
||||
{
|
||||
uint16_t hash10 = (hash >> 12) & 0x3FFu;
|
||||
int idx_hash = (hash10 * 23) % CALLSIGN_HASHTABLE_SIZE;
|
||||
while (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||
{
|
||||
if (((callsign_hashtable[idx_hash].hash & 0x3FFFFFu) == hash) && (0 == strcmp(callsign_hashtable[idx_hash].callsign, callsign)))
|
||||
{
|
||||
// reset age
|
||||
callsign_hashtable[idx_hash].hash &= 0x3FFFFFu;
|
||||
LOG(LOG_DEBUG, "Found a duplicate [%s]\n", callsign);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LOG_DEBUG, "Hash table clash!\n");
|
||||
// Move on to check the next entry in hash table
|
||||
idx_hash = (idx_hash + 1) % CALLSIGN_HASHTABLE_SIZE;
|
||||
}
|
||||
}
|
||||
callsign_hashtable_size++;
|
||||
strncpy(callsign_hashtable[idx_hash].callsign, callsign, 11);
|
||||
callsign_hashtable[idx_hash].callsign[11] = '\0';
|
||||
callsign_hashtable[idx_hash].hash = hash;
|
||||
}
|
||||
|
||||
bool hashtable_lookup(ftx_callsign_hash_type_t hash_type, uint32_t hash, char* callsign)
|
||||
{
|
||||
uint8_t hash_shift = (hash_type == FTX_CALLSIGN_HASH_10_BITS) ? 12 : (hash_type == FTX_CALLSIGN_HASH_12_BITS ? 10 : 0);
|
||||
uint16_t hash10 = (hash >> (12 - hash_shift)) & 0x3FFu;
|
||||
int idx_hash = (hash10 * 23) % CALLSIGN_HASHTABLE_SIZE;
|
||||
while (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||
{
|
||||
if (((callsign_hashtable[idx_hash].hash & 0x3FFFFFu) >> hash_shift) == hash)
|
||||
{
|
||||
strcpy(callsign, callsign_hashtable[idx_hash].callsign);
|
||||
return true;
|
||||
}
|
||||
// Move on to check the next entry in hash table
|
||||
idx_hash = (idx_hash + 1) % CALLSIGN_HASHTABLE_SIZE;
|
||||
}
|
||||
callsign[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
ftx_callsign_hash_interface_t hash_if = {
|
||||
.lookup_hash = hashtable_lookup,
|
||||
.save_hash = hashtable_add
|
||||
};
|
||||
|
||||
void decode(const monitor_t* mon, struct tm* tm_slot_start)
|
||||
{
|
||||
const ftx_waterfall_t* wf = &mon->wf;
|
||||
// Find top candidates by Costas sync score and localize them in time and frequency
|
||||
ftx_candidate_t candidate_list[kMax_candidates];
|
||||
int num_candidates = ftx_find_candidates(wf, kMax_candidates, candidate_list, kMin_score);
|
||||
|
||||
// Hash table for decoded messages (to check for duplicates)
|
||||
int num_decoded = 0;
|
||||
ftx_message_t decoded[kMax_decoded_messages];
|
||||
ftx_message_t* decoded_hashtable[kMax_decoded_messages];
|
||||
|
||||
// Initialize hash table pointers
|
||||
for (int i = 0; i < kMax_decoded_messages; ++i)
|
||||
{
|
||||
decoded_hashtable[i] = NULL;
|
||||
}
|
||||
|
||||
// Go over candidates and attempt to decode messages
|
||||
for (int idx = 0; idx < num_candidates; ++idx)
|
||||
{
|
||||
const ftx_candidate_t* cand = &candidate_list[idx];
|
||||
|
||||
float freq_hz = (mon->min_bin + cand->freq_offset + (float)cand->freq_sub / wf->freq_osr) / mon->symbol_period;
|
||||
float time_sec = (cand->time_offset + (float)cand->time_sub / wf->time_osr) * mon->symbol_period;
|
||||
|
||||
#ifdef WATERFALL_USE_PHASE
|
||||
// int resynth_len = 12000 * 16;
|
||||
// float resynth_signal[resynth_len];
|
||||
// for (int pos = 0; pos < resynth_len; ++pos)
|
||||
// {
|
||||
// resynth_signal[pos] = 0;
|
||||
// }
|
||||
// monitor_resynth(mon, cand, resynth_signal);
|
||||
// char resynth_path[80];
|
||||
// sprintf(resynth_path, "resynth_%04f_%02.1f.wav", freq_hz, time_sec);
|
||||
// save_wav(resynth_signal, resynth_len, 12000, resynth_path);
|
||||
#endif
|
||||
|
||||
ftx_message_t message;
|
||||
ftx_decode_status_t status;
|
||||
if (!ftx_decode_candidate(wf, cand, kLDPC_iterations, &message, &status))
|
||||
{
|
||||
if (status.ldpc_errors > 0)
|
||||
{
|
||||
LOG(LOG_DEBUG, "LDPC decode: %d errors\n", status.ldpc_errors);
|
||||
}
|
||||
else if (status.crc_calculated != status.crc_extracted)
|
||||
{
|
||||
LOG(LOG_DEBUG, "CRC mismatch!\n");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG(LOG_DEBUG, "Checking hash table for %4.1fs / %4.1fHz [%d]...\n", time_sec, freq_hz, cand->score);
|
||||
int idx_hash = message.hash % kMax_decoded_messages;
|
||||
bool found_empty_slot = false;
|
||||
bool found_duplicate = false;
|
||||
do
|
||||
{
|
||||
if (decoded_hashtable[idx_hash] == NULL)
|
||||
{
|
||||
LOG(LOG_DEBUG, "Found an empty slot\n");
|
||||
found_empty_slot = true;
|
||||
}
|
||||
else if ((decoded_hashtable[idx_hash]->hash == message.hash) && (0 == memcmp(decoded_hashtable[idx_hash]->payload, message.payload, sizeof(message.payload))))
|
||||
{
|
||||
LOG(LOG_DEBUG, "Found a duplicate!\n");
|
||||
found_duplicate = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LOG_DEBUG, "Hash table clash!\n");
|
||||
// Move on to check the next entry in hash table
|
||||
idx_hash = (idx_hash + 1) % kMax_decoded_messages;
|
||||
}
|
||||
} while (!found_empty_slot && !found_duplicate);
|
||||
|
||||
if (found_empty_slot)
|
||||
{
|
||||
// Fill the empty hashtable slot
|
||||
memcpy(&decoded[idx_hash], &message, sizeof(message));
|
||||
decoded_hashtable[idx_hash] = &decoded[idx_hash];
|
||||
++num_decoded;
|
||||
|
||||
char text[FTX_MAX_MESSAGE_LENGTH];
|
||||
ftx_message_offsets_t offsets;
|
||||
ftx_message_rc_t unpack_status = ftx_message_decode(&message, &hash_if, text, &offsets);
|
||||
if (unpack_status != FTX_MESSAGE_RC_OK)
|
||||
{
|
||||
snprintf(text, sizeof(text), "Error [%d] while unpacking!", (int)unpack_status);
|
||||
}
|
||||
|
||||
// Fake WSJT-X-like output for now
|
||||
float snr = cand->score * 0.5f; // TODO: compute better approximation of SNR
|
||||
printf("%02d%02d%02d %+05.1f %+4.2f %4.0f ~ %s\n",
|
||||
tm_slot_start->tm_hour, tm_slot_start->tm_min, tm_slot_start->tm_sec,
|
||||
snr, time_sec, freq_hz, text);
|
||||
}
|
||||
}
|
||||
LOG(LOG_INFO, "Decoded %d messages, callsign hashtable size %d\n", num_decoded, callsign_hashtable_size);
|
||||
hashtable_cleanup(10);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
// Accepted arguments
|
||||
const char* wav_path = NULL;
|
||||
const char* dev_name = NULL;
|
||||
ftx_protocol_t protocol = FTX_PROTOCOL_FT8;
|
||||
float time_shift = 0.8;
|
||||
|
||||
// Parse arguments one by one
|
||||
int arg_idx = 1;
|
||||
while (arg_idx < argc)
|
||||
{
|
||||
// Check if the current argument is an option (-xxx)
|
||||
if (argv[arg_idx][0] == '-')
|
||||
{
|
||||
// Check agaist valid options
|
||||
if (0 == strcmp(argv[arg_idx], "-ft4"))
|
||||
{
|
||||
protocol = FTX_PROTOCOL_FT4;
|
||||
}
|
||||
else if (0 == strcmp(argv[arg_idx], "-list"))
|
||||
{
|
||||
audio_init();
|
||||
audio_list();
|
||||
return 0;
|
||||
}
|
||||
else if (0 == strcmp(argv[arg_idx], "-dev"))
|
||||
{
|
||||
if (arg_idx + 1 < argc)
|
||||
{
|
||||
++arg_idx;
|
||||
dev_name = argv[arg_idx];
|
||||
}
|
||||
else
|
||||
{
|
||||
usage("Expected an audio device name after -dev");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
usage("Unknown command line option");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (wav_path == NULL)
|
||||
{
|
||||
wav_path = argv[arg_idx];
|
||||
}
|
||||
else
|
||||
{
|
||||
usage("Multiple positional arguments");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
++arg_idx;
|
||||
}
|
||||
// Check if all mandatory arguments have been received
|
||||
if (wav_path == NULL && dev_name == NULL)
|
||||
{
|
||||
usage("Expected either INPUT file path or DEVICE name");
|
||||
return -1;
|
||||
}
|
||||
|
||||
float slot_period = ((protocol == FTX_PROTOCOL_FT8) ? FT8_SLOT_TIME : FT4_SLOT_TIME);
|
||||
int sample_rate = 12000;
|
||||
int num_samples = slot_period * sample_rate;
|
||||
float signal[num_samples];
|
||||
bool is_live = false;
|
||||
|
||||
if (wav_path != NULL)
|
||||
{
|
||||
int rc = load_wav(signal, &num_samples, &sample_rate, wav_path);
|
||||
if (rc < 0)
|
||||
{
|
||||
LOG(LOG_ERROR, "ERROR: cannot load wave file %s\n", wav_path);
|
||||
return -1;
|
||||
}
|
||||
LOG(LOG_INFO, "Sample rate %d Hz, %d samples, %.3f seconds\n", sample_rate, num_samples, (double)num_samples / sample_rate);
|
||||
}
|
||||
else if (dev_name != NULL)
|
||||
{
|
||||
audio_init();
|
||||
audio_open(dev_name);
|
||||
num_samples = (slot_period - 0.4f) * sample_rate;
|
||||
is_live = true;
|
||||
}
|
||||
|
||||
// Compute FFT over the whole signal and store it
|
||||
monitor_t mon;
|
||||
monitor_config_t mon_cfg = {
|
||||
.f_min = 200,
|
||||
.f_max = 3000,
|
||||
.sample_rate = sample_rate,
|
||||
.time_osr = kTime_osr,
|
||||
.freq_osr = kFreq_osr,
|
||||
.protocol = protocol
|
||||
};
|
||||
|
||||
hashtable_init();
|
||||
|
||||
monitor_init(&mon, &mon_cfg);
|
||||
LOG(LOG_DEBUG, "Waterfall allocated %d symbols\n", mon.wf.max_blocks);
|
||||
|
||||
do
|
||||
{
|
||||
struct tm tm_slot_start = { 0 };
|
||||
if (is_live)
|
||||
{
|
||||
// Wait for the start of time slot
|
||||
while (true)
|
||||
{
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_REALTIME, &spec);
|
||||
double time = (double)spec.tv_sec + (spec.tv_nsec / 1e9);
|
||||
double time_within_slot = fmod(time - time_shift, slot_period);
|
||||
if (time_within_slot > slot_period / 4)
|
||||
{
|
||||
audio_read(signal, mon.block_size);
|
||||
}
|
||||
else
|
||||
{
|
||||
time_t time_slot_start = (time_t)(time - time_within_slot);
|
||||
gmtime_r(&time_slot_start, &tm_slot_start);
|
||||
LOG(LOG_INFO, "Time within slot %02d%02d%02d: %.3f s\n", tm_slot_start.tm_hour,
|
||||
tm_slot_start.tm_min, tm_slot_start.tm_sec, time_within_slot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process and accumulate audio data in a monitor/waterfall instance
|
||||
for (int frame_pos = 0; frame_pos + mon.block_size <= num_samples; frame_pos += mon.block_size)
|
||||
{
|
||||
if (dev_name != NULL)
|
||||
{
|
||||
audio_read(signal + frame_pos, mon.block_size);
|
||||
}
|
||||
// LOG(LOG_DEBUG, "Frame pos: %.3fs\n", (float)(frame_pos + mon.block_size) / sample_rate);
|
||||
fprintf(stderr, "#");
|
||||
// Process the waveform data frame by frame - you could have a live loop here with data from an audio device
|
||||
monitor_process(&mon, signal + frame_pos);
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
LOG(LOG_DEBUG, "Waterfall accumulated %d symbols\n", mon.wf.num_blocks);
|
||||
LOG(LOG_INFO, "Max magnitude: %.1f dB\n", mon.max_mag);
|
||||
|
||||
// Decode accumulated data (containing slightly less than a full time slot)
|
||||
decode(&mon, &tm_slot_start);
|
||||
|
||||
// Reset internal variables for the next time slot
|
||||
monitor_reset(&mon);
|
||||
} while (is_live);
|
||||
|
||||
monitor_free(&mon);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Vendored
+189
@@ -0,0 +1,189 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "common/common.h"
|
||||
#include "common/wave.h"
|
||||
#include "ft8/message.h"
|
||||
#include "ft8/encode.h"
|
||||
#include "ft8/constants.h"
|
||||
|
||||
#define LOG_LEVEL LOG_INFO
|
||||
#include "ft8/debug.h"
|
||||
|
||||
#define FT8_SYMBOL_BT 2.0f ///< symbol smoothing filter bandwidth factor (BT)
|
||||
#define FT4_SYMBOL_BT 1.0f ///< symbol smoothing filter bandwidth factor (BT)
|
||||
|
||||
#define GFSK_CONST_K 5.336446f ///< == pi * sqrt(2 / log(2))
|
||||
|
||||
/// Computes a GFSK smoothing pulse.
|
||||
/// The pulse is theoretically infinitely long, however, here it's truncated at 3 times the symbol length.
|
||||
/// This means the pulse array has to have space for 3*n_spsym elements.
|
||||
/// @param[in] n_spsym Number of samples per symbol
|
||||
/// @param[in] b Shape parameter (values defined for FT8/FT4)
|
||||
/// @param[out] pulse Output array of pulse samples
|
||||
///
|
||||
void gfsk_pulse(int n_spsym, float symbol_bt, float* pulse)
|
||||
{
|
||||
for (int i = 0; i < 3 * n_spsym; ++i)
|
||||
{
|
||||
float t = i / (float)n_spsym - 1.5f;
|
||||
float arg1 = GFSK_CONST_K * symbol_bt * (t + 0.5f);
|
||||
float arg2 = GFSK_CONST_K * symbol_bt * (t - 0.5f);
|
||||
pulse[i] = (erff(arg1) - erff(arg2)) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthesize waveform data using GFSK phase shaping.
|
||||
/// The output waveform will contain n_sym symbols.
|
||||
/// @param[in] symbols Array of symbols (tones) (0-7 for FT8)
|
||||
/// @param[in] n_sym Number of symbols in the symbol array
|
||||
/// @param[in] f0 Audio frequency in Hertz for the symbol 0 (base frequency)
|
||||
/// @param[in] symbol_bt Symbol smoothing filter bandwidth (2 for FT8, 1 for FT4)
|
||||
/// @param[in] symbol_period Symbol period (duration), seconds
|
||||
/// @param[in] signal_rate Sample rate of synthesized signal, Hertz
|
||||
/// @param[out] signal Output array of signal waveform samples (should have space for n_sym*n_spsym samples)
|
||||
///
|
||||
void synth_gfsk(const uint8_t* symbols, int n_sym, float f0, float symbol_bt, float symbol_period, int signal_rate, float* signal)
|
||||
{
|
||||
int n_spsym = (int)(0.5f + signal_rate * symbol_period); // Samples per symbol
|
||||
int n_wave = n_sym * n_spsym; // Number of output samples
|
||||
float hmod = 1.0f;
|
||||
|
||||
LOG(LOG_DEBUG, "n_spsym = %d\n", n_spsym);
|
||||
// Compute the smoothed frequency waveform.
|
||||
// Length = (nsym+2)*n_spsym samples, first and last symbols extended
|
||||
float dphi_peak = 2 * M_PI * hmod / n_spsym;
|
||||
float dphi[n_wave + 2 * n_spsym];
|
||||
|
||||
// Shift frequency up by f0
|
||||
for (int i = 0; i < n_wave + 2 * n_spsym; ++i)
|
||||
{
|
||||
dphi[i] = 2 * M_PI * f0 / signal_rate;
|
||||
}
|
||||
|
||||
float pulse[3 * n_spsym];
|
||||
gfsk_pulse(n_spsym, symbol_bt, pulse);
|
||||
|
||||
for (int i = 0; i < n_sym; ++i)
|
||||
{
|
||||
int ib = i * n_spsym;
|
||||
for (int j = 0; j < 3 * n_spsym; ++j)
|
||||
{
|
||||
dphi[j + ib] += dphi_peak * symbols[i] * pulse[j];
|
||||
}
|
||||
}
|
||||
|
||||
// Add dummy symbols at beginning and end with tone values equal to 1st and last symbol, respectively
|
||||
for (int j = 0; j < 2 * n_spsym; ++j)
|
||||
{
|
||||
dphi[j] += dphi_peak * pulse[j + n_spsym] * symbols[0];
|
||||
dphi[j + n_sym * n_spsym] += dphi_peak * pulse[j] * symbols[n_sym - 1];
|
||||
}
|
||||
|
||||
// Calculate and insert the audio waveform
|
||||
float phi = 0;
|
||||
for (int k = 0; k < n_wave; ++k)
|
||||
{ // Don't include dummy symbols
|
||||
signal[k] = sinf(phi);
|
||||
phi = fmodf(phi + dphi[k + n_spsym], 2 * M_PI);
|
||||
}
|
||||
|
||||
// Apply envelope shaping to the first and last symbols
|
||||
int n_ramp = n_spsym / 8;
|
||||
for (int i = 0; i < n_ramp; ++i)
|
||||
{
|
||||
float env = (1 - cosf(2 * M_PI * i / (2 * n_ramp))) / 2;
|
||||
signal[i] *= env;
|
||||
signal[n_wave - 1 - i] *= env;
|
||||
}
|
||||
}
|
||||
|
||||
void usage()
|
||||
{
|
||||
printf("Generate a 15-second WAV file encoding a given message.\n");
|
||||
printf("Usage:\n");
|
||||
printf("\n");
|
||||
printf("gen_ft8 MESSAGE WAV_FILE [FREQUENCY]\n");
|
||||
printf("\n");
|
||||
printf("(Note that you might have to enclose your message in quote marks if it contains spaces)\n");
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
// Expect two command-line arguments
|
||||
if (argc < 3)
|
||||
{
|
||||
usage();
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* message = argv[1];
|
||||
const char* wav_path = argv[2];
|
||||
float frequency = 1000.0;
|
||||
if (argc > 3)
|
||||
{
|
||||
frequency = atof(argv[3]);
|
||||
}
|
||||
bool is_ft4 = (argc > 4) && (0 == strcmp(argv[4], "-ft4"));
|
||||
|
||||
// First, pack the text data into binary message
|
||||
ftx_message_t msg;
|
||||
ftx_message_rc_t rc = ftx_message_encode(&msg, NULL, message);
|
||||
if (rc != FTX_MESSAGE_RC_OK)
|
||||
{
|
||||
printf("Cannot parse message!\n");
|
||||
printf("RC = %d\n", (int)rc);
|
||||
return -2;
|
||||
}
|
||||
|
||||
printf("Packed data: ");
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
printf("%02x ", msg.payload[j]);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
int num_tones = (is_ft4) ? FT4_NN : FT8_NN;
|
||||
float symbol_period = (is_ft4) ? FT4_SYMBOL_PERIOD : FT8_SYMBOL_PERIOD;
|
||||
float symbol_bt = (is_ft4) ? FT4_SYMBOL_BT : FT8_SYMBOL_BT;
|
||||
float slot_time = (is_ft4) ? FT4_SLOT_TIME : FT8_SLOT_TIME;
|
||||
|
||||
// Second, encode the binary message as a sequence of FSK tones
|
||||
uint8_t tones[num_tones]; // Array of 79 tones (symbols)
|
||||
if (is_ft4)
|
||||
{
|
||||
ft4_encode(msg.payload, tones);
|
||||
}
|
||||
else
|
||||
{
|
||||
ft8_encode(msg.payload, tones);
|
||||
}
|
||||
|
||||
printf("FSK tones: ");
|
||||
for (int j = 0; j < num_tones; ++j)
|
||||
{
|
||||
printf("%d", tones[j]);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
// Third, convert the FSK tones into an audio signal
|
||||
int sample_rate = 12000;
|
||||
int num_samples = (int)(0.5f + num_tones * symbol_period * sample_rate); // Number of samples in the data signal
|
||||
int num_silence = (slot_time * sample_rate - num_samples) / 2; // Silence padding at both ends to make 15 seconds
|
||||
int num_total_samples = num_silence + num_samples + num_silence; // Number of samples in the padded signal
|
||||
float signal[num_total_samples];
|
||||
for (int i = 0; i < num_silence; i++)
|
||||
{
|
||||
signal[i] = 0;
|
||||
signal[i + num_samples + num_silence] = 0;
|
||||
}
|
||||
|
||||
// Synthesize waveform data (signal) and save it as WAV file
|
||||
synth_gfsk(tones, num_tones, frequency, symbol_bt, symbol_period, sample_rate, signal + num_silence);
|
||||
save_wav(signal, num_total_samples, sample_rate, wav_path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user