kopia lustrzana https://github.com/kgoba/ft8_lib
Added live decoding based on Portaudio
rodzic
a05dbae957
commit
6f528128ee
|
@ -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
|
|
@ -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_
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
66
ft8/decode.c
66
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;
|
||||
|
|
17
ft8/decode.h
17
ft8/decode.h
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
12
ft8/text.c
12
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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue