mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
123 lines
3.4 KiB
C++
123 lines
3.4 KiB
C++
#ifndef DETECTOR_HPP__
|
|
#define DETECTOR_HPP__
|
|
|
|
#include "JS8_Audio/AudioDevice.h"
|
|
|
|
#include <QMutex>
|
|
#include <vendor/Eigen/Dense>
|
|
|
|
#include <array>
|
|
|
|
// Output device that distributes data in predefined chunks via a signal;
|
|
// underlying device for this abstraction is just the buffer that stores
|
|
// samples throughout a receiving period.
|
|
|
|
class Detector : public AudioDevice {
|
|
Q_OBJECT;
|
|
|
|
// We downsample the input data from 48kHz to 12kHz through this
|
|
// lowpass FIR filter.
|
|
|
|
class Filter final {
|
|
public:
|
|
// Amount we're going to downsample; a factor of 4, i.e., 48kHz to
|
|
// 12kHz, and number of taps in the FIR lowpass filter we're going
|
|
// to use for the downsample process. These together result in the
|
|
// amount to shift data in the FIR filter each time we input a new
|
|
// sample.
|
|
|
|
static constexpr std::size_t NDOWN = 48 / 12;
|
|
static constexpr std::size_t NTAPS = 49;
|
|
static constexpr std::size_t SHIFT = NTAPS - NDOWN;
|
|
|
|
// Our FIR is constructed of a pair of Eigen vectors, each NTAPS in
|
|
// size. Loading in a sample consists of mapping it to a read-only
|
|
// view of an Eigen vector, NDOWN in size.
|
|
|
|
using Vector = Eigen::Vector<float, NTAPS>;
|
|
using Sample = Eigen::Map<Eigen::Vector<short, NDOWN> const>;
|
|
|
|
// Constructor; we require an array of lowpass FIR coefficients,
|
|
// equal in size to the number of taps.
|
|
|
|
explicit Filter(std::array<Vector::value_type, NTAPS> const &lowpass)
|
|
: m_w(lowpass.data()), m_t(Vector::Zero()) {}
|
|
|
|
// Shift existing data in the lowpass FIR to make room for a new
|
|
// sample and load it in; downsample through the filter.
|
|
|
|
auto downSample(Sample::value_type const *const data) {
|
|
m_t.head(SHIFT) = m_t.segment(NDOWN, SHIFT);
|
|
m_t.tail(NDOWN) = Sample(data).cast<Vector::value_type>();
|
|
|
|
return static_cast<Sample::value_type>(std::round(m_w.dot(m_t)));
|
|
}
|
|
|
|
private:
|
|
// Data members
|
|
|
|
Eigen::Map<Vector const> m_w;
|
|
Vector m_t;
|
|
};
|
|
|
|
// Size of a maximally-sized buffer.
|
|
|
|
static constexpr std::size_t MaxBufferSize = 7 * 512;
|
|
|
|
// A De-interleaved sample buffer big enough for all the
|
|
// samples for one increment of data (a signals worth) at
|
|
// the input sample rate.
|
|
|
|
using Buffer = std::array<short, MaxBufferSize * Filter::NDOWN>;
|
|
|
|
public:
|
|
// Constructor
|
|
|
|
Detector(unsigned frameRate, unsigned periodLengthInSeconds,
|
|
QObject *parent = nullptr);
|
|
|
|
// Inline accessors
|
|
|
|
unsigned period() const { return m_period; }
|
|
|
|
// Inline manipulators
|
|
|
|
QMutex *getMutex() { return &m_lock; }
|
|
void setTRPeriod(unsigned p) { m_period = p; }
|
|
|
|
// Accessors
|
|
|
|
unsigned secondInPeriod() const;
|
|
|
|
// Manipulators
|
|
|
|
void clear();
|
|
bool reset() override;
|
|
void resetBufferContent();
|
|
void resetBufferPosition();
|
|
|
|
// Signals and slots
|
|
|
|
Q_SIGNAL void framesWritten(qint64) const;
|
|
Q_SLOT void setBlockSize(unsigned);
|
|
|
|
protected:
|
|
// We don't produce data; we're a sink for it.
|
|
|
|
qint64 readData(char *, qint64) override { return -1; }
|
|
qint64 writeData(char const *, qint64) override;
|
|
|
|
private:
|
|
// Data members
|
|
|
|
unsigned m_frameRate;
|
|
unsigned m_period;
|
|
QMutex m_lock;
|
|
Filter m_filter;
|
|
Buffer m_buffer;
|
|
Buffer::size_type m_bufferPos = 0;
|
|
std::size_t m_samplesPerFFT = MaxBufferSize;
|
|
qint32 m_ns = 999;
|
|
};
|
|
|
|
#endif
|