#include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_LEVEL LOG_INFO #include 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_rc_t unpack_status = ftx_message_decode(&message, &hash_if, text); 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; }