TinyModem
Header-only software audio modem library
Loading...
Searching...
No Matches
CSSDecoder.h
Go to the documentation of this file.
1#pragma once
2
3#include <math.h>
4#include <string.h>
5
6#include "AudioTools.h"
7#include "CSS.h"
8#include "DSPUtils.h"
9#include "ModemBase.h"
10
11namespace tinymodem {
12
31class CSSDecoder : public ModemDecoder {
32 public:
33 CSSDecoder() = default;
34 CSSDecoder(Print& out) { setOutput(out); }
35
38
40 void setAudioInfo(audio_tools::AudioInfo info) override {
41 if (info.sample_rate == cfg.sample_rate && info.channels == cfg.channels &&
42 info.bits_per_sample == cfg.bits_per_sample)
43 return;
44 cfg.sample_rate = info.sample_rate;
45 cfg.channels = info.channels;
46 cfg.bits_per_sample = info.bits_per_sample;
47 if (is_active) begin();
48 }
49
50 audio_tools::AudioInfo audioInfo() override { return cfg; }
51
52 using audio_tools::AudioDecoder::begin;
53
56 cfg = config;
57 return begin();
58 }
59
60 bool begin() override {
61 is_active = false;
62 if (!cfg.isValid()) return false;
63 if (!input.begin(cfg.bits_per_sample, cfg.channels, cfg.channel))
64 return false;
65 n = cfg.chips();
66 m = OVERSAMPLING * n;
67 if (!fft.begin(m)) return false;
68 // baseband with twice the bandwidth as sample rate
69 converter.begin(cfg.sample_rate, OVERSAMPLING * cfg.bandwidth,
70 0.5f / OVERSAMPLING);
71 double w = 2.0 * M_PI * cfg.center_freq / cfg.sample_rate;
72 mix_step_re = cos(w);
73 mix_step_im = -sin(w);
74 ring_re.resize(RING * m);
75 ring_im.resize(RING * m);
76 fft_re.resize(m);
77 fft_im.resize(m);
78 power.resize(m);
79 chirp_re.resize(m);
80 chirp_im.resize(m);
81 for (int k = 0; k < m; k++) {
82 // baseband up chirp from -B/2 to B/2: the phase is the integral of
83 // the frequency
84 double u = (double)k / m; // 0 - 1 within the symbol
85 double ph = 2.0 * M_PI * n * (0.5 * u * u - 0.5 * u);
86 chirp_re[k] = cos(ph);
87 chirp_im[k] = sin(ph);
88 }
91 frames = errors = 0;
92 resetState();
93 is_active = true;
94 return true;
95 }
96
98 void end() override {
99 if (!is_active) return;
100 flush();
101 status("### CSS frames=%u errors=%u ###\n", (unsigned)frames,
102 (unsigned)errors);
103 is_active = false;
104 }
105
107 void flush() override {
108 if (!is_active) return;
109 // silence: the last symbol and the delay of the filters
110 size_t len = (size_t)((2.0 * n + 64) / cfg.bandwidth * cfg.sample_rate);
111 for (size_t j = 0; j < len && !is_finished; j++) addSample(0.0f);
112 if (state >= HEADER) {
113 errors++;
114 endOfCarrier(false);
115 }
116 bool finished = is_finished;
117 resetState();
118 is_finished = finished;
119 }
120
122 size_t write(const uint8_t* data, size_t len) override {
123 if (!is_active) return 0;
124 input.write(data, len, [this](float x) {
125 if (cfg.rx_noise != 0.0f) x += noise() * cfg.rx_noise * 2.0f;
126 addSample(x);
127 });
128 return len;
129 }
130
132 bool isCarrier() override { return state >= HEADER; }
133
135 bool isFinished() override { return is_finished; }
136
138 size_t frameCount() { return frames; }
139
141 size_t errorCount() { return errors; }
142
144 float frequencyOffset() { return freq_offset * cfg.bandwidth / n; }
145
147 CSSConfig& config() { return cfg; }
148
149 operator bool() override { return is_active; }
150
151 protected:
153 static constexpr int RING = 4; // buffered symbols
157 static constexpr int OVERSAMPLING = 2;
158 static constexpr int PREAMBLE_HITS = 3; // windows with the same bin
160 static constexpr float TRACKING_GAIN = 0.3f;
162 static constexpr float DRIFT_GAIN = 0.02f;
164 static constexpr float MIN_DRIFT = 0.08f;
166 static constexpr int MAX_UPS = 16;
167
169 struct Peak {
170 int bin = 0;
171 float pos = 0; // with fractional part
172 float power = 0; // power of the peak bin
173 float ratio = 0; // peak power / mean power of the other bins
174 };
175
177 bool is_active = false;
178 bool is_finished = false;
180 uint32_t noise_seed = 1;
181 int n = 128; // chips (bins) per symbol
182 int m = 256; // baseband samples per symbol
184 SampleRateConverter converter; // complex baseband
185 float mix_re = 1, mix_im = 0, mix_step_re = 1, mix_step_im = 0;
186 uint32_t mix_count = 0;
187 audio_tools::Vector<float> ring_re, ring_im, fft_re, fft_im, power;
188 audio_tools::Vector<float> chirp_re, chirp_im;
189 int64_t count = 0; // number of baseband samples
190 int64_t next_start = 0; // start of the next window
191 // receiver state
193 int hits = 0, candidate = 0;
195 float last_up = 0; // signed peak of the last preamble window
196 float ups[MAX_UPS]; // peaks of the preamble windows after the alignment
197 int nups = 0;
198 float drift = 0; // change of the peak per symbol (different clock)
199 float rate_factor = 1; // clock of the sender relative to our clock
200 float sync_offset = 0; // frequency offset of the sync in bins
201 float freq_offset = 0; // in bins
202 audio_tools::Vector<uint16_t> symbols;
203 int nsymbols = 0, needed = 0, frame_len = 0;
204 float snr_total = 0;
205 size_t frames = 0, errors = 0;
206
207 void resetState() {
208 state = SEARCH;
209 hits = 0;
210 count = 0;
211 next_start = 0;
212 mix_re = 1;
213 mix_im = 0;
214 converter.reset();
215 is_finished = false;
216 input.reset();
217 }
218
220 float noise() {
221 noise_seed = noise_seed * 1103515245u + 12345u;
222 return ((noise_seed >> 8) & 0xFFFF) / 65535.0f - 0.5f;
223 }
224
226 void addSample(float x) {
227 if (is_finished) return;
228 float i = x * mix_re, q = x * mix_im;
229 float r = mix_re * mix_step_re - mix_im * mix_step_im;
231 mix_re = r;
232 if (++mix_count % 1024 == 0) {
233 float mag = sqrtf(mix_re * mix_re + mix_im * mix_im);
234 mix_re /= mag;
235 mix_im /= mag;
236 }
237 converter.writeComplex(i, q, [this](float re, float im) {
238 addBaseband(re, im);
239 });
240 }
241
242 void addBaseband(float re, float im) {
243 int idx = count % (RING * m);
244 // factor 2: the mixer provides half of the amplitude
245 ring_re[idx] = 2 * re;
246 ring_im[idx] = 2 * im;
247 count++;
248 while (!is_finished && count >= next_start + m) processWindow();
249 }
250
254 Peak spectrum(bool up, float offset) {
255 double w = -2.0 * M_PI * offset / m;
256 float rot_re = cos(w), rot_im = sin(w);
257 float c_re = 1, c_im = 0;
258 for (int k = 0; k < m; k++) {
259 int idx = (next_start + k) % (RING * m);
260 float r_re = ring_re[idx], r_im = ring_im[idx];
261 // up: multiply with conj(chirp), down: with chirp
262 float ch_re = chirp_re[k], ch_im = up ? -chirp_im[k] : chirp_im[k];
263 float y_re = r_re * ch_re - r_im * ch_im;
264 float y_im = r_re * ch_im + r_im * ch_re;
265 fft_re[k] = y_re * c_re - y_im * c_im;
266 fft_im[k] = y_re * c_im + y_im * c_re;
267 float t = c_re * rot_re - c_im * rot_im;
268 c_im = c_re * rot_im + c_im * rot_re;
269 c_re = t;
270 }
271 fft.process(fft_re.data(), fft_im.data());
272 for (int k = 0; k < m; k++)
273 power[k] = fft_re[k] * fft_re[k] + fft_im[k] * fft_im[k];
274 // the part after the wrap around is a tone n bins lower: we add both
275 Peak result;
276 float total = 0, best = 0;
277 for (int k = 0; k < n; k++) {
278 float p = power[k] + power[k + n];
279 total += p;
280 if (p > best) {
281 best = p;
282 result.bin = k;
283 }
284 }
285 result.power = best;
286 float mean = (total - best) / (n - 1);
287 result.ratio = mean > 0 ? best / mean : 0;
288 // fractional position of the stronger tone (Quinn's first estimator,
289 // which is exact for a rectangular window)
290 int k = power[result.bin] >= power[result.bin + n] ? result.bin
291 : result.bin + n;
292 int l = (k + m - 1) % m, r = (k + 1) % m;
293 float pk = power[k];
294 float delta = 0;
295 if (pk > 0) {
296 float a1 = (fft_re[l] * fft_re[k] + fft_im[l] * fft_im[k]) / pk;
297 float a2 = (fft_re[r] * fft_re[k] + fft_im[r] * fft_im[k]) / pk;
298 float d1 = a1 / (1.0f - a1), d2 = -a2 / (1.0f - a2);
299 delta = (d1 > 0 && d2 > 0) ? d2 : d1;
300 if (delta > 0.5f) delta = 0.5f;
301 if (delta < -0.5f) delta = -0.5f;
302 }
303 result.pos = result.bin + delta;
304 return result;
305 }
306
308 float signedBin(float pos) {
309 while (pos > n / 2) pos -= n;
310 while (pos <= -n / 2) pos += n;
311 return pos;
312 }
313
315 switch (state) {
316 case SEARCH:
317 search();
318 break;
319 case SYNC:
320 sync();
321 break;
322 default:
323 data();
324 break;
325 }
326 }
327
329 void search() {
330 Peak up = spectrum(true, 0);
331 if (up.ratio >= cfg.detect_threshold) {
332 // the bin may move a little with a different clock of the sender
333 int dist = abs((int)signedBin(up.bin - candidate));
334 hits = hits > 0 && dist <= 2 ? hits + 1 : 1;
335 candidate = up.bin;
336 } else {
337 hits = 0;
338 }
339 if (hits >= PREAMBLE_HITS) {
340 // the window start moves to the symbol start (plus the frequency
341 // offset, which is determined with the down chirps)
342 int shift = -lroundf(OVERSAMPLING * signedBin(up.pos));
343 next_start += m + shift;
344 state = SYNC;
345 nups = 0;
346 sync_windows = 0;
347 last_up = 0;
348 hits = 0;
349 return;
350 }
351 next_start += m;
352 }
353
356 void sync() {
357 Peak up = spectrum(true, 0);
358 Peak down = spectrum(false, 0);
359 if (down.power > up.power && down.ratio >= cfg.detect_threshold) {
360 // a different clock of the sender changes the timing by drift per
361 // symbol: up = tau + offset, down = -(tau + drift) + offset
363 float p_down = signedBin(down.pos);
364 float tau = (last_up - p_down - drift) / 2;
365 float offset = (last_up + p_down + drift) / 2;
366 // timing of the first down chirp
367 tau += drift;
368 // a clear drift is caused by a different clock: from now on we sample
369 // with the clock of the sender, which also corrects the chirp rate. A
370 // small drift is followed by the tracking.
371 rate_factor = fabsf(drift) > MIN_DRIFT ? 1.0f + drift / n : 1.0f;
372 converter.setRateFactor(rate_factor);
373 drift = 0;
374 // the window moves by whole samples, the rest is part of the frequency
375 // offset (a fractional time offset is harmless with the oversampling)
376 int shift = lroundf(OVERSAMPLING * tau);
377 freq_offset = offset + (OVERSAMPLING * tau - shift) / OVERSAMPLING;
379 // skip the second down chirp
380 next_start += 2 * m - shift;
381 nsymbols = 0;
383 snr_total = 0;
384 state = HEADER;
385 status("### CARRIER css sf%d %.0f Hz @ %.1f Hz ###\n",
386 cfg.spreading_factor, (double)cfg.bandwidth,
387 (double)(cfg.center_freq + frequencyOffset()));
388 return;
389 }
390 last_up = signedBin(up.pos);
391 if (nups < MAX_UPS) ups[nups++] = last_up;
392 next_start += m;
393 if (++sync_windows > cfg.preamble_symbols + 4 ||
394 up.ratio < cfg.detect_threshold) {
395 state = SEARCH;
396 hits = 0;
397 }
398 }
399
402 if (nups < 2) return 0;
403 float sx = 0, sy = 0, sxx = 0, sxy = 0;
404 for (int j = 0; j < nups; j++) {
405 sx += j;
406 sy += ups[j];
407 sxx += (float)j * j;
408 sxy += j * ups[j];
409 }
410 float d = nups * sxx - sx * sx;
411 return d > 0 ? (nups * sxy - sx * sy) / d : 0;
412 }
413
415 void data() {
416 Peak peak = spectrum(true, freq_offset);
417 int value = peak.bin;
418 snr_total += peak.ratio;
419 // second order loop: the remaining offset of the peak (frequency error
420 // and the timing drift of a remaining clock difference) is corrected in
421 // the frequency domain. With the oversampling a small time offset of the
422 // window only costs a little energy.
423 float error = signedBin(peak.pos - value);
424 freq_offset += TRACKING_GAIN * error + drift;
425 drift += DRIFT_GAIN * error;
426 // a change of a whole bin since the sync is caused by a timing drift: we
427 // move the window by one chip, which moves the peak by one bin
428 int shift = 0;
429 if (freq_offset - sync_offset > 1.0f) shift = 1;
430 if (freq_offset - sync_offset < -1.0f) shift = -1;
431 freq_offset -= shift;
432 next_start += m - OVERSAMPLING * shift;
433 symbols[nsymbols++] = value;
434 if (nsymbols < needed) return;
435
436 int sf = cfg.spreading_factor;
437 if (state == HEADER) {
438 uint8_t header[CSSFrame::HEADER_BYTES];
439 CSSFrame::fromSymbols(symbols.data(), nsymbols, sf, header,
440 sizeof(header));
442 if (frame_len < 0) {
443 errors++;
444 endOfCarrier(false);
445 return;
446 }
447 state = PAYLOAD;
448 nsymbols = 0;
450 CSSFrame::payloadBytes(frame_len, cfg.ecc_bytes), sf);
451 return;
452 }
453 uint8_t payload[CSSConfig::MAX_FRAME + 2 + RSCodec::MAX_ECC];
454 int len = CSSFrame::payloadBytes(frame_len, cfg.ecc_bytes);
455 CSSFrame::fromSymbols(symbols.data(), nsymbols, sf, payload, len);
456 int corrected = CSSFrame::decodePayload(payload, frame_len, cfg.ecc_bytes);
457 if (corrected < 0) {
458 errors++;
459 endOfCarrier(false);
460 return;
461 }
462 frames++;
463 if (p_print != nullptr) p_print->write(payload, frame_len);
464 endOfCarrier(true, corrected);
465 }
466
467 void endOfCarrier(bool ok, int corrected = 0) {
468 rate_factor = 1;
469 converter.setRateFactor(1);
470 int total = nsymbols + (state == PAYLOAD
472 cfg.spreading_factor)
473 : 0);
474 float snr = total > 0 ? 10 * log10f(snr_total / total) : 0;
475 if (ok)
476 status("\n### NOCARRIER ndata=%d corrected=%d snr=%.1f dB ###\n",
477 frame_len, corrected, (double)snr);
478 else
479 status("\n### NOCARRIER frame error snr=%.1f dB ###\n", (double)snr);
480 state = SEARCH;
481 hits = 0;
482 if (cfg.rx_one) is_finished = true;
483 }
484};
485
486} // namespace tinymodem
static constexpr float TRACKING_GAIN
tracking of the remaining offset per symbol
Definition CSSDecoder.h:160
bool begin(CSSConfig config)
Starts the processing with the indicated configuration.
Definition CSSDecoder.h:55
float sync_offset
Definition CSSDecoder.h:200
void sync()
Definition CSSDecoder.h:356
CSSConfig defaultConfig()
Provides a default configuration: css7 (SF 7, 1000 Hz).
Definition CSSDecoder.h:37
CSSDecoder(Print &out)
Definition CSSDecoder.h:34
float frequencyOffset()
Frequency offset of the last frame in Hz.
Definition CSSDecoder.h:144
bool isCarrier() override
true while a frame is received
Definition CSSDecoder.h:132
float snr_total
Definition CSSDecoder.h:204
bool is_active
Definition CSSDecoder.h:177
float drift
Definition CSSDecoder.h:198
float noise()
uniform random value in the range -0.5 to 0.5
Definition CSSDecoder.h:220
audio_tools::Vector< float > fft_re
Definition CSSDecoder.h:187
audio_tools::Vector< float > power
Definition CSSDecoder.h:187
size_t errorCount()
Number of frames which could not be decoded.
Definition CSSDecoder.h:141
State
Definition CSSDecoder.h:152
@ PAYLOAD
Definition CSSDecoder.h:152
@ SYNC
Definition CSSDecoder.h:152
@ HEADER
Definition CSSDecoder.h:152
@ SEARCH
Definition CSSDecoder.h:152
static constexpr int MAX_UPS
max preamble windows for the drift estimation
Definition CSSDecoder.h:166
void endOfCarrier(bool ok, int corrected=0)
Definition CSSDecoder.h:467
bool isFinished() override
true if the decoding was stopped (rx_one)
Definition CSSDecoder.h:135
int n
Definition CSSDecoder.h:181
float signedBin(float pos)
signed value of a bin (-n/2 to n/2)
Definition CSSDecoder.h:308
bool begin() override
Definition CSSDecoder.h:60
int m
Definition CSSDecoder.h:182
float mix_step_im
Definition CSSDecoder.h:185
audio_tools::Vector< float > fft_im
Definition CSSDecoder.h:187
uint32_t mix_count
Definition CSSDecoder.h:186
size_t errors
Definition CSSDecoder.h:205
bool is_finished
Definition CSSDecoder.h:178
size_t frames
Definition CSSDecoder.h:205
int nsymbols
Definition CSSDecoder.h:203
float rate_factor
Definition CSSDecoder.h:199
CSSConfig cfg
Definition CSSDecoder.h:176
static constexpr int OVERSAMPLING
Definition CSSDecoder.h:157
CSSConfig & config()
The configuration which was provided in begin().
Definition CSSDecoder.h:147
audio_tools::Vector< float > chirp_im
Definition CSSDecoder.h:188
void resetState()
Definition CSSDecoder.h:207
SampleRateConverter converter
Definition CSSDecoder.h:184
float mix_step_re
Definition CSSDecoder.h:185
int nups
Definition CSSDecoder.h:197
void end() override
Processes the remaining samples and reports the statistics.
Definition CSSDecoder.h:98
static constexpr int RING
Definition CSSDecoder.h:153
size_t frameCount()
Number of received frames with a correct CRC.
Definition CSSDecoder.h:138
audio_tools::Vector< uint16_t > symbols
Definition CSSDecoder.h:202
int needed
Definition CSSDecoder.h:203
float freq_offset
Definition CSSDecoder.h:201
void addBaseband(float re, float im)
Definition CSSDecoder.h:242
audio_tools::Vector< float > chirp_re
Definition CSSDecoder.h:188
float ups[MAX_UPS]
Definition CSSDecoder.h:196
static constexpr int PREAMBLE_HITS
Definition CSSDecoder.h:158
audio_tools::AudioInfo audioInfo() override
Definition CSSDecoder.h:50
int64_t count
Definition CSSDecoder.h:189
void flush() override
Processes the remaining samples (e.g. at the end of the input).
Definition CSSDecoder.h:107
void processWindow()
Definition CSSDecoder.h:314
audio_tools::Vector< float > ring_re
Definition CSSDecoder.h:187
int candidate
Definition CSSDecoder.h:193
void setAudioInfo(audio_tools::AudioInfo info) override
Defines the format of the input audio data.
Definition CSSDecoder.h:40
void data()
Header and payload symbols.
Definition CSSDecoder.h:415
void addSample(float x)
Mixes a sample to the baseband and resamples it to the bandwidth.
Definition CSSDecoder.h:226
SimpleFFT fft
Definition CSSDecoder.h:183
int64_t next_start
Definition CSSDecoder.h:190
uint32_t noise_seed
Definition CSSDecoder.h:180
State state
Definition CSSDecoder.h:192
int sync_windows
Definition CSSDecoder.h:194
static constexpr float DRIFT_GAIN
tracking of the remaining drift per symbol
Definition CSSDecoder.h:162
audio_tools::Vector< float > ring_im
Definition CSSDecoder.h:187
Peak spectrum(bool up, float offset)
Definition CSSDecoder.h:254
static constexpr float MIN_DRIFT
min drift in bins per symbol for the correction of the sample rate
Definition CSSDecoder.h:164
float mix_re
Definition CSSDecoder.h:185
int frame_len
Definition CSSDecoder.h:203
float estimateDrift()
Change of the preamble peaks per symbol (least squares fit).
Definition CSSDecoder.h:401
void search()
Preamble: the same bin in consecutive windows.
Definition CSSDecoder.h:329
int hits
Definition CSSDecoder.h:193
PcmInput input
Definition CSSDecoder.h:179
float last_up
Definition CSSDecoder.h:195
float mix_im
Definition CSSDecoder.h:185
size_t write(const uint8_t *data, size_t len) override
Provide PCM data to the decoder.
Definition CSSDecoder.h:122
static int symbols(int bytes, int sf)
number of symbols for the indicated number of bytes
Definition CSS.h:157
static constexpr int HEADER_BYTES
Definition CSS.h:154
static int payloadBytes(int len, int ecc)
bytes of the payload code word
Definition CSS.h:160
static void fromSymbols(const uint16_t *symbols, int n, int sf, uint8_t *data, int len)
Converts symbols into bytes.
Definition CSS.h:211
static int decodeHeader(uint8_t *data)
Corrects the header in place: returns the length or -1 if invalid.
Definition CSS.h:169
static int decodePayload(uint8_t *data, int len, int ecc)
Definition CSS.h:187
Common API of the demodulators (FSKDecoder, MFSKDecoder): PCM audio data is written to the decoder an...
Definition ModemBase.h:344
void status(const char *fmt,...)
Definition ModemBase.h:367
Assembles the PCM frames from the written bytes and provides the samples of the selected channel as f...
Definition ModemBase.h:170
static constexpr int MAX_ECC
Definition ReedSolomon.h:24
Converts the sample rate of a signal with a windowed sinc low pass filter (by default the cut off is ...
Definition DSPUtils.h:15
Radix 2 complex FFT with a precomputed twiddle table.
Definition DSPUtils.h:131
TinyModem: software audio modems and packets with error correction.
Definition AX25.h:11
Configuration of the chirp spread spectrum modem (CSSEncoder, CSSDecoder), which works like LoRa in t...
Definition CSS.h:31
static constexpr int MAX_FRAME
max payload of a frame
Definition CSS.h:118
Peak of a spectrum.
Definition CSSDecoder.h:169
float ratio
Definition CSSDecoder.h:173
float pos
Definition CSSDecoder.h:171
int bin
Definition CSSDecoder.h:170
float power
Definition CSSDecoder.h:172