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 */