% tcohpsk.m
% David Rowe Oct 2014
%
% Octave coherent PSK modem script that has two modes:
%
% i) tests the C port of the coherent PSK modem.  This script loads
%    the output of unittest/tcohpsk.c and compares it to the output of
%    the reference versions of the same modem written in Octave.
%

% (ii) Runs the Octave version of the cohpsk modem to tune and develop
%      it, including extensive channel simulations such as AWGN noise,
%      fading/HF, frequency offset, frequency drift, and tx/rx sample
%      rate differences.

%  TODO:
%
%  [X] Test
%      [X] AWGN channel
%      [X] freq offset
%      [X] fading channel
%      [X] freq drift
%      [X] timing drift
%  [X] tune perf/impl loss to get closer to ideal
%      [X] linear interp of phase for better fading perf
%  [X] freq offset/drift feedback loop 
%  [X] PAPR measurement and reduction
%  [X] false sync
%      [X] doesn't sync up on noise (used EsNo = -12)
%      [X] similar but invalid signal like huge f off
%  [X] ability to "unsync" when signal disappears
%  [ ] some calibrated tests against FreeDV 1600
%      + compare sound quality at various Es/Nos
%  [ ] sync
%      + set some req & implement
%      [ ] way to handle eom w/o nasties
%          + like mute output when signal has gone or v low snr
%          + instantaneous snr
%  [X] ssb tx filter with 3dB passband ripple
%      + diverisity helped for AWGN BER 0.024 down to 0.016
%      + Only a small change in fading perf with filter on/off
%      + however other filters may have other effects, should test this, 
%        e.g. scatter plots, some sort of BER metric?
%  [X] EsNo estimation
%  [ ] filter reqd with compression?
%      + make sure not too much noise passed into noise floor
%  [X] different diversity combination
%      + taking largest symbol didn't help
%  [X] histogram of bit errors
%      + lot of data
%      + ssb filter
%      + compression
%      + make sure it's flat with many errors

pkg load signal;
more off;

global passes = 0;
global fails = 0;

cohpsk_dev;
fdmdv_common;
autotest;

rand('state',1); 
randn('state',1);

% select which test  ----------------------------------------------------------

test = 'compare to c';
%test = 'awgn';
%test = 'fading';

% some parameters that can be over ridden, e.g. to disable parts of modem

initial_sync = 0;  % setting this to 1 put us straight into sync w/o freq offset est
ftrack_en    = 1;  % set to 1 to enable freq tracking
ssb_tx_filt  = 0;  % set to 1 to to simulate SSB tx filter with passband ripple
Fs           = 7500;

% predefined tests ....

if strcmp(test, 'compare to c')
  frames = 30;
  foff =  58.7;
  dfoff = -0.5/Fs;
  EsNodB = 8;
  fading_en = 0;
  hf_delay_ms = 2;
  compare_with_c = 1;
  sample_rate_ppm = -1500;
  ssb_tx_filt  = 0;
end

% should be BER around 0.015 to 0.02

if strcmp(test, 'awgn')
  frames = 100;
  foff =  58.7;
  dfoff = -0.5/Fs;
  EsNodB = 8;
  fading_en = 0;
  hf_delay_ms = 2;
  compare_with_c = 0;
  sample_rate_ppm = 0;
end

% Similar to AWGN - should be BER around 0.015 to 0.02

if strcmp(test, 'fading');
  frames = 100;
  foff = -25;
  dfoff = 0.5/Fs;
  EsNodB = 12;
  fading_en = 1;
  hf_delay_ms = 2;
  compare_with_c = 0;
  sample_rate_ppm = 0;
end

EsNo = 10^(EsNodB/10);

% modem constants ----------------------------------------------------------

Rs = 75;               % symbol rate in Hz
Nc = 7;                % number of carriers
Nd = 2;                % diveristy factor
framesize = 56;        % number of payload data bits in the frame

Nsw = 4;               % frames we demod for initial sync window
afdmdv.Nsym = 6;       % size of tx/tx root nyquist filter in symbols
afdmdv.Nt = 5;         % number of symbols we estimate timing over

clip = 6.5;            % Clipping of tx signal to reduce PAPR. Adjust by 
                       % experiment as Nc and Nd change.  Check out no noise 
                       % scatter diagram and AWGN/fading BER perf
                       % at operating points

% FDMDV init ---------------------------------------------------------------

afdmdv.Fs = Fs;
afdmdv.Nc = Nd*Nc-1;
afdmdv.Rs = Rs;

if Fs/afdmdv.Rs != floor(Fs/afdmdv.Rs)
  printf("\n  Oops, Fs/Rs must be an integer!\n\n");
  return
end

M = afdmdv.M  = afdmdv.Fs/afdmdv.Rs;
afdmdv.Nfilter = afdmdv.Nsym*M;
afdmdv.tx_filter_memory = zeros(afdmdv.Nc+1, afdmdv.Nfilter);
excess_bw = 0.5;
afdmdv.gt_alpha5_root = gen_rn_coeffs(excess_bw, 1/Fs, Rs, afdmdv.Nsym, afdmdv.M);

Fcentre = afdmdv.Fcentre = 1500;
afdmdv.Fsep = afdmdv.Rs*(1+excess_bw);
afdmdv.phase_tx = ones(afdmdv.Nc+1,1);

% non linear carrier spacing, combined with clip, helps PAPR a lot!

freq_hz = afdmdv.Fsep*( -Nc*Nd/2 - 0.5 + (1:Nc*Nd).^0.98 );
afdmdv.freq_pol = 2*pi*freq_hz/Fs;
afdmdv.freq = exp(j*afdmdv.freq_pol);
afdmdv.Fcentre = 1500;

afdmdv.fbb_rect = exp(j*2*pi*Fcentre/Fs);
afdmdv.fbb_phase_tx = 1;
afdmdv.fbb_phase_rx = 1;

afdmdv.Nrxdec = 31;
afdmdv.rxdec_coeff = fir1(afdmdv.Nrxdec-1, 0.25)';
afdmdv.rxdec_lpf_mem = zeros(1,afdmdv.Nrxdec-1+afdmdv.M);

P = afdmdv.P = 4;
afdmdv.phase_rx = ones(afdmdv.Nc+1,1);
afdmdv.Nfilter = afdmdv.Nsym*afdmdv.M;
afdmdv.rx_fdm_mem = zeros(1,afdmdv.Nfilter + afdmdv.M);
Q = afdmdv.Q = afdmdv.M/4;
if Q != floor(Q)
  printf("\n  Yeah .... if (Fs/Rs)/4 = M/4 isn't an integer we will just go and break things.\n\n");
end

afdmdv.rx_filter_mem_timing = zeros(afdmdv.Nc+1, afdmdv.Nt*afdmdv.P);
afdmdv.Nfiltertiming = afdmdv.M + afdmdv.Nfilter + afdmdv.M;

afdmdv.rx_filter_memory = zeros(afdmdv.Nc+1, afdmdv.Nfilter);

afdmdv.filt = 0;
afdmdv.prev_rx_symb = ones(1,afdmdv.Nc+1);

% COHPSK Init --------------------------------------------------------

acohpsk = standard_init();
acohpsk.framesize        = framesize;
acohpsk.ldpc_code        = 0;
acohpsk.ldpc_code_rate   = 1;
acohpsk.Nc               = Nc;
acohpsk.Rs               = Rs;
acohpsk.Ns               = 4;
acohpsk.coh_en           = 1;
acohpsk.Nd               = Nd;
acohpsk.modulation       = 'qpsk';
acohpsk.do_write_pilot_file = 0;      % enable this to dump pilot symbols to C .h file, e.g. if frame params change
acohpsk = symbol_rate_init(acohpsk);
acohpsk.Ndft = 1024;
acohpsk.f_est = afdmdv.Fcentre;

ch_fdm_frame_buf = zeros(1, Nsw*acohpsk.Nsymbrowpilot*afdmdv.M);

% -----------------------------------------------------------

tx_bits_log = [];
tx_symb_log = [];
rx_amp_log = [];
rx_phi_log = [];
ch_symb_log = [];
rx_symb_log = [];
rx_bits_log = [];
tx_bits_prev_log = [];
uvnoise_log = [];
nerr_log = [];
tx_baseband_log = [];
tx_fdm_frame_log = [];
ch_fdm_frame_log = [];
rx_fdm_frame_bb_log = [];
rx_filt_log = [];
rx_fdm_filter_log = [];
rx_baseband_log = [];
rx_fdm_frame_log = [];
ct_symb_ff_log = [];
rx_timing_log = [];
ratio_log = [];
foff_log = [];
f_est_log = [];
sig_rms_log = [];
noise_rms_log = [];           
noise_rms_filt_log = [];

% Channel modeling and BER measurement ----------------------------------------

rand('state',1); 
tx_bits_coh = round(rand(1,framesize*10));
ptx_bits_coh = 1;

Nerrs = Tbits = 0;
prev_tx_bits = prev_tx_bits2 = [];
error_positions_hist = zeros(1,framesize);

phase_ch = 1;
sync = initial_sync;
acohpsk.f_est = Fcentre;
acohpsk.f_fine_est = 0;
acohpsk.ct = 4;
acohpsk.ftrack_en = ftrack_en;

if fading_en
  [spread spread_2ms hf_gain] = init_hf_model(Fs, frames*acohpsk.Nsymbrowpilot*afdmdv.M);
  hf_n = 1;
  nhfdelay = floor(hf_delay_ms*Fs/1000);
  ch_fdm_delay = zeros(1, acohpsk.Nsymbrowpilot*M + nhfdelay);
end

% simulated SSB tx filter

[b, a] = cheby1(4, 3, [600, 2600]/(Fs/2));
[y filt_states] = filter(b,a,0);
h = freqz(b,a,(600:2600)/(Fs/(2*pi)));
filt_gain = (2600-600)/sum(abs(h) .^ 2);   % ensures power after filter == before filter

noise_rms_filt = 0;

% main loop --------------------------------------------------------------------

% run mod and channel as aseparate loop so we can resample to simulate sample rate differences

for f=1:frames
  tx_bits = tx_bits_coh(ptx_bits_coh:ptx_bits_coh+framesize-1);
  ptx_bits_coh += framesize;
  if ptx_bits_coh > length(tx_bits_coh)
    ptx_bits_coh = 1;
  end

  tx_bits_log = [tx_bits_log tx_bits];

  [tx_symb tx_bits] = bits_to_qpsk_symbols(acohpsk, tx_bits, []);
  tx_symb_log = [tx_symb_log; tx_symb];
  
  tx_fdm_frame = [];
  for r=1:acohpsk.Nsymbrowpilot
    tx_onesymb = tx_symb(r,:);
    [tx_baseband afdmdv] = tx_filter(afdmdv, tx_onesymb);
    tx_baseband_log = [tx_baseband_log tx_baseband];
    [tx_fdm afdmdv] = fdm_upconvert(afdmdv, tx_baseband);
    tx_fdm_frame = [tx_fdm_frame tx_fdm];
  end

  % clipping, which along with non-linear carrier spacing, improves PAPR
  % The value of clip is a function of Nc and is adjusted experimentally
  % such that the BER hit over no clipping at Es/No=8dB is small.

  ind = find(abs(tx_fdm_frame) > clip);
  tx_fdm_frame(ind) = clip*exp(j*angle(tx_fdm_frame(ind)));

  tx_fdm_frame_log = [tx_fdm_frame_log tx_fdm_frame];

  %
  % Channel --------------------------------------------------------------------
  %

  % simulate tx SSB filter with ripple

  if ssb_tx_filt
    [tx_fdm_frame filt_states] = filter(b,a,sqrt(filt_gain)*tx_fdm_frame, filt_states);
  end

  % frequency offset and frequency drift

  ch_fdm_frame = zeros(1,acohpsk.Nsymbrowpilot*M);
  for i=1:acohpsk.Nsymbrowpilot*M
    foff_rect = exp(j*2*pi*foff/Fs);
    foff += dfoff;
    phase_ch *= foff_rect;
    ch_fdm_frame(i) = tx_fdm_frame(i) * phase_ch;
  end
  foff_log = [foff_log foff];
  phase_ch /= abs(phase_ch);

  % optional fading

  if fading_en
    ch_fdm_delay(1:nhfdelay) = ch_fdm_delay(acohpsk.Nsymbrowpilot*M+1:nhfdelay+acohpsk.Nsymbrowpilot*M);
    ch_fdm_delay(nhfdelay+1:nhfdelay+acohpsk.Nsymbrowpilot*M) = ch_fdm_frame;

    for i=1:acohpsk.Nsymbrowpilot*M
      ahf_model = hf_gain*(spread(hf_n)*ch_fdm_frame(i) + spread_2ms(hf_n)*ch_fdm_delay(i));
      ch_fdm_frame(i) = ahf_model;
      hf_n++;
    end
  end

  % each carrier has power = 2, total power 2Nc, total symbol rate NcRs, noise BW B=Fs
  % Es/No = (C/Rs)/(N/B), N = var = 2NcFs/NcRs(Es/No) = 2Fs/Rs(Es/No)

  variance = 2*Fs/(acohpsk.Rs*EsNo);
  uvnoise = sqrt(0.5)*(randn(1,acohpsk.Nsymbrowpilot*M) + j*randn(1,acohpsk.Nsymbrowpilot*M));
  uvnoise_log = [uvnoise_log uvnoise];
  noise = sqrt(variance)*uvnoise;

  ch_fdm_frame += noise;

  ch_fdm_frame_log = [ch_fdm_frame_log ch_fdm_frame];
end

% simulate difference in sample clocks

tin=1;
tout=1;
ch_fdm_frame_log_out = zeros(1,length(ch_fdm_frame_log));
while tin < length(ch_fdm_frame_log)
      t1 = floor(tin);
      t2 = ceil(tin);
      f = tin - t1;
      ch_fdm_frame_log_out(tout) = (1-f)*ch_fdm_frame_log(t1) + f*ch_fdm_frame_log(t2);
      tout += 1;
      tin  += 1+sample_rate_ppm/1E6;
end
ch_fdm_frame_log = ch_fdm_frame_log_out(1:tout-1);

% Now run demod ----------------------------------------------------------------

ch_fdm_frame_log_index = 1;
nin = M;
f = 0;
nin_frame = acohpsk.Nsymbrowpilot*M;

%while (ch_fdm_frame_log_index + acohpsk.Nsymbrowpilot*M+M/P) < length(ch_fdm_frame_log)
for f=1:frames;
  acohpsk.frame = f;

  ch_fdm_frame = ch_fdm_frame_log(ch_fdm_frame_log_index:ch_fdm_frame_log_index + nin_frame - 1);
  ch_fdm_frame_log_index += nin_frame;

  %
  % Demod ----------------------------------------------------------------------
  %

  % store two frames of received samples so we can rewind if we get a good candidate

  ch_fdm_frame_buf(1:Nsw*acohpsk.Nsymbrowpilot*M-nin_frame) = ch_fdm_frame_buf(nin_frame+1:Nsw*acohpsk.Nsymbrowpilot*M);
  ch_fdm_frame_buf(Nsw*acohpsk.Nsymbrowpilot*M-nin_frame+1:Nsw*acohpsk.Nsymbrowpilot*M) = ch_fdm_frame;

  next_sync = sync;

  % if out of sync do Initial Freq offset estimation over NSW frames to flush out memories

  if (sync == 0)

    % we can test +/- 20Hz, so we break this up into 3 tests to cover +/- 60Hz

    max_ratio = 0;
    for acohpsk.f_est = Fcentre-40:40:Fcentre+40
       
      printf("  [%d] acohpsk.f_est: %f +/- 20\n", f, acohpsk.f_est);

      % we are out of sync so reset f_est and process two frames to clean out memories

      [ch_symb rx_timing rx_filt rx_baseband afdmdv acohpsk.f_est] = rate_Fs_rx_processing(afdmdv, ch_fdm_frame_buf, acohpsk.f_est, Nsw*acohpsk.Nsymbrowpilot, nin, 0);
      rx_baseband_log = [rx_baseband_log rx_baseband];
      
      rx_filt_log = [rx_filt_log rx_filt];
      ch_symb_log = [ch_symb_log; ch_symb];
      rx_timing_log = [rx_timing_log rx_timing];

      for i=1:Nsw-1
        acohpsk.ct_symb_buf = update_ct_symb_buf(acohpsk.ct_symb_buf, ch_symb((i-1)*acohpsk.Nsymbrowpilot+1:i*acohpsk.Nsymbrowpilot,:), acohpsk.Nct_sym_buf, acohpsk.Nsymbrowpilot);
      end
      [anext_sync acohpsk] = frame_sync_fine_freq_est(acohpsk, ch_symb((Nsw-1)*acohpsk.Nsymbrowpilot+1:Nsw*acohpsk.Nsymbrowpilot,:), sync, next_sync);

      if anext_sync == 1
        %printf("  [%d] acohpsk.ratio: %f\n", f, acohpsk.ratio);
        if acohpsk.ratio > max_ratio
          max_ratio   = acohpsk.ratio;
          f_est       = acohpsk.f_est - acohpsk.f_fine_est;
          next_sync   = anext_sync;
        end
      end
    end

    if next_sync == 1

      % we've found a sync candidate!
      % re-process last two frames with adjusted f_est then check again

      acohpsk.f_est = f_est;

      printf("  [%d] trying sync and f_est: %f\n", f, acohpsk.f_est);

      [ch_symb rx_timing rx_filt rx_baseband afdmdv f_est] = rate_Fs_rx_processing(afdmdv, ch_fdm_frame_buf, acohpsk.f_est, Nsw*acohpsk.Nsymbrowpilot, nin, 0);
      rx_baseband_log = [rx_baseband_log rx_baseband];
      rx_filt_log = [rx_filt_log rx_filt];
      ch_symb_log = [ch_symb_log; ch_symb];
      rx_timing_log = [rx_timing_log rx_timing];

      for i=1:Nsw-1
        acohpsk.ct_symb_buf = update_ct_symb_buf(acohpsk.ct_symb_buf, ch_symb((i-1)*acohpsk.Nsymbrowpilot+1:i*acohpsk.Nsymbrowpilot,:), acohpsk.Nct_sym_buf, acohpsk.Nsymbrowpilot);
      end
      [next_sync acohpsk] = frame_sync_fine_freq_est(acohpsk, ch_symb((Nsw-1)*acohpsk.Nsymbrowpilot+1:Nsw*acohpsk.Nsymbrowpilot,:), sync, next_sync);
      if abs(acohpsk.f_fine_est) > 2
        printf("  [%d] Hmm %f is a bit big so back to coarse est ...\n", f, acohpsk.f_fine_est);
        next_sync = 0;
      end

      if acohpsk.ratio < 0.9
        next_sync = 0;
      end
      if next_sync == 1
        % OK we are in sync!
        % demodulate first frame (demod completed below)

        printf("  [%d] in sync! f_est: %f ratio: %f \n", f, f_est, acohpsk.ratio);
        acohpsk.ct_symb_ff_buf(1:acohpsk.Nsymbrowpilot+2,:) = acohpsk.ct_symb_buf(acohpsk.ct+1:acohpsk.ct+acohpsk.Nsymbrowpilot+2,:);
      end
    end  
  end

  % If in sync just do sample rate processing on latest frame

  if sync == 1
    [ch_symb rx_timing rx_filt rx_baseband afdmdv acohpsk.f_est] = rate_Fs_rx_processing(afdmdv, ch_fdm_frame, acohpsk.f_est, acohpsk.Nsymbrowpilot, nin, acohpsk.ftrack_en);
    [next_sync acohpsk] = frame_sync_fine_freq_est(acohpsk, ch_symb, sync, next_sync);

    acohpsk.ct_symb_ff_buf(1:2,:) = acohpsk.ct_symb_ff_buf(acohpsk.Nsymbrowpilot+1:acohpsk.Nsymbrowpilot+2,:);
    acohpsk.ct_symb_ff_buf(3:acohpsk.Nsymbrowpilot+2,:) = acohpsk.ct_symb_buf(acohpsk.ct+3:acohpsk.ct+acohpsk.Nsymbrowpilot+2,:);

    rx_baseband_log = [rx_baseband_log rx_baseband];
    rx_filt_log = [rx_filt_log rx_filt];
    ch_symb_log = [ch_symb_log; ch_symb];     
    rx_timing_log = [rx_timing_log rx_timing];
    f_est_log = [f_est_log acohpsk.f_est];
  end

  % if we are in sync complete demodulation with symbol rate processing

  if (next_sync == 1) || (sync == 1)
    [rx_symb rx_bits rx_symb_linear amp_ phi_ sig_rms noise_rms] = qpsk_symbols_to_bits(acohpsk, acohpsk.ct_symb_ff_buf);
    rx_symb_log = [rx_symb_log; rx_symb];
    rx_amp_log = [rx_amp_log; amp_];
    rx_phi_log = [rx_phi_log; phi_];
    rx_bits_log = [rx_bits_log rx_bits];
    tx_bits_prev_log = [tx_bits_prev_log prev_tx_bits2];
    ratio_log = [ratio_log acohpsk.ratio];
    ct_symb_ff_log = [ct_symb_ff_log; acohpsk.ct_symb_ff_buf(1:acohpsk.Nsymbrowpilot,:)];
    sig_rms_log = [sig_rms_log sig_rms];
    noise_rms_log = [noise_rms_log noise_rms];
    noise_rms_filt = 0.9*noise_rms_filt + 0.1*noise_rms;
    noise_rms_filt_log = [noise_rms_filt_log noise_rms_filt];

    % BER stats

    if f > 2
      error_positions = xor(tx_bits_log((f-3)*framesize+1:(f-2)*framesize), rx_bits);
      Nerrs  += sum(error_positions);
      nerr_log = [nerr_log sum(error_positions)];
      Tbits += length(error_positions);
      error_positions_hist += error_positions;
    end
    printf("\r  [%d]", f);
  end

  % reset BER stats if we lose sync

  if sync == 1
    %Nerrs = 0;
    %Tbits = 0;
    %nerr_log = [];
  end

  [sync acohpsk] = sync_state_machine(acohpsk, sync, next_sync);

  % work out how many samples we need for next time

  nin = M;
  if sync == 1
    if rx_timing(length(rx_timing)) > M/P
      nin = M + M/P;
    end
    if rx_timing(length(rx_timing)) < -M/P
      nin = M - M/P;
    end
  end
  nin_frame = (acohpsk.Nsymbrowpilot-1)*M + nin;

  prev_tx_bits2 = prev_tx_bits;
  prev_tx_bits = tx_bits;

end

ber = Nerrs/Tbits;
printf("\nOctave EsNodB: %4.1f ber..: %4.3f Nerrs..: %d Tbits..: %d\n", EsNodB, ber, Nerrs, Tbits);

if compare_with_c

  % Output vectors from C port ---------------------------------------------------

  load tcohpsk_out.txt

  % Determine bit error rate

  
  sz = length(rx_bits_log_c);
  Nerrs_c = sum(xor(tx_bits_log(1:sz-framesize), rx_bits_log_c(framesize+1:sz)));
  Tbits_c = length(tx_bits_prev_log);
  ber_c = Nerrs_c/Tbits_c;
  printf("C EsNodB.....: %4.1f ber_c: %4.3f Nerrs_c: %d Tbits_c: %d\n", EsNodB, ber_c, Nerrs_c, Tbits_c);
  
  stem_sig_and_error(1, 111, tx_bits_log_c, tx_bits_log - tx_bits_log_c, 'tx bits', [1 length(tx_bits_log) -1.5 1.5])
  
  stem_sig_and_error(2, 211, real(tx_symb_log_c), real(tx_symb_log - tx_symb_log_c), 'tx symb re', [1 length(tx_symb_log_c) -1.5 1.5])
  stem_sig_and_error(2, 212, imag(tx_symb_log_c), imag(tx_symb_log - tx_symb_log_c), 'tx symb im', [1 length(tx_symb_log_c) -1.5 1.5])

  stem_sig_and_error(3, 211, real(tx_fdm_frame_log_c), real(tx_fdm_frame_log - tx_fdm_frame_log_c), 'tx fdm frame re', [1 length(tx_fdm_frame_log) -10 10])
  stem_sig_and_error(3, 212, imag(tx_fdm_frame_log_c), imag(tx_fdm_frame_log - tx_fdm_frame_log_c), 'tx fdm frame im', [1 length(tx_fdm_frame_log) -10 10])
  stem_sig_and_error(4, 211, real(ch_fdm_frame_log_c), real(ch_fdm_frame_log - ch_fdm_frame_log_c), 'ch fdm frame re', [1 length(ch_fdm_frame_log) -10 10])
  stem_sig_and_error(4, 212, imag(ch_fdm_frame_log_c), imag(ch_fdm_frame_log - ch_fdm_frame_log_c), 'ch fdm frame im', [1 length(ch_fdm_frame_log) -10 10])

  c = 1;
  stem_sig_and_error(5, 211, real(rx_baseband_log_c(c,:)), real(rx_baseband_log(c,:) - rx_baseband_log_c(c,:)), 'rx baseband re', [1 length(rx_baseband_log) -10 10])
  stem_sig_and_error(5, 212, imag(rx_baseband_log_c(c,:)), imag(rx_baseband_log(c,:) - rx_baseband_log_c(c,:)), 'rx baseband im', [1 length(rx_baseband_log) -10 10])
  stem_sig_and_error(6, 211, real(rx_filt_log_c(c,:)), real(rx_filt_log(c,:) - rx_filt_log_c(c,:)), 'rx filt re', [1 length(rx_filt_log) -1 1])
  stem_sig_and_error(6, 212, imag(rx_filt_log_c(c,:)), imag(rx_filt_log(c,:) - rx_filt_log_c(c,:)), 'rx filt im', [1 length(rx_filt_log) -1 1])

  [n m] = size(ch_symb_log);
  stem_sig_and_error(7, 211, real(ch_symb_log_c), real(ch_symb_log - ch_symb_log_c), 'ch symb re', [1 n -1.5 1.5])
  stem_sig_and_error(7, 212, imag(ch_symb_log_c), imag(ch_symb_log - ch_symb_log_c), 'ch symb im', [1 n -1.5 1.5])

  [n m] = size(rx_symb_log);
  stem_sig_and_error(8, 211, rx_amp_log_c, rx_amp_log - rx_amp_log_c, 'Amp Est', [1 n -1.5 1.5])
  phi_log_diff = rx_phi_log - rx_phi_log_c;
  phi_log_diff(find(phi_log_diff > pi)) -= 2*pi;
  phi_log_diff(find(phi_log_diff < -pi)) += 2*pi;
  stem_sig_and_error(8, 212, rx_phi_log_c, phi_log_diff, 'Phase Est', [1 n -4 4])
  stem_sig_and_error(9, 211, real(rx_symb_log_c), real(rx_symb_log - rx_symb_log_c), 'rx symb re', [1 n -1.5 1.5])
  stem_sig_and_error(9, 212, imag(rx_symb_log_c), imag(rx_symb_log - rx_symb_log_c), 'rx symb im', [1 n -1.5 1.5])

  stem_sig_and_error(10, 111, rx_bits_log_c, rx_bits_log - rx_bits_log_c, 'rx bits', [1 length(rx_bits_log) -1.5 1.5])
  stem_sig_and_error(11, 111, f_est_log_c - Fcentre - foff, f_est_log - f_est_log_c, 'f est', [1 length(f_est_log) -5 5])
  stem_sig_and_error(12, 111, rx_timing_log_c, rx_timing_log_c - rx_timing_log, 'rx timing', [1 length(rx_timing_log) -M M])

  check(tx_bits_log, tx_bits_log_c, 'tx_bits');
  check(tx_symb_log, tx_symb_log_c, 'tx_symb');
  check(tx_fdm_frame_log, tx_fdm_frame_log_c, 'tx_fdm_frame',0.01);
  check(ch_fdm_frame_log, ch_fdm_frame_log_c, 'ch_fdm_frame',0.01);
  check(ch_symb_log, ch_symb_log_c, 'ch_symb',0.05);
  check(rx_amp_log, rx_amp_log_c, 'rx_amp_log',0.01);
  check(phi_log_diff, zeros(length(phi_log_diff), Nc*Nd), 'rx_phi_log',0.1);
  check(rx_symb_log, rx_symb_log_c, 'rx_symb',0.01);
  check(rx_timing_log, rx_timing_log_c, 'rx_timing',0.005);
  check(rx_bits_log, rx_bits_log_c, 'rx_bits');
  check(f_est_log, f_est_log_c, 'f_est');
  check(sig_rms_log, sig_rms_log_c, 'sig_rms');
  check(noise_rms_log, noise_rms_log_c, 'noise_rms');
  
  printf("\npasses: %d fails: %d\n", passes, fails);

else
  
  papr = max(tx_fdm_frame_log.*conj(tx_fdm_frame_log)) / mean(tx_fdm_frame_log.*conj(tx_fdm_frame_log));
  papr_dB = 10*log10(papr);
  printf("av tx pwr: %4.2f PAPR: %4.2f av rx pwr: %4.2f\n", var(tx_fdm_frame_log), papr_dB, var(ch_fdm_frame_log));

  % some other useful plots

  f = figure(1)
  clf
  subplot(211)
  plot(real(tx_fdm_frame_log))
  title('tx fdm real');
  subplot(212)
  plot(imag(tx_fdm_frame_log))
  title('tx fdm imag');

  f = figure(2)
  clf
  spec = 20*log10(abs(fft(tx_fdm_frame_log)));
  l = length(spec);
  plot((Fs/l)*(1:l), spec)
  axis([1 Fs/2 0 max(spec)]);
  title('tx spectrum');
  ylabel('Amplitude (dB)')
  xlabel('Frequency (Hz)')
  grid;

  f = figure(3)
  clf;
  % plot combined signals to show diversity gains
  combined = rx_symb_log(:,1:Nc);
  for d=2:Nd
    combined += rx_symb_log(:, (d-1)*Nc+1:d*Nc);
  end
  plot(combined*exp(j*pi/4)/sqrt(Nd),'+')
  title('Scatter');
  ymax = abs(max(max(combined)));
  axis([-ymax ymax -ymax ymax])

  f = figure(4)
  clf;
  subplot(211)
  plot(rx_phi_log)
  subplot(212)
  plot(rx_amp_log)

  f = figure(5)
  clf;
  subplot(211)
  plot(rx_timing_log)
  title('rx timing');
  subplot(212)
  stem(ratio_log)
  title('Sync ratio');

  f = figure(6)
  clf;
  subplot(211)
  stem(nerr_log)
  title('Bit Errors');
  subplot(212)
  plot(noise_rms_filt_log,'r', sig_rms_log,'g');
  title('Est rms signal and noise')

  f = figure(7);
  clf;
  subplot(211)
  plot(foff_log,';freq offset;');
  hold on;
  plot(f_est_log - Fcentre,'g;freq offset est;');
  hold off;
  title('freq offset');
  legend("boxoff");  
  subplot(212)
  plot(foff_log(1:length(f_est_log)) - f_est_log + Fcentre)
  title('freq offset estimation error');

  f = figure(8)
  clf
  h = freqz(b,a,Fs/2);
  plot(20*log10(abs(h)))
  axis([1 Fs/2 -20 0])
  grid
  title('SSB tx filter')

  f = figure(9)
  clf
  plot(error_positions_hist)    
  title('histogram of bit errors')                               

  
end


% function to write C header file of noise samples so C version gives
% exactly the same results

function write_noise_file(uvnoise_log)

  m = length(uvnoise_log);

  filename = sprintf("../unittest/noise_samples.h");
  f=fopen(filename,"wt");
  fprintf(f,"/* unit variance complex noise samples */\n\n");
  fprintf(f,"/* Generated by write_noise_file() Octave function */\n\n");
  fprintf(f,"COMP noise[]={\n");
  for r=1:m
    if r < m
      fprintf(f, "  {%f,%f},\n", real(uvnoise_log(r)), imag(uvnoise_log(r)));
    else
      fprintf(f, "  {%f,%f}\n};", real(uvnoise_log(r)), imag(uvnoise_log(r)));
    end
  end

  fclose(f);
endfunction


% function to write float fading samples for use by C programs

%function write_noise_file(raw_file_name, Fs, dopplerSpreadHz, len_samples)
%  spread = doppler_spread(dopplerSpreadHz, Fs, len_samples);
%  spread_2ms = doppler_spread(dopplerSpreadHz, Fs, len_samples);
%  hf_gain = 1.0/sqrt(var(spread)+var(spread_2ms));
%
%  % interleave real imag samples
%
%  inter = zeros(1,len_samples*4);
%  inter(1:4) = hf_gain;
%  for i=1:len_samples
%    inter(i*4+1) = real(spread(i));
%    inter(i*4+2) = imag(spread(i));
%    inter(i*4+3) = real(spread_2ms(i));
%    inter(i*4+4) = imag(spread_2ms(i));
%  end
%  f = fopen(raw_file_name,"wb");
%  fwrite(f, inter, "float32");
%  fclose(f);
%endfunction
