Detecting CTCSS tones with Goertzel’s algorithm
April 21, 2006 Embedded Staff
Most engineers involved in the design of Software Defined Radio (SDR) applications are well acquainted with the Fast Fourier Transform (FFT) and related algorithms. The lesser-known Goertzel Discrete Fourier Transform (DFT) may not be as familiar to many. Here is a relatively simple technique that uses the modified form of the Goertzel algorithm to detect squelch tones that can be adapted to many applications where the detection or analysis of a limited set of frequencies is desired.
Continuous Tone Coded Squelch System (CTCSS) is a generic term used by the mobile radio industry; the technology itself accepts desired signals and rejects undesired ones on a voice radio channel. It's also known under the brand names PL (Private Line) to Motorola users or Channel Guard to GE/Ericsson users. This mechanism is used by both mobile handheld radios and base stations/repeaters to maximize the use of a single shared frequency among many users. The system involves the use of a sub-audible tone injected into the voice-modulation line of the transmitter and a matching tone detector in the receiver after the FM discriminator. When activated, the presence of a CTCSS tone on a carrier opens the audio-squelch circuit of the receiver, allowing the signal modulation to pass through to the transmitter and be heard by the user. Typical implementations use a set of 32 to 50 frequencies in the 67- to 254-Hz range to signal receivers as to whether or not the received signal should be passed to the user or squelched.
Figure 1 shows a block diagram of a simple software-defined radio (SDR) implementation of a voice radio receiver supporting this feature. This article is only concerned with the CTCSS detector and filter block outlined in this diagram. These components take as input the voice signal, the (possibly encoded) CTCSS signal, and a user-selected CTCSS threshold level and output the filtered voice signal as well as an indication of which tone, if any, is present in the signal. External squelch logic takes the user-selected tone and the actual tone present and passes the voice signal if the selected tone and present tone match.
I'll describe a method to detect CTCSS signals in this SDR environment using the modified Goertzel discrete Fourier transform (DFT) algorithm. We'll examine the algorithm itself and how it has been used by many others to detect dual-tone multifrequency (DTMF) signaling. I'll show you how the algorithm has been adapted to this particular application, and I'll present a simple C++ implementation of the algorithm along with how well it typically performs and how it can be improved.
The Goertzel algorithm
The Goertzel algorithm has been described before and applied to the problem of detecting the presences of specific frequencies within a signal, often for the purpose of detecting DTMF signaling. For this application, I'll use the optimized Goertzel algorithm described by Kevin Banks with the following parameters and modifications.1
Frequencies of interest
Various CTCSS standards define from 32 to 50 “sub-audible” frequencies that can be selected to signal squelch. I've selected the standard Electronic Industries Alliance (EIA) tone set shown in Table 1.2 Extending the algorithm to an arbitrary tone set is straightforward, and the code samples will support application defined tone sets.
Sampling rate
The sampling rate of voice signals in SDR applications is usually going to be either 8kHz or 16kHz. The algorithms presented here provide better detection characteristics at a high sample rate with the obvious expense of added computational cost. We'll use an 8kHz input signal.
Block size
The block size is the number (N ) of samples evaluated during the per-sample processing phase of the Goertzel algorithm. The value of N and the selected or given sample rate determines the frequency resolution of the algorithm. Selecting large N gives better frequency discrimination at the expense of more time to detect a frequency of interest. Our analysis will trade off these two parameters to find the minimum time required to reliably detect our frequencies of interest given a reference input signal.
Step 1
The optimized Goertzel algorithm has three steps. First, a set of precomputed constants are evaluated for use during the signal-analysis phase. These constants, one set per frequency of interest fi , are summarized as follows:
ki = ((N * fi ) / fsr ) coeffi = 2 * cosine ( ((2 * π ) / N ) * ki )
(1)
where:
N is the block size as I've described,
ƒi are the frequencies of interest (the CTCSS tones from Table 1), and
fsr is the sample rate frequency (8,000 in this case)
Contrary to Banks and according to a paper by Jimmy Beard and coauthors, ki is not limited to integers, which should allow for better frequency discrimination or faster tone detection (or both).1,3 At this point I noticed something interesting. Since we're going to allow the DFT coeffcient ki to take on real values, it's clear from Equation 1 that the cosine coeffcient for each frequency of interest is independent of the block size N . The simplified equation then becomes:
coeffi = 2 * cosine (2 * π * fi / fsr )
(2)
This has a practical advantage in that we can change N dynamically during any stage of this algorithm without affecting previously processed samples or recalculating our DFT coeffcients. Later we'll see how we can use this property to our advantage.
Step 2
Next, for each of the N input samples x (n ) processed during the per-sample processing phase, the values Q 0i , Q 1i , and Q 2i are evaluated as:
Q 0i = coeffi * Q 1i – Q 2i + x (n )
Q 2i = Q 1i
Q 1i = Q 0i (3)
Step 3
Finally, for the optimized or modified Goertzel, the relative power at each of the frequencies of interest is given by:
Relative poweri = Q*i 2 + Q 2i 2 – Q 1i * Q 2i * coeffi
(4)
To determine if any of the frequencies of interest are present in the input signal, the value of the relative power (magnitude2 ) at each frequency can be compared to a threshold, to each other, or a combination of these. Since we're not interested in the actual power at each frequency but rather the relative power across all of the frequencies of interest, we won't worry about taking the square root of the mag2 .
C++ class implementation
This C++ class implementation won't be the optimal for all, or even most, real-world applications. I present it strictly to illustrate the use of the modified Goertzel algorithm to detect CTCSS signaling tones and to easily test the performance of the algorithm.
The CTCSS Detector class
The primary function of this class is to take a series of input audio samples, perform the computations on each sample as described above, and after each N samples evaluate the results to determine if one of a set of frequencies is present in the signal. The public methods provided by the CTCSSDetector class are shown in Listing 1. The default constructor initializes the class instance to detect frequencies from the EIA standard CTCSS tone set. An additional constructor is provided for the user to detect frequencies from an alternate tone set.
Listing 1 CTCSSDetector class public methods
// CTCSSDetector: Continuous Tone Coded Squelch System// tone detector class based on the Modified Goertzel// algorithm.class CTCSSDetector { public: // Constructors and Destructor CTCSSDetector( ); // allows user defined CTCSS tone set CTCSSDetector( int nTones, double *tones ); ~CTCSSDetector(); // setup the basic parameters and coefficients void SetCoefficients ( int N, // the algorithm "block" size int SampleRate );// input signal sample rate // set the detection threshold void SetThreshold ( double thold ); // analyze a sample set and optionally filter // the tone frequencies. bool Analyze ( AudioSample *samples, // input signal int nSamples, bool filter = false ); int GetNTones();// get the number of defined tones. // get the currently detected tone, if any int GetDetectedTone (); // Get the max power at the detected tone. double GetMaxPower (); void Reset ( ); // reset the analysis algorithm // display information about the set of tones and // progress of the algorithm. void ShowTones(); void ShowPower( double atfreq ); void ShowFreqs(); protected: // Override these to change behavior of the detector virtual void InitializePower(); virtual void AccumulatePower( int tone, int &maxTone); virtual void EvaluatePower( int maxPowerTone);
The Setcoeffcients() method sets up the basic parameters and coeffcients of the Goertzel algorithm for the values given for N , the block size, and the sample rate of the voice signal. Details of this method are shown in Listing 2. This method saves block size and signal sample rate in the class instance and determines the filter coeffcients for each of the frequencies of interest. Once the coeffcients have been set the ShowTones() method can be used to display details of the tone set and calculated k and coeffcient for each CTCSS tone.
Listing 2 SetCoefficient() method
void CTCSSDetector::SetCoefficient (int _N, int _samplerate ) { N = _N; // save the basic parameters for use during analysis SampleRate = _samplerate; // for each of the frequencies (tones) of interest calculate // k and the associated filter coefficient as per the Goertzel // algorithm. Note: we are using a real value (as apposed to // an integer as described in some references. k is retained // for later display. The tone set is specified in the // constructor. Notice that the resulting coefficients are // independent of N. for ( int j = 0; j < nTones; ++j ) { k[j] = ((double)N * toneSet[j]) / (double)SampleRate; coef[j] = 2.0 * cos((2.0 * M_PI * toneSet[j])/(double)SampleRate) }}
The most significant method of the class is the Analyze() method. This processes each input signal sample, and after every N samples determines if one of the frequencies of interest is present. Analyze() also allows the user to specify whether the CTCSS tones should be removed from the signal with a standard high-pass filter.
Listing 3 shows the heart of the CTCSS tone-detection algorithm, the Analyze() method. As we've said, this method uses the modified Goertzel algorithm to detect specific frequencies (the CTCSS tones) in a given input signal. This method can optionally remove the CTCSS tones using a standard high-pass filter.
Listing 3 Analyze() method
// Analyze an input signal for the presence of CTCSS tones.bool CTCSSDetector::Analyze ( AudioSample *voiceSamples, // input signal int nsamples, // samples in signal bool filter ) // filter option{double in; // normalize input signal.bool ready = false; // true if the algorithm has a resultAudioSample *sample = voiceSamples;// number of samples required to complete this block.int samples_to_process = min(nsamples, N - samplesProcessed);detectedTone = -2; // indicates block not complete// process all of the samples or enough to complete blockfor ( int i = 0; i < samples_to_process; ++i ) { in = (double)*(sample++) / 32767.0; // normalize this->Feedback(in); // Goertzel feedback }samplesProcessed += samples_to_process;if ( samplesProcessed == N ) { // completed a block of Nthis->Feedforward(); // calculate the power at each tonesamplesProcessed = 0;// process remaining samples for next block.for ( int i = samples_to_process; i < nsamples; ++i ) { in = (double)*(sample++) / 32767.0; // normalize this->Feedback(in); // Goertzel feedback } ready = true; // have a result }if ( filter ) // filter the original input signal? toneFilter.FilterTone( voiceSamples, nsamples ); return ready; }
The Analyze() method first determines how many samples from the input are required to be processed to complete the in-process block of N samples. This method can be called with 1 to N input samples but, as designed, more than N samples in one invocation is an error. Each sample is normalized to the -1.0 to 1.0 range and then the feedback stage of the algorithm is applied. After each N samples, the feed-forward stage is applied, which is where the relative power magnitudes are calculated for each CTCSS tone. If after each N samples more samples remain in the input signal, they're processed by the feedback stage at this point to start the analysis of the next block of N After the input signal has been processed, a high-pass filter can be applied to remove the CTCSS tones if any are present. I have found this to be necessary because, contrary to the description in CTCSS literature, these tones are definitely not sub-audible. The value returned from this method is an indication whether enough signal has been processed to provide a tone-detection indication. The actual tone detected, if any, can be determined using the GetDetectedTone() method, shown in Listing 1.
The Feedback() method, as shown in Listing 4, is a very small piece of code that, as expected, does the per-sample feedback stage of the Goertzel algorithm.
Listing 4 Feedback() method
void CTCSSDetector::Feedback (double in) {double t;// feedback for each tonefor ( int j = 0; j < nTones; ++j ) { t = u0[j]; u0[j] = in + (coef[j] * u0[j]) - u1[j]; u1[j] = t; }}
The last phase of the detection algorithm is the feed-forward phase where the relative magnitude squared of the power at each of the frequencies of interest is determined. It's for this step that the algorithm is called the modified or optimized form of Goertzel's algorithm because it doesn't evaluate the imaginary part of the standard algorithm. The code shown in Listing 5 is a segment from the Feedforward() method showing the calculation of the relative power at each frequency of interest. By default, the three virtual methods called during this processing simply initialize the calculations, accumulate total power, find the maximum power, and finally evaluate the results to determine which tone, if any, is present. This method is structured in this way to enable a user to easily derive from this class and override these methods to apply different power evaluation criteria to the resulting relative powers. The underlying algorithm operates exactly the same but the results may be interpreted differently.
Listing 5 Feed-forward phase processing
// feed forward for each CTCSS tone.InitializePower();for ( int j = 0; j < nTones; ++j power[j] = (u0[j] * u0[j]) + (u1[ (coef[j] * u0[j] * u1[j]); AccumulatePower( j, MaxPowerIndex // reset for next block. u0[j] = u1[j] = 0.0; }EvaluatePower( MaxPowerIndex );
Two additional utility classes are included with the detector class. First, a class to insert an arbitrary pure tone into the audio signal is provided and second, a filter class is provided to remove any CTCSS tones present in a given voice signal. Both of these classes will be used for testing and characterization of the detector algorithm. The filter design is a Butterworth high-pass filter generated by Terry Fisher's filter-design application with order of 10 and a corner frequency of 350Hz.4 I found it necessary in testing to use such a high corner frequency and order to remove all of the high-frequency CTCSS tones. An alternative to this “one filter fits all tones” solution is to define a set of filters to be used when a specific frequency is detected. This definition would allow a simpler filter design (smaller order, less computation) when lower-frequency tones are detected and would also preserve more of the voice information present in the lower frequencies in that case.
Listing 6 shows the class definition for the tone generator used for testing the performance of the detector. This class has methods to specify the frequency, sample rate, and amplitude parameters and a method to add to or replace a given input signal with the desired frequency. This tone generator can produce any frequency in the frequency range of 30Hz up to half the specified sampling rate, and from 0% to 100% of full scale. We'll be using this tone generator to test the CTCSS detector algorithm by injecting frequencies at the specific CTCSS tone frequencies and across the frequency of interest spectrum at small increments to test the response and effectiveness of the detector.
Listing 6 Tone generator class definition
// class generates an audio tone used specifically for testing// the CTCSS detector. It can be used to generate a tone between// 30 and 0.5 * the specified sample rate at any amplitude 0%// to 100%.class CTCSSToneGenerator{ public: CTCSSToneGenerator( double f = 100.0, double a = 10.0, int sr = 8000 ); ~CTCSSToneGenerator(); // Insert the tone into an existing signal overwrite = true // or generate a pure tone. The freq and amplitude must // have been previously specified. void InsertTone ( AudioSample *buf, int nsamples, bool overwrite = true ); // set the freq of the tone to generate void SetFreq ( double f ); // set the amp of the tone to generate. void SetAmp ( double a ); protected: int sampleRate; double scalingConst; // amplitude invariant double cosignConst; // sample rate and frequency invariant int start; // state information to preserve phase.};
Test applications
To test and characterize this algorithm I've developed two simple test applications with the basic structure shown in Figure 2. The first will generate a series of input tones and output the frequency response under various conditions. The response can be analyzed with or without the presence of a voice signal, which is useful for understanding the limits of the algorithm's tone-detection capabilities.
The second application tests the performance of the detector with a real-time voice input and a simple user interface to adjust the inserted CTCSS tone and amplitude. The output of this test indicates whether a tone is present and if so, the power at that tone's frequency. The voice and inserted tone are recorded in a WAV file that is then played back with the detection and filter algorithms active.
The first thing I wanted to examine was the relative power across a number of frequencies of interest to determine how well the algorithm could discriminate between frequencies that are very close (0.1Hz) together. I chose to look at the frequencies in the low range of CTCSS tones (67 to 100Hz) because these are the closest together, with a minimum tone spacing of 2.5Hz between tones 2 (71.9kHz) and 3 (74.4kHz), and are therefore likely the hardest to discriminate. The code shown in Listing 7 performs this analysis. This code segment assumes that a voice signal (voicedata) has either been read in from some source or initialized to 0. The output of the ShowPower() method is comma-delimited text of the relative power at each of the defined frequencies of interest. The resulting output file can then be loaded into a spreadsheet to generate plots, as shown in Figures 3 through 6, or used to perform other analysis.
Listing 7 Tone generator for testing detector
// insert a tone from 66 to 205 Hz in 0.1 Hz increments// and then output the detected power at each freq of// interest.for ( int i = 0; i <= (205 - 66) * 10; ++i ) { memcpy ( data, voicedata, blockSize * 2); toneGen.SetFreq(66.0 + ((double)i * 0.1)); toneGen.InsertTone ( data, blockSize, FALSE ); ctcssDetector.Analyze ( data, bucketSize ); ctcssDetector.ShowPower(66.0 + ((double)i * 0.1)); }
Pure tone frequency response
My first test was to use the code in Listing 7 to analyze a sequence of tones without any voice present. This initial test used a block size N of 8,000 samples (1 second) and 10% of full-scale tone signal strength. The plot in Figure 3 shows the relative power (vertical axis) of 11 frequencies of interest for input frequencies from 66 to 100Hz in increments of 0.1Hz (horizontal axis). This plot gives a good indication just how sensitively this algorithm can discriminate frequencies. To determine exactly where it's ambiguous as to the presence of one frequency of interest (ƒi ) versus another, I've plotted from this same set of data the frequency response for each tone between 67 and 84Hz. Figure 4 shows that as our input signal moves away from each fi the relative power rapidly drops off and, just as important, the relative power for each of the other fi is relatively small. Also shown is the sum of all calculated powers at each input frequency. You can see that at each fi the power from that frequency is significantly more than the sum of all the other frequencies combined. For instance, at 77Hz the relative power from the 77Hz input signal is 400 and the sum of all the others is just 200. This is the basis for our decision to detect tones by comparing the power at each fi with the sum of all of the other powers.
Performing this same analysis with different block sizes, input-signal amplitude, and in the presence of voice clearly showed the relationship between the algorithm block size, the input CTCSS tone amplitude, and the effect of input voice on the ability of the algorithm to detect the correct tone. For instance, when the amplitude of the input CTCSS tone is 5% of full scale and the block size is kept at 8,000, the relative power at that frequency is reduced to 200, and a 2% signal results in a max relative power of 80 as shown in Figure 5. But, when the block size is shortened the power accumulation is spread out significantly, making it much harder to detect a specific frequency. This spreading effect can be seen in Figure 6.
Figure 6 shows a frequency response for a block size of 4,000 and an input signal of 2%. In this case, the max relative power at each tone frequency is half that of the 8,000 block size, and it's spread across a much wider frequency range.
This effect clearly illustrates the tradeoff when choosing a block size. Making the block size large gives us a much better frequency resolution but at the expense of time (to accumulate the necessary samples to complete the algorithm). Likewise, adding more signal strength to the inserted CTCSS tone makes it easier to detect the tone but that's generally outside of the detector's control. My goal here was to reliably detect tones at less than 5% of full-scale signal strength. This brings us to our second test application, which verifies the operation of the algorithm with real-time and stored voice input.
Tone detector test application
This test is a simple program that enables the user to input voice with a microphone and sound card, insert a specified CTCSS tone, execute the detection algorithm on that signal, and evaluate the results. While this is occurring, a simple user interface provides the means to dynamically change various parameters of the algorithm such as the input-tone amplitude, the base-threshold level, and whether the output signal should be filtered.This test operates in two phases. First is the real-time phase where “live” audio is input, processed, and written to a WAV file (voice and user-selected tone). When the first phase is complete the recording is replayed through the detector and output through the sound card to verify the results from the first phase.The output of this test is an indication each time a new tone is detected and the power at that tone's frequency. A short code segment showing how the CTCSS detector class is used to detect a tone is shown in Listing 8. In this segment a block-size segment of voice is read, a specified tone is inserted if one has been specified, and the result is analyzed for the presence of a tone. Each time a different tone is detected it's reported, along with the power calculated at that tone's frequency. While this loop is executing the user can change parameters such as the block size, detection threshold, input tone, and input-tone signal strength.
Listing 8 Tone detection code segment
// setup the input voice channel voiceChannel.SetBuffers(4000,2); voiceChannel.StartRecording(); int currentTone = -1; while ( !exitFlag.Wait(100) ) { // read 1 block size of voice. voiceChannel.Read( voicedata, (ctcssDetector.GetBlockSize() * 2) ); // if we have selected a tone to insert, // insert it into the voice. if ( (toneToInsert >= 0) && (toneToInsert < ctcssDetector.GetNTones() ) ) { toneGen.InsertTone (voicedata, ctcssDetector.GetBlockSize(), FALSE ); } // write the voice plus tone to a wave file. wfile.Write ( voicedata, voiceChannel.GetLastReadCount() ); // analyze the resulting signal for tones. // Only report changes if ( ctcssDetector.Analyze (voicedata, ctcssDetector.GetBlockSize() ) && (currentTone != ctcssDetector.GetDetectedTone()) ) { currentTone = ctcssDetector.GetDetectedTone(); PTRACE (2," Tone is " << ctcssDetector.GetDetectedTone() << " with power: " << ctcssDetector.GetMaxPower() << ", Total power: " << ctcssDetector.GetTotalPower() ); }}
The second phase takes the WAV file generated in the first phase and runs it through the detector to verify the results of the first phase and to enable the user to vary algorithm parameters to see their effect on detection performance. Also, the voice output of the detector is played back through the sound card. The code segment implementing these functions is shown in Listing 9. During this phase the user can change the block size, detection threshold to affect algorithm performance, and also enable or disable the filtering of the embedded tones to judge the performance of the high-pass filter.
Listing 9 Playback phase of tone detection test
ctcssDetector.Reset();currentTone = -1;// read the wave file one block at a time.while ( wfile.Read(voicedata, (ctcssDetector.GetBlockSize()
Share this: