diff --git a/common/audio.c b/common/audio.c new file mode 100644 index 0000000..9f34acb --- /dev/null +++ b/common/audio.c @@ -0,0 +1,170 @@ +#include "audio.h" + +#include +#include + +#ifdef USE_PORTAUDIO +#include + +typedef struct +{ + PaStream* instream; +} audio_context_t; + +static audio_context_t audio_context; + +static int audio_cb(void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData) +{ + audio_context_t* context = (audio_context_t*)userData; + float* samples_in = (float*)inputBuffer; + + // PaTime time = data->startTime + timeInfo->inputBufferAdcTime; + printf("Callback with %ld samples\n", framesPerBuffer); + return 0; +} + +void audio_list(void) +{ + PaError pa_rc; + + pa_rc = Pa_Initialize(); // Initialize PortAudio + if (pa_rc != paNoError) + { + printf("Error initializing PortAudio.\n"); + printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc); + return; + } + + int numDevices; + numDevices = Pa_GetDeviceCount(); + if (numDevices < 0) + { + printf("ERROR: Pa_CountDevices returned 0x%x\n", numDevices); + return; + } + + printf("%d audio devices found:\n", numDevices); + for (int i = 0; i < numDevices; i++) + { + const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i); + + PaStreamParameters inputParameters = { + .device = i, + .channelCount = 1, // 1 = mono, 2 = stereo + .sampleFormat = paFloat32, + .suggestedLatency = 0.2, + .hostApiSpecificStreamInfo = NULL + }; + double sample_rate = 12000; // sample rate (frames per second) + pa_rc = Pa_IsFormatSupported(&inputParameters, NULL, sample_rate); + + printf("%d: [%s] [%s]\n", (i + 1), deviceInfo->name, (pa_rc == paNoError) ? "OK" : "NOT SUPPORTED"); + } +} + +int audio_init(void) +{ + PaError pa_rc; + + pa_rc = Pa_Initialize(); // Initialize PortAudio + if (pa_rc != paNoError) + { + printf("Error initializing PortAudio.\n"); + printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc); + Pa_Terminate(); // I don't think we need this but... + return -1; + } + return 0; +} + +int audio_open(const char* name) +{ + PaError pa_rc; + audio_context.instream = NULL; + + PaDeviceIndex ndevice_in = -1; + int numDevices = Pa_GetDeviceCount(); + for (int i = 0; i < numDevices; i++) + { + const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i); + if (0 == strcmp(deviceInfo->name, name)) + { + ndevice_in = i; + break; + } + } + + if (ndevice_in < 0) + { + printf("Could not find device [%s].\n", name); + audio_list(); + return -1; + } + + unsigned long nfpb = 1920 / 4; // frames per buffer + double sample_rate = 12000; // sample rate (frames per second) + + PaStreamParameters inputParameters = { + .device = ndevice_in, + .channelCount = 1, // 1 = mono, 2 = stereo + .sampleFormat = paFloat32, + .suggestedLatency = 0.2, + .hostApiSpecificStreamInfo = NULL + }; + + // Test if this configuration actually works, so we do not run into an ugly assertion + pa_rc = Pa_IsFormatSupported(&inputParameters, NULL, sample_rate); + if (pa_rc != paNoError) + { + printf("Error opening input audio stream.\n"); + printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc); + return -2; + } + + PaStream* instream; + pa_rc = Pa_OpenStream( + &instream, // address of stream + &inputParameters, + NULL, + sample_rate, // Sample rate + nfpb, // Frames per buffer + paNoFlag, + NULL /*(PaStreamCallback*)audio_cb*/, // Callback routine + NULL /*(void*)&audio_context*/); // address of data structure + if (pa_rc != paNoError) + { // We should have no error here usually + printf("Error opening input audio stream:\n"); + printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc); + return -3; + } + // printf("Successfully opened audio input.\n"); + + pa_rc = Pa_StartStream(instream); // Start input stream + if (pa_rc != paNoError) + { + printf("Error starting input audio stream!\n"); + printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc); + return -4; + } + + audio_context.instream = instream; + + // while (Pa_IsStreamActive(instream)) + // { + // Pa_Sleep(100); + // } + // Pa_AbortStream(instream); // Abort stream + // Pa_CloseStream(instream); // Close stream, we're done. + + return 0; +} + +int audio_read(float* buffer, int num_samples) +{ + PaError pa_rc; + pa_rc = Pa_ReadStream(audio_context.instream, (void*)buffer, num_samples); + return 0; +} + +#endif \ No newline at end of file diff --git a/common/audio.h b/common/audio.h new file mode 100644 index 0000000..213ca29 --- /dev/null +++ b/common/audio.h @@ -0,0 +1,18 @@ +#ifndef _INCLUDE_AUDIO_H_ +#define _INCLUDE_AUDIO_H_ + +#ifdef __cplusplus +extern "C" +{ +#endif + +int audio_init(void); +void audio_list(void); +int audio_open(const char* name); +int audio_read(float* buffer, int num_samples); + +#ifdef __cplusplus +} +#endif + +#endif // _INCLUDE_AUDIO_H_ \ No newline at end of file diff --git a/common/monitor.c b/common/monitor.c index ee26491..b2c596c 100644 --- a/common/monitor.c +++ b/common/monitor.c @@ -53,8 +53,8 @@ static void waterfall_free(waterfall_t* me) void monitor_init(monitor_t* me, const monitor_config_t* cfg) { - float slot_time = (cfg->protocol == PROTO_FT4) ? FT4_SLOT_TIME : FT8_SLOT_TIME; - float symbol_period = (cfg->protocol == PROTO_FT4) ? FT4_SYMBOL_PERIOD : FT8_SYMBOL_PERIOD; + float slot_time = (cfg->protocol == FTX_PROTOCOL_FT4) ? FT4_SLOT_TIME : FT8_SLOT_TIME; + float symbol_period = (cfg->protocol == FTX_PROTOCOL_FT4) ? FT4_SYMBOL_PERIOD : FT8_SYMBOL_PERIOD; // Compute DSP parameters that depend on the sample rate me->block_size = (int)(cfg->sample_rate * symbol_period); // samples corresponding to one FSK symbol me->subblock_size = me->block_size / cfg->time_osr; @@ -110,7 +110,7 @@ void monitor_free(monitor_t* me) void monitor_reset(monitor_t* me) { me->wf.num_blocks = 0; - me->max_mag = 0; + me->max_mag = -120.0f; } // Compute FFT magnitudes (log wf) for a frame in the signal and update waterfall data diff --git a/common/wave.c b/common/wave.c index e02757f..9c0668a 100644 --- a/common/wave.c +++ b/common/wave.c @@ -7,11 +7,11 @@ #include // Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers. -void save_wav(const float* signal, int num_samples, int sample_rate, const char* path) +int save_wav(const float* signal, int num_samples, int sample_rate, const char* path) { char subChunk1ID[4] = { 'f', 'm', 't', ' ' }; uint32_t subChunk1Size = 16; // 16 for PCM - uint16_t audioFormat = 1; // PCM = 1 + uint16_t audioFormat = 1; // PCM = 1 uint16_t numChannels = 1; uint16_t bitsPerSample = 16; uint32_t sampleRate = sample_rate; @@ -37,6 +37,8 @@ void save_wav(const float* signal, int num_samples, int sample_rate, const char* } FILE* f = fopen(path, "wb"); + if (f == NULL) + return -1; // NOTE: works only on little-endian architecture fwrite(chunkID, sizeof(chunkID), 1, f); @@ -60,28 +62,31 @@ void save_wav(const float* signal, int num_samples, int sample_rate, const char* fclose(f); free(raw_data); + return 0; } // Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers. int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path) { - char subChunk1ID[4]; // = {'f', 'm', 't', ' '}; + char subChunk1ID[4]; // = {'f', 'm', 't', ' '}; uint32_t subChunk1Size; // = 16; // 16 for PCM - uint16_t audioFormat; // = 1; // PCM = 1 - uint16_t numChannels; // = 1; + uint16_t audioFormat; // = 1; // PCM = 1 + uint16_t numChannels; // = 1; uint16_t bitsPerSample; // = 16; uint32_t sampleRate; uint16_t blockAlign; // = numChannels * bitsPerSample / 8; - uint32_t byteRate; // = sampleRate * blockAlign; + uint32_t byteRate; // = sampleRate * blockAlign; - char subChunk2ID[4]; // = {'d', 'a', 't', 'a'}; + char subChunk2ID[4]; // = {'d', 'a', 't', 'a'}; uint32_t subChunk2Size; // = num_samples * blockAlign; - char chunkID[4]; // = {'R', 'I', 'F', 'F'}; + char chunkID[4]; // = {'R', 'I', 'F', 'F'}; uint32_t chunkSize; // = 4 + (8 + subChunk1Size) + (8 + subChunk2Size); - char format[4]; // = {'W', 'A', 'V', 'E'}; + char format[4]; // = {'W', 'A', 'V', 'E'}; FILE* f = fopen(path, "rb"); + if (f == NULL) + return -1; // NOTE: works only on little-endian architecture fread((void*)chunkID, sizeof(chunkID), 1, f); @@ -91,7 +96,7 @@ int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path fread((void*)subChunk1ID, sizeof(subChunk1ID), 1, f); fread((void*)&subChunk1Size, sizeof(subChunk1Size), 1, f); if (subChunk1Size != 16) - return -1; + return -2; fread((void*)&audioFormat, sizeof(audioFormat), 1, f); fread((void*)&numChannels, sizeof(numChannels), 1, f); @@ -101,13 +106,13 @@ int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path fread((void*)&bitsPerSample, sizeof(bitsPerSample), 1, f); if (audioFormat != 1 || numChannels != 1 || bitsPerSample != 16) - return -1; + return -3; fread((void*)subChunk2ID, sizeof(subChunk2ID), 1, f); fread((void*)&subChunk2Size, sizeof(subChunk2Size), 1, f); if (subChunk2Size / blockAlign > *num_samples) - return -2; + return -4; *num_samples = subChunk2Size / blockAlign; *sample_rate = sampleRate; diff --git a/common/wave.h b/common/wave.h index a2176df..2d60725 100644 --- a/common/wave.h +++ b/common/wave.h @@ -6,11 +6,11 @@ extern "C" { #endif - // Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers. - void save_wav(const float* signal, int num_samples, int sample_rate, const char* path); +// Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers. +int save_wav(const float* signal, int num_samples, int sample_rate, const char* path); - // Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers. - int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path); +// Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers. +int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path); #ifdef __cplusplus } diff --git a/demo/decode_ft8.c b/demo/decode_ft8.c index e8b9249..57fce1c 100644 --- a/demo/decode_ft8.c +++ b/demo/decode_ft8.c @@ -3,266 +3,141 @@ #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 = 120; -const int kLDPC_iterations = 20; +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(void) +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\n\n"); fprintf(stderr, "Decode a 15-second (or slighly shorter) WAV file.\n"); } -#ifdef USE_PORTAUDIO -#include "portaudio.h" +#define CALLSIGN_HASHTABLE_SIZE 256 -typedef struct +static struct { - PaTime startTime; -} audio_cb_context_t; + char callsign[12]; + uint32_t hash; +} callsign_hashtable[CALLSIGN_HASHTABLE_SIZE]; -static audio_cb_context_t audio_cb_context; +static int callsign_hashtable_size; -static int audio_cb(void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer, - const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData) +void hashtable_init(void) { - audio_cb_context_t* context = (audio_cb_context_t*)userData; - int16_t* samples_in = (int16_t*)inputBuffer; - - // PaTime time = data->startTime + timeInfo->inputBufferAdcTime; - return 0; + callsign_hashtable_size = 0; + memset(callsign_hashtable, 0, sizeof(callsign_hashtable)); } -void audio_list(void) +void hashtable_cleanup(uint8_t max_age) { - PaError pa_rc; - - pa_rc = Pa_Initialize(); // Initialize PortAudio - if (pa_rc != paNoError) + for (int idx_hash = 0; idx_hash < CALLSIGN_HASHTABLE_SIZE; ++idx_hash) { - printf("Error initializing PortAudio.\n"); - printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc); - return; - } - - int numDevices; - numDevices = Pa_GetDeviceCount(); - if (numDevices < 0) - { - printf("ERROR: Pa_CountDevices returned 0x%x\n", numDevices); - return; - } - - printf("%d audio devices found:\n", numDevices); - for (int i = 0; i < numDevices; i++) - { - const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i); - - PaStreamParameters inputParameters = { - .device = i, - .channelCount = 1, // 1 = mono, 2 = stereo - .sampleFormat = paInt16, - .suggestedLatency = 0.2, - .hostApiSpecificStreamInfo = NULL - }; - double sample_rate = 12000; // sample rate (frames per second) - pa_rc = Pa_IsFormatSupported(&inputParameters, NULL, sample_rate); - - printf("%d: [%s] [%s]\n", (i + 1), deviceInfo->name, (pa_rc == paNoError) ? "OK" : "NOT SUPPORTED"); - } -} - -int audio_open(const char* name) -{ - PaError pa_rc; - - pa_rc = Pa_Initialize(); // Initialize PortAudio - if (pa_rc != paNoError) - { - printf("Error initializing PortAudio.\n"); - printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc); - Pa_Terminate(); // I don't think we need this but... - return -1; - } - - PaDeviceIndex ndevice_in = -1; - int numDevices = Pa_GetDeviceCount(); - for (int i = 0; i < numDevices; i++) - { - const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i); - if (0 == strcmp(deviceInfo->name, name)) + if (callsign_hashtable[idx_hash].callsign[0] != '\0') { - ndevice_in = i; - break; - } - } - - if (ndevice_in < 0) - { - printf("Could not find device [%s].\n", name); - audio_list(); - return -1; - } - - PaStream* instream; - unsigned long nfpb = 1920 / 4; // frames per buffer - double sample_rate = 12000; // sample rate (frames per second) - - PaStreamParameters inputParameters = { - .device = ndevice_in, - .channelCount = 1, // 1 = mono, 2 = stereo - .sampleFormat = paInt16, - .suggestedLatency = 0.2, - .hostApiSpecificStreamInfo = NULL - }; - - // Test if this configuration actually works, so we do not run into an ugly assertion - pa_rc = Pa_IsFormatSupported(&inputParameters, NULL, sample_rate); - if (pa_rc != paNoError) - { - printf("Error opening input audio stream.\n"); - printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc); - return -2; - } - - pa_rc = Pa_OpenStream( - &instream, // address of stream - &inputParameters, - NULL, - sample_rate, // Sample rate - nfpb, // Frames per buffer - paNoFlag, - (PaStreamCallback*)audio_cb, // Callback routine - (void*)&audio_cb_context); // address of data structure - if (pa_rc != paNoError) - { // We should have no error here usually - printf("Error opening input audio stream:\n"); - printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc); - return -3; - } - // printf("Successfully opened audio input.\n"); - - pa_rc = Pa_StartStream(instream); // Start input stream - if (pa_rc != paNoError) - { - printf("Error starting input audio stream!\n"); - printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc); - return -4; - } - - // while (Pa_IsStreamActive(instream)) - // { - // Pa_Sleep(100); - // } - // Pa_AbortStream(instream); // Abort stream - // Pa_CloseStream(instream); // Close stream, we're done. - - return 0; -} -#endif - -int main(int argc, char** argv) -{ - // Accepted arguments - const char* wav_path = NULL; - bool is_ft8 = true; - - // 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")) + uint8_t age = (uint8_t)(callsign_hashtable[idx_hash].hash >> 24); + if (age > max_age) { - is_ft8 = false; + 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 { - usage(); - return -1; + // 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 { - if (wav_path == NULL) - { - wav_path = argv[arg_idx]; - } - else - { - usage(); - return -1; - } + 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; } - ++arg_idx; } - // Check if all mandatory arguments have been received - if (wav_path == NULL) + 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_e 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)) & 0x3FF; + int idx_hash = (hash10 * 23) % CALLSIGN_HASHTABLE_SIZE; + while (callsign_hashtable[idx_hash].callsign[0] != '\0') { - usage(); - return -1; + 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; +} - audio_list(); - - int sample_rate = 12000; - int num_samples = 15 * sample_rate; - float signal[num_samples]; - - int rc = load_wav(signal, &num_samples, &sample_rate, wav_path); - if (rc < 0) - { - return -1; - } - - LOG(LOG_INFO, "Sample rate %d Hz, %d samples, %.3f seconds\n", sample_rate, num_samples, (double)num_samples / sample_rate); - - // 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 = is_ft8 ? PROTO_FT8 : PROTO_FT4 - }; - monitor_init(&mon, &mon_cfg); - LOG(LOG_DEBUG, "Waterfall allocated %d symbols\n", mon.wf.max_blocks); - for (int frame_pos = 0; frame_pos + mon.block_size <= num_samples; frame_pos += mon.block_size) - { - // 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); - } - LOG(LOG_DEBUG, "Waterfall accumulated %d symbols\n", mon.wf.num_blocks); - LOG(LOG_INFO, "Max magnitude: %.1f dB\n", mon.max_mag); +ftx_callsign_hash_interface_t hash_if = { + .lookup_hash = hashtable_lookup, + .save_hash = hashtable_add +}; +void decode(const monitor_t* mon) +{ + const waterfall_t* wf = &mon->wf; // Find top candidates by Costas sync score and localize them in time and frequency candidate_t candidate_list[kMax_candidates]; - int num_candidates = ft8_find_sync(&mon.wf, kMax_candidates, candidate_list, kMin_score); + int num_candidates = ft8_find_sync(wf, kMax_candidates, candidate_list, kMin_score); // Hash table for decoded messages (to check for duplicates) int num_decoded = 0; - message_t decoded[kMax_decoded_messages]; - message_t* decoded_hashtable[kMax_decoded_messages]; + 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) @@ -274,17 +149,16 @@ int main(int argc, char** argv) for (int idx = 0; idx < num_candidates; ++idx) { const candidate_t* cand = &candidate_list[idx]; - if (cand->score < kMin_score) - continue; - float freq_hz = (mon.min_bin + cand->freq_offset + (float)cand->freq_sub / mon.wf.freq_osr) / mon.symbol_period; - float time_sec = (cand->time_offset + (float)cand->time_sub / mon.wf.time_osr) * mon.symbol_period; + 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; - message_t message; + ftx_message_t message; decode_status_t status; - if (!ft8_decode(&mon.wf, cand, &message, kLDPC_iterations, NULL, &status)) + if (!ft8_decode(wf, cand, kLDPC_iterations, &message, &status)) { - // printf("000000 %3d %+4.2f %4.0f ~ ---\n", cand->score, time_sec, freq_hz); + // float snr = cand->score * 0.5f; // TODO: compute better approximation of SNR + // printf("000000 %2.1f %+4.2f %4.0f ~ %s\n", snr, time_sec, freq_hz, "---"); if (status.ldpc_errors > 0) { LOG(LOG_DEBUG, "LDPC decode: %d errors\n", status.ldpc_errors); @@ -293,10 +167,6 @@ int main(int argc, char** argv) { LOG(LOG_DEBUG, "CRC mismatch!\n"); } - else if (status.unpack_status != 0) - { - LOG(LOG_DEBUG, "Error while unpacking!\n"); - } continue; } @@ -311,9 +181,9 @@ int main(int argc, char** argv) LOG(LOG_DEBUG, "Found an empty slot\n"); found_empty_slot = true; } - else if ((decoded_hashtable[idx_hash]->hash == message.hash) && (0 == strcmp(decoded_hashtable[idx_hash]->text, message.text))) + 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 [%s]\n", message.text); + LOG(LOG_DEBUG, "Found a duplicate!\n"); found_duplicate = true; } else @@ -331,12 +201,180 @@ int main(int argc, char** argv) decoded_hashtable[idx_hash] = &decoded[idx_hash]; ++num_decoded; + char text[FTX_MAX_MESSAGE_LENGTH]; + // int unpack_status = unpack77(message.payload, text, NULL); + int unpack_status = ftx_message_decode(&message, &hash_if, text); + if (unpack_status != 0) + { + strcpy(text, "Error while unpacking!"); + } + + // uint8_t i3 = ftx_message_get_i3(&message); + // if (i3 == 0) + // { + // uint8_t n3 = ftx_message_get_n3(&message); + // printf("000000 %02d %+4.2f %4.0f [%d.%d] ~ %s\n", cand->score, time_sec, freq_hz, i3, n3, text); + // } + // else + // printf("000000 %02d %+4.2f %4.0f [%d ] ~ %s\n", cand->score, time_sec, freq_hz, i3, text); + // Fake WSJT-X-like output for now float snr = cand->score * 0.5f; // TODO: compute better approximation of SNR - printf("000000 %2.1f %+4.2f %4.0f ~ %s\n", snr, time_sec, freq_hz, message.text); + printf("000000 %+05.1f %+4.2f %4.0f ~ %s\n", snr, time_sec, freq_hz, text); } } - LOG(LOG_INFO, "Decoded %d messages\n", num_decoded); + 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_time = ((protocol == FTX_PROTOCOL_FT8) ? FT8_SLOT_TIME : FT4_SLOT_TIME); + int sample_rate = 12000; + int num_samples = slot_time * sample_rate; + float signal[num_samples]; + bool isContinuous = 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_time - 0.4f) * sample_rate; + isContinuous = 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 + { + if (dev_name != NULL) + { + // Wait for the start of time slot + while (true) + { + struct timespec spec; + clock_gettime(CLOCK_REALTIME, &spec); + float time_within_slot = fmod((double)spec.tv_sec + (spec.tv_nsec * 1e-9) - time_shift, slot_time); + if (time_within_slot > slot_time / 3) + audio_read(signal, mon.block_size); + else + { + LOG(LOG_INFO, "Time within slot: %.3f s\n", 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); + + // Reset internal variables for the next time slot + monitor_reset(&mon); + } while (isContinuous); monitor_free(&mon); diff --git a/ft8/constants.h b/ft8/constants.h index 2f16bf8..eb50fca 100644 --- a/ft8/constants.h +++ b/ft8/constants.h @@ -51,8 +51,8 @@ extern "C" typedef enum { - PROTO_FT4, - PROTO_FT8 + FTX_PROTOCOL_FT4, + FTX_PROTOCOL_FT8 } ftx_protocol_t; /// Costas 7x7 tone pattern for synchronization diff --git a/ft8/decode.c b/ft8/decode.c index 116a1bc..6125234 100644 --- a/ft8/decode.c +++ b/ft8/decode.c @@ -53,7 +53,7 @@ int ft8_snr(const waterfall_t* wf, const candidate_t* candidate) // Get the pointer to symbol 0 of the candidate const uint8_t* mag_cand = get_cand_mag(wf, candidate); - if (wf->protocol == PROTO_FT4) + if (wf->protocol == FTX_PROTOCOL_FT4) { } @@ -246,7 +246,7 @@ int ft8_find_sync(const waterfall_t* wf, int num_candidates, candidate_t heap[], { for (candidate.freq_offset = 0; (candidate.freq_offset + 7) < wf->num_bins; ++candidate.freq_offset) { - if (wf->protocol == PROTO_FT4) + if (wf->protocol == FTX_PROTOCOL_FT4) { candidate.score = ft4_sync_score(wf, &candidate); } @@ -261,10 +261,10 @@ int ft8_find_sync(const waterfall_t* wf, int num_candidates, candidate_t heap[], // If the heap is full AND the current candidate is better than // the worst in the heap, we remove the worst and make space - if (heap_size == num_candidates && candidate.score > heap[0].score) + if ((heap_size == num_candidates) && (candidate.score > heap[0].score)) { - heap[0] = heap[heap_size - 1]; --heap_size; + heap[0] = heap[heap_size]; heapify_down(heap, heap_size); } @@ -284,6 +284,10 @@ int ft8_find_sync(const waterfall_t* wf, int num_candidates, candidate_t heap[], int len_unsorted = heap_size; while (len_unsorted > 1) { + // Take the top (index 0) element which is guaranteed to have the smallest score, + // exchange it with the last element in the heap, and decrease the heap size. + // Then restore the heap property in the new, smaller heap. + // At the end the elements will be sorted in descending order. candidate_t tmp = heap[len_unsorted - 1]; heap[len_unsorted - 1] = heap[0]; heap[0] = tmp; @@ -374,10 +378,10 @@ static void ftx_normalize_logl(float* log174) } } -bool ft8_decode(const waterfall_t* wf, const candidate_t* cand, message_t* message, int max_iterations, const unpack_hash_interface_t* hash_if, decode_status_t* status) +bool ft8_decode(const waterfall_t* wf, const candidate_t* cand, int max_iterations, ftx_message_t* message, decode_status_t* status) { float log174[FTX_LDPC_N]; // message bits encoded as likelihood - if (wf->protocol == PROTO_FT4) + if (wf->protocol == FTX_PROTOCOL_FT4) { ft4_extract_likelihood(wf, cand, log174); } @@ -413,27 +417,27 @@ bool ft8_decode(const waterfall_t* wf, const candidate_t* cand, message_t* messa return false; } - if (wf->protocol == PROTO_FT4) + // Reuse CRC value as a hash for the message (TODO: 14 bits only, should perhaps use full 16 or 32 bits?) + message->hash = status->crc_calculated; + + if (wf->protocol == FTX_PROTOCOL_FT4) { // '[..] for FT4 only, in order to avoid transmitting a long string of zeros when sending CQ messages, // the assembled 77-bit message is bitwise exclusive-OR’ed with [a] pseudorandom sequence before computing the CRC and FEC parity bits' for (int i = 0; i < 10; ++i) { - a91[i] ^= kFT4_XOR_sequence[i]; + message->payload[i] = a91[i] ^ kFT4_XOR_sequence[i]; + } + } + else + { + for (int i = 0; i < 10; ++i) + { + message->payload[i] = a91[i]; } } // LOG(LOG_DEBUG, "Decoded message (CRC %04x), trying to unpack...\n", status->crc_extracted); - status->unpack_status = unpack77(a91, message->text, hash_if); - - if (status->unpack_status < 0) - { - return false; - } - - // Reuse binary message CRC as hash value for the message - message->hash = status->crc_extracted; - return true; } @@ -450,30 +454,33 @@ static float max4(float a, float b, float c, float d) static void heapify_down(candidate_t heap[], int heap_size) { // heapify from the root down - int current = 0; + int current = 0; // root node while (true) { - int largest = current; int left = 2 * current + 1; int right = left + 1; - if (left < heap_size && heap[left].score < heap[largest].score) + // Find the smallest value of (parent, left child, right child) + int smallest = current; + if ((left < heap_size) && (heap[left].score < heap[smallest].score)) { - largest = left; + smallest = left; } - if (right < heap_size && heap[right].score < heap[largest].score) + if ((right < heap_size) && (heap[right].score < heap[smallest].score)) { - largest = right; + smallest = right; } - if (largest == current) + + if (smallest == current) { break; } - candidate_t tmp = heap[largest]; - heap[largest] = heap[current]; + // Exchange the current node with the smallest child and move down to it + candidate_t tmp = heap[smallest]; + heap[smallest] = heap[current]; heap[current] = tmp; - current = largest; + current = smallest; } } @@ -484,11 +491,12 @@ static void heapify_up(candidate_t heap[], int heap_size) while (current > 0) { int parent = (current - 1) / 2; - if (heap[current].score >= heap[parent].score) + if (!(heap[current].score < heap[parent].score)) { break; } + // Exchange the current node with its parent and move up candidate_t tmp = heap[parent]; heap[parent] = heap[current]; heap[current] = tmp; diff --git a/ft8/decode.h b/ft8/decode.h index 4bc3731..81237c0 100644 --- a/ft8/decode.h +++ b/ft8/decode.h @@ -5,15 +5,13 @@ #include #include "constants.h" -#include "unpack.h" +#include "message.h" #ifdef __cplusplus extern "C" { #endif -#define FTX_MAX_MESSAGE_LENGTH 35 ///< max message length = callsign[13] + space + callsign[13] + space + report[6] + terminator - /// Input structure to ft8_find_sync() function. This structure describes stored waterfall data over the whole message slot. /// Fields time_osr and freq_osr specify additional oversampling rate for time and frequency resolution. /// If time_osr=1, FFT magnitude data is collected once for every symbol transmitted, i.e. every 1/6.25 = 0.16 seconds. @@ -44,20 +42,13 @@ typedef struct int16_t snr; } candidate_t; -/// Structure that holds the decoded message -typedef struct -{ - char text[FTX_MAX_MESSAGE_LENGTH]; ///< Plain text - uint16_t hash; ///< Hash value to be used in hash table and quick checking for duplicates -} message_t; - /// Structure that contains the status of various steps during decoding of a message typedef struct { int ldpc_errors; ///< Number of LDPC errors during decoding uint16_t crc_extracted; ///< CRC value recovered from the message uint16_t crc_calculated; ///< CRC value calculated over the payload - int unpack_status; ///< Return value of the unpack routine + // int unpack_status; ///< Return value of the unpack routine } decode_status_t; /// Localize top N candidates in frequency and time according to their sync strength (looking at Costas symbols) @@ -73,11 +64,11 @@ int ft8_find_sync(const waterfall_t* power, int num_candidates, candidate_t heap /// Attempt to decode a message candidate. Extracts the bit probabilities, runs LDPC decoder, checks CRC and unpacks the message in plain text. /// @param[in] power Waterfall data collected during message slot /// @param[in] cand Candidate to decode -/// @param[out] message message_t structure that will receive the decoded message /// @param[in] max_iterations Maximum allowed LDPC iterations (lower number means faster decode, but less precise) +/// @param[out] message ftx_message_t structure that will receive the decoded message /// @param[out] status decode_status_t structure that will be filled with the status of various decoding steps /// @return True if the decoding was successful, false otherwise (check status for details) -bool ft8_decode(const waterfall_t* power, const candidate_t* cand, message_t* message, int max_iterations, const unpack_hash_interface_t* hash_if, decode_status_t* status); +bool ft8_decode(const waterfall_t* power, const candidate_t* cand, int max_iterations, ftx_message_t* message, decode_status_t* status); #ifdef __cplusplus } diff --git a/ft8/message.c b/ft8/message.c index 3da80ca..9d46a60 100644 --- a/ft8/message.c +++ b/ft8/message.c @@ -3,7 +3,7 @@ #include #include -#define LOG_LEVEL LOG_DEBUG +#define LOG_LEVEL LOG_WARN #include "debug.h" #define MAX22 ((uint32_t)4194304ul) @@ -53,10 +53,19 @@ void ftx_message_init(ftx_message_t* msg) memset((void*)msg, 0, sizeof(ftx_message_t)); } -// bool ftx_message_check_recipient(const ftx_message_t* msg, const char* callsign) -// { -// return false; -// } +uint8_t ftx_message_get_i3(const ftx_message_t* msg) +{ + // Extract i3 (bits 74..76) + uint8_t i3 = (msg->payload[9] >> 3) & 0x07u; + return i3; +} + +uint8_t ftx_message_get_n3(const ftx_message_t* msg) +{ + // Extract n3 (bits 71..73) + uint8_t n3 = ((msg->payload[8] << 2) & 0x04u) | ((msg->payload[9] >> 6) & 0x03u); + return n3; +} ftx_message_type_t ftx_message_get_type(const ftx_message_t* msg) { @@ -288,10 +297,10 @@ ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_ { ftx_message_rc_t rc; - char buf[31]; // 12 + 12 + 7 (std/nonstd) / 14 (free text) / 19 (telemetry) + char buf[35]; // 13 + 13 + 6 (std/nonstd) / 14 (free text) / 19 (telemetry) char* field1 = buf; - char* field2 = buf + 12; - char* field3 = buf + 12 + 12; + char* field2 = buf + 14; + char* field3 = buf + 14 + 14; message[0] = '\0'; @@ -306,18 +315,38 @@ ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_ break; case FTX_MESSAGE_TYPE_FREE_TEXT: ftx_message_decode_free(msg, field1); + field2 = NULL; + field3 = NULL; rc = FTX_MESSAGE_RC_OK; break; case FTX_MESSAGE_TYPE_TELEMETRY: ftx_message_decode_telemetry_hex(msg, field1); + field2 = NULL; + field3 = NULL; rc = FTX_MESSAGE_RC_OK; break; default: // not handled yet + field1 = NULL; rc = FTX_MESSAGE_RC_ERROR_TYPE; break; } - // TODO join fields via whitespace + + if (field1 != NULL) + { + // TODO join fields via whitespace + message = append_string(message, field1); + if (field2 != NULL) + { + message = append_string(message, " "); + message = append_string(message, field2); + if (field3 != NULL) + { + message = append_string(message, " "); + message = append_string(message, field3); + } + } + } return rc; } @@ -398,7 +427,7 @@ ftx_message_rc_t ftx_message_decode_nonstd(const ftx_message_t* msg, ftx_callsig unpack58(n58, hash_if, call_decoded); // Decode the other call from hash lookup table - char call_3[12]; + char call_3[14]; lookup_callsign(hash_if, FTX_CALLSIGN_HASH_12_BITS, n12, call_3); // Possibly flip them around @@ -462,7 +491,7 @@ void ftx_message_decode_telemetry_hex(const ftx_message_t* msg, char* telemetry_ for (int i = 0; i < 9; ++i) { uint8_t nibble1 = (b71[i] >> 4); - uint8_t nibble2 = (b71[i] & 0x0F); + uint8_t nibble2 = (b71[i] & 0x0Fu); char c1 = (nibble1 > 9) ? (nibble1 - 10 + 'A') : nibble1 + '0'; char c2 = (nibble2 > 9) ? (nibble2 - 10 + 'A') : nibble2 + '0'; telemetry_hex[i * 2] = c1; @@ -479,7 +508,7 @@ void ftx_message_decode_telemetry(const ftx_message_t* msg, uint8_t* telemetry) for (int i = 0; i < 9; ++i) { telemetry[i] = (carry << 7) | (msg->payload[i] >> 1); - carry = (msg->payload[i] & 0x01); + carry = (msg->payload[i] & 0x01u); } } @@ -544,7 +573,7 @@ static bool save_callsign(const ftx_callsign_hash_interface_t* hash_if, const ch i++; } - uint32_t n22 = (47055833459ull * n58) >> (64 - 22); + uint32_t n22 = ((47055833459ull * n58) >> (64 - 22)) & (0x3FFFFFul); uint32_t n12 = n22 >> 10; uint32_t n10 = n22 >> 12; LOG(LOG_DEBUG, "save_callsign('%s') = [n22=%d, n12=%d, n10=%d]\n", callsign, n22, n12, n10); diff --git a/ft8/message.h b/ft8/message.h index d3fa1bb..2f44fe0 100644 --- a/ft8/message.h +++ b/ft8/message.h @@ -5,16 +5,17 @@ #include #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif -#define PAYLOAD_LENGTH 77 -#define PAYLOAD_LENGTH_BYTES 10 +#define FTX_PAYLOAD_LENGTH_BYTES 10 ///< number of bytes to hold 77 bits of FTx payload data +#define FTX_MAX_MESSAGE_LENGTH 35 ///< max message length = callsign[13] + space + callsign[13] + space + report[6] + terminator /// Structure that holds the decoded message typedef struct { - uint8_t payload[PAYLOAD_LENGTH_BYTES]; + uint8_t payload[FTX_PAYLOAD_LENGTH_BYTES]; uint16_t hash; ///< Hash value to be used in hash table and quick checking for duplicates } ftx_message_t; @@ -82,9 +83,13 @@ typedef enum // Nonstd. call - all the rest, limited to 3-11 characters either alphanumeric or stroke (/) void ftx_message_init(ftx_message_t* msg); -bool ftx_message_check_recipient(const ftx_message_t* msg, const char* callsign); + +uint8_t ftx_message_get_i3(const ftx_message_t* msg); +uint8_t ftx_message_get_n3(const ftx_message_t* msg); ftx_message_type_t ftx_message_get_type(const ftx_message_t* msg); +// bool ftx_message_check_recipient(const ftx_message_t* msg, const char* callsign); + /// Pack (encode) a text message ftx_message_rc_t ftx_message_encode(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* message_text); diff --git a/ft8/text.c b/ft8/text.c index aa79a0a..ff2d20e 100644 --- a/ft8/text.c +++ b/ft8/text.c @@ -107,6 +107,18 @@ void fmtmsg(char* msg_out, const char* msg_in) *msg_out = 0; // Add zero termination } +char* append_string(char* string, const char* token) +{ + while (*token != '\0') + { + *string = *token; + string++; + token++; + } + *string = '\0'; + return string; +} + const char* copy_token(char* token, int length, const char* string) { // Copy characters until a whitespace character or the end of string diff --git a/ft8/text.h b/ft8/text.h index 46a3379..e4c28d8 100644 --- a/ft8/text.h +++ b/ft8/text.h @@ -5,7 +5,8 @@ #include #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif // Utility functions for characters and strings @@ -44,6 +45,8 @@ void fmtmsg(char* msg_out, const char* msg_in); /// @return Pointer to the next token (can be passed to copy_token to extract the next token) const char* copy_token(char* token, int length, const char* string); +char* append_string(char* string, const char* token); + // Parse a 2 digit integer from string int dd_to_int(const char* str, int length);