/* * ctcss.h * * Copyright (C) 2022-2023 charlie-foxtrot * * 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 . */ #include // M_PI #include // sort #include "logging.h" // debug_print() #include "ctcss.h" using namespace std; // Implementation of https://www.embedded.com/detecting-ctcss-tones-with-goertzels-algorithm/ // also https://www.embedded.com/the-goertzel-algorithm/ ToneDetector::ToneDetector(float tone_freq, float sample_rate, int window_size) { tone_freq_ = tone_freq; magnitude_ = 0.0; window_size_ = window_size; int k = (0.5 + window_size * tone_freq / sample_rate); float omega = (2.0 * M_PI * k) / window_size; coeff_ = 2.0 * cos(omega); reset(); } void ToneDetector::process_sample(const float& sample) { q0_ = coeff_ * q1_ - q2_ + sample; q2_ = q1_; q1_ = q0_; count_++; if (count_ == window_size_) { magnitude_ = q1_ * q1_ + q2_ * q2_ - q1_ * q2_ * coeff_; count_ = 0; } } void ToneDetector::reset(void) { count_ = 0; q0_ = q1_ = q2_ = 0.0; } bool ToneDetectorSet::add(const float& tone_freq, const float& sample_rate, int window_size) { ToneDetector new_tone = ToneDetector(tone_freq, sample_rate, window_size); for (const auto tone : tones_) { if (new_tone.coefficient() == tone.coefficient()) { debug_print("Skipping tone %f, too close to other tones\n", tone_freq); return false; } } tones_.push_back(new_tone); return true; } void ToneDetectorSet::process_sample(const float& sample) { for (vector::iterator it = tones_.begin(); it != tones_.end(); ++it) { it->process_sample(sample); } } void ToneDetectorSet::reset(void) { for (vector::iterator it = tones_.begin(); it != tones_.end(); ++it) { it->reset(); } } float ToneDetectorSet::sorted_powers(vector& powers) { powers.clear(); float total_power = 0.0; for (size_t i = 0; i < tones_.size(); ++i) { powers.push_back({tones_[i].relative_power(), tones_[i].freq()}); total_power += tones_[i].relative_power(); } sort(powers.begin(), powers.end(), [](PowerIndex a, PowerIndex b) { return a.power > b.power; }); return total_power / tones_.size(); } vector CTCSS::standard_tones = {67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, 150.0, 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 250.3, 254.1}; CTCSS::CTCSS(const float& ctcss_freq, const float& sample_rate, int window_size) : enabled_(true), ctcss_freq_(ctcss_freq), window_size_(window_size), found_count_(0), not_found_count_(0) { debug_print("Adding CTCSS detector for %f Hz with a sample rate of %f and window %d\n", ctcss_freq, sample_rate, window_size_); // Add the target CTCSS frequency first followed by the other "standard tones", except those // within +/- 5 Hz powers_.add(ctcss_freq, sample_rate, window_size_); for (const auto tone : standard_tones) { if (abs(ctcss_freq - tone) < 5) { debug_print("Skipping tone %f, too close to other tones\n", tone); continue; } powers_.add(tone, sample_rate, window_size_); } // clear all values to start NOTE: has_tone_ will be true until the first window count of samples are processed reset(); } void CTCSS::process_audio_sample(const float& sample) { if (!enabled_) { return; } powers_.process_sample(sample); sample_count_++; if (sample_count_ < window_size_) { return; } enough_samples_ = true; // if this is sample fills out the window then check if one of the "strongest" // tones is the CTCSS tone we are looking for. NOTE: there can be multiple "strongest" // tones based on floating point math vector tone_powers; float avg_power = powers_.sorted_powers(tone_powers); float ctcss_tone_power = 0.0; for (const auto i : tone_powers) { if (i.freq == ctcss_freq_) { ctcss_tone_power = i.power; break; } } if (ctcss_tone_power == tone_powers[0].power && ctcss_tone_power > avg_power) { debug_print("CTCSS tone of %f Hz detected\n", ctcss_freq_); has_tone_ = true; found_count_++; } else { debug_print("CTCSS tone of %f Hz not detected - highest power was %f Hz at %f vs %f\n", ctcss_freq_, tone_powers[0].freq, tone_powers[0].power, ctcss_tone_power); has_tone_ = false; not_found_count_++; } // reset everything for the next window's worth of samples powers_.reset(); sample_count_ = 0; } void CTCSS::reset(void) { if (enabled_) { powers_.reset(); enough_samples_ = false; sample_count_ = 0; has_tone_ = false; } }