kopia lustrzana https://github.com/mobilinkd/m17-cxx-demod
				
				
				
			Merge kalman filter clock recovery changes.
						commit
						5ff118718c
					
				| 
						 | 
				
			
			@ -30,6 +30,26 @@ find_package(codec2 REQUIRED)
 | 
			
		|||
set(Boost_USE_STATIC_LIBS FALSE)
 | 
			
		||||
find_package(Boost COMPONENTS program_options REQUIRED)
 | 
			
		||||
 | 
			
		||||
if (BLAZE_INCLUDE_DIR)
 | 
			
		||||
 | 
			
		||||
  # in cache already
 | 
			
		||||
  set(BLAZE_FOUND TRUE)
 | 
			
		||||
  message(STATUS "Have blaze")
 | 
			
		||||
else (BLAZE_INCLUDE_DIR)
 | 
			
		||||
 | 
			
		||||
find_path(BLAZE_INCLUDE_DIR NAMES blaze/Blaze.h
 | 
			
		||||
     PATHS
 | 
			
		||||
     ${INCLUDE_INSTALL_DIR}
 | 
			
		||||
   )
 | 
			
		||||
 | 
			
		||||
include(FindPackageHandleStandardArgs)
 | 
			
		||||
find_package_handle_standard_args(BLAZE DEFAULT_MSG BLAZE_INCLUDE_DIR)
 | 
			
		||||
 | 
			
		||||
mark_as_advanced(BLAZE_INCLUDE_DIR)
 | 
			
		||||
message(STATUS "Found blaze")
 | 
			
		||||
 | 
			
		||||
endif(BLAZE_INCLUDE_DIR)
 | 
			
		||||
 | 
			
		||||
# Add subdirectories
 | 
			
		||||
add_subdirectory(src)
 | 
			
		||||
add_subdirectory(apps)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,8 @@ stream from STDIN and writes out an M17 4-FSK baseband stream at 48k SPS,
 | 
			
		|||
 | 
			
		||||
This code requires the codec2-devel, boost-devel and gtest-devel packages be installed.
 | 
			
		||||
 | 
			
		||||
This code also now requires the Blaze C++ math library.
 | 
			
		||||
 | 
			
		||||
It also requires a modern C++17 compiler (GCC 8 minimum).
 | 
			
		||||
 | 
			
		||||
### Build Steps
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -316,7 +316,7 @@ bool handle_frame(mobilinkd::M17FrameDecoder::output_buffer_t const& frame, int
 | 
			
		|||
            result = dump_lsf(frame.lsf);
 | 
			
		||||
            break;
 | 
			
		||||
        case FrameType::LICH:
 | 
			
		||||
            std::cerr << "LICH" << std::endl;
 | 
			
		||||
            std::cerr << "\nLICH" << std::endl;
 | 
			
		||||
            break;
 | 
			
		||||
        case FrameType::STREAM:
 | 
			
		||||
            result = demodulate_audio(frame.stream, viterbi_cost);
 | 
			
		||||
| 
						 | 
				
			
			@ -341,20 +341,20 @@ void diagnostic_callback(bool dcd, FloatType evm, FloatType deviation, FloatType
 | 
			
		|||
{
 | 
			
		||||
    if (debug) {
 | 
			
		||||
        std::cerr << "\rdcd: " << std::setw(1) << int(dcd)
 | 
			
		||||
            << ", evm: " << std::setfill(' ') << std::setprecision(4) << std::setw(8) << evm * 100 <<"%"
 | 
			
		||||
            << ", deviation: " << std::setprecision(4) << std::setw(8) << deviation
 | 
			
		||||
            << ", freq offset: " << std::setprecision(4) << std::setw(8) << offset
 | 
			
		||||
            << ", locked: " << std::boolalpha << std::setw(6) << locked << std::dec
 | 
			
		||||
            << ", clock: " << std::setprecision(7) << std::setw(8) << clock
 | 
			
		||||
            << ", sample: " << std::setw(1) << sample_index << ", "  << sync_index << ", " << clock_index
 | 
			
		||||
            << ", cost: " << viterbi_cost;
 | 
			
		||||
            << ", evm: " << std::setfill(' ') << std::setprecision(2) << std::setw(6) << evm * 100 <<"%"
 | 
			
		||||
            << ", deviation: " << std::setw(5) << int(deviation)
 | 
			
		||||
            << "Hz, freq offset: " << std::setfill(' ') << std::setw(5) << int(offset * 800)
 | 
			
		||||
            << "Hz, locked: " << std::boolalpha << std::setw(5) << locked << std::dec
 | 
			
		||||
            << ", clock: " << std::setprecision(2) << std::fixed << std::setw(8) << (clock * 1'000'000.0)
 | 
			
		||||
            << "ppm, sample: " << std::setw(1) << sample_index << ", "  << sync_index << ", " << clock_index
 | 
			
		||||
            << ", cost: " << std::setfill(' ') << std::setw(3) << viterbi_cost;
 | 
			
		||||
    }
 | 
			
		||||
        
 | 
			
		||||
    if (!dcd && prbs.sync()) { // Seems like there should be a better way to do this.
 | 
			
		||||
    if (!dcd && (prbs.bits() > 0)) { // Seems like there should be a better way to do this.
 | 
			
		||||
        prbs.reset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (prbs.sync() && !quiet) {
 | 
			
		||||
    if ((prbs.bits() > 0) && !quiet) {
 | 
			
		||||
        if (!debug) {
 | 
			
		||||
            std::cerr << '\r';
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -486,7 +486,7 @@ int main(int argc, char* argv[])
 | 
			
		|||
        int16_t sample;
 | 
			
		||||
        std::cin.read(reinterpret_cast<char*>(&sample), 2);
 | 
			
		||||
        if (invert_input) sample *= -1;
 | 
			
		||||
        demod(sample / 44000.0);
 | 
			
		||||
        demod(sample / 41067.0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::cerr << std::endl;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -415,7 +415,7 @@
 | 
			
		|||
   "name": "python",
 | 
			
		||||
   "nbconvert_exporter": "python",
 | 
			
		||||
   "pygments_lexer": "ipython3",
 | 
			
		||||
   "version": "3.10.1"
 | 
			
		||||
   "version": "3.10.8"
 | 
			
		||||
  }
 | 
			
		||||
 },
 | 
			
		||||
 "nbformat": 4,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,9 @@
 | 
			
		|||
// Copyright 2021 Mobilinkd LLC.
 | 
			
		||||
// Copyright 2022 Mobilinkd LLC.
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "KalmanFilter.h"
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
| 
						 | 
				
			
			@ -11,196 +13,94 @@
 | 
			
		|||
namespace mobilinkd
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calculate the phase estimates for each sample position.
 | 
			
		||||
 * 
 | 
			
		||||
 * This performs a running calculation of the phase of each bit position.
 | 
			
		||||
 * It is very noisy for individual samples, but quite accurate when
 | 
			
		||||
 * averaged over an entire M17 frame.
 | 
			
		||||
 * 
 | 
			
		||||
 * It is designed to be used to calculate the best bit position for each
 | 
			
		||||
 * frame of data.  Samples are collected and averaged.  When update() is
 | 
			
		||||
 * called, the best sample index and clock are estimated, and the counters
 | 
			
		||||
 * reset for the next frame.
 | 
			
		||||
 * 
 | 
			
		||||
 * It starts counting bit 0 as the first bit received after a reset.
 | 
			
		||||
 *
 | 
			
		||||
 * This is very efficient as it only uses addition and subtraction for
 | 
			
		||||
 * each bit sample.  And uses one multiply and divide per update (per
 | 
			
		||||
 * frame).
 | 
			
		||||
 *
 | 
			
		||||
 * This will permit a clock error of up to 500ppm.  This allows up to
 | 
			
		||||
 * 250ppm error for both transmitter and receiver clocks.  This is
 | 
			
		||||
 * less than one sample per frame when the sample rate is 48000 SPS.
 | 
			
		||||
 *
 | 
			
		||||
 * @inv current_index_ is in the interval [0, SAMPLES_PER_SYMBOL).
 | 
			
		||||
 * @inv sample_index_ is in the interval [0, SAMPLES_PER_SYMBOL).
 | 
			
		||||
 * @inv clock_ is in the interval [0.9995, 1.0005]
 | 
			
		||||
 */
 | 
			
		||||
template <typename FloatType, size_t SampleRate, size_t SymbolRate>
 | 
			
		||||
class ClockRecovery
 | 
			
		||||
template <typename FloatType, size_t SamplesPerSymbol>
 | 
			
		||||
struct ClockRecovery
 | 
			
		||||
{
 | 
			
		||||
    static constexpr size_t SAMPLES_PER_SYMBOL = SampleRate / SymbolRate;
 | 
			
		||||
    static constexpr int8_t MAX_OFFSET = SAMPLES_PER_SYMBOL / 2;
 | 
			
		||||
    static constexpr FloatType dx = 1.0 / SAMPLES_PER_SYMBOL;
 | 
			
		||||
    static constexpr FloatType MAX_CLOCK = 1.0005;
 | 
			
		||||
    static constexpr FloatType MIN_CLOCK = 0.9995;
 | 
			
		||||
 | 
			
		||||
    std::array<FloatType, SAMPLES_PER_SYMBOL> estimates_;
 | 
			
		||||
    size_t sample_count_ = 0;
 | 
			
		||||
    uint16_t frame_count_ = 0;
 | 
			
		||||
    uint8_t sample_index_ = 0;
 | 
			
		||||
    uint8_t prev_sample_index_ = 0;
 | 
			
		||||
    uint8_t index_ = 0;
 | 
			
		||||
    FloatType offset_ = 0.0;
 | 
			
		||||
    FloatType clock_ = 1.0;
 | 
			
		||||
    FloatType prev_sample_ = 0.0;
 | 
			
		||||
    m17::KalmanFilter<FloatType, SamplesPerSymbol> kf_;
 | 
			
		||||
    size_t count_ = 0;
 | 
			
		||||
    int8_t sample_index_ = 0;
 | 
			
		||||
    FloatType clock_estimate_ = 0.;
 | 
			
		||||
    FloatType sample_estimate_ = 0.;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find the sample index.
 | 
			
		||||
     * Reset the clock recovery after the first sync word is received.
 | 
			
		||||
     * This is used as the starting state of the Kalman filter. Providing
 | 
			
		||||
     * the filter with a realistic starting point causes it to converge
 | 
			
		||||
     * must faster.
 | 
			
		||||
     * 
 | 
			
		||||
     * @param[in] index starting sample index.
 | 
			
		||||
     */
 | 
			
		||||
    void reset(FloatType index)
 | 
			
		||||
    {
 | 
			
		||||
        kf_.reset(index);
 | 
			
		||||
        count_ = 0;
 | 
			
		||||
        sample_index_ = index;
 | 
			
		||||
        clock_estimate_ = 0.;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Count each sample.
 | 
			
		||||
    void operator()(FloatType)
 | 
			
		||||
    {
 | 
			
		||||
        ++count_;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the filter with the estimated index from the sync word. The
 | 
			
		||||
     * result is a new estimate of the state, the remote symbol clock offset
 | 
			
		||||
     * relative to our clock.  It can be faster (>0.0) or slower (<0.0).
 | 
			
		||||
     * 
 | 
			
		||||
     * @param[in] index is the new symbol sample position from the sync word.
 | 
			
		||||
     */
 | 
			
		||||
    bool update(uint8_t index)
 | 
			
		||||
    {
 | 
			
		||||
        auto f = kf_.update(index, count_);
 | 
			
		||||
 | 
			
		||||
        // Constrain sample index to [0..SamplesPerSymbol), wrapping if needed.
 | 
			
		||||
        sample_estimate_ = f[0];
 | 
			
		||||
        sample_index_ = int8_t(round(sample_estimate_));
 | 
			
		||||
        sample_index_ = sample_index_ < 0 ? sample_index_ + SamplesPerSymbol : sample_index_;
 | 
			
		||||
        sample_index_ = sample_index_ >= int8_t(SamplesPerSymbol) ? sample_index_ - SamplesPerSymbol : sample_index_;
 | 
			
		||||
        clock_estimate_ = f[1];
 | 
			
		||||
        count_ = 0;
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This is used when no sync word is found. The sample index is updated
 | 
			
		||||
     * based on the current clock estimate, the last known good sample
 | 
			
		||||
     * estimate, and the number of samples processed.
 | 
			
		||||
     *
 | 
			
		||||
     * There are @p SAMPLES_PER_INDEX bins.  It is expected that half are
 | 
			
		||||
     * positive values and half are negative.  The positive and negative
 | 
			
		||||
     * bins will be grouped together such that there is a single transition
 | 
			
		||||
     * from positive values to negative values.
 | 
			
		||||
     *
 | 
			
		||||
     * The best bit position is always the position with the positive value
 | 
			
		||||
     * at that transition point.  It will be the bit index with the highest
 | 
			
		||||
     * energy.
 | 
			
		||||
     *
 | 
			
		||||
     * @post sample_index_ contains the best sample point.
 | 
			
		||||
     * The sample and clock estimates from the filter remain unchanged.
 | 
			
		||||
     */
 | 
			
		||||
    void update_sample_index_()
 | 
			
		||||
    bool update()
 | 
			
		||||
    {
 | 
			
		||||
        uint8_t index = 0;
 | 
			
		||||
        auto csw = std::fmod((sample_estimate_ + clock_estimate_ * count_), SamplesPerSymbol);
 | 
			
		||||
        if (csw < 0.) csw += SamplesPerSymbol;
 | 
			
		||||
        else if (csw >= SamplesPerSymbol) csw -= SamplesPerSymbol;
 | 
			
		||||
 | 
			
		||||
        // Find falling edge.
 | 
			
		||||
        bool is_positive = false;
 | 
			
		||||
        for (size_t i = 0; i != SAMPLES_PER_SYMBOL; ++i)
 | 
			
		||||
        {
 | 
			
		||||
            FloatType phase = estimates_[i];
 | 
			
		||||
        // Constrain sample index to [0..SamplesPerSymbol), wrapping if needed.
 | 
			
		||||
        sample_index_ = int8_t(round(csw));
 | 
			
		||||
        sample_index_ = sample_index_ < 0 ? sample_index_ + SamplesPerSymbol : sample_index_;
 | 
			
		||||
        sample_index_ = sample_index_ >= int8_t(SamplesPerSymbol) ? sample_index_ - SamplesPerSymbol : sample_index_;
 | 
			
		||||
 | 
			
		||||
            if (!is_positive && phase > 0)
 | 
			
		||||
            {
 | 
			
		||||
                is_positive = true;
 | 
			
		||||
            }
 | 
			
		||||
            else if (is_positive && phase < 0)
 | 
			
		||||
            {
 | 
			
		||||
                index = i;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        sample_index_ = index == 0 ? SAMPLES_PER_SYMBOL - 1 : index - 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Compute the drift in sample points from the last update.
 | 
			
		||||
     *
 | 
			
		||||
     * This should never be greater than one.
 | 
			
		||||
     */
 | 
			
		||||
    FloatType calc_offset_()
 | 
			
		||||
    {
 | 
			
		||||
        int8_t offset = sample_index_ - prev_sample_index_;
 | 
			
		||||
 | 
			
		||||
        // When in spec, the clock should drift by less than 1 sample per frame.
 | 
			
		||||
        if (offset >= MAX_OFFSET) [[unlikely]]
 | 
			
		||||
        {
 | 
			
		||||
            offset -= SAMPLES_PER_SYMBOL;
 | 
			
		||||
        }
 | 
			
		||||
        else if (offset <= -MAX_OFFSET) [[unlikely]]
 | 
			
		||||
        {
 | 
			
		||||
            offset += SAMPLES_PER_SYMBOL;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return offset;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void update_clock_()
 | 
			
		||||
    {
 | 
			
		||||
        // update_sample_index_() must be called first.
 | 
			
		||||
 | 
			
		||||
        if (frame_count_ == 0) [[unlikely]]
 | 
			
		||||
        {
 | 
			
		||||
            prev_sample_index_ = sample_index_;
 | 
			
		||||
            offset_ = 0.0;
 | 
			
		||||
            clock_ = 1.0;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        offset_ += calc_offset_();
 | 
			
		||||
        prev_sample_index_ = sample_index_;
 | 
			
		||||
        clock_ = 1.0 + (offset_ / (frame_count_ * sample_count_));
 | 
			
		||||
        clock_ = std::min(MAX_CLOCK, std::max(MIN_CLOCK, clock_));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    ClockRecovery()
 | 
			
		||||
    {
 | 
			
		||||
        estimates_.fill(0);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Update clock recovery with the given sample.  This will advance the
 | 
			
		||||
     * current sample index by 1.
 | 
			
		||||
     */
 | 
			
		||||
    void operator()(FloatType sample)
 | 
			
		||||
    {
 | 
			
		||||
        FloatType dy = (sample - prev_sample_);
 | 
			
		||||
 | 
			
		||||
        if (sample + prev_sample_ < 0)
 | 
			
		||||
        {
 | 
			
		||||
            // Invert the phase estimate when sample midpoint is less than 0.
 | 
			
		||||
            dy = -dy;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        prev_sample_ = sample;
 | 
			
		||||
        
 | 
			
		||||
        estimates_[index_] += dy;
 | 
			
		||||
        index_ += 1;
 | 
			
		||||
        if (index_ == SAMPLES_PER_SYMBOL)
 | 
			
		||||
        {
 | 
			
		||||
            index_ = 0;
 | 
			
		||||
        }
 | 
			
		||||
        sample_count_ += 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reset the state of the clock recovery system.  This should be called
 | 
			
		||||
     * when a new transmission is detected.
 | 
			
		||||
     */
 | 
			
		||||
    void reset()
 | 
			
		||||
    {
 | 
			
		||||
        sample_count_ = 0;
 | 
			
		||||
        frame_count_ = 0;
 | 
			
		||||
        index_ = 0;
 | 
			
		||||
        sample_index_ = 0;
 | 
			
		||||
        estimates_.fill(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the current sample index.  This will always be in the range of
 | 
			
		||||
     * [0..SAMPLES_PER_SYMBOL).
 | 
			
		||||
     */
 | 
			
		||||
    uint8_t current_index() const
 | 
			
		||||
    {
 | 
			
		||||
        return index_;
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the estimated sample clock increment based on the last update.
 | 
			
		||||
     * 
 | 
			
		||||
     *
 | 
			
		||||
     * The value is only valid after samples have been collected and update()
 | 
			
		||||
     * has been called.
 | 
			
		||||
     */
 | 
			
		||||
    FloatType clock_estimate() const
 | 
			
		||||
    {
 | 
			
		||||
        return clock_;
 | 
			
		||||
        return clock_estimate_;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return the estimated "best sample index" based on the last update.
 | 
			
		||||
     * 
 | 
			
		||||
     *
 | 
			
		||||
     * The value is only valid after samples have been collected and update()
 | 
			
		||||
     * has been called.
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -208,35 +108,7 @@ public:
 | 
			
		|||
    {
 | 
			
		||||
        return sample_index_;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the sample index and clock estimates, and reset the state for
 | 
			
		||||
     * the next frame of data.
 | 
			
		||||
     * 
 | 
			
		||||
     * @pre index_ = 0
 | 
			
		||||
     * @pre sample_count_ > 0
 | 
			
		||||
     * 
 | 
			
		||||
     * After this is called, sample_index() and clock_estimate() will have
 | 
			
		||||
     * valid, updated results.
 | 
			
		||||
     * 
 | 
			
		||||
     * The more samples between calls to update, the more accurate the
 | 
			
		||||
     * estimates will be.
 | 
			
		||||
     * 
 | 
			
		||||
     * @return true if the preconditions are met and the update has been
 | 
			
		||||
     *  performed, otherwise false.
 | 
			
		||||
     */
 | 
			
		||||
    bool update()
 | 
			
		||||
    {
 | 
			
		||||
        if (!(sample_count_ != 0 && index_ == 0)) return false;
 | 
			
		||||
 | 
			
		||||
        update_sample_index_();
 | 
			
		||||
        update_clock_();
 | 
			
		||||
               
 | 
			
		||||
        frame_count_ = std::min(0x1000, 1 + frame_count_);
 | 
			
		||||
        sample_count_ = 0;
 | 
			
		||||
        estimates_.fill(0);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
} // mobilinkd
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,16 +85,32 @@ struct Correlator
 | 
			
		|||
        size_t min_count = 0;
 | 
			
		||||
        size_t max_count = 0;
 | 
			
		||||
        size_t index = 0;
 | 
			
		||||
        FloatType min_level = buffer_[sample_index];
 | 
			
		||||
        FloatType max_level = buffer_[sample_index];
 | 
			
		||||
 | 
			
		||||
        for (size_t i = sample_index; i < buffer_.size(); i += SAMPLES_PER_SYMBOL)
 | 
			
		||||
        {
 | 
			
		||||
            min_level = std::min(min_level, buffer_[i]);
 | 
			
		||||
            max_level = std::max(max_level, buffer_[i]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        FloatType avg = max_level + min_level / 2.;
 | 
			
		||||
 | 
			
		||||
        for (size_t i = sample_index; i < buffer_.size(); i += SAMPLES_PER_SYMBOL)
 | 
			
		||||
        {
 | 
			
		||||
            tmp[index++] = buffer_[i] * 1000.;
 | 
			
		||||
            max_sum += buffer_[i] * ((buffer_[i] > 0.));
 | 
			
		||||
            min_sum += buffer_[i] * ((buffer_[i] < 0.));
 | 
			
		||||
            max_count += (buffer_[i] > 0.);
 | 
			
		||||
            min_count += (buffer_[i] < 0.);
 | 
			
		||||
            bool high = buffer_[i] > avg;
 | 
			
		||||
            bool low = buffer_[i] < avg;
 | 
			
		||||
            max_sum += buffer_[i] * high;
 | 
			
		||||
            min_sum += buffer_[i] * low;
 | 
			
		||||
            max_count += high;
 | 
			
		||||
            min_count += low;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        FloatType mn = min_count > 0 ? min_sum / min_count : min_level;
 | 
			
		||||
        FloatType mx = max_count > 0 ? max_sum / max_count : max_level;
 | 
			
		||||
 | 
			
		||||
        return std::make_tuple(min_sum / min_count, max_sum / max_count);
 | 
			
		||||
        return std::make_tuple(mn, mx);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -140,12 +156,30 @@ struct SyncWord
 | 
			
		|||
		return (value > limit_1 || value < limit_2) ? value : 0.0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bool is_triggered() const { return triggered_; }
 | 
			
		||||
 | 
			
		||||
	void find_peak(value_type value)
 | 
			
		||||
	{
 | 
			
		||||
		triggered_ = false;
 | 
			
		||||
		timing_index_ = 0;
 | 
			
		||||
		auto peak_value = value;
 | 
			
		||||
		uint8_t index = 0;
 | 
			
		||||
		for (auto f : samples_)
 | 
			
		||||
		{
 | 
			
		||||
			if (abs(f) > abs(peak_value))
 | 
			
		||||
			{
 | 
			
		||||
				peak_value = f;
 | 
			
		||||
				timing_index_ = index;
 | 
			
		||||
			}
 | 
			
		||||
			index += 1;
 | 
			
		||||
		}
 | 
			
		||||
		updated_ = peak_value > 0 ? 1 : -1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	size_t operator()(Correlator& correlator)
 | 
			
		||||
	{
 | 
			
		||||
		auto value = triggered(correlator);
 | 
			
		||||
 | 
			
		||||
		value_type peak_value = 0;
 | 
			
		||||
 | 
			
		||||
		if (value != 0)
 | 
			
		||||
		{
 | 
			
		||||
			if (!triggered_)
 | 
			
		||||
| 
						 | 
				
			
			@ -159,21 +193,7 @@ struct SyncWord
 | 
			
		|||
		{
 | 
			
		||||
			if (triggered_)
 | 
			
		||||
			{
 | 
			
		||||
				// Calculate the timing index on the falling edge.
 | 
			
		||||
				triggered_ = false;
 | 
			
		||||
				timing_index_ = 0;
 | 
			
		||||
				peak_value = value;
 | 
			
		||||
				uint8_t index = 0;
 | 
			
		||||
				for (auto f : samples_)
 | 
			
		||||
				{
 | 
			
		||||
					if (abs(f) > abs(peak_value))
 | 
			
		||||
					{
 | 
			
		||||
						peak_value = f;
 | 
			
		||||
						timing_index_ = index;
 | 
			
		||||
					}
 | 
			
		||||
					index += 1;
 | 
			
		||||
				}
 | 
			
		||||
				updated_ = peak_value > 0 ? 1 : -1;
 | 
			
		||||
				find_peak(value);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return timing_index_;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -68,7 +68,7 @@ struct DataCarrierDetect
 | 
			
		|||
        triggered_ = triggered_ ? level_ > ltrigger_ : level_ > htrigger_;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    void unlock() { triggered_ = false; }
 | 
			
		||||
    FloatType level() const { return level_; }
 | 
			
		||||
    bool dcd() const { return triggered_; }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,127 +3,54 @@
 | 
			
		|||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "IirFilter.h"
 | 
			
		||||
#include "KalmanFilter.h"
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cmath>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
 | 
			
		||||
namespace mobilinkd {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Deviation and zero-offset estimator.
 | 
			
		||||
 *
 | 
			
		||||
 * Accepts samples which are periodically used to update estimates of the
 | 
			
		||||
 * input signal deviation and zero offset.
 | 
			
		||||
 *
 | 
			
		||||
 * Samples must be provided at the ideal sample point (the point with the
 | 
			
		||||
 * peak bit energy).
 | 
			
		||||
 *
 | 
			
		||||
 * Estimates are expected to be updated at each sync word.  But they can
 | 
			
		||||
 * be updated more frequently, such as during the preamble.
 | 
			
		||||
 */
 | 
			
		||||
template <typename FloatType>
 | 
			
		||||
class FreqDevEstimator
 | 
			
		||||
{
 | 
			
		||||
    using sample_filter_t = BaseIirFilter<FloatType, 3>;
 | 
			
		||||
    static constexpr FloatType DEVIATION = 2400.;
 | 
			
		||||
 | 
			
		||||
    // IIR with Nyquist of 1/4.
 | 
			
		||||
    static constexpr std::array<FloatType, 3> dc_b = { 0.09763107,  0.19526215,  0.09763107 };
 | 
			
		||||
    static constexpr std::array<FloatType, 3> dc_a = { 1.        , -0.94280904,  0.33333333 };
 | 
			
		||||
 | 
			
		||||
    static constexpr FloatType MAX_DC_ERROR = 0.2;
 | 
			
		||||
 | 
			
		||||
    FloatType min_est_ = 0.0;
 | 
			
		||||
	FloatType max_est_ = 0.0;
 | 
			
		||||
	FloatType min_cutoff_ = 0.0;
 | 
			
		||||
	FloatType max_cutoff_ = 0.0;
 | 
			
		||||
	FloatType min_var_ = 0.0;
 | 
			
		||||
	FloatType max_var_ = 0.0;
 | 
			
		||||
	size_t min_count_ = 0;
 | 
			
		||||
	size_t max_count_ = 0;
 | 
			
		||||
	FloatType deviation_ = 0.0;
 | 
			
		||||
	FloatType offset_ = 0.0;
 | 
			
		||||
	FloatType error_ = 0.0;
 | 
			
		||||
	FloatType idev_ = 1.0;
 | 
			
		||||
    sample_filter_t dc_filter_{dc_b, dc_a};
 | 
			
		||||
    m17::SymbolKalmanFilter<FloatType> minFilter_;
 | 
			
		||||
    m17::SymbolKalmanFilter<FloatType> maxFilter_;
 | 
			
		||||
    FloatType idev_ = 0.;
 | 
			
		||||
    FloatType offset_ = 0.;
 | 
			
		||||
    bool reset_ = true;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
 | 
			
		||||
	void reset()
 | 
			
		||||
	{
 | 
			
		||||
		min_est_ = 0.0;
 | 
			
		||||
		max_est_ = 0.0;
 | 
			
		||||
		min_var_ = 0.0;
 | 
			
		||||
		max_var_ = 0.0;
 | 
			
		||||
		min_count_ = 0;
 | 
			
		||||
		max_count_ = 0;
 | 
			
		||||
		min_cutoff_ = 0.0;
 | 
			
		||||
		max_cutoff_ = 0.0;
 | 
			
		||||
	}
 | 
			
		||||
    void reset()
 | 
			
		||||
    {
 | 
			
		||||
        reset_ = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	void sample(FloatType sample)
 | 
			
		||||
	{
 | 
			
		||||
		if (sample < 1.5 * min_est_)
 | 
			
		||||
		{
 | 
			
		||||
			min_count_ = 1;
 | 
			
		||||
			min_est_ = sample;
 | 
			
		||||
			min_var_ = 0.0;
 | 
			
		||||
			min_cutoff_ = min_est_ * 0.666666;
 | 
			
		||||
		}
 | 
			
		||||
		else if (sample < min_cutoff_)
 | 
			
		||||
		{
 | 
			
		||||
			min_count_ += 1;
 | 
			
		||||
			min_est_ += sample;
 | 
			
		||||
			FloatType var = (min_est_ / min_count_) - sample;
 | 
			
		||||
			min_var_ += var * var;
 | 
			
		||||
		}
 | 
			
		||||
		else if (sample > 1.5 * max_est_)
 | 
			
		||||
		{
 | 
			
		||||
			max_count_ = 1;
 | 
			
		||||
			max_est_ = sample;
 | 
			
		||||
			max_var_ = 0.0;
 | 
			
		||||
			max_cutoff_ = max_est_ * 0.666666;
 | 
			
		||||
		}
 | 
			
		||||
		else if (sample > max_cutoff_)
 | 
			
		||||
		{
 | 
			
		||||
			max_count_ += 1;
 | 
			
		||||
			max_est_ += sample;
 | 
			
		||||
			FloatType var = (max_est_ / max_count_) - sample;
 | 
			
		||||
			max_var_ += var * var;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
    void update(FloatType minValue, FloatType maxValue)
 | 
			
		||||
    {
 | 
			
		||||
        auto mnf = minFilter_.update(minValue, 192);
 | 
			
		||||
        auto mxf = maxFilter_.update(maxValue, 192);
 | 
			
		||||
        offset_ = (mxf[0] + mnf[0]) / 2.;
 | 
			
		||||
        idev_ = 6.0 / (mxf[0] - mnf[0]);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Update the estimates for deviation, offset, and EVM (error).  Note
 | 
			
		||||
	 * that the estimates for error are using a sloppy implementation for
 | 
			
		||||
	 * calculating variance to reduce the memory requirements.  This is
 | 
			
		||||
	 * because this is designed for embedded use.
 | 
			
		||||
	 */
 | 
			
		||||
	void update()
 | 
			
		||||
	{
 | 
			
		||||
		if (max_count_ < 2 || min_count_ < 2) return;
 | 
			
		||||
		FloatType max_ = max_est_ / max_count_;
 | 
			
		||||
		FloatType min_ = min_est_ / min_count_;
 | 
			
		||||
		deviation_ = (max_ - min_) / 6.0;
 | 
			
		||||
		offset_ =  dc_filter_(std::max(std::min(max_ + min_, deviation_ * MAX_DC_ERROR), deviation_ * -MAX_DC_ERROR));
 | 
			
		||||
		error_ = (std::sqrt(max_var_ / (max_count_ - 1)) + std::sqrt(min_var_ / (min_count_ - 1))) * 0.5;
 | 
			
		||||
		if (deviation_ > 0) idev_ = 1.0 / deviation_;
 | 
			
		||||
		min_cutoff_ = offset_ - deviation_ * 2;
 | 
			
		||||
		max_cutoff_ = offset_ + deviation_ * 2;
 | 
			
		||||
		max_est_ = max_;
 | 
			
		||||
		min_est_ = min_;
 | 
			
		||||
		max_count_ = 1;
 | 
			
		||||
		min_count_ = 1;
 | 
			
		||||
		max_var_ = 0.0;
 | 
			
		||||
		min_var_ = 0.0;
 | 
			
		||||
	}
 | 
			
		||||
        if (isnan(mnf) || isnan(mxf)) reset_ = true;
 | 
			
		||||
 | 
			
		||||
	FloatType deviation() const { return deviation_; }
 | 
			
		||||
	FloatType offset() const { return offset_; }
 | 
			
		||||
	FloatType error() const { return error_; }
 | 
			
		||||
	FloatType idev() const { return idev_; }
 | 
			
		||||
        if (reset_)
 | 
			
		||||
        {
 | 
			
		||||
            reset_ = false;
 | 
			
		||||
            minFilter_.reset(minValue);
 | 
			
		||||
            maxFilter_.reset(maxValue);
 | 
			
		||||
            offset_ = (minValue + maxValue) / 2;
 | 
			
		||||
            idev_ = 6.0 / (maxValue - minValue);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    FloatType idev() const { return idev_; }
 | 
			
		||||
    FloatType offset() const { return offset_; }
 | 
			
		||||
    FloatType deviation() const { return DEVIATION / idev_; }
 | 
			
		||||
    FloatType error() const { return 0.; }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // mobilinkd
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,111 @@
 | 
			
		|||
// Copyright 2022 Mobilinkd LLC.
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wfloat-equal"
 | 
			
		||||
 | 
			
		||||
#include <blaze/math/Matrix.h>
 | 
			
		||||
#include <blaze/math/Vector.h>
 | 
			
		||||
#include <blaze/Math.h>
 | 
			
		||||
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
 | 
			
		||||
#include <cmath>
 | 
			
		||||
 | 
			
		||||
namespace mobilinkd { namespace m17 {
 | 
			
		||||
 | 
			
		||||
template <typename FloatType, size_t SamplesPerSymbol>
 | 
			
		||||
struct KalmanFilter
 | 
			
		||||
{
 | 
			
		||||
    blaze::StaticVector<FloatType, 2> x;
 | 
			
		||||
    blaze::StaticMatrix<FloatType, 2, 2> P;
 | 
			
		||||
    blaze::StaticMatrix<FloatType, 2, 2> F;
 | 
			
		||||
    blaze::StaticMatrix<FloatType, 1, 2> H = {{1., 0.}};
 | 
			
		||||
    blaze::StaticMatrix<FloatType, 1, 1> R = {{0.5}};
 | 
			
		||||
    blaze::StaticMatrix<FloatType, 2, 2> Q = {{6.25e-13, 1.25e-12},{1.25e-12, 2.50e-12}};
 | 
			
		||||
 | 
			
		||||
    KalmanFilter()
 | 
			
		||||
    {
 | 
			
		||||
        reset(0.);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void reset(FloatType z)
 | 
			
		||||
    {
 | 
			
		||||
        x = {z, 0.};
 | 
			
		||||
        P = {{4., 0.}, {0., 0.00000025}};
 | 
			
		||||
        F = {{1., 1.}, {0., 1.}};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [[gnu::noinline]]
 | 
			
		||||
    auto update(FloatType z, size_t dt)
 | 
			
		||||
    {
 | 
			
		||||
        F(0,1) = FloatType(dt);
 | 
			
		||||
 | 
			
		||||
        x = F * x;
 | 
			
		||||
        P = F * P * blaze::trans(F) + Q;
 | 
			
		||||
        auto S = H * P * blaze::trans(H) + R;
 | 
			
		||||
        auto K = P * blaze::trans(H) * (1.0 / S(0, 0));
 | 
			
		||||
 | 
			
		||||
        // Normalize incoming index
 | 
			
		||||
        if (z - x[0] < (SamplesPerSymbol / -2.0))
 | 
			
		||||
            z += SamplesPerSymbol;
 | 
			
		||||
        else if (z - x[0] > (SamplesPerSymbol / 2.0))
 | 
			
		||||
            z-= SamplesPerSymbol;
 | 
			
		||||
 | 
			
		||||
        auto y = z - H * x;
 | 
			
		||||
 | 
			
		||||
        x += K * y;
 | 
			
		||||
 | 
			
		||||
        // Normalize the filtered sample point
 | 
			
		||||
        while (x[0] >= SamplesPerSymbol) x[0] -= SamplesPerSymbol;
 | 
			
		||||
        while (x[0] < 0) x[0] += SamplesPerSymbol;
 | 
			
		||||
        P = P - K * H * P;
 | 
			
		||||
        return x;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <typename FloatType>
 | 
			
		||||
struct SymbolKalmanFilter
 | 
			
		||||
{
 | 
			
		||||
    blaze::StaticVector<FloatType, 2> x;
 | 
			
		||||
    blaze::StaticMatrix<FloatType, 2, 2> P;
 | 
			
		||||
    blaze::StaticMatrix<FloatType, 2, 2> F;
 | 
			
		||||
    blaze::StaticMatrix<FloatType, 1, 2> H = {{1., 0.}};
 | 
			
		||||
    blaze::StaticMatrix<FloatType, 1, 1> R = {{0.5}};
 | 
			
		||||
    blaze::StaticMatrix<FloatType, 2, 2> Q = {{6.25e-13, 1.25e-12},{1.25e-12, 2.50e-12}};
 | 
			
		||||
 | 
			
		||||
    SymbolKalmanFilter()
 | 
			
		||||
    {
 | 
			
		||||
        reset(0.);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void reset(FloatType z)
 | 
			
		||||
    {
 | 
			
		||||
        x = {z, 0.};
 | 
			
		||||
        P = {{4., 0.}, {0., 0.00000025}};
 | 
			
		||||
        F = {{1., 1.}, {0., 1.}};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [[gnu::noinline]]
 | 
			
		||||
    auto update(FloatType z, size_t dt)
 | 
			
		||||
    {
 | 
			
		||||
        F(0,1) = FloatType(dt);
 | 
			
		||||
 | 
			
		||||
        x = F * x;
 | 
			
		||||
        P = F * P * blaze::trans(F) + Q;
 | 
			
		||||
        auto S = H * P * blaze::trans(H) + R;
 | 
			
		||||
        auto K = P * blaze::trans(H) * (1.0 / S(0, 0));
 | 
			
		||||
 | 
			
		||||
        auto y = z - H * x;
 | 
			
		||||
 | 
			
		||||
        x += K * y;
 | 
			
		||||
 | 
			
		||||
        // Normalize the filtered sample point
 | 
			
		||||
        P = P - K * H * P;
 | 
			
		||||
        return x;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}} // mobilinkd::m17
 | 
			
		||||
										
											
												Plik diff jest za duży
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -246,7 +246,7 @@ struct M17FrameDecoder
 | 
			
		|||
 | 
			
		||||
        if (checksum == 0)
 | 
			
		||||
        {
 | 
			
		||||
        	lich_segments = 0;
 | 
			
		||||
            lich_segments = 0;
 | 
			
		||||
            state_ = State::STREAM;
 | 
			
		||||
            viterbi_cost = 0;
 | 
			
		||||
            output_buffer.type = FrameType::LSF;
 | 
			
		||||
| 
						 | 
				
			
			@ -282,16 +282,10 @@ struct M17FrameDecoder
 | 
			
		|||
        viterbi_cost = viterbi_.decode(depuncture_buffer.stream, decode_buffer.stream);
 | 
			
		||||
        to_byte_array(decode_buffer.stream, output_buffer.stream);
 | 
			
		||||
 | 
			
		||||
        if ((viterbi_cost < 60) && (output_buffer.stream[0] & 0x80))
 | 
			
		||||
        {
 | 
			
		||||
            // fputs("\nEOS\n", stderr);
 | 
			
		||||
            state_ = State::LSF;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        output_buffer.type = FrameType::STREAM;
 | 
			
		||||
        callback_(output_buffer, viterbi_cost);
 | 
			
		||||
 | 
			
		||||
        return state_ == State::LSF ? DecodeResult::EOS : DecodeResult::OK;
 | 
			
		||||
        return DecodeResult::OK;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,85 @@
 | 
			
		|||
// Copyright 2020-2022 Mobilinkd LLC <rob@mobilinkd.com>
 | 
			
		||||
// All rights reserved.
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <cmath>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
 | 
			
		||||
namespace mobilinkd {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Compute a running standard deviation. Avoids having to store
 | 
			
		||||
 * all of the samples.
 | 
			
		||||
 *
 | 
			
		||||
 * Based on https://dsp.stackexchange.com/a/1187/36581
 | 
			
		||||
 */
 | 
			
		||||
template <typename FloatType>
 | 
			
		||||
struct StandardDeviation
 | 
			
		||||
{
 | 
			
		||||
    FloatType mean{0.0};
 | 
			
		||||
    FloatType S{0.0};
 | 
			
		||||
    size_t samples{0};
 | 
			
		||||
 | 
			
		||||
    void reset()
 | 
			
		||||
    {
 | 
			
		||||
        mean = 0.0;
 | 
			
		||||
        S = 0.0;
 | 
			
		||||
        samples = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void capture(float sample)
 | 
			
		||||
    {
 | 
			
		||||
        auto prev = mean;
 | 
			
		||||
        samples += 1;
 | 
			
		||||
        mean = mean + (sample - mean) / samples;
 | 
			
		||||
        S = S + (sample - mean) * (sample - prev);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    FloatType variance() const
 | 
			
		||||
    {
 | 
			
		||||
        return samples == 0 ? -1.0 :  S / samples;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    FloatType stdev() const
 | 
			
		||||
    {
 | 
			
		||||
        FloatType result = -1.0;
 | 
			
		||||
        if (samples) result = std::sqrt(variance());
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // SNR in dB
 | 
			
		||||
    FloatType SNR() const
 | 
			
		||||
    {
 | 
			
		||||
        return 10.0 * std::log10(mean / stdev());
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
template <typename FloatType, size_t N>
 | 
			
		||||
struct RunningStandardDeviation
 | 
			
		||||
{
 | 
			
		||||
    FloatType S{1.0};
 | 
			
		||||
    FloatType alpha{1.0 / N};
 | 
			
		||||
 | 
			
		||||
    void reset()
 | 
			
		||||
    {
 | 
			
		||||
        S = 0.0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void capture(float sample)
 | 
			
		||||
    {
 | 
			
		||||
        S -= S * alpha;
 | 
			
		||||
        S += (sample * sample) * alpha;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    FloatType variance() const
 | 
			
		||||
    {
 | 
			
		||||
        return S;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    FloatType stdev() const
 | 
			
		||||
    {
 | 
			
		||||
        return std::sqrt(variance());
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // mobilinkd
 | 
			
		||||
| 
						 | 
				
			
			@ -1,83 +1,54 @@
 | 
			
		|||
// Copyright 2020 Mobilinkd LLC.
 | 
			
		||||
// Copyright 2020-2022 Mobilinkd LLC.
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "IirFilter.h"
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <numeric>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <tuple>
 | 
			
		||||
#include "StandardDeviation.h"
 | 
			
		||||
 | 
			
		||||
namespace mobilinkd
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
template <typename FloatType, size_t N>
 | 
			
		||||
/**
 | 
			
		||||
 * Compute the EVM of a symbol. This assumes that the incoming sample has
 | 
			
		||||
 * been normalized, meaning its offset and scale has been adjusted so that
 | 
			
		||||
 * nominal values fall exactly on the symbol values of -3, -1, 1, 3. It
 | 
			
		||||
 * determines the nearest symbol value and computes the variance.
 | 
			
		||||
 * 
 | 
			
		||||
 * This uses a running standard deviation with a nominal length of 184
 | 
			
		||||
 * symbols. That is the payload size of an M17 frame.
 | 
			
		||||
 */
 | 
			
		||||
template <typename FloatType>
 | 
			
		||||
struct SymbolEvm
 | 
			
		||||
{
 | 
			
		||||
    using filter_type = BaseIirFilter<FloatType, N>;
 | 
			
		||||
    using symbol_t = int;
 | 
			
		||||
    using result_type = std::tuple<symbol_t, FloatType>;
 | 
			
		||||
 | 
			
		||||
    filter_type filter_;
 | 
			
		||||
    std::optional<FloatType> erasure_limit_;
 | 
			
		||||
    FloatType evm_ = 0.0;
 | 
			
		||||
 | 
			
		||||
    SymbolEvm(filter_type&& filter, std::optional<FloatType> erasure_limit = std::nullopt)
 | 
			
		||||
    : filter_(std::forward<filter_type>(filter))
 | 
			
		||||
    , erasure_limit_(erasure_limit)
 | 
			
		||||
    {}
 | 
			
		||||
    
 | 
			
		||||
    FloatType evm() const { return evm_; }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Decode a normalized sample into a symbol.  Symbols
 | 
			
		||||
     * are decoded into +3, +1, -1, -3.  If an erasure limit
 | 
			
		||||
     * is set, symbols outside this limit are 'erased' and
 | 
			
		||||
     * returned as 0.
 | 
			
		||||
     */
 | 
			
		||||
    result_type operator()(FloatType sample)
 | 
			
		||||
    RunningStandardDeviation<FloatType, 184> stddev;
 | 
			
		||||
   
 | 
			
		||||
    void reset()
 | 
			
		||||
    {
 | 
			
		||||
        symbol_t symbol;
 | 
			
		||||
        FloatType evm;
 | 
			
		||||
        stddev.reset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        sample = std::min(3.0, std::max(-3.0, sample));
 | 
			
		||||
    FloatType evm() const { return stddev.stdev(); }
 | 
			
		||||
 | 
			
		||||
    void update(FloatType sample)
 | 
			
		||||
    {
 | 
			
		||||
        FloatType evm;
 | 
			
		||||
 | 
			
		||||
        if (sample > 2)
 | 
			
		||||
        {
 | 
			
		||||
            symbol = 3;
 | 
			
		||||
            evm = (sample - 3) * 0.333333;
 | 
			
		||||
            stddev.capture(sample - 3);
 | 
			
		||||
        }
 | 
			
		||||
        else if (sample > 0)
 | 
			
		||||
        {
 | 
			
		||||
            symbol = 1;
 | 
			
		||||
            evm = sample - 1;
 | 
			
		||||
            stddev.capture(sample - 1);
 | 
			
		||||
        }
 | 
			
		||||
        else if (sample >= -2)
 | 
			
		||||
        else if (sample > -2)
 | 
			
		||||
        {
 | 
			
		||||
            symbol = -1;
 | 
			
		||||
            evm = sample + 1;
 | 
			
		||||
            stddev.capture(sample + 1);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            symbol = -3;
 | 
			
		||||
            evm = (sample + 3) * 0.333333;
 | 
			
		||||
            stddev.capture(sample + 3);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (erasure_limit_ and (abs(evm) > *erasure_limit_)) symbol = 0;
 | 
			
		||||
        
 | 
			
		||||
        evm_ = filter_(evm);
 | 
			
		||||
        
 | 
			
		||||
        return std::make_tuple(symbol, evm);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <typename FloatType, size_t N>
 | 
			
		||||
SymbolEvm<FloatType, N> makeSymbolEvm(
 | 
			
		||||
    BaseIirFilter<FloatType, N>&& filter, std::optional<FloatType> erasure_limit = std::nullopt)
 | 
			
		||||
{
 | 
			
		||||
    return std::move(SymbolEvm<FloatType, N>(std::move(filter), erasure_limit));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // mobilinkd
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <cstdlib>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <cassert>
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <bitset>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,9 +21,10 @@ class ClockRecoveryTest : public ::testing::Test {
 | 
			
		|||
 | 
			
		||||
TEST_F(ClockRecoveryTest, construct)
 | 
			
		||||
{
 | 
			
		||||
    auto cr = mobilinkd::ClockRecovery<float, 48000, 4800>();
 | 
			
		||||
    auto cr = mobilinkd::ClockRecovery<float, 10>();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# if 0
 | 
			
		||||
TEST_F(ClockRecoveryTest, recover_preamble)
 | 
			
		||||
{
 | 
			
		||||
    // 2400Hz sine wave -- same as M17 preamble.
 | 
			
		||||
| 
						 | 
				
			
			@ -113,3 +114,5 @@ TEST_F(ClockRecoveryTest, all_phases)
 | 
			
		|||
        EXPECT_EQ(int(cr.sample_index()), expected[p]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,31 +25,11 @@ TEST_F(FreqDevEstimatorTest, construct)
 | 
			
		|||
 | 
			
		||||
TEST_F(FreqDevEstimatorTest, fde_preamble)
 | 
			
		||||
{
 | 
			
		||||
    constexpr std::array<float, 24> input = {1,1,1,1,1,1,1,1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
 | 
			
		||||
 | 
			
		||||
    auto fde = mobilinkd::FreqDevEstimator<float>();
 | 
			
		||||
    std::for_each(input.begin(), input.end(), [&fde](float x){fde.sample(x * 3);});
 | 
			
		||||
    std::for_each(input.begin(), input.end(), [&fde](float x){fde.sample(x * 3);});
 | 
			
		||||
    std::for_each(input.begin(), input.end(), [&fde](float x){fde.sample(x * 3);});
 | 
			
		||||
    fde.update(-3, 3);
 | 
			
		||||
    fde.update(-3, 3);
 | 
			
		||||
    fde.update(-3, 3);
 | 
			
		||||
 | 
			
		||||
    fde.update();
 | 
			
		||||
 | 
			
		||||
    EXPECT_NEAR(fde.deviation(), 1, .1);
 | 
			
		||||
    EXPECT_NEAR(fde.error(), 0, .1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(FreqDevEstimatorTest, fde_mixed)
 | 
			
		||||
{
 | 
			
		||||
    constexpr std::array<float, 16> input = {1,1,1,1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,-1};
 | 
			
		||||
 | 
			
		||||
    auto fde = mobilinkd::FreqDevEstimator<float>();
 | 
			
		||||
    std::for_each(input.begin(), input.end(), [&fde](float x){fde.sample(x * 3);});
 | 
			
		||||
    std::for_each(input.begin(), input.end(), [&fde](float x){fde.sample(x);});
 | 
			
		||||
    std::for_each(input.begin(), input.end(), [&fde](float x){fde.sample(x * 3);});
 | 
			
		||||
    std::for_each(input.begin(), input.end(), [&fde](float x){fde.sample(x);});
 | 
			
		||||
 | 
			
		||||
    fde.update();
 | 
			
		||||
 | 
			
		||||
    EXPECT_NEAR(fde.deviation(), 1, .1);
 | 
			
		||||
    EXPECT_NEAR(fde.deviation(), 2400, .1);
 | 
			
		||||
    EXPECT_NEAR(fde.error(), 0, .1);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue