mirror of
				https://github.com/AlexandreRouma/SDRPlusPlus.git
				synced 2025-10-31 08:58:13 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			noise_redu
			...
			new_source
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c488d72ce2 | 
							
								
								
									
										7
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,7 +0,0 @@ | ||||
| # Important | ||||
|  | ||||
| Only minor bug fixes and bandplans are accepted. | ||||
|  | ||||
| Pull requests adding features or any bug fix that requires significant code changes will be automatically rejected. | ||||
|  | ||||
| Open an issue requesting a feature or discussing a possible bugfix instead. | ||||
| @@ -13,7 +13,6 @@ endif (USE_BUNDLE_DEFAULTS) | ||||
| file(GLOB_RECURSE SRC "src/*.cpp" "src/*.c") | ||||
|  | ||||
| add_definitions(-DSDRPP_IS_CORE) | ||||
| add_definitions(-DFLOG_ANDROID_TAG="SDR++") | ||||
| if (MSVC) | ||||
|     set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) | ||||
| endif () | ||||
|   | ||||
| @@ -55,33 +55,24 @@ namespace sdrpp_credits { | ||||
|         "Dale L Puckett (K0HYD)", | ||||
|         "Daniele D'Agnelli", | ||||
|         "D. Jones", | ||||
|         "Dexruus", | ||||
|         "EB3FRN", | ||||
|         "Eric Johnson", | ||||
|         "Ernest Murphy (NH7L)", | ||||
|         "Flinger Films", | ||||
|         "Frank Werner (HB9FXQ)", | ||||
|         "gringogrigio", | ||||
|         "Jeff Moe", | ||||
|         "Joe Cupano", | ||||
|         "KD1SQ", | ||||
|         "Kezza", | ||||
|         "Krys Kamieniecki", | ||||
|         "Lee Donaghy", | ||||
|         "Lee KD1SQ", | ||||
|         ".lozenge. (Hank Hill)", | ||||
|         "Martin Herren (HB9FXX)", | ||||
|         "ON4MU", | ||||
|         "Passion-Radio.com", | ||||
|         "Paul Maine", | ||||
|         "Peter Betz", | ||||
|         "Scanner School", | ||||
|         "Scott Palmer", | ||||
|         "SignalsEverywhere", | ||||
|         "Syne Ardwin (WI9SYN)", | ||||
|         "W4IPA", | ||||
|         "William Arcand (W1WRA)", | ||||
|         "Yves Rougy", | ||||
|         "Zipper" | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -67,6 +67,10 @@ namespace dsp::buffer { | ||||
|                 sizes[writeCur] = count; | ||||
|                 writeCur++; | ||||
|                 writeCur = ((writeCur) % TEST_BUFFER_SIZE); | ||||
|  | ||||
|                 // if (((writeCur - readCur + TEST_BUFFER_SIZE) % TEST_BUFFER_SIZE) >= (TEST_BUFFER_SIZE-2)) { | ||||
|                 //     flog::warn("Overflow"); | ||||
|                 // } | ||||
|             } | ||||
|             cnd.notify_all(); | ||||
|             _in->flush(); | ||||
|   | ||||
| @@ -9,7 +9,7 @@ namespace dsp::convert { | ||||
|  | ||||
|         StereoToMono(stream<stereo_t>* in) { base_type::init(in); } | ||||
|          | ||||
|         static inline int process(int count, const stereo_t* in, float* out) { | ||||
|         inline int process(int count, const stereo_t* in, float* out) { | ||||
|             for (int i = 0; i < count; i++) { | ||||
|                 out[i] = (in[i].l + in[i].r) / 2.0f; | ||||
|             } | ||||
|   | ||||
| @@ -3,8 +3,6 @@ | ||||
| #include "quadrature.h" | ||||
| #include "../filter/fir.h" | ||||
| #include "../taps/low_pass.h" | ||||
| #include "../taps/high_pass.h" | ||||
| #include "../taps/band_pass.h" | ||||
| #include "../convert/mono_to_stereo.h" | ||||
|  | ||||
| namespace dsp::demod { | ||||
| @@ -19,26 +17,22 @@ namespace dsp::demod { | ||||
|         ~FM() { | ||||
|             if (!base_type::_block_init) { return; } | ||||
|             base_type::stop(); | ||||
|             dsp::taps::free(filterTaps); | ||||
|             dsp::taps::free(lpfTaps); | ||||
|         } | ||||
|  | ||||
|         void init(dsp::stream<dsp::complex_t>* in, double samplerate, double bandwidth, bool lowPass, bool highPass) { | ||||
|         void init(dsp::stream<dsp::complex_t>* in, double samplerate, double bandwidth, bool lowPass) { | ||||
|             _samplerate = samplerate; | ||||
|             _bandwidth = bandwidth; | ||||
|             _lowPass = lowPass; | ||||
|             _highPass = highPass; | ||||
|  | ||||
|             demod.init(NULL, bandwidth / 2.0, _samplerate); | ||||
|             loadDummyTaps(); | ||||
|             fir.init(NULL, filterTaps); | ||||
|  | ||||
|             // Initialize taps | ||||
|             updateFilter(lowPass, highPass); | ||||
|             lpfTaps = dsp::taps::lowPass(_bandwidth / 2.0, (_bandwidth / 2.0) * 0.1, _samplerate); | ||||
|             lpf.init(NULL, lpfTaps); | ||||
|  | ||||
|             if constexpr (std::is_same_v<T, float>) { | ||||
|                 demod.out.free(); | ||||
|             } | ||||
|             fir.out.free(); | ||||
|             lpf.out.free(); | ||||
|  | ||||
|             base_type::init(in); | ||||
|         } | ||||
| @@ -49,7 +43,9 @@ namespace dsp::demod { | ||||
|             base_type::tempStop(); | ||||
|             _samplerate = samplerate; | ||||
|             demod.setDeviation(_bandwidth / 2.0, _samplerate); | ||||
|             updateFilter(_lowPass, _highPass); | ||||
|             dsp::taps::free(lpfTaps); | ||||
|             lpfTaps = dsp::taps::lowPass(_bandwidth / 2.0, (_bandwidth / 2.0) * 0.1, _samplerate); | ||||
|             lpf.setTaps(lpfTaps); | ||||
|             base_type::tempStart(); | ||||
|         } | ||||
|  | ||||
| @@ -58,20 +54,19 @@ namespace dsp::demod { | ||||
|             std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx); | ||||
|             if (bandwidth == _bandwidth) { return; } | ||||
|             _bandwidth = bandwidth; | ||||
|             std::lock_guard<std::mutex> lck2(lpfMtx); | ||||
|             demod.setDeviation(_bandwidth / 2.0, _samplerate); | ||||
|             updateFilter(_lowPass, _highPass); | ||||
|             dsp::taps::free(lpfTaps); | ||||
|             lpfTaps = dsp::taps::lowPass(_bandwidth / 2, (_bandwidth / 2) * 0.1, _samplerate); | ||||
|             lpf.setTaps(lpfTaps); | ||||
|         } | ||||
|  | ||||
|         void setLowPass(bool lowPass) { | ||||
|             assert(base_type::_block_init); | ||||
|             std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx); | ||||
|             updateFilter(lowPass, _highPass); | ||||
|         } | ||||
|  | ||||
|         void setHighPass(bool highPass) { | ||||
|             assert(base_type::_block_init); | ||||
|             std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx); | ||||
|             updateFilter(_lowPass, highPass); | ||||
|             std::lock_guard<std::mutex> lck2(lpfMtx); | ||||
|             _lowPass = lowPass; | ||||
|             lpf.reset(); | ||||
|         } | ||||
|  | ||||
|         void reset() { | ||||
| @@ -79,23 +74,23 @@ namespace dsp::demod { | ||||
|             std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx); | ||||
|             base_type::tempStop(); | ||||
|             demod.reset(); | ||||
|             fir.reset(); | ||||
|             lpf.reset(); | ||||
|             base_type::tempStart(); | ||||
|         } | ||||
|  | ||||
|         inline int process(int count, dsp::complex_t* in, T* out) { | ||||
|             if constexpr (std::is_same_v<T, float>) { | ||||
|                 demod.process(count, in, out); | ||||
|                 if (filtering) { | ||||
|                     std::lock_guard<std::mutex> lck(filterMtx); | ||||
|                     fir.process(count, out, out); | ||||
|                 if (_lowPass) { | ||||
|                     std::lock_guard<std::mutex> lck(lpfMtx); | ||||
|                     lpf.process(count, out, out); | ||||
|                 } | ||||
|             } | ||||
|             if constexpr (std::is_same_v<T, stereo_t>) { | ||||
|                 demod.process(count, in, demod.out.writeBuf); | ||||
|                 if (filtering) { | ||||
|                     std::lock_guard<std::mutex> lck(filterMtx); | ||||
|                     fir.process(count, demod.out.writeBuf, demod.out.writeBuf); | ||||
|                 if (_lowPass) { | ||||
|                     std::lock_guard<std::mutex> lck(lpfMtx); | ||||
|                     lpf.process(count, demod.out.writeBuf, demod.out.writeBuf); | ||||
|                 } | ||||
|                 convert::MonoToStereo::process(count, demod.out.writeBuf, out); | ||||
|             } | ||||
| @@ -114,50 +109,13 @@ namespace dsp::demod { | ||||
|         } | ||||
|  | ||||
|     private: | ||||
|         void updateFilter(bool lowPass, bool highPass) { | ||||
|             std::lock_guard<std::mutex> lck(filterMtx); | ||||
|  | ||||
|             // Update values | ||||
|             _lowPass = lowPass; | ||||
|             _highPass = highPass; | ||||
|             filtering = (lowPass || highPass); | ||||
|  | ||||
|             // Free filter taps | ||||
|             dsp::taps::free(filterTaps); | ||||
|  | ||||
|             // Generate filter depending on low and high pass settings | ||||
|             if (_lowPass && _highPass) { | ||||
|                 filterTaps = dsp::taps::bandPass<float>(300.0, _bandwidth / 2.0, 100.0, _samplerate); | ||||
|             } | ||||
|             else if (_highPass) { | ||||
|                 filterTaps = dsp::taps::highPass(300.0, 100.0, _samplerate); | ||||
|             } | ||||
|             else if (_lowPass) { | ||||
|                 filterTaps = dsp::taps::lowPass(_bandwidth / 2.0, (_bandwidth / 2.0) * 0.1, _samplerate); | ||||
|             } | ||||
|             else { | ||||
|                 loadDummyTaps(); | ||||
|             } | ||||
|  | ||||
|             // Set filter to use new taps | ||||
|             fir.setTaps(filterTaps); | ||||
|             fir.reset(); | ||||
|         } | ||||
|  | ||||
|         void loadDummyTaps() { | ||||
|             float dummyTap = 1.0f; | ||||
|             filterTaps = dsp::taps::fromArray<float>(1, &dummyTap); | ||||
|         } | ||||
|  | ||||
|         double _samplerate; | ||||
|         double _bandwidth; | ||||
|         bool _lowPass; | ||||
|         bool _highPass; | ||||
|         bool filtering; | ||||
|  | ||||
|         Quadrature demod; | ||||
|         tap<float> filterTaps; | ||||
|         filter::FIR<float, float> fir; | ||||
|         std::mutex filterMtx; | ||||
|         tap<float> lpfTaps; | ||||
|         filter::FIR<float, float> lpf; | ||||
|         std::mutex lpfMtx; | ||||
|     }; | ||||
| } | ||||
| @@ -2,6 +2,5 @@ | ||||
| #include "../multirate/rrc_interpolator.h" | ||||
|  | ||||
| namespace dsp::mod { | ||||
|     // TODO: Check if resample before RRC is better than using the RRC taps as a filter (bandwidth probably not correct for alias-free resampling) | ||||
|     typedef multirate::RRCInterpolator<complex_t> PSK; | ||||
| } | ||||
| @@ -5,7 +5,7 @@ | ||||
| #include "../math/hz_to_rads.h" | ||||
|  | ||||
| namespace dsp::mod { | ||||
|     class Quadrature : public Processor<float, complex_t> { | ||||
|     class Quadrature : Processor<float, complex_t> { | ||||
|         using base_type = Processor<float, complex_t>; | ||||
|     public: | ||||
|         Quadrature() {} | ||||
|   | ||||
| @@ -83,6 +83,8 @@ namespace dsp::multirate { | ||||
|             int interp = OutSR / gcd; | ||||
|             int decim = InSR / gcd; | ||||
|  | ||||
|             flog::warn("interp: {0}, decim: {1}", interp, decim); | ||||
|  | ||||
|             // Configure resampler | ||||
|             double tapSamplerate = _symbolrate * (double)interp; | ||||
|             rrcTaps = taps::rootRaisedCosine<float>(_rrcTapCount * interp, _rrcBeta, _symbolrate, tapSamplerate); | ||||
|   | ||||
| @@ -1,183 +0,0 @@ | ||||
| #pragma once | ||||
| #include "../processor.h" | ||||
| #include "../window/nuttall.h" | ||||
| #include <fftw3.h> | ||||
| #include "../convert/stereo_to_mono.h" | ||||
|  | ||||
| namespace dsp::noise_reduction { | ||||
|     class Audio : public Processor<stereo_t, stereo_t> { | ||||
|         using base_type = Processor<stereo_t, stereo_t>; | ||||
|     public: | ||||
|         Audio() {} | ||||
|  | ||||
|         Audio(stream<stereo_t>* in, int bins) { init(in, bins); } | ||||
|  | ||||
|         ~Audio() { | ||||
|             if (!base_type::_block_init) { return; } | ||||
|             base_type::stop(); | ||||
|             destroyBuffers(); | ||||
|         } | ||||
|  | ||||
|         void init(stream<stereo_t>* in, int bins) { | ||||
|             _bins = bins; | ||||
|             complexBins = (bins / 2) + 1; | ||||
|             normFactor = 1.0f / (float)_bins; | ||||
|             initBuffers(); | ||||
|             base_type::init(in); | ||||
|         } | ||||
|  | ||||
|         void setBins(int bins) { | ||||
|             assert(base_type::_block_init); | ||||
|             std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx); | ||||
|             base_type::tempStop(); | ||||
|             _bins = bins; | ||||
|             complexBins = (bins / 2) + 1; | ||||
|             normFactor = 1.0f / (float)_bins; | ||||
|             destroyBuffers(); | ||||
|             initBuffers(); | ||||
|             base_type::tempStart(); | ||||
|         } | ||||
|  | ||||
|         void setLevel(float level) { | ||||
|             _level = powf(10.0f, level * 0.1f); | ||||
|         } | ||||
|  | ||||
|         void reset() { | ||||
|             assert(base_type::_block_init); | ||||
|             std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx); | ||||
|             base_type::tempStop(); | ||||
|             buffer::clear(buffer, _bins - 1); | ||||
|             buffer::clear(backFFTIn, _bins); | ||||
|             buffer::clear(noisePrint, _bins); | ||||
|             base_type::tempStart(); | ||||
|         } | ||||
|  | ||||
|         int process(int count, const stereo_t* in, stereo_t* out) { | ||||
|             // Write new input data to buffer | ||||
|             convert::StereoToMono::process(count, in, bufferStart); | ||||
|              | ||||
|             // Iterate the FFT | ||||
|             for (int i = 0; i < count; i++) { | ||||
|                 // Apply windows | ||||
|                 volk_32f_x2_multiply_32f(forwFFTIn, &buffer[i], fftWin, _bins); | ||||
|  | ||||
|                 // Do forward FFT | ||||
|                 fftwf_execute(forwardPlan); | ||||
|  | ||||
|                 // Get bin amplitude and square to get power | ||||
|                 volk_32fc_magnitude_32f(ampBuf, (lv_32fc_t*)forwFFTOut, complexBins); | ||||
|  | ||||
|                 // Update noise print using a running average | ||||
|                 volk_32f_s32f_multiply_32f(scaledAmps, ampBuf, alpha, complexBins); | ||||
|                 volk_32f_s32f_multiply_32f(noisePrint, noisePrint, beta, complexBins); | ||||
|                 volk_32f_x2_add_32f(noisePrint, noisePrint, scaledAmps, complexBins); | ||||
|  | ||||
|                 // Clamp amplitudes | ||||
|                 volk_32f_x2_max_32f(ampBuf, ampBuf, noisePrint, complexBins); | ||||
|  | ||||
|                 // Compute Wiener (funny) filter | ||||
|                 volk_32f_x2_subtract_32f(scaledAmps, ampBuf, noisePrint, complexBins); | ||||
|                 volk_32f_x2_divide_32f(scaledAmps, scaledAmps, ampBuf, complexBins); | ||||
|  | ||||
|                 // Apply wiener filter to bins | ||||
|                 volk_32fc_32f_multiply_32fc((lv_32fc_t*)backFFTIn, (lv_32fc_t*)forwFFTOut, scaledAmps, complexBins); | ||||
|  | ||||
|                 // Do reverse FFT and get first element | ||||
|                 fftwf_execute(backwardPlan); | ||||
|                 out[i].l = backFFTOut[_bins / 2]; | ||||
|                 out[i].r = backFFTOut[_bins / 2]; | ||||
|             } | ||||
|  | ||||
|             // Correct amplitude | ||||
|             volk_32f_s32f_multiply_32f((float*)out, (float*)out, normFactor, count*2); | ||||
|  | ||||
|             // Move buffer buffer | ||||
|             memmove(buffer, &buffer[count], (_bins - 1) * sizeof(float)); | ||||
|  | ||||
|             return count; | ||||
|         } | ||||
|  | ||||
|         int run() { | ||||
|             int count = base_type::_in->read(); | ||||
|             if (count < 0) { return -1; } | ||||
|  | ||||
|             process(count, base_type::_in->readBuf, base_type::out.writeBuf); | ||||
|  | ||||
|             // Swap if some data was generated | ||||
|             base_type::_in->flush(); | ||||
|             if (!base_type::out.swap(count)) { return -1; } | ||||
|             return count; | ||||
|         } | ||||
|  | ||||
|     protected: | ||||
|         void initBuffers() { | ||||
|             // Allocate FFT buffers | ||||
|             forwFFTIn = (float*)fftwf_malloc(_bins * sizeof(float)); | ||||
|             forwFFTOut = (complex_t*)fftwf_malloc(_bins * sizeof(complex_t)); | ||||
|             backFFTIn = (complex_t*)fftwf_malloc(_bins * sizeof(complex_t)); | ||||
|             backFFTOut = (float*)fftwf_malloc(_bins * sizeof(float)); | ||||
|  | ||||
|             // Allocate and clear delay buffer | ||||
|             buffer = buffer::alloc<float>(STREAM_BUFFER_SIZE + 64000); | ||||
|             bufferStart = &buffer[_bins - 1]; | ||||
|             buffer::clear(buffer, _bins - 1); | ||||
|  | ||||
|             // Clear backward FFT input | ||||
|             buffer::clear(backFFTIn, _bins); | ||||
|  | ||||
|             // Allocate amplitude buffer | ||||
|             ampBuf = buffer::alloc<float>(_bins); | ||||
|             scaledAmps = buffer::alloc<float>(_bins); | ||||
|             noisePrint = buffer::alloc<float>(_bins); | ||||
|             buffer::clear(noisePrint, _bins); | ||||
|  | ||||
|             // Allocate and generate Window | ||||
|             fftWin = buffer::alloc<float>(_bins); | ||||
|             for (int i = 0; i < _bins; i++) { fftWin[i] = window::nuttall(i, _bins - 1); } | ||||
|  | ||||
|             // Plan FFTs | ||||
|             forwardPlan = fftwf_plan_dft_r2c_1d(_bins, forwFFTIn, (fftwf_complex*)forwFFTOut, FFTW_ESTIMATE); | ||||
|             backwardPlan = fftwf_plan_dft_c2r_1d(_bins, (fftwf_complex*)backFFTIn, backFFTOut, FFTW_ESTIMATE); | ||||
|         } | ||||
|  | ||||
|         void destroyBuffers() { | ||||
|             fftwf_destroy_plan(forwardPlan); | ||||
|             fftwf_destroy_plan(backwardPlan); | ||||
|             fftwf_free(forwFFTIn); | ||||
|             fftwf_free(forwFFTOut); | ||||
|             fftwf_free(backFFTIn); | ||||
|             fftwf_free(backFFTOut); | ||||
|             buffer::free(buffer); | ||||
|             buffer::free(ampBuf); | ||||
|             buffer::free(scaledAmps); | ||||
|             buffer::free(noisePrint); | ||||
|             buffer::free(fftWin); | ||||
|         } | ||||
|  | ||||
|         float _level = 0.0f; | ||||
|  | ||||
|         float* forwFFTIn; | ||||
|         complex_t* forwFFTOut; | ||||
|         complex_t* backFFTIn; | ||||
|         float* backFFTOut; | ||||
|  | ||||
|         fftwf_plan forwardPlan; | ||||
|         fftwf_plan backwardPlan; | ||||
|  | ||||
|         float* buffer; | ||||
|         float* bufferStart; | ||||
|  | ||||
|         float* fftWin; | ||||
|  | ||||
|         float* ampBuf; | ||||
|         float* scaledAmps; | ||||
|         float* noisePrint; | ||||
|  | ||||
|         int _bins; | ||||
|         int complexBins; | ||||
|         float normFactor = 1.0f; | ||||
|  | ||||
|         float alpha = 0.0001f; | ||||
|         float beta = 0.9999f; | ||||
|     };  | ||||
| } | ||||
| @@ -37,17 +37,21 @@ namespace dsp::noise_reduction { | ||||
|  | ||||
|         inline int process(int count, complex_t* in, complex_t* out) { | ||||
|             for (int i = 0; i < count; i++) { | ||||
|                 // Get signal amplitude and pass value if null | ||||
|                 // Get signal amplitude | ||||
|                 float inAmp = in[i].amplitude(); | ||||
|                 if (!inAmp) { | ||||
|                     out[i] = in[i]; | ||||
|  | ||||
|                 // Update average amplitude | ||||
|                 float gain = 1.0f; | ||||
|                 if (inAmp != 0.0f) { | ||||
|                     amp = (amp * _invRate) + (inAmp * _rate); | ||||
|                     float excess = inAmp / amp; | ||||
|                     if (excess > _level) { | ||||
|                         gain = 1.0f / excess; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Update running average of amplitude | ||||
|                 amp = (_rate*inAmp) + (_invRate*amp); | ||||
|  | ||||
|                 // Null out if spike (Note: ideally, it should try to guess the real data) | ||||
|                 out[i] = (inAmp > _level*amp) ? complex_t{0.0f,0.0f} : in[i]; | ||||
|                  | ||||
|                 // Scale output by gain | ||||
|                 out[i] = in[i] * gain; | ||||
|             } | ||||
|             return count; | ||||
|         } | ||||
|   | ||||
| @@ -15,7 +15,7 @@ namespace dsp::taps { | ||||
|         if (oddTapCount && !(count % 2)) { count++; } | ||||
|         return windowedSinc<T>(count, (bandStop - bandStart) / 2.0, sampleRate, [=](double n, double N) { | ||||
|             if constexpr (std::is_same_v<T, float>) { | ||||
|                 return 2.0f * cosf(offsetOmega * (float)n) * window::nuttall(n, N); | ||||
|                 return cosf(offsetOmega * (float)n) * window::nuttall(n, N); | ||||
|             } | ||||
|             if constexpr (std::is_same_v<T, complex_t>) { | ||||
|                 // The offset is negative to flip the taps. Complex bandpass are asymetric | ||||
|   | ||||
| @@ -15,10 +15,6 @@ namespace sourcemenu { | ||||
|     bool iqCorrection = false; | ||||
|     bool invertIQ = false; | ||||
|  | ||||
|     EventHandler<std::string> sourceRegisteredHandler; | ||||
|     EventHandler<std::string> sourceUnregisterHandler; | ||||
|     EventHandler<std::string> sourceUnregisteredHandler; | ||||
|  | ||||
|     std::vector<std::string> sourceNames; | ||||
|     std::string sourceNamesTxt; | ||||
|     std::string selectedSource; | ||||
| @@ -99,10 +95,10 @@ namespace sourcemenu { | ||||
|         } | ||||
|         sourceId = std::distance(sourceNames.begin(), it); | ||||
|         selectedSource = sourceNames[sourceId]; | ||||
|         sigpath::sourceManager.selectSource(sourceNames[sourceId]); | ||||
|         sigpath::sourceManager.select(sourceNames[sourceId]); | ||||
|     } | ||||
|  | ||||
|     void onSourceRegistered(std::string name, void* ctx) { | ||||
|     void onSourceRegistered(std::string name) { | ||||
|         refreshSources(); | ||||
|  | ||||
|         if (selectedSource.empty()) { | ||||
| @@ -114,13 +110,13 @@ namespace sourcemenu { | ||||
|         sourceId = std::distance(sourceNames.begin(), std::find(sourceNames.begin(), sourceNames.end(), selectedSource)); | ||||
|     } | ||||
|  | ||||
|     void onSourceUnregister(std::string name, void* ctx) { | ||||
|     void onSourceUnregister(std::string name) { | ||||
|         if (name != selectedSource) { return; } | ||||
|  | ||||
|         // TODO: Stop everything | ||||
|     } | ||||
|  | ||||
|     void onSourceUnregistered(std::string name, void* ctx) { | ||||
|     void onSourceUnregistered(std::string name) { | ||||
|         refreshSources(); | ||||
|  | ||||
|         if (sourceNames.empty()) { | ||||
| @@ -153,12 +149,9 @@ namespace sourcemenu { | ||||
|         selectSource(selected); | ||||
|         sigpath::iqFrontEnd.setDecimation(1 << decimationPower); | ||||
|  | ||||
|         sourceRegisteredHandler.handler = onSourceRegistered; | ||||
|         sourceUnregisterHandler.handler = onSourceUnregister; | ||||
|         sourceUnregisteredHandler.handler = onSourceUnregistered; | ||||
|         sigpath::sourceManager.onSourceRegistered.bindHandler(&sourceRegisteredHandler); | ||||
|         sigpath::sourceManager.onSourceUnregister.bindHandler(&sourceUnregisterHandler); | ||||
|         sigpath::sourceManager.onSourceUnregistered.bindHandler(&sourceUnregisteredHandler); | ||||
|         sigpath::sourceManager.onSourceRegistered.bind(onSourceRegistered); | ||||
|         sigpath::sourceManager.onSourceUnregister.bind(onSourceUnregister); | ||||
|         sigpath::sourceManager.onSourceUnregistered.bind(onSourceUnregistered); | ||||
|  | ||||
|         core::configManager.release(); | ||||
|     } | ||||
| @@ -179,7 +172,7 @@ namespace sourcemenu { | ||||
|  | ||||
|         if (running) { style::endDisabled(); } | ||||
|  | ||||
|         sigpath::sourceManager.showSelectedMenu(); | ||||
|         sigpath::sourceManager.showMenu(); | ||||
|  | ||||
|         if (ImGui::Checkbox("IQ Correction##_sdrpp_iq_corr", &iqCorrection)) { | ||||
|             sigpath::iqFrontEnd.setDCBlocking(iqCorrection); | ||||
|   | ||||
| @@ -146,7 +146,7 @@ namespace server { | ||||
|         // Load sourceId from config | ||||
|         sourceId = 0; | ||||
|         if (sourceList.keyExists(sourceName)) { sourceId = sourceList.keyId(sourceName); } | ||||
|         sigpath::sourceManager.selectSource(sourceList[sourceId]); | ||||
|         sigpath::sourceManager.select(sourceList[sourceId]); | ||||
|  | ||||
|         // TODO: Use command line option | ||||
|         std::string host = (std::string)core::args["addr"]; | ||||
| @@ -280,8 +280,7 @@ namespace server { | ||||
|             } | ||||
|         } | ||||
|         else if (cmd == COMMAND_START) { | ||||
|             sigpath::sourceManager.start(); | ||||
|             running = true; | ||||
|             running = sigpath::sourceManager.start(); | ||||
|         } | ||||
|         else if (cmd == COMMAND_STOP) { | ||||
|             sigpath::sourceManager.stop(); | ||||
| @@ -309,14 +308,14 @@ namespace server { | ||||
|         SmGui::FillWidth(); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::Combo("##sdrpp_server_src_sel", &sourceId, sourceList.txt)) { | ||||
|             sigpath::sourceManager.selectSource(sourceList[sourceId]); | ||||
|             sigpath::sourceManager.select(sourceList[sourceId]); | ||||
|             core::configManager.acquire(); | ||||
|             core::configManager.conf["source"] = sourceList.key(sourceId); | ||||
|             core::configManager.release(true); | ||||
|         } | ||||
|         if (running) { SmGui::EndDisabled(); } | ||||
|  | ||||
|         sigpath::sourceManager.showSelectedMenu(); | ||||
|         sigpath::sourceManager.showMenu(); | ||||
|     } | ||||
|  | ||||
|     void renderUI(SmGui::DrawList* dl, std::string diffId, SmGui::DrawListElem diffValue) { | ||||
|   | ||||
| @@ -1,106 +1,186 @@ | ||||
| #include <server.h> | ||||
| #include <signal_path/source.h> | ||||
| #include "source.h" | ||||
| #include <utils/flog.h> | ||||
| #include <signal_path/signal_path.h> | ||||
| #include <core.h> | ||||
|  | ||||
| SourceManager::SourceManager() { | ||||
| } | ||||
| void SourceManager::registerSource(const std::string& name, Source* source) { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
| void SourceManager::registerSource(std::string name, SourceHandler* handler) { | ||||
|     // Check arguments | ||||
|     if (source || name.empty()) { | ||||
|         flog::error("Invalid argument to register source", name); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Check that a source with that name doesn't already exist | ||||
|     if (sources.find(name) != sources.end()) { | ||||
|         flog::error("Tried to register new source with existing name: {0}", name); | ||||
|         flog::error("Tried to register source with existing name: {}", name); | ||||
|         return; | ||||
|     } | ||||
|     sources[name] = handler; | ||||
|     onSourceRegistered.emit(name); | ||||
|  | ||||
|     // Add source to map | ||||
|     sources[name] = source; | ||||
|  | ||||
|     // Add source to lists | ||||
|     sourceNames.push_back(name); | ||||
|     onSourceRegistered(name); | ||||
| } | ||||
|  | ||||
| void SourceManager::unregisterSource(std::string name) { | ||||
| void SourceManager::unregisterSource(const std::string& name) { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|     // Check that a source with that name exists | ||||
|     if (sources.find(name) == sources.end()) { | ||||
|         flog::error("Tried to unregister non existent source: {0}", name); | ||||
|         flog::error("Tried to unregister a non-existent source: {}", name); | ||||
|         return; | ||||
|     } | ||||
|     onSourceUnregister.emit(name); | ||||
|     if (name == selectedName) { | ||||
|         if (selectedHandler != NULL) { | ||||
|             sources[selectedName]->deselectHandler(sources[selectedName]->ctx); | ||||
|         } | ||||
|         sigpath::iqFrontEnd.setInput(&nullSource); | ||||
|         selectedHandler = NULL; | ||||
|     } | ||||
|  | ||||
|     // Notify event listeners of the imminent deletion | ||||
|     onSourceUnregister(name); | ||||
|  | ||||
|     // Delete from lists | ||||
|     sourceNames.erase(std::find(sourceNames.begin(), sourceNames.end(), name)); | ||||
|     sources.erase(name); | ||||
|     onSourceUnregistered.emit(name); | ||||
|  | ||||
|     // Notify event listeners of the deletion | ||||
|     onSourceUnregistered(name); | ||||
| } | ||||
|  | ||||
| std::vector<std::string> SourceManager::getSourceNames() { | ||||
|     std::vector<std::string> names; | ||||
|     for (auto const& [name, src] : sources) { names.push_back(name); } | ||||
|     return names; | ||||
| const std::vector<std::string>& SourceManager::getSourceNames() { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|     return sourceNames; | ||||
| } | ||||
|  | ||||
| void SourceManager::selectSource(std::string name) { | ||||
| void SourceManager::select(const std::string& name) { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|     // make sure that source isn't currently selected | ||||
|     if (selectedSourceName == name) { return; } | ||||
|  | ||||
|     // Deselect current source | ||||
|     deselect(); | ||||
|  | ||||
|     // Check that a source with that name exists | ||||
|     if (sources.find(name) == sources.end()) { | ||||
|         flog::error("Tried to select non existent source: {0}", name); | ||||
|         flog::error("Tried to select a non-existent source: {}", name); | ||||
|         return; | ||||
|     } | ||||
|     if (selectedHandler != NULL) { | ||||
|         sources[selectedName]->deselectHandler(sources[selectedName]->ctx); | ||||
|     } | ||||
|     selectedHandler = sources[name]; | ||||
|     selectedHandler->selectHandler(selectedHandler->ctx); | ||||
|     selectedName = name; | ||||
|     if (core::args["server"].b()) { | ||||
|         server::setInput(selectedHandler->stream); | ||||
|     } | ||||
|     else { | ||||
|         sigpath::iqFrontEnd.setInput(selectedHandler->stream); | ||||
|     } | ||||
|     // Set server input here | ||||
|  | ||||
|     // Select the source | ||||
|     selectedSourceName = name; | ||||
|     selectedSource = sources[name]; | ||||
|  | ||||
|     // Call the selected source | ||||
|     selectedSource->select(); | ||||
|  | ||||
|     // Retune to make sure the source has the latest frequency | ||||
|     tune(frequency); | ||||
| } | ||||
|  | ||||
| void SourceManager::showSelectedMenu() { | ||||
|     if (selectedHandler == NULL) { | ||||
|         return; | ||||
|     } | ||||
|     selectedHandler->menuHandler(selectedHandler->ctx); | ||||
| const std::string& SourceManager::getSelected() { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|     return selectedSourceName; | ||||
| } | ||||
|  | ||||
| void SourceManager::start() { | ||||
|     if (selectedHandler == NULL) { | ||||
|         return; | ||||
|     } | ||||
|     selectedHandler->startHandler(selectedHandler->ctx); | ||||
| bool SourceManager::start() { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|     // Check if not already running | ||||
|     if (running) { return true; } | ||||
|  | ||||
|     // Call source if selected and save if started | ||||
|     running = (!selectedSource) ? false : selectedSource->start(); | ||||
|  | ||||
|     return running; | ||||
| } | ||||
|  | ||||
| void SourceManager::stop() { | ||||
|     if (selectedHandler == NULL) { | ||||
|         return; | ||||
|     } | ||||
|     selectedHandler->stopHandler(selectedHandler->ctx); | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|     // Check if running | ||||
|     if (!running) { return; } | ||||
|  | ||||
|     // Call source if selected and save state | ||||
|     if (selectedSource) { selectedSource->stop(); } | ||||
|     running = false; | ||||
| } | ||||
|  | ||||
| bool SourceManager::isRunning() { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|     return running; | ||||
| } | ||||
|  | ||||
| void SourceManager::tune(double freq) { | ||||
|     if (selectedHandler == NULL) { | ||||
|         return; | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|     // Save frequency | ||||
|     frequency = freq; | ||||
|      | ||||
|     // Call source if selected | ||||
|     if (selectedSource) { | ||||
|         selectedSource->tune(((mode == TUNING_MODE_NORMAL) ? freq : ifFrequency) + offset); | ||||
|     } | ||||
|     // TODO: No need to always retune the hardware in panadpter mode | ||||
|     selectedHandler->tuneHandler(((tuneMode == TuningMode::NORMAL) ? freq : ifFreq) + tuneOffset, selectedHandler->ctx); | ||||
|     onRetune.emit(freq); | ||||
|     currentFreq = freq; | ||||
| } | ||||
|  | ||||
| void SourceManager::showMenu() { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|      | ||||
|     // Call source if selected | ||||
|     if (selectedSource) { selectedSource->showMenu(); } | ||||
| } | ||||
|  | ||||
| double SourceManager::getSamplerate() { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|     return samplerate; | ||||
| } | ||||
|  | ||||
| // =========== TODO: These functions should not happen in this class =========== | ||||
|  | ||||
| void SourceManager::setTuningOffset(double offset) { | ||||
|     tuneOffset = offset; | ||||
|     tune(currentFreq); | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|     // Update offset | ||||
|     this->offset = offset; | ||||
|  | ||||
|     // Retune to take affect | ||||
|     tune(frequency); | ||||
| } | ||||
|  | ||||
| void SourceManager::setTuningMode(TuningMode mode) { | ||||
|     tuneMode = mode; | ||||
|     tune(currentFreq); | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|     // Update mode | ||||
|     this->mode = mode; | ||||
|  | ||||
|     // Retune to take affect | ||||
|     tune(frequency); | ||||
| } | ||||
|  | ||||
| void SourceManager::setPanadpterIF(double freq) { | ||||
|     ifFreq = freq; | ||||
|     tune(currentFreq); | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|     // Update offset | ||||
|     ifFrequency = freq; | ||||
|  | ||||
|     // Return to take affect if in panadapter mode  | ||||
|     if (mode == TUNING_MODE_PANADAPTER) { tune(frequency); } | ||||
| } | ||||
|  | ||||
| // ============================================================================= | ||||
|  | ||||
| void SourceManager::deselect() { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|     // Call source if selected | ||||
|     if (selectedSource) { selectedSource->deselect(); } | ||||
|  | ||||
|     // Mark as deselected | ||||
|     selectedSourceName.clear(); | ||||
|     selectedSource = NULL; | ||||
| } | ||||
|  | ||||
| void SourceManager::setSamplerate(double samplerate) { | ||||
|     std::lock_guard<std::recursive_mutex> lck(mtx); | ||||
|  | ||||
|     // Save samplerate and emit event | ||||
|     this->samplerate = samplerate; | ||||
|     onSamplerateChanged(samplerate); | ||||
| } | ||||
| @@ -1,56 +1,153 @@ | ||||
| #pragma once | ||||
| #include <string> | ||||
| #include <vector> | ||||
| #include <functional> | ||||
| #include <map> | ||||
| #include <dsp/stream.h> | ||||
| #include <mutex> | ||||
| #include <dsp/types.h> | ||||
| #include <dsp/stream.h> | ||||
| #include <utils/event.h> | ||||
|  | ||||
| enum TuningMode { | ||||
|     TUNING_MODE_NORMAL, | ||||
|     TUNING_MODE_PANADAPTER | ||||
| }; | ||||
|  | ||||
| class Source; | ||||
|  | ||||
| class SourceManager { | ||||
|     friend Source; | ||||
| public: | ||||
|     SourceManager(); | ||||
|     /** | ||||
|      * Register a source. | ||||
|      * @param name Name of the source. | ||||
|      * @param source Pointer to the source instance. | ||||
|      */ | ||||
|     void registerSource(const std::string& name, Source* source); | ||||
|  | ||||
|     struct SourceHandler { | ||||
|         dsp::stream<dsp::complex_t>* stream; | ||||
|         void (*menuHandler)(void* ctx); | ||||
|         void (*selectHandler)(void* ctx); | ||||
|         void (*deselectHandler)(void* ctx); | ||||
|         void (*startHandler)(void* ctx); | ||||
|         void (*stopHandler)(void* ctx); | ||||
|         void (*tuneHandler)(double freq, void* ctx); | ||||
|         void* ctx; | ||||
|     }; | ||||
|     /** | ||||
|      * Unregister a source. | ||||
|      * @param name Name of the source. | ||||
|      */ | ||||
|     void unregisterSource(const std::string& name); | ||||
|  | ||||
|     enum TuningMode { | ||||
|         NORMAL, | ||||
|         PANADAPTER | ||||
|     }; | ||||
|     /** | ||||
|      * Get a list of source names. | ||||
|      * @return List of source names. | ||||
|      */ | ||||
|     const std::vector<std::string>& getSourceNames(); | ||||
|  | ||||
|     void registerSource(std::string name, SourceHandler* handler); | ||||
|     void unregisterSource(std::string name); | ||||
|     void selectSource(std::string name); | ||||
|     void showSelectedMenu(); | ||||
|     void start(); | ||||
|     /** | ||||
|      * Select a source. | ||||
|      * @param name Name of the source. | ||||
|      */ | ||||
|     void select(const std::string& name); | ||||
|  | ||||
|     /** | ||||
|      * Get the name of the currently selected source. | ||||
|      * @return Name of the source or empty if no source is selected. | ||||
|      */ | ||||
|     const std::string& getSelected(); | ||||
|      | ||||
|     /** | ||||
|      * Start the radio. | ||||
|      * @return True if the radio started successfully, false if not. | ||||
|      */ | ||||
|     bool start(); | ||||
|  | ||||
|     /** | ||||
|      * Stop the radio. | ||||
|      */ | ||||
|     void stop(); | ||||
|  | ||||
|     /** | ||||
|      * Check if the radio is running. | ||||
|      * @return True if the radio is running, false if not. | ||||
|      */ | ||||
|     bool isRunning(); | ||||
|  | ||||
|     /** | ||||
|      * Tune the radio. | ||||
|      * @param freq Frequency in Hz. | ||||
|      */ | ||||
|     void tune(double freq); | ||||
|  | ||||
|     /** | ||||
|      * Tune the radio. | ||||
|      * @param freq Frequency to tune the radio to. | ||||
|      */ | ||||
|     void showMenu(); | ||||
|  | ||||
|     /** | ||||
|      * Get the current samplerate of the radio. | ||||
|      * @return Samplerate in Hz. | ||||
|      */ | ||||
|     double getSamplerate(); | ||||
|  | ||||
|     // =========== TODO: These functions should not happen in this class =========== | ||||
|  | ||||
|     /** | ||||
|      * Set offset to add to the tuned frequency. | ||||
|      * @param offset Offset in Hz. | ||||
|      */ | ||||
|     void setTuningOffset(double offset); | ||||
|  | ||||
|     /** | ||||
|      * Set tuning mode. | ||||
|      * @param mode Tuning mode. | ||||
|      */ | ||||
|     void setTuningMode(TuningMode mode); | ||||
|  | ||||
|     /** | ||||
|      * Set panadapter mode IF frequency. | ||||
|      * @param freq IF frequency in Hz. | ||||
|      */ | ||||
|     void setPanadpterIF(double freq); | ||||
|  | ||||
|     std::vector<std::string> getSourceNames(); | ||||
|     // ============================================================================= | ||||
|  | ||||
|     // Emitted after a new source has been registered. | ||||
|     Event<std::string> onSourceRegistered; | ||||
|  | ||||
|     // Emitted when a source is about to be unregistered. | ||||
|     Event<std::string> onSourceUnregister; | ||||
|  | ||||
|     // Emitted after a source has been unregistered. | ||||
|     Event<std::string> onSourceUnregistered; | ||||
|  | ||||
|     // Emitted when the samplerate of the incoming IQ has changed. | ||||
|     Event<double> onSamplerateChanged; | ||||
|  | ||||
|     // Emitted when the source manager is instructed to tune the radio. | ||||
|     Event<double> onRetune; | ||||
|  | ||||
| private: | ||||
|     std::map<std::string, SourceHandler*> sources; | ||||
|     std::string selectedName; | ||||
|     SourceHandler* selectedHandler = NULL; | ||||
|     double tuneOffset; | ||||
|     double currentFreq; | ||||
|     double ifFreq = 0.0; | ||||
|     TuningMode tuneMode = TuningMode::NORMAL; | ||||
|     dsp::stream<dsp::complex_t> nullSource; | ||||
|     void deselect(); | ||||
|     void setSamplerate(double samplerate); | ||||
|      | ||||
|     std::vector<std::string> sourceNames; | ||||
|     std::map<std::string, Source*> sources; | ||||
|  | ||||
|     std::string selectedSourceName = ""; | ||||
|     Source* selectedSource = NULL; | ||||
|  | ||||
|     bool running = false; | ||||
|     double samplerate = 1e6; | ||||
|     double frequency = 100e6; | ||||
|     double offset = 0; | ||||
|     double ifFrequency = 8.830e6; | ||||
|     TuningMode mode = TUNING_MODE_NORMAL; | ||||
|  | ||||
|     std::recursive_mutex mtx; | ||||
| }; | ||||
|  | ||||
| class Source { | ||||
| public: | ||||
|     virtual void showMenu() {} | ||||
|     virtual void select() = 0; | ||||
|     virtual void deselect() {} | ||||
|     virtual bool start() = 0; | ||||
|     virtual void stop() = 0; | ||||
|     virtual void tune(double freq) {} | ||||
|  | ||||
|     dsp::stream<dsp::complex_t> stream; | ||||
| }; | ||||
| @@ -1,43 +1,51 @@ | ||||
| #pragma once | ||||
| #include <vector> | ||||
| #include <utils/flog.h> | ||||
| #include <functional> | ||||
| #include <stdexcept> | ||||
| #include <mutex> | ||||
| #include <map> | ||||
|  | ||||
| template <class T> | ||||
| struct EventHandler { | ||||
|     EventHandler() {} | ||||
|     EventHandler(void (*handler)(T, void*), void* ctx) { | ||||
|         this->handler = handler; | ||||
|         this->ctx = ctx; | ||||
|     } | ||||
| typedef int HandlerID; | ||||
|  | ||||
|     void (*handler)(T, void*); | ||||
|     void* ctx; | ||||
| }; | ||||
|  | ||||
| template <class T> | ||||
| template <typename... Args> | ||||
| class Event { | ||||
|     using Handler = std::function<void(Args...)>; | ||||
| public: | ||||
|     Event() {} | ||||
|     ~Event() {} | ||||
|  | ||||
|     void emit(T value) { | ||||
|         for (auto const& handler : handlers) { | ||||
|             handler->handler(value, handler->ctx); | ||||
|         } | ||||
|     HandlerID bind(Handler handler) { | ||||
|         std::lock_guard<std::mutex> lck(mtx); | ||||
|         HandlerID id = genID(); | ||||
|         handlers[id] = handler; | ||||
|         return id; | ||||
|     } | ||||
|  | ||||
|     void bindHandler(EventHandler<T>* handler) { | ||||
|         handlers.push_back(handler); | ||||
|     template<typename MHandler, class T> | ||||
|     HandlerID bind(MHandler handler, T* ctx) { | ||||
|         return bind([=](Args... args){ | ||||
|             (ctx->*handler)(args...); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     void unbindHandler(EventHandler<T>* handler) { | ||||
|         if (std::find(handlers.begin(), handlers.end(), handler) == handlers.end()) { | ||||
|             flog::error("Tried to remove a non-existent event handler"); | ||||
|             return; | ||||
|     void unbind(HandlerID id) { | ||||
|         std::lock_guard<std::mutex> lck(mtx); | ||||
|         if (handlers.find(id) == handlers.end()) { | ||||
|             throw std::runtime_error("Could not unbind handler, unknown ID"); | ||||
|         } | ||||
|         handlers.erase(id); | ||||
|     } | ||||
|  | ||||
|     void operator()(Args... args) { | ||||
|         std::lock_guard<std::mutex> lck(mtx); | ||||
|         for (const auto& [desc, handler] : handlers) { | ||||
|             handler(args...); | ||||
|         } | ||||
|         handlers.erase(std::remove(handlers.begin(), handlers.end(), handler), handlers.end()); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     std::vector<EventHandler<T>*> handlers; | ||||
|     HandlerID genID() { | ||||
|         int id; | ||||
|         for (id = 1; handlers.find(id) != handlers.end(); id++); | ||||
|         return id; | ||||
|     } | ||||
|  | ||||
|     std::map<HandlerID, Handler> handlers; | ||||
|     std::mutex mtx; | ||||
| }; | ||||
| @@ -169,7 +169,7 @@ namespace flog { | ||||
|             fprintf(outStream, "] %s\n", out.c_str()); | ||||
| #elif defined(__ANDROID__) | ||||
|             // Print format string | ||||
|             __android_log_print(TYPE_PRIORITIES[type], FLOG_ANDROID_TAG, COLOR_WHITE "[%02d/%02d/%02d %02d:%02d:%02d.%03d] [%s%s" COLOR_WHITE "] %s\n", | ||||
|             __android_log_buf_print(LOG_ID_DEFAULT, TYPE_PRIORITIES[type], FLOG_ANDROID_TAG, COLOR_WHITE "[%02d/%02d/%02d %02d:%02d:%02d.%03d] [%s%s" COLOR_WHITE "] %s\n", | ||||
|                     nowc->tm_mday, nowc->tm_mon + 1, nowc->tm_year + 1900, nowc->tm_hour, nowc->tm_min, nowc->tm_sec, 0, TYPE_COLORS[type], TYPE_STR[type], out.c_str()); | ||||
| #else | ||||
|             // Print format string | ||||
|   | ||||
| @@ -288,7 +288,6 @@ namespace net { | ||||
|  | ||||
|         // Save data | ||||
|         for (auto iface = addresses; iface; iface = iface->ifa_next) { | ||||
|             if (!iface->ifa_addr || !iface->ifa_netmask) { continue; } | ||||
|             if (iface->ifa_addr->sa_family != AF_INET) { continue; } | ||||
|             InterfaceInfo info; | ||||
|             info.address = ntohl(*(uint32_t*)&iface->ifa_addr->sa_data[2]); | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| #include <utils/networking.h> | ||||
| #include <assert.h> | ||||
| #include <utils/flog.h> | ||||
| #include <stdexcept> | ||||
|  | ||||
| namespace net { | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ class OptionList { | ||||
| public: | ||||
|     OptionList() { updateText(); } | ||||
|  | ||||
|     void define(const K& key, const std::string& name, const T& value) { | ||||
|     void define(K key, std::string name, T value) { | ||||
|         if (keyExists(key)) { throw std::runtime_error("Key already exists"); } | ||||
|         if (nameExists(name)) { throw std::runtime_error("Name already exists"); } | ||||
|         if (valueExists(value)) { throw std::runtime_error("Value already exists"); } | ||||
| @@ -18,27 +18,27 @@ public: | ||||
|         updateText(); | ||||
|     } | ||||
|  | ||||
|     void define(const std::string& name, const T& value) { | ||||
|     void define(std::string name, T value) { | ||||
|         define(name, name, value); | ||||
|     } | ||||
|  | ||||
|     void undefine(int id) { | ||||
|     void undefined(int id) { | ||||
|         keys.erase(keys.begin() + id); | ||||
|         names.erase(names.begin() + id); | ||||
|         values.erase(values.begin() + id); | ||||
|         updateText(); | ||||
|     } | ||||
|  | ||||
|     void undefineKey(const K& key) { | ||||
|         undefine(keyId(key)); | ||||
|     void undefineKey(K key) { | ||||
|         undefined(keyId(key)); | ||||
|     } | ||||
|  | ||||
|     void undefineName(const std::string& name) { | ||||
|         undefine(nameId(name)); | ||||
|     void undefineName(std::string name) { | ||||
|         undefined(nameId(name)); | ||||
|     } | ||||
|  | ||||
|     void undefineValue(const T& value) { | ||||
|         undefine(valueId(value)); | ||||
|     void undefineValue(T value) { | ||||
|         undefined(valueId(value)); | ||||
|     } | ||||
|  | ||||
|     void clear() { | ||||
| @@ -48,61 +48,61 @@ public: | ||||
|         updateText(); | ||||
|     } | ||||
|  | ||||
|     int size() const { | ||||
|     int size() { | ||||
|         return keys.size(); | ||||
|     } | ||||
|  | ||||
|     bool empty() const { | ||||
|     bool empty() { | ||||
|         return keys.empty(); | ||||
|     } | ||||
|  | ||||
|     bool keyExists(const K& key) const { | ||||
|     bool keyExists(K key) { | ||||
|         if (std::find(keys.begin(), keys.end(), key) != keys.end()) { return true; } | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     bool nameExists(const std::string& name) const { | ||||
|     bool nameExists(std::string name) { | ||||
|         if (std::find(names.begin(), names.end(), name) != names.end()) { return true; } | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     bool valueExists(const T& value) const { | ||||
|     bool valueExists(T value) { | ||||
|         if (std::find(values.begin(), values.end(), value) != values.end()) { return true; } | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     int keyId(const K& key) const { | ||||
|     int keyId(K key) { | ||||
|         auto it = std::find(keys.begin(), keys.end(), key); | ||||
|         if (it == keys.end()) { throw std::runtime_error("Key doesn't exists"); } | ||||
|         return std::distance(keys.begin(), it); | ||||
|     } | ||||
|  | ||||
|     int nameId(const std::string& name) const { | ||||
|     int nameId(std::string name) { | ||||
|         auto it = std::find(names.begin(), names.end(), name); | ||||
|         if (it == names.end()) { throw std::runtime_error("Name doesn't exists"); } | ||||
|         return std::distance(names.begin(), it); | ||||
|     } | ||||
|  | ||||
|     int valueId(const T& value) const { | ||||
|     int valueId(T value) { | ||||
|         auto it = std::find(values.begin(), values.end(), value); | ||||
|         if (it == values.end()) { throw std::runtime_error("Value doesn't exists"); } | ||||
|         return std::distance(values.begin(), it); | ||||
|     } | ||||
|  | ||||
|     inline const K& key(int id) const { | ||||
|     K key(int id) { | ||||
|         return keys[id]; | ||||
|     } | ||||
|  | ||||
|     inline const std::string& name(int id) const { | ||||
|     std::string name(int id) { | ||||
|         return names[id]; | ||||
|     } | ||||
|  | ||||
|     inline const T& value(int id) const { | ||||
|     T value(int id) { | ||||
|         return values[id]; | ||||
|     } | ||||
|  | ||||
|     inline const T& operator[](int& id) const { | ||||
|         return values[id]; | ||||
|     T operator[](int& id) { | ||||
|         return value(id); | ||||
|     } | ||||
|  | ||||
|     const char* txt = NULL; | ||||
|   | ||||
| @@ -214,10 +214,10 @@ private: | ||||
|  | ||||
|         if (ImGui::Checkbox(CONCAT("Show Reference Lines##m17_showlines_", _this->name), &_this->showLines)) { | ||||
|             if (_this->showLines) { | ||||
|                 _this->diag.lines.push_back(-1.0); | ||||
|                 _this->diag.lines.push_back(-1.0/3.0); | ||||
|                 _this->diag.lines.push_back(1.0/3.0); | ||||
|                 _this->diag.lines.push_back(1.0); | ||||
|                 _this->diag.lines.push_back(-0.75f); | ||||
|                 _this->diag.lines.push_back(-0.25f); | ||||
|                 _this->diag.lines.push_back(0.25f); | ||||
|                 _this->diag.lines.push_back(0.75f); | ||||
|             } | ||||
|             else { | ||||
|                 _this->diag.lines.clear(); | ||||
|   | ||||
| @@ -45,7 +45,6 @@ namespace demod { | ||||
|         virtual int getDefaultDeemphasisMode() = 0; | ||||
|         virtual bool getFMIFNRAllowed() = 0; | ||||
|         virtual bool getNBAllowed() = 0; | ||||
|         virtual bool getAFNRAllowed() = 0; | ||||
|         virtual dsp::stream<dsp::stereo_t>* getOutput() = 0; | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -86,7 +86,6 @@ namespace demod { | ||||
|         int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } | ||||
|         bool getFMIFNRAllowed() { return false; } | ||||
|         bool getNBAllowed() { return false; } | ||||
|         bool getAFNRAllowed() { return false; } | ||||
|         dsp::stream<dsp::stereo_t>* getOutput() { return &demod.out; } | ||||
|  | ||||
|     private: | ||||
|   | ||||
| @@ -92,7 +92,6 @@ namespace demod { | ||||
|         int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } | ||||
|         bool getFMIFNRAllowed() { return false; } | ||||
|         bool getNBAllowed() { return false; } | ||||
|         bool getAFNRAllowed() { return false; } | ||||
|         dsp::stream<dsp::stereo_t>* getOutput() { return &demod.out; } | ||||
|  | ||||
|     private: | ||||
|   | ||||
| @@ -79,7 +79,6 @@ namespace demod { | ||||
|         int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } | ||||
|         bool getFMIFNRAllowed() { return false; } | ||||
|         bool getNBAllowed() { return true; } | ||||
|         bool getAFNRAllowed() { return false; } | ||||
|         dsp::stream<dsp::stereo_t>* getOutput() { return &demod.out; } | ||||
|  | ||||
|     private: | ||||
|   | ||||
| @@ -79,7 +79,6 @@ namespace demod { | ||||
|         int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } | ||||
|         bool getFMIFNRAllowed() { return false; } | ||||
|         bool getNBAllowed() { return true; } | ||||
|         bool getAFNRAllowed() { return false; } | ||||
|         dsp::stream<dsp::stereo_t>* getOutput() { return &demod.out; } | ||||
|  | ||||
|     private: | ||||
|   | ||||
| @@ -19,17 +19,15 @@ namespace demod { | ||||
|  | ||||
|             // Load config | ||||
|             _config->acquire(); | ||||
|             bool modified = false; | ||||
|             if (config->conf[name][getName()].contains("lowPass")) { | ||||
|                 _lowPass = config->conf[name][getName()]["lowPass"]; | ||||
|             } | ||||
|             if (config->conf[name][getName()].contains("highPass")) { | ||||
|                 _highPass = config->conf[name][getName()]["highPass"]; | ||||
|             } | ||||
|             _config->release(); | ||||
|             _config->release(modified); | ||||
|  | ||||
|  | ||||
|             // Define structure | ||||
|             demod.init(input, getIFSampleRate(), bandwidth, _lowPass, _highPass); | ||||
|             demod.init(input, getIFSampleRate(), bandwidth, _lowPass); | ||||
|         } | ||||
|  | ||||
|         void start() { demod.start(); } | ||||
| @@ -43,12 +41,6 @@ namespace demod { | ||||
|                 _config->conf[name][getName()]["lowPass"] = _lowPass; | ||||
|                 _config->release(true); | ||||
|             } | ||||
|             if (ImGui::Checkbox(("High Pass##_radio_wfm_highpass_" + name).c_str(), &_highPass)) { | ||||
|                 demod.setHighPass(_highPass); | ||||
|                 _config->acquire(); | ||||
|                 _config->conf[name][getName()]["highPass"] = _highPass; | ||||
|                 _config->release(true); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         void setBandwidth(double bandwidth) { | ||||
| @@ -75,7 +67,6 @@ namespace demod { | ||||
|         int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } | ||||
|         bool getFMIFNRAllowed() { return true; } | ||||
|         bool getNBAllowed() { return false; } | ||||
|         bool getAFNRAllowed() { return false; } | ||||
|         dsp::stream<dsp::stereo_t>* getOutput() { return &demod.out; } | ||||
|  | ||||
|     private: | ||||
| @@ -84,7 +75,6 @@ namespace demod { | ||||
|         ConfigManager* _config = NULL; | ||||
|  | ||||
|         bool _lowPass = true; | ||||
|         bool _highPass = false; | ||||
|  | ||||
|         std::string name; | ||||
|     }; | ||||
|   | ||||
| @@ -59,7 +59,6 @@ namespace demod { | ||||
|         int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } | ||||
|         bool getFMIFNRAllowed() { return false; } | ||||
|         bool getNBAllowed() { return true; } | ||||
|         bool getAFNRAllowed() { return false; } | ||||
|         dsp::stream<dsp::stereo_t>* getOutput() { return &c2s.out; } | ||||
|  | ||||
|     private: | ||||
|   | ||||
| @@ -80,7 +80,6 @@ namespace demod { | ||||
|         int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } | ||||
|         bool getFMIFNRAllowed() { return false; } | ||||
|         bool getNBAllowed() { return true; } | ||||
|         bool getAFNRAllowed() { return true; } | ||||
|         dsp::stream<dsp::stereo_t>* getOutput() { return &demod.out; } | ||||
|  | ||||
|     private: | ||||
|   | ||||
| @@ -130,7 +130,6 @@ namespace demod { | ||||
|         int getDefaultDeemphasisMode() { return DEEMP_MODE_50US; } | ||||
|         bool getFMIFNRAllowed() { return true; } | ||||
|         bool getNBAllowed() { return false; } | ||||
|         bool getAFNRAllowed() { return false; } | ||||
|         dsp::stream<dsp::stereo_t>* getOutput() { return &demod.out; } | ||||
|  | ||||
|         // ============= DEDICATED FUNCTIONS ============= | ||||
|   | ||||
| @@ -9,7 +9,6 @@ | ||||
| #include <dsp/noise_reduction/noise_blanker.h> | ||||
| #include <dsp/noise_reduction/fm_if.h> | ||||
| #include <dsp/noise_reduction/squelch.h> | ||||
| #include <dsp/noise_reduction/audio.h> | ||||
| #include <dsp/multirate/rational_resampler.h> | ||||
| #include <dsp/filter/deephasis.h> | ||||
| #include <core.h> | ||||
| @@ -84,11 +83,9 @@ public: | ||||
|  | ||||
|         resamp.init(NULL, 250000.0, 48000.0); | ||||
|         deemp.init(NULL, 50e-6, 48000.0); | ||||
|         afNR.init(NULL, 1024); | ||||
|  | ||||
|         afChain.addBlock(&resamp, true); | ||||
|         afChain.addBlock(&deemp, false); | ||||
|         afChain.addBlock(&afNR, false); | ||||
|  | ||||
|         // Initialize the sink | ||||
|         srChangeHandler.ctx = this; | ||||
| @@ -250,12 +247,6 @@ private: | ||||
|             if (!_this->nbEnabled && _this->enabled) { style::endDisabled(); } | ||||
|         } | ||||
|          | ||||
|         // Noise reduction | ||||
|         if (_this->afNRAllowed) { | ||||
|             if (ImGui::Checkbox(("Audio Noise Reduction##_radio_afnr_ena_" + _this->name).c_str(), &_this->afNREnabled)) { | ||||
|                 _this->setAFNREnabled(_this->afNREnabled); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Squelch | ||||
|         if (ImGui::Checkbox(("Squelch##_radio_sqelch_ena_" + _this->name).c_str(), &_this->squelchEnabled)) { | ||||
| @@ -379,8 +370,6 @@ private: | ||||
|         fmIFPresetId = ifnrPresets.valueId(IFNR_PRESET_VOICE); | ||||
|         nbAllowed = selectedDemod->getNBAllowed(); | ||||
|         nbEnabled = false; | ||||
|         afNRAllowed = selectedDemod->getAFNRAllowed(); | ||||
|         afNREnabled = false; | ||||
|         nbLevel = 0.0f; | ||||
|         double ifSamplerate = selectedDemod->getIFSampleRate(); | ||||
|         config.acquire(); | ||||
| @@ -422,9 +411,6 @@ private: | ||||
|         if (config.conf[name][selectedDemod->getName()].contains("noiseBlankerLevel")) { | ||||
|             nbLevel = config.conf[name][selectedDemod->getName()]["noiseBlankerLevel"]; | ||||
|         } | ||||
|         if (config.conf[name][selectedDemod->getName()].contains("audioNoiseReductionEnabled")) { | ||||
|             nbEnabled = config.conf[name][selectedDemod->getName()]["audioNoiseReductionEnabled"]; | ||||
|         } | ||||
|         config.release(); | ||||
|  | ||||
|         // Configure VFO | ||||
| @@ -460,10 +446,7 @@ private: | ||||
|             afChain.enableBlock(&resamp, [=](dsp::stream<dsp::stereo_t>* out){ stream.setInput(out); }); | ||||
|  | ||||
|             // Configure deemphasis | ||||
|             setDeemphasisMode(deempAllowed ? deempModes[deempId] : DEEMP_MODE_NONE); | ||||
|  | ||||
|             // Configure AF NR | ||||
|             setAFNREnabled(afNRAllowed && afNREnabled); | ||||
|             setDeemphasisMode(deempModes[deempId]); | ||||
|         } | ||||
|         else { | ||||
|             // Disable everything if post processing is disabled | ||||
| @@ -525,17 +508,6 @@ private: | ||||
|         config.release(true); | ||||
|     } | ||||
|  | ||||
|     void setAFNREnabled(bool enable) { | ||||
|         afNREnabled = enable; | ||||
|         if (!postProcEnabled || !selectedDemod) { return; } | ||||
|         afChain.setBlockEnabled(&afNR, afNREnabled, [=](dsp::stream<dsp::stereo_t>* out){ stream.setInput(out); }); | ||||
|  | ||||
|         // Save config | ||||
|         config.acquire(); | ||||
|         config.conf[name][selectedDemod->getName()]["audioNoiseReductionEnabled"] = nbEnabled; | ||||
|         config.release(true); | ||||
|     } | ||||
|  | ||||
|     void setNBEnabled(bool enable) { | ||||
|         nbEnabled = enable; | ||||
|         if (!selectedDemod) { return; } | ||||
| @@ -688,7 +660,6 @@ private: | ||||
|     dsp::chain<dsp::stereo_t> afChain; | ||||
|     dsp::multirate::RationalResampler<dsp::stereo_t> resamp; | ||||
|     dsp::filter::Deemphasis<dsp::stereo_t> deemp; | ||||
|     dsp::noise_reduction::Audio afNR; | ||||
|  | ||||
|     SinkManager::Stream stream; | ||||
|  | ||||
| @@ -712,9 +683,6 @@ private: | ||||
|     int deempId = 0; | ||||
|     bool deempAllowed; | ||||
|  | ||||
|     bool afNREnabled = false; | ||||
|     bool afNRAllowed; | ||||
|  | ||||
|     bool FMIFNRAllowed; | ||||
|     bool FMIFNREnabled = false; | ||||
|     int fmIFPresetId; | ||||
|   | ||||
| @@ -23,8 +23,7 @@ | ||||
| #include <utils/wav.h> | ||||
|  | ||||
| #define CONCAT(a, b) ((std::string(a) + b).c_str()) | ||||
|  | ||||
| #define SILENCE_LVL 10e-6 | ||||
| #define SILENCE_LVL 10e-20 | ||||
|  | ||||
| SDRPP_MOD_INFO{ | ||||
|     /* Name:            */ "recorder", | ||||
| @@ -316,11 +315,11 @@ private: | ||||
|             } | ||||
|             if (_this->recording) { style::endDisabled(); } | ||||
|  | ||||
|             if (ImGui::Checkbox(CONCAT("Ignore silence##_recorder_ignore_silence_", _this->name), &_this->ignoreSilence)) { | ||||
|                 config.acquire(); | ||||
|                 config.conf[_this->name]["ignoreSilence"] = _this->ignoreSilence; | ||||
|                 config.release(true); | ||||
|             } | ||||
|             // if (ImGui::Checkbox(CONCAT("Ignore silence##_recorder_ignore_silence_", _this->name), &_this->ignoreSilence)) { | ||||
|             //     config.acquire(); | ||||
|             //     config.conf[_this->name]["ignoreSilence"] = _this->ignoreSilence; | ||||
|             //     config.release(true); | ||||
|             // } | ||||
|         } | ||||
|  | ||||
|         // Record button | ||||
| @@ -339,13 +338,7 @@ private: | ||||
|             uint64_t seconds = _this->writer.getSamplesWritten() / _this->samplerate; | ||||
|             time_t diff = seconds; | ||||
|             tm* dtm = gmtime(&diff); | ||||
|  | ||||
|             if (_this->ignoreSilence && _this->ignoringSilence) { | ||||
|                 ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Paused %02d:%02d:%02d", dtm->tm_hour, dtm->tm_min, dtm->tm_sec); | ||||
|             } | ||||
|             else { | ||||
|                 ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Recording %02d:%02d:%02d", dtm->tm_hour, dtm->tm_min, dtm->tm_sec); | ||||
|             } | ||||
|             ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Recording %02d:%02d:%02d", dtm->tm_hour, dtm->tm_min, dtm->tm_sec); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -487,31 +480,13 @@ private: | ||||
|  | ||||
|     static void stereoHandler(dsp::stereo_t* data, int count, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|         if (_this->ignoreSilence) { | ||||
|             float absMax = 0.0f; | ||||
|             float* _data = (float*)data; | ||||
|             int _count = count * 2; | ||||
|             for (int i = 0; i < _count; i++) { | ||||
|                 float val = fabsf(_data[i]); | ||||
|                 if (val > absMax) { absMax = val; } | ||||
|             } | ||||
|             _this->ignoringSilence = (absMax < SILENCE_LVL); | ||||
|             if (_this->ignoringSilence) { return; } | ||||
|         } | ||||
|         // TODO: Ignore silence | ||||
|         _this->writer.write((float*)data, count); | ||||
|     } | ||||
|  | ||||
|     static void monoHandler(float* data, int count, void* ctx) { | ||||
|         RecorderModule* _this = (RecorderModule*)ctx; | ||||
|         if (_this->ignoreSilence) { | ||||
|             float absMax = 0.0f; | ||||
|             for (int i = 0; i < count; i++) { | ||||
|                 float val = fabsf(data[i]); | ||||
|                 if (val > absMax) { absMax = val; } | ||||
|             } | ||||
|             _this->ignoringSilence = (absMax < SILENCE_LVL); | ||||
|             if (_this->ignoringSilence) { return; } | ||||
|         } | ||||
|         // TODO: Ignore silence | ||||
|         _this->writer.write(data, count); | ||||
|     } | ||||
|  | ||||
| @@ -554,7 +529,6 @@ private: | ||||
|     dsp::stereo_t audioLvl = { -100.0f, -100.0f }; | ||||
|  | ||||
|     bool recording = false; | ||||
|     bool ignoringSilence = false; | ||||
|     wav::Writer writer; | ||||
|     std::recursive_mutex recMtx; | ||||
|     dsp::stream<dsp::complex_t>* basebandStream; | ||||
|   | ||||
| @@ -45,9 +45,6 @@ public: | ||||
|         } | ||||
|         config.release(); | ||||
|  | ||||
|         _retuneHandler.ctx = this; | ||||
|         _retuneHandler.handler = retuneHandler; | ||||
|  | ||||
|         gui::menu.registerEntry(name, menuHandler, this, NULL); | ||||
|     } | ||||
|  | ||||
| @@ -87,8 +84,8 @@ public: | ||||
|  | ||||
|         // Switch source to panadapter mode | ||||
|         sigpath::sourceManager.setPanadpterIF(ifFreq); | ||||
|         sigpath::sourceManager.setTuningMode(SourceManager::TuningMode::PANADAPTER); | ||||
|         sigpath::sourceManager.onRetune.bindHandler(&_retuneHandler); | ||||
|         sigpath::sourceManager.setTuningMode(TUNING_MODE_PANADAPTER); | ||||
|         retuneHandlerId = sigpath::sourceManager.onRetune.bind(retuneHandler, this); | ||||
|  | ||||
|         running = true; | ||||
|     } | ||||
| @@ -98,8 +95,8 @@ public: | ||||
|         if (!running) { return; } | ||||
|  | ||||
|         // Switch source back to normal mode | ||||
|         sigpath::sourceManager.onRetune.unbindHandler(&_retuneHandler); | ||||
|         sigpath::sourceManager.setTuningMode(SourceManager::TuningMode::NORMAL); | ||||
|         sigpath::sourceManager.onRetune.unbind(retuneHandlerId); | ||||
|         sigpath::sourceManager.setTuningMode(TUNING_MODE_NORMAL); | ||||
|  | ||||
|         // Disconnect from rigctl server | ||||
|         client->close(); | ||||
| @@ -159,10 +156,9 @@ private: | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static void retuneHandler(double freq, void* ctx) { | ||||
|         RigctlClientModule* _this = (RigctlClientModule*)ctx; | ||||
|         if (!_this->client || !_this->client->isOpen()) { return; } | ||||
|         if (_this->client->setFreq(freq)) { | ||||
|     void retuneHandler(double freq) { | ||||
|         if (!client || !client->isOpen()) { return; } | ||||
|         if (client->setFreq(freq)) { | ||||
|             flog::error("Could not set frequency"); | ||||
|         } | ||||
|     } | ||||
| @@ -178,7 +174,7 @@ private: | ||||
|  | ||||
|     double ifFreq = 8830000.0; | ||||
|  | ||||
|     EventHandler<double> _retuneHandler; | ||||
|     HandlerID retuneHandlerId; | ||||
| }; | ||||
|  | ||||
| MOD_EXPORT void _INIT_() { | ||||
|   | ||||
							
								
								
									
										11
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								readme.md
									
									
									
									
									
								
							| @@ -429,34 +429,25 @@ I will soon publish a contributing.md listing the code style to use. | ||||
| * Dale L Puckett (K0HYD) | ||||
| * [Daniele D'Agnelli](https://linkedin.com/in/dagnelli) | ||||
| * D. Jones | ||||
| * Dexruus | ||||
| * [EB3FRN](https://www.eb3frn.net/) | ||||
| * Eric Johnson | ||||
| * Ernest Murphy (NH7L) | ||||
| * Flinger Films | ||||
| * [Frank Werner (HB9FXQ)](https://twitter.com/HB9FXQ) | ||||
| * gringogrigio | ||||
| * Jeff Moe | ||||
| * Joe Cupano | ||||
| * KD1SQ | ||||
| * Kezza | ||||
| * Krys Kamieniecki | ||||
| * Lee Donaghy | ||||
| * Lee KD1SQ | ||||
| * .lozenge. (Hank Hill) | ||||
| * Martin Herren (HB9FXX) | ||||
| * ON4MU | ||||
| * [Passion-Radio.com](https://passion-radio.com/) | ||||
| * Paul Maine | ||||
| * Peter Betz | ||||
| * [Scanner School](https://scannerschool.com/) | ||||
| * Scott Palmer | ||||
| * [SignalsEverywhere](https://signalseverywhere.com/) | ||||
| * Syne Ardwin (WI9SYN) | ||||
| * [W4IPA](https://twitter.com/W4IPAstroke5) | ||||
| * William Arcand (W1WRA) | ||||
| * [Yves Rougy](https://www.twitch.tv/yorzian) | ||||
| * [Zipper](https://github.com/reppiZ) | ||||
| * [Zipper](github.com/reppiZ) | ||||
|  | ||||
| ## Contributors | ||||
|  | ||||
|   | ||||
| @@ -1,274 +0,0 @@ | ||||
| { | ||||
|   "name": "Belgium", | ||||
|   "country_name": "Belgium", | ||||
|   "country_code": "BE", | ||||
|   "author_name": "Bastien Cabay - ON4BCY", | ||||
|   "author_url": "https://qrz.com/db/ON4BCY", | ||||
|   "bands": [ | ||||
|     { | ||||
|       "name": "2200m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 135700, | ||||
|       "end": 137800 | ||||
|     }, | ||||
|     { | ||||
|       "name": "630m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 472000, | ||||
|       "end": 479000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "600m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 501000, | ||||
|       "end": 504000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "AM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 526500, | ||||
|       "end": 1606500 | ||||
|     }, | ||||
|     { | ||||
|       "name": "160m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 1810000, | ||||
|       "end": 2000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "80m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 3500000, | ||||
|       "end": 3800000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "60m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 5351500, | ||||
|       "end": 5366500 | ||||
|     }, | ||||
|     { | ||||
|       "name": "AM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 5950000, | ||||
|       "end": 6200000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "40m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 7000000, | ||||
|       "end": 7200000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "AM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 7200000, | ||||
|       "end": 7300000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "AM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 9500000, | ||||
|       "end": 9900000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "30m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 10100000, | ||||
|       "end": 10150000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "AM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 11650000, | ||||
|       "end": 12050000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "AM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 13600000, | ||||
|       "end": 13800000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "20m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 14000000, | ||||
|       "end": 14350000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "AM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 15100000, | ||||
|       "end": 15600000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "AM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 17550000, | ||||
|       "end": 17900000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "17m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 18068000, | ||||
|       "end": 18168000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "15m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 21000000, | ||||
|       "end": 21450000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "AM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 21450000, | ||||
|       "end": 21850000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "12m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 24890000, | ||||
|       "end": 24990000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "AM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 25670000, | ||||
|       "end": 26100000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "11m - Citizen Band", | ||||
|       "type": "amateur", | ||||
|       "start": 26960000, | ||||
|       "end": 27410000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "10m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 28000000, | ||||
|       "end": 29700000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "6m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 50000000, | ||||
|       "end": 52000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "4m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 69945000, | ||||
|       "end": 69955000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "4m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 70190000, | ||||
|       "end": 70412500 | ||||
|     }, | ||||
|     { | ||||
|       "name": "FM Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 87500000, | ||||
|       "end": 108000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "Space Exploration / Meteorology Sat. / S-PCS", | ||||
|       "type": "satellite", | ||||
|       "start": 137000000, | ||||
|       "end": 138000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "2m - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 144000000, | ||||
|       "end": 146000000 | ||||
|     }, | ||||
|  | ||||
|     { | ||||
|       "name": "T-DAB Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 174000000, | ||||
|       "end": 223000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "70cm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 430000000, | ||||
|       "end": 440000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "PMR446", | ||||
|       "type": "amateur", | ||||
|       "start": 446000000, | ||||
|       "end": 446200000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "DVB-T - Broadcast", | ||||
|       "type": "broadcast", | ||||
|       "start": 470000000, | ||||
|       "end": 790000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "23cm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 1240000000, | ||||
|       "end": 1300000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "13cm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 2300000000, | ||||
|       "end": 2450000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "6cm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 5650000000, | ||||
|       "end": 5850000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "3cm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 10000000000, | ||||
|       "end": 10500000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "1.25cm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 24000000000, | ||||
|       "end": 24250000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "6mm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 47000000000, | ||||
|       "end": 47200000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "4mm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 75500000000, | ||||
|       "end": 81000000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "2.5mm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 122250000000, | ||||
|       "end": 123000000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "2mm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 142000000000, | ||||
|       "end": 149000000000 | ||||
|     }, | ||||
|     { | ||||
|       "name": "1mm - Amateur", | ||||
|       "type": "amateur", | ||||
|       "start": 241000000000, | ||||
|       "end": 250000000000 | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -25,7 +25,7 @@ SDRPP_MOD_INFO{ | ||||
|  | ||||
| ConfigManager config; | ||||
|  | ||||
| class AirspySourceModule : public ModuleManager::Instance { | ||||
| class AirspySourceModule : public ModuleManager::Instance, public Source { | ||||
| public: | ||||
|     AirspySourceModule(std::string name) { | ||||
|         this->name = name; | ||||
| @@ -34,15 +34,6 @@ public: | ||||
|  | ||||
|         sampleRate = 10000000.0; | ||||
|  | ||||
|         handler.ctx = this; | ||||
|         handler.selectHandler = menuSelected; | ||||
|         handler.deselectHandler = menuDeselected; | ||||
|         handler.menuHandler = menuHandler; | ||||
|         handler.startHandler = start; | ||||
|         handler.stopHandler = stop; | ||||
|         handler.tuneHandler = tune; | ||||
|         handler.stream = &stream; | ||||
|  | ||||
|         refresh(); | ||||
|         if (sampleRateList.size() > 0) { | ||||
|             sampleRate = sampleRateList[0]; | ||||
| @@ -54,11 +45,11 @@ public: | ||||
|         config.release(); | ||||
|         selectByString(devSerial); | ||||
|  | ||||
|         sigpath::sourceManager.registerSource("Airspy", &handler); | ||||
|         sigpath::sourceManager.registerSource("Airspy", this); | ||||
|     } | ||||
|  | ||||
|     ~AirspySourceModule() { | ||||
|         stop(this); | ||||
|         stop(); | ||||
|         sigpath::sourceManager.unregisterSource("Airspy"); | ||||
|         airspy_exit(); | ||||
|     } | ||||
| @@ -231,6 +222,315 @@ public: | ||||
|         airspy_close(dev); | ||||
|     } | ||||
|  | ||||
|         void select() { | ||||
|         core::setInputSampleRate(sampleRate); | ||||
|         flog::info("AirspySourceModule '{0}': Select!", name); | ||||
|     } | ||||
|  | ||||
|     void deselect() { | ||||
|         flog::info("AirspySourceModule '{0}': Deselect!", name); | ||||
|     } | ||||
|  | ||||
|     bool start() { | ||||
|         if (running) { return true; } | ||||
|         if (selectedSerial == 0) { | ||||
|             flog::error("Tried to start Airspy source with null serial"); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
| #ifndef __ANDROID__ | ||||
|         int err = airspy_open_sn(&openDev, selectedSerial); | ||||
| #else | ||||
|         int err = airspy_open_fd(&openDev, devFd); | ||||
| #endif | ||||
|         if (err != 0) { | ||||
|             char buf[1024]; | ||||
|             sprintf(buf, "%016" PRIX64, selectedSerial); | ||||
|             flog::error("Could not open Airspy {0}", buf); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         airspy_set_samplerate(openDev, sampleRateList[srId]); | ||||
|         airspy_set_freq(openDev, freq); | ||||
|  | ||||
|         if (gainMode == 0) { | ||||
|             airspy_set_lna_agc(openDev, 0); | ||||
|             airspy_set_mixer_agc(openDev, 0); | ||||
|             airspy_set_sensitivity_gain(openDev, sensitiveGain); | ||||
|         } | ||||
|         else if (gainMode == 1) { | ||||
|             airspy_set_lna_agc(openDev, 0); | ||||
|             airspy_set_mixer_agc(openDev, 0); | ||||
|             airspy_set_linearity_gain(openDev, linearGain); | ||||
|         } | ||||
|         else if (gainMode == 2) { | ||||
|             if (lnaAgc) { | ||||
|                 airspy_set_lna_agc(openDev, 1); | ||||
|             } | ||||
|             else { | ||||
|                 airspy_set_lna_agc(openDev, 0); | ||||
|                 airspy_set_lna_gain(openDev, lnaGain); | ||||
|             } | ||||
|             if (mixerAgc) { | ||||
|                 airspy_set_mixer_agc(openDev, 1); | ||||
|             } | ||||
|             else { | ||||
|                 airspy_set_mixer_agc(openDev, 0); | ||||
|                 airspy_set_mixer_gain(openDev, mixerGain); | ||||
|             } | ||||
|             airspy_set_vga_gain(openDev, vgaGain); | ||||
|         } | ||||
|  | ||||
|         airspy_set_rf_bias(openDev, biasT); | ||||
|  | ||||
|         airspy_start_rx(openDev, callback, this); | ||||
|  | ||||
|         running = true; | ||||
|         flog::info("AirspySourceModule '{0}': Start!", name); | ||||
|     } | ||||
|  | ||||
|     void stop() { | ||||
|         if (!running) { return; } | ||||
|         running = false; | ||||
|         stream.stopWriter(); | ||||
|         airspy_close(openDev); | ||||
|         stream.clearWriteStop(); | ||||
|         flog::info("AirspySourceModule '{0}': Stop!", name); | ||||
|     } | ||||
|  | ||||
|     void tune(double freq) { | ||||
|         this->freq = freq; | ||||
|         if (running) { | ||||
|             airspy_set_freq(openDev, freq); | ||||
|         } | ||||
|         flog::info("AirspySourceModule '{0}': Tune: {1}!", name, freq); | ||||
|     } | ||||
|  | ||||
|     void showMenu() { | ||||
|         if (running) { SmGui::BeginDisabled(); } | ||||
|  | ||||
|         SmGui::FillWidth(); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::Combo(CONCAT("##_airspy_dev_sel_", name), &devId, devListTxt.c_str())) { | ||||
|             selectBySerial(devList[devId]); | ||||
|             core::setInputSampleRate(sampleRate); | ||||
|             if (selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["device"] = selectedSerStr; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (SmGui::Combo(CONCAT("##_airspy_sr_sel_", name), &srId, sampleRateListTxt.c_str())) { | ||||
|             sampleRate = sampleRateList[srId]; | ||||
|             core::setInputSampleRate(sampleRate); | ||||
|             if (selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["devices"][selectedSerStr]["sampleRate"] = sampleRate; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         SmGui::SameLine(); | ||||
|         SmGui::FillWidth(); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::Button(CONCAT("Refresh##_airspy_refr_", name))) { | ||||
|             refresh(); | ||||
|             config.acquire(); | ||||
|             std::string devSerial = config.conf["device"]; | ||||
|             config.release(); | ||||
|             selectByString(devSerial); | ||||
|             core::setInputSampleRate(sampleRate); | ||||
|         } | ||||
|  | ||||
|         if (running) { SmGui::EndDisabled(); } | ||||
|  | ||||
|         SmGui::BeginGroup(); | ||||
|         SmGui::Columns(3, CONCAT("AirspyGainModeColumns##_", name), false); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::RadioButton(CONCAT("Sensitive##_airspy_gm_", name), gainMode == 0)) { | ||||
|             gainMode = 0; | ||||
|             if (running) { | ||||
|                 airspy_set_lna_agc(openDev, 0); | ||||
|                 airspy_set_mixer_agc(openDev, 0); | ||||
|                 airspy_set_sensitivity_gain(openDev, sensitiveGain); | ||||
|             } | ||||
|             if (selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["devices"][selectedSerStr]["gainMode"] = 0; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|         SmGui::NextColumn(); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::RadioButton(CONCAT("Linear##_airspy_gm_", name), gainMode == 1)) { | ||||
|             gainMode = 1; | ||||
|             if (running) { | ||||
|                 airspy_set_lna_agc(openDev, 0); | ||||
|                 airspy_set_mixer_agc(openDev, 0); | ||||
|                 airspy_set_linearity_gain(openDev, linearGain); | ||||
|             } | ||||
|             if (selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["devices"][selectedSerStr]["gainMode"] = 1; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|         SmGui::NextColumn(); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::RadioButton(CONCAT("Free##_airspy_gm_", name), gainMode == 2)) { | ||||
|             gainMode = 2; | ||||
|             if (running) { | ||||
|                 if (lnaAgc) { | ||||
|                     airspy_set_lna_agc(openDev, 1); | ||||
|                 } | ||||
|                 else { | ||||
|                     airspy_set_lna_agc(openDev, 0); | ||||
|                     airspy_set_lna_gain(openDev, lnaGain); | ||||
|                 } | ||||
|                 if (mixerAgc) { | ||||
|                     airspy_set_mixer_agc(openDev, 1); | ||||
|                 } | ||||
|                 else { | ||||
|                     airspy_set_mixer_agc(openDev, 0); | ||||
|                     airspy_set_mixer_gain(openDev, mixerGain); | ||||
|                 } | ||||
|                 airspy_set_vga_gain(openDev, vgaGain); | ||||
|             } | ||||
|             if (selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["devices"][selectedSerStr]["gainMode"] = 2; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|         SmGui::Columns(1, CONCAT("EndAirspyGainModeColumns##_", name), false); | ||||
|         SmGui::EndGroup(); | ||||
|  | ||||
|         // Gain menus | ||||
|  | ||||
|         if (gainMode == 0) { | ||||
|             SmGui::LeftLabel("Gain"); | ||||
|             SmGui::FillWidth(); | ||||
|             if (SmGui::SliderInt(CONCAT("##_airspy_sens_gain_", name), &sensitiveGain, 0, 21)) { | ||||
|                 if (running) { | ||||
|                     airspy_set_sensitivity_gain(openDev, sensitiveGain); | ||||
|                 } | ||||
|                 if (selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][selectedSerStr]["sensitiveGain"] = sensitiveGain; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         else if (gainMode == 1) { | ||||
|             SmGui::LeftLabel("Gain"); | ||||
|             SmGui::FillWidth(); | ||||
|             if (SmGui::SliderInt(CONCAT("##_airspy_lin_gain_", name), &linearGain, 0, 21)) { | ||||
|                 if (running) { | ||||
|                     airspy_set_linearity_gain(openDev, linearGain); | ||||
|                 } | ||||
|                 if (selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][selectedSerStr]["linearGain"] = linearGain; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         else if (gainMode == 2) { | ||||
|             // TODO: Switch to a table for alignment | ||||
|             if (lnaAgc) { SmGui::BeginDisabled(); } | ||||
|             SmGui::LeftLabel("LNA Gain"); | ||||
|             SmGui::FillWidth(); | ||||
|             if (SmGui::SliderInt(CONCAT("##_airspy_lna_gain_", name), &lnaGain, 0, 15)) { | ||||
|                 if (running) { | ||||
|                     airspy_set_lna_gain(openDev, lnaGain); | ||||
|                 } | ||||
|                 if (selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][selectedSerStr]["lnaGain"] = lnaGain; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|             if (lnaAgc) { SmGui::EndDisabled(); } | ||||
|  | ||||
|             if (mixerAgc) { SmGui::BeginDisabled(); } | ||||
|             SmGui::LeftLabel("Mixer Gain"); | ||||
|             SmGui::FillWidth(); | ||||
|             if (SmGui::SliderInt(CONCAT("##_airspy_mix_gain_", name), &mixerGain, 0, 15)) { | ||||
|                 if (running) { | ||||
|                     airspy_set_mixer_gain(openDev, mixerGain); | ||||
|                 } | ||||
|                 if (selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][selectedSerStr]["mixerGain"] = mixerGain; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|             if (mixerAgc) { SmGui::EndDisabled(); } | ||||
|  | ||||
|             SmGui::LeftLabel("VGA Gain"); | ||||
|             SmGui::FillWidth(); | ||||
|             if (SmGui::SliderInt(CONCAT("##_airspy_vga_gain_", name), &vgaGain, 0, 15)) { | ||||
|                 if (running) { | ||||
|                     airspy_set_vga_gain(openDev, vgaGain); | ||||
|                 } | ||||
|                 if (selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][selectedSerStr]["vgaGain"] = vgaGain; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // AGC Control | ||||
|             SmGui::ForceSync(); | ||||
|             if (SmGui::Checkbox(CONCAT("LNA AGC##_airspy_", name), &lnaAgc)) { | ||||
|                 if (running) { | ||||
|                     if (lnaAgc) { | ||||
|                         airspy_set_lna_agc(openDev, 1); | ||||
|                     } | ||||
|                     else { | ||||
|                         airspy_set_lna_agc(openDev, 0); | ||||
|                         airspy_set_lna_gain(openDev, lnaGain); | ||||
|                     } | ||||
|                 } | ||||
|                 if (selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][selectedSerStr]["lnaAgc"] = lnaAgc; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|             SmGui::ForceSync(); | ||||
|             if (SmGui::Checkbox(CONCAT("Mixer AGC##_airspy_", name), &mixerAgc)) { | ||||
|                 if (running) { | ||||
|                     if (mixerAgc) { | ||||
|                         airspy_set_mixer_agc(openDev, 1); | ||||
|                     } | ||||
|                     else { | ||||
|                         airspy_set_mixer_agc(openDev, 0); | ||||
|                         airspy_set_mixer_gain(openDev, mixerGain); | ||||
|                     } | ||||
|                 } | ||||
|                 if (selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][selectedSerStr]["mixerAgc"] = mixerAgc; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Bias T | ||||
|         if (SmGui::Checkbox(CONCAT("Bias T##_airspy_", name), &biasT)) { | ||||
|             if (running) { | ||||
|                 airspy_set_rf_bias(openDev, biasT); | ||||
|             } | ||||
|             if (selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["devices"][selectedSerStr]["biasT"] = biasT; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     std::string getBandwdithScaled(double bw) { | ||||
|         char buf[1024]; | ||||
| @@ -246,322 +546,6 @@ private: | ||||
|         return std::string(buf); | ||||
|     } | ||||
|  | ||||
|     static void menuSelected(void* ctx) { | ||||
|         AirspySourceModule* _this = (AirspySourceModule*)ctx; | ||||
|         core::setInputSampleRate(_this->sampleRate); | ||||
|         flog::info("AirspySourceModule '{0}': Menu Select!", _this->name); | ||||
|     } | ||||
|  | ||||
|     static void menuDeselected(void* ctx) { | ||||
|         AirspySourceModule* _this = (AirspySourceModule*)ctx; | ||||
|         flog::info("AirspySourceModule '{0}': Menu Deselect!", _this->name); | ||||
|     } | ||||
|  | ||||
|     static void start(void* ctx) { | ||||
|         AirspySourceModule* _this = (AirspySourceModule*)ctx; | ||||
|         if (_this->running) { return; } | ||||
|         if (_this->selectedSerial == 0) { | ||||
|             flog::error("Tried to start Airspy source with null serial"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| #ifndef __ANDROID__ | ||||
|         int err = airspy_open_sn(&_this->openDev, _this->selectedSerial); | ||||
| #else | ||||
|         int err = airspy_open_fd(&_this->openDev, _this->devFd); | ||||
| #endif | ||||
|         if (err != 0) { | ||||
|             char buf[1024]; | ||||
|             sprintf(buf, "%016" PRIX64, _this->selectedSerial); | ||||
|             flog::error("Could not open Airspy {0}", buf); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         airspy_set_samplerate(_this->openDev, _this->sampleRateList[_this->srId]); | ||||
|         airspy_set_freq(_this->openDev, _this->freq); | ||||
|  | ||||
|         if (_this->gainMode == 0) { | ||||
|             airspy_set_lna_agc(_this->openDev, 0); | ||||
|             airspy_set_mixer_agc(_this->openDev, 0); | ||||
|             airspy_set_sensitivity_gain(_this->openDev, _this->sensitiveGain); | ||||
|         } | ||||
|         else if (_this->gainMode == 1) { | ||||
|             airspy_set_lna_agc(_this->openDev, 0); | ||||
|             airspy_set_mixer_agc(_this->openDev, 0); | ||||
|             airspy_set_linearity_gain(_this->openDev, _this->linearGain); | ||||
|         } | ||||
|         else if (_this->gainMode == 2) { | ||||
|             if (_this->lnaAgc) { | ||||
|                 airspy_set_lna_agc(_this->openDev, 1); | ||||
|             } | ||||
|             else { | ||||
|                 airspy_set_lna_agc(_this->openDev, 0); | ||||
|                 airspy_set_lna_gain(_this->openDev, _this->lnaGain); | ||||
|             } | ||||
|             if (_this->mixerAgc) { | ||||
|                 airspy_set_mixer_agc(_this->openDev, 1); | ||||
|             } | ||||
|             else { | ||||
|                 airspy_set_mixer_agc(_this->openDev, 0); | ||||
|                 airspy_set_mixer_gain(_this->openDev, _this->mixerGain); | ||||
|             } | ||||
|             airspy_set_vga_gain(_this->openDev, _this->vgaGain); | ||||
|         } | ||||
|  | ||||
|         airspy_set_rf_bias(_this->openDev, _this->biasT); | ||||
|  | ||||
|         airspy_start_rx(_this->openDev, callback, _this); | ||||
|  | ||||
|         _this->running = true; | ||||
|         flog::info("AirspySourceModule '{0}': Start!", _this->name); | ||||
|     } | ||||
|  | ||||
|     static void stop(void* ctx) { | ||||
|         AirspySourceModule* _this = (AirspySourceModule*)ctx; | ||||
|         if (!_this->running) { return; } | ||||
|         _this->running = false; | ||||
|         _this->stream.stopWriter(); | ||||
|         airspy_close(_this->openDev); | ||||
|         _this->stream.clearWriteStop(); | ||||
|         flog::info("AirspySourceModule '{0}': Stop!", _this->name); | ||||
|     } | ||||
|  | ||||
|     static void tune(double freq, void* ctx) { | ||||
|         AirspySourceModule* _this = (AirspySourceModule*)ctx; | ||||
|         if (_this->running) { | ||||
|             airspy_set_freq(_this->openDev, freq); | ||||
|         } | ||||
|         _this->freq = freq; | ||||
|         flog::info("AirspySourceModule '{0}': Tune: {1}!", _this->name, freq); | ||||
|     } | ||||
|  | ||||
|     static void menuHandler(void* ctx) { | ||||
|         AirspySourceModule* _this = (AirspySourceModule*)ctx; | ||||
|  | ||||
|         if (_this->running) { SmGui::BeginDisabled(); } | ||||
|  | ||||
|         SmGui::FillWidth(); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::Combo(CONCAT("##_airspy_dev_sel_", _this->name), &_this->devId, _this->devListTxt.c_str())) { | ||||
|             _this->selectBySerial(_this->devList[_this->devId]); | ||||
|             core::setInputSampleRate(_this->sampleRate); | ||||
|             if (_this->selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["device"] = _this->selectedSerStr; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (SmGui::Combo(CONCAT("##_airspy_sr_sel_", _this->name), &_this->srId, _this->sampleRateListTxt.c_str())) { | ||||
|             _this->sampleRate = _this->sampleRateList[_this->srId]; | ||||
|             core::setInputSampleRate(_this->sampleRate); | ||||
|             if (_this->selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["devices"][_this->selectedSerStr]["sampleRate"] = _this->sampleRate; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         SmGui::SameLine(); | ||||
|         SmGui::FillWidth(); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::Button(CONCAT("Refresh##_airspy_refr_", _this->name))) { | ||||
|             _this->refresh(); | ||||
|             config.acquire(); | ||||
|             std::string devSerial = config.conf["device"]; | ||||
|             config.release(); | ||||
|             _this->selectByString(devSerial); | ||||
|             core::setInputSampleRate(_this->sampleRate); | ||||
|         } | ||||
|  | ||||
|         if (_this->running) { SmGui::EndDisabled(); } | ||||
|  | ||||
|         SmGui::BeginGroup(); | ||||
|         SmGui::Columns(3, CONCAT("AirspyGainModeColumns##_", _this->name), false); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::RadioButton(CONCAT("Sensitive##_airspy_gm_", _this->name), _this->gainMode == 0)) { | ||||
|             _this->gainMode = 0; | ||||
|             if (_this->running) { | ||||
|                 airspy_set_lna_agc(_this->openDev, 0); | ||||
|                 airspy_set_mixer_agc(_this->openDev, 0); | ||||
|                 airspy_set_sensitivity_gain(_this->openDev, _this->sensitiveGain); | ||||
|             } | ||||
|             if (_this->selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["devices"][_this->selectedSerStr]["gainMode"] = 0; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|         SmGui::NextColumn(); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::RadioButton(CONCAT("Linear##_airspy_gm_", _this->name), _this->gainMode == 1)) { | ||||
|             _this->gainMode = 1; | ||||
|             if (_this->running) { | ||||
|                 airspy_set_lna_agc(_this->openDev, 0); | ||||
|                 airspy_set_mixer_agc(_this->openDev, 0); | ||||
|                 airspy_set_linearity_gain(_this->openDev, _this->linearGain); | ||||
|             } | ||||
|             if (_this->selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["devices"][_this->selectedSerStr]["gainMode"] = 1; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|         SmGui::NextColumn(); | ||||
|         SmGui::ForceSync(); | ||||
|         if (SmGui::RadioButton(CONCAT("Free##_airspy_gm_", _this->name), _this->gainMode == 2)) { | ||||
|             _this->gainMode = 2; | ||||
|             if (_this->running) { | ||||
|                 if (_this->lnaAgc) { | ||||
|                     airspy_set_lna_agc(_this->openDev, 1); | ||||
|                 } | ||||
|                 else { | ||||
|                     airspy_set_lna_agc(_this->openDev, 0); | ||||
|                     airspy_set_lna_gain(_this->openDev, _this->lnaGain); | ||||
|                 } | ||||
|                 if (_this->mixerAgc) { | ||||
|                     airspy_set_mixer_agc(_this->openDev, 1); | ||||
|                 } | ||||
|                 else { | ||||
|                     airspy_set_mixer_agc(_this->openDev, 0); | ||||
|                     airspy_set_mixer_gain(_this->openDev, _this->mixerGain); | ||||
|                 } | ||||
|                 airspy_set_vga_gain(_this->openDev, _this->vgaGain); | ||||
|             } | ||||
|             if (_this->selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["devices"][_this->selectedSerStr]["gainMode"] = 2; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|         SmGui::Columns(1, CONCAT("EndAirspyGainModeColumns##_", _this->name), false); | ||||
|         SmGui::EndGroup(); | ||||
|  | ||||
|         // Gain menus | ||||
|  | ||||
|         if (_this->gainMode == 0) { | ||||
|             SmGui::LeftLabel("Gain"); | ||||
|             SmGui::FillWidth(); | ||||
|             if (SmGui::SliderInt(CONCAT("##_airspy_sens_gain_", _this->name), &_this->sensitiveGain, 0, 21)) { | ||||
|                 if (_this->running) { | ||||
|                     airspy_set_sensitivity_gain(_this->openDev, _this->sensitiveGain); | ||||
|                 } | ||||
|                 if (_this->selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][_this->selectedSerStr]["sensitiveGain"] = _this->sensitiveGain; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         else if (_this->gainMode == 1) { | ||||
|             SmGui::LeftLabel("Gain"); | ||||
|             SmGui::FillWidth(); | ||||
|             if (SmGui::SliderInt(CONCAT("##_airspy_lin_gain_", _this->name), &_this->linearGain, 0, 21)) { | ||||
|                 if (_this->running) { | ||||
|                     airspy_set_linearity_gain(_this->openDev, _this->linearGain); | ||||
|                 } | ||||
|                 if (_this->selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][_this->selectedSerStr]["linearGain"] = _this->linearGain; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         else if (_this->gainMode == 2) { | ||||
|             // TODO: Switch to a table for alignment | ||||
|             if (_this->lnaAgc) { SmGui::BeginDisabled(); } | ||||
|             SmGui::LeftLabel("LNA Gain"); | ||||
|             SmGui::FillWidth(); | ||||
|             if (SmGui::SliderInt(CONCAT("##_airspy_lna_gain_", _this->name), &_this->lnaGain, 0, 15)) { | ||||
|                 if (_this->running) { | ||||
|                     airspy_set_lna_gain(_this->openDev, _this->lnaGain); | ||||
|                 } | ||||
|                 if (_this->selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][_this->selectedSerStr]["lnaGain"] = _this->lnaGain; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|             if (_this->lnaAgc) { SmGui::EndDisabled(); } | ||||
|  | ||||
|             if (_this->mixerAgc) { SmGui::BeginDisabled(); } | ||||
|             SmGui::LeftLabel("Mixer Gain"); | ||||
|             SmGui::FillWidth(); | ||||
|             if (SmGui::SliderInt(CONCAT("##_airspy_mix_gain_", _this->name), &_this->mixerGain, 0, 15)) { | ||||
|                 if (_this->running) { | ||||
|                     airspy_set_mixer_gain(_this->openDev, _this->mixerGain); | ||||
|                 } | ||||
|                 if (_this->selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][_this->selectedSerStr]["mixerGain"] = _this->mixerGain; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|             if (_this->mixerAgc) { SmGui::EndDisabled(); } | ||||
|  | ||||
|             SmGui::LeftLabel("VGA Gain"); | ||||
|             SmGui::FillWidth(); | ||||
|             if (SmGui::SliderInt(CONCAT("##_airspy_vga_gain_", _this->name), &_this->vgaGain, 0, 15)) { | ||||
|                 if (_this->running) { | ||||
|                     airspy_set_vga_gain(_this->openDev, _this->vgaGain); | ||||
|                 } | ||||
|                 if (_this->selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][_this->selectedSerStr]["vgaGain"] = _this->vgaGain; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // AGC Control | ||||
|             SmGui::ForceSync(); | ||||
|             if (SmGui::Checkbox(CONCAT("LNA AGC##_airspy_", _this->name), &_this->lnaAgc)) { | ||||
|                 if (_this->running) { | ||||
|                     if (_this->lnaAgc) { | ||||
|                         airspy_set_lna_agc(_this->openDev, 1); | ||||
|                     } | ||||
|                     else { | ||||
|                         airspy_set_lna_agc(_this->openDev, 0); | ||||
|                         airspy_set_lna_gain(_this->openDev, _this->lnaGain); | ||||
|                     } | ||||
|                 } | ||||
|                 if (_this->selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][_this->selectedSerStr]["lnaAgc"] = _this->lnaAgc; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|             SmGui::ForceSync(); | ||||
|             if (SmGui::Checkbox(CONCAT("Mixer AGC##_airspy_", _this->name), &_this->mixerAgc)) { | ||||
|                 if (_this->running) { | ||||
|                     if (_this->mixerAgc) { | ||||
|                         airspy_set_mixer_agc(_this->openDev, 1); | ||||
|                     } | ||||
|                     else { | ||||
|                         airspy_set_mixer_agc(_this->openDev, 0); | ||||
|                         airspy_set_mixer_gain(_this->openDev, _this->mixerGain); | ||||
|                     } | ||||
|                 } | ||||
|                 if (_this->selectedSerStr != "") { | ||||
|                     config.acquire(); | ||||
|                     config.conf["devices"][_this->selectedSerStr]["mixerAgc"] = _this->mixerAgc; | ||||
|                     config.release(true); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Bias T | ||||
|         if (SmGui::Checkbox(CONCAT("Bias T##_airspy_", _this->name), &_this->biasT)) { | ||||
|             if (_this->running) { | ||||
|                 airspy_set_rf_bias(_this->openDev, _this->biasT); | ||||
|             } | ||||
|             if (_this->selectedSerStr != "") { | ||||
|                 config.acquire(); | ||||
|                 config.conf["devices"][_this->selectedSerStr]["biasT"] = _this->biasT; | ||||
|                 config.release(true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static int callback(airspy_transfer_t* transfer) { | ||||
|         AirspySourceModule* _this = (AirspySourceModule*)transfer->ctx; | ||||
|         memcpy(_this->stream.writeBuf, transfer->samples, transfer->sample_count * sizeof(dsp::complex_t)); | ||||
| @@ -574,7 +558,6 @@ private: | ||||
|     bool enabled = true; | ||||
|     dsp::stream<dsp::complex_t> stream; | ||||
|     double sampleRate; | ||||
|     SourceManager::SourceHandler handler; | ||||
|     bool running = false; | ||||
|     double freq; | ||||
|     uint64_t selectedSerial = 0; | ||||
|   | ||||
| @@ -25,7 +25,7 @@ ConfigManager config; | ||||
| struct DeviceInfo { | ||||
|     RtAudio::DeviceInfo info; | ||||
|     int id; | ||||
|     bool operator==(const struct DeviceInfo& other) const { | ||||
|     bool operator==(const struct DeviceInfo& other) { | ||||
|         return other.id == id; | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -205,7 +205,6 @@ namespace hermes { | ||||
|     } | ||||
|  | ||||
|     std::vector<Info> discover() { | ||||
|         // TODO: Maybe try to instead detect on each interface as a work around for 0.0.0.0 not receiving anything? | ||||
|         auto sock = net::openudp("0.0.0.0", 1024); | ||||
|          | ||||
|         // Build discovery packet | ||||
|   | ||||
| @@ -39,7 +39,7 @@ namespace hermes { | ||||
|         uint8_t gatewareVerMin; | ||||
|         BoardID boardId; | ||||
|  | ||||
|         bool operator==(const Info& b) const { | ||||
|         bool operator==(const Info& b) { | ||||
|             return !memcmp(mac, b.mac, 6); | ||||
|         } | ||||
|     }; | ||||
|   | ||||
| @@ -319,7 +319,6 @@ private: | ||||
|     static void start(void* ctx) { | ||||
|         LimeSDRSourceModule* _this = (LimeSDRSourceModule*)ctx; | ||||
|         if (_this->running) { return; } | ||||
|         if (_this->selectedDevName.empty()) { return; } | ||||
|  | ||||
|         // Open device | ||||
|         _this->openDev = NULL; | ||||
| @@ -330,10 +329,7 @@ private: | ||||
|         if (err) { | ||||
|             LMS_Close(_this->openDev); | ||||
|             LMS_Open(&_this->openDev, _this->devList[_this->devId], NULL); | ||||
|             if (err = LMS_Init(_this->openDev)) { | ||||
|                 flog::error("Failed to re-initialize device ({})", err); | ||||
|                 return; | ||||
|             } | ||||
|             LMS_Init(_this->openDev); | ||||
|         } | ||||
|  | ||||
|         flog::warn("Channel count: {0}", LMS_GetNumChannels(_this->openDev, false)); | ||||
| @@ -550,4 +546,4 @@ MOD_EXPORT void _DELETE_INSTANCE_(ModuleManager::Instance* instance) { | ||||
| MOD_EXPORT void _END_() { | ||||
|     config.disableAutoSave(); | ||||
|     config.save(); | ||||
| } | ||||
| } | ||||
| @@ -108,12 +108,12 @@ namespace rfspace { | ||||
|             } | ||||
|             break; | ||||
|         case RFSPACE_DEV_ID_NET_SDR: | ||||
|         case RFSPACE_DEV_ID_SDR_IP: | ||||
|         default: | ||||
|             for (int n = 80000000 / (4 * 25); n >= 32000; n /= 2) { | ||||
|                 sr.push_back(n); | ||||
|             } | ||||
|             break; | ||||
|         default: | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         return sr; | ||||
|   | ||||
| @@ -503,7 +503,7 @@ private: | ||||
|     float refStep = 0.5; | ||||
|  | ||||
|     struct SRCombo { | ||||
|         bool operator==(const SRCombo& b) const { | ||||
|         bool operator==(const SRCombo& b) { | ||||
|             return baseId == b.baseId && decimId == b.decimId; | ||||
|         } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user