diff options
Diffstat (limited to 'src/ch.c')
| -rw-r--r-- | src/ch.c | 479 |
1 files changed, 479 insertions, 0 deletions
diff --git a/src/ch.c b/src/ch.c new file mode 100644 index 0000000..57411bd --- /dev/null +++ b/src/ch.c @@ -0,0 +1,479 @@ +/*---------------------------------------------------------------------------*\ + + FILE........: ch.c + AUTHOR......: David Rowe + DATE CREATED: May 2015 + + Channel simulation program for testing command line versions of modems. + +\*---------------------------------------------------------------------------*/ + +/* + Copyright (C) 2015-2022 David Rowe + + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 2.1, as + published by the Free Software Foundation. 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 Lesser General Public License + along with this program; if not, see <http://www.gnu.org/licenses/>. +*/ + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <math.h> +#include <errno.h> +#include <getopt.h> + +#include "freedv_api.h" +#include "codec2_cohpsk.h" +#include "comp_prim.h" +#include "ht_coeff.h" +#include "ssbfilt_coeff.h" + +#include "debug_alloc.h" + +#define BUF_N 160 +#define MPG_DELAY_MS 0.5 +#define MPP_DELAY_MS 2.0 +#define MPD_DELAY_MS 4.0 + +/* see instructions below for how to generate these files */ + +#define DEFAULT_FADING_DIR "unittest" +#define MPG_FADING_FILE_NAME "slow_fading_samples.float" +#define MPP_FADING_FILE_NAME "fast_fading_samples.float" +#define MPD_FADING_FILE_NAME "faster_fading_samples.float" + +// Gaussian from uniform: +float gaussian(void) { + double x = (double)rand() / RAND_MAX; + double y = (double)rand() / RAND_MAX; + double z = sqrt(-2 * log(x)) * cos(2 * M_PI * y); + return sqrt(1./2.) * z; +} + +// complex noise sample +COMP noise(void) { + COMP n = {gaussian(),gaussian()}; + return n; +} + +int main(int argc, char *argv[]) +{ + FILE *fin, *ffading, *fout; + char *fading_dir; + float NodB, foff_hz; + int fading_en, nhfdelay; + + short buf[BUF_N]; + float htbuf[HT_N+BUF_N]; + COMP ch_in[BUF_N]; + COMP ch_fdm[BUF_N]; + COMP ssbfiltbuf[SSBFILT_N+BUF_N]; + COMP ssbfiltout[BUF_N]; + + COMP phase_ch; + float No, variance; + COMP scaled_noise; + float hf_gain; + COMP *ch_fdm_delay = NULL, aspread, aspread_2ms, delayed, direct; + float tx_pwr, tx_pwr_fade, noise_pwr, user_multipath_delay; + int frames, i, j, k, Fs, ret, nclipped, noutclipped, ssbfilt_en, complex_out, ctest; + float sam, peak, clip, papr, CNo, snr3k, gain; + + if (argc < 3) { + helpmsg: + fprintf(stderr, "Command line channel simulation tool.\n" + "\n" + "usage: %s InputRealModemRawFile OutputRealModemRawFile [Options]\n" + "\n" + " real int16 input -> Gain -> Hilbert Transform -> clipper -> freq shift ->\n" + " Multipath -> AWGN noise -> SSB filter -> real int16 output\n" + "\n" + "[--clip int16] Hilbert clipper (clip complex signal magnitude, default 32767)\n" + "[--complexout] Optional int16 IQ complex output (default real int16)\n" + "[--ctest] Check PAPR is around 0dB, used to support ctests\n" + "[--freq FoffHz] Frequency offset (default 0Hz)\n" + "[--fading_dir Path] path to multipath fading files (default 'unittest')\n" + "[--Fs SampleRateHz] Sample rate of simulation (default 8000 Hz)\n" + "[--gain G] Linear gain (default 1.0)\n" + "[--mpg] Multipath good 0.1Hz Doppler, 0.5ms delay\n" + "[--mpp] Multipath poor 1.0Hz Doppler, 2.0ms delay\n" + "[--mpd] Multipath disturbed 2.0Hz Doppler, 4.0ms delay\n" + "[--ssbfilt 0|1] SSB bandwidth filter (default 1 on)\n" + "[--mulipath_delay ms] Optionally adjust multipath delay\n" + "[--No dBHz] AWGN Noise density dB/Hz (default -100)" + "\n" + , argv[0]); + exit(1); + } + + if (strcmp(argv[1], "-") == 0) fin = stdin; + else if ( (fin = fopen(argv[1],"rb")) == NULL ) { + fprintf(stderr, "ch: Error opening input modem raw file: %s: %s.\n", + argv[1], strerror(errno)); + exit(1); + } + + if (strcmp(argv[2], "-") == 0) fout = stdout; + else if ( (fout = fopen(argv[2],"wb")) == NULL ) { + fprintf(stderr, "ch: Error opening output modem raw file: %s: %s.\n", + argv[2], strerror(errno)); + exit(1); + } + + NodB = -100; + Fs = 8000; foff_hz = 0.0; fading_en = 0; ctest = 0; + clip =32767; gain = 1.0; + ssbfilt_en = 1; complex_out = 0; + fading_dir = strdup(DEFAULT_FADING_DIR); user_multipath_delay = -1.0; + + int o = 0; + int opt_idx = 0; + while( o != -1 ){ + static struct option long_opts[] = { + {"complexout", no_argument, 0, 'o'}, + {"ctest", no_argument, 0, 't'}, + {"clip", required_argument, 0, 'c'}, + {"fading_dir", required_argument, 0, 'u'}, + {"freq", required_argument, 0, 'f'}, + {"Fs", required_argument, 0, 'r'}, + {"gain", required_argument, 0, 'g'}, + {"ssbfilt", required_argument, 0, 's'}, + {"help", no_argument, 0, 'h'}, + {"mpg", no_argument, 0, 'i'}, + {"mpp", no_argument, 0, 'p'}, + {"mpd", no_argument, 0, 'd'}, + {"multipath_delay", required_argument, 0, 'm'}, + {"No", required_argument, 0, 'n'}, + {0, 0, 0, 0} + }; + + o = getopt_long(argc,argv,"c:df:g:im:n:opr:s:tu:h",long_opts,&opt_idx); + + switch(o) { + case 'c': + clip = atof(optarg); + break; + case 'd': + fading_en = 3; + break; + case 'f': + foff_hz = atof(optarg); + break; + case 'g': + gain = atof(optarg); + break; + case 'i': + fading_en = 1; + break; + case 'm': + user_multipath_delay = atof(optarg); + break; + case 'n': + NodB = atof(optarg); + break; + case 'o': + complex_out = 1; + break; + case 'p': + fading_en = 2; + break; + case 'r': + Fs = atoi(optarg); + break; + case 's': + ssbfilt_en = atoi(optarg); + break; + case 't': + ctest = 1; + break; + case 'u': + fading_dir = strdup(optarg); + break; + case 'h': + case '?': + goto helpmsg; + break; + } + } + + phase_ch.real = 1.0; phase_ch.imag = 0.0; + + /* N = var = NoFs */ + + // arbitrary noise scaling, to maintain backwards compatibility with many tests. TODO make the No + // units more sensible, and fix all the tests that depend on this scaling + No = pow(10.0, NodB/10.0)*1000*1000; + variance = Fs*No; + + tx_pwr = tx_pwr_fade = noise_pwr = 0.0; + noutclipped = 0; nclipped = 0; + peak = 0.0; + + /* init HF fading model */ + + ffading = NULL; + nhfdelay = 0; + if (fading_en) { + char fname[256]; + + if (fading_en == 1) { + sprintf(fname, "%s/%s", fading_dir, MPG_FADING_FILE_NAME); + ffading = fopen(fname, "rb"); + if (ffading == NULL) { + cant_load_fading_file: + fprintf(stderr, "-----------------------------------------------------\n"); + fprintf(stderr, "ch ERROR: Can't find fading file: %s\n", fname); + fprintf(stderr, "\nAdjust path --fading_dir or use GNU Octave to generate:\n\n"); + gen_fading_file: + fprintf(stderr, "$ octave --no-gui\n"); + fprintf(stderr, "octave:24> pkg load signal\n"); + fprintf(stderr, "octave:24> time_secs=60\n"); + fprintf(stderr, "octave:25> ch_fading(\"faster_fading_samples.float\", 8000, 2.0, 8000*time_secs)\n"); + fprintf(stderr, "octave:26> ch_fading(\"fast_fading_samples.float\", 8000, 1.0, 8000*time_secs)\n"); + fprintf(stderr, "octave:27> ch_fading(\"slow_fading_samples.float\", 8000, 0.1, 8000*time_secs)\n"); + fprintf(stderr, "-----------------------------------------------------\n"); + exit(1); + } + nhfdelay = floor(MPG_DELAY_MS*Fs/1000); + } + + if (fading_en == 2) { + sprintf(fname, "%s/%s", fading_dir, MPP_FADING_FILE_NAME); + ffading = fopen(fname, "rb"); + if (ffading == NULL) goto cant_load_fading_file; + nhfdelay = floor(MPP_DELAY_MS*Fs/1000); + } + + if (fading_en == 3) { + sprintf(fname, "%s/%s", fading_dir, MPD_FADING_FILE_NAME); + ffading = fopen(fname, "rb"); + if (ffading == NULL) goto cant_load_fading_file; + nhfdelay = floor(MPD_DELAY_MS*Fs/1000); + } + + ch_fdm_delay = (COMP*)MALLOC((nhfdelay+COHPSK_NOM_SAMPLES_PER_FRAME)*sizeof(COMP)); + assert(ch_fdm_delay != NULL); + for(i=0; i<nhfdelay+COHPSK_NOM_SAMPLES_PER_FRAME; i++) { + ch_fdm_delay[i].real = 0.0; + ch_fdm_delay[i].imag = 0.0; + } + + /* optionally override delay from command line */ + if (user_multipath_delay >= 0.0) nhfdelay = floor(user_multipath_delay*Fs/1000); + + /* first values in file are HF gains */ + + for (i=0; i<4; i++) + ret = fread(&hf_gain, sizeof(float), 1, ffading); + //fprintf(stderr, "hf_gain: %f\n", hf_gain); + } + + assert(HT_N == sizeof(ht_coeff)/sizeof(COMP)); + for(i=0; i<HT_N; i++) { + htbuf[i] = 0.0; + } + for(i=0; i<SSBFILT_N; i++) { + ssbfiltbuf[i].real = 0.0; ssbfiltbuf[i].imag = 0.0; + } + COMP lo_phase = {1.0,0.0}; + COMP lo_freq; + lo_freq.real = cos(2.0*M_PI*SSBFILT_CENTRE/Fs); + lo_freq.imag = sin(2.0*M_PI*SSBFILT_CENTRE/Fs); + + fprintf(stderr, "ch: Fs: %d NodB: %4.2f foff: %4.2f Hz fading: %d nhfdelay: %d clip: %4.2f ssbfilt: %d complexout: %d\n", + Fs, NodB, foff_hz, fading_en, nhfdelay, clip, ssbfilt_en, complex_out); + + /* --------------------------------------------------------*\ + Main Loop + \*---------------------------------------------------------*/ + + frames = 0; + while(fread(buf, sizeof(short), BUF_N, fin) == BUF_N) { + frames++; + + /* Hilbert Transform to produce complex signal so we can do + single sided freq shifts, HF channel models, and analog + compression. Allows us to use real signal I/O. + + As the real and imag filters both have unity gain, ch_in[] + has twice the power of the real input signal buf[]. + */ + + for(i=0, j=HT_N; i<BUF_N; i++,j++) { + + htbuf[j] = (float)buf[i]*gain; + + /* FIR filter with HT to get imag, just delay to get real */ + + ch_in[i].real = 0.0; + ch_in[i].imag = 0.0; + for(k=0; k<HT_N; k++) { + ch_in[i].real += htbuf[j-k]*ht_coeff[k].real; + ch_in[i].imag += htbuf[j-k]*ht_coeff[k].imag; + } + //printf("%d %f %f\n", i, ch_in[i].real, ch_in[i].imag); + } + assert(j <= (BUF_N+HT_N)); + + /* update HT memory */ + for(i=0; i<HT_N; i++) + htbuf[i] = htbuf[i+BUF_N]; + + /* --------------------------------------------------------*\ + Clipping mag of complex signal + \*---------------------------------------------------------*/ + + for(i=0; i<BUF_N; i++) { + float mag = sqrt(ch_in[i].real*ch_in[i].real + ch_in[i].imag*ch_in[i].imag); + float angle = atan2(ch_in[i].imag, ch_in[i].real); + if (mag > clip) { + mag = clip; + nclipped++; + } + tx_pwr += mag*mag; + /* we get a bit of overshoot in peak measurements if HT filter hasn't been primed */ + if (frames*BUF_N > HT_N) + if (mag > peak) { + peak = mag; + //fprintf(stderr, "%d %f\n",frames, mag); + } + ch_in[i].real = mag*cos(angle); + ch_in[i].imag = mag*sin(angle); + } + + /* --------------------------------------------------------*\ + Channel + \*---------------------------------------------------------*/ + + fdmdv_freq_shift_coh(ch_fdm, ch_in, foff_hz, Fs, &phase_ch, BUF_N); + + /* optional HF fading -------------------------------------*/ + + if (fading_en) { + + /* update delayed signal buffer */ + + for(i=0; i<nhfdelay; i++) + ch_fdm_delay[i] = ch_fdm_delay[i+BUF_N]; + for(j=0; j<BUF_N; i++, j++) + ch_fdm_delay[i] = ch_fdm[j]; + + /* combine direct and delayed paths, both multiplied by + "spreading" (Doppler) functions */ + + for(i=0; i<BUF_N; i++) { + ret = fread(&aspread, sizeof(COMP), 1, ffading); + if (ret == 0) { + fprintf(stderr, "ch: Fading file finished - simulation stopping. You may need more samples:\n"); + goto gen_fading_file; + } + ret = fread(&aspread_2ms, sizeof(COMP), 1, ffading); + if (ret == 0) { + fprintf(stderr, "ch: Fading file finished - simulation stopping. You may need more samples:\n"); + goto gen_fading_file; + } + //printf("%f %f %f %f\n", aspread.real, aspread.imag, aspread_2ms.real, aspread_2ms.imag); + + direct = cmult(aspread, ch_fdm[i]); + delayed = cmult(aspread_2ms, ch_fdm_delay[i]); + ch_fdm[i] = fcmult(hf_gain, cadd(direct, delayed)); + } + } + + /* Measure power after fading model to make sure average pwr + is the same as AWGN channels. We only output the real + signal, which is half the power. */ + + for(i=0; i<BUF_N; i++) { + tx_pwr_fade += pow(ch_fdm[i].real, 2.0); + } + + /* AWGN noise ------------------------------------------*/ + + for(i=0; i<BUF_N; i++) { + COMP n = noise(); + scaled_noise = fcmult(sqrt(variance), n); + ch_fdm[i] = cadd(ch_fdm[i], scaled_noise); + noise_pwr += pow(scaled_noise.real, 2.0) + pow(scaled_noise.imag, 2.0); + } + + /* FIR filter to simulate (a rather flat) SSB filter. We + filter the complex signal by shifting it down to DC and + using real coefficients. */ + + for(i=0, j=SSBFILT_N; i<BUF_N; i++,j++) { + if (ssbfilt_en) { + ssbfiltbuf[j] = cmult(ch_fdm[i], cconj(lo_phase)); + ssbfiltout[i].real = 0.0; ssbfiltout[i].imag = 0.0; + for(k=0; k<SSBFILT_N; k++) { + ssbfiltout[i].real += ssbfiltbuf[j-k].real*ssbfilt_coeff[k]; + ssbfiltout[i].imag += ssbfiltbuf[j-k].imag*ssbfilt_coeff[k]; + } + ssbfiltout[i] = cmult(ssbfiltout[i], lo_phase); + lo_phase = cmult(lo_phase, lo_freq); + } + else { + ssbfiltout[i] = ch_fdm[i]; + } + } + + /* update SSB filter memory */ + for(i=0; i<SSBFILT_N; i++) + ssbfiltbuf[i] = ssbfiltbuf[i+BUF_N]; + + int nout = (complex_out+1)*BUF_N; + short bufout[nout], *pout=bufout; + for(i=0; i<BUF_N; i++) { + sam = ssbfiltout[i].real; + if (sam > 32767.0) { noutclipped++; sam = 32767.0; } + if (sam < -32767.0) { noutclipped++; sam = -32767.0; } + *pout++ = sam; + if (complex_out) { + sam = ssbfiltout[i].imag; + if (sam > 32767.0) { noutclipped++; sam = 32767.0; } + if (sam < -32767.0) { noutclipped++; sam = -32767.0; } + *pout++ = sam; + } + } + + fwrite(bufout, sizeof(short), nout, fout); + + /* if this is in a pipeline, we probably don't want the usual + buffering to occur */ + + if (fout == stdout) fflush(stdout); + } + + fclose(fin); + fclose(fout); + + int nsamples = frames*BUF_N; + papr = 10*log10(peak*peak/(tx_pwr/nsamples)); + CNo = 10*log10(tx_pwr/(noise_pwr/(Fs))); + snr3k = CNo - 10*log10(3000); + float outclipped_percent = noutclipped*100.0/nsamples; + fprintf(stderr, "ch: SNR3k(dB): %8.2f C/No....: %8.2f\n", snr3k, CNo); + fprintf(stderr, "ch: peak.....: %8.2f RMS.....: %8.2f CPAPR.....: %5.2f \n", peak, sqrt(tx_pwr/nsamples), papr); + fprintf(stderr, "ch: Nsamples.: %8d clipped.: %8.2f%% OutClipped: %5.2f%%\n", + nsamples, nclipped*100.0/nsamples, outclipped_percent); + if (outclipped_percent > 0.1) fprintf(stderr, "ch: WARNING output clipping\n"); + + if (ffading != NULL) fclose(ffading); + if (ch_fdm_delay != NULL) FREE(ch_fdm_delay); + if (ctest) { + /* special ctest mode: check CPAPR is around 0dB */ + if (fabs(papr) < 0.7) return 0; else return 1; + } + else return 0; +} |
