/* * aptdec - A lightweight FOSS (NOAA) APT decoder * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include "filter.h" #include "util.h" #include "algebra.h" #define LOW_PASS_SIZE 101 #define CARRIER_FREQ 2400.0f #define MAX_CARRIER_OFFSET 10.0f const float sync_pattern[] = {-1, -1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 0}; #define SYNC_SIZE (sizeof(sync_pattern)/sizeof(sync_pattern[0])) typedef struct { float alpha; float beta; float min_freq; float max_freq; float freq; float phase; } pll_t; typedef struct { float *ring_buffer; size_t ring_size; float *taps; size_t ntaps; } fir_t; struct aptdec { float sample_rate; float sync_frequency; pll_t *pll; fir_t *hilbert; float low_pass[LOW_PASS_SIZE]; float row_buffer[APTDEC_IMG_WIDTH + SYNC_SIZE + 2]; float interpolator_buffer[APTDEC_BUFFER_SIZE]; size_t interpolator_n; float interpolator_offset; }; char *aptdec_get_version(void) { return VERSION; } fir_t *fir_init(size_t max_size, size_t ntaps) { fir_t *fir = calloc(1, sizeof(fir_t)); fir->ntaps = ntaps; fir->ring_size = max_size + ntaps; fir->taps = calloc(ntaps, sizeof(float)); fir->ring_buffer = calloc(max_size + ntaps, sizeof(float)); return fir; } void fir_free(fir_t *fir) { free(fir->ring_buffer); free(fir->taps); free(fir); } pll_t *pll_init(float alpha, float beta, float min_freq, float max_freq, float sample_rate) { pll_t *pll = calloc(1, sizeof(pll_t)); pll->alpha = alpha; pll->beta = beta; pll->min_freq = M_TAUf * min_freq / sample_rate; pll->max_freq = M_TAUf * max_freq / sample_rate; pll->phase = 0.0f; pll->freq = 0.0f; return pll; } aptdec_t *aptdec_init(float sample_rate) { if (sample_rate > 96000 || sample_rate < (CARRIER_FREQ + APTDEC_IMG_WIDTH) * 2.0f) { return NULL; } aptdec_t *aptdec = calloc(1, sizeof(aptdec_t)); aptdec->sample_rate = sample_rate; aptdec->sync_frequency = 1.0f; aptdec->interpolator_n = APTDEC_BUFFER_SIZE; aptdec->interpolator_offset = 0.0f; // PLL configuration // https://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html float damp = 0.7f; float bw = 0.005f; float alpha = (4.0f * damp * bw) / (1.0f + 2.0f * damp * bw + bw * bw); float beta = (4.0f * bw * bw) / (1.0f + 2.0f * damp * bw + bw * bw); aptdec->pll = pll_init(alpha, beta, CARRIER_FREQ-MAX_CARRIER_OFFSET, CARRIER_FREQ+MAX_CARRIER_OFFSET, sample_rate); if (aptdec->pll == NULL) { free(aptdec); return NULL; } // Hilbert transform aptdec->hilbert = fir_init(APTDEC_BUFFER_SIZE, 31); if (aptdec->hilbert == NULL) { free(aptdec->pll); free(aptdec); return NULL; } design_hilbert(aptdec->hilbert->taps, aptdec->hilbert->ntaps); design_low_pass(aptdec->low_pass, aptdec->sample_rate, (2080.0f + CARRIER_FREQ) / 2.0f, LOW_PASS_SIZE); return aptdec; } void aptdec_free(aptdec_t *aptdec) { fir_free(aptdec->hilbert); free(aptdec->pll); free(aptdec); } static complexf_t pll_work(pll_t *pll, complexf_t in) { // Internal oscillator (90deg offset) complexf_t osc = complex_build(cosf(pll->phase), -sinf(pll->phase)); in = complex_multiply(in, osc); // Error detector float error = cargf(in); // Loop filter (single pole IIR) pll->freq += pll->beta * error; pll->freq = clamp(pll->freq, pll->min_freq, pll->max_freq); pll->phase += pll->freq + (pll->alpha * error); pll->phase = remainderf(pll->phase, M_TAUf); return in; } static int am_demod(aptdec_t *aptdec, float *out, size_t count, aptdec_callback_t callback, void *context) { size_t read = callback(&aptdec->hilbert->ring_buffer[aptdec->hilbert->ntaps], count, context); for (size_t i = 0; i < read; i++) { complexf_t sample = hilbert_transform(&aptdec->hilbert->ring_buffer[i], aptdec->hilbert->taps, aptdec->hilbert->ntaps); out[i] = crealf(pll_work(aptdec->pll, sample)); } memcpy(aptdec->hilbert->ring_buffer, &aptdec->hilbert->ring_buffer[read], aptdec->hilbert->ntaps*sizeof(float)); return read; } static int get_pixels(aptdec_t *aptdec, float *out, size_t count, aptdec_callback_t callback, void *context) { float ratio = aptdec->sample_rate / (4160.0f * aptdec->sync_frequency); for (size_t i = 0; i < count; i++) { // Get more samples if there are less than `LOW_PASS_SIZE` available if (aptdec->interpolator_n + LOW_PASS_SIZE > APTDEC_BUFFER_SIZE) { memcpy(aptdec->interpolator_buffer, &aptdec->interpolator_buffer[aptdec->interpolator_n], (APTDEC_BUFFER_SIZE-aptdec->interpolator_n) * sizeof(float)); size_t read = am_demod(aptdec, &aptdec->interpolator_buffer[APTDEC_BUFFER_SIZE-aptdec->interpolator_n], aptdec->interpolator_n, callback, context); if (read != aptdec->interpolator_n) { return i; } aptdec->interpolator_n = 0; } out[i] = interpolating_convolve(&aptdec->interpolator_buffer[aptdec->interpolator_n], aptdec->low_pass, LOW_PASS_SIZE, aptdec->interpolator_offset); // Do not question the sacred code int shift = ceilf(ratio - aptdec->interpolator_offset); aptdec->interpolator_offset = shift + aptdec->interpolator_offset - ratio; aptdec->interpolator_n += shift; } return count; } // Get an entire row of pixels, aligned with sync markers int aptdec_getrow(aptdec_t *aptdec, float *row, aptdec_callback_t callback, void *context) { // Wrap the circular buffer memcpy(aptdec->row_buffer, &aptdec->row_buffer[APTDEC_IMG_WIDTH], (SYNC_SIZE + 2) * sizeof(float)); // Get a lines worth (APTDEC_IMG_WIDTH) of samples if (get_pixels(aptdec, &aptdec->row_buffer[SYNC_SIZE + 2], APTDEC_IMG_WIDTH, callback, context) != APTDEC_IMG_WIDTH) { return 0; } // Error detector float left = FLT_MIN; float middle = FLT_MIN; float right = FLT_MIN; size_t phase = 0; for (size_t i = 0; i < APTDEC_IMG_WIDTH; i++) { float _left = convolve(&aptdec->row_buffer[i + 0], sync_pattern, SYNC_SIZE); float _middle = convolve(&aptdec->row_buffer[i + 1], sync_pattern, SYNC_SIZE); float _right = convolve(&aptdec->row_buffer[i + 2], sync_pattern, SYNC_SIZE); if (_middle > middle) { left = _left; middle = _middle; right = _right; phase = i + 1; } } // Frequency float bias = (left / middle) - (right / middle); aptdec->sync_frequency = 1.0f + bias / APTDEC_IMG_WIDTH / 4.0f; // Phase memcpy(&row[APTDEC_IMG_WIDTH], &aptdec->row_buffer[phase], (APTDEC_IMG_WIDTH - phase) * sizeof(float)); memcpy(&row[APTDEC_IMG_WIDTH - phase], aptdec->row_buffer, phase * sizeof(float)); return 1; }