js8call/JS8_Mode/Detector.cpp

238 lines
7.2 KiB
C++
Raw Normal View History

2026-01-09 12:48:09 -05:00
/**
* @file Detector.cpp
* @brief Implementation of Detector class
2026-01-18 12:53:55 -06:00
* Output device that distributes data in predefined chunks via a signal
2026-01-09 12:48:09 -05:00
*/
2026-01-18 12:53:55 -06:00
#include "Detector.h"
#include "JS8_Include/commons.h"
#include "JS8_Main/DriftingDateTime.h"
2026-01-18 12:53:55 -06:00
#include <QDateTime>
#include <QLoggingCategory>
#include <QMutexLocker>
#include <QtAlgorithms>
2026-01-18 12:53:55 -06:00
#include <algorithm>
#include <cmath>
/******************************************************************************/
// FIR Filter Coefficients
/******************************************************************************/
namespace {
// Filter coefficients for an FIR lowpass filter designed using ScopeFIR.
//
// fsample = 48000 Hz
// Ntaps = 49
// fc = 4500 Hz
// fstop = 6000 Hz
// Ripple = 1 dB
// Stop Atten = 40 dB
// fout = 12000 Hz
constexpr std::array LOWPASS{
0.000861074040f, 0.010051920210f, 0.010161983649f, 0.011363155076f,
0.008706594219f, 0.002613872664f, -0.005202883094f, -0.011720748164f,
-0.013752163325f, -0.009431602741f, 0.000539063909f, 0.012636767098f,
0.021494659597f, 0.021951235065f, 0.011564169382f, -0.007656470131f,
-0.028965787341f, -0.042637874109f, -0.039203309748f, -0.013153301537f,
0.034320769178f, 0.094717832646f, 0.154224604789f, 0.197758325022f,
0.213715139513f, 0.197758325022f, 0.154224604789f, 0.094717832646f,
0.034320769178f, -0.013153301537f, -0.039203309748f, -0.042637874109f,
-0.028965787341f, -0.007656470131f, 0.011564169382f, 0.021951235065f,
0.021494659597f, 0.012636767098f, 0.000539063909f, -0.009431602741f,
-0.013752163325f, -0.011720748164f, -0.005202883094f, 0.002613872664f,
0.008706594219f, 0.011363155076f, 0.010161983649f, 0.010051920210f,
0.000861074040f};
} // namespace
/******************************************************************************/
// Implementation
/******************************************************************************/
#include "moc_Detector.cpp"
Q_DECLARE_LOGGING_CATEGORY(detector_js8)
2026-01-09 12:48:09 -05:00
/**
* @brief Construct a new Detector object
2026-01-18 12:53:55 -06:00
*
* @param frameRate
* @param periodLengthInSeconds
* @param parent
2026-01-09 12:48:09 -05:00
*/
Detector::Detector(unsigned frameRate, unsigned periodLengthInSeconds,
QObject *parent)
: AudioDevice(parent), m_frameRate(frameRate),
m_period(periodLengthInSeconds), m_filter(LOWPASS) {
clear();
}
2026-01-09 12:48:09 -05:00
/**
* @brief Set the block size for FFT processing
2026-01-18 12:53:55 -06:00
*
* @param n
2026-01-09 12:48:09 -05:00
*/
void Detector::setBlockSize(unsigned n) { m_samplesPerFFT = n; }
2026-01-09 12:48:09 -05:00
/**
* @brief Reset the detector state
2026-01-18 12:53:55 -06:00
*
* @return true
* @return false
2026-01-09 12:48:09 -05:00
*/
bool Detector::reset() {
clear();
// don't call base call reset because it calls seek(0) which causes
// a warning
return isOpen();
}
2026-01-09 12:48:09 -05:00
/**
* @brief Clear the detector buffer
2026-01-18 12:53:55 -06:00
*
2026-01-09 12:48:09 -05:00
*/
void Detector::clear() {
#if JS8_RING_BUFFER
resetBufferPosition();
resetBufferContent();
#else
dec_data.params.kin = 0;
m_bufferPos = 0;
#endif
// fill buffer with zeros (G4WJS commented out because it might cause
// decoder hangs) qFill (dec_data.d2, dec_data.d2 + sizeof (dec_data.d2) /
// sizeof (dec_data.d2[0]), 0);
}
2026-01-09 12:48:09 -05:00
/**
* @brief Reset the buffer position based on current time
2026-01-18 12:53:55 -06:00
*
2026-01-09 12:48:09 -05:00
*/
void Detector::resetBufferPosition() {
QMutexLocker mutex(&m_lock);
// set index to roughly where we are in time (1ms resolution)
qint64 const now = DriftingDateTime::currentMSecsSinceEpoch();
unsigned const msInPeriod = (now % 86400000LL) % (m_period * 1000);
int const prevKin = dec_data.params.kin;
dec_data.params.kin = qMin(
(msInPeriod * m_frameRate) / 1000,
static_cast<unsigned>(sizeof(dec_data.d2) / sizeof(dec_data.d2[0])));
m_bufferPos = 0;
m_ns = secondInPeriod();
int const delta = dec_data.params.kin - prevKin;
qCDebug(detector_js8) << "advancing detector buffer from" << prevKin << "to"
<< dec_data.params.kin << "delta" << delta;
// rotate buffer moving the contents that were at prevKin to the new kin
// position
if (delta < 0) {
std::rotate(std::begin(dec_data.d2), std::begin(dec_data.d2) - delta,
std::end(dec_data.d2));
} else {
std::rotate(std::rbegin(dec_data.d2), std::rbegin(dec_data.d2) + delta,
std::rend(dec_data.d2));
}
}
2026-01-09 12:48:09 -05:00
/**
* @brief Reset the buffer content to zero
2026-01-18 12:53:55 -06:00
*
2026-01-09 12:48:09 -05:00
*/
void Detector::resetBufferContent() {
QMutexLocker mutex(&m_lock);
std::fill(std::begin(dec_data.d2), std::end(dec_data.d2), 0);
qCDebug(detector_js8) << "clearing detector buffer content";
}
2026-01-09 12:48:09 -05:00
/**
* @brief Write data to the detector buffer
2026-01-18 12:53:55 -06:00
*
* @param data
* @param maxSize
* @return qint64
2026-01-09 12:48:09 -05:00
*/
qint64 Detector::writeData(char const *const data, qint64 const maxSize) {
QMutexLocker mutex(&m_lock);
// When ns has wrapped around to zero, restart the buffers.
int const ns = secondInPeriod();
if (ns < m_ns) {
dec_data.params.kin = 0;
m_bufferPos = 0;
}
m_ns = ns;
// No torn frames.
Q_ASSERT(!(maxSize % static_cast<qint64>(bytesPerFrame())));
// These are in terms of input frames (not down sampled).
size_t const framesAcceptable =
(sizeof(dec_data.d2) / sizeof(dec_data.d2[0]) - dec_data.params.kin) *
Filter::NDOWN;
size_t const framesAccepted =
qMin(static_cast<size_t>(maxSize / bytesPerFrame()), framesAcceptable);
if (framesAccepted < static_cast<size_t>(maxSize / bytesPerFrame())) {
qCDebug(detector_js8)
<< "dropped " << maxSize / bytesPerFrame() - framesAccepted
<< " frames of data on the floor!" << dec_data.params.kin << ns;
}
for (auto remaining = framesAccepted; remaining;) {
size_t const numFramesProcessed =
qMin(m_samplesPerFFT * Filter::NDOWN - m_bufferPos, remaining);
store(&data[(framesAccepted - remaining) * bytesPerFrame()],
numFramesProcessed, &m_buffer[m_bufferPos]);
m_bufferPos += numFramesProcessed;
if (m_bufferPos == m_samplesPerFFT * Filter::NDOWN) {
if (dec_data.params.kin >= 0 &&
dec_data.params.kin <
static_cast<int>(JS8_NTMAX * 12000 - m_samplesPerFFT)) {
for (std::size_t i = 0; i < m_samplesPerFFT; ++i) {
dec_data.d2[dec_data.params.kin++] =
m_filter.downSample(&m_buffer[i * Filter::NDOWN]);
}
}
Q_EMIT framesWritten(dec_data.params.kin);
m_bufferPos = 0;
}
remaining -= numFramesProcessed;
}
// We drop any data past the end of the buffer on the floor
// until the next period starts
return maxSize;
}
2026-01-09 12:48:09 -05:00
/**
* @brief Get the current second in the period
2026-01-18 12:53:55 -06:00
*
* @return unsigned
2026-01-09 12:48:09 -05:00
*/
unsigned Detector::secondInPeriod() const {
// we take the time of the data as the following assuming no latency
// delivering it to us (not true but close enough for us)
qint64 now(DriftingDateTime::currentMSecsSinceEpoch());
unsigned secondInToday((now % 86400000LL) / 1000);
return secondInToday % m_period;
}
/******************************************************************************/
Q_LOGGING_CATEGORY(detector_js8, "detector.js8", QtWarningMsg)