diff --git a/src/apps/leanmlmrx.cc b/src/apps/leanmlmrx.cc new file mode 100644 index 0000000..2ad4f9e --- /dev/null +++ b/src/apps/leanmlmrx.cc @@ -0,0 +1,833 @@ +// This file is part of LeanSDR (c) . +// See the toplevel README for more information. + +// Multi-channel legacy decoder. + +// Currently FM mono only. + +// Multithreading is a quick hack; will be replaced with proper +// synchronization primitives. + +#include +#include +#include +#include +#include "fftw3.h" +#include +#include + +// For control socket +#include +#include + +// For PMP +#include +#include + +#define MLMRX_DEEMPH 1 + +void fatal(const char *s) { perror(s); exit(1); } +void fail(const char *s) { fprintf(stderr, "** leanmlmrx: %s\n", s); exit(1); } + +// Max FM channels +static const int MAXCHANS = 201; // 20 MHz, 100 kHz spacing + +// Number of FFTs between thread synchronizations. +static const int wqsize = 1024; // about 100 Hz for Fq=200kHz + +struct ci16 { int16_t re; int16_t im; }; + +struct thread_data { + struct runtime *run; + pthread_t pth; + struct job { + ci16 *buf1; // Leftover samples from previous chunk + int nbuf1; + ci16 *buf2; // New samples + volatile bool go; // buf ready. Set by reader, cleared by fft. + uint16_t *ph; // Estimated phases [nchans] + volatile bool done; // ph ready. Set by fft, cleared by joiner. + } jobs[wqsize]; + int qread, qfft, qjoin; + fftwf_complex *in, *out; + fftwf_plan p; + uint64_t nexecs; +}; + +static const int NTHREADS = 2; + +inline void yield() { + usleep(1000); + //pthread_yield(); +} + +uint64_t nwaitr=0, nwaitf1=0, nwaitf2=0, nwaitj=0; + +struct chan { + double F; + int ibin; // Nearest left bin + float bw[2][2][2]; // weight[bin0|1][line][col] + bool enabled; + uint16_t derot; + // Runtime + uint16_t prevph; // Discriminator state + float rms; // For squelch +}; + +struct config { + bool pmp; // PMP input mode + float Fs; // Sample rate (Hz) + float Fc; // Center frequency (Hz) + float Fq; // Quadrature rate (Hz), 0=autoselect + float maxdev; // FM maximum deviation (Hz) + float deemph; // De-emphasis time constant (s) + int N; // FFT size + int nchans; + chan chans[MAXCHANS]; + float squelch; // RMS threshold (0..1, 0 = monitor) + float Fau; // Audio sample rate (Hz), 0=autoselect + bool wav; // Output wav header + int fd_info; // FD for aux output, or -1 + float info_rate; // Spectrum estimation rate (Hz) + int fd_control; // FD for control input, or -1 + + config() + : pmp(false), Fs(25.6e6), Fc(98e6), Fq(0), + maxdev(75e3), deemph(50e-6), + N(64), nchans(0), squelch(0), + Fau(44100), + wav(false), fd_info(-1), info_rate(1), + fd_control(-1) + { }; +}; + +struct spectrum_estimator { + int size; + fftwf_complex *in, *out; + fftwf_plan plan; + float *avgpower; + spectrum_estimator(int _size) { + size = _size; + in = (fftwf_complex*)fftwf_malloc(sizeof(*in )*size); + out = (fftwf_complex*)fftwf_malloc(sizeof(*out)*size); + plan = fftwf_plan_dft_1d(size, in, out, -1, FFTW_ESTIMATE); + avgpower = NULL; + } + void process(ci16 *buf) { + for ( int i=0; iFs/cfg->Fq + 0.5); + if ( stride < cfg->N ) fail("FFT windows overlap"); + + next_th = 0; + nskip = 0; + leftover = new ci16[stride]; + nleftover = 0; + eof = false; + iqmin.re = iqmin.im = 32767; + iqmax.re = iqmax.im = -32768; + nsamples = 0; + + // Aux output + if ( cfg->fd_info < 0 ) { + f_info = NULL; + spestim = NULL; + } else { + f_info = fdopen(cfg->fd_info, "w"); + if ( ! f_info ) fatal("fdopen(fd_info)"); + spestim = new spectrum_estimator(1024); + fflush(f_info); + } + spestim_phase = 0; + // Setup control input + if ( cfg->fd_control < 0 ) + f_control = NULL; + else { + int flags = fcntl(cfg->fd_control, F_GETFL, 0); + fcntl(cfg->fd_control, F_SETFL, flags|O_NONBLOCK); + f_control = fdopen(cfg->fd_control, "r"); + if ( ! f_control ) fatal("fdopen(fd_control)"); + } + } +}; + +void process_samples(runtime *run, ci16 *buf, int count) { + config *cfg = run->cfg; + + // Spectrum estimation + if ( run->f_info ) { + run->spestim_phase += count; + spectrum_estimator *spe = run->spestim; + if ( run->spestim_phase>=cfg->Fs/cfg->info_rate && count>=spe->size ) { + run->spestim_phase = 0; + spe->process(buf); + spe->output(run->f_info); + // Also output generic information + fprintf(run->f_info, "CHANNELS ["); + for ( int i=0; inchans; ++i ) + fprintf(run->f_info, "%s{\"freq\":\"%.5f\",\"enabled\":%s}", + (i?",":""), cfg->chans[i].F/1e6, + (cfg->chans[i].enabled?"true":"false")); + fprintf(run->f_info, "]\n"); + fflush(run->f_info); + } + } + + run->nsamples += count; + + // Inside a skipped segment ? + if ( count < run->nskip ) { + run->nskip -= count; + return; + } + buf += run->nskip; + count -= run->nskip; + run->nskip = 0; + + while ( run->nleftover+count > cfg->N ) { + // Enough samples for one FFT + // in run->leftover[run->nleftover] followed by buf[count]. + { + // Statistics on one sample per chunk + ci16 *s = buf; + if ( s->re < run->iqmin.re ) run->iqmin.re = s->re; + if ( s->re > run->iqmax.re ) run->iqmax.re = s->re; + if ( s->im < run->iqmin.re ) run->iqmin.im = s->im; + if ( s->im > run->iqmax.re ) run->iqmax.im = s->im; + } + // Assign FFT job to next thread + { + thread_data *td = &run->threads[run->next_th]; + run->next_th = (run->next_th+1) % NTHREADS; + // Add to queue + thread_data::job *job = &td->jobs[td->qread]; + while ( job->go ) { ++nwaitr; yield(); } // Busy + job->buf1 = run->leftover; + job->nbuf1 = run->nleftover; + job->buf2 = buf; + job->go = true; + td->qread = (td->qread+1) % wqsize; + } + // Skip to end of chunk + // TBD Allow overlapping here (careful if nleftover!=0) + int nused = cfg->N - run->nleftover; + buf += nused; + count -= nused; + run->nleftover = 0; + // Skip to beginning of next chunk + int skip = run->stride - cfg->N; + if ( count > skip ) { + buf += skip; + count -= skip; + } else { + count = 0; + run->nskip = skip - count; + break; + } + } + + // Copy leftover samples, if any. + // Cost of copying is negligible if batch size is much larger than N. + memcpy(run->leftover+run->nleftover, buf, sizeof(ci16)*count); + run->nleftover += count; +} + +void poll_control(runtime *run) { + if ( ! run->f_control ) return; // Control channel not used + char cmd[256]; + if ( ! fgets(cmd, sizeof(cmd), run->f_control) ) return; // EWOULDBLOCK + config *cfg = run->cfg; + // Parse command on control channel + int arg; + if ( sscanf(cmd,"MUTE %d", &arg)==1 && arg>=0 && argnchans ) + cfg->chans[arg].enabled = false; + else if ( sscanf(cmd,"UNMUTE %d",&arg)==1 && arg>=0 && argnchans ) + cfg->chans[arg].enabled = true; + else if ( sscanf(cmd,"GET /MUTE=%d", &arg)==1 && arg>=0 && argnchans ) + cfg->chans[arg].enabled = false; + else if ( sscanf(cmd,"GET /UNMUTE=%d",&arg)==1 && arg>=0 && argnchans ) + cfg->chans[arg].enabled = true; + else + fprintf(stderr, "Ignoring unrecognized command '%s'\n", cmd); +} + +// Reader thread (PMP streaming variant) + +void thread_reader_pmp(runtime *run) { + int fd = open("/dev/mem", O_RDONLY); + static off_t memsize = (off_t)512 << 20; + void *map = mmap(NULL, memsize, PROT_READ, MAP_SHARED, fd, 0); + if ( map == MAP_FAILED ) fatal("mmap"); + char *physmem = (char*)map; + + struct { + uint64_t magic; + uint64_t physaddr; + uint64_t size; + uint64_t canary; + } pointer; + + while ( fread(&pointer, sizeof(pointer), 1, stdin) == 1 ) { + if ( pointer.magic != 0x504d5031 ) fatal("PMP: bad magic"); + char *buf = physmem + pointer.physaddr; + if ( *(uint64_t*)buf == pointer.canary ) + process_samples(run, (ci16*)buf, pointer.size/sizeof(ci16)); + else + fprintf(stderr, "PMP: Buffer overrun\n"); + poll_control(run); + } +} + +// Reader thread, pipe streaming variant + +void thread_reader_stdin(runtime *run) { + // Read alternately into two buffers each large enough to fill + // the work queue, so that we won't overwrite data before it + // has been processed. + static const int bufsize = 1 << 20; + ci16 *buf1 = new ci16[bufsize]; + ci16 *buf2 = new ci16[bufsize]; + + while ( true ) { + size_t nr1 = fread(buf1, sizeof(ci16), bufsize, stdin); + if ( ! nr1 ) break; + process_samples(run, buf1, nr1); + size_t nr2 = fread(buf2, sizeof(ci16), bufsize, stdin); + if ( ! nr2 ) break; + process_samples(run, buf2, nr2); + #if 0 + // Wait before overwriting the input buffer + for ( int t=0; tthreads[t].jobs[i].go ) { ++nwaitr; yield(); } + #endif + poll_control(run); + } + + fprintf(stderr, "EOF\n"); + fprintf(stderr, "IQ range %d..%d + %d..%dw\n", + run->iqmin.re, run->iqmax.re, run->iqmin.im, run->iqmax.im); +} + +void *thread_reader(void *arg) { + runtime *run = (runtime*)arg; + + if ( run->cfg->pmp ) + thread_reader_pmp(run); + else + thread_reader_stdin(run); + +#if 1 + float dur = run->nsamples / run->cfg->Fs; + fprintf(stderr, "nsamples=%lld (%f s)\n", (long long)run->nsamples, dur); + fprintf(stderr, "nwaitr=%lld (%.0f Hz)\n", (long long)nwaitr, nwaitr/dur); + fprintf(stderr, "nwaitf1=%lld (%.0f Hz)\n", (long long)nwaitf1, nwaitf1/dur); + fprintf(stderr, "nwaitf2=%lld (%.0f Hz)\n", (long long)nwaitf2, nwaitf2/dur); + fprintf(stderr, "nwaitj=%lld (%.0f Hz)\n", (long long)nwaitj, nwaitj/dur); + uint64_t nexecs = run->threads[0].nexecs + run->threads[1].nexecs; + fprintf(stderr, "nfft=%lld+%lld=%lld (%.0f Hz)\n", + (long long)run->threads[0].nexecs, + (long long)run->threads[1].nexecs, + (long long)nexecs, + nexecs/dur); +#endif + + run->eof = true; + return NULL; +} + +// FFT worker threads + +void *thread_fft(void *arg) { + thread_data *td = (thread_data*)arg; + runtime *run = td->run; + config *cfg = run->cfg; + + while ( true ) { + thread_data::job *job = &td->jobs[td->qfft]; + while ( ! job->go ) { ++nwaitf1; yield(); } // buf is not ready + fftwf_complex *pin = td->in; +#define IQSHIFT 0 // Use this to simulate 1-bit sampling + // Use leftover samples + ci16 *buf = job->buf1; + for ( int n=job->nbuf1; n--; ++buf,++pin ) { + (*pin)[0] = buf->re >> IQSHIFT; + (*pin)[1] = buf->im >> IQSHIFT; + } + buf = job->buf2; + for ( int n=cfg->N-job->nbuf1; n--; ++buf,++pin ) { + (*pin)[0] = buf->re >> IQSHIFT; + (*pin)[1] = buf->im >> IQSHIFT; + } + // Release buf + job->go = false; +#if 0 + // Hamming + for ( int i=0; ip); + ++td->nexecs; + while ( job->done ) { ++nwaitf2; yield(); } // ph[] is busy + // Compute phases + uint16_t *pph = job->ph; + for ( chan *ch=cfg->chans; + chchans+cfg->nchans; + ++ch,++pph ) { + fftwf_complex *p = &td->out[ch->ibin]; + // Apply linear combination of nearest bins + float d[2] = { 0, 0 }; + for ( int b=0; b<2; ++b ) + for ( int i=0; i<2; ++i ) + d[i] += ch->bw[b][i][0]*p[b][0] + ch->bw[b][i][1]*p[b][1]; + // atan +#if 0 + int16_t sph = atan2f(d[1], d[0]) * 65536 / (2*M_PI); // float->int + uint16_t ph = sph; // uint -> int +#else + while ( d[0]<-126 || d[0]>126 || d[1]<-126 || d[1]>126 ) { + d[0] *= 0.5; + d[1] *= 0.5; + } + uint8_t ix=(int8_t)d[0], iy=(int8_t)d[1]; + uint16_t ph = run->lut_atan2[iy][ix]; +#endif + *pph = ph; + } + job->done = true; + td->qfft = (td->qfft+1) % wqsize; + } +} + +// WAV utilities + +void fwbe32(FILE *f, uint32_t v) { + fprintf(f, "%c%c%c%c", v>>24, (v>>16)&255, (v>>8)&255, v&255); +} +void fwle32(FILE *f, uint32_t v) { + fprintf(f, "%c%c%c%c", v&255, (v>>8)&255, (v>>16)&255, (v>>24)&255); +} +void fwle16(FILE *f, uint16_t v) { + fprintf(f, "%c%c", v&255, (v>>8)&255); +} +void write_wav_header(FILE *f, float Fau) { + uint32_t nsamples = 0; + fwbe32(f, 0x52494646); // tag='RIFF' + fwle32(f, nsamples ? 36+nsamples : 0); // chunk size + fwbe32(f, 0x57415645); // format='WAVE' + fwbe32(f, 0x666d7420); // id='fmt ' + fwle32(f, 16); // fmt chunk size + fwle16(f, 1); // codec=PCM + fwle16(f, 1); // channels=1 + fwle32(f, Fau); // sample rate + fwle32(f, Fau); // byte rate + fwle16(f, 1); // alignment + fwle16(f, 8); // bits per sample + fwbe32(f, 0x64617461); // id='data' + fwle32(f, nsamples); // data chunk size +} + +int run(config &cfg) { + int audiodecim; // +decimation or -interpolation factor + if ( ! cfg.Fq ) { + if ( cfg.Fau ) { + // Select smallest (sub)multiple of audio rate with enough bandwidth + if ( cfg.Fau > 2*cfg.maxdev ) { + audiodecim = -floor(cfg.Fau/(2*cfg.maxdev)); + cfg.Fq = cfg.Fau / (-audiodecim); + } else { + audiodecim = ceilf((2*cfg.maxdev)/cfg.Fau); + cfg.Fq = cfg.Fau * audiodecim; + } + } else { + // Demodulate consecutive blocs + cfg.Fq = cfg.Fs / cfg.N; + cfg.Fau = cfg.Fq; + audiodecim = 1; + } + } else { + if ( ! cfg.Fau ) { + // Output audio without decimation + cfg.Fau = cfg.Fq; + audiodecim = 1; + } else { + // Check that audio decimation is integer + audiodecim = floor(cfg.Fq/cfg.Fau + 0.5); + if ( fabs(cfg.Fau*audiodecim-cfg.Fq) > 0.5 ) + fatal("Audio decimation ratio (Fq/Fa) must be an integer\n"); + } + } + + if ( cfg.Fq < 2*cfg.maxdev ) + fprintf(stderr, "Warning: Fq too slow for FM deviation\n"); + + fprintf(stderr, "IQ sample rate %.3f kHz\n", cfg.Fs/1000); + fprintf(stderr, "Channel quadrature rate %.3f kHz\n", cfg.Fq/1000); + fprintf(stderr, "Audio rate %.0f Hz\n", cfg.Fau); + fprintf(stderr, "FFT cut-off %.0f kHz\n", cfg.Fs/cfg.N/1000); + + fprintf(stderr, "Realtime requires %.0f %d-point FFTs per second\n", + cfg.Fq, cfg.N); + + runtime run(&cfg); + + // Setup channels + for ( struct chan *ch=cfg.chans; chF-cfg.Fc) / cfg.Fs; + int bin = floor(fbin); + float Frel = fbin - bin; + if ( Frel < 0.125 ) { // Round to 0 + // Use low bin + ch->bw[0][0][0] = 1; ch->bw[0][0][1] = 0; + ch->bw[0][1][0] = 0; ch->bw[0][1][1] = 1; + ch->bw[1][0][0] = 0; ch->bw[1][0][1] = 0; + ch->bw[1][1][0] = 0; ch->bw[1][1][1] = 0; + } else if ( Frel < 0.375 ) { // Rount to 0.25 + // Rotate -45x3, +135 + ch->bw[0][0][0] = 0.707; ch->bw[0][0][1] = 0.707; + ch->bw[0][1][0] = -0.070; ch->bw[0][1][1] = 0.707; + ch->bw[1][0][0] = -0.2; ch->bw[1][0][1] = -0.2; + ch->bw[1][1][0] = 0.2; ch->bw[1][1][1] = -0.2; + } else if ( Frel < 0.625 ) { // Round to 0.5 + // Rotate -90, +90 + ch->bw[0][0][0] = 0; ch->bw[0][0][1] = 1; + ch->bw[0][1][0] = -1; ch->bw[0][1][1] = 0; + ch->bw[1][0][0] = 0; ch->bw[1][0][1] = -1; + ch->bw[1][1][0] = 1; ch->bw[1][1][1] = 0; + } else if ( Frel < 0.875 ) { // Round to 0.75 + // Rotate -135, +45x3 + ch->bw[0][0][0] = -0.2; ch->bw[0][0][1] = 0.2; + ch->bw[0][1][0] = -0.2; ch->bw[0][1][1] = -0.2; + ch->bw[1][0][0] = 0.707; ch->bw[1][0][1] = -0.707; + ch->bw[1][1][0] = 0.707; ch->bw[1][1][1] = 0.707; + } else { // Round to 1 + // Use high bin + ch->bw[0][0][0] = 0; ch->bw[0][0][1] = 0; + ch->bw[0][1][0] = 0; ch->bw[0][1][1] = 0; + ch->bw[1][0][0] = 1; ch->bw[1][0][1] = 0; + ch->bw[1][1][0] = 0; ch->bw[1][1][1] = 1; + } + + // Scale for fast atan2 lookup + for ( int b=0; b<2; ++b ) + for ( int i=0; i<2; ++i ) + for ( int j=0; j<2; ++j ) + ch->bw[b][i][j] *= 8.0 * 128 / 2048 / cfg.N; + + ch->ibin = (cfg.N+bin) % cfg.N; + float derot = 2*M_PI * (ch->F-cfg.Fc) * run.stride/cfg.Fs; + while ( derot > M_PI ) derot -= 2*M_PI; + while ( derot < -M_PI ) derot += 2*M_PI; + ch->derot = (int16_t) (derot * 65536 / (2*M_PI)); + fprintf(stderr, " channel fbin=%.2f ibins=%d,%d derot=%f\n", + fbin, ch->ibin, ch->ibin+1, derot); + ch->prevph = 0; + ch->rms = 1; + } + + // Spawn FFT threads + for ( thread_data *td=run.threads; tdrun = &run; + for ( int i=0; ijobs[i].ph = new uint16_t[cfg.nchans]; + td->jobs[i].go = false; + td->jobs[i].done = false; + } + td->qread = td->qfft = td->qjoin = 0; + td->in = (fftwf_complex*) fftwf_malloc(sizeof(*td->in) *cfg.N); + td->out = (fftwf_complex*) fftwf_malloc(sizeof(*td->out)*cfg.N); + td->p = fftwf_plan_dft_1d(cfg.N, td->in, td->out, -1, FFTW_ESTIMATE); + if ( pthread_create(&td->pth, NULL, thread_fft, td) ) + fatal("pthread_create"); + td->nexecs = 0; + } + + // Spawn the reader/dispatcher thread + if ( pthread_create(&run.reader, NULL, thread_reader, &run) ) + fatal("pthread_create"); + +#if 0 + { + float Ftest = 4.25; + fprintf(stderr, "Carrier %f between bins\n", Ftest); + thread_data *td = &run.threads[0]; + for ( int i=0; iin[i][0]=cosf(2*M_PI*Ftest*i/cfg.N); + td->in[i][1]=sinf(2*M_PI*Ftest*i/cfg.N); + } + fftwf_execute(td->p); + for ( int i=0; i<10; ++i )a + fprintf(stderr, "%3d %f %f\n", i, td->out[i][0], td->out[i][1]); + } + exit(0); +#endif + + // Demodulate and output + + fprintf(stderr, "De-emphasis: %.0f us\n", cfg.deemph*1e6); + float alpha_deemph = 1 / (cfg.Fq*cfg.deemph); + + float t_squelch = 0.1; // Squelch response time (s) + float alpha_squelch = 1 / (cfg.Fau*t_squelch); + + uint16_t audioclock = 0; + + float discr_gain = cfg.Fq/65536/(2*cfg.maxdev); // Scale from 16-bit angles + discr_gain *= 0.75; // Allow some roll-off + discr_gain *= 256; // Scale to 8-bit audio out + + float deemph = 0; // Filter state + + if ( cfg.wav ) { write_wav_header(stdout, cfg.Fau); fflush(stdout); } + + static const int audiobatch = 8192; + int8_t audiobuf[audiobatch], *audioptr=audiobuf; + + while ( ! run.eof ) { + for ( thread_data *td=run.threads; tdjobs[td->qjoin]; + while ( !job->done && !run.eof ) { ++nwaitj; yield(); } // Not ready + if ( run.eof ) break; + // Compute one audio sample + { + float audio = 0; // Sum of channels + int nactive = 0; // Unmuted and not squelched + uint16_t *pph = job->ph; + for ( chan *ch=cfg.chans; chenabled ) continue; + int16_t dph = *pph - ch->prevph - ch->derot; // uint modulo -> int + ch->prevph = *pph; + float dev = dph; + if ( cfg.squelch ) { + ch->rms = ch->rms*(1-alpha_squelch) + + (dev*dev/(32768*32768))*alpha_squelch; + if ( ch->rms > 1-cfg.squelch ) continue; + } + audio += dev; + ++nactive; + } + + deemph = deemph*(1-alpha_deemph) + audio*alpha_deemph; + audio = deemph; + + if ( audiodecim < 0 ) { + // Upsample (repeat) + audio *= run.lut_audioscale[nactive]; + int8_t au = audio * discr_gain; + if ( cfg.wav ) au ^= 128; // Make unsigned + for ( int repeat=-audiodecim; --repeat>=0; ) { + *audioptr = au; + if ( ++audioptr == audiobuf+audiobatch ) { + int nw = write(1, audiobuf, sizeof(audiobuf)); + if ( nw != sizeof(audiobuf) ) fatal("write"); + audioptr = audiobuf; + } + } + } else { + // Downsample (decimate) + if ( ++audioclock == audiodecim ) { + audioclock = 0; + audio *= run.lut_audioscale[nactive]; + int8_t au = audio * discr_gain; + if ( cfg.wav ) au ^= 128; // Make unsigned + *audioptr = au; + if ( ++audioptr == audiobuf+audiobatch ) { + int nw = write(1, audiobuf, sizeof(audiobuf)); + if ( nw != sizeof(audiobuf) ) fatal("write"); + audioptr = audiobuf; + } + } + } + } + job->done = false; + td->qjoin = (td->qjoin+1) % wqsize; + } // threads + } // main loop + + for ( thread_data *td=run.threads; tdp); + //fftwf_free(in); // double-free bug ? + //fftwf_free(out); + } + + fprintf(stderr, "Exiting.\n"); + + return 0; +} + +// CLI + +void usage(const char *name, FILE *f, int c, const char *info=NULL) { + fprintf(f, "Usage: %s [options] CHANNEL ... < IQ > RAWAUDIO\n", name); + fprintf(f, "Read int16 I/Q from stdin, demodulate multiple FM channels,\n" + "write int8 mono audio to stdout.\n"); + fprintf + (f, + "\nOptions:\n" + " --pmp Input by reference to /dev/mem\n" + " --fs FLOAT IQ sampling rate (Hz)\n" + " --fc FLOAT Center RF frequency (Hz)\n" + " -N INT FFT size\n" + " --fq FLOAT Quadrature rate (Hz)\n" + " --maxdev FLOAT FM deviation (Hz)\n" + " --deemph FLOAT De-emphasis time constant (s)\n" + " --fa FLOAT Audio sampling rate\n" + " --wav Output WAV header\n" + " --fd-info INT Output aux info to this file descriptor\n" + " --info-rate FLOAT Aux info refresh rate (Hz)\n" + " --fd-control INT Read MUTE/UNMUTE requests from this FD\n" + "\n" + "Channel syntax:\n" + " FreqMHz Single channel\n" + " Min:Step:Max Multiple channels with fixed separation\n" + " (...) Same as above, muted initially\n" + ); + if ( info ) fprintf(f, "Error while processing '%s'\n", info); + exit(c); +} + +void add_chan(config *cfg, double fMHz, bool enabled) { + float f = fMHz * 1e6; + if ( cfg->nchans == MAXCHANS ) fail("Too many channels"); + struct chan *ch = &cfg->chans[cfg->nchans]; + ch->F = f; + ch->enabled = enabled; + ++cfg->nchans; +} + +#ifndef VERSION +#define VERSION "undefined" +#endif + +int main(int argc, char *argv[]) { + config cfg; + + for ( int i=1; i