diff --git a/config.cpp b/config.cpp index b414deb..e8e0aa3 100644 --- a/config.cpp +++ b/config.cpp @@ -211,11 +211,7 @@ static struct freq_t *mk_freqlist( int n ) fl[i].frequency = 0; fl[i].label = NULL; fl[i].agcavgfast = 0.5f; - fl[i].agcavgslow = 0.5f; - fl[i].filter_avg = 0.5f; - fl[i].agcmin = 100.0f; - fl[i].agclow = 0; - fl[i].sqlevel = -1; + fl[i].squelch = Squelch(); fl[i].active_counter = 0; } return fl; @@ -258,7 +254,6 @@ static int parse_channels(libconfig::Setting &chans, device_t *dev, int i) { channel->wavein[k] = 20; channel->waveout[k] = 0.5; } - channel->agcsq = 1; channel->axcindicate = NO_SIGNAL; channel->modulation = MOD_AM; channel->mode = MM_MONO; @@ -331,7 +326,7 @@ static int parse_channels(libconfig::Setting &chans, device_t *dev, int i) { if(libconfig::Setting::TypeList == chans[j]["squelch"].getType()) { // New-style array of per-frequency squelch settings for(int f = 0; ffreq_count; f++) { - channel->freqlist[f].sqlevel = (int)chans[j]["squelch"][f]; + channel->freqlist[f].squelch = Squelch((int)chans[j]["squelch"][f]); } // NB: no value check; -1 allows auto-squelch for // some frequencies and not others. @@ -343,7 +338,7 @@ static int parse_channels(libconfig::Setting &chans, device_t *dev, int i) { error(); } for(int f = 0; ffreq_count; f++) { - channel->freqlist[f].sqlevel = sqlevel; + channel->freqlist[f].squelch = Squelch(sqlevel); } } else { cerr<<"Invalid value for squelch (should be int or list - use parentheses)\n"; diff --git a/input-file.cpp b/input-file.cpp index 870f9b0..9f341f4 100644 --- a/input-file.cpp +++ b/input-file.cpp @@ -71,7 +71,7 @@ int file_init(input_t * const input) { dev_data->input_file = fopen(dev_data->filepath, "rb"); if(!dev_data->input_file) { - cerr << "File input failed to open '" << dev_data->filepath << "'\n"; + cerr << "File input failed to open '" << dev_data->filepath << "' - " << strerror(errno) << endl; error(); } diff --git a/makefile b/makefile index 1df3509..0004043 100644 --- a/makefile +++ b/makefile @@ -23,7 +23,7 @@ SUBDIRS = hello_fft CLEANDIRS = $(SUBDIRS:%=clean-%) BIN = rtl_airband -OBJ = rtl_airband.o input-common.o input-helpers.o input-file.o output.o config.o util.o mixer.o +OBJ = rtl_airband.o input-common.o input-helpers.o input-file.o output.o config.o util.o mixer.o squelch.o FFT = hello_fft/hello_fft.a .PHONY: all clean install help $(SUBDIRS) $(CLEANDIRS) @@ -129,7 +129,7 @@ help: $(FFT): hello_fft ; -config.o: rtl_airband.h input-common.h +config.o: rtl_airband.h input-common.h squelch.h input-common.o: input-common.h @@ -145,7 +145,7 @@ input-file.o: rtl_airband.h input-common.h input-helpers.h input-file.h mixer.o: rtl_airband.h -rtl_airband.o: rtl_airband.h input-common.h +rtl_airband.o: rtl_airband.h input-common.h squelch.h output.o: rtl_airband.h input-common.h @@ -153,6 +153,8 @@ pulse.o: rtl_airband.h util.o: rtl_airband.h +squelch.o : squelch.h + $(SUBDIRS): $(MAKE) -C $@ diff --git a/output.cpp b/output.cpp index 0bf6a43..fcd1000 100644 --- a/output.cpp +++ b/output.cpp @@ -579,7 +579,7 @@ static void output_channel_noise_levels(FILE *f) { channel_t* channel = devices[i].channels + j; for (int k = 0; k < channel->freq_count; k++) { print_channel_metric(f, "channel_noise_level", channel->freqlist[k].frequency, channel->freqlist[k].label); - fprintf(f, "\t%.3f\n", channel->freqlist[k].agcmin); + fprintf(f, "\t%.3f\n", channel->freqlist[k].squelch.noise_floor()); } } } diff --git a/rtl_airband.cpp b/rtl_airband.cpp index c330ccf..d508ea9 100644 --- a/rtl_airband.cpp +++ b/rtl_airband.cpp @@ -68,6 +68,7 @@ #include #include "input-common.h" #include "rtl_airband.h" +#include "squelch.h" #ifdef WITH_PROFILING #include "gperftools/profiler.h" @@ -533,107 +534,75 @@ void *demodulate(void *params) { AFC afc(dev, i); channel_t* channel = dev->channels + i; freq_t *fparms = channel->freqlist + channel->freq_idx; + + // TODO: why not just do this in the loop below? #if defined (__arm__) || defined (__aarch64__) - float agcmin2 = fparms->agcmin * 4.5f; + float agcmin2 = fparms->squelch.noise_floor() * 4.5f; for (int j = 0; j < WAVE_BATCH + AGC_EXTRA; j++) { channel->waveref[j] = min(channel->wavein[j], agcmin2); } #else - __m128 agccap = _mm_set1_ps(fparms->agcmin * 4.5f); + __m128 agccap = _mm_set1_ps(fparms->squelch.noise_floor() * 4.5f); for (int j = 0; j < WAVE_BATCH + AGC_EXTRA; j += 4) { __m128 t = _mm_loadu_ps(channel->wavein + j); _mm_storeu_ps(channel->waveref + j, _mm_min_ps(t, agccap)); } #endif + for (int j = AGC_EXTRA; j < WAVE_BATCH + AGC_EXTRA; j++) { - // auto noise floor - if (fparms->sqlevel < 0 && j % 16 == 0) { - fparms->agcmin = fparms->agcmin * 0.97f + min(fparms->agcavgslow, fparms->agcmin) * 0.03f + 0.0001f; - } - // average power - fparms->agcavgslow = fparms->agcavgslow * 0.99f + channel->waveref[j] * 0.01f; - - float sqlevel = fparms->sqlevel >= 0 ? (float)fparms->sqlevel : 3.0f * fparms->agcmin; - if (channel->agcsq > 0) { - channel->agcsq = max(channel->agcsq - 1, 1); - if (channel->agcsq == 1 && fparms->agcavgslow > sqlevel) { - channel->agcsq = -AGC_EXTRA * 2; - channel->axcindicate = SIGNAL; - if(channel->modulation == MOD_AM) { - // fade in - for (int k = j - AGC_EXTRA; k < j; k++) { - if (channel->wavein[k] > sqlevel) { - fparms->agcavgfast = fparms->agcavgfast * 0.9f + channel->wavein[k] * 0.1f; - } - } - } - } - } else { - if (channel->wavein[j] > sqlevel) { - if(channel->modulation == MOD_AM) - fparms->agcavgfast = fparms->agcavgfast * 0.995f + channel->wavein[j] * 0.005f; - fparms->agclow = 0; - } else { - fparms->agclow++; - } - channel->agcsq = min(channel->agcsq + 1, -1); - if ((channel->agcsq == -1 && fparms->agcavgslow < sqlevel) || fparms->agclow == AGC_EXTRA - 12) { - channel->agcsq = AGC_EXTRA * 2; - channel->axcindicate = NO_SIGNAL; - if(channel->modulation == MOD_AM) { - // fade out - for (int k = j - AGC_EXTRA + 1; k < j; k++) { - channel->waveout[k] = channel->waveout[k - 1] * 0.94f; - } - } - } - } + float &real = channel->iq_in[2*(j - AGC_EXTRA)]; + float &imag = channel->iq_in[2*(j - AGC_EXTRA)+1]; + + fparms->squelch.process_reference_sample(channel->waveref[j]); - bool signal_filtered = false; - if(channel->agcsq < 0 && channel->needs_raw_iq) { + // If squelch is open / opening and using I/Q, then cleanup the signal and possibly update squelch. + if (fparms->squelch.should_filter_sample() && channel->needs_raw_iq) { // remove phase rotation introduced by FFT sliding window - float swf, cwf, re, im; + float swf, cwf, re_tmp, im_tmp; sincosf_lut(channel->dm_phi, &swf, &cwf); - multiply(channel->iq_in[2*(j - AGC_EXTRA)], channel->iq_in[2*(j - AGC_EXTRA)+1], cwf, -swf, &re, &im); + multiply(real, imag, cwf, -swf, &re_tmp, &im_tmp); channel->dm_phi += channel->dm_dphi; channel->dm_phi &= 0xffffff; // apply lowpass filter, will be a no-op if not configured - fparms->lowpass_filter.apply(re, im); + fparms->lowpass_filter.apply(re_tmp, im_tmp); // update I/Q and wave - channel->iq_in[2*(j - AGC_EXTRA)] = re; - channel->iq_in[2*(j - AGC_EXTRA)+1] = im; - channel->wavein[j] = sqrt(re * re + im * im); - - if(fparms->lowpass_filter.enabled()) { - fparms->filter_avg = fparms->filter_avg * 0.999f + channel->wavein[j] * 0.001f; - - if (fparms->filter_avg < sqlevel) { - signal_filtered = true; - channel->axcindicate = NO_SIGNAL; - } else { - channel->axcindicate = SIGNAL; - } + real = re_tmp; + imag = im_tmp; + channel->wavein[j] = sqrt(real * real + imag * imag); + + // update squelch post-cleanup + if (fparms->lowpass_filter.enabled()) { + fparms->squelch.process_filtered_sample(channel->wavein[j]); } } - if(channel->agcsq != -1 || signal_filtered) { - channel->waveout[j] = 0; - if(channel->has_iq_outputs) { - channel->iq_out[2*(j - AGC_EXTRA)] = 0; - channel->iq_out[2*(j - AGC_EXTRA)+1] = 0; + + if(channel->modulation == MOD_AM) { + // if squelch is just opening then fade in, or if just closing fade out + if (fparms->squelch.should_fade_in()) { + for (int k = j - AGC_EXTRA; k < j; k++) { + if (channel->wavein[k] >= fparms->squelch.squelch_level()) { + fparms->agcavgfast = fparms->agcavgfast * 0.9f + channel->wavein[k] * 0.1f; + } + } + } else if (fparms->squelch.should_fade_out()) { + for (int k = j - AGC_EXTRA + 1; k < j; k++) { + channel->waveout[k] = channel->waveout[k - 1] * 0.94f; + } } - } else { - const float &re = channel->iq_in[2*(j - AGC_EXTRA)]; - const float &im = channel->iq_in[2*(j - AGC_EXTRA)+1]; - if(channel->has_iq_outputs) { - channel->iq_out[2*(j - AGC_EXTRA)] = re; - channel->iq_out[2*(j - AGC_EXTRA)+1] = im; + if( (fparms->squelch.get_state() == Squelch::OPEN || fparms->squelch.get_state() == Squelch::OPENING) && channel->wavein[j] > fparms->squelch.squelch_level() ) { + // TODO: Possible Improvement - re-visit this, should it move to is_open()? + fparms->agcavgfast = fparms->agcavgfast * 0.995f + channel->wavein[j] * 0.005f; } + } + // If squelch is still open then do modulation-specific processing + if (fparms->squelch.is_open()) { if(channel->modulation == MOD_AM) { + channel->waveout[j] = (channel->wavein[j - AGC_EXTRA] - fparms->agcavgfast) / (fparms->agcavgfast * 1.5f); if (abs(channel->waveout[j]) > 0.8f) { channel->waveout[j] *= 0.85f; @@ -642,23 +611,40 @@ void *demodulate(void *params) { } #ifdef NFM else if(channel->modulation == MOD_NFM) { -// FM demod + // FM demod if(fm_demod == FM_FAST_ATAN2) { - channel->waveout[j] = polar_disc_fast(re, im, channel->pr, channel->pj); + channel->waveout[j] = polar_disc_fast(real, imag, channel->pr, channel->pj); } else if(fm_demod == FM_QUADRI_DEMOD) { - channel->waveout[j] = fm_quadri_demod(re, im, channel->pr, channel->pj); + channel->waveout[j] = fm_quadri_demod(real, imag, channel->pr, channel->pj); } - channel->pr = re; - channel->pj = im; -// de-emphasis IIR + DC blocking + channel->pr = real; + channel->pj = imag; + + // de-emphasis IIR + DC blocking fparms->agcavgfast = fparms->agcavgfast * 0.995f + channel->waveout[j] * 0.005f; channel->waveout[j] -= fparms->agcavgfast; channel->waveout[j] = channel->waveout[j] * (1.0f - channel->alpha) + channel->waveout[j-1] * channel->alpha; } #endif // NFM -// apply the notch filter. If no filter configured, this will no-op + // apply the notch filter, will be a no-op if not configured fparms->notch_filter.apply(channel->waveout[j]); + + channel->axcindicate = SIGNAL; + if(channel->has_iq_outputs) { + channel->iq_out[2*(j - AGC_EXTRA)] = real; + channel->iq_out[2*(j - AGC_EXTRA)+1] = imag; + } + + // Squelch is closed + } else { + channel->waveout[j] = 0; + // TODO: Possible Improvement - set channel->axcindicate to NO_SIGNAL at start of loop and dont clear here to allow output() to pick up the end of a transmission + channel->axcindicate = NO_SIGNAL; + if(channel->has_iq_outputs) { + channel->iq_out[2*(j - AGC_EXTRA)] = 0; + channel->iq_out[2*(j - AGC_EXTRA)+1] = 0; + } } } memmove(channel->wavein, channel->wavein + WAVE_BATCH, (dev->waveend - WAVE_BATCH) * sizeof(float)); @@ -675,16 +661,18 @@ void *demodulate(void *params) { if (tui) { if(dev->mode == R_SCAN) { GOTOXY(0, device_num * 17 + dev->row + 3); + // TODO: change to dB printf("%4.0f/%3.0f%c %7.3f ", - fparms->agcavgslow, - (fparms->sqlevel >= 0 ? fparms->sqlevel : fparms->agcmin), + fparms->squelch.power_level(), + fparms->squelch.squelch_level(), channel->axcindicate, (dev->channels[0].freqlist[channel->freq_idx].frequency / 1000000.0)); } else { GOTOXY(i*10, device_num * 17 + dev->row + 3); + // TODO: change to dB printf("%4.0f/%3.0f%c ", - fparms->agcavgslow, - (fparms->sqlevel >= 0 ? fparms->sqlevel : fparms->agcmin), + fparms->squelch.power_level(), + fparms->squelch.squelch_level(), channel->axcindicate); } fflush(stdout); diff --git a/rtl_airband.h b/rtl_airband.h index ad80840..183950b 100644 --- a/rtl_airband.h +++ b/rtl_airband.h @@ -38,6 +38,7 @@ #include #endif #include "input-common.h" // input_t +#include "squelch.h" #ifndef RTL_AIRBAND_VERSION #define RTL_AIRBAND_VERSION "3.1.0" @@ -240,11 +241,7 @@ struct freq_t { int frequency; // scan frequency char *label; // frequency label float agcavgfast; // average power, for AGC - float agcavgslow; // average power, for squelch level detection - float filter_avg; // average power, for post-filter squelch level detection - float agcmin; // noise level - int sqlevel; // manually configured squelch level - int agclow; // low level sample count + Squelch squelch; size_t active_counter; // count of loops where channel has signal NotchFilter notch_filter; // notch filter - good to remove CTCSS tones LowpassFilter lowpass_filter; // lowpass filter, applied to I/Q after derotation, set at bandwidth/2 to remove out of band noise @@ -264,7 +261,6 @@ struct channel_t { uint32_t dm_dphi, dm_phi; // derotation frequency and current phase value enum modulations modulation; enum mix_modes mode; // mono or stereo - int agcsq; // squelch status, negative: signal, positive: suppressed status axcindicate; unsigned char afc; //0 - AFC disabled; 1 - minimal AFC; 2 - more aggressive AFC and so on to 255 struct freq_t *freqlist; diff --git a/squelch.cpp b/squelch.cpp new file mode 100644 index 0000000..1a7e944 --- /dev/null +++ b/squelch.cpp @@ -0,0 +1,222 @@ +#include "squelch.h" + +#include "rtl_airband.h" // needed for debug_print() + +using namespace std; + +Squelch::Squelch(int manual) : + manual_(manual) +{ + agcmin_ = 100.0f; + agcavgslow_ = 0.5f; + post_filter_avg_ = 0.5f; + + // TODO: Possible Improvement - revisit magic numbers + flap_delay_ = AGC_EXTRA * 2 - 1; + low_power_abort_ = AGC_EXTRA - 12; + + next_state_ = CLOSED; + current_state_ = CLOSED; + + delay_ = 0; + open_count_ = 0; + sample_count_ = 0; + low_power_count_ = 0; + + debug_print("Created Squelch, flap_delay: %d, low_power_abort: %d, manual: %d\n", flap_delay_, low_power_abort_, manual_); +} + +bool Squelch::is_open(void) const { + // TODO: Causes Diff - remove checks on next_state_ + return (current_state_ == OPEN && next_state_ != CLOSING); +} + +bool Squelch::should_filter_sample(void) const { + // TODO: Causes Diff - remove checks on next_state_ + if (next_state_ == CLOSING) { + return false; + } + if (current_state_ == OPEN || current_state_ == OPENING || next_state_ == OPENING) { + return true; + } + return false; +} + +bool Squelch::should_fade_in(void) const { + return (next_state_ == OPENING && current_state_ != OPENING); +} + +bool Squelch::should_fade_out(void) const { + return (next_state_ == CLOSING && current_state_ != CLOSING); +} + +const Squelch::State & Squelch::get_state(void) const { + return current_state_; +} + +const float & Squelch::noise_floor(void) const { + return agcmin_; +} + +const float & Squelch::power_level(void) const { + return agcavgslow_; +} + +const size_t & Squelch::open_count(void) const { + return open_count_; +} + +float Squelch::squelch_level(void) const { + if (is_manual()) { + return manual_; + } + return 3.0f * noise_floor(); +} + +bool Squelch::is_manual(void) const { + return manual_ >= 0; +} + +bool Squelch::has_power(void) const { + return power_level() >= squelch_level(); +} + +void Squelch::process_reference_sample(const float &sample) { + + // Update current state based on previous state from last iteration + update_current_state(); + + sample_count_++; + + // auto noise floor + // TODO: Possible Improvement - update noise floor with each sample + // TODO: Causes Diff - remove +3 + if ((sample_count_ + 3) % 16 == 0) { + agcmin_ = agcmin_ * 0.97f + std::min(agcavgslow_, agcmin_) * 0.03f + 0.0001f; + } + + // average power + agcavgslow_ = agcavgslow_ * 0.99f + sample * 0.01f; + + // Check power against thresholds + if (current_state_ == OPEN && has_power() == false) { + debug_print("Closing at %zu: no power after timeout (%f < %f)\n", sample_count_, power_level(), squelch_level()); + set_state(CLOSING); + } + + if (current_state_ == CLOSED && has_power() == true) { + debug_print("Opening at %zu: power (%f >= %f)\n", sample_count_, power_level(), squelch_level()); + set_state(OPENING); + } + + // Override squelch and close if there are repeated samples under the squelch level + if((current_state_ == OPEN || current_state_ == OPENING) && next_state_ != CLOSING) { + if (sample >= squelch_level()) { + low_power_count_ = 0; + } else { + low_power_count_++; + if (low_power_count_ >= low_power_abort_) { + debug_print("Closing at %zu: low power count %d\n", sample_count_, low_power_count_); + set_state(CLOSING); + } + } + } +} + +void Squelch::process_filtered_sample(const float &sample) { + if (should_filter_sample() == false) { + return; + } + + // average power + post_filter_avg_ = post_filter_avg_ * 0.999f + sample * 0.001f; + + if ((current_state_ == OPEN || current_state_ == OPENING || next_state_ == OPEN || next_state_ == OPENING) && post_filter_avg_ < squelch_level()) { + debug_print("Closing at %zu: power post filter (%f < %f)\n", sample_count_, post_filter_avg_, squelch_level()); + set_state(CLOSING); + } +} + +void Squelch::set_state(State update) { + + // Valid transitions (current_state_ -> next_state_) are: + // - OPENING -> OPENING + // - OPENING -> CLOSING + // - OPENING -> OPEN + // - OPEN -> OPEN + // - OPEN -> CLOSING + // - CLOSING -> OPENING + // - CLOSING -> CLOSING + // - CLOSING -> CLOSED + // - CLOSED -> CLOSED + // - CLOSED -> OPENING + + // Invalid transistions (current_state_ -> next_state_) are: + // - OPENING -> CLOSED (must go through CLOSING to get to CLOSED) + // - OPEN -> OPENING (if already OPEN cant go backwards) + // - OPEN -> CLOSED (must go through CLOSING to get to CLOSED) + // - CLOSING -> OPEN (must go through OPENING to get to OPEN) + // - CLOSED -> CLOSING (if already CLOSED cant go backwards) + // - CLOSED -> OPEN (must go through OPENING to get to OPEN) + + // must go through OPENING to get to OPEN (unless already OPEN) + if (update == OPEN && current_state_ != OPEN && current_state_ != OPENING) { + update = OPENING; + } + + // must go through CLOSING to get to CLOSED (unless already CLOSED) + if (update == CLOSED && current_state_ != CLOSING && current_state_ != CLOSED) { + update = CLOSING; + } + + // if already OPEN cant go backwards + if (update == OPENING && current_state_ == OPEN) { + update = OPEN; + } + + // if already CLOSED cant go backwards + if (update == CLOSING && current_state_ == CLOSED) { + update = CLOSED; + } + + next_state_ = update; +} + +void Squelch::update_current_state(void) { + if (next_state_ == OPENING) { + if (current_state_ != OPENING) { + open_count_++; + delay_ = flap_delay_; + low_power_count_ = 0; + // TODO: Causes Diff - re-initialize post_filter_avg_ = agcavgslow_ + current_state_ = next_state_; + } else { + delay_--; + // TODO: Causes Diff - remove start + if (delay_ == 2) { + delay_ = 0; + } + // TODO: Causes Diff - remove end + if (delay_ <= 2) { + next_state_ = OPEN; + } + } + } else if (next_state_ == CLOSING) { + if (current_state_ != CLOSING) { + delay_ = flap_delay_; + current_state_ = next_state_; + } else { + delay_--; + // TODO: Causes Diff - remove start + if (delay_ == 2) { + delay_ = 0; + } + // TODO: Causes Diff - remove end + if (delay_ == 0) { + next_state_ = CLOSED; + } + } + } else { + current_state_ = next_state_; + } +} diff --git a/squelch.h b/squelch.h new file mode 100644 index 0000000..cf6a343 --- /dev/null +++ b/squelch.h @@ -0,0 +1,56 @@ +#ifndef _SQUELCH_H +#define _SQUELCH_H + +#include // needed for size_t + +class Squelch { +public: + + enum State { + OPENING, // Transitioning closed -> open + OPEN, // Audio not suppressed + CLOSING, // Transitioning open -> closed + CLOSED // Audio is suppressed + }; + + Squelch(int manual = -1); + + void process_reference_sample(const float &sample); + void process_filtered_sample(const float &sample); + + bool is_open(void) const; + bool should_filter_sample(void) const; + + bool should_fade_in(void) const; + bool should_fade_out(void) const; + + const State & get_state(void) const; + const float & noise_floor(void) const; + const float & power_level(void) const; + const size_t & open_count(void) const; + float squelch_level(void) const; + +private: + int flap_delay_; // how long to wait after opening/closing before changing + int low_power_abort_; // number of repeated samples below squelch to cause a close + int manual_; // manually configured squelch level, < 0 for disabled + + float agcmin_; // noise level + float agcavgslow_; // average power for reference sample + float post_filter_avg_; // average power for post-filter sample + + State next_state_; + State current_state_; + + int delay_; // samples to wait before making next squelch decision + size_t open_count_; // number of times squelch is opened + size_t sample_count_; // number of samples processed (for logging) + int low_power_count_; // number of repeated samples below squelch + + void set_state(State update); + void update_current_state(void); + bool has_power(void) const; + bool is_manual(void) const; +}; + +#endif