Added live decoding based on Portaudio

pull/37/head
Karlis Goba 2022-06-19 20:01:21 +03:00
rodzic a05dbae957
commit 6f528128ee
13 zmienionych plików z 585 dodań i 306 usunięć

170
common/audio.c 100644
Wyświetl plik

@ -0,0 +1,170 @@
#include "audio.h"
#include <stdio.h>
#include <string.h>
#ifdef USE_PORTAUDIO
#include <portaudio.h>
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

18
common/audio.h 100644
Wyświetl plik

@ -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_

Wyświetl plik

@ -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

Wyświetl plik

@ -7,11 +7,11 @@
#include <stdint.h>
// 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;

Wyświetl plik

@ -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
}

Wyświetl plik

@ -3,266 +3,141 @@
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
#include <time.h>
#include <ft8/decode.h>
#include <ft8/encode.h>
#include <ft8/unpack.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 = 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);

Wyświetl plik

@ -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

Wyświetl plik

@ -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-ORed 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;

Wyświetl plik

@ -5,15 +5,13 @@
#include <stdbool.h>
#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
}

Wyświetl plik

@ -3,7 +3,7 @@
#include <stdlib.h>
#include <string.h>
#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);

Wyświetl plik

@ -5,16 +5,17 @@
#include <stdbool.h>
#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);

Wyświetl plik

@ -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

Wyświetl plik

@ -5,7 +5,8 @@
#include <stdint.h>
#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);