1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-12-23 10:05:46 -05:00
sdrangel/libfreedv/cohpsk.cpp

1395 lines
43 KiB
C++

/*---------------------------------------------------------------------------*\
FILE........: cohpsk.c
AUTHOR......: David Rowe
DATE CREATED: March 2015
Functions that implement a coherent PSK FDM modem.
\*---------------------------------------------------------------------------*/
/*
Copyright (C) 2015 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/>.
*/
/*---------------------------------------------------------------------------*\
INCLUDES
\*---------------------------------------------------------------------------*/
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "codec2_cohpsk.h"
#include "cohpsk_defs.h"
#include "cohpsk_internal.h"
#include "fdmdv_internal.h"
#include "pilots_coh.h"
#include "comp_prim.h"
#include "kiss_fft.h"
#include "linreg.h"
#include "rn_coh.h"
#include "test_bits_coh.h"
namespace FreeDV
{
static COMP qpsk_mod[] = {
{ 1.0, 0.0},
{ 0.0, 1.0},
{ 0.0,-1.0},
{-1.0, 0.0}
};
static int sampling_points[] = {0, 1, 6, 7};
void corr_with_pilots_comp(float *corr_out, float *mag_out, struct COHPSK *coh, int t, COMP f_fine);
void update_ct_symb_buf(COMP ct_symb_buf[][COHPSK_NC*ND], COMP ch_symb[][COHPSK_NC*ND]);
/*---------------------------------------------------------------------------*\
FUNCTIONS
\*---------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------* \
FUNCTION....: cohpsk_create
AUTHOR......: David Rowe
DATE CREATED: Marcg 2015
Create and initialise an instance of the modem. Returns a pointer
to the modem states or NULL on failure. One set of states is
sufficient for a full duplex modem.
\*---------------------------------------------------------------------------*/
struct COHPSK *cohpsk_create(void)
{
struct COHPSK *coh;
struct FDMDV *fdmdv;
int r,c,p,i;
float freq_hz;
assert(COHPSK_NC == PILOTS_NC);
assert(COHPSK_NOM_SAMPLES_PER_FRAME == (COHPSK_M*NSYMROWPILOT));
assert(COHPSK_MAX_SAMPLES_PER_FRAME == (COHPSK_M*NSYMROWPILOT+COHPSK_M/P));
assert(COHPSK_ND == ND);
assert(COHPSK_NSYM == NSYM); /* as we want to use the tx sym mem on fdmdv */
assert(COHPSK_NT == NT);
coh = (struct COHPSK*) malloc(sizeof(struct COHPSK));
if (coh == NULL)
return NULL;
/* set up buffer of tx pilot symbols for coh demod on rx */
for(r=0; r<2*NPILOTSFRAME; ) {
for(p=0; p<NPILOTSFRAME; r++, p++) {
for(c=0; c<COHPSK_NC; c++) {
coh->pilot2[r][c] = pilots_coh[p][c];
}
}
}
/* Clear symbol buffer memory */
for (r=0; r<NCT_SYMB_BUF; r++) {
for(c=0; c<COHPSK_NC*ND; c++) {
coh->ct_symb_buf[r][c].real = 0.0;
coh->ct_symb_buf[r][c].imag = 0.0;
}
}
coh->ff_phase.real = 1.0; coh->ff_phase.imag = 0.0;
coh->sync = 0;
coh->frame = 0;
coh->ratio = 0.0;
coh->nin = COHPSK_M;
/* clear sync window buffer */
for (i=0; i<NSW*NSYMROWPILOT*COHPSK_M; i++) {
coh->ch_fdm_frame_buf[i].real = 0.0;
coh->ch_fdm_frame_buf[i].imag = 0.0;
}
/* set up fdmdv states so we can use those modem functions */
fdmdv = fdmdv_create(COHPSK_NC*ND - 1);
fdmdv->fsep = COHPSK_RS*(1.0 + COHPSK_EXCESS_BW);
for(c=0; c<COHPSK_NC*ND; c++) {
fdmdv->phase_tx[c].real = 1.0;
fdmdv->phase_tx[c].imag = 0.0;
/* note non-linear carrier spacing to help PAPR, works v well in conjunction with CLIP */
freq_hz = fdmdv->fsep*( -(COHPSK_NC*ND)/2 - 0.5 + pow(c + 1.0, 0.98) );
fdmdv->freq[c].real = cosf(2.0*M_PI*freq_hz/COHPSK_FS);
fdmdv->freq[c].imag = sinf(2.0*M_PI*freq_hz/COHPSK_FS);
fdmdv->freq_pol[c] = 2.0*M_PI*freq_hz/COHPSK_FS;
//printf("c: %d %f %f\n",c,freq_hz,fdmdv->freq_pol[c]);
for(i=0; i<COHPSK_NFILTER; i++) {
coh->rx_filter_memory[c][i].real = 0.0;
coh->rx_filter_memory[c][i].imag = 0.0;
}
/* optional per-carrier amplitude weighting for testing */
coh->carrier_ampl[c] = 1.0;
}
fdmdv->fbb_rect.real = cosf(2.0*PI*FDMDV_FCENTRE/COHPSK_FS);
fdmdv->fbb_rect.imag = sinf(2.0*PI*FDMDV_FCENTRE/COHPSK_FS);
fdmdv->fbb_pol = 2.0*PI*FDMDV_FCENTRE/COHPSK_FS;
coh->fdmdv = fdmdv;
coh->sig_rms = coh->noise_rms = 0.0;
for(c=0; c<COHPSK_NC*ND; c++) {
for (r=0; r<NSYMROW; r++) {
coh->rx_symb[r][c].real = 0.0;
coh->rx_symb[r][c].imag = 0.0;
}
}
coh->verbose = 0;
/* disable optional logging by default */
coh->rx_baseband_log = NULL;
coh->rx_baseband_log_col_index = 0;
coh->rx_filt_log = NULL;
coh->rx_filt_log_col_index = 0;
coh->ch_symb_log = NULL;
coh->ch_symb_log_r = 0;
coh->rx_timing_log = NULL;
coh->rx_timing_log_index = 0;
/* test frames */
coh->ptest_bits_coh_tx = coh->ptest_bits_coh_rx[0] = coh->ptest_bits_coh_rx[1] = (int*)test_bits_coh;
coh->ptest_bits_coh_end = (int*)test_bits_coh + sizeof(test_bits_coh)/sizeof(int);
/* Disable 'reduce' frequency estimation mode */
coh->freq_est_mode_reduced = 0;
return coh;
}
/*---------------------------------------------------------------------------*\
FUNCTION....: cohpsk_destroy
AUTHOR......: David Rowe
DATE CREATED: March 2015
Destroy an instance of the modem.
\*---------------------------------------------------------------------------*/
void cohpsk_destroy(struct COHPSK *coh)
{
assert(coh != NULL);
fdmdv_destroy(coh->fdmdv);
free(coh);
}
/*---------------------------------------------------------------------------*\
FUNCTION....: bits_to_qpsk_symbols()
AUTHOR......: David Rowe
DATE CREATED: March 2015
Rate Rs modulator. Maps bits to parallel DQPSK symbols and inserts pilot symbols.
\*---------------------------------------------------------------------------*/
void bits_to_qpsk_symbols(COMP tx_symb[][COHPSK_NC*ND], int tx_bits[], int nbits)
{
int i, r, c, p_r, data_r, d, diversity;
short bits;
/* check allowed number of bits supplied matches number of QPSK
symbols in the frame */
assert( (NSYMROW*COHPSK_NC*2 == nbits) || (NSYMROW*COHPSK_NC*2*ND == nbits));
/* if we input twice as many bits we don't do diversity */
if (NSYMROW*COHPSK_NC*2 == nbits) {
diversity = 1; /* diversity mode */
}
else {
diversity = 2; /* twice as many bits, non diversity mode */
}
/*
Insert two rows of Nc pilots at beginning of data frame.
Organise QPSK symbols into a NSYMBROWS rows by PILOTS_NC*ND cols matrix,
each column is a carrier, time flows down the cols......
Note: the "& 0x1" prevents and non binary tx_bits[] screwing up
our lives. Call me defensive.
sqrtf(ND) term ensures the same energy/symbol for different
diversity factors.
*/
r = 0;
for(p_r=0; p_r<2; p_r++) {
for(c=0; c<COHPSK_NC*ND; c++) {
tx_symb[r][c].real = pilots_coh[p_r][c % COHPSK_NC]/sqrtf(ND);
tx_symb[r][c].imag = 0.0;
}
r++;
}
for(data_r=0; data_r<NSYMROW; data_r++, r++) {
for(c=0; c<COHPSK_NC*diversity; c++) {
i = c*NSYMROW + data_r;
bits = (tx_bits[2*i]&0x1)<<1 | (tx_bits[2*i+1]&0x1);
tx_symb[r][c] = fcmult(1.0/sqrtf(ND),qpsk_mod[bits]);
}
}
assert(p_r == NPILOTSFRAME);
assert(r == NSYMROWPILOT);
/* if in diversity mode, copy symbols to upper carriers */
for(d=1; d<1+ND-diversity; d++) {
for(r=0; r<NSYMROWPILOT; r++) {
for(c=0; c<COHPSK_NC; c++) {
tx_symb[r][c+COHPSK_NC*d] = tx_symb[r][c];
}
}
}
}
/*---------------------------------------------------------------------------*\
FUNCTION....: qpsk_symbols_to_bits()
AUTHOR......: David Rowe
DATE CREATED: March 2015
Rate Rs demodulator. Extract pilot symbols and estimate amplitude and phase
of each carrier. Correct phase of data symbols and convert to bits.
Further improvement. In channels with slowly changing phase we
could optionally use pilots from several past and future symbols.
\*---------------------------------------------------------------------------*/
void qpsk_symbols_to_bits(struct COHPSK *coh, float rx_bits[], COMP ct_symb_buf[][COHPSK_NC*ND])
{
int p, r, c, i, pc, d, n;
float x[NPILOTSFRAME+2], x1;
COMP y[NPILOTSFRAME+2], yfit;
COMP rx_symb_linear[NSYMROW*COHPSK_NC*ND];
COMP m, b;
COMP __attribute__((unused)) corr, rot, pi_on_4, phi_rect, div_symb;
float mag, __attribute__((unused)) phi_, __attribute__((unused)) amp_;
float sum_x, sum_xx, noise_var;
COMP s;
pi_on_4.real = cosf(M_PI/4); pi_on_4.imag = sinf(M_PI/4);
for(c=0; c<COHPSK_NC*ND; c++) {
/* Set up lin reg model and interpolate phase. Works better than average for channels with
quickly changing phase, like HF. */
for(p=0; p<NPILOTSFRAME+2; p++) {
x[p] = sampling_points[p];
pc = c % COHPSK_NC;
y[p] = fcmult(coh->pilot2[p][pc], ct_symb_buf[sampling_points[p]][c]);
}
linreg(&m, &b, x, y, NPILOTSFRAME+2);
for(r=0; r<NSYMROW; r++) {
x1 = (float)(r+NPILOTSFRAME);
yfit = cadd(fcmult(x1,m),b);
coh->phi_[r][c] = atan2(yfit.imag, yfit.real);
}
/* amplitude estimation */
mag = 0.0;
for(p=0; p<NPILOTSFRAME+2; p++) {
mag += cabsolute(ct_symb_buf[sampling_points[p]][c]);
}
amp_ = mag/(NPILOTSFRAME+2);
for(r=0; r<NSYMROW; r++) {
coh->amp_[r][c] = amp_;
}
}
/* now correct phase of data symbols */
for(c=0; c<COHPSK_NC*ND; c++) {
for (r=0; r<NSYMROW; r++) {
phi_rect.real = cosf(coh->phi_[r][c]); phi_rect.imag = -sinf(coh->phi_[r][c]);
coh->rx_symb[r][c] = cmult(ct_symb_buf[NPILOTSFRAME + r][c], phi_rect);
i = c*NSYMROW + r;
rx_symb_linear[i] = coh->rx_symb[r][c];
}
}
/* and finally optional diversity combination, note output is soft decn a "1" is < 0 */
for(c=0; c<COHPSK_NC; c++) {
for(r=0; r<NSYMROW; r++) {
div_symb = coh->rx_symb[r][c];
for (d=1; d<ND; d++) {
div_symb = cadd(div_symb, coh->rx_symb[r][c + COHPSK_NC*d]);
}
rot = cmult(div_symb, pi_on_4);
i = c*NSYMROW + r;
rx_bits[2*i+1] = rot.real;
rx_bits[2*i] = rot.imag;
/* demodulate bits from upper and lower carriers separately for test purposes */
assert(ND == 2);
i = c*NSYMROW + r;
rot = cmult(coh->rx_symb[r][c], pi_on_4);
coh->rx_bits_lower[2*i+1] = rot.real;
coh->rx_bits_lower[2*i] = rot.imag;
rot = cmult(coh->rx_symb[r][c + COHPSK_NC], pi_on_4);
coh->rx_bits_upper[2*i+1] = rot.real;
coh->rx_bits_upper[2*i] = rot.imag;
}
}
/* estimate RMS signal and noise */
mag = 0.0;
for(i=0; i<NSYMROW*COHPSK_NC*ND; i++)
mag += cabsolute(rx_symb_linear[i]);
coh->sig_rms = mag/(NSYMROW*COHPSK_NC*ND);
sum_x = 0;
sum_xx = 0;
n = 0;
for (i=0; i<NSYMROW*COHPSK_NC*ND; i++) {
s = rx_symb_linear[i];
if (fabsf(s.real) > coh->sig_rms) {
sum_x += s.imag;
sum_xx += s.imag*s.imag;
n++;
}
}
noise_var = 0;
if (n > 1) {
noise_var = (n*sum_xx - sum_x*sum_x)/(n*(n-1));
}
coh->noise_rms = sqrtf(noise_var);
}
/*---------------------------------------------------------------------------*\
FUNCTION....: tx_filter_and_upconvert_coh()
AUTHOR......: David Rowe
DATE CREATED: May 2015
Given NC symbols construct M samples (1 symbol) of NC filtered
and upconverted symbols.
TODO: work out a way to merge with fdmdv version, e.g. run time define M/NSYM,
and run unittests on fdmdv and cohpsk modem afterwards.
\*---------------------------------------------------------------------------*/
void tx_filter_and_upconvert_coh(COMP tx_fdm[], int Nc,const COMP tx_symbols[],
COMP tx_filter_memory[COHPSK_NC*ND][COHPSK_NSYM],
COMP phase_tx[], COMP freq[],
COMP *fbb_phase, COMP fbb_rect)
{
int c;
int i,j,k;
COMP gain;
COMP tx_baseband;
COMP two = {2.0, 0.0};
float mag;
gain.real = sqrtf(2.0)/2.0;
gain.imag = 0.0;
for(i=0; i<COHPSK_M; i++) {
tx_fdm[i].real = 0.0;
tx_fdm[i].imag = 0.0;
}
for(c=0; c<Nc; c++)
tx_filter_memory[c][COHPSK_NSYM-1] = cmult(tx_symbols[c], gain);
/*
tx filter each symbol, generate M filtered output samples for
each symbol, which we then freq shift and sum with other
carriers. Efficient polyphase filter techniques used as
tx_filter_memory is sparse
*/
for(c=0; c<Nc; c++) {
for(i=0; i<COHPSK_M; i++) {
const COMP * tx_filter_memory_cn = (COMP*) &tx_filter_memory[c];
/* filter sample of symbol for carrier c */
tx_baseband.real = 0;
tx_baseband.imag = 0;
for(j=0,k=COHPSK_M-i-1; j<COHPSK_NSYM; j++,k+=COHPSK_M){
tx_baseband = cadd(tx_baseband,fcmult(COHPSK_M,fcmult(gt_alpha5_root_coh[k],tx_filter_memory_cn[j])));
}
/* freq shift and sum */
phase_tx[c] = cmult(phase_tx[c], freq[c]);
tx_fdm[i] = cadd(tx_fdm[i], cmult(tx_baseband, phase_tx[c]));
}
//exit(0);
}
/* shift whole thing up to carrier freq */
for (i=0; i<COHPSK_M; i++) {
*fbb_phase = cmult(*fbb_phase, fbb_rect);
tx_fdm[i] = cmult(tx_fdm[i], *fbb_phase);
}
/*
Scale such that total Carrier power C of real(tx_fdm) = Nc. This
excludes the power of the pilot tone.
We return the complex (single sided) signal to make frequency
shifting for the purpose of testing easier
*/
for (i=0; i<COHPSK_M; i++)
tx_fdm[i] = cmult(two, tx_fdm[i]);
/* normalise digital oscillators as the magnitude can drift over time */
for (c=0; c<Nc; c++) {
mag = cabsolute(phase_tx[c]);
phase_tx[c].real /= mag;
phase_tx[c].imag /= mag;
}
mag = cabsolute(*fbb_phase);
fbb_phase->real /= mag;
fbb_phase->imag /= mag;
/* shift memory, inserting zeros at end */
for(i=0; i<COHPSK_NSYM-1; i++) {
for(c=0; c<Nc; c++) {
tx_filter_memory[c][i] = tx_filter_memory[c][i+1];
}
}
for(c=0; c<Nc; c++) {
tx_filter_memory[c][COHPSK_NSYM-1].real = 0.0;
tx_filter_memory[c][COHPSK_NSYM-1].imag = 0.0;
}
}
void corr_with_pilots_comp(float *corr_out, float *mag_out, struct COHPSK *coh, int t, COMP dp_f_fine)
{
COMP acorr, f_fine_rect, f_corr;
float mag, corr;
int c, p, pc;
//1,2,7,8
f_fine_rect.real = 1;
f_fine_rect.imag = 0;
COMP f_fine_rects[4];
//dp_f_fine = comp_exp_j(2*m_pi*f_fine/cohpsk_rs);
f_fine_rect = cmult(f_fine_rect,dp_f_fine); //sampling_points[0]+1 = 1
f_fine_rects[0] = dp_f_fine;
f_fine_rect = cmult(dp_f_fine,dp_f_fine); //sampling_points[1]+1 = 2
f_fine_rects[1] = f_fine_rect;
f_fine_rect = cmult(f_fine_rect,dp_f_fine); // = 2
f_fine_rect = cmult(f_fine_rect,dp_f_fine); // = 3
f_fine_rect = cmult(f_fine_rect,dp_f_fine); // = 4
f_fine_rect = cmult(f_fine_rect,dp_f_fine); // = 5
f_fine_rect = cmult(f_fine_rect,dp_f_fine); // = 6
f_fine_rect = cmult(f_fine_rect,dp_f_fine); //sampling_points[2]+1 = 7
f_fine_rects[2] = f_fine_rect;
f_fine_rect = cmult(f_fine_rect,dp_f_fine); //sampling_points[2]+1 = 8
f_fine_rects[3] = f_fine_rect;
corr = 0.0; mag = 0.0;
for (c=0; c<COHPSK_NC*ND; c++) {
acorr.real = 0.0; acorr.imag = 0.0;
for (p=0; p<NPILOTSFRAME+2; p++) {
//f_fine_rect.real = cosf(f_fine*2.0*M_PI*(sampling_points[p]+1.0)/COHPSK_RS);
//f_fine_rect.imag = sinf(f_fine*2.0*M_PI*(sampling_points[p]+1.0)/COHPSK_RS);
f_fine_rect = f_fine_rects[p];
f_corr = cmult(f_fine_rect, coh->ct_symb_buf[t+sampling_points[p]][c]);
pc = c % COHPSK_NC;
acorr = cadd(acorr, fcmult(coh->pilot2[p][pc], f_corr));
mag += cabsolute(f_corr);
}
corr += cabsolute(acorr);
}
*corr_out = corr;
*mag_out = mag;
}
/*---------------------------------------------------------------------------*\
FUNCTION....: frame_sync_fine_freq_est()
AUTHOR......: David Rowe
DATE CREATED: April 2015
Returns an estimate of frame sync (coarse timing) offset and fine
frequency offset, advances to next sync state if we have a reliable
match for frame sync.
\*---------------------------------------------------------------------------*/
void frame_sync_fine_freq_est(struct COHPSK *coh, COMP ch_symb[][COHPSK_NC*ND], int sync, int *next_sync)
{
int t;
float f_fine, mag, max_corr, max_mag, corr, delta_f_fine, f_fine_range ;
COMP f_fine_d_ph;
if(coh->freq_est_mode_reduced){
delta_f_fine = 1.3;
f_fine_range = 10;
}else{
delta_f_fine = .25;
f_fine_range = 20;
}
/* Represent f_fine scan as delta2-phase */
const COMP f_fine_d2_ph = comp_exp_j(2*M_PI*delta_f_fine/COHPSK_RS);
f_fine = -f_fine_range;
update_ct_symb_buf(coh->ct_symb_buf, ch_symb);
/* sample pilots at start of this frame and start of next frame */
if (sync == 0) {
/* Represent f_fine as complex delta-phase instead of frequency */
f_fine_d_ph = comp_exp_j(2*M_PI*f_fine/COHPSK_RS);
/* sample correlation over 2D grid of time and fine freq points */
max_corr = max_mag = 0;
for (f_fine=-f_fine_range; f_fine<=f_fine_range; f_fine+=delta_f_fine) {
for (t=0; t<NSYMROWPILOT; t++) {
corr_with_pilots_comp(&corr,&mag,coh,t,f_fine_d_ph);
if (corr >= max_corr) {
max_corr = corr;
max_mag = mag;
coh->ct = t;
coh->f_fine_est = f_fine;
}
}
/* Advance f_fine */
f_fine_d_ph = cmult(f_fine_d_ph,f_fine_d2_ph);
}
coh->ff_rect.real = cosf(coh->f_fine_est*2.0*M_PI/COHPSK_RS);
coh->ff_rect.imag = -sinf(coh->f_fine_est*2.0*M_PI/COHPSK_RS);
if (coh->verbose)
fprintf(stderr, " [%d] fine freq f: %6.2f max_ratio: %f ct: %d\n", coh->frame, (double)coh->f_fine_est, (double)(max_corr/max_mag), coh->ct);
if (max_corr/max_mag > 0.9) {
if (coh->verbose)
fprintf(stderr, " [%d] encouraging sync word!\n", coh->frame);
coh->sync_timer = 0;
*next_sync = 1;
}
else {
*next_sync = 0;
}
coh->ratio = max_corr/max_mag;
}
}
void update_ct_symb_buf(COMP ct_symb_buf[][COHPSK_NC*ND], COMP ch_symb[][COHPSK_NC*ND])
{
int r, c, i;
/* update memory in symbol buffer */
for(r=0; r<NCT_SYMB_BUF-NSYMROWPILOT; r++) {
for(c=0; c<COHPSK_NC*ND; c++)
ct_symb_buf[r][c] = ct_symb_buf[r+NSYMROWPILOT][c];
}
for(r=NCT_SYMB_BUF-NSYMROWPILOT, i=0; r<NCT_SYMB_BUF; r++, i++) {
for(c=0; c<COHPSK_NC*ND; c++)
ct_symb_buf[r][c] = ch_symb[i][c];
}
}
int sync_state_machine(struct COHPSK *coh, int sync, int next_sync)
{
float corr, mag;
if (sync == 1) {
/* check that sync is still good, fall out of sync on consecutive bad frames */
corr_with_pilots_comp(&corr, &mag, coh, coh->ct, comp_exp_j(2*M_PI*coh->f_fine_est/COHPSK_RS));
coh->ratio = fabsf(corr)/mag;
// printf("%f\n", cabsolute(corr)/mag);
if (fabsf(corr)/mag < 0.8)
coh->sync_timer++;
else
coh->sync_timer = 0;
if (coh->sync_timer == 10) {
if (coh->verbose)
fprintf(stderr," [%d] lost sync ....\n", coh->frame);
next_sync = 0;
}
}
sync = next_sync;
return sync;
}
/*---------------------------------------------------------------------------*\
FUNCTION....: cohpsk_mod()
AUTHOR......: David Rowe
DATE CREATED: 5/4/2015
COHPSK modulator, take a frame of COHPSK_BITS_PER_FRAME or
2*COHPSK_BITS_PER_FRAME bits and generates a frame of
COHPSK_NOM_SAMPLES_PER_FRAME modulated symbols.
if nbits == COHPSK_BITS_PER_FRAME, diveristy mode is used, if nbits
== 2*COHPSK_BITS_PER_FRAME diversity mode is not used.
The output signal is complex to support single sided frequency
shifting, for example when testing frequency offsets in channel
simulation.
\*---------------------------------------------------------------------------*/
void cohpsk_mod(struct COHPSK *coh, COMP tx_fdm[], int tx_bits[], int nbits)
{
struct FDMDV *fdmdv = coh->fdmdv;
COMP tx_symb[NSYMROWPILOT][COHPSK_NC*ND];
COMP tx_onesym[COHPSK_NC*ND];
int r,c;
bits_to_qpsk_symbols(tx_symb, tx_bits, nbits);
for(r=0; r<NSYMROWPILOT; r++) {
for(c=0; c<COHPSK_NC*ND; c++)
tx_onesym[c] = fcmult(coh->carrier_ampl[c], tx_symb[r][c]);
tx_filter_and_upconvert_coh(&tx_fdm[r*COHPSK_M], COHPSK_NC*ND , tx_onesym, fdmdv->tx_filter_memory,
fdmdv->phase_tx, fdmdv->freq, &fdmdv->fbb_phase_tx, fdmdv->fbb_rect);
}
}
/*---------------------------------------------------------------------------*\
FUNCTION....: cohpsk_clip()
AUTHOR......: David Rowe
DATE CREATED: May 2015
Hard clips a cohpsk modulator signal to improve PAPR, CLIP threshold
hard coded and will need to be changed if NC*ND does.
\*---------------------------------------------------------------------------*/
void cohpsk_clip(COMP tx_fdm[], float clip_thresh, int n)
{
COMP sam;
float mag;
int i;
for(i=0; i<n; i++) {
sam = tx_fdm[i];
mag = cabsolute(sam);
if (mag > clip_thresh) {
sam = fcmult(clip_thresh/mag, sam);
}
tx_fdm[i] = sam;
}
}
/*---------------------------------------------------------------------------*\
FUNCTION....: fdm_downconvert_coh
AUTHOR......: David Rowe
DATE CREATED: May 2015
Frequency shift each modem carrier down to NC baseband signals.
TODO: try to combine with fdmdv version, carefully re-test fdmdv modem.
\*---------------------------------------------------------------------------*/
void fdm_downconvert_coh(COMP rx_baseband[COHPSK_NC][COHPSK_M+COHPSK_M/P], int Nc, COMP rx_fdm[], COMP phase_rx[], COMP freq[], int nin)
{
int i,c;
float mag;
/* maximum number of input samples to demod */
assert(nin <= (COHPSK_M+COHPSK_M/P));
/* downconvert */
for (c=0; c<Nc; c++)
for (i=0; i<nin; i++) {
phase_rx[c] = cmult(phase_rx[c], freq[c]);
rx_baseband[c][i] = cmult(rx_fdm[i], cconj(phase_rx[c]));
}
/* normalise digital oscilators as the magnitude can drift over time */
for (c=0; c<Nc; c++) {
mag = cabsolute(phase_rx[c]);
phase_rx[c].real /= mag;
phase_rx[c].imag /= mag;
}
}
/*---------------------------------------------------------------------------*\
FUNCTION....: rx_filter_coh()
AUTHOR......: David Rowe
DATE CREATED: May 2015
cohpsk version of fdmdv.c rx_filter function.
TODO: see if we can merge the two! Will require re-testing of fdmdv modem.
\*---------------------------------------------------------------------------*/
void rx_filter_coh(COMP rx_filt[COHPSK_NC+1][P+1], int Nc, COMP rx_baseband[COHPSK_NC+1][COHPSK_M+COHPSK_M/P], COMP rx_filter_memory[COHPSK_NC+1][+COHPSK_NFILTER], int nin)
{
int c, i,j,k;
int n=COHPSK_M/P;
COMP acc;
/* rx filter each symbol, generate P filtered output samples for
each symbol. Note we keep filter memory at rate M, it's just
the filter output at rate P */
for(i=0, j=0; i<nin; i+=n,j++) {
/* latest input sample */
for(c=0; c<Nc; c++){
k=COHPSK_NFILTER-n;
memcpy(&rx_filter_memory[c][k],&rx_baseband[c][i],n*sizeof(COMP));
}
/* convolution (filtering) */
for(c=0; c<Nc; c++) {
/* Cast into const so the compiler doesn't expect aliasing */
const COMP * rx_filt_lc = &rx_filter_memory[c][0];
acc.real = 0.0f;
acc.imag = 0.0f;
for(k=0; k<COHPSK_NFILTER; k++){
acc = cadd(acc, fcmult(gt_alpha5_root_coh[k], rx_filt_lc[k]));
}
rx_filt[c][j] = acc;
}
/* make room for next input sample */
for(c=0; c<Nc; c++){
memcpy(&rx_filter_memory[c][0],&rx_filter_memory[c][n],(COHPSK_NFILTER-n)*sizeof(COMP));
}
}
assert(j <= (P+1)); /* check for any over runs */
}
/*---------------------------------------------------------------------------*\
FUNCTION....: fdmdv_freq_shift_coh()
AUTHOR......: David Rowe
DATE CREATED: May 2015
Frequency shift modem signal. The use of complex input and output allows
single sided frequency shifting (no images).
\*---------------------------------------------------------------------------*/
void fdmdv_freq_shift_coh(COMP rx_fdm_fcorr[], COMP rx_fdm[], float foff, float Fs,
COMP *foff_phase_rect, int nin)
{
COMP foff_rect;
float mag;
int i;
foff_rect.real = cosf(2.0*PI*foff/Fs);
foff_rect.imag = sinf(2.0*PI*foff/Fs);
for(i=0; i<nin; i++) {
*foff_phase_rect = cmult(*foff_phase_rect, foff_rect);
rx_fdm_fcorr[i] = cmult(rx_fdm[i], *foff_phase_rect);
}
/* normalise digital oscilator as the magnitude can drfift over time */
mag = cabsolute(*foff_phase_rect);
foff_phase_rect->real /= mag;
foff_phase_rect->imag /= mag;
}
void rate_Fs_rx_processing(struct COHPSK *coh, COMP ch_symb[][COHPSK_NC*ND], COMP ch_fdm_frame[], float *f_est, int nsymb, int nin, int freq_track)
{
struct FDMDV *fdmdv = coh->fdmdv;
int r, c, i, ch_fdm_frame_index;
COMP rx_fdm_frame_bb[COHPSK_M+COHPSK_M/P];
COMP rx_baseband[COHPSK_NC*ND][COHPSK_M+COHPSK_M/P];
COMP rx_filt[COHPSK_NC*ND][P+1];
float env[NT*P], rx_timing;
COMP rx_onesym[COHPSK_NC*ND];
float beta, g;
COMP adiff, amod_strip, mod_strip;
ch_fdm_frame_index = 0;
rx_timing = 0;
for (r=0; r<nsymb; r++) {
fdmdv_freq_shift_coh(rx_fdm_frame_bb, &ch_fdm_frame[ch_fdm_frame_index], -(*f_est), COHPSK_FS, &fdmdv->fbb_phase_rx, nin);
ch_fdm_frame_index += nin;
fdm_downconvert_coh(rx_baseband, COHPSK_NC*ND, rx_fdm_frame_bb, fdmdv->phase_rx, fdmdv->freq, nin);
rx_filter_coh(rx_filt, COHPSK_NC*ND, rx_baseband, coh->rx_filter_memory, nin);
rx_timing = rx_est_timing(rx_onesym, fdmdv->Nc, rx_filt, fdmdv->rx_filter_mem_timing, env, nin, COHPSK_M);
for(c=0; c<COHPSK_NC*ND; c++) {
ch_symb[r][c] = rx_onesym[c];
}
/* freq tracking, see test_ftrack.m for unit test. Placed in
this function as it needs to work on a symbol by symbol
abasis rather than frame by frame. This means the control
loop operates at a sample rate of Rs = 50Hz for say 1 Hz/s
drift. */
if (freq_track) {
beta = 0.005;
g = 0.2;
/* combine difference on phase from last symbol over Nc carriers */
mod_strip.real = 0.0; mod_strip.imag = 0.0;
for(c=0; c<fdmdv->Nc+1; c++) {
//printf("rx_onesym[%d] %f %f prev_rx_symbols[%d] %f %f\n", c, rx_onesym[c].real, rx_onesym[c].imag,
// fdmdv->prev_rx_symbols[c].real, fdmdv->prev_rx_symbols[c].imag);
adiff = cmult(rx_onesym[c], cconj(fdmdv->prev_rx_symbols[c]));
fdmdv->prev_rx_symbols[c] = rx_onesym[c];
/* 4th power strips QPSK modulation, by multiplying phase by 4
Using the abs value of the real coord was found to help
non-linear issues when noise power was large. */
amod_strip = cmult(adiff, adiff);
amod_strip = cmult(amod_strip, amod_strip);
amod_strip.real = fabsf(amod_strip.real);
mod_strip = cadd(mod_strip, amod_strip);
}
//printf("modstrip: %f %f\n", mod_strip.real, mod_strip.imag);
/* loop filter made up of 1st order IIR plus integrator. Integerator
was found to be reqd */
fdmdv->foff_filt = (1.0-beta)*fdmdv->foff_filt + beta*atan2(mod_strip.imag, mod_strip.real);
//printf("foff_filt: %f angle: %f\n", fdmdv->foff_filt, atan2(mod_strip.imag, mod_strip.real));
*f_est += g*fdmdv->foff_filt;
}
/* Optional logging used for testing against Octave version */
if (coh->rx_baseband_log) {
assert(nin <= (COHPSK_M+COHPSK_M/P));
for(c=0; c<COHPSK_NC*ND; c++) {
for(i=0; i<nin; i++) {
coh->rx_baseband_log[c*coh->rx_baseband_log_col_sz + coh->rx_baseband_log_col_index + i] = rx_baseband[c][i];
}
}
coh->rx_baseband_log_col_index += nin;
assert(coh->rx_baseband_log_col_index <= coh->rx_baseband_log_col_sz);
}
if (coh->rx_filt_log) {
for(c=0; c<COHPSK_NC*ND; c++) {
for(i=0; i<nin/(COHPSK_M/P); i++) {
coh->rx_filt_log[c*coh->rx_filt_log_col_sz + coh->rx_filt_log_col_index + i] = rx_filt[c][i];
}
}
coh->rx_filt_log_col_index += nin/(COHPSK_M/P);
}
if (coh->ch_symb_log) {
for(c=0; c<COHPSK_NC*ND; c++) {
coh->ch_symb_log[coh->ch_symb_log_r*COHPSK_NC*ND + c] = ch_symb[r][c];
}
coh->ch_symb_log_r++;
}
if (coh->rx_timing_log) {
coh->rx_timing_log[coh->rx_timing_log_index] = rx_timing;
coh->rx_timing_log_index++;
//printf("rx_timing_log_index: %d\n", coh->rx_timing_log_index);
}
/* we only allow a timing shift on one symbol per frame */
if (nin != COHPSK_M)
nin = COHPSK_M;
}
coh->rx_timing = rx_timing;
}
/*---------------------------------------------------------------------------*\
FUNCTION....: cohpsk_set_freq_est_mode()
AUTHOR......: Brady O'Brien
DATE CREATED: 12 Dec 2017
Enables or disables a 'simple' frequency estimation mode. Simple frequency
estimation uses substantially less CPU when cohpsk modem is not sunk than
default mode, but may take many frames to sync.
\*---------------------------------------------------------------------------*/
void cohpsk_set_freq_est_mode(struct COHPSK *coh, int use_simple_mode){
if(use_simple_mode){
coh->freq_est_mode_reduced = 1;
}else{
coh->freq_est_mode_reduced = 0;
}
}
/*---------------------------------------------------------------------------*\
FUNCTION....: cohpsk_demod()
AUTHOR......: David Rowe
DATE CREATED: 5/4/2015
COHPSK demodulator, takes an array of (nominally) nin_frame =
COHPSK_NOM_SAMPLES_PER_FRAME modulated samples, returns an array of
COHPSK_BITS_PER_FRAME bits.
The input signal is complex to support single sided frequency shifting
before the demod input (e.g. click to tune feature).
\*---------------------------------------------------------------------------*/
void cohpsk_demod(struct COHPSK *coh, float rx_bits[], int *sync_good, COMP rx_fdm[], int *nin_frame)
{
COMP ch_symb[NSW*NSYMROWPILOT][COHPSK_NC*ND];
int i, j, sync, anext_sync, next_sync, nin, r, c, ns_done;
float max_ratio, f_est;
assert(*nin_frame <= COHPSK_MAX_SAMPLES_PER_FRAME);
next_sync = sync = coh->sync;
for (i=0; i<NSW*NSYMROWPILOT*COHPSK_M-*nin_frame; i++)
coh->ch_fdm_frame_buf[i] = coh->ch_fdm_frame_buf[i+*nin_frame];
//printf("nin_frame: %d i: %d i+nin_frame: %d\n", *nin_frame, i, i+*nin_frame);
for (j=0; i<NSW*NSYMROWPILOT*COHPSK_M; i++,j++)
coh->ch_fdm_frame_buf[i] = rx_fdm[j];
//printf("i: %d j: %d rx_fdm[0]: %f %f\n", i,j, rx_fdm[0].real, rx_fdm[0].imag);
/* if out of sync do Initial Freq offset estimation using NSW frames to flush out filter memories */
if (sync == 0) {
max_ratio = 0.0;
f_est = 0.0;
coh->f_est -= 20;
if(coh->f_est < FDMDV_FCENTRE - 60.0){
coh->f_est = FDMDV_FCENTRE + 60;
}
if(!coh->freq_est_mode_reduced){
coh->f_est = FDMDV_FCENTRE-40.0;
}
ns_done = 0;
//for (coh->f_est = FDMDV_FCENTRE-40.0; coh->f_est <= FDMDV_FCENTRE+40.0; coh->f_est += 40.0)
while(!ns_done){
/* Use slower freq estimator; only do one chunk of freq range */
if(coh->freq_est_mode_reduced){
coh->f_est -= 20;
if(coh->f_est < FDMDV_FCENTRE - 60.0){
coh->f_est = FDMDV_FCENTRE + 60;
}
ns_done = 1;
}else{
/* we can test +/- 20Hz, so we break this up into 3 tests to cover +/- 60Hz */
if(coh->f_est > FDMDV_FCENTRE+40.0) ns_done = 1;
}
if (coh->verbose)
fprintf(stderr, " [%d] acohpsk.f_est: %f +/- 20\n", coh->frame, (double)coh->f_est);
/* we are out of sync so reset f_est and process two frames to clean out memories */
rate_Fs_rx_processing(coh, ch_symb, coh->ch_fdm_frame_buf, &coh->f_est, NSW*NSYMROWPILOT, COHPSK_M, 0);
for (i=0; i<NSW-1; i++) {
update_ct_symb_buf(coh->ct_symb_buf, &ch_symb[i*NSYMROWPILOT]);
}
frame_sync_fine_freq_est(coh, &ch_symb[(NSW-1)*NSYMROWPILOT], sync, &anext_sync);
if (anext_sync == 1) {
//printf(" [%d] acohpsk.ratio: %f\n", f, coh->ratio);
if (coh->ratio > max_ratio) {
max_ratio = coh->ratio;
f_est = coh->f_est - coh->f_fine_est;
next_sync = anext_sync;
}
}
if(!coh->freq_est_mode_reduced){
coh->f_est += 40;
}
}
if (next_sync == 1) {
/* we've found a sync candidate!
re-process last NSW frames with adjusted f_est then check again */
coh->f_est = f_est;
if (coh->verbose)
fprintf(stderr, " [%d] trying sync and f_est: %f\n", coh->frame, (double)coh->f_est);
rate_Fs_rx_processing(coh, ch_symb, coh->ch_fdm_frame_buf, &coh->f_est, NSW*NSYMROWPILOT, COHPSK_M, 0);
for (i=0; i<NSW-1; i++) {
update_ct_symb_buf(coh->ct_symb_buf, &ch_symb[i*NSYMROWPILOT]);
}
/*
for(i=0; i<NSW*NSYMROWPILOT; i++) {
printf("%f %f\n", ch_symb[i][0].real, ch_symb[i][0].imag);
}
*/
/*
for(i=0; i<NCT_SYMB_BUF; i++) {
printf("%f %f\n", coh->ct_symb_buf[i][0].real, coh->ct_symb_buf[i][0].imag);
}
*/
frame_sync_fine_freq_est(coh, &ch_symb[(NSW-1)*NSYMROWPILOT], sync, &next_sync);
if (fabs(coh->f_fine_est) > 2.0) {
if (coh->verbose)
fprintf(stderr, " [%d] Hmm %f is a bit big :(\n", coh->frame, (double)coh->f_fine_est);
next_sync = 0;
}
}
if (next_sync == 1) {
/* OK we are in sync!
demodulate first frame (demod completed below) */
if (coh->verbose)
fprintf(stderr, " [%d] in sync! f_est: %f ratio: %f \n", coh->frame, (double)coh->f_est, (double)coh->ratio);
for(r=0; r<NSYMROWPILOT+2; r++)
for(c=0; c<COHPSK_NC*ND; c++)
coh->ct_symb_ff_buf[r][c] = coh->ct_symb_buf[coh->ct+r][c];
}
}
/* If in sync just do sample rate processing on latest frame */
if (sync == 1) {
rate_Fs_rx_processing(coh, ch_symb, rx_fdm, &coh->f_est, NSYMROWPILOT, coh->nin, 1);
frame_sync_fine_freq_est(coh, ch_symb, sync, &next_sync);
for(r=0; r<2; r++)
for(c=0; c<COHPSK_NC*ND; c++)
coh->ct_symb_ff_buf[r][c] = coh->ct_symb_ff_buf[r+NSYMROWPILOT][c];
for(; r<NSYMROWPILOT+2; r++)
for(c=0; c<COHPSK_NC*ND; c++)
coh->ct_symb_ff_buf[r][c] = coh->ct_symb_buf[coh->ct+r][c];
}
/* if we are in sync complete demodulation with symbol rate processing */
*sync_good = 0;
if ((next_sync == 1) || (sync == 1)) {
qpsk_symbols_to_bits(coh, rx_bits, coh->ct_symb_ff_buf);
*sync_good = 1;
}
sync = sync_state_machine(coh, sync, next_sync);
coh->sync = sync;
/* work out how many samples we need for the next call to account
for differences in tx and rx sample clocks */
nin = COHPSK_M;
if (sync == 1) {
if (coh->rx_timing > COHPSK_M/P)
nin = COHPSK_M + COHPSK_M/P;
if (coh->rx_timing < -COHPSK_M/P)
nin = COHPSK_M - COHPSK_M/P;
}
coh->nin = nin;
*nin_frame = (NSYMROWPILOT-1)*COHPSK_M + nin;
//if (coh->verbose)
// fprintf(stderr, "%f %d %d\n", coh->rx_timing, nin, *nin_frame);
}
/*---------------------------------------------------------------------------*\
FUNCTION....: cohpsk_fs_offset()
AUTHOR......: David Rowe
DATE CREATED: May 2015
Simulates small Fs offset between mod and demod.
\*---------------------------------------------------------------------------*/
int cohpsk_fs_offset(COMP out[], COMP in[], int n, float sample_rate_ppm)
{
double tin, f;
int tout, t1, t2;
tin = 0.0; tout = 0;
while (tin < n) {
t1 = floor(tin);
t2 = ceil(tin);
f = tin - t1;
out[tout].real = (1.0-f)*in[t1].real + f*in[t2].real;
out[tout].imag = (1.0-f)*in[t1].imag + f*in[t2].imag;
tout += 1;
tin += 1.0 + sample_rate_ppm/1E6;
//printf("tin: %f tout: %d f: %f\n", tin, tout, f);
}
return tout;
}
/*---------------------------------------------------------------------------*\
FUNCTION....: cohpsk_get_demod_stats()
AUTHOR......: David Rowe
DATE CREATED: 14 June 2015
Fills stats structure with a bunch of demod information.
\*---------------------------------------------------------------------------*/
void cohpsk_get_demod_stats(struct COHPSK *coh, struct MODEM_STATS *stats)
{
int c,r;
COMP pi_4;
float new_snr_est;
pi_4.real = cosf(M_PI/4.0);
pi_4.imag = sinf(M_PI/4.0);
stats->Nc = COHPSK_NC*ND;
assert(stats->Nc <= MODEM_STATS_NC_MAX);
new_snr_est = 20*log10((coh->sig_rms+1E-6)/(coh->noise_rms+1E-6)) - 10*log10(3000.0/700.0);
stats->snr_est = 0.9*stats->snr_est + 0.1*new_snr_est;
//fprintf(stderr, "sig_rms: %f noise_rms: %f snr_est: %f\n", coh->sig_rms, coh->noise_rms, stats->snr_est);
stats->sync = coh->sync;
stats->foff = coh->f_est - FDMDV_FCENTRE;
stats->rx_timing = coh->rx_timing;
stats->clock_offset = 0.0; /* TODO - implement clock offset estimation */
assert(NSYMROW <= MODEM_STATS_NR_MAX);
stats->nr = NSYMROW;
for(c=0; c<COHPSK_NC*ND; c++) {
for (r=0; r<NSYMROW; r++) {
stats->rx_symbols[r][c] = cmult(coh->rx_symb[r][c], pi_4);
}
}
}
void cohpsk_set_verbose(struct COHPSK *coh, int verbose)
{
assert(coh != NULL);
coh->verbose = verbose;
}
void cohpsk_set_frame(struct COHPSK *coh, int frame)
{
assert(coh != NULL);
coh->frame = frame;
}
/*---------------------------------------------------------------------------*\
FUNCTION....: cohpsk_get_test_bits()
AUTHOR......: David Rowe
DATE CREATED: June 2015
Returns a frame of known test bits.
\*---------------------------------------------------------------------------*/
void cohpsk_get_test_bits(struct COHPSK *coh, int rx_bits[])
{
memcpy(rx_bits, coh->ptest_bits_coh_tx, sizeof(int)*COHPSK_BITS_PER_FRAME);
coh->ptest_bits_coh_tx += COHPSK_BITS_PER_FRAME;
if (coh->ptest_bits_coh_tx >=coh->ptest_bits_coh_end) {
coh->ptest_bits_coh_tx = (int*)test_bits_coh;
}
}
/*---------------------------------------------------------------------------*\
FUNCTION....: cohpsk_put_test_bits()
AUTHOR......: David Rowe
DATE CREATED: June 2015
Accepts bits from demod and attempts to sync with the known
test_bits sequence. When synced measures bit errors.
Has states to track two separate received test sequences based on
channel 0 or 1.
\*---------------------------------------------------------------------------*/
void cohpsk_put_test_bits(struct COHPSK *coh, int *state, short error_pattern[],
int *bit_errors, char rx_bits_char[], int channel)
{
int i, next_state, anerror;
int rx_bits[COHPSK_BITS_PER_FRAME];
assert((channel == 0) || (channel == 1));
int *ptest_bits_coh_rx = coh->ptest_bits_coh_rx[channel];
for(i=0; i<COHPSK_BITS_PER_FRAME; i++) {
rx_bits[i] = rx_bits_char[i];
}
*bit_errors = 0;
for(i=0; i<COHPSK_BITS_PER_FRAME; i++) {
anerror = (rx_bits[i] & 0x1) ^ ptest_bits_coh_rx[i];
if ((anerror < 0) || (anerror > 1)) {
fprintf(stderr, "i: %d rx_bits: %d ptest_bits_coh_rx: %d\n", i, rx_bits[i], ptest_bits_coh_rx[i]);
}
*bit_errors += anerror;
error_pattern[i] = anerror;
}
/* state logic */
next_state = *state;
if (*state == 0) {
if (*bit_errors < 4) {
next_state = 1;
ptest_bits_coh_rx += COHPSK_BITS_PER_FRAME;
if (ptest_bits_coh_rx >= coh->ptest_bits_coh_end) {
ptest_bits_coh_rx = (int*)test_bits_coh;
}
}
}
/* if 5 frames with large BER reset test frame sync */
if (*state > 0) {
if (*bit_errors > 8) {
if (*state == 6)
next_state = 0;
else
next_state = *state+1;
}
else
next_state = 1;
}
if (*state > 0) {
ptest_bits_coh_rx += COHPSK_BITS_PER_FRAME;
if (ptest_bits_coh_rx >= coh->ptest_bits_coh_end) {
ptest_bits_coh_rx = (int*)test_bits_coh;
}
}
//fprintf(stderr, "state: %d next_state: %d bit_errors: %d\n", *state, next_state, *bit_errors);
*state = next_state;
coh->ptest_bits_coh_rx[channel] = ptest_bits_coh_rx;
}
int cohpsk_error_pattern_size(void) {
return COHPSK_BITS_PER_FRAME;
}
float *cohpsk_get_rx_bits_lower(struct COHPSK *coh) {
return coh->rx_bits_lower;
}
float *cohpsk_get_rx_bits_upper(struct COHPSK *coh) {
return coh->rx_bits_upper;
}
void cohpsk_set_carrier_ampl(struct COHPSK *coh, int c, float ampl) {
assert(c < COHPSK_NC*ND);
coh->carrier_ampl[c] = ampl;
fprintf(stderr, "cohpsk_set_carrier_ampl: %d %f\n", c, (double)ampl);
}
} // FreeDV