/* Copyright (C) 2017 Matthias P. Braendli, matthias.braendli@mpb.li http://opendigitalradio.org */ /* This file is part of ODR-DPD. ODR-DPD is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. ODR-DPD 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 General Public License along with ODR-DPD. If not, see . */ #include "AlignSample.hpp" #include #include #include #include #include namespace fftw { class plan { public: template plan(size_t N, T& in, T& out, int direction) { m_plan = fftwf_plan_dft_1d(N, reinterpret_cast(&in.front()), reinterpret_cast(&out.front()), FFTW_FORWARD, FFTW_MEASURE); } plan(const plan& other) = delete; const plan& operator=(const plan& other) = delete; void execute(void) { fftwf_execute(m_plan); } ~plan() { fftwf_destroy_plan(m_plan); } private: fftwf_plan m_plan; }; } void AlignSample::push_tx_samples(complexf* samps, size_t len, double first_sample_time) { std::lock_guard lock(m_mutex); std::copy(samps, samps + len, std::back_inserter(m_txsamples)); if (m_first_tx_sample_time == 0) { m_first_tx_sample_time = first_sample_time; } } void AlignSample::push_rx_samples(complexf* samps, size_t len, double first_sample_time) { std::lock_guard lock(m_mutex); if (m_first_rx_sample_time == 0 and first_sample_time != 0) { std::copy(samps, samps + len, std::back_inserter(m_rxsamples)); m_first_rx_sample_time = first_sample_time; m_num_rx_samples_dropped = 0; } else if (m_first_rx_sample_time != 0) { // We have previously received samples with valid timestamp double delta = m_rx_sample_time(m_rxsamples.size()) - first_sample_time; if (std::fabs(delta) > 1e-3) { fprintf(stderr, "RX sample time %f expected %f. Delta of %f(%zu)." " Resetting RX\n", m_rx_sample_time(m_rxsamples.size()), first_sample_time, delta, (size_t)std::fabs(delta * (double)samplerate)); m_first_rx_sample_time = 0; m_num_rx_samples_dropped = 0; m_rxsamples.clear(); } std::copy(samps, samps + len, std::back_inserter(m_rxsamples)); } else if (first_sample_time == 0) { MDEBUG("RX timestamp missing\n"); throw std::runtime_error("RX timestamp missing"); } } void AlignSample::reset_rx() { std::lock_guard lock(m_mutex); m_first_rx_sample_time = 0; m_num_rx_samples_dropped = 0; m_rxsamples.clear(); } bool AlignSample::ready(size_t min_samples) { std::lock_guard lock(m_mutex); return align() and m_rxsamples.size() > min_samples and m_txsamples.size() > min_samples; } CorrelationResult AlignSample::crosscorrelate(size_t len) { double rx_ts = 0; double tx_ts = 0; /* The size of the FFT is twice as long as the desired * correlation length, because the FFT does a circular * correlation, and we therefore pad half with zeros */ const size_t N = 2 * len; vec_cf rx_fft_in(N); vec_cf rx_fft_out(N); vec_cf tx_fft_in(N); vec_cf tx_fft_out(N); vec_cf ifft_in(N); vec_cf ifft_out(N); fftw::plan rx_fft_plan(N, rx_fft_in, rx_fft_out, FFTW_FORWARD); fftw::plan tx_fft_plan(N, tx_fft_in, tx_fft_out, FFTW_FORWARD); fftw::plan ifft_plan(N, ifft_in, ifft_out, FFTW_BACKWARD); // Do a quick copy, so as to free the mutex { std::lock_guard lock(m_mutex); if (!align() or m_rxsamples.size() < len or m_txsamples.size() < len) { CorrelationResult result(0); return result; } std::copy(m_rxsamples.begin(), m_rxsamples.begin() + len, rx_fft_in.begin()); std::copy(m_txsamples.begin(), m_txsamples.begin() + len, tx_fft_in.begin()); // the other half of the buffers are set to 0 m_drop_rx_samples(len); m_drop_tx_samples(len); rx_ts = m_rx_sample_time(); tx_ts = m_tx_sample_time(); } CorrelationResult result(len); result.rx_timestamp = rx_ts; result.tx_timestamp = tx_ts; // Calculate power for (size_t i = 0; i < len; i++) { result.rx_power += std::norm(rx_fft_in[i]); } result.rx_power = std::sqrt(result.rx_power); for (size_t i = 0; i < len; i++) { result.tx_power += std::norm(tx_fft_in[i]); } result.tx_power = std::sqrt(result.tx_power); // Implement // corr(a, b) = ifft(fft(a) * conj(fft(b))) // Attention: circular correlation ! rx_fft_plan.execute(); tx_fft_plan.execute(); for (size_t i = 0; i < len; i++) { ifft_in[i] = tx_fft_out[i] * std::conj(rx_fft_out[i]); } ifft_plan.execute(); for (size_t i = 0; i < len; i++) { result.correlation[i] = ifft_out[i]; } return result; } void AlignSample::delay_rx_samples(size_t delay) { std::lock_guard lock(m_mutex); if (align() and delay > 0 and m_rxsamples.size() > delay) { m_rxsamples.erase( m_rxsamples.begin(), m_rxsamples.begin() + delay - 1); } } static std::vector gen_omega(size_t length) { if ((length % 2) == 1) { throw std::runtime_error("Needs an even length array."); } size_t halflength = length/2; double factor = 2.0 * M_PI / length; std::vector omega(length); for (size_t i = 0; i < halflength; i++) { omega[i] = factor * i; } for (size_t i = halflength+1; i < length; i++) { omega[i] = factor * ((ssize_t)i - length); } return omega; } /* Find subsample delay in signal versus the reference signal ref and * correct it. This assumes that the offset between signal and ref * is less than one sample. */ static vec_cf align_subsample(vec_cf& signal, vec_cf& ref) { size_t N = signal.size(); vec_cf fft_sig(N); vec_cf rotate_vec(N); vec_cf corr_sig(N); fftw::plan plan(N, signal, fft_sig, FFTW_FORWARD); fftw::plan plan_ifft(N, rotate_vec, corr_sig, FFTW_BACKWARD); plan.execute(); const std::vector omega = gen_omega(N); // Calculate the correlation for a lag of tau, which // must be between -1 and 1 samples auto correlate_point = [&](float tau) { // A subsample offset between two signals corresponds, in the frequency // domain, to a linearly increasing phase shift, whose slope // corresponds to the delay. // // Here, we build this phase shift in rotate_vec, and multiply it with // our signal. for (size_t i = 0; i < N; i++) { const complexf angle(0, tau * i); rotate_vec[i] = std::exp(angle); // e^(j*tau*i) } // zero-frequency is rotate_vec[0], so rotate_vec[N/2] is the // bin corresponding to the [-1, 1, -1, 1, ...] time signal, which // is both the maximum positive and negative frequency. // I don't remember why we handle it differently. rotate_vec[N/2] = cos(M_PI * tau); for (size_t i = 0; i < N; i++) { rotate_vec[i] *= fft_sig[i]; } plan_ifft.execute(); // corr_sig = IFFT(rotate_vec) // Calculate correlation only the real part of the signal: // The following implements the elementwise vector operation // corr_real = real(corr_sig) * real(ref) // in an awkward way. std::vector corr_real(N); std::transform(corr_sig.begin(), corr_sig.end(), corr_real.begin(), [&](complexf f){ return f.real(); }); for (size_t i = 0; i < N; i++) { complexf f = corr_real[i] * ref[i]; corr_real[i] = f.real(); } // TODO: replace by the following and verify it's equivalent /* for (size_t i = 0; i < N; i++) { corr_real[i] = corr_sig[i].real() * ref[i].real(); } */ // TODO why do we only look at the real part? Because it's faster than // a complex cross-correlation? Clarify! // Compare with /* for (size_t i = 0; i < N; i++) { corr_real[i] = (corr_sig[i] * std::conj(ref[i])).real(); } */ // The correlation is the sum: return std::accumulate(corr_real.begin(), corr_real.end(), 0.0f); }; const float start_arg = -1; const float end_arg = 1; // Let boost find the tau value with the minimal correlation auto tau = boost::math::tools::brent_find_minima( correlate_point, start_arg, end_arg, 1000).first; fprintf(stderr, "Fractional delay is %f\n", tau); // Prepare rotate_vec = fft_sig with rotated phase for (size_t i = 0; i < N; i++) { const complexf angle(0, tau * i); rotate_vec[i] = std::exp(angle); } rotate_vec[N/2] = cos(M_PI * tau); for (size_t i = 0; i < N; i++) { rotate_vec[i] *= fft_sig[i]; } plan_ifft.execute(); // corr_sig = IFFT(rotate_vec) return corr_sig; } std::pair AlignSample::get_samples( size_t len) { std::pair rval; std::lock_guard lock(m_mutex); if (align() and m_rxsamples.size() > len and m_txsamples.size() > len) { rval.first.reserve(len); rval.second.reserve(len); std::copy(m_rxsamples.begin(), m_rxsamples.begin() + len, std::back_inserter(rval.first)); std::copy(m_txsamples.begin(), m_txsamples.begin() + len, std::back_inserter(rval.second)); rval.first = align_subsample(rval.first, rval.second); } return rval; } void AlignSample::consume(size_t samples) { std::lock_guard lock(m_mutex); if (align() and m_rxsamples.size() > samples and m_txsamples.size() > samples) { m_drop_rx_samples(samples); m_drop_tx_samples(samples); } } bool AlignSample::align() { if (std::abs(m_rx_sample_time() - m_tx_sample_time()) < 1e-6) { return true; } else if (m_rx_sample_time() < m_tx_sample_time()) { size_t rx_samples_to_skip = (m_tx_sample_time() - m_rx_sample_time()) * samplerate; if (rx_samples_to_skip > m_rxsamples.size()) { return false; } m_drop_rx_samples(rx_samples_to_skip); return true; } else if (m_rx_sample_time() > m_tx_sample_time()) { size_t tx_samples_to_skip = (m_rx_sample_time() - m_tx_sample_time()) * samplerate; if (tx_samples_to_skip > m_txsamples.size()) { return false; } m_drop_tx_samples(tx_samples_to_skip); return true; } return false; }