├── .gitignore ├── dsp ├── sincos.h ├── agc.h ├── agc.c ├── timing.h ├── sincos.c ├── filter.h ├── pll.h ├── filter.c ├── timing.c └── pll.c ├── tui.h ├── Makefile ├── cmake_uninstall.cmake.in ├── wavfile.h ├── LICENSE ├── demod.h ├── utils.h ├── wavfile.c ├── CMakeLists.txt ├── demod.c ├── utils.c ├── README.md ├── tui.c └── main.c /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | **/tags 3 | -------------------------------------------------------------------------------- /dsp/sincos.h: -------------------------------------------------------------------------------- 1 | #ifndef sincos_h 2 | #define sincos_h 3 | 4 | float fast_sin(float x); 5 | float fast_cos(float x); 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /dsp/agc.h: -------------------------------------------------------------------------------- 1 | #ifndef agc_h 2 | #define agc_h 3 | #include 4 | 5 | /** 6 | * Automatic gain control loop 7 | * 8 | * @param sample sample to rescale 9 | * @return scaled sample 10 | */ 11 | float complex agc_apply(float complex sample); 12 | 13 | /** 14 | * Get the current gain of the AGC 15 | * 16 | * @return gain 17 | */ 18 | float agc_get_gain(); 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /dsp/agc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "agc.h" 3 | #include "utils.h" 4 | 5 | #define FLOAT_TARGET_MAG 190 6 | #define BIAS_POLE 0.001f 7 | #define GAIN_POLE 0.0001f 8 | 9 | static float _float_gain = 1; 10 | static float complex _float_bias = 0; 11 | 12 | float complex 13 | agc_apply(float complex sample) 14 | { 15 | /* Remove DC bias */ 16 | _float_bias = _float_bias*(1-BIAS_POLE) + BIAS_POLE*sample; 17 | sample -= _float_bias; 18 | 19 | /* Apply AGC */ 20 | sample *= _float_gain; 21 | _float_gain += GAIN_POLE*(FLOAT_TARGET_MAG - cabsf(sample)); 22 | _float_gain = MAX(0, _float_gain); 23 | 24 | return sample; 25 | } 26 | 27 | float 28 | agc_get_gain() 29 | { 30 | return _float_gain; 31 | } 32 | -------------------------------------------------------------------------------- /tui.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Auxiliary functions abstracting the ncurses text user interface 3 | */ 4 | #ifndef METEOR_TUI_H 5 | #define METEOR_TUI_H 6 | 7 | #include 8 | 9 | #define CONSTELL_MAX 31 10 | 11 | void tui_init(unsigned upd_interval); 12 | void tui_deinit(void); 13 | void tui_handle_resize(void); 14 | 15 | int tui_process_input(void); 16 | 17 | int tui_print_info(const char *msg, ...); 18 | void tui_update_pll(float freq, float rate, int islocked, float gain); 19 | void tui_draw_constellation(const int8_t *dots, unsigned count); 20 | void tui_update_file_in(unsigned rate, uint64_t done, uint64_t duration); 21 | void tui_update_data_out(unsigned nbytes); 22 | int tui_wait_for_user_input(void); 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /dsp/timing.h: -------------------------------------------------------------------------------- 1 | #ifndef timing_h 2 | #define timing_h 3 | 4 | #include 5 | 6 | /** 7 | * Initialize M&M symbol timing estimator 8 | * 9 | * @param sym_freq expected symbol frequency 10 | * @param bw bandwidth of the loop filter 11 | */ 12 | void timing_init(float sym_freq, float bw); 13 | 14 | /** 15 | * Update symbol timing estimate 16 | * 17 | * @param sample sample to update the estimate with 18 | */ 19 | void retime(float complex sample); 20 | 21 | /** 22 | * Advance the internal symbol clock by one sample (not symbol, sample) 23 | */ 24 | int advance_timeslot(); 25 | int advance_timeslot_dual(); 26 | 27 | /** 28 | * Get the M&M symbol frequency estimate 29 | * 30 | * @return frequency 31 | */ 32 | float mm_omega(); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # The old way of compiling and installing (0.x): 2 | # make 3 | # sudo make install 4 | # 5 | # The new way of compiling and installing (1.x): 6 | # mkdir build && cd build && cmake .. && make 7 | # sudo make install 8 | # 9 | 10 | .PHONY: default install clean 11 | 12 | default: build/meteor_demod 13 | 14 | build/meteor_demod: 15 | @echo "===========================================================" 16 | @echo "!!! Please fix your scripts to use the new build system !!!" 17 | @echo "!!! Check comments in Makefile to see how !!!" 18 | @echo "===========================================================" 19 | mkdir -p build 20 | cd build && cmake .. && make 21 | 22 | install: default 23 | cd build && make install 24 | 25 | uninstall: 26 | cd build && make uninstall 27 | 28 | clean: 29 | rm -rf build 30 | 31 | -------------------------------------------------------------------------------- /cmake_uninstall.cmake.in: -------------------------------------------------------------------------------- 1 | if(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt") 2 | message(FATAL_ERROR "Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt") 3 | endif() 4 | 5 | file(READ "@CMAKE_BINARY_DIR@/install_manifest.txt" files) 6 | string(REGEX REPLACE "\n" ";" files "${files}") 7 | foreach(file ${files}) 8 | message(STATUS "Uninstalling $ENV{DESTDIR}${file}") 9 | if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") 10 | exec_program( 11 | "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" 12 | OUTPUT_VARIABLE rm_out 13 | RETURN_VALUE rm_retval 14 | ) 15 | if(NOT "${rm_retval}" STREQUAL 0) 16 | message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") 17 | endif() 18 | else(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") 19 | message(STATUS "File $ENV{DESTDIR}${file} does not exist.") 20 | endif() 21 | endforeach() 22 | 23 | -------------------------------------------------------------------------------- /dsp/sincos.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "sincos.h" 4 | 5 | #define Q_SHIFT 14 6 | 7 | typedef int16_t fp16_t; 8 | typedef int32_t fp32_t; 9 | 10 | static fp32_t fpmul32(fp32_t x, fp32_t y); 11 | 12 | float 13 | fast_sin(float fx) 14 | { 15 | const int qN = 14; // 1<> (2*qN - Q_SHIFT); 29 | 30 | y = b - fpmul32(x2, c); 31 | y = a - fpmul32(x2, y); 32 | 33 | return (float)(sign < 0 ? -y : y) / (1<> Q_SHIFT; 47 | } 48 | -------------------------------------------------------------------------------- /wavfile.h: -------------------------------------------------------------------------------- 1 | #ifndef wavfile_h 2 | #define wavfile_h 3 | 4 | #include 5 | #include 6 | 7 | /** 8 | * Parse the WAV header in a file if available, and seek the file descriptor to 9 | * the start of the raw samples array 10 | * 11 | * @param fd wav file descriptor 12 | * @param samplerate pointer filled with the samplerate 13 | * @param bps pointer filled with the number of bits per sample 14 | * @param samples_start offset of the samples array from the start of the file 15 | * 16 | * @return 0 on success 17 | * 1 if the file is not a valid WAV file 18 | */ 19 | int wav_parse(FILE *fd, int *samplerate, int *bps); 20 | 21 | /** 22 | * Read a sample from the given wav file, converting it to fpcomplex_t and 23 | * normalizing 24 | * 25 | * @param dst pointer to the destination sample 26 | * @param bps bits per sample of the wav file 27 | * @param fd descriptor of the wav file, pointing to the next sample to read 28 | * @return 0 on success 29 | * 1 on failure 30 | */ 31 | int wav_read(float complex *dst, int bps, FILE *fd); 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 dbdexter-dev 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the “Software”), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /dsp/filter.h: -------------------------------------------------------------------------------- 1 | #ifndef filter_h 2 | #define filter_h 3 | #include 4 | 5 | typedef struct { 6 | float complex *mem; 7 | float *coeffs; 8 | int interp_factor; 9 | int size; 10 | int idx; 11 | } Filter; 12 | 13 | /** 14 | * Initialize a FIR filter with coefficients corresponding to a root-raised 15 | * cosine filter 16 | * 17 | * @param flt filter object to initialize 18 | * @param order order of the filter (e.g. 16 = 16 + 1 + 16 taps) 19 | * @param osf oversampling factor (samples per symbol) 20 | * @param alpha filter alpha parameter 21 | * @param factor interpolation factor 22 | * 23 | * @return 0 on success, 1 on failure 24 | */ 25 | int filter_init_rrc(Filter *flt, unsigned order, float osf, float alpha, unsigned factor); 26 | 27 | /** 28 | * Deinitialize a filter object 29 | * 30 | * @param flt filter to deinitalize 31 | */ 32 | void filter_deinit(Filter *flt); 33 | 34 | /** 35 | * Feed a sample to a filter object 36 | * 37 | * @param flt filter to pass the sample through 38 | * @param sample sample to feed 39 | */ 40 | void filter_fwd_sample(Filter *flt, float complex sample); 41 | 42 | /** 43 | * Get the output of a filter object 44 | * 45 | * @param flt filter to read the sample from 46 | * @param phase for interpolating filters, the phase to fetch the sample from 47 | * @return filter output 48 | */ 49 | float complex filter_get(Filter *flt, unsigned phase); 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /demod.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "dsp/agc.h" 3 | #include "dsp/filter.h" 4 | #include "dsp/pll.h" 5 | #include "dsp/timing.h" 6 | 7 | /* Satellite specific settings */ 8 | #define RRC_ALPHA 0.6 9 | #define SYM_RATE 72000.0 10 | 11 | /* Decoder specific settings */ 12 | #define RRC_ORDER 32 13 | #define INTERP_FACTOR 5 14 | #define SYM_BW 0.00005 15 | #define PLL_BW 1 16 | 17 | /** 18 | * Initialize demodulator 19 | * 20 | * @param pll_bw carrier estimator PLL bandwidth 21 | * @param sym_bw symbol timing estimator bandwidth 22 | * @param samplerate input sample rate 23 | * @param symrate expected symbol rate 24 | * @param interp_factor filter interpolation factor 25 | * @param rrc_order root-raised cosine order 26 | * @param oqpsk 1 if oqpsk, 0 if qpsk 27 | * @param freq_max max carrier frequency deviation, see pll.h for more info 28 | */ 29 | void demod_init(float pll_bw, float sym_bw, int samplerate, int symrate, int interp_factor, int rrc_order, int oqpsk, float freq_max); 30 | 31 | /** 32 | * Deinitialize demodulator 33 | */ 34 | void demod_deinit(); 35 | 36 | /** 37 | * Feed a QPSK sample into the demodulator 38 | * 39 | * @param sample pointer to sample to use 40 | * @return 1 if sample was updated to a demodulateed symbol, 0 otherwise 41 | */ 42 | int demod_qpsk(float complex *sample); 43 | 44 | /** 45 | * Feed a QPSK sample into the demodulator 46 | * 47 | * @param sample pointer to sample to use 48 | * @return 1 if sample was updated to a demodulateed symbol, 0 otherwise 49 | */ 50 | int demod_oqpsk(float complex *sample); 51 | -------------------------------------------------------------------------------- /dsp/pll.h: -------------------------------------------------------------------------------- 1 | #ifndef pll_h 2 | #define pll_h 3 | #include 4 | 5 | /** 6 | * Initialize phase locked loop 7 | * 8 | * @param bw bandwidth of the loop filter 9 | * @param oqpsk 0 if QPSK modulation, 1 if OQPSK 10 | * @param freq_max maximum carrier deviation, in (1/symbol_rate) rad/s 11 | * e.g. freq_max=0.3 -> +-3.5kHz @72ksym/s, +-3.8kHz @80ksym/s 12 | */ 13 | void pll_init(float bw, int oqpsk, float freq_max); 14 | 15 | /** 16 | * Get the PLL local oscillator frequency 17 | * 18 | * @return frequency 19 | */ 20 | float pll_get_freq(); 21 | 22 | /** 23 | * Get current PLL status 24 | * 25 | * @return 0 if unlocked, 1 if locked 26 | */ 27 | int pll_get_locked(); 28 | 29 | /** 30 | * Check whether the PLL locked at least once in the past 31 | * 32 | * @return 0 if it never locked, 1 if it did 33 | */ 34 | int pll_did_lock_once(); 35 | 36 | /** 37 | * Update the carrier estimate based on a sample pair 38 | * (for QPSK, sample = cosample) 39 | * 40 | * @param i in-phase sample 41 | * @param q quadrature sample 42 | */ 43 | void pll_update_estimate(float i, float q); 44 | 45 | /** 46 | * Mix a sample with the local oscillator 47 | * 48 | * @param sample sample to mix 49 | * @return PLL output 50 | */ 51 | float complex pll_mix(float complex sample); 52 | 53 | /** 54 | * Partially mix a sample with the local oscillator, returning only the I branch 55 | * or the Q branch depending on the function 56 | * 57 | * @param sample sample to mix 58 | * @return PLL output (I branch only/Q branch only) 59 | */ 60 | float pll_mix_i(float complex sample); 61 | float pll_mix_q(float complex sample); 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /utils.h: -------------------------------------------------------------------------------- 1 | #ifndef utils_h 2 | #define utils_h 3 | 4 | #include 5 | 6 | #ifndef VERSION 7 | #define VERSION "(unknown version)" 8 | #endif 9 | 10 | 11 | #define DO_PRAGMA(x) _Pragma(#x) 12 | 13 | /* Portable unroll pragma, for some reason clang defines __GNUC__ but uses the 14 | * non-GCC unroll pragma format */ 15 | #if defined(__clang__) 16 | #define PRAGMA_UNROLL(x) DO_PRAGMA(unroll x) 17 | #elif defined(__GNUC__) 18 | #define PRAGMA_UNROLL(x) DO_PRAGMA(GCC unroll x) 19 | #else 20 | #define PRAGMA_UNROLL(x) DO_PRAGMA(unroll x) 21 | #endif 22 | 23 | #define MAX(x, y) (((x) > (y)) ? (x) : (y)) 24 | #define MIN(x, y) (((x) < (y)) ? (x) : (y)) 25 | #define LEN(x) (sizeof(x)/sizeof(x[0])) 26 | #ifndef sgn 27 | #define sgn(x) ((x) < 0 ? -1 : 1) 28 | #endif 29 | 30 | /** 31 | * Generate a symbol filename based on the current date and time 32 | * 33 | * @return filename 34 | */ 35 | char* gen_fname(); 36 | 37 | /** 38 | * Format a number into a more readable format 39 | * 40 | * @param value value to 41 | * @param buf buffer to write the resulting string to 42 | */ 43 | void humanize(size_t value, char *buf); 44 | 45 | /** 46 | * Format a number of seconds into HH:MM:SS format 47 | * 48 | * @param secs seconds 49 | * @param buf buffer to write the resulting string to 50 | */ 51 | void seconds_to_str(unsigned secs, char *buf); 52 | 53 | /** 54 | * Convert a "human-readable" number into a float 55 | * e.g. 137.1M = 137100.0 56 | * 57 | * @param human string to parse 58 | * @return parsed float 59 | */ 60 | float human_to_float(const char *human); 61 | 62 | /** 63 | * Write usage info to stdout 64 | * 65 | * @param progname executable name 66 | */ 67 | void usage(const char *progname); 68 | 69 | /** 70 | * Write version info to stdout 71 | */ 72 | void version(); 73 | 74 | #endif 75 | -------------------------------------------------------------------------------- /wavfile.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "dsp/agc.h" 6 | #include "wavfile.h" 7 | 8 | #define FILE_BUFFER_SIZE 32768 9 | static union { 10 | uint8_t bytes[FILE_BUFFER_SIZE]; 11 | int16_t words[FILE_BUFFER_SIZE/2]; 12 | float floats[FILE_BUFFER_SIZE/4]; 13 | } _buffer; 14 | static size_t _offset; 15 | 16 | struct wave_header { 17 | char _riff[4]; /* Literally RIFF */ 18 | uint32_t chunk_size; 19 | char _filetype[4]; /* Literally WAVE */ 20 | 21 | char _fmt[4]; /* Literally fmt */ 22 | uint32_t subchunk_size; 23 | uint16_t audio_format; 24 | uint16_t num_channels; 25 | uint32_t sample_rate; 26 | uint32_t byte_rate; 27 | uint16_t block_align; 28 | uint16_t bits_per_sample; 29 | char _data[4]; /* Literally data */ 30 | uint32_t subchunk2_size; 31 | }; 32 | 33 | int 34 | wav_parse(FILE *fd, int *samplerate, int *bps) 35 | { 36 | struct wave_header header; 37 | 38 | if (!(fread(&header, sizeof(struct wave_header), 1, fd))) return 1; 39 | 40 | if (strncmp(header._riff, "RIFF", 4)) return 1; 41 | if (strncmp(header._filetype, "WAVE", 4)) return 1; 42 | if (header.num_channels != 2) return 1; 43 | 44 | if (!(*bps = header.bits_per_sample)) return 1; 45 | *samplerate = (int)header.sample_rate; 46 | 47 | return 0; 48 | } 49 | 50 | int 51 | wav_read(float complex *dst, int bps, FILE *fd) 52 | { 53 | float complex tmp; 54 | 55 | if (!_offset && !fread(&_buffer.bytes, sizeof(_buffer.bytes), 1, fd)) return 0; 56 | 57 | switch (bps) { 58 | case 8: 59 | /* Unsigned byte */ 60 | tmp = (int)_buffer.bytes[_offset]-128 + I*((int)_buffer.bytes[_offset+1]-128); 61 | break; 62 | case 16: 63 | /* Signed short */ 64 | tmp = _buffer.words[_offset] + I*_buffer.words[_offset+1]; 65 | break; 66 | case 32: 67 | /* Float */ 68 | tmp = _buffer.floats[_offset] + I*_buffer.floats[_offset+1]; 69 | break; 70 | default: 71 | return 0; 72 | break; 73 | } 74 | 75 | _offset += 2; 76 | if (_offset*bps/8 >= sizeof(_buffer)) _offset = 0; 77 | 78 | *dst = tmp; 79 | return 1; 80 | } 81 | -------------------------------------------------------------------------------- /dsp/filter.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "filter.h" 5 | #include "utils.h" 6 | 7 | float rrc_coeff(int stage_no, unsigned n_taps, float osf, float alpha); 8 | 9 | int 10 | filter_init_rrc(Filter *flt, unsigned order, float osf, float alpha, unsigned factor) 11 | { 12 | const unsigned taps = order*2+1; 13 | unsigned i, j; 14 | 15 | if (!(flt->coeffs = malloc(sizeof(*flt->coeffs) * taps * factor))) return 1; 16 | if (!(flt->mem = calloc(taps, sizeof(*flt->mem)))) return 1; 17 | 18 | for (j=0; j<(unsigned)factor; j++) { 19 | for (i=0; icoeffs[j*taps + i] = rrc_coeff(i*factor + j, taps*factor, osf*factor, alpha); 21 | } 22 | } 23 | 24 | flt->size = taps; 25 | flt->interp_factor = factor; 26 | 27 | return 0; 28 | } 29 | 30 | void 31 | filter_deinit(Filter *flt) 32 | { 33 | if (flt->mem) { free(flt->mem); flt->mem=NULL; } 34 | if (flt->coeffs) { free(flt->coeffs); flt->coeffs=NULL; } 35 | flt->size = 0; 36 | } 37 | 38 | void 39 | filter_fwd_sample(Filter *flt, float complex sample) 40 | { 41 | flt->mem[flt->idx++] = sample; 42 | flt->idx %= flt->size; 43 | } 44 | 45 | float complex 46 | filter_get(Filter *flt, unsigned phase) 47 | { 48 | float complex result; 49 | int i, j; 50 | 51 | result = 0; 52 | j = (flt->interp_factor - phase - 1)*flt->size; 53 | 54 | /* Chunk 1: from current position to end */ 55 | for (i=flt->idx; isize; i++, j++) { 56 | result += flt->mem[i] * flt->coeffs[j]; 57 | } 58 | 59 | /* Chunk 2: from start to current position - 1 */ 60 | for (i=0; iidx; i++, j++) { 61 | result += flt->mem[i] * flt->coeffs[j]; 62 | } 63 | 64 | return result; 65 | } 66 | 67 | /*Static functions {{{*/ 68 | /* Variable alpha RRC filter coefficients */ 69 | /* Taken from https://www.michael-joost.de/rrcfilter.pdf */ 70 | float 71 | rrc_coeff(int stage_no, unsigned taps, float osf, float alpha) 72 | { 73 | const float norm = 2.0/5.0; 74 | float coeff; 75 | float t; 76 | float interm; 77 | int order; 78 | 79 | order = (taps - 1)/2; 80 | 81 | /* Handle the 0/0 case */ 82 | if (order == stage_no) { 83 | return norm * (1-alpha+4*alpha/M_PI); 84 | } 85 | 86 | t = abs(order - stage_no)/osf; 87 | coeff = sinf(M_PI*t*(1-alpha)) + 4*alpha*t*cosf(M_PI*t*(1+alpha)); 88 | interm = M_PI*t*(1-(4*alpha*t)*(4*alpha*t)); 89 | 90 | /* Hamming window */ 91 | coeff *= 0.42 - 0.5*cosf(2*M_PI*stage_no/(taps-1)) + 0.08*cosf(4*M_PI*stage_no/(taps-1)); 92 | 93 | return coeff / interm * norm; 94 | } 95 | /*}}}*/ 96 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | option(ENABLE_TUI "Enable Ncurses TUI" ON) 4 | 5 | project(meteor_demod 6 | VERSION 1.0 7 | DESCRIPTION "QPSK demodulator" 8 | LANGUAGES C) 9 | 10 | add_definitions(-DVERSION="${CMAKE_PROJECT_VERSION}") 11 | 12 | if (NOT CMAKE_BUILD_TYPE) 13 | set(CMAKE_BUILD_TYPE "Release") 14 | endif() 15 | 16 | set(CMAKE_C_STANDARD 99) 17 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -Wextra -Wimplicit-fallthrough") 18 | set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -march=native -ftree-vectorize") 19 | # ARM architectures need -mfpu=auto in order to enable NEON when available, 20 | # but that option is unrecognized by x86 gcc (and possibly others): only 21 | # add it to the release flags when the compiler's target is arm 22 | # This is not a problem for arm64, as NEON support is mandatory for that arch 23 | execute_process(COMMAND "${CMAKE_C_COMPILER}" "-dumpmachine" COMMAND "grep" "arm" OUTPUT_QUIET RESULT_VARIABLE is_arm) 24 | if (is_arm EQUAL "0") 25 | set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -mcpu=native -mfpu=auto") 26 | endif() 27 | 28 | 29 | set(COMMON_SOURCES 30 | dsp/agc.c dsp/agc.h 31 | dsp/filter.c dsp/filter.h 32 | dsp/pll.c dsp/pll.h 33 | dsp/timing.c dsp/timing.h 34 | dsp/sincos.c dsp/sincos.h 35 | 36 | demod.c demod.h 37 | utils.c utils.h 38 | ) 39 | 40 | if (ENABLE_TUI) 41 | find_library(NCURSES_LIBRARY NAMES ncurses ncursesw) 42 | if (NCURSES_LIBRARY) 43 | add_definitions(-DENABLE_TUI) 44 | set(COMMON_SOURCES ${COMMON_SOURCES} tui.c tui.h) 45 | else() 46 | message(WARNING "ncurses not found, fancy TUI will not be available") 47 | endif() 48 | endif() 49 | 50 | set(COMMON_INC_DIRS 51 | ${PROJECT_SOURCE_DIR} 52 | ) 53 | 54 | # Main executable target 55 | add_executable(meteor_demod main.c wavfile.c ${COMMON_SOURCES}) 56 | target_include_directories(meteor_demod PUBLIC ${COMMON_INC_DIRS}) 57 | target_link_libraries(meteor_demod PUBLIC m pthread) 58 | 59 | # Add links to ncurses if enabled 60 | if (ENABLE_TUI AND NCURSES_LIBRARY) 61 | target_link_libraries(meteor_demod PUBLIC ${NCURSES_LIBRARY}) 62 | endif() 63 | 64 | install(TARGETS meteor_demod DESTINATION bin) 65 | 66 | # uninstall target 67 | if(NOT TARGET uninstall) 68 | configure_file( 69 | "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" 70 | "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" 71 | IMMEDIATE @ONLY) 72 | 73 | add_custom_target(uninstall 74 | COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) 75 | endif() 76 | 77 | -------------------------------------------------------------------------------- /demod.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "demod.h" 4 | 5 | static Filter _rrc_filter; 6 | 7 | void 8 | demod_init(float pll_bw, float sym_bw, int samplerate, int symrate, int interp_factor, int rrc_order, int oqpsk, float freq_max) 9 | { 10 | const int multiplier = oqpsk ? 1 : 2; /* OQPSK uses two samples per symbol */ 11 | 12 | pll_init(2*M_PI*pll_bw/(multiplier * symrate), oqpsk, freq_max); 13 | timing_init(2*M_PI*symrate/(samplerate*interp_factor), sym_bw/interp_factor); 14 | filter_init_rrc(&_rrc_filter, rrc_order, (float)samplerate/symrate, RRC_ALPHA, interp_factor); 15 | } 16 | 17 | void 18 | demod_deinit() 19 | { 20 | filter_deinit(&_rrc_filter); 21 | } 22 | 23 | int 24 | demod_qpsk(float complex *sample) 25 | { 26 | float complex out; 27 | int i, ret; 28 | 29 | filter_fwd_sample(&_rrc_filter, *sample); 30 | 31 | /* Check if this sample is in the correct timeslot */ 32 | ret = 0; 33 | for (i=0; i<_rrc_filter.interp_factor; i++) { 34 | if (advance_timeslot()) { 35 | out = filter_get(&_rrc_filter, i); /* Get the filter output */ 36 | out = agc_apply(out); /* Apply AGC */ 37 | out = pll_mix(out); /* Mix with local oscillator */ 38 | 39 | retime(out); /* Update symbol clock */ 40 | pll_update_estimate(crealf(out), cimagf(out)); /* Update carrier frequency */ 41 | 42 | *sample = out; /* Write out symbol */ 43 | ret = 1; 44 | } 45 | } 46 | 47 | return ret; 48 | } 49 | 50 | int 51 | demod_oqpsk(float complex *restrict sample) 52 | { 53 | float complex out; 54 | static float inphase; 55 | float quad; 56 | int i, ret; 57 | 58 | filter_fwd_sample(&_rrc_filter, *sample); 59 | 60 | /* Check if this sample is in the correct timeslot */ 61 | ret = 0; 62 | for (i=0; i<_rrc_filter.interp_factor; i++) { 63 | switch (advance_timeslot_dual()) { 64 | case 0: 65 | break; 66 | case 1: 67 | /* Intersample */ 68 | out = filter_get(&_rrc_filter, i); 69 | out = agc_apply(out); 70 | inphase = pll_mix_i(out); /* We only care about the I value */ 71 | break; 72 | case 2: 73 | /* Actual sample */ 74 | out = filter_get(&_rrc_filter, i); /* Get the filter output */ 75 | out = agc_apply(out); /* Apply AGC */ 76 | quad = pll_mix_q(out); /* We only care about the Q value */ 77 | 78 | *sample = inphase + I*quad; 79 | 80 | retime(*sample); /* Update symbol clock */ 81 | pll_update_estimate(inphase, quad); /* Update carrier frequency */ 82 | ret = 1; 83 | break; 84 | default: 85 | break; 86 | 87 | } 88 | } 89 | 90 | return ret; 91 | } 92 | -------------------------------------------------------------------------------- /dsp/timing.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "timing.h" 4 | #include "utils.h" 5 | 6 | /* freq will be at most +-2**-FREQ_DEV_EXP outside of the range */ 7 | #define FREQ_DEV_EXP 12 8 | 9 | static void update_estimate(float err); 10 | static void update_alpha_beta(float damp, float bw); 11 | static float mm_err(float prev, float cur); 12 | 13 | static float _prev; 14 | static float _phase, _freq; /* Symbol phase and rate estimate */ 15 | static float _freq_max_dev, _center_freq; /* Max freq deviation and center freq */ 16 | static float _alpha, _beta; /* Proportional and integral loop gain */ 17 | 18 | void 19 | timing_init(float sym_freq, float bw) 20 | { 21 | _freq = sym_freq; 22 | _center_freq = _freq; 23 | _freq_max_dev = _freq / (1<= 2*(float)M_PI; 38 | } 39 | 40 | int 41 | advance_timeslot_dual() 42 | { 43 | static int state = 1; 44 | int ret; 45 | 46 | /* Phase up */ 47 | _phase += _freq; 48 | 49 | /* Check if the timeslot is right */ 50 | if (_phase >= state * (float)M_PI) { 51 | ret = state; 52 | state = (state % 2) + 1; 53 | return ret; 54 | } 55 | 56 | return 0; 57 | } 58 | 59 | void 60 | retime(float complex sample) 61 | { 62 | float err; 63 | 64 | /* Compute timing error */ 65 | err = mm_err(_prev, cimagf(sample)); 66 | _prev = cimagf(sample); 67 | 68 | /* Update phase and freq estimate */ 69 | update_estimate(err); 70 | } 71 | 72 | /* Static functions {{{ */ 73 | static void 74 | update_estimate(float error) 75 | { 76 | float freq_delta; 77 | 78 | freq_delta = _freq - _center_freq; 79 | 80 | _phase -= 2*M_PI + _alpha*error; 81 | freq_delta -= _beta*error; 82 | 83 | /* Clip _freq between _freq - _freq_max_dev and _freq + _freq_max_dev */ 84 | freq_delta = MAX(-_freq_max_dev, MIN(_freq_max_dev, freq_delta)); 85 | //freq_delta = ((fabs(freq_delta + _freq_max_dev) - fabs(freq_delta - _freq_max_dev)) / 2.0); 86 | _freq = _center_freq + freq_delta; 87 | } 88 | 89 | static float 90 | mm_err(float prev, float cur) 91 | { 92 | /* sgn() can be replaced by any decision function used to determine the 93 | * symbol value */ 94 | return sgn(prev)*cur - sgn(cur)*prev; 95 | } 96 | 97 | static void 98 | update_alpha_beta(float damp, float bw) 99 | { 100 | float denom; 101 | 102 | denom = (1 + 2*damp*bw + bw*bw); 103 | _alpha = 4*damp*bw/denom; 104 | _beta = 4*bw*bw/denom; 105 | } 106 | /* }}} */ 107 | -------------------------------------------------------------------------------- /utils.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "utils.h" 4 | 5 | static char _generated_fname[sizeof("LRPT_YYYY_MM_DD_HH_MM.s") + 1]; 6 | 7 | char* 8 | gen_fname() 9 | { 10 | time_t t; 11 | struct tm *tm; 12 | 13 | t = time(NULL); 14 | tm = localtime(&t); 15 | 16 | strftime(_generated_fname, sizeof(_generated_fname), "LRPT_%Y_%m_%d-%H_%M.s", tm); 17 | 18 | return _generated_fname; 19 | } 20 | 21 | void 22 | humanize(size_t count, char *buf) 23 | { 24 | const char suffix[] = " kMGTPE"; 25 | float fcount; 26 | int exp_3; 27 | 28 | if (count < 1000) { 29 | sprintf(buf, "%lu %c", count, suffix[0]); 30 | } else { 31 | for (exp_3 = 0, fcount = count; fcount > 1000; fcount /= 1000, exp_3++) 32 | ; 33 | if (fcount > 99.9) { 34 | sprintf(buf, "%3.f %c", fcount, suffix[exp_3]); 35 | } else if (fcount > 9.99) { 36 | sprintf(buf, "%3.1f %c", fcount, suffix[exp_3]); 37 | } else { 38 | sprintf(buf, "%3.2f %c", fcount, suffix[exp_3]); 39 | } 40 | } 41 | } 42 | 43 | void 44 | seconds_to_str(unsigned secs, char *buf) 45 | { 46 | unsigned h, m, s; 47 | 48 | if (secs > 99*60*60) { 49 | sprintf(buf, "00:00:00"); 50 | return; 51 | } 52 | 53 | s = secs % 60; 54 | m = (secs / 60) % 60; 55 | h = secs / 3600; 56 | sprintf(buf, "%02u:%02u:%02u", h, m, s); 57 | } 58 | 59 | float 60 | human_to_float(const char *human) 61 | { 62 | int ret; 63 | float tmp; 64 | const char *suffix; 65 | 66 | tmp = atof(human); 67 | 68 | /* Search for the suffix */ 69 | for (suffix=human; (*suffix >= '0' && *suffix <= '9') || *suffix == '.'; suffix++); 70 | 71 | switch(*suffix) { 72 | case 'k': 73 | case 'K': 74 | ret = tmp * 1000; 75 | break; 76 | case 'M': 77 | ret = tmp * 1000000; 78 | break; 79 | default: 80 | ret = tmp; 81 | break; 82 | } 83 | 84 | return ret; 85 | 86 | } 87 | 88 | void 89 | usage(const char *pname) 90 | { 91 | fprintf(stderr, "Usage: %s [options] file_in\n", pname); 92 | fprintf(stderr, 93 | " -B, --batch Disable TUI and all control characters (aka \"script-friendly mode\")\n" 94 | " -m, --mode Specify the signal modulation scheme (default: qpsk, valid modes: qpsk, oqpsk)\n" 95 | " -o, --output Output decoded symbols to \n" 96 | " -q, --quiet Do not print status information\n" 97 | " -r, --symrate Set the symbol rate to (default: 72000)\n" 98 | " -R, --refresh-rate Refresh the status screen every ms (default: 50ms in TUI mode, 2000ms in batch mode)\n" 99 | " -s, --samplerate Force the input samplerate to (default: auto)\n" 100 | " --bps Force the input bits per sample to (default: 16)\n" 101 | " --stdout Write output symbols to stdout (implies -B, -q)\n" 102 | "\n" 103 | " -h, --help Print this help screen\n" 104 | " -v, --version Print version info\n" 105 | "\n" 106 | "Advanced options:\n" 107 | " -b, --pll-bw Set the PLL bandwidth to (default: 1)\n" 108 | " -d, --freq-delta Set the maximum carrier deviation to (default: +-3.5kHz)\n" 109 | " -f, --fir-order Set the RRC filter order to (default: 32)\n" 110 | " -O, --oversamp Set the interpolation factor to (default: 5)\n" 111 | ); 112 | } 113 | 114 | void 115 | version() 116 | { 117 | fprintf(stderr, "meteor_demod v" VERSION "\n"); 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Meteor-M2 series demodulator 2 | ============================ 3 | 4 | This is a free, open-source LRPT demodulator for the Meteor-M2 Russian weather 5 | satellite series. It supports reading from a I/Q recording in .wav format, 6 | and it outputs an 8-bit soft-QPSK file, from which you can generate an image 7 | with the help of LRPTofflineDecoder, 8 | [meteor\_decode](https://github.com/dbdexter-dev/meteor_decode) or 9 | [medet](https://github.com/artlav/meteor_decoder). 10 | 11 | Features: 12 | - Support for regular (72k, `-r 72000`) and interleaved (80k, `-r 80000`) modes 13 | - Support for QPSK and OQPSK modulation schemes 14 | - Can read samples from stdin (pass `-` in place of a filename) 15 | - Can output samples to stdout (`--stdout`, disables all status indicators) 16 | 17 | 18 | Compiling and installing 19 | ------------------------ 20 | 21 | ``` 22 | mkdir build && cd build 23 | cmake .. 24 | make 25 | sudo make install 26 | ``` 27 | 28 | If you don't need the fancy ncurses interface, you can disable it at compile 29 | time by running `cmake -DENABLE_TUI=OFF ..` when configuring. 30 | 31 | 32 | Usage info 33 | ---------- 34 | ``` 35 | Usage: meteor_demod [options] file_in 36 | -B, --batch Disable TUI and all control characters (aka "script-friendly mode") 37 | -d, --freq-delta Set the maximum carrier devation to (default: +-3.5kHz) 38 | -m, --mode Specify the signal modulation scheme (default: qpsk, valid modes: qpsk, oqpsk) 39 | -o, --output Output decoded symbols to 40 | -q, --quiet Do not print status information 41 | -r, --symrate Set the symbol rate to (default: 72000) 42 | -R, --refresh-rate Refresh the status screen every ms (default: 50ms in TUI mode, 2000ms in batch mode) 43 | -s, --samplerate Force the input samplerate to (default: auto) 44 | --bps Force the input bits per sample to (default: 16) 45 | --stdout Write output symbols to stdout (implies -B, -q) 46 | 47 | -h, --help Print this help screen 48 | -v, --version Print version info 49 | 50 | Advanced options: 51 | -b, --pll-bw Set the PLL bandwidth to (default: 1) 52 | -f, --fir-order Set the RRC filter order to (default: 32) 53 | -O, --oversamp Set the interpolation factor to (default: 5) 54 | ``` 55 | 56 | Advanced options explanation 57 | ---------------------------- 58 | 59 | - `-b, --pll-bw`: higher = potentially faster carrier acquisition, but worse 60 | tracking performance if the signal is weak. Does not affect CPU usage. 61 | - `-f, --fir-order`: higher = more accurate signal filtering, but higher CPU usage. 62 | 16-32 is a good range, above 64 is most likely overkill. 63 | - `-O, --oversamp`: higher = more accurate symbol timing recovery, but higher 64 | CPU usage. Can be reduced if input sampling rate is high, although it's 65 | more efficient to use a low sampling rate and a high oversampling value than 66 | vice-versa. 67 | 68 | 69 | Live demodulation 70 | ----------------- 71 | Starting from v1.0, you can live demodulate on a toaster if that's your thing 72 | (~35% peak CPU usage on a Raspberry Pi Zero): 73 | 74 | ``` 75 | rtl_sdr -s 230000 -f 137.1M -g -p - | meteor_demod --bps 8 -s 230000 -B - 76 | ``` 77 | 78 | If you want to see the constellation diagram while demodulating live: 79 | 80 | ``` 81 | mkfifo /tmp/raw_samples 82 | meteor_demod --bps 8 -s 230000 /tmp/raw_samples & 83 | rtl_sdr -s 230000 -f 137.1M -g -p /tmp/raw_samples 84 | rm /tmp/raw_samples 85 | ``` 86 | 87 | With a decoder that supports reading symbols from stdin, you can even decode live 88 | (~75% peak CPU usage on a Raspberry Pi Zero): 89 | 90 | ``` 91 | rtl_sdr -s 230000 -f 137.1M -g -p - | meteor_demod --bps 8 -s 230000 --stdout - | meteor_decode -o live.bmp - 92 | ``` 93 | -------------------------------------------------------------------------------- /dsp/pll.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "pll.h" 3 | #include "utils.h" 4 | #include "sincos.h" 5 | 6 | #define FREQ_MAX 0.3f 7 | #define ERR_POLE 0.001f 8 | #define M_1_SQRT2 0.7071067811865475f 9 | 10 | 11 | static void update_estimate(float error); 12 | static void update_alpha_beta(float damp, float bw); 13 | static float compute_error(float re, float im); 14 | static float lut_tanh(float x); 15 | 16 | static float _freq, _phase; 17 | static float _alpha, _beta; 18 | static float _lut_tanh[32]; 19 | static float _err; 20 | static int _locked, _locked_once; 21 | static float _bw; 22 | static float _fmax; 23 | 24 | void 25 | pll_init(float bw, int oqpsk, float freq_max) 26 | { 27 | int i; 28 | 29 | /* Use default if freq_max is negative, use 1 if freq_max > 1 */ 30 | if (freq_max < 0) freq_max = FREQ_MAX; 31 | else freq_max = MIN(1.0f, freq_max); 32 | 33 | _freq = 0; 34 | _phase = 0; 35 | _locked = _locked_once = 0; 36 | _err = 1000; 37 | _bw = bw; 38 | _fmax = (oqpsk ? freq_max/2 : freq_max); 39 | 40 | for (i=0; i<(int)LEN(_lut_tanh); i++) { 41 | _lut_tanh[i] = (float)tanh(i-16); 42 | } 43 | update_alpha_beta(M_1_SQRT2, bw); 44 | } 45 | 46 | float pll_get_freq() { return _freq; } 47 | int pll_get_locked() { return _locked; } 48 | int pll_did_lock_once() { return _locked_once; } 49 | 50 | float complex 51 | pll_mix(float complex sample) 52 | { 53 | const float sine = fast_sin(-_phase); 54 | const float cosine = fast_cos(-_phase); 55 | const float re = crealf(sample); 56 | const float im = cimagf(sample); 57 | 58 | /* Mix sample */ 59 | sample = (re*cosine - im*sine) + I*(re*sine + im*cosine); 60 | _phase += _freq; /* Advance phase based on frequency */ 61 | if (_phase >= 2*M_PI) _phase -= 2*M_PI; 62 | 63 | return sample; 64 | } 65 | 66 | float 67 | pll_mix_i(float complex sample) 68 | { 69 | const float sine = fast_sin(-_phase); 70 | const float cosine = fast_cos(-_phase); 71 | const float re = crealf(sample); 72 | const float im = cimagf(sample); 73 | float result; 74 | 75 | /* Mix sample (real part only) */ 76 | result = re*cosine - im*sine; 77 | _phase += _freq; /* Advance phase based on frequency */ 78 | if (_phase >= 2*M_PI) _phase -= 2*M_PI; 79 | 80 | return result; 81 | } 82 | 83 | float 84 | pll_mix_q(float complex sample) 85 | { 86 | const float sine = fast_sin(-_phase); 87 | const float cosine = fast_cos(-_phase); 88 | const float re = crealf(sample); 89 | const float im = cimagf(sample); 90 | float result; 91 | 92 | result = re*sine + im*cosine; 93 | _phase += _freq; /* Advance phase based on frequency */ 94 | if (_phase >= 2*M_PI) _phase -= 2*M_PI; 95 | 96 | return result; 97 | } 98 | 99 | void 100 | pll_update_estimate(float i, float q) 101 | { 102 | float error; 103 | error = compute_error(i, q); 104 | 105 | update_estimate(error); 106 | } 107 | 108 | /* Static functions {{{ */ 109 | static void 110 | update_estimate(float error) 111 | { 112 | static int updown = 1; 113 | _phase = fmod(_phase + _alpha*error, 2*M_PI); 114 | _freq += _beta*error; 115 | 116 | /* Lock detection */ 117 | _err = _err*(1-ERR_POLE) + fabs(error)*ERR_POLE; 118 | if (_err < 85 && !_locked) { 119 | _locked = 1; 120 | _locked_once = 1; 121 | } else if (_err > 105 && _locked) { 122 | _locked = 0; 123 | } 124 | 125 | /* If unlocked, scan the frequency range up and down */ 126 | if (!_locked) _freq += 0.000001 * updown; 127 | updown = (_freq >= _fmax) ? -1 : (_freq <= -_fmax) ? 1 : updown; 128 | _freq = MAX(-_fmax, MIN(_fmax, _freq)); 129 | 130 | } 131 | 132 | static void 133 | update_alpha_beta(float damp, float bw) 134 | { 135 | float denom; 136 | 137 | denom = (1 + 2*damp*bw + bw*bw); 138 | _alpha = 4*damp*bw/denom; 139 | _beta = 4*bw*bw/denom; 140 | } 141 | 142 | static float 143 | compute_error(float re, float im) 144 | { 145 | float err; 146 | 147 | err = lut_tanh(re) * im - 148 | lut_tanh(im) * re; 149 | 150 | return err; 151 | } 152 | 153 | float 154 | lut_tanh(float val) 155 | { 156 | if (val > 15) return 1; 157 | if (val < -16) return -1; 158 | return _lut_tanh[(int)val+16]; 159 | } 160 | /* }}} */ 161 | -------------------------------------------------------------------------------- /tui.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "tui.h" 9 | #include "utils.h" 10 | 11 | /* Saved for when we want to wait indefinitely for user input */ 12 | static unsigned _upd_interval; 13 | 14 | enum { 15 | PAIR_DEF = 1, 16 | PAIR_GREEN_DEF = 2, 17 | PAIR_RED_DEF = 3 18 | }; 19 | 20 | static void print_banner(WINDOW *win); 21 | static void iq_draw_quadrants(WINDOW *win); 22 | static void windows_init(int rows, int col); 23 | 24 | /* Struct holding all the windows */ 25 | struct { 26 | WINDOW *banner_top; 27 | WINDOW *iq; 28 | WINDOW *pll, *filein, *dataout; 29 | WINDOW *infowin; 30 | } tui; 31 | 32 | /* Initialize the ncurses tui */ 33 | void 34 | tui_init(unsigned upd_interval) 35 | { 36 | int rows, cols; 37 | setlocale(LC_ALL, ""); 38 | 39 | initscr(); 40 | noecho(); 41 | cbreak(); 42 | curs_set(0); 43 | 44 | use_default_colors(); 45 | start_color(); 46 | init_pair(PAIR_DEF, -1, -1); 47 | init_pair(PAIR_RED_DEF, COLOR_RED, -1); 48 | init_pair(PAIR_GREEN_DEF, COLOR_GREEN, -1); 49 | 50 | getmaxyx(stdscr, rows, cols); 51 | _upd_interval = upd_interval; 52 | 53 | windows_init(rows, cols); 54 | tui_update_pll(0, 0, 0, 1); 55 | tui_handle_resize(); 56 | } 57 | 58 | /* Handle terminal resizing by moving the windows around */ 59 | void 60 | tui_handle_resize() 61 | { 62 | int nr, nc; 63 | int iq_size; 64 | 65 | /* Re-initialize ncurses with the correct dimensions */ 66 | werase(stdscr); 67 | endwin(); 68 | 69 | refresh(); 70 | getmaxyx(stdscr, nr, nc); 71 | 72 | iq_size = (MIN(CONSTELL_MAX, MIN(nr, nc/3))) | 0x3; 73 | 74 | wresize(tui.banner_top, 1, nc); 75 | mvwin(tui.banner_top, 0, 0); 76 | wresize(tui.iq, iq_size/2, iq_size); 77 | mvwin(tui.iq, 2, 0); 78 | wresize(tui.pll, 3, nc - iq_size - 2); 79 | mvwin(tui.pll, 2, iq_size+2); 80 | wresize(tui.filein, 2, nc - iq_size - 2); 81 | mvwin(tui.filein, 6, iq_size+2); 82 | wresize(tui.dataout, 2, nc - iq_size - 2); 83 | mvwin(tui.dataout, 9, iq_size+2); 84 | wresize(tui.infowin, nr-iq_size/2-4, nc); 85 | mvwin(tui.infowin, MAX(12, 2+iq_size/2+1), 0); 86 | 87 | print_banner(tui.banner_top); 88 | iq_draw_quadrants(tui.iq); 89 | wrefresh(tui.iq); 90 | } 91 | 92 | /* Get user input, return 1 if an abort was requested, 0 otherwise. This also 93 | * doubles as a throttling for the refresh rate, since wgetch() blocks for 94 | * upd_interval milliseconds before returning if no key is pressed */ 95 | int 96 | tui_process_input() 97 | { 98 | int ch; 99 | ch = wgetch(tui.infowin); 100 | 101 | switch(ch) { 102 | case KEY_RESIZE: 103 | tui_handle_resize(); 104 | break; 105 | case 'q': 106 | return 1; 107 | break; 108 | default: 109 | break; 110 | } 111 | wrefresh(tui.infowin); 112 | return 0; 113 | } 114 | 115 | /* Print a log message to the info window */ 116 | int 117 | tui_print_info(const char *msg, ...) 118 | { 119 | time_t t; 120 | va_list ap; 121 | struct tm* tm; 122 | char timestr[] = "HH:MM:SS"; 123 | 124 | assert(tui.infowin); 125 | 126 | t = time(NULL); 127 | tm = localtime(&t); 128 | strftime(timestr, sizeof(timestr), "%T", tm); 129 | wprintw(tui.infowin, "(%s) ", timestr); 130 | 131 | va_start(ap, msg); 132 | vw_printw(tui.infowin, msg, ap); 133 | va_end(ap); 134 | 135 | return 0; 136 | } 137 | 138 | /* Update the PLL info displayed */ 139 | void 140 | tui_update_pll(float freq, float rate, int islocked, float gain) 141 | { 142 | assert(tui.pll); 143 | 144 | werase(tui.pll); 145 | wmove(tui.pll, 0, 0); 146 | wattrset(tui.pll, A_BOLD); 147 | wprintw(tui.pll, "PLL status: "); 148 | if (islocked) { 149 | wattrset(tui.pll, A_BOLD | COLOR_PAIR(PAIR_GREEN_DEF)); 150 | wprintw(tui.pll, "Locked\n"); 151 | } else { 152 | wattrset(tui.pll, A_BOLD | COLOR_PAIR(PAIR_RED_DEF)); 153 | wprintw(tui.pll, "Acquiring...\n"); 154 | } 155 | wattrset(tui.pll, COLOR_PAIR(PAIR_DEF)); 156 | wattroff(tui.pll, A_BOLD); 157 | wprintw(tui.pll, "Gain\tCarrier freq\tSymbol rate\n"); 158 | wprintw(tui.pll, "%.3f\t%+7.1f Hz\t%7.1f Hz\n", gain, freq, rate); 159 | wrefresh(tui.pll); 160 | } 161 | 162 | /* Draw an indicative constellation plot, not an accurate one, but still 163 | * indicative of the signal quality (*dots should be mutexed for this to be 164 | * accurate, since it comes directly from the decoding thread's memory domain) */ 165 | void 166 | tui_draw_constellation(const int8_t *dots, unsigned count) 167 | { 168 | int8_t x, y; 169 | int nr, nc; 170 | unsigned i; 171 | int prev; 172 | 173 | assert(tui.iq); 174 | 175 | getmaxyx(tui.iq, nr, nc); 176 | 177 | werase(tui.iq); 178 | for (i=0; i 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "demod.h" 11 | #include "dsp/timing.h" 12 | #include "utils.h" 13 | #include "wavfile.h" 14 | #ifdef ENABLE_TUI 15 | #include 16 | #include "tui.h" 17 | #endif 18 | 19 | #define SHORTOPTS "a:Bb:d:f:hm:o:O:qR:r:s:S:v" 20 | #define RINGSIZE 512 21 | 22 | struct thropts { 23 | FILE *samples_file, *soft_file; 24 | int bps; 25 | int (*demod)(float complex *sample); 26 | int done; 27 | unsigned long bytes_out; 28 | pthread_t main_tid; 29 | }; 30 | 31 | static void* thread_process(void *parms); 32 | static void noop(int x) { return; } 33 | 34 | static int8_t _symbols_ring[2*RINGSIZE]; 35 | static struct option longopts[] = { 36 | { "batch", 0, NULL, 'B' }, 37 | { "pll-bw", 1, NULL, 'b' }, 38 | { "freq-delta", 1, NULL, 'd' }, 39 | { "fir-order", 1, NULL, 'f' }, 40 | { "help", 0, NULL, 'h' }, 41 | { "mode", 1, NULL, 'm' }, 42 | { "output", 1, NULL, 'o' }, 43 | { "oversamp", 1, NULL, 'O' }, 44 | { "quiet", 0, NULL, 'q' }, 45 | { "refresh-rate", 1, NULL, 'R' }, 46 | { "symrate", 1, NULL, 'r' }, 47 | { "stdout", 0, NULL, 0x00}, 48 | { "samplerate", 1, NULL, 's' }, 49 | { "bps", 1, NULL, 'S' }, 50 | { "version", 0, NULL, 'v' }, 51 | }; 52 | 53 | 54 | int 55 | main(int argc, char *argv[]) 56 | { 57 | unsigned long file_len, tmp; 58 | FILE *samples_file, *soft_file; 59 | int c, i, j=0; 60 | volatile struct thropts thread_args; 61 | pthread_t tid; 62 | struct timespec sleep_timespec; 63 | float freq_hz, rate_hz; 64 | 65 | /* Command-line changeable parameters {{{ */ 66 | float pll_bw = PLL_BW; 67 | int rrc_order = RRC_ORDER; 68 | int interp_factor = INTERP_FACTOR; 69 | int quiet = 0; 70 | float symrate = SYM_RATE; 71 | float freq_max_delta = -1; 72 | int (*demod)(float complex *sample) = demod_qpsk; 73 | int (*message)(const char *fmt, ...) = printf; 74 | int batch = 0; 75 | int update_interval = -1; 76 | int bps = 0; 77 | int samplerate = -1; 78 | int stdout_mode = 0; 79 | char *output_fname = NULL; 80 | /* }}} */ 81 | /* Parse command-line options {{{ */ 82 | while ((c = getopt_long(argc, argv, SHORTOPTS, longopts, NULL)) != -1) { 83 | switch (c) { 84 | case 0x00: 85 | /* Stdout mode */ 86 | stdout_mode = 1; 87 | break; 88 | case 'b': 89 | pll_bw = human_to_float(optarg); 90 | break; 91 | case 'B': 92 | batch = 1; 93 | break; 94 | case 'd': 95 | freq_max_delta = human_to_float(optarg); 96 | break; 97 | case 'f': 98 | rrc_order = atoi(optarg); 99 | break; 100 | case 'h': 101 | usage(argv[0]); 102 | return 0; 103 | case 'm': 104 | if (!strcmp(optarg, "oqpsk")) demod = demod_oqpsk; 105 | break; 106 | case 'o': 107 | output_fname = optarg; 108 | break; 109 | case 'O': 110 | interp_factor = atoi(optarg); 111 | break; 112 | case 'q': 113 | quiet = 1; 114 | break; 115 | case 'R': 116 | update_interval = atoi(optarg); 117 | break; 118 | case 'r': 119 | symrate = human_to_float(optarg); 120 | break; 121 | case 's': 122 | samplerate = human_to_float(optarg); 123 | break; 124 | case 'S': 125 | bps = atoi(optarg); 126 | break; 127 | case 'v': 128 | version(); 129 | return 0; 130 | default: 131 | usage(argv[0]); 132 | return 1; 133 | } 134 | } 135 | 136 | freq_max_delta = freq_max_delta * (2*M_PI)/symrate; 137 | 138 | if (argc - optind < 1) { 139 | usage(argv[0]); 140 | return 1; 141 | } 142 | 143 | if (!output_fname) output_fname = gen_fname(); 144 | if (update_interval < 0) update_interval = batch ? 2000 : 50; 145 | if (stdout_mode) { 146 | batch = 1; 147 | quiet = 1; 148 | } 149 | #ifdef ENABLE_TUI 150 | message = batch ? printf : tui_print_info; 151 | #endif 152 | /* }}} */ 153 | 154 | /* Open input file */ 155 | if (!strcmp(argv[optind], "-")) { 156 | samples_file = stdin; 157 | batch = 1; /* Ncurses doesn't play nice with stdin samples */ 158 | } else if (!(samples_file = fopen(argv[optind], "rb"))) { 159 | fprintf(stderr, "Could not open input file\n"); 160 | return 1; 161 | } 162 | 163 | /* Parse wav header. If it fails, assume raw data */ 164 | if (wav_parse(samples_file, &samplerate, &bps)) { 165 | fseek(samples_file, 0, SEEK_SET); 166 | } 167 | 168 | if (samplerate < 0) { 169 | fprintf(stderr, "Could not auto-detect sample rate. Please specify it with -s \n"); 170 | usage(argv[0]); 171 | return 1; 172 | } 173 | if (!bps) { 174 | fprintf(stderr, "Could not auto-detect bits per sample, assuming 16\n"); 175 | bps = 16; 176 | } 177 | 178 | /* Open output file */ 179 | if (stdout_mode) { 180 | soft_file = stdout; 181 | } else if (!(soft_file = fopen(output_fname, "wb"))) { 182 | fprintf(stderr, "Could not open output file\n"); 183 | return 1; 184 | } 185 | 186 | /* Initialize subsystems */ 187 | demod_init(pll_bw, SYM_BW, samplerate, symrate, interp_factor, rrc_order, demod == demod_oqpsk, freq_max_delta); 188 | 189 | /* Get file length */ 190 | tmp = ftell(samples_file); 191 | fseek(samples_file, 0, SEEK_END); 192 | file_len = MAX(0, ftell(samples_file)); 193 | fseek(samples_file, tmp, SEEK_SET); 194 | 195 | 196 | #ifdef ENABLE_TUI 197 | if (!batch) tui_init(update_interval); 198 | #endif 199 | 200 | if (!quiet) message("Input: %s, output: %s\n", argv[optind], output_fname); 201 | 202 | /* Prepare thread arguments */ 203 | thread_args.samples_file = samples_file; 204 | thread_args.soft_file = soft_file; 205 | thread_args.bps = bps; 206 | thread_args.demod = demod; 207 | thread_args.done = 0; 208 | thread_args.main_tid = pthread_self(); 209 | 210 | sleep_timespec.tv_sec = update_interval / 1000; 211 | sleep_timespec.tv_nsec = (update_interval % 1000) * 1000L * 1000; 212 | 213 | /* SIGUSR1 is used just to wake up the main thread when the demod thread 214 | * exits, so connect it to a no-op handler */ 215 | signal(SIGUSR1, &noop); 216 | 217 | /* Launch demod thread */ 218 | pthread_create(&tid, NULL, thread_process, (void*)&thread_args); 219 | if (!quiet) message("Demodulator initialized\n"); 220 | 221 | #ifdef ENABLE_TUI 222 | if (!batch) { 223 | /* TUI mode: update ncurses screen until done */ 224 | while (!thread_args.done) { 225 | /* Exit on user request. Also throttles refresh rate */ 226 | if (tui_process_input()) { 227 | thread_args.done = 1; 228 | break; 229 | } 230 | 231 | freq_hz = pll_get_freq()*symrate/(2*M_PI)*(demod == demod_oqpsk ? 2 : 1); 232 | rate_hz = mm_omega()*(samplerate*interp_factor)/(2*M_PI); 233 | 234 | /* Update TUI */ 235 | tui_update_file_in(2*samplerate*bps/8, ftell(samples_file), file_len); 236 | tui_update_data_out(thread_args.bytes_out); 237 | tui_update_pll(freq_hz, rate_hz, pll_get_locked(), agc_get_gain()); 238 | tui_draw_constellation(_symbols_ring, LEN(_symbols_ring)); 239 | } 240 | 241 | message("Demodulation complete\n"); 242 | message("Press any key to exit...\n"); 243 | tui_wait_for_user_input(); 244 | tui_deinit(); 245 | } else { 246 | #endif 247 | if (!quiet) { 248 | /* Batch mode: periodically write status line */ 249 | while (!thread_args.done) { 250 | freq_hz = pll_get_freq()*symrate/(2*M_PI)*(demod == demod_oqpsk ? 2 : 1); 251 | rate_hz = mm_omega()*(samplerate*interp_factor)/(2*M_PI); 252 | 253 | message(batch ? "\n" : "\033[1K\r"); 254 | message("(%5.1f%%) Carrier: %+7.1f Hz, Symbol rate: %.1f Hz, Locked: %s", 255 | file_len ? 100.0 * ftell(samples_file)/file_len : 0, 256 | freq_hz, 257 | rate_hz, 258 | pll_get_locked() ? "Yes" : "No"); 259 | fflush(stdout); 260 | nanosleep(&sleep_timespec, NULL); 261 | } 262 | printf("\n"); 263 | } 264 | 265 | #ifdef ENABLE_TUI 266 | } /* if (!batch) else */ 267 | #endif 268 | 269 | /* Join demod thread */ 270 | pthread_join(tid, NULL); 271 | 272 | /* Cleanup */ 273 | demod_deinit(); 274 | if (soft_file != stdout) fclose(soft_file); 275 | if (samples_file != stdin) fclose(samples_file); 276 | 277 | return 0; 278 | } 279 | 280 | /* Static functions {{{ */ 281 | /** 282 | * Core processing functionality, split into a function for easy threading 283 | */ 284 | static void* 285 | thread_process(void *x) 286 | { 287 | float complex sample; 288 | FILE *samples_file, *soft_file; 289 | int bps; 290 | int (*demod)(float complex *sample); 291 | unsigned ring_idx; 292 | volatile struct thropts *parms = (struct thropts *)x; 293 | 294 | samples_file = parms->samples_file; 295 | soft_file = parms->soft_file; 296 | bps = parms->bps; 297 | demod = parms->demod; 298 | ring_idx = 0; 299 | 300 | 301 | /* Main processing loop */ 302 | parms->bytes_out = 0; 303 | while (!parms->done && wav_read(&sample, bps, samples_file)) { 304 | if (demod(&sample)) { 305 | _symbols_ring[ring_idx++] = MAX(-127, MIN(127, crealf(sample)/2)); 306 | _symbols_ring[ring_idx++] = MAX(-127, MIN(127, cimagf(sample)/2)); 307 | 308 | if (ring_idx >= LEN(_symbols_ring)) { 309 | ring_idx = 0; 310 | 311 | /* Only write symbols after the PLL locked once */ 312 | if (pll_did_lock_once()) { 313 | fwrite(_symbols_ring, RINGSIZE, 2, soft_file); 314 | parms->bytes_out += LEN(_symbols_ring); 315 | } 316 | } 317 | } 318 | } 319 | 320 | /* Flush output buffer */ 321 | fwrite(_symbols_ring, ring_idx, 2, soft_file); 322 | parms->bytes_out += ring_idx; 323 | parms->done = 1; 324 | 325 | /* Wake up main thread */ 326 | pthread_kill(parms->main_tid, SIGUSR1); 327 | 328 | return NULL; 329 | } 330 | /* }}} */ 331 | --------------------------------------------------------------------------------