ctcss.cpp 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. /*
  2. * ctcss.h
  3. *
  4. * Copyright (C) 2022-2023 charlie-foxtrot
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. #include <math.h> // M_PI
  20. #include <algorithm> // sort
  21. #include "logging.h" // debug_print()
  22. #include "ctcss.h"
  23. using namespace std;
  24. // Implementation of https://www.embedded.com/detecting-ctcss-tones-with-goertzels-algorithm/
  25. // also https://www.embedded.com/the-goertzel-algorithm/
  26. ToneDetector::ToneDetector(float tone_freq, float sample_rate, int window_size) {
  27. tone_freq_ = tone_freq;
  28. magnitude_ = 0.0;
  29. window_size_ = window_size;
  30. int k = (0.5 + window_size * tone_freq / sample_rate);
  31. float omega = (2.0 * M_PI * k) / window_size;
  32. coeff_ = 2.0 * cos(omega);
  33. reset();
  34. }
  35. void ToneDetector::process_sample(const float& sample) {
  36. q0_ = coeff_ * q1_ - q2_ + sample;
  37. q2_ = q1_;
  38. q1_ = q0_;
  39. count_++;
  40. if (count_ == window_size_) {
  41. magnitude_ = q1_ * q1_ + q2_ * q2_ - q1_ * q2_ * coeff_;
  42. count_ = 0;
  43. }
  44. }
  45. void ToneDetector::reset(void) {
  46. count_ = 0;
  47. q0_ = q1_ = q2_ = 0.0;
  48. }
  49. bool ToneDetectorSet::add(const float& tone_freq, const float& sample_rate, int window_size) {
  50. ToneDetector new_tone = ToneDetector(tone_freq, sample_rate, window_size);
  51. for (const auto tone : tones_) {
  52. if (new_tone.coefficient() == tone.coefficient()) {
  53. debug_print("Skipping tone %f, too close to other tones\n", tone_freq);
  54. return false;
  55. }
  56. }
  57. tones_.push_back(new_tone);
  58. return true;
  59. }
  60. void ToneDetectorSet::process_sample(const float& sample) {
  61. for (vector<ToneDetector>::iterator it = tones_.begin(); it != tones_.end(); ++it) {
  62. it->process_sample(sample);
  63. }
  64. }
  65. void ToneDetectorSet::reset(void) {
  66. for (vector<ToneDetector>::iterator it = tones_.begin(); it != tones_.end(); ++it) {
  67. it->reset();
  68. }
  69. }
  70. float ToneDetectorSet::sorted_powers(vector<ToneDetectorSet::PowerIndex>& powers) {
  71. powers.clear();
  72. float total_power = 0.0;
  73. for (size_t i = 0; i < tones_.size(); ++i) {
  74. powers.push_back({tones_[i].relative_power(), tones_[i].freq()});
  75. total_power += tones_[i].relative_power();
  76. }
  77. sort(powers.begin(), powers.end(), [](PowerIndex a, PowerIndex b) { return a.power > b.power; });
  78. return total_power / tones_.size();
  79. }
  80. vector<float> 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,
  81. 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,
  82. 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};
  83. 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) {
  84. debug_print("Adding CTCSS detector for %f Hz with a sample rate of %f and window %d\n", ctcss_freq, sample_rate, window_size_);
  85. // Add the target CTCSS frequency first followed by the other "standard tones", except those
  86. // within +/- 5 Hz
  87. powers_.add(ctcss_freq, sample_rate, window_size_);
  88. for (const auto tone : standard_tones) {
  89. if (abs(ctcss_freq - tone) < 5) {
  90. debug_print("Skipping tone %f, too close to other tones\n", tone);
  91. continue;
  92. }
  93. powers_.add(tone, sample_rate, window_size_);
  94. }
  95. // clear all values to start NOTE: has_tone_ will be true until the first window count of samples are processed
  96. reset();
  97. }
  98. void CTCSS::process_audio_sample(const float& sample) {
  99. if (!enabled_) {
  100. return;
  101. }
  102. powers_.process_sample(sample);
  103. sample_count_++;
  104. if (sample_count_ < window_size_) {
  105. return;
  106. }
  107. enough_samples_ = true;
  108. // if this is sample fills out the window then check if one of the "strongest"
  109. // tones is the CTCSS tone we are looking for. NOTE: there can be multiple "strongest"
  110. // tones based on floating point math
  111. vector<ToneDetectorSet::PowerIndex> tone_powers;
  112. float avg_power = powers_.sorted_powers(tone_powers);
  113. float ctcss_tone_power = 0.0;
  114. for (const auto i : tone_powers) {
  115. if (i.freq == ctcss_freq_) {
  116. ctcss_tone_power = i.power;
  117. break;
  118. }
  119. }
  120. if (ctcss_tone_power == tone_powers[0].power && ctcss_tone_power > avg_power) {
  121. debug_print("CTCSS tone of %f Hz detected\n", ctcss_freq_);
  122. has_tone_ = true;
  123. found_count_++;
  124. } else {
  125. 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);
  126. has_tone_ = false;
  127. not_found_count_++;
  128. }
  129. // reset everything for the next window's worth of samples
  130. powers_.reset();
  131. sample_count_ = 0;
  132. }
  133. void CTCSS::reset(void) {
  134. if (enabled_) {
  135. powers_.reset();
  136. enough_samples_ = false;
  137. sample_count_ = 0;
  138. has_tone_ = false;
  139. }
  140. }