added squelch to FM

pull/6/merge
boblark 3 years ago
parent b20de073f2
commit 131bdce9de
  1. 63
      RadioFMDetector_F32.cpp
  2. 130
      RadioFMDetector_F32.h
  3. 127
      examples/ReceiverFM/ReceiverFM.ino

@ -25,7 +25,7 @@
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*
* See RadioFMDetector_F32.h for usage details
*/
#include "RadioFMDetector_F32.h"
@ -35,39 +35,40 @@
// ==== UPDATE ====
void RadioFMDetector_F32::update(void) {
audio_block_f32_t *blockIn, *blockOut=NULL;
audio_block_f32_t *blockIn, *blockOut=NULL, *blockZero;
static float saveIn = 0.0f; // for squelch
static float saveOut = 0.0f;
uint16_t i, index_sine;
float32_t deltaPhase, a, b, dtemp1, dtemp2;
float32_t v_i[128]; // max size
float32_t v_q[128];
mathDSP_F32 mathDSP1; // Math support functions
#if TEST_TIME_FM
if (iitt++ >1000000) iitt = -10;
uint32_t t1, t2;
uint32_t t1, t2;
t1 = tElapse;
#endif
// Get input to FM Detector block
blockIn = AudioStream_F32::receiveWritable_f32(0);
if (!blockIn) {
if(errorPrintFM) Serial.println("FMDET-ERR: No input memory");
return;
}
}
// If there's no coefficient table, give up.
if (fir_IQ_Coeffs == NULL) {
if(errorPrintFM) Serial.println("FMDET-ERR: No IQ FIR Coefficients");
AudioStream_F32::release(blockIn);
return;
}
}
if (fir_Out_Coeffs == NULL) {
if(errorPrintFM) Serial.println("FMDET-ERR: No Out FIR Coefficients");
AudioStream_F32::release(blockIn);
return;
}
}
// Try to get a block for the FM output
blockOut = AudioStream_F32::allocate_f32();
@ -75,7 +76,10 @@ void RadioFMDetector_F32::update(void) {
if(errorPrintFM) Serial.println("FMDET-ERR: No Output Memory");
AudioStream_F32::release(blockIn);
return;
}
}
blockZero = AudioStream_F32::allocate_f32();
for(int kk=0; kk<128; kk++) blockZero->data[kk] = 0.0f;
// Generate sine and cosine of center frequency and double-balance mix
// these with the input signal to produce an intermediate result
@ -99,8 +103,8 @@ void RadioFMDetector_F32::update(void) {
b = sinTable512_f32[index_sine+1];
/* deltaPhase will be the same as used for sin */
v_q[i] = blockIn->data[i] * (a + 0.001953125*(b-a)*deltaPhase);
}
}
// Do I FIR and Q FIR. We can borrow blockIn and blockOut at this point
//void arm_fir_f32( const arm_fir_instance_f32* S, float32_t* pSrc, float32_t* pDst, uint32_t blockSize)
arm_fir_f32(&FMDet_I_inst, v_i, blockIn->data, (uint32_t)blockIn->length);
@ -119,18 +123,39 @@ void RadioFMDetector_F32::update(void) {
// Data point is now dtemp2. Apply single pole de-emphasis LPF, in place
dLast = Kdem * dtemp2 + OneMinusKdem * dLast;
blockIn->data[i] = dLast; // and save to an array
}
}
// Do output FIR filter. Data now in blockIn.
arm_fir_f32(&FMDet_Out_inst, blockIn->data, blockOut->data, (uint32_t)blockIn->length);
AudioStream_F32::release(blockIn);
// Transmit the data
// Squelch picks the audio from before the output filter and does a 4-pole BiQuad BPF
// blockIn->data still has the data we need
arm_biquad_cascade_df1_f32(&iirSqIn_inst, blockIn->data,
blockIn->data, blockIn->length);
// Update the Squelch full-wave envelope detector and single pole LPF
float sumNoise = 0.0f;
for(i=0; i<block_size; i++)
sumNoise += fabsf(blockIn->data[i]); // Ave of rectified noise
squelchLevel = alpha*(sumNoise + saveIn) + gamma*saveOut; // 1 pole
saveIn = sumNoise;
saveOut = squelchLevel;
// Add hysteresis here <<<<
// Squelch gate sends audio to output 1 (right) if squelch is open
if(squelchLevel < squelchThreshold)
AudioStream_F32::transmit(blockOut, 1);
else
AudioStream_F32::transmit(blockZero, 1);
// The un-squelch controlled audio for tone decoding, etc., goes to output 0
AudioStream_F32::transmit(blockOut, 0);
AudioStream_F32::release(blockOut);
#if TEST_TIME_FM
AudioStream_F32::release(blockIn); // Give back the blocks
AudioStream_F32::release(blockOut); AudioStream_F32::release(blockZero);
#if TEST_TIME_FM
t2 = tElapse;
if(iitt++ < 0) {Serial.print("At end of FM Det "); Serial.println (t2 - t1); }
if(iitt++ < 0) {Serial.print("At end of FM Det "); Serial.println (t2 - t1); }
t1 = tElapse;
#endif
#endif
}

@ -33,7 +33,7 @@
* frequency of the input sine wave frequency, i.e., an it is an FM detector.
* The input needs to be band limited below the lower frequency side of the
* input, typically 10 kHz. This is not part of this block.
*
*
* NOTE: Due to the sample frequencies we are working with, like 44.1 kHz, this
* detector cannot handle full FM broadcast bandwidths. It is suitable for
* NBFM as used in communications, marine radio, ham radio, etc.
@ -51,17 +51,17 @@
* be removed before the atan2. A pair of FIR filters, using FIR_IQ_Coeffs
* are used. These are again programmable and default to a 29-tap LPF with
* a 5 kHz cutoff.
*
*
* Status: Tested static, tested with FM modulated Fluke 6061B.
* An input of about 60 microvolts to the SGTL5000 gave 12 dB SINAD.
* The output sounded good. Tested T3.6 and T4.0. No known bugs
*
*
* Output: Float, sensitivity is 2*pi*(f - fCenter)*sample_rate_Hz
* For 44117Hz samplerate, this is 0.000142421 per Hz
*
*
* Accuracy: The function used is precise. However, the approximations, such
* fastAtan2, slightly limit the accuracy. A 200 point sample of a
* 14 kHz input had an average error of 0.03 Hz
* 14 kHz input had an average error of 0.03 Hz
* and a standard deviation of 0.81 Hz.
* The largest errors in this sample were about +/- 1.7 Hz. This is
* with the default filters.
@ -80,24 +80,24 @@
* filterIQ(float *fir_IQ_Coeffs, uint nFIR_IQ) sets output filtering where:
* float32_t* fir_IQ_Coeffs is an array of coefficients
* uint nFIR_IQ is the number of coefficients, max 60
*
*
* setSampleRate_Hz(float32_t _sampleRate_Hz) allows dynamic changing of
* the sample rate (experimental as of May 2020).
*
*
* returnInitializeFMError() Returns the initialization errors.
* B0001 (value 1) is an error in the IQ FIR Coefficients or quantity.
* B0010 (value 2) is an error in the Output FIR Coefficients or quantity.
* B0100 (value 4) is an error in the de-emphasis constant
* B1000 (value 8) is center frequency above half-sample frequency.
* All for debug.
*
*
* showError(uint16_t e) Turns error printing in the update function on (e=1)
* or off (e=0). For debug only.
*
* Time: For T3.6, an update of a 128 sample block, 370 microseconds, or
* 2.9 microseconds per data point.
* For T4.0, 87 microseconds, or 0.68 microseconds per data point.
*
*
* Error checking: See functions setSampleRate_Hz() and returnInitializeFMError()
* above.
*/
@ -115,7 +115,7 @@
#define TEST_TIME_FM 0
class RadioFMDetector_F32 : public AudioStream_F32 {
//GUI: inputs:1, outputs:1 //this line used for automatic generation of GUI node
//GUI: inputs:1, outputs:2 //this line used for automatic generation of GUI node
//GUI: shortName: FMDetector
public:
// Default block size and sample rate:
@ -163,26 +163,53 @@ public:
initializeFM();
}
void setSampleRate_Hz(float32_t _sampleRate_Hz) {
if (fCenter > _sampleRate_Hz/2.0f) { // Check freq range
// Provide for changing to user supplied BiQuad for Squelch input.
void setSquelchFilter(float* _sqCoeffs) {
if( _sqCoeffs==NULL)
pCfSq = coeffSqIn; // Default filter
else
pCfSq = _sqCoeffs;
initializeFM();
}
// The squelch level reads nominally 0.0 to 1.0 where
float getSquelchLevel (void) {
return squelchLevel;
}
// The squelch threshold is nominally 0.7 where
// 0.0 always lets audio through.
void setSquelchThreshold (float _sqTh) {
squelchThreshold = _sqTh;
}
void setSquelchDecay (float _sqDcy) {
gamma = _sqDcy;
alpha = 0.5f*(1.0f - gamma);
}
/* void setSampleRate_Hz(float32_t _sampleRate_Hz) {
if (fCenter > _sampleRate_Hz/2.0f) { // Check freq range
initializeFMErrors |= 8;
return;
}
}
sampleRate_Hz = _sampleRate_Hz;
// update phase increment for new frequency
phaseIncrement = 512.0f * fCenter / sampleRate_Hz;
}
} // NOT SUPPORTED - No Dynamic changes
*/
void showError(uint16_t e) {
errorPrintFM = e;
}
}
uint16_t returnInitializeFMError(void) {
return initializeFMErrors;
}
return initializeFMErrors;
}
void update(void);
private:
// One input data pointer
audio_block_f32_t *inputQueueArray_f32[1];
@ -207,6 +234,25 @@ private:
float32_t* fir_IQ_Coeffs = fir_IQ29;
uint16_t nFIR_Out = 39;
float32_t* fir_Out_Coeffs = fir_Out39;
/* Info - The structure from arm_biquad_casd_df1_inst_f32 consists of
* uint32_t numStages;
* const float32_t *pCoeffs; //Points to the array of coefficients, length 5*numStages.
* float32_t *pState; //Points to the array of state variables, length 4*numStages. */
arm_biquad_casd_df1_inst_f32 iirSqIn_inst;
// Default 2 stage Squelch input BiQuad filter, 3000 Hz, 4000 Hz both Q=5
// The -6 dB points are 2680 and 4420 Hz
// The -20 dB points are 2300 and 5300 Hz
float coeffSqIn[10] = {
0.0398031529f, 0.0f, -0.0398031529f, 1.74762569f, -0.92039369f,
0.0511929547f, 0.0f, -0.0511929547f, 1.59770204f, -0.89761409f};
float* pCfSq = coeffSqIn;
float stateSqIn[8];
float squelchThreshold = 0.7f;
float squelchLevel = 1.0f;
float gamma = 0.99;
float alpha = 0.5f*(1.0f - gamma);
#if TEST_TIME_FM
elapsedMicros tElapse;
int32_t iitt = 999000; // count up to a million during startup
@ -220,29 +266,33 @@ int32_t iitt = 999000; // count up to a million during startup
arm_fir_instance_f32 FMDet_Out_inst;
float32_t State_Out_F32[AUDIO_BLOCK_SAMPLES + MAX_FIR_OUT_COEFFS];
// Initialize the FM Detector, part of setting up and changing parameters
void initializeFM(void) {
if (fir_IQ_Coeffs && nFIR_IQ <= MAX_FIR_IQ_COEFFS) {
if (fir_IQ_Coeffs && nFIR_IQ <= MAX_FIR_IQ_COEFFS) {
/* the instance setup call
* void arm_fir_init_f32(
* void arm_fir_init_f32(
* arm_fir_instance_f32* S, points to instance of floating-point FIR filter structure.
* uint16_t numTaps, Number of filter coefficients in the filter.
* float32_t* pCoeffs, points to the filter coefficients buffer.
* float32_t* pState, points to the state buffer.
* uint32_t blockSize) Number of samples that are processed per call.
*/
arm_fir_init_f32(&FMDet_I_inst, nFIR_IQ, (float32_t*)fir_IQ_Coeffs, &State_I_F32[0], (uint32_t)block_size);
arm_fir_init_f32(&FMDet_Q_inst, nFIR_IQ, (float32_t*)fir_IQ_Coeffs, &State_Q_F32[0], (uint32_t)block_size);
}
else initializeFMErrors |= B0001;
arm_fir_init_f32(&FMDet_I_inst, nFIR_IQ, (float32_t*)fir_IQ_Coeffs, &State_I_F32[0], (uint32_t)block_size);
arm_fir_init_f32(&FMDet_Q_inst, nFIR_IQ, (float32_t*)fir_IQ_Coeffs, &State_Q_F32[0], (uint32_t)block_size);
}
else initializeFMErrors |= B0001;
if (fir_Out_Coeffs && nFIR_Out <= MAX_FIR_OUT_COEFFS) {
arm_fir_init_f32(&FMDet_Out_inst, nFIR_Out, (float32_t*)fir_Out_Coeffs, &State_Out_F32[0], (uint32_t)block_size);
}
}
else initializeFMErrors |= B0010;
dLast = 0.0;
}
// Initialize squelch Input BPF BiQuad instance (ARM DSP Math Library)
// https://www.keil.com/pack/doc/CMSIS/DSP/html/group__BiquadCascadeDF1.html
arm_biquad_cascade_df1_init_f32(&iirSqIn_inst, 2, pCfSq, &stateSqIn[0]);
}
/* FIR filter designed with http://t-filter.appspot.com
* fs = 44100 Hz, < 5kHz ripple 0.29 dB, >9 kHz, -62 dB, 29 taps
@ -259,19 +309,19 @@ int32_t iitt = 999000; // count up to a million during startup
/* FIR filter designed with http://t-filter.appspot.com
* fs = 44100 Hz, < 3kHz ripple 0.36 dB, >6 kHz, -60 dB, 39 taps
* Corrected to give DC gain = 1.00
* Corrected to give DC gain = 1.00
*/
float32_t fir_Out39[39] = {
-0.0008908477f, -0.0008401274f, -0.0001837353f, 0.0017556005f,
0.0049353322f, 0.0084952916f, 0.0107668722f, 0.0097441685f,
0.0039877576f, -0.0063455016f, -0.0188069300f, -0.0287453055f,
-0.0303831521f, -0.0186809770f, 0.0085931270f, 0.0493875744f,
0.0971742012f, 0.1423015880f, 0.1745838382f, 0.1863024485f,
0.1745838382f, 0.1423015880f, 0.0971742012f, 0.0493875744f,
0.0085931270f, -0.0186809770f, -0.0303831521f, -0.0287453055f,
-0.0188069300f, -0.0063455016f, 0.0039877576f, 0.0097441685f,
0.0107668722f, 0.0084952916f, 0.0049353322f, 0.0017556005f,
-0.0008908477f, -0.0008401274f, -0.0001837353f, 0.0017556005f,
0.0049353322f, 0.0084952916f, 0.0107668722f, 0.0097441685f,
0.0039877576f, -0.0063455016f, -0.0188069300f, -0.0287453055f,
-0.0303831521f, -0.0186809770f, 0.0085931270f, 0.0493875744f,
0.0971742012f, 0.1423015880f, 0.1745838382f, 0.1863024485f,
0.1745838382f, 0.1423015880f, 0.0971742012f, 0.0493875744f,
0.0085931270f, -0.0186809770f, -0.0303831521f, -0.0287453055f,
-0.0188069300f, -0.0063455016f, 0.0039877576f, 0.0097441685f,
0.0107668722f, 0.0084952916f, 0.0049353322f, 0.0017556005f,
-0.0001837353f, -0.0008401274f, -0.0008908477f };
};
#endif
};
#endif

@ -1,11 +1,23 @@
/* ReceiverFM.ino Bob Larkin 26 April 2020
* This is a simple test of introducing a sine wave to the
/* ReceiverFM.ino Bob Larkin 26 April 2020
* No copyright.
*
* This INO runs the FM detector on input data from the Codec. It
* requires an FM modulated signal source at 15 kHz. The Serial out is
* the squelch level generated by the squelch noise detector.
*
* Commented out is a test of introducing a sine wave to the
* FM Detector and taking 512 samples of the output. It is
* a static test with a fixed frequency for test and so
* the output DC value and noise can be tested. Note that the 512
* samples include the startup transient, so the first 300,
* or so, points should be ignored in seeing the DC value.
*
* An option is to change the squelch noise filter (commented out)
* and this serves as an example of changing the 10 coefficients.
*
* Another commented out loop code will print the observed spectrum using
* the 1024 point FFT. This shows the NBFM de-emphasis curve.
*
* Change the value of sine1.frequency to see the DC output change.
* See FMReceiver2.ino for testing with real AC modulation.
*
@ -22,32 +34,32 @@
#define SINE_ADC 0
#if SINE_ADC
AudioSynthGaussian_F32 gwn1;
AudioSynthWaveformSine_F32 sine1;
AudioMixer4_F32 mix1;
RadioFMDetector_F32 fmDet1;
AudioOutputI2S_F32 i2sOut;
AudioAnalyzeFFT1024_F32 fft1;
AudioRecordQueue_F32 queue1;
AudioConvert_F32toI16 cnvrtOut;
AudioOutputI2S i2sOut;
AudioControlSGTL5000 sgtl5000_1;
AudioConnection_F32 connect1(sine1, 0, fmDet1, 0);
AudioConnection_F32 connect3(fmDet1, 0, cnvrtOut, 0);
AudioConnection connect4(cnvrtOut, 0, i2sOut, 0); // left
AudioConnection_F32 connect5(fmDet1, 0, queue1, 0);
AudioConnection_F32 connectA(gwn1, 0, mix1, 0);
AudioConnection_F32 connectB(sine1, 0, mix1, 1);
AudioConnection_F32 connectC(mix1, 0, fmDet1, 0);
#else // Input from Teensy Audio Adaptor SGTL5000
// Note - With no input, the FM detector output is all noise. This
// can be loud, so one can add a gain block at the fmDet1 output (like 0.05 gain).
AudioInputI2S i2sIn;
AudioConvert_I16toF32 cnvrtIn;
RadioFMDetector_F32 fmDet1;
AudioRecordQueue_F32 queue1;
AudioConvert_F32toI16 cnvrtOut;
AudioOutputI2S i2sOut;
AudioControlSGTL5000 sgtl5000_1;
AudioConnection connect1(i2sIn, 0, cnvrtIn, 0); // left
AudioConnection_F32 connect2(cnvrtIn, 0, fmDet1, 0);
AudioConnection_F32 connect3(fmDet1, 0, cnvrtOut, 0);
AudioConnection_F32 connect5(fmDet1, 0, queue1, 0);
AudioConnection connect7(cnvrtOut, 0, i2sOut, 0);
// can be loud, so lower the gain below (like 0.05 gain).
AudioInputI2S_F32 i2sIn;
RadioFMDetector_F32 fmDet1;
AudioAnalyzeFFT1024_F32 fft1;
AudioRecordQueue_F32 queue1;
AudioOutputI2S_F32 i2sOut;
AudioControlSGTL5000 sgtl5000_1;
AudioConnection_F32 connectD(i2sIn, 0, fmDet1, 0); // left
#endif
// Common for both input sources
AudioConnection_F32 connect1(fmDet1, 1, i2sOut, 0); // Squelched
AudioConnection_F32 connect2(fmDet1, 0, fft1, 0);
AudioConnection_F32 connect3(fmDet1, 0, queue1, 0);
float dt1[512]; // Place to save output
float *pq1, *pd1;
@ -56,17 +68,22 @@ int i;
void setup(void) {
AudioMemory(5);
AudioMemory_F32(5);
Serial.begin(300); delay(1000); // Any rate is OK
AudioMemory_F32(100);
Serial.begin(300); delay(500); // Any rate is OK
Serial.println("Serial Started");
sgtl5000_1.enable();
sgtl5000_1.inputSelect(AUDIO_INPUT_LINEIN);
#if SINE_ADC
sine1.frequency(14000.0);
sine1.frequency(15000.0);
sine1.amplitude(0.0001f);
gwn1.amplitude(0.1f);
#endif
fft1.setOutputType(FFT_DBFS);
fft1.setNAverage(10);
// The FM detector has error checking during object construction
// when Serial.print is not available. See RadioFMDetector_F32.h:
Serial.print("FM Initialization errors: ");
@ -75,31 +92,60 @@ void setup(void) {
// The following enables error checking inside of the "ubdate()"
// Output goes to the Serial (USB) Monitor. Normally, this is quiet.
fmDet1.showError(1);
fmDet1.setSquelchThreshold(0.7f);
// Here is an example of designing and using a non-default squelch
// noise filter. The INO provides storage for the new coefficients.
// static float newFilter[10];
// freq Q ptr to Coeff
// setBandpassBiQuad(2000.0f, 3.0f, &newFilter[0]);
// setBandpassBiQuad(4000.0f, 3.0f, &newFilter[5]);
// fmDet1.setSquelchFilter(newFilter);
// Set the volume control (0.0 to 1.0)
i2sOut.setGain(0.05f);
queue1.begin();
i = 0; k=0;
}
void loop(void) {
float lData[512];
Serial.print("sqLevel "); Serial.println(fmDet1.getSquelchLevel(), 6);
delay(500);
/*
if( fft1.available() ) {
float* pd = fft1.getData();
for(int k=0; k<512; k++)
lData[k] = pd[k];
for(int k=0; k<512; k++)
Serial.println(lData[k],3);
Serial.println(" -------");
}
*/
/*
// Collect 512 samples and output to Serial
// This "if" will be active for i = 0,1,2,3
if (queue1.available() >= 1) {
if( i>=0 && i<4) {
pq1 = queue1.readBuffer();
pd1 = &dt1[i*128];
for(k=0; k<128; k++) {
*pd1++ = *pq1++;
}
for(k=0; k<128; k++)
*pd1++ = *pq1++;
queue1.freeBuffer();
if(i++==3) {
i=4; // Only collect 4 blocks
queue1.end(); // No more data to queue1
}
}
}
else {
queue1.freeBuffer();
}
}
}
// We have 512 data samples. Serial.print them
if(i == 4) {
#if SINE_ADC
@ -111,9 +157,9 @@ void loop(void) {
Serial.print (0.000022667*(float32_t)k, 6); Serial.print (", ");
Serial.print (dt1[k],7); Serial.print (", ");
Serial.println (dt1[k]/0.000142421, 2); // Convert to Hz
}
}
i = 5;
}
}
if(i==5) {
i = 6;
Serial.print("CPU: Percent Usage, Max: ");
@ -131,5 +177,22 @@ void loop(void) {
Serial.print(", ");
Serial.println(AudioMemoryUsageMax_F32());
Serial.println();
}
*/
}
}
// Find BiQuad Coefficients for Squelch Noise filter.
// Only used if the default -6 dB points of 2680 and 4420 Hz
// are not suitable
void setBandpassBiQuad(float frequency, float q, float* cf) {
double w0 = frequency * (2 * 3.141592654 / 44100); // sampleRate_Hz);
double sinW0 = sin(w0);
double alpha = sinW0 / ((double)q * 2.0);
double cosW0 = cos(w0);
double scale = 1.0 / (1.0+alpha);
/* b0 */ cf[0] = alpha * scale;
/* b1 */ cf[1] = 0;
/* b2 */ cf[2] = (-alpha) * scale;
/* a1 */ cf[3] = -(-2.0 * cosW0) * scale;
/* a2 */ cf[4] = -(1.0 - alpha) * scale;
}

Loading…
Cancel
Save