diff --git a/openrtx/include/core/ctcssDetector.hpp b/openrtx/include/core/ctcssDetector.hpp new file mode 100644 index 00000000..a920ba05 --- /dev/null +++ b/openrtx/include/core/ctcssDetector.hpp @@ -0,0 +1,151 @@ +/*************************************************************************** + * Copyright (C) 2025 by Federico Amedeo Izzo IU2NUO, * + * Niccolò Izzo IU2KIN * + * Frederik Saraci IU2NRO * + * Silvano Seva IU2KWO * + * * + * This program 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. * + * * + * This program 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 this program; if not, see * + ***************************************************************************/ + +#ifndef CTCSS_DETECTOR_H +#define CTCSS_DETECTOR_H + +#ifndef __cplusplus +#error This header is C++ only! +#endif + +#include +#include +#include +#include + +/* + * Goertzel filter coefficients for the 50 CTCSS tones, computed for a sampling + * frequency of 2kHz. + */ +static constexpr std::array < float, 50 > ctcssCoeffs2k = +{ + 1.955858f, 1.952788f, 1.949194f, 1.945616f, 1.941767f, 1.937634f, 1.933200f, + 1.928450f, 1.923195f, 1.917936f, 1.911955f, 1.907097f, 1.902113f, 1.895202f, + 1.887648f, 1.879838f, 1.871331f, 1.862315f, 1.852531f, 1.842180f, 1.830988f, + 1.818907f, 1.806161f, 1.792725f, 1.778002f, 1.762507f, 1.753218f, 1.745912f, + 1.735704f, 1.728163f, 1.717311f, 1.709207f, 1.697685f, 1.688992f, 1.676770f, + 1.667463f, 1.654514f, 1.644208f, 1.630498f, 1.619878f, 1.605010f, 1.593692f, + 1.577610f, 1.548608f, 1.517951f, 1.503955f, 1.485167f, 1.450171f, 1.412880f, + 1.395880f +}; + +/** + * CTCSS detector class, based on the modified Goertzel filter. + * The detector is designed to be called recursively: each time the number of + * accumulated samples reaches the detection window, the energy of each CTCSS + * frequency is evaluated to find the one with the maximum value. If the ratio + * between the peak energy and the average of all the energies is above the + * threshold, the tone corresponding to the peak is considered detected. + */ +class CtcssDetector +{ +public: + + /** + * Constructor. + * + * @param coeffs: Goertzel filter coefficients. + * @param window: size of the detection window. + * @param threshold: detection threshold. + */ + CtcssDetector(const std::array< float, 50 >& coeffs, + const uint32_t window, const float threshold) : + threshold(threshold), window(window), goertzel(coeffs), + totSamples(0), overThresh(false) { } + + /** + * Destructor. + */ + ~CtcssDetector() { } + + /** + * Update the internal states of the Goertzel filter. + * + * @param samples: pointer to new input values. + * @param numSamples: number of new input values. + */ + void update(const int16_t *samples, const size_t numSamples) + { + goertzel.samples(samples, numSamples); + totSamples += numSamples; + + if(totSamples >= window) + { + analyze(); + goertzel.reset(); + totSamples = 0; + } + } + + /** + * Check if a CTCSS tone is being detected. + * + * @param toneIdx: index of the CTCSS tone. + * @return true if the CTCSS tone is active. + */ + bool toneDetected(const uint8_t toneIdx) + { + return (toneIdx == activeToneIdx) && (overThresh == true); + } + + /** + * Reset detector state. + */ + void reset() + { + goertzel.reset(); + totSamples = 0; + overThresh = false; + } + +private: + + /** + * Compute signal power and determine if a CTCSS tone is active. + */ + void analyze() + { + float avgPower = 0.0f; + float maxPower = 0.0f; + + for(size_t i = 0; i < 50; i++) + { + float power = goertzel.power(i); + avgPower += power; + if(maxPower < power) + { + maxPower = power; + activeToneIdx = i; + } + } + + avgPower /= 50.0f; + overThresh = (maxPower >= (avgPower * threshold)) ? true : false; + } + + const float threshold; + const uint32_t window; + Goertzel< 50 > goertzel; + uint32_t totSamples; + uint8_t activeToneIdx; + bool overThresh; +}; + +#endif /* CTCSS_DETECTOR_H */ diff --git a/openrtx/include/core/goertzel.hpp b/openrtx/include/core/goertzel.hpp new file mode 100644 index 00000000..7991bd54 --- /dev/null +++ b/openrtx/include/core/goertzel.hpp @@ -0,0 +1,112 @@ +/*************************************************************************** + * Copyright (C) 2025 by Federico Amedeo Izzo IU2NUO, * + * Niccolò Izzo IU2KIN * + * Frederik Saraci IU2NRO * + * Silvano Seva IU2KWO * + * * + * This program 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. * + * * + * This program 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 this program; if not, see * + ***************************************************************************/ + +#ifndef GOERTZEL_H +#define GOERTZEL_H + +#ifndef __cplusplus +#error This header is C++ only! +#endif + +#include + +/** + * Class for modified Goertzel filter with configurable coefficients. + */ +template < size_t N > +class Goertzel +{ +public: + + /** + * Constructor. + * + * @param coeffs: Goertzel coefficients. + */ + Goertzel(const std::array< float, N >& coeffs) : k(coeffs) + { + reset(); + } + + /** + * Destructor. + */ + ~Goertzel() { } + + /** + * Update the internal states of the Goertzel filter. + * + * @param input: input value for the current time step. + */ + void sample(const int16_t value) + { + for(size_t i = 0; i < N; i++) + { + float u = static_cast< float >(value) + (k[i] * u0[i]) - u1[i]; + u1[i] = u0[i]; + u0[i] = u; + } + } + + /** + * Update the internal states of the Goertzel filter. + * + * @param samples: pointer to new input values. + * @param numSamples: number of new input values. + */ + void samples(const int16_t *samples, const size_t numSamples) + { + for(size_t i = 0; i < numSamples; i++) + sample(samples[i]); + } + + /** + * Get signal power at a given frequency index. + * + * @param index: frequency index. + * @return signal power. + */ + float power(const size_t freq) + { + if(freq >= N) + return 0; + + return (u0[freq] * u0[freq]) + + (u1[freq] * u1[freq]) - + (u0[freq] * u1[freq] * k[freq]); + } + + /** + * Reset filter history. + */ + void reset() + { + u0.fill(0.0f); + u1.fill(0.0f); + } + +private: + + const std::array< float, N >& k; + std::array< float, N > u0; + std::array< float, N > u1; +}; + +#endif /* GOERTZEL_H */