kopia lustrzana https://github.com/kgoba/ft8_lib
Move decode/encode/test.c into apps folder and compile with libfft, libft8 library link
rodzic
b095d79590
commit
46f0832952
34
Makefile
34
Makefile
|
@ -1,27 +1,15 @@
|
|||
CFLAGS = -O3
|
||||
CPPFLAGS = -std=c11 -I.
|
||||
LDFLAGS = -lm
|
||||
SUBDIRS := libs
|
||||
SUBDIRS += apps
|
||||
|
||||
TARGETS = gen_ft8 decode_ft8 test
|
||||
all install:
|
||||
echo $(SUBDIRS)
|
||||
@for i in `echo $(SUBDIRS)`; do \
|
||||
$(MAKE) -C $$i $@ || exit 1; \
|
||||
done
|
||||
|
||||
.PHONY: run_tests all clean
|
||||
|
||||
all: $(TARGETS)
|
||||
clean mrproper:
|
||||
@for i in `echo $(SUBDIRS)`; do \
|
||||
$(MAKE) -C $$i $@ || exit 1; \
|
||||
done
|
||||
|
||||
run_tests: test
|
||||
@./test
|
||||
|
||||
gen_ft8: gen_ft8.o ft8/constants.o ft8/text.o ft8/pack.o ft8/encode.o ft8/crc.o common/wave.o
|
||||
$(CXX) $(LDFLAGS) -o $@ $^
|
||||
|
||||
test: test.o ft8/pack.o ft8/encode.o ft8/crc.o ft8/text.o ft8/constants.o fft/kiss_fftr.o fft/kiss_fft.o
|
||||
$(CXX) $(LDFLAGS) -o $@ $^
|
||||
|
||||
decode_ft8: decode_ft8.o fft/kiss_fftr.o fft/kiss_fft.o ft8/decode.o ft8/encode.o ft8/crc.o ft8/ldpc.o ft8/unpack.o ft8/text.o ft8/constants.o common/wave.o
|
||||
$(CXX) $(LDFLAGS) -o $@ $^
|
||||
|
||||
clean:
|
||||
rm -f *.o ft8/*.o common/*.o fft/*.o $(TARGETS)
|
||||
install:
|
||||
$(AR) rc libft8.a ft8/constants.o ft8/encode.o ft8/pack.o ft8/text.o common/wave.o
|
||||
install libft8.a /usr/lib/libft8.a
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
CC = gcc
|
||||
|
||||
CFLAGS = -std=gnu11 -O3
|
||||
CFLAGS += -I./common
|
||||
CFLAGS += -I../libs/libfft/include
|
||||
CFLAGS += -I../libs/libft8/include
|
||||
|
||||
LDFLAGS = -lm
|
||||
LDFLAGS += -L../libs/libfft/bin -lfft
|
||||
LDFLAGS += -L../libs/libft8/bin -lft8
|
||||
|
||||
TARGETS = gen_ft8 decode_ft8 test
|
||||
|
||||
.PHONY: run_tests all clean
|
||||
|
||||
all: $(TARGETS)
|
||||
|
||||
run_tests: test
|
||||
@./test
|
||||
|
||||
gen_ft8: gen_ft8.o common/wave.o
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
test: test.o
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
decode_ft8: decode_ft8.o common/wave.o
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f *.o common/*.o $(TARGETS)
|
||||
install:
|
||||
$(INSTALL) -D decode_ft8 /usr/bin/decode_ft8
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#define LOG_DEBUG 0
|
||||
#define LOG_INFO 1
|
||||
#define LOG_WARN 2
|
||||
#define LOG_ERROR 3
|
||||
#define LOG_FATAL 4
|
||||
|
||||
|
||||
#define LOG(level, ...) if (level >= LOG_LEVEL) fprintf(stderr, __VA_ARGS__)
|
|
@ -0,0 +1,130 @@
|
|||
#include "wave.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#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)
|
||||
{
|
||||
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 bitsPerSample = 16;
|
||||
uint32_t sampleRate = sample_rate;
|
||||
uint16_t blockAlign = numChannels * bitsPerSample / 8;
|
||||
uint32_t byteRate = sampleRate * blockAlign;
|
||||
int i = 0;
|
||||
|
||||
char subChunk2ID[4] = {'d', 'a', 't', 'a'};
|
||||
uint32_t subChunk2Size = num_samples * blockAlign;
|
||||
|
||||
char chunkID[4] = {'R', 'I', 'F', 'F'};
|
||||
uint32_t chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size);
|
||||
char format[4] = {'W', 'A', 'V', 'E'};
|
||||
|
||||
int16_t *raw_data = (int16_t *)malloc(num_samples * blockAlign);
|
||||
for (i = 0; i < num_samples; i++)
|
||||
{
|
||||
float x = signal[i];
|
||||
if (x > 1.0)
|
||||
x = 1.0;
|
||||
else if (x < -1.0)
|
||||
x = -1.0;
|
||||
raw_data[i] = (int)(0.5 + (x * 32767.0));
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "wb");
|
||||
|
||||
// NOTE: works only on little-endian architecture
|
||||
fwrite(chunkID, sizeof(chunkID), 1, f);
|
||||
fwrite(&chunkSize, sizeof(chunkSize), 1, f);
|
||||
fwrite(format, sizeof(format), 1, f);
|
||||
|
||||
fwrite(subChunk1ID, sizeof(subChunk1ID), 1, f);
|
||||
fwrite(&subChunk1Size, sizeof(subChunk1Size), 1, f);
|
||||
fwrite(&audioFormat, sizeof(audioFormat), 1, f);
|
||||
fwrite(&numChannels, sizeof(numChannels), 1, f);
|
||||
fwrite(&sampleRate, sizeof(sampleRate), 1, f);
|
||||
fwrite(&byteRate, sizeof(byteRate), 1, f);
|
||||
fwrite(&blockAlign, sizeof(blockAlign), 1, f);
|
||||
fwrite(&bitsPerSample, sizeof(bitsPerSample), 1, f);
|
||||
|
||||
fwrite(subChunk2ID, sizeof(subChunk2ID), 1, f);
|
||||
fwrite(&subChunk2Size, sizeof(subChunk2Size), 1, f);
|
||||
|
||||
fwrite(raw_data, blockAlign, num_samples, f);
|
||||
|
||||
fclose(f);
|
||||
|
||||
free(raw_data);
|
||||
}
|
||||
|
||||
// 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', ' '};
|
||||
uint32_t subChunk1Size; // = 16; // 16 for PCM
|
||||
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;
|
||||
|
||||
char subChunk2ID[4]; // = {'d', 'a', 't', 'a'};
|
||||
uint32_t subChunk2Size; // = num_samples * blockAlign;
|
||||
|
||||
char chunkID[4]; // = {'R', 'I', 'F', 'F'};
|
||||
uint32_t chunkSize; // = 4 + (8 + subChunk1Size) + (8 + subChunk2Size);
|
||||
char format[4]; // = {'W', 'A', 'V', 'E'};
|
||||
int i = 0;
|
||||
|
||||
FILE *f = fopen(path, "rb");
|
||||
|
||||
// NOTE: works only on little-endian architecture
|
||||
fread((void *)chunkID, sizeof(chunkID), 1, f);
|
||||
fread((void *)&chunkSize, sizeof(chunkSize), 1, f);
|
||||
fread((void *)format, sizeof(format), 1, f);
|
||||
|
||||
fread((void *)subChunk1ID, sizeof(subChunk1ID), 1, f);
|
||||
fread((void *)&subChunk1Size, sizeof(subChunk1Size), 1, f);
|
||||
if (subChunk1Size != 16)
|
||||
return -1;
|
||||
|
||||
fread((void *)&audioFormat, sizeof(audioFormat), 1, f);
|
||||
fread((void *)&numChannels, sizeof(numChannels), 1, f);
|
||||
fread((void *)&sampleRate, sizeof(sampleRate), 1, f);
|
||||
fread((void *)&byteRate, sizeof(byteRate), 1, f);
|
||||
fread((void *)&blockAlign, sizeof(blockAlign), 1, f);
|
||||
fread((void *)&bitsPerSample, sizeof(bitsPerSample), 1, f);
|
||||
|
||||
if (audioFormat != 1 || numChannels != 1 || bitsPerSample != 16)
|
||||
return -1;
|
||||
|
||||
fread((void *)subChunk2ID, sizeof(subChunk2ID), 1, f);
|
||||
fread((void *)&subChunk2Size, sizeof(subChunk2Size), 1, f);
|
||||
|
||||
if (subChunk2Size / blockAlign > *num_samples)
|
||||
return -2;
|
||||
|
||||
*num_samples = subChunk2Size / blockAlign;
|
||||
*sample_rate = sampleRate;
|
||||
|
||||
int16_t *raw_data = (int16_t *)malloc(*num_samples * blockAlign);
|
||||
|
||||
fread((void *)raw_data, blockAlign, *num_samples, f);
|
||||
for (i = 0; i < *num_samples; i++)
|
||||
{
|
||||
signal[i] = raw_data[i] / 32768.0f;
|
||||
}
|
||||
|
||||
free(raw_data);
|
||||
|
||||
fclose(f);
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
#ifndef _INCLUDE_WAVE_H_
|
||||
#define _INCLUDE_WAVE_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);
|
||||
|
||||
// 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);
|
||||
|
||||
#endif // _INCLUDE_WAVE_H_
|
|
@ -0,0 +1,271 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "ft8_unpack.h"
|
||||
#include "ft8_ldpc.h"
|
||||
#include "ft8_decode.h"
|
||||
#include "ft8_constants.h"
|
||||
#include "ft8_encode.h"
|
||||
#include "ft8_crc.h"
|
||||
|
||||
#include "wave.h"
|
||||
#include "debug.h"
|
||||
#include "kiss_fftr.h"
|
||||
|
||||
#define LOG_LEVEL LOG_INFO
|
||||
|
||||
const int kMin_score = 10; // Minimum sync score threshold for candidates
|
||||
const int kMax_candidates = 120;
|
||||
const int kLDPC_iterations = 20;
|
||||
|
||||
const int kMax_decoded_messages = 50;
|
||||
|
||||
const int kFreq_osr = 2;
|
||||
const int kTime_osr = 2;
|
||||
|
||||
const float kFSK_dev = 6.25f; // tone deviation in Hz and symbol rate
|
||||
|
||||
void usage()
|
||||
{
|
||||
fprintf(stderr, "Decode a 15-second (or slighly shorter) WAV file.\n");
|
||||
}
|
||||
|
||||
float hann_i(int i, int N)
|
||||
{
|
||||
float x = sinf((float)M_PI * i / N);
|
||||
return x * x;
|
||||
}
|
||||
|
||||
float hamming_i(int i, int N)
|
||||
{
|
||||
const float a0 = (float)25 / 46;
|
||||
const float a1 = 1 - a0;
|
||||
|
||||
float x1 = cosf(2 * (float)M_PI * i / N);
|
||||
return a0 - a1 * x1;
|
||||
}
|
||||
|
||||
float blackman_i(int i, int N)
|
||||
{
|
||||
const float alpha = 0.16f; // or 2860/18608
|
||||
const float a0 = (1 - alpha) / 2;
|
||||
const float a1 = 1.0f / 2;
|
||||
const float a2 = alpha / 2;
|
||||
|
||||
float x1 = cosf(2 * (float)M_PI * i / N);
|
||||
float x2 = 2 * x1 * x1 - 1; // Use double angle formula
|
||||
|
||||
return a0 - a1 * x1 + a2 * x2;
|
||||
}
|
||||
|
||||
static float max2(float a, float b)
|
||||
{
|
||||
return (a >= b) ? a : b;
|
||||
}
|
||||
|
||||
// Compute FFT magnitudes (log power) for each timeslot in the signal
|
||||
void extract_power(const float signal[], waterfall_t *power, int block_size)
|
||||
{
|
||||
const int subblock_size = block_size / power->time_osr;
|
||||
const int nfft = block_size * power->freq_osr;
|
||||
const float fft_norm = 2.0f / nfft;
|
||||
const int len_window = 1.8f * block_size; // hand-picked and optimized
|
||||
|
||||
float window[nfft];
|
||||
for (int i = 0; i < nfft; ++i)
|
||||
{
|
||||
// window[i] = 1;
|
||||
// window[i] = hann_i(i, nfft);
|
||||
// window[i] = blackman_i(i, nfft);
|
||||
// window[i] = hamming_i(i, nfft);
|
||||
window[i] = (i < len_window) ? hann_i(i, len_window) : 0;
|
||||
}
|
||||
|
||||
size_t fft_work_size;
|
||||
kiss_fftr_alloc(nfft, 0, 0, &fft_work_size);
|
||||
|
||||
LOG(LOG_INFO, "Block size = %d\n", block_size);
|
||||
LOG(LOG_INFO, "Subblock size = %d\n", subblock_size);
|
||||
LOG(LOG_INFO, "N_FFT = %d\n", nfft);
|
||||
LOG(LOG_INFO, "FFT work area = %lu\n", fft_work_size);
|
||||
|
||||
void *fft_work = malloc(fft_work_size);
|
||||
kiss_fftr_cfg fft_cfg = kiss_fftr_alloc(nfft, 0, fft_work, &fft_work_size);
|
||||
|
||||
int offset = 0;
|
||||
float max_mag = -120.0f;
|
||||
for (int idx_block = 0; idx_block < power->num_blocks; ++idx_block)
|
||||
{
|
||||
// Loop over two possible time offsets (0 and block_size/2)
|
||||
for (int time_sub = 0; time_sub < power->time_osr; ++time_sub)
|
||||
{
|
||||
kiss_fft_scalar timedata[nfft];
|
||||
kiss_fft_cpx freqdata[nfft / 2 + 1];
|
||||
float mag_db[nfft / 2 + 1];
|
||||
|
||||
// Extract windowed signal block
|
||||
for (int pos = 0; pos < nfft; ++pos)
|
||||
{
|
||||
timedata[pos] = window[pos] * signal[(idx_block * block_size) + (time_sub * subblock_size) + pos];
|
||||
}
|
||||
|
||||
kiss_fftr(fft_cfg, timedata, freqdata);
|
||||
|
||||
// Compute log magnitude in decibels
|
||||
for (int idx_bin = 0; idx_bin < nfft / 2 + 1; ++idx_bin)
|
||||
{
|
||||
float mag2 = (freqdata[idx_bin].i * freqdata[idx_bin].i) + (freqdata[idx_bin].r * freqdata[idx_bin].r);
|
||||
mag_db[idx_bin] = 10.0f * log10f(1E-12f + mag2 * fft_norm * fft_norm);
|
||||
}
|
||||
|
||||
// Loop over two possible frequency bin offsets (for averaging)
|
||||
for (int freq_sub = 0; freq_sub < power->freq_osr; ++freq_sub)
|
||||
{
|
||||
for (int pos = 0; pos < power->num_bins; ++pos)
|
||||
{
|
||||
float db = mag_db[pos * power->freq_osr + freq_sub];
|
||||
// Scale decibels to unsigned 8-bit range and clamp the value
|
||||
// Range 0-240 covers -120..0 dB in 0.5 dB steps
|
||||
int scaled = (int)(2 * db + 240);
|
||||
|
||||
power->mag[offset] = (scaled < 0) ? 0 : ((scaled > 255) ? 255 : scaled);
|
||||
++offset;
|
||||
|
||||
if (db > max_mag)
|
||||
max_mag = db;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG(LOG_INFO, "Max magnitude: %.1f dB\n", max_mag);
|
||||
free(fft_work);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Expect one command-line argument
|
||||
if (argc < 2)
|
||||
{
|
||||
usage();
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *wav_path = argv[1];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Compute DSP parameters that depend on the sample rate
|
||||
const int num_bins = (int)(sample_rate / (2 * kFSK_dev)); // number bins of FSK tone width that the spectrum can be divided into
|
||||
const int block_size = (int)(sample_rate / kFSK_dev); // samples corresponding to one FSK symbol
|
||||
const int subblock_size = block_size / kTime_osr;
|
||||
const int nfft = block_size * kFreq_osr;
|
||||
const int num_blocks = (num_samples - nfft + subblock_size) / block_size;
|
||||
|
||||
LOG(LOG_INFO, "Sample rate %d Hz, %d blocks, %d bins\n", sample_rate, num_blocks, num_bins);
|
||||
|
||||
// Compute FFT over the whole signal and store it
|
||||
uint8_t mag_power[num_blocks * kFreq_osr * kTime_osr * num_bins];
|
||||
waterfall_t power = {
|
||||
.num_blocks = num_blocks,
|
||||
.num_bins = num_bins,
|
||||
.time_osr = kTime_osr,
|
||||
.freq_osr = kFreq_osr,
|
||||
.mag = mag_power};
|
||||
extract_power(signal, &power, block_size);
|
||||
|
||||
// Find top candidates by Costas sync score and localize them in time and frequency
|
||||
candidate_t candidate_list[kMax_candidates];
|
||||
int num_candidates = find_sync(&power, 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];
|
||||
|
||||
// Initialize hash table pointers
|
||||
for (int i = 0; i < kMax_decoded_messages; ++i)
|
||||
{
|
||||
decoded_hashtable[i] = NULL;
|
||||
}
|
||||
|
||||
// Go over candidates and attempt to decode messages
|
||||
for (int idx = 0; idx < num_candidates; ++idx)
|
||||
{
|
||||
const candidate_t *cand = &candidate_list[idx];
|
||||
if (cand->score < kMin_score)
|
||||
continue;
|
||||
|
||||
float freq_hz = (cand->freq_offset + (float)cand->freq_sub / kFreq_osr) * kFSK_dev;
|
||||
float time_sec = (cand->time_offset + (float)cand->time_sub / kTime_osr) / kFSK_dev;
|
||||
|
||||
message_t message;
|
||||
decode_status_t status;
|
||||
if (!decode(&power, cand, &message, kLDPC_iterations, &status))
|
||||
{
|
||||
if (status.ldpc_errors > 0)
|
||||
{
|
||||
LOG(LOG_DEBUG, "LDPC decode: %d errors\n", status.ldpc_errors);
|
||||
}
|
||||
else if (status.crc_calculated != status.crc_extracted)
|
||||
{
|
||||
LOG(LOG_DEBUG, "CRC mismatch!\n");
|
||||
}
|
||||
else if (status.unpack_status != 0)
|
||||
{
|
||||
LOG(LOG_DEBUG, "Error while unpacking!\n");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG(LOG_DEBUG, "Checking hash table for %4.1fs / %4.1fHz [%d]...\n", time_sec, freq_hz, cand->score);
|
||||
int idx_hash = message.hash % kMax_decoded_messages;
|
||||
bool found_empty_slot = false;
|
||||
bool found_duplicate = false;
|
||||
do
|
||||
{
|
||||
if (decoded_hashtable[idx_hash] == NULL)
|
||||
{
|
||||
LOG(LOG_DEBUG, "Found an empty slot\n");
|
||||
found_empty_slot = true;
|
||||
}
|
||||
else if ((decoded_hashtable[idx_hash]->hash == message.hash) && (0 == strcmp(decoded_hashtable[idx_hash]->text, message.text)))
|
||||
{
|
||||
LOG(LOG_DEBUG, "Found a duplicate [%s]\n", message.text);
|
||||
found_duplicate = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LOG_DEBUG, "Hash table clash!\n");
|
||||
// Move on to check the next entry in hash table
|
||||
idx_hash = (idx_hash + 1) % kMax_decoded_messages;
|
||||
}
|
||||
} while (!found_empty_slot && !found_duplicate);
|
||||
|
||||
if (found_empty_slot)
|
||||
{
|
||||
// Fill the empty hashtable slot
|
||||
memcpy(&decoded[idx_hash], &message, sizeof(message));
|
||||
decoded_hashtable[idx_hash] = &decoded[idx_hash];
|
||||
++num_decoded;
|
||||
|
||||
// Fake WSJT-X-like output for now
|
||||
int snr = 0; // TODO: compute SNR
|
||||
printf("000000 %3d %+4.2f %4.0f ~ %s\n", cand->score, time_sec, freq_hz, message.text);
|
||||
}
|
||||
}
|
||||
LOG(LOG_INFO, "Decoded %d messages\n", num_decoded);
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "wave.h"
|
||||
#include "debug.h"
|
||||
#include "ft8_pack.h"
|
||||
#include "ft8_encode.h"
|
||||
#include "ft8_constants.h"
|
||||
|
||||
#define LOG_LEVEL LOG_INFO
|
||||
|
||||
#define FT8_SLOT_TIME 15.0f // total length of output waveform in seconds
|
||||
#define FT8_SYMBOL_RATE 6.25f // tone deviation (and symbol rate) in Hz
|
||||
#define FT8_SYMBOL_BT 2.0f // symbol smoothing filter bandwidth factor (BT)
|
||||
|
||||
#define FT4_SLOT_TIME 7.5f // total length of output waveform in seconds
|
||||
#define FT4_SYMBOL_RATE 20.833333f // tone deviation (and symbol rate) in Hz
|
||||
#define FT4_SYMBOL_BT 1.0f // symbol smoothing filter bandwidth factor (BT)
|
||||
|
||||
#define GFSK_CONST_K 5.336446f // pi * sqrt(2 / log(2))
|
||||
|
||||
/// Computes a GFSK smoothing pulse.
|
||||
/// The pulse is theoretically infinitely long, however, here it's truncated at 3 times the symbol length.
|
||||
/// This means the pulse array has to have space for 3*n_spsym elements.
|
||||
/// @param[in] n_spsym Number of samples per symbol
|
||||
/// @param[in] b Shape parameter (values defined for FT8/FT4)
|
||||
/// @param[out] pulse Output array of pulse samples
|
||||
///
|
||||
void gfsk_pulse(int n_spsym, float symbol_bt, float *pulse)
|
||||
{
|
||||
int i = 0;
|
||||
for (i = 0; i < 3 * n_spsym; ++i)
|
||||
{
|
||||
float t = i / (float)n_spsym - 1.5f;
|
||||
float arg1 = GFSK_CONST_K * symbol_bt * (t + 0.5f);
|
||||
float arg2 = GFSK_CONST_K * symbol_bt * (t - 0.5f);
|
||||
pulse[i] = (erff(arg1) - erff(arg2)) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthesize waveform data using GFSK phase shaping.
|
||||
/// The output waveform will contain n_sym symbols.
|
||||
/// @param[in] symbols Array of symbols (tones) (0-7 for FT8)
|
||||
/// @param[in] n_sym Number of symbols in the symbol array
|
||||
/// @param[in] f0 Audio frequency in Hertz for the symbol 0 (base frequency)
|
||||
/// @param[in] symbol_bt Symbol smoothing filter bandwidth (2 for FT8, 1 for FT4)
|
||||
/// @param[in] symbol_rate Rate of symbols per second, Hertz
|
||||
/// @param[in] signal_rate Sample rate of synthesized signal, Hertz
|
||||
/// @param[out] signal Output array of signal waveform samples (should have space for n_sym*n_spsym samples)
|
||||
///
|
||||
void synth_gfsk(const uint8_t *symbols, int n_sym, float f0, float symbol_bt, float symbol_rate, int signal_rate, float *signal)
|
||||
{
|
||||
int n_spsym = (int)(0.5f + signal_rate / symbol_rate); // Samples per symbol
|
||||
int n_wave = n_sym * n_spsym; // Number of output samples
|
||||
float hmod = 1.0f;
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int k = 0;
|
||||
|
||||
LOG(LOG_DEBUG, "n_spsym = %d\n", n_spsym);
|
||||
// Compute the smoothed frequency waveform.
|
||||
// Length = (nsym+2)*n_spsym samples, first and last symbols extended
|
||||
float dphi_peak = 2 * M_PI * hmod / n_spsym;
|
||||
float dphi[n_wave + 2 * n_spsym];
|
||||
|
||||
// Shift frequency up by f0
|
||||
for (i = 0; i < n_wave + 2 * n_spsym; ++i)
|
||||
{
|
||||
dphi[i] = 2 * M_PI * f0 / signal_rate;
|
||||
}
|
||||
|
||||
float pulse[3 * n_spsym];
|
||||
gfsk_pulse(n_spsym, symbol_bt, pulse);
|
||||
|
||||
for (i = 0; i < n_sym; ++i)
|
||||
{
|
||||
int ib = i * n_spsym;
|
||||
for (j = 0; j < 3 * n_spsym; ++j)
|
||||
{
|
||||
dphi[j + ib] += dphi_peak * symbols[i] * pulse[j];
|
||||
}
|
||||
}
|
||||
|
||||
// Add dummy symbols at beginning and end with tone values equal to 1st and last symbol, respectively
|
||||
for (j = 0; j < 2 * n_spsym; ++j)
|
||||
{
|
||||
dphi[j] += dphi_peak * pulse[j + n_spsym] * symbols[0];
|
||||
dphi[j + n_sym * n_spsym] += dphi_peak * pulse[j] * symbols[n_sym - 1];
|
||||
}
|
||||
|
||||
// Calculate and insert the audio waveform
|
||||
float phi = 0;
|
||||
for (k = 0; k < n_wave; ++k)
|
||||
{ // Don't include dummy symbols
|
||||
signal[k] = sinf(phi);
|
||||
phi = fmodf(phi + dphi[k + n_spsym], 2 * M_PI);
|
||||
}
|
||||
|
||||
// Apply envelope shaping to the first and last symbols
|
||||
int n_ramp = n_spsym / 8;
|
||||
for (i = 0; i < n_ramp; ++i)
|
||||
{
|
||||
float env = (1 - cosf(2 * M_PI * i / (2 * n_ramp))) / 2;
|
||||
signal[i] *= env;
|
||||
signal[n_wave - 1 - i] *= env;
|
||||
}
|
||||
}
|
||||
|
||||
void usage()
|
||||
{
|
||||
printf("Generate a 15-second WAV file encoding a given message.\n");
|
||||
printf("Usage:\n");
|
||||
printf("\n");
|
||||
printf("gen_ft8 MESSAGE WAV_FILE [FREQUENCY]\n");
|
||||
printf("\n");
|
||||
printf("(Note that you might have to enclose your message in quote marks if it contains spaces)\n");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Expect two command-line arguments
|
||||
if (argc < 3)
|
||||
{
|
||||
usage();
|
||||
return -1;
|
||||
}
|
||||
|
||||
int j = 0;
|
||||
int i = 0;
|
||||
|
||||
const char *message = argv[1];
|
||||
const char *wav_path = argv[2];
|
||||
float frequency = 1000.0;
|
||||
if (argc > 3)
|
||||
{
|
||||
frequency = atof(argv[3]);
|
||||
}
|
||||
bool is_ft4 = (argc > 4) && (0 == strcmp(argv[4], "-ft4"));
|
||||
|
||||
// First, pack the text data into binary message
|
||||
uint8_t packed[FT8_LDPC_K_BYTES];
|
||||
int rc = pack77(message, packed);
|
||||
if (rc < 0)
|
||||
{
|
||||
printf("Cannot parse message!\n");
|
||||
printf("RC = %d\n", rc);
|
||||
return -2;
|
||||
}
|
||||
|
||||
printf("Packed data: ");
|
||||
for (j = 0; j < 10; ++j)
|
||||
{
|
||||
printf("%02x ", packed[j]);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
if (is_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 (i = 0; i < 10; ++i)
|
||||
{
|
||||
packed[i] ^= kFT4_XOR_sequence[i];
|
||||
}
|
||||
}
|
||||
|
||||
int num_tones = (is_ft4) ? FT4_NN : FT8_NN;
|
||||
float symbol_rate = (is_ft4) ? FT4_SYMBOL_RATE : FT8_SYMBOL_RATE;
|
||||
float symbol_bt = (is_ft4) ? FT4_SYMBOL_BT : FT8_SYMBOL_BT;
|
||||
float slot_time = (is_ft4) ? FT4_SLOT_TIME : FT8_SLOT_TIME;
|
||||
|
||||
// Second, encode the binary message as a sequence of FSK tones
|
||||
uint8_t tones[num_tones]; // Array of 79 tones (symbols)
|
||||
if (is_ft4)
|
||||
{
|
||||
genft4(packed, tones);
|
||||
}
|
||||
else
|
||||
{
|
||||
genft8(packed, tones);
|
||||
}
|
||||
|
||||
printf("FSK tones: ");
|
||||
for (j = 0; j < num_tones; ++j)
|
||||
{
|
||||
printf("%d", tones[j]);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
// Third, convert the FSK tones into an audio signal
|
||||
int sample_rate = 12000;
|
||||
int num_samples = (int)(0.5f + num_tones / symbol_rate * sample_rate); // Number of samples in the data signal
|
||||
int num_silence = (slot_time * sample_rate - num_samples) / 2; // Silence padding at both ends to make 15 seconds
|
||||
int num_total_samples = num_silence + num_samples + num_silence; // Number of samples in the padded signal
|
||||
float signal[num_total_samples];
|
||||
for (i = 0; i < num_silence; i++)
|
||||
{
|
||||
signal[i] = 0;
|
||||
signal[i + num_samples + num_silence] = 0;
|
||||
}
|
||||
|
||||
// Synthesize waveform data (signal) and save it as WAV file
|
||||
synth_gfsk(tones, num_tones, frequency, symbol_bt, symbol_rate, sample_rate, signal + num_silence);
|
||||
save_wav(signal, num_total_samples, sample_rate, wav_path);
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "ft8_text.h"
|
||||
#include "ft8_pack.h"
|
||||
#include "ft8_encode.h"
|
||||
#include "ft8_constants.h"
|
||||
|
||||
#include "kiss_fftr.h"
|
||||
#include "debug.h"
|
||||
|
||||
#define LOG_LEVEL LOG_INFO
|
||||
|
||||
void convert_8bit_to_6bit(uint8_t *dst, const uint8_t *src, int nBits)
|
||||
{
|
||||
// Zero-fill the destination array as we will only be setting bits later
|
||||
for (int j = 0; j < (nBits + 5) / 6; ++j)
|
||||
{
|
||||
dst[j] = 0;
|
||||
}
|
||||
|
||||
// Set the relevant bits
|
||||
uint8_t mask_src = (1 << 7);
|
||||
uint8_t mask_dst = (1 << 5);
|
||||
for (int i = 0, j = 0; nBits > 0; --nBits)
|
||||
{
|
||||
if (src[i] & mask_src)
|
||||
{
|
||||
dst[j] |= mask_dst;
|
||||
}
|
||||
mask_src >>= 1;
|
||||
if (mask_src == 0)
|
||||
{
|
||||
mask_src = (1 << 7);
|
||||
++i;
|
||||
}
|
||||
mask_dst >>= 1;
|
||||
if (mask_dst == 0)
|
||||
{
|
||||
mask_dst = (1 << 5);
|
||||
++j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
bool test1() {
|
||||
//const char *msg = "CQ DL7ACA JO40"; // 62, 32, 32, 49, 37, 27, 59, 2, 30, 19, 49, 16
|
||||
const char *msg = "VA3UG F1HMR 73"; // 52, 54, 60, 12, 55, 54, 7, 19, 2, 23, 59, 16
|
||||
//const char *msg = "RA3Y VE3NLS 73"; // 46, 6, 32, 22, 55, 20, 11, 32, 53, 23, 59, 16
|
||||
uint8_t a72[9];
|
||||
|
||||
int rc = packmsg(msg, a72);
|
||||
if (rc < 0) return false;
|
||||
|
||||
LOG(LOG_INFO, "8-bit packed: ");
|
||||
for (int i = 0; i < 9; ++i) {
|
||||
LOG(LOG_INFO, "%02x ", a72[i]);
|
||||
}
|
||||
LOG(LOG_INFO, "\n");
|
||||
|
||||
uint8_t a72_6bit[12];
|
||||
convert_8bit_to_6bit(a72_6bit, a72, 72);
|
||||
LOG(LOG_INFO, "6-bit packed: ");
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
LOG(LOG_INFO, "%d ", a72_6bit[i]);
|
||||
}
|
||||
LOG(LOG_INFO, "\n");
|
||||
|
||||
char msg_out_raw[14];
|
||||
unpack(a72, msg_out_raw);
|
||||
|
||||
char msg_out[14];
|
||||
fmtmsg(msg_out, msg_out_raw);
|
||||
LOG(LOG_INFO, "msg_out = [%s]\n", msg_out);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void test2() {
|
||||
uint8_t test_in[11] = { 0xF1, 0x02, 0x03, 0x04, 0x05, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xFF };
|
||||
uint8_t test_out[22];
|
||||
|
||||
encode174(test_in, test_out);
|
||||
|
||||
for (int j = 0; j < 22; ++j) {
|
||||
LOG(LOG_INFO, "%02x ", test_out[j]);
|
||||
}
|
||||
LOG(LOG_INFO, "\n");
|
||||
}
|
||||
|
||||
|
||||
void test3() {
|
||||
uint8_t test_in2[10] = { 0x11, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x10, 0x04, 0x01, 0x00 };
|
||||
uint16_t crc1 = ft8_crc(test_in2, 76); // Calculate CRC of 76 bits only
|
||||
LOG(LOG_INFO, "CRC: %04x\n", crc1); // should be 0x0708
|
||||
}
|
||||
*/
|
||||
|
||||
void test_tones(float *log174)
|
||||
{
|
||||
// Just a test case
|
||||
for (int i = 0; i < FT8_ND; ++i)
|
||||
{
|
||||
const uint8_t inv_map[8] = {0, 1, 3, 2, 6, 4, 5, 7};
|
||||
uint8_t tone = ("0000000011721762454112705354533170166234757420515470163426"[i]) - '0';
|
||||
uint8_t b3 = inv_map[tone];
|
||||
log174[3 * i] = (b3 & 4) ? +1.0 : -1.0;
|
||||
log174[3 * i + 1] = (b3 & 2) ? +1.0 : -1.0;
|
||||
log174[3 * i + 2] = (b3 & 1) ? +1.0 : -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
void test4()
|
||||
{
|
||||
const int nfft = 128;
|
||||
const float fft_norm = 2.0 / nfft;
|
||||
|
||||
size_t fft_work_size;
|
||||
kiss_fftr_alloc(nfft, 0, 0, &fft_work_size);
|
||||
|
||||
printf("N_FFT = %d\n", nfft);
|
||||
printf("FFT work area = %lu\n", fft_work_size);
|
||||
|
||||
void *fft_work = malloc(fft_work_size);
|
||||
kiss_fftr_cfg fft_cfg = kiss_fftr_alloc(nfft, 0, fft_work, &fft_work_size);
|
||||
|
||||
kiss_fft_scalar window[nfft];
|
||||
for (int i = 0; i < nfft; ++i)
|
||||
{
|
||||
window[i] = sinf(i * 2 * (float)M_PI / nfft);
|
||||
}
|
||||
|
||||
kiss_fft_cpx freqdata[nfft / 2 + 1];
|
||||
kiss_fftr(fft_cfg, window, freqdata);
|
||||
|
||||
float mag_db[nfft];
|
||||
// Compute log magnitude in decibels
|
||||
for (int j = 0; j < nfft / 2 + 1; ++j)
|
||||
{
|
||||
float mag2 = (freqdata[j].i * freqdata[j].i + freqdata[j].r * freqdata[j].r);
|
||||
mag_db[j] = 10.0f * log10f(1E-10f + mag2 * fft_norm * fft_norm);
|
||||
}
|
||||
|
||||
printf("F[0] = %.1f dB\n", mag_db[0]);
|
||||
printf("F[1] = %.3f dB\n", mag_db[1]);
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
//test1();
|
||||
test4();
|
||||
|
||||
return 0;
|
||||
}
|
Ładowanie…
Reference in New Issue