From 3b74153ec6350788310b4fe5a95ed5ff7bb146d7 Mon Sep 17 00:00:00 2001 From: f4exb Date: Fri, 15 Nov 2019 01:04:24 +0100 Subject: [PATCH] SampleSourceFifo refactoring and Tx code reorganization --- CMakeLists.txt | 4 +- devices/bladerf1/devicebladerf1shared.cpp | 4 +- devices/bladerf1/devicebladerf1shared.h | 4 +- devices/bladerf2/devicebladerf2shared.cpp | 4 +- devices/bladerf2/devicebladerf2shared.h | 4 +- devices/hackrf/devicehackrfshared.cpp | 3 +- devices/hackrf/devicehackrfshared.h | 3 +- devices/limesdr/devicelimesdrshared.cpp | 3 +- devices/limesdr/devicelimesdrshared.h | 3 +- devices/plutosdr/deviceplutosdrshared.cpp | 3 +- devices/plutosdr/deviceplutosdrshared.h | 3 +- devices/soapysdr/devicesoapysdrshared.cpp | 4 +- devices/soapysdr/devicesoapysdrshared.h | 4 +- devices/xtrx/devicextrxshared.cpp | 3 +- devices/xtrx/devicextrxshared.h | 3 +- doc/model/SDRangelFlow.odg | Bin 18007 -> 23883 bytes .../beamsteeringcwmodbaseband.cpp | 247 ++++ plugins/channeltx/filesource/CMakeLists.txt | 8 +- plugins/channeltx/filesource/filesource.cpp | 411 ++---- plugins/channeltx/filesource/filesource.h | 205 +-- .../filesource/filesourcebaseband.cpp | 234 ++++ .../channeltx/filesource/filesourcebaseband.h | 184 +++ .../channeltx/filesource/filesourcegui.cpp | 40 +- plugins/channeltx/filesource/filesourcegui.h | 1 - .../channeltx/filesource/filesourceplugin.cpp | 2 +- .../channeltx/filesource/filesourcereport.cpp | 29 + .../channeltx/filesource/filesourcereport.h | 130 ++ .../channeltx/filesource/filesourcesource.cpp | 283 ++++ .../channeltx/filesource/filesourcesource.h | 127 ++ plugins/channeltx/localsource/CMakeLists.txt | 8 +- plugins/channeltx/localsource/localsource.cpp | 316 ++--- plugins/channeltx/localsource/localsource.h | 78 +- plugins/channeltx/localsource/localsource.pro | 52 - .../localsource/localsourcebaseband.cpp | 219 ++++ .../localsource/localsourcebaseband.h | 170 +++ .../channeltx/localsource/localsourcegui.cpp | 34 +- .../channeltx/localsource/localsourcegui.h | 4 +- .../channeltx/localsource/localsourcegui.ui | 20 + .../localsource/localsourceplugin.cpp | 2 +- .../localsource/localsourcesettings.cpp | 1 + .../localsource/localsourcesettings.h | 1 + .../localsource/localsourcesource.cpp | 156 +++ .../channeltx/localsource/localsourcesource.h | 61 + .../localsource/localsourcethread.cpp | 12 +- .../channeltx/localsource/localsourcethread.h | 10 +- plugins/channeltx/modam/CMakeLists.txt | 4 + plugins/channeltx/modam/ammod.cpp | 505 ++----- plugins/channeltx/modam/ammod.h | 106 +- plugins/channeltx/modam/ammodbaseband.cpp | 229 ++++ plugins/channeltx/modam/ammodbaseband.h | 123 ++ plugins/channeltx/modam/ammodgui.cpp | 4 +- plugins/channeltx/modam/ammodplugin.cpp | 2 +- plugins/channeltx/modam/ammodsource.cpp | 330 +++++ plugins/channeltx/modam/ammodsource.h | 117 ++ .../channeltx/modam/ammodwebapiadapter.cpp | 1 + plugins/channeltx/modatv/CMakeLists.txt | 8 +- plugins/channeltx/modatv/atvmod.cpp | 1163 ++--------------- plugins/channeltx/modatv/atvmod.h | 625 +-------- plugins/channeltx/modatv/atvmodbaseband.cpp | 255 ++++ plugins/channeltx/modatv/atvmodbaseband.h | 252 ++++ plugins/channeltx/modatv/atvmodgui.cpp | 170 +-- plugins/channeltx/modatv/atvmodplugin.cpp | 2 +- plugins/channeltx/modatv/atvmodreport.cpp | 29 + plugins/channeltx/modatv/atvmodreport.h | 165 +++ plugins/channeltx/modatv/atvmodsource.cpp | 1073 +++++++++++++++ plugins/channeltx/modatv/atvmodsource.h | 502 +++++++ plugins/channeltx/modfreedv/CMakeLists.txt | 4 + plugins/channeltx/modfreedv/freedvmod.cpp | 699 ++-------- plugins/channeltx/modfreedv/freedvmod.h | 131 +- .../channeltx/modfreedv/freedvmodbaseband.cpp | 218 +++ .../channeltx/modfreedv/freedvmodbaseband.h | 124 ++ plugins/channeltx/modfreedv/freedvmodgui.cpp | 3 +- .../channeltx/modfreedv/freedvmodplugin.cpp | 2 +- .../channeltx/modfreedv/freedvmodsource.cpp | 525 ++++++++ plugins/channeltx/modfreedv/freedvmodsource.h | 140 ++ .../modfreedv/freedvmodwebapiadapter.cpp | 1 + plugins/channeltx/modnfm/CMakeLists.txt | 6 +- plugins/channeltx/modnfm/nfmmod.cpp | 547 ++------ plugins/channeltx/modnfm/nfmmod.h | 118 +- plugins/channeltx/modnfm/nfmmodbaseband.cpp | 228 ++++ plugins/channeltx/modnfm/nfmmodbaseband.h | 123 ++ plugins/channeltx/modnfm/nfmmodgui.cpp | 3 +- plugins/channeltx/modnfm/nfmmodplugin.cpp | 2 +- plugins/channeltx/modnfm/nfmmodsettings.cpp | 4 +- plugins/channeltx/modnfm/nfmmodsource.cpp | 359 +++++ plugins/channeltx/modnfm/nfmmodsource.h | 127 ++ .../channeltx/modnfm/nfmmodwebapiadapter.cpp | 1 + plugins/channeltx/modssb/CMakeLists.txt | 4 + plugins/channeltx/modssb/ssbmod.cpp | 819 ++---------- plugins/channeltx/modssb/ssbmod.h | 129 +- plugins/channeltx/modssb/ssbmodbaseband.cpp | 227 ++++ plugins/channeltx/modssb/ssbmodbaseband.h | 124 ++ plugins/channeltx/modssb/ssbmodgui.cpp | 5 +- plugins/channeltx/modssb/ssbmodplugin.cpp | 2 +- plugins/channeltx/modssb/ssbmodsource.cpp | 638 +++++++++ plugins/channeltx/modssb/ssbmodsource.h | 137 ++ .../channeltx/modssb/ssbmodwebapiadapter.cpp | 1 + plugins/channeltx/modwfm/CMakeLists.txt | 6 +- plugins/channeltx/modwfm/wfmmod.cpp | 421 ++---- plugins/channeltx/modwfm/wfmmod.h | 86 +- plugins/channeltx/modwfm/wfmmodbaseband.cpp | 218 +++ plugins/channeltx/modwfm/wfmmodbaseband.h | 121 ++ plugins/channeltx/modwfm/wfmmodgui.cpp | 5 +- plugins/channeltx/modwfm/wfmmodplugin.cpp | 2 +- plugins/channeltx/modwfm/wfmmodsource.cpp | 298 +++++ plugins/channeltx/modwfm/wfmmodsource.h | 108 ++ .../channeltx/modwfm/wfmmodwebapiadapter.cpp | 1 + plugins/channeltx/remotesource/CMakeLists.txt | 8 +- plugins/channeltx/remotesource/readme.md | 8 +- .../channeltx/remotesource/remotesource.cpp | 330 +---- plugins/channeltx/remotesource/remotesource.h | 64 +- .../remotesource/remotesourcebaseband.cpp | 187 +++ .../remotesource/remotesourcebaseband.h | 110 ++ .../remotesource/remotesourcegui.cpp | 22 +- .../channeltx/remotesource/remotesourcegui.h | 1 + .../remotesource/remotesourceplugin.cpp | 2 +- .../remotesource/remotesourcesource.cpp | 265 ++++ .../remotesource/remotesourcesource.h | 87 ++ plugins/channeltx/udpsource/CMakeLists.txt | 6 +- plugins/channeltx/udpsource/udpsource.cpp | 522 +------- plugins/channeltx/udpsource/udpsource.h | 228 +--- .../channeltx/udpsource/udpsourcebaseband.cpp | 204 +++ .../channeltx/udpsource/udpsourcebaseband.h | 155 +++ plugins/channeltx/udpsource/udpsourcegui.cpp | 2 +- .../channeltx/udpsource/udpsourceplugin.cpp | 2 +- .../channeltx/udpsource/udpsourcesource.cpp | 457 +++++++ plugins/channeltx/udpsource/udpsourcesource.h | 197 +++ .../udpsource/udpsourceudphandler.cpp | 4 +- plugins/samplemimo/testmi/testmi.cpp | 24 +- plugins/samplesink/CMakeLists.txt | 2 +- .../bladerf1output/bladerf1output.cpp | 50 +- .../bladerf1output/bladerf1outputplugin.cpp | 2 +- .../bladerf1output/bladerf1outputthread.cpp | 40 +- .../bladerf1output/bladerf1outputthread.h | 8 +- .../bladerf2output/bladerf2output.cpp | 54 +- .../bladerf2output/bladerf2outputplugin.cpp | 2 +- .../bladerf2output/bladerf2outputthread.cpp | 87 +- .../bladerf2output/bladerf2outputthread.h | 9 +- .../samplesink/filesink/filesinkplugin.cpp | 2 +- .../samplesink/filesink/filesinkthread.cpp | 98 +- plugins/samplesink/filesink/filesinkthread.h | 7 +- .../samplesink/hackrfoutput/hackrfoutput.cpp | 42 +- .../hackrfoutput/hackrfoutputplugin.cpp | 2 +- .../hackrfoutput/hackrfoutputthread.cpp | 26 +- .../hackrfoutput/hackrfoutputthread.h | 7 +- .../limesdroutput/limesdroutput.cpp | 38 +- .../limesdroutput/limesdroutputplugin.cpp | 2 +- .../limesdroutput/limesdroutputthread.cpp | 41 +- .../limesdroutput/limesdroutputthread.h | 8 +- .../samplesink/localoutput/localoutput.cpp | 27 +- .../localoutput/localoutputplugin.cpp | 2 +- .../plutosdroutput/plutosdroutput.cpp | 35 +- .../plutosdroutput/plutosdroutputthread.cpp | 37 +- .../plutosdroutput/plutosdroutputthread.h | 7 +- .../samplesink/remoteoutput/remoteoutput.cpp | 50 +- .../remoteoutput/remoteoutputgui.cpp | 47 +- .../remoteoutput/remoteoutputplugin.cpp | 2 +- .../remoteoutput/remoteoutputthread.cpp | 32 +- .../remoteoutput/remoteoutputthread.h | 6 +- .../soapysdroutput/soapysdroutput.cpp | 50 +- .../soapysdroutput/soapysdroutputplugin.cpp | 2 +- .../soapysdroutput/soapysdroutputthread.cpp | 335 ++--- .../soapysdroutput/soapysdroutputthread.h | 14 +- .../samplesink/testsink/testsinkplugin.cpp | 2 +- .../samplesink/testsink/testsinkthread.cpp | 93 +- plugins/samplesink/testsink/testsinkthread.h | 7 +- plugins/samplesink/xtrxoutput/xtrxoutput.cpp | 37 +- .../xtrxoutput/xtrxoutputplugin.cpp | 2 +- .../xtrxoutput/xtrxoutputthread.cpp | 125 +- .../samplesink/xtrxoutput/xtrxoutputthread.h | 10 +- sdrbase/CMakeLists.txt | 10 +- sdrbase/device/deviceapi.cpp | 8 +- sdrbase/device/deviceapi.h | 6 +- sdrbase/dsp/bandpass.h | 4 +- sdrbase/dsp/basebandsamplesource.cpp | 50 +- sdrbase/dsp/basebandsamplesource.h | 27 +- sdrbase/dsp/channelsamplesource.h | 1 + sdrbase/dsp/cwkeyer.cpp | 14 +- sdrbase/dsp/devicesamplesink.h | 6 +- sdrbase/dsp/dspcommands.cpp | 2 - sdrbase/dsp/dspcommands.h | 24 - sdrbase/dsp/dspdevicemimoengine.cpp | 196 +-- sdrbase/dsp/dspdevicemimoengine.h | 31 +- sdrbase/dsp/dspdevicesinkengine.cpp | 275 ++-- sdrbase/dsp/dspdevicesinkengine.h | 29 +- sdrbase/dsp/filterrc.cpp | 27 + sdrbase/dsp/filterrc.h | 29 +- sdrbase/dsp/lowpass.h | 3 - sdrbase/dsp/mimochannel.h | 2 +- sdrbase/dsp/samplemofifo.cpp | 88 +- sdrbase/dsp/samplemofifo.h | 14 +- sdrbase/dsp/samplesourcefifo.cpp | 134 ++ sdrbase/dsp/samplesourcefifo.h | 83 ++ sdrbase/dsp/upsamplechannelizer.cpp | 95 +- sdrbase/dsp/upsamplechannelizer.h | 25 +- sdrbase/webapi/webapirequestmapper.cpp | 7 + sdrgui/mainwindow.cpp | 12 +- .../code/qt5/client/SWGHttpRequest.cpp | 2 +- 198 files changed, 13267 insertions(+), 7750 deletions(-) create mode 100644 plugins/channelmimo/beamsteeringcwmod/beamsteeringcwmodbaseband.cpp create mode 100644 plugins/channeltx/filesource/filesourcebaseband.cpp create mode 100644 plugins/channeltx/filesource/filesourcebaseband.h create mode 100644 plugins/channeltx/filesource/filesourcereport.cpp create mode 100644 plugins/channeltx/filesource/filesourcereport.h create mode 100644 plugins/channeltx/filesource/filesourcesource.cpp create mode 100644 plugins/channeltx/filesource/filesourcesource.h delete mode 100644 plugins/channeltx/localsource/localsource.pro create mode 100644 plugins/channeltx/localsource/localsourcebaseband.cpp create mode 100644 plugins/channeltx/localsource/localsourcebaseband.h create mode 100644 plugins/channeltx/localsource/localsourcesource.cpp create mode 100644 plugins/channeltx/localsource/localsourcesource.h create mode 100644 plugins/channeltx/modam/ammodbaseband.cpp create mode 100644 plugins/channeltx/modam/ammodbaseband.h create mode 100644 plugins/channeltx/modam/ammodsource.cpp create mode 100644 plugins/channeltx/modam/ammodsource.h create mode 100644 plugins/channeltx/modatv/atvmodbaseband.cpp create mode 100644 plugins/channeltx/modatv/atvmodbaseband.h create mode 100644 plugins/channeltx/modatv/atvmodreport.cpp create mode 100644 plugins/channeltx/modatv/atvmodreport.h create mode 100644 plugins/channeltx/modatv/atvmodsource.cpp create mode 100644 plugins/channeltx/modatv/atvmodsource.h create mode 100644 plugins/channeltx/modfreedv/freedvmodbaseband.cpp create mode 100644 plugins/channeltx/modfreedv/freedvmodbaseband.h create mode 100644 plugins/channeltx/modfreedv/freedvmodsource.cpp create mode 100644 plugins/channeltx/modfreedv/freedvmodsource.h create mode 100644 plugins/channeltx/modnfm/nfmmodbaseband.cpp create mode 100644 plugins/channeltx/modnfm/nfmmodbaseband.h create mode 100644 plugins/channeltx/modnfm/nfmmodsource.cpp create mode 100644 plugins/channeltx/modnfm/nfmmodsource.h create mode 100644 plugins/channeltx/modssb/ssbmodbaseband.cpp create mode 100644 plugins/channeltx/modssb/ssbmodbaseband.h create mode 100644 plugins/channeltx/modssb/ssbmodsource.cpp create mode 100644 plugins/channeltx/modssb/ssbmodsource.h create mode 100644 plugins/channeltx/modwfm/wfmmodbaseband.cpp create mode 100644 plugins/channeltx/modwfm/wfmmodbaseband.h create mode 100644 plugins/channeltx/modwfm/wfmmodsource.cpp create mode 100644 plugins/channeltx/modwfm/wfmmodsource.h create mode 100644 plugins/channeltx/remotesource/remotesourcebaseband.cpp create mode 100644 plugins/channeltx/remotesource/remotesourcebaseband.h create mode 100644 plugins/channeltx/remotesource/remotesourcesource.cpp create mode 100644 plugins/channeltx/remotesource/remotesourcesource.h create mode 100644 plugins/channeltx/udpsource/udpsourcebaseband.cpp create mode 100644 plugins/channeltx/udpsource/udpsourcebaseband.h create mode 100644 plugins/channeltx/udpsource/udpsourcesource.cpp create mode 100644 plugins/channeltx/udpsource/udpsourcesource.h create mode 100644 sdrbase/dsp/samplesourcefifo.cpp create mode 100644 sdrbase/dsp/samplesourcefifo.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c87bd25a..bba1c77ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,8 +17,8 @@ set(CMAKE_CXX_EXTENSIONS OFF) # configure version set(sdrangel_VERSION_MAJOR "4") -set(sdrangel_VERSION_MINOR "11") -set(sdrangel_VERSION_PATCH "12") +set(sdrangel_VERSION_MINOR "12") +set(sdrangel_VERSION_PATCH "0") set(sdrangel_VERSION_SUFFIX "") # SDRAngel cmake options diff --git a/devices/bladerf1/devicebladerf1shared.cpp b/devices/bladerf1/devicebladerf1shared.cpp index 934f9fe84..dc373cd30 100644 --- a/devices/bladerf1/devicebladerf1shared.cpp +++ b/devices/bladerf1/devicebladerf1shared.cpp @@ -17,6 +17,4 @@ #include "../bladerf1/devicebladerf1shared.h" -const float DeviceBladeRF1Shared::m_sampleFifoLengthInSeconds = 0.25; -const int DeviceBladeRF1Shared::m_sampleFifoMinSize = 75000; // 300 kS/s knee -const int DeviceBladeRF1Shared::m_sampleFifoMinSize32 = 150000; // Fixed for interpolation by 32 +const unsigned int DeviceBladeRF1Shared::m_sampleFifoMinRate = 48000; diff --git a/devices/bladerf1/devicebladerf1shared.h b/devices/bladerf1/devicebladerf1shared.h index 2f0f4fd4c..eea918389 100644 --- a/devices/bladerf1/devicebladerf1shared.h +++ b/devices/bladerf1/devicebladerf1shared.h @@ -24,9 +24,7 @@ class DEVICES_API DeviceBladeRF1Shared { public: - static const float m_sampleFifoLengthInSeconds; - static const int m_sampleFifoMinSize; - static const int m_sampleFifoMinSize32; + static const unsigned int m_sampleFifoMinRate; }; diff --git a/devices/bladerf2/devicebladerf2shared.cpp b/devices/bladerf2/devicebladerf2shared.cpp index 84f4461bd..d2975c7e0 100644 --- a/devices/bladerf2/devicebladerf2shared.cpp +++ b/devices/bladerf2/devicebladerf2shared.cpp @@ -19,9 +19,7 @@ MESSAGE_CLASS_DEFINITION(DeviceBladeRF2Shared::MsgReportBuddyChange, Message) -const float DeviceBladeRF2Shared::m_sampleFifoLengthInSeconds = 0.25; -const int DeviceBladeRF2Shared::m_sampleFifoMinSize = 75000; // 300 kS/s knee -const int DeviceBladeRF2Shared::m_sampleFifoMinSize32 = 150000; // Fixed for interpolation by 32 +const unsigned int DeviceBladeRF2Shared::m_sampleFifoMinRate = 48000; DeviceBladeRF2Shared::DeviceBladeRF2Shared() : m_dev(0), diff --git a/devices/bladerf2/devicebladerf2shared.h b/devices/bladerf2/devicebladerf2shared.h index 347ea170a..878bce927 100644 --- a/devices/bladerf2/devicebladerf2shared.h +++ b/devices/bladerf2/devicebladerf2shared.h @@ -85,9 +85,7 @@ public: BladeRF2Input *m_source; BladeRF2Output *m_sink; - static const float m_sampleFifoLengthInSeconds; - static const int m_sampleFifoMinSize; - static const int m_sampleFifoMinSize32; + static const unsigned int m_sampleFifoMinRate; }; diff --git a/devices/hackrf/devicehackrfshared.cpp b/devices/hackrf/devicehackrfshared.cpp index 69fa9249d..a76505e3d 100644 --- a/devices/hackrf/devicehackrfshared.cpp +++ b/devices/hackrf/devicehackrfshared.cpp @@ -19,5 +19,4 @@ MESSAGE_CLASS_DEFINITION(DeviceHackRFShared::MsgSynchronizeFrequency, Message) -const float DeviceHackRFShared::m_sampleFifoLengthInSeconds = 0.25; -const int DeviceHackRFShared::m_sampleFifoMinSize = 150000; // 600kS/s knee +const unsigned int DeviceHackRFShared::m_sampleFifoMinRate = 48000; // 48kS/s knee diff --git a/devices/hackrf/devicehackrfshared.h b/devices/hackrf/devicehackrfshared.h index cf83658af..2eb7c4bf9 100644 --- a/devices/hackrf/devicehackrfshared.h +++ b/devices/hackrf/devicehackrfshared.h @@ -47,8 +47,7 @@ public: { } }; - static const float m_sampleFifoLengthInSeconds; - static const int m_sampleFifoMinSize; + static const unsigned int m_sampleFifoMinRate; }; diff --git a/devices/limesdr/devicelimesdrshared.cpp b/devices/limesdr/devicelimesdrshared.cpp index 151e9c318..33f855458 100644 --- a/devices/limesdr/devicelimesdrshared.cpp +++ b/devices/limesdr/devicelimesdrshared.cpp @@ -22,5 +22,4 @@ MESSAGE_CLASS_DEFINITION(DeviceLimeSDRShared::MsgReportClockSourceChange, Messag MESSAGE_CLASS_DEFINITION(DeviceLimeSDRShared::MsgReportGPIOChange, Message) MESSAGE_CLASS_DEFINITION(DeviceLimeSDRShared::MsgReportDeviceInfo, Message) -const float DeviceLimeSDRShared::m_sampleFifoLengthInSeconds = 0.25; -const int DeviceLimeSDRShared::m_sampleFifoMinSize = 48000; // 192kS/s knee +const unsigned int DeviceLimeSDRShared::m_sampleFifoMinRate = 48000; diff --git a/devices/limesdr/devicelimesdrshared.h b/devices/limesdr/devicelimesdrshared.h index c2ba75997..74f44e0ab 100644 --- a/devices/limesdr/devicelimesdrshared.h +++ b/devices/limesdr/devicelimesdrshared.h @@ -161,8 +161,7 @@ public: uint32_t m_log2Soft; bool m_threadWasRunning; //!< flag to know if thread needs to be resumed after suspend - static const float m_sampleFifoLengthInSeconds; - static const int m_sampleFifoMinSize; + static const unsigned int m_sampleFifoMinRate; DeviceLimeSDRShared() : m_deviceParams(0), diff --git a/devices/plutosdr/deviceplutosdrshared.cpp b/devices/plutosdr/deviceplutosdrshared.cpp index 1b7cd3882..a26d0518c 100644 --- a/devices/plutosdr/deviceplutosdrshared.cpp +++ b/devices/plutosdr/deviceplutosdrshared.cpp @@ -19,5 +19,4 @@ MESSAGE_CLASS_DEFINITION(DevicePlutoSDRShared::MsgCrossReportToBuddy, Message) -const float DevicePlutoSDRShared::m_sampleFifoLengthInSeconds = 0.25; -const int DevicePlutoSDRShared::m_sampleFifoMinSize = 48000; // 192kS/s knee +const unsigned int DevicePlutoSDRShared::m_sampleFifoMinRate = 48000; diff --git a/devices/plutosdr/deviceplutosdrshared.h b/devices/plutosdr/deviceplutosdrshared.h index 14bd6b452..c01d89d0b 100644 --- a/devices/plutosdr/deviceplutosdrshared.h +++ b/devices/plutosdr/deviceplutosdrshared.h @@ -90,8 +90,7 @@ public: ThreadInterface *m_thread; //!< holds the thread address if started else 0 bool m_threadWasRunning; //!< flag to know if thread needs to be resumed after suspend - static const float m_sampleFifoLengthInSeconds; - static const int m_sampleFifoMinSize; + static const unsigned int m_sampleFifoMinRate; DevicePlutoSDRShared() : m_deviceParams(0), diff --git a/devices/soapysdr/devicesoapysdrshared.cpp b/devices/soapysdr/devicesoapysdrshared.cpp index f34e92f07..a7a44bc30 100644 --- a/devices/soapysdr/devicesoapysdrshared.cpp +++ b/devices/soapysdr/devicesoapysdrshared.cpp @@ -20,9 +20,7 @@ MESSAGE_CLASS_DEFINITION(DeviceSoapySDRShared::MsgReportBuddyChange, Message) MESSAGE_CLASS_DEFINITION(DeviceSoapySDRShared::MsgReportDeviceArgsChange, Message) -const float DeviceSoapySDRShared::m_sampleFifoLengthInSeconds = 0.25; -const int DeviceSoapySDRShared::m_sampleFifoMinSize = 75000; // 300 kS/s knee -const int DeviceSoapySDRShared::m_sampleFifoMinSize32 = 150000; // Fixed for interpolation by 32 +const unsigned int DeviceSoapySDRShared::m_sampleFifoMinRate = 48000; DeviceSoapySDRShared::DeviceSoapySDRShared() : m_device(0), diff --git a/devices/soapysdr/devicesoapysdrshared.h b/devices/soapysdr/devicesoapysdrshared.h index 72b48cfcb..0562b8a94 100644 --- a/devices/soapysdr/devicesoapysdrshared.h +++ b/devices/soapysdr/devicesoapysdrshared.h @@ -110,9 +110,7 @@ public: SoapySDRInput *m_source; SoapySDROutput *m_sink; - static const float m_sampleFifoLengthInSeconds; - static const int m_sampleFifoMinSize; - static const int m_sampleFifoMinSize32; + static const unsigned int m_sampleFifoMinRate; }; diff --git a/devices/xtrx/devicextrxshared.cpp b/devices/xtrx/devicextrxshared.cpp index 99ebbc3ab..87c5b910a 100644 --- a/devices/xtrx/devicextrxshared.cpp +++ b/devices/xtrx/devicextrxshared.cpp @@ -24,8 +24,7 @@ MESSAGE_CLASS_DEFINITION(DeviceXTRXShared::MsgReportBuddyChange, Message) MESSAGE_CLASS_DEFINITION(DeviceXTRXShared::MsgReportClockSourceChange, Message) MESSAGE_CLASS_DEFINITION(DeviceXTRXShared::MsgReportDeviceInfo, Message) -const float DeviceXTRXShared::m_sampleFifoLengthInSeconds = 0.25; -const int DeviceXTRXShared::m_sampleFifoMinSize = 48000; // 192kS/s knee +const unsigned int DeviceXTRXShared::m_sampleFifoMinRate = 48000; DeviceXTRXShared::DeviceXTRXShared() : m_dev(0), diff --git a/devices/xtrx/devicextrxshared.h b/devices/xtrx/devicextrxshared.h index 7486169cd..69ed80b44 100644 --- a/devices/xtrx/devicextrxshared.h +++ b/devices/xtrx/devicextrxshared.h @@ -141,8 +141,7 @@ public: ThreadInterface *m_thread; //!< holds the thread address if started else 0 bool m_threadWasRunning; //!< flag to know if thread needs to be resumed after suspend - static const float m_sampleFifoLengthInSeconds; - static const int m_sampleFifoMinSize; + static const unsigned int m_sampleFifoMinRate; DeviceXTRXShared(); ~DeviceXTRXShared(); diff --git a/doc/model/SDRangelFlow.odg b/doc/model/SDRangelFlow.odg index aa6ad094f662741bb1c1d66ee8eb64a6506cdd4a..048f2dc464f1454784b5683d0b3684ad58b7e123 100644 GIT binary patch literal 23883 zcmb5V1#sQUwk^7rA!de{nVFd#Gcz+YQ%uVo+c7(4W@ct)W@e6KW_;P_{`;%`f9IZ8 zuT|YrcWF#X-Lo`Dw}upDz#-590B8W9QQb-*8=PT)761VJlm9jW?5ym}T)iC4j2s

|GfwoQ)hUtxQ}L|78#w8v0)b|JwL} z)4vRwxf(He+Sx`WC^@VXp@(cg($Xzyz^@=i#r=`?PI;KIN=TCSj>OBsPDjApp?H1l zchBIqQ$0j^n&J-|FZASUlDi|RR1r@G4Pbp!b|FpNO5U-$#jffVz0YNuo{o^AfWOo! z<+m-Y&L>)omXjbqsWdNgA7j>Qo@1U)JuS#0x0R}&wOr%okp86ZWxV7Ko!NE#fn;TS zP7ud8J?qJnADG=*)jTN)6KzTg1r;VKJ2Qa@pl&vwj?Jt@tM-eiLN zjwl_6NnC+xYNjE}YPsx4eK;Uwqe1U#gZ(RiWuvtNJ!_4F@?|CRWTOO3stS6W{wq!w zmDdwA!_dzA`qA-v){r5yCrrr^HEvcu&5*>ddi1IxQ=(cRObCRiEXP5X(F7`<1sS@& ziU_Q$m=g4qxfLQz0M|BhpE^uG+mv&}C=F?Y5GzNkbgs`6K=IDKLd_Q`kCFOA+CM{g z3prWjBbjqf?e`+>TZ3S)cd3~ot`2`$=CAFcaZ;T9Cjt3Of?pRg>)AGKg<1`OrcCy3 z@<-@>%x&K@HB!UxbiFv5k-^Dqe=2HTK18f6bQ%h`?9Uu{UgNwFM_|^c? znh7Q!wUaTVWq4Vr8>RmCWJ)iH1?C%MAlL)7zWDr7+}GgTlf8$+JqtM*p0s6MExOt7 z{5*!#^;;YQJ6CTNG-5SoRmxmX_lGp8NnINxkWYFGt|hEt7wgKf4x;xZ#0MXOQ^r8= z261LzdS>66bYSTDWN-qW!Jox#GlH$DMAor`{DiAkY#}-iwiB8w&z)e*vdMhK6J^ez zQp;N#oDr!iqk{?hC#o}swbbDGnQst{*hDIpbBolwPi){nbw^%fQ`vqR1l0$xS7-c6 zf&9_57JPjp$hr9yJN+V9C12Js3T$Xs{(f*ytdIO1vP;LC=Ps1Z|24Q+bgP_)yTREf z?&LL#kCT;%YCLvdo1mWbwJzuo8V~8Smy4hdTOwT|NojT=+?kIz<{NcJfXH-DYBbfo zy`-!kf2es77mKKn#$iu7q$!$gKVk-$#q$RnhKtiJR`>6RHWuVsZkqJ6lXYBTIOjR z!D%}X5So%&;3$`o@x?Q3oT2tX1iyva+q0H)Y=7#0jXz$IHrV_vd5JfG$FKwkDK*yv zkN%0cuuGI10}dacaoE9r283Fs$soy-)(b~D8`xYI{6q2X&H=>`Z`X!lp9_a^ZU?z z8C*%zgX1NLO3Sb6c&Ukpq=+F~<7K)8%$m^sw#TC)sV5q|H5)%z*X`rS@_N{b9jSYH z|M*5Ic@XaagG6u25}O`kVUT_(A$uz;XzrZJq_ddyWFvr0k6YhHk^JER zV1@s&Qc#hKG$ghepQz}{Oy+I|Phwj!5}>&;OipIV-V;J##W0&t`5c%FhG03!sL(0O z!xloZ<=ss-kj`0O+J>AQG?~^9Y`<__c{z#+56>*U*69jOTd#3+Gwj~Hn__Ak66_wD z<-p0$^~sKLKrq=k9kJzT2n)E@Bvs>nm|i>Ss!S*6#oF}0B}`h>8SW2QZ%AfWNPe_? zZp{tgF*0)TJcFC1Bd4R1cE|-K-nra-ZoO!K;VkuLAQ0s1kCaNQt9k&MbaP~;aSrAG+O@a@P*II0$vJ#Q=xO^B7MDo?pCbFvI zgZnP9WfHcffgAzL6Qa?5O@01?p?PTVPwa8NXpR>70su%*{HNGM`8W26I@p_AS-3g> zi^I6EFdCVdnAw{BtvWa}nz%VT|3hK^h9&U7Aq?stq5=T>r}&%xE2RB5LHe(@|0;3* z?*9K~T^$^39gXbGY#IM&9M-?V{iRTD#zxNn0rl^$=IG$)=J*fv{}SioYUJwX@}IAV z_iygf(b>Vm+05ntfF%4kNGm%d3o{o+5i3_aBS)A2aJ&DmbIffWj9ktBQ|J6U)IUS< zzdzzUa?ya*R{-Fj{0~ai(#_7;-pI<+iASL-t836c8 zasHBIXz;&f*v9N%hx_X*vZ`X>5Rh<4=l6SO#sjW2yKRo?0}5wgi5MTN@_$(;X}b7tj6?BlS;yjLe7<1(UVQagh$DmN!7y{ z0FacF)Y8%t(smcN5SB3VQ8M?kv9a;?HumuFpwkNB*9&AePGB`l<*-cSwapZ?&jq)b z5H|}EvrQ6m%vQ1cq2Uyv;+Fc&IakU(U)rrw)~is-yHLfqM9Vkd+Vh8*Pr`R!5JoNr zeE>aY7^g^-h;+QPMw*6VtD@;IKZ|?|w+c7kT3>ZgnGUE}H@HG4ti>VlmsMuS_s9&x z&?>_p)rMhprV+JfQLR?d^|mpcc5#hX@m-Dy&Cb7C+*3Nd(>r7m*7Se&sw8h{q;2YD zZ5c)PTK*b#OdfPj9dgQ=^3Ur1o;B*1H(_70n+gDA!og)DBj;jZ2BQkK%t zRI;*G^YGO2^GC3XMu~`osphkjP z+>)B|n#TIPytaz=`uh6Xrq-sGw$9E@PoUsdDD zHDehKlLxQ;k$F}qP z4)TW1D<=Q6jP2IVTsF>Kb*yuM6)6?t23+oFD3(G638yg!ViwA=nHw)_r3tPWewokWrc2@WPEF3;; z9$tNpkAH4%eteAY@9*y(UYwqu?i^q5o?h=?Je^+Lo!p=Nxqi5~fBif^zq`A;eR#fp zzJ7Rm`S|$o@bSL~0MN;#z6q;(uKmeG@?1Cd8}4^v`e?AZ1kp-3EW5$u(CXDV@fDlt z?U}N+uq;c&l()tljyJX3N9QA%S$V&@jBZqQ+IeuBT-W@8-|SWrVM7i-x>co=5M;%s6u(I@QEQ-EAY|P{rLD%pkXD$;A~?9 zwEW+;lIHhwiOY^#`4B5>V(_OhZ`ySOI$Hp*vPP?3=C+4{yN!ewfCL9U!PE%|c-kEq zsmko{_YU)jI*0v3xXR3eii1r+u!^?U=xu3f>bdL)1jHF(hepr9huzlwjDI#`mi;lYN$0we(1Fq$cHyeCrA-Nz4JIjxE>Oyt9ijsS10hNZ9yGfST4c8GwzfuZq9du z`7x2qx&G%6_#JK)7@KALkF7j#5IqTCSniiyEj7T#z!bIzvlWa9tw_7=+$2@ZJ?8W( z5b|nkSwX-gk1FCy8U560b;RxWhQM;lG$E;!$AlOvL!hf$x=;1zh3hSt=lj9vqcwi_ z=lbUdd}x)RbgR#R)8#JJY4~$~mnGein^!&oi@4b6eu34vpzpol&+La+7xT2^AJCIw`nQQ@JM}Sq9Yd}(z~H@i1I)Af z+DWLPjP+ycw+rmf8LvS&FVSrwS@#n0bP4=AhTN5ff`)ZPE->{cwm`=K4cuiLWUI9ddT?BKfbQmuy1 zuT@50ijM;WD6i___yQvINd5D`klk6{4o7(F>b9!K!E5xmLY?94#qUW9Fx|yu&GrUJ zlroQT9^Dhfe1F&P-p0Lu$$I=goRy!q#r)|s5$QHzh0>x4_`LZ|r~%Vs(%PQI-jzV=aK;O}r6S(MGUh+*EYg#@(o8btAe|Zi0b*XkhI( zVvmi4g+U~k`$h6EyGxlbZ?t4bWWvPW{xWnHF7$hsJ3RFm2q7Eg(V*_ z*J`?uB$6i4<}l25pU}D`Ms!c9T**m3H=!PuQc3~v;Ku7*=6><6{_Vt-;frXt>v+dK zL(ss6NYsr|TH6M0IefO{ssf;M;kj(9|CG1z@1DO-e&?V1;Kld#xaZC6e4i31uEznG zO>As5g0vx&$EK_3{)}RTMnW+)I|<#xYPPwK&Mg$wl80vOJbU`+;?G51KNa~;Rng?L zus4HE`6z5}RbRLg-li27ng7m0PIcY4?~^mF44O%KeU{1S#Y8L2#z*%un;MNLBll|c zdj^De#o98^-mc*FgZBXA!T>+u`$fLgxB-c#SmeHdXq$0C!O+|Q?(lrAByb-+z?G4` zA61SKb*+s5I}(3MEiY^n;TXZwUeF805(Gfs89)n{AhZ_sYZaW+C`HP_)A$2yrk)Rg zVeQDI5ZN=P9Nx3xXp#jc+Xm}tO+YJT1(Q~nUldTFCe%J<3EwkMd-o?=W*>Y^rweS& zEfT~`2v}#2alt?=YG(w3I$}(9A~BvprxjpKlSUkU=5=ld(QGAvPINw=-$mbp>toA} zHRC?RZJ7gqSYuWER2be*7`w7*&14XiQWQwumUYvp1i@A{5Wtp9RaZ<*E9`IyKQ#aj zP#Xc!?NW@w@nrVp!az-$Yt=WdWaE29fQB!i;-bx2U zdQ?tZe(>_w``8v*a3S%f0@$82Froy@$ksgOh@Z-27`$io)G$Vi_WtSq=@!SSV9Htt z#bwTc<`d=@H6H1c=3sKam)3550k&X}rWg82$^iA7*#q!K)gZAL(+xc+fUBB>zmb_# zyAr;7)tfP*(AIPW;1sSlEal)ShWc1K{~u?U|{Ker4FMABK`}T5Aq=)l2&F0LvrZ)hZ2W!xvA32UT@T zy`rxSsa1k(z02_Do9bpBe1Nfy18U0muro!hH_bIiP*)cem77X6@2YA7J#3#5J!m+K zn1!%3>?rfh)?x7(nl|!~XN z3W`}=(`Lbn4Ocb+s@?&AW$XlKuaBdcgoQlw0D5dW*j!e1Inu}NUD|C6#w#9t>#^Mf zhiriPuq{48x&2iyA!|TrYvYIZ6BPlQ7?N(cCoMuJTe{F(H4U~ie!ToVb2N7-pF;%q zB}bGJ=$;a=Dwd@h#Qrs4&rh(4O?y;I&V=AdGRd!}B(YeDe*OZ4$^m(R%wZPkufPhQ zuC||GTb^}2A1#i<38lO}-A`V6uNk&@faF`CX4@VIZTS%iVGg+AeC5kSi!kJy@S(=d zTjuwj<9kB+PW38bAVy{mBH$EUX_)~_Rj3m{S1Jc=1g9FAMUPRVF&vKm^LRn<;(PeZ z*0RH2;ziC*8}RPi*~!0r&-3y>*I{H<2s@wl{L2B(3dKdsNd@L-S=(=88bv>Wv5bV1cDjlxB=x6e_E|SZ21iG- zK%R#}@5^^_mgiP-t#)^*TD?fFj8bSr_wUv(7V}6{_0*)9D%Hj~RiN(YBhJq&84Ov- zSV=MhgFF*S$V*iSWF!c1onO>H%wMt}&m}C1iwV+{d2cwBfn&vE9r2E@zUv<6Tj67Y z${4nUYgx1y+loj&FnR@ZjJs`F`V!r9TD<{ffms9=j4XpFs_H^~SPiaFDn&?!uqdz) zD*ZjS;20oVmVGFXbGWFDb_=owi|LPt1LMtr6rD_BBiWu+8~+pOB;iUjgN&d z*g9}`_ciQtJEY0|0*?l)#+4fm*ursx6pzP)5<)2R=`rqI>%{0mEAPN&$Xgpr0yc)`! z47$WN-Fl>ykM)R$$5K*3ZV!2ZVp-OasmYwi)CmH!B(^0Wh7xdqNHpxkLdnF(NTj9vt*pFO%Sa4}yR5@QABt{>;4>QtCN^$} z0w8lYXM0^fh^$Z!#*!4icSF~*z|Iv47+`AA$ z0W7|b*S!Q9YT-XEP^2Be)sG&j3k@~@rX_OJDr$pH0w^wX8QKFO_`Q+~H6W@!TXO_J ztGz-~`jw(>uOqc+!*lz!UpmFi`BLhIHw_(WEcG%WYl`a0KtlR*O4ve&=3+#b^Y04# z=*Vm(pq-`jICUFrK)Rp_7Qo+Y&_eu+GhdJj0$41iwg)6(j+h!wkDOgs*{pQ$pQ16xF3y6UN8@#to>J zm!Dz-6JmLm*QbV7r%HHQTqTv^5mw~_(~3cz0U$frRh!bn8V|4fKoTJpc5F%}DwV@Y zla#q7Y<}US>}_tMu|M4 zO6PoNuHFw&ZKGyM$!4W;42%G)ao>nk5t!Tm^Pt?4O!CGR-fqH>E#V8kAJG@w%LGi(Tlk`Ui| zEU`c|Kp|I}P!uZ)Fu-fW6k{anuuB{n?G=pbjw=Ad4IT9Bdm`I3dlePmuHj5PzdtBq4c=$4{(dkNJemnJWw1UtkaRM# z3<#ZZ6GGA*`SVaSw*$HWo{V3)Bmzq@{|v~6PL(s{)5GuCgt7$V<&iN&x@wuI4Ye^-Fe&>LysiUeJI(7X4ph7M6 z2qrqYFe~xm?2M9NTo%KC9$`eRrwjNEzVygXCJht-<&V6H>%0+BhTM69W9+mL7H0q* zKM$<1o`9mt0Yp^V3Wz}Q+y`t1y1j0X7qsW~Fxmk?p0?5MR@~J*t#d4#M>tXgCb)nL z$qw^{cl@FGz~6%_ozPA_h^T0sRj_E_l?Gf*&;v&1GH$wsw4h0KAsXwjcqVd^P$vPF z=sjh!-i|gX8;lz%4z%`kHH+l&d7Cb90KiC&d0|ie9xnX26DS~d-%Gu9)egPKY_-CY zuPQ27phfMInNLQUk(!4RfLBP>jWU((n>g5){2J6l!WGE<0j>s)R6gAF{O(h3_Fi<4H9&q;wOwu*r+XsTLpVEAXYw?Y?D*Bvuu6- z&(_IS#waYMDI#PQ73k1YYUeo^XOL#pNXZfhfXk_^D~+;(BS{UAjK4vYO`QgFe1-0F zN&JyJo9wL@Efy~z@K{PVY1%cuI+<AG#dr=nk1lb53tGUp&7xqwy;`imzk|qToe>|3)b@# zj@H|g!vhbmnhR+3t}8TEW^)uHbqPek6h;Bn!CEq&LM#@PSo8v73BAs*ecE3EY50mm z5{P^sSNk!1YSgrR0)hZjZwv@n9MM%Oip|I={$CYu1O-o?Q&UrL09Ha@R}R(_fd$rE zo$G@P?fFB9>5e)%1jP{aHR<9^J3b%$6BINeoR0Fo@(10(OS@*znh%O-2LcfQ16zVI!AfNe1-JpDa}#}k^K=e)1;N5JK2=XK&;eq_!D3@~ zawnLOF(5}ywqChEC%{wyxJLlWS@@lJIs`7zl8`p|yv5%2Q|jYvVmF^kO{TW=Z9DWx z;pS%8(4PsBpWxa60X{cNW@L!c}k zpXb%=Wry3e*j0iK22%U5 zeQyaCFjjIQjwU%GWQzsjoL%;Np}%9#KV3+dgea!m;$gqWR0ghvbbU6@Oeo72KS|FjJBVo9t~$)rJ?Xtzb9Bn`zS%*lh7RsurX@Zj zzavx^o&zI#XON{3)f>?pz%UiR{&8Lrvd>QPtxOK8wCOw#;MPZBUYSC7Sy!bPbyP_u zv)|4bQJa3z54j8|5!mWpU;Q-_HWkuAUt-)l)@yBT-z<3Cfr7znX`!(foo|VAW)bG? zlITmCQqX4<6H_-#7KuCHwFQZD6(zC*eS#y`HOp(UYFHW2JS-{M`$aUS1FDDy`Ar|e zA~%QpYaNgbpTN~9$q9``on`t(^ouh6Z)GwmO4=W+FvLEf+b=faoPtl4n>#JUx|i62 zu$4$|O0xFn{A)(&AtUoGj44C*`N-quv@%|LWZ&`GKzCIIfyCjaW$PX6f$iGZpAp29 zAICpu*3hvgZ3`?_&+tNTiK@ZWK|xo}7kBnzm1SuoRfn&4GCvvs9^ZaEn-d%pF+Di^ zsmgX0ex`E|1;p$rNVmTaup;abQ2R46!D%zk`w z&A)6675^}lf}6p?Gq}n?r;m;w#S+<1gD$cq#bdyMPeqJJz|IzFX})89*6_t+4YL4A zE;hxpePX8ip(Z{+AH))<1bX-k7Ca_ZNeL&-6#I`pU}ZyOBk4Lf;%ud7e_r_d%+qcP z?AJ>kzl$Whi4Pc3+lAFj;XXj3&sWt)tS@7=5WmHxj8+yCG>B(IKx?1^vBw^oo$l5r zmKTE6%*2~|iIXj~WTTbd+rI=_MPAy!Y28V@Hn?Teltz2!mskbx-w!!tN!AWrliarK zko2N4&WMuw%MW~|(Y^j8f!MaZa~gi54SObKycvq)40q*~z4n8!(_-oyTI6aIWZBUX zch)Dv7nRSWx?$Mu$9Biu?Kwk=4|-?@bR(p|J5@&}nqkZF>~i{SMW5QvSyi3hVf3*2 zxt&H?)Q2yBVD2=VSo?sv3^aNxB8@-gkR;?sR3M5G=Jq74nuZWQ@^Z>sN}o-TEie80 z8O8uftwHxdU+sk`z4BHX3m@;4m=ho9pX#G+PvL5YSXZo~&IZXRydKj@{skkIYXVaKjR@)1w zrN5^nKTj-L*ejw4rbRb0ZM+&mS|LeAU|D;fqv^z^QW(6soE%16aLsL?y2}SIQ;Mgoe6O|<=S|k5;w`v9)Ga)Gw1b05w#>L z_tcYls4DC(Wv_K3%`y@w`z+~E;O-`VY{z!uVId|wAF3A(LCdhhKkN$Ri>Eg_6#BBT|%{+};a{+n3usD1LF}=H*ke#|G zZz}4u`}3oinacus_((N714CFGQ^;!R1r;6owJ!z%Z>dSJ57uwv2rlbcR78dMLe}C8 z3zcC04gzmz8x*vL69_5Snu8gf95!=c3mw&v+07MPWby6fOpJt$7guCI@-*_>$&D?v zoIaUA^;Fn2T-3aVg{b9=$o}0uHp=%@r|;;Q2zjfQw2Hi@3AG`<;mfmUzzKxaDO8Ll ztzao)+5AbOwMrftt7dkX2rKo&0F1?Vw^KxI3};ykWVYWyDP?Ko&R=y8$kwAsY8y%} zHQ@ATge7@ZuC?=i$eACh_BI$ll;!Jx|9mL!`v+r#;rF!%(Re%R5LB3ycYM zo$I}RV`s(JrzMfqLCYl-+Q^0OXl+fGOQ(W}hcizMIbKHx6uUf=mr~2KoZA$~w2~%s z3>AuZD!1zD8N3JL3Ic)XGo;nfDpMK-Y`Pc0#1l`mpuPR=zNM+p0X>71epB0rL$s%8_)ph>IlI>qvu-S{m7zO@(L`bs#bS+Joowr zV&7@K_v~C+zMck+%p786cj4_zY)7WgD>t+!+f)jt4PD>gKQ?|5&Cs2$GoWh0x=)0~ z5gC5*(`yuBd^sqXG!>J&*^r19p21}+-WT8+&fmq#Ji@}vw}4TbhEi@fG^WLa%OWop zeUvmhY!9(tQ?fBe&EWe`|K=iHrp*`9O+c}2AywPH#ddnWeCroc-^5WMDpUe7VnC@XGxetKxKjQ$&o_HeaL$Cm&zqzs^LnzerRlhG)?-(h5i*}VGWD}ru+0od7Ad`BfTl`7x^t> zzP2}|`}!fnf+{~4 zp_|>49q)qw=w#Tg_-A8b0DyJw|JKR;JKZnFUmhnXrYcM?B`?lsXJl_>ZszhYu}!6l zjCDT~^5!wRxVMa*y4b*I-V9m+l;UA6E79L+BXO&*85$)6IeF3QBvbxlY8Xd*0{bG0 z$TtZD??c84u(>L+Gu(&P&ywvG`}Q?Wu@zxiXPUB`=)FceO+>QH2rP1HI3)`DOepKj zDNL%->{n;gHg{H_je7tGqCA@Y$sxo-t8AsT*{cWyB@!?!j%Cgb-#V5xIL@lQ%pQ}6 zO)>f2u?yy;j=53ebcV@EWBCvCOkUais%U@W(ib!C7ccc+{aC&Fki&|fPHPM%3dKWJ!GD+E6(H376Ms5 za~!g(pSsg1NpY7*tA24JXkuy77p2{#V3lXW@V92q#q|La^V>fRw*6ia{^|4~_BKqG zzhV^T|ERb9J1Ym6IM}=XKf0zrzU%S(;z=v*-C#=X?h`Ab=r^!Al}W~I^pn<`o%xE* zc(C2JIZ`zvC{i;-k$UXA{worn+1&%F0BCctR3QrB)zZR%s~QT-)y_fd_D%YweiEF; z*Wz@qy6>+5-OJu^jeS!V1O86mr~CPs{GHu)htGM=QCA+qVF!xQCok;krhKQmwYqG? zcJD^71OwmKtBk5GvRU&kulk6j>!V5cHwvxc@^?LlfklV9%Q3-KTZ6Ad%@&b^?akNY zOLyj7>TR4awjYzL#@828IKIrCsnuqZ7!KJKMV#HXnE6(HHC?(A>t2u%$%8)kuj^r} z?(KfJPfv&AUmxL|6Yfq?f{mV1^yq;E$-kGTPf2vxu?2@BQ~XztP+E4Jy4|rINL8Mm zv{4EcZ*N>OaPleE#2fYWimyZIjjG3C=Ff%q_)&fTWc@6)c`OjV^~5Y3a!sO7l7f_* z_?rj3lV{zFR=0Pq!P`^bZ+G7Vs^$t$J+2#3%Z5&U?B3Fzt*<}Gq@)#}5xDCuc0{K8 zW1hGB&KRS{y9;jZGGNDtqsE74jdhLrw8~PEGT4$9jZgWHXqJSk@-@89(octcuP1;Aw+X z!(WcsLGEIWXnF+OtC@>rz==o&AoXG>N$9cc%zv7}6XTEnLq1`(L6b`AxUKiV55 zqdiS?V?rn;rL74VAL7e>$PZ28O&d0tM9ksSE4HCM#(5a^x>ER~S*=)7)w4BGH1{XJ z*s0xJohD28*$t=(s8|nj!W1F;Jh?k8A2{3IYWgvmJxp5SMD5R#5ioLW`z8D?zA>Tv zOz(ex*nQ8SZinxj8nzrPfKVZME&R!Dur2VGY?*2M;cce(u^U9npH@xCKS&!2O>s77 zid|WN@%!0*BC(f2`WXA3KP_m8fSWxqijfb}@p7+Q;`6o@+oBH#JeH$6-LB&tY(B;c z&jx_XfB@6`YRa+AbGfJ25RtXQ|3Y6*#|Pv0LBC3|YR~2Wy!^TA^A?HOcGg!fbo_=Y z$j_L!fHn4x&?)>{f<`rXJbTNrtL?!zmZc({c5ZOGNnxYtr*(}OqQI;zp#Wxm)+|ui zsFOWs$)6tcNLZbp*zUGUnEys$A0wORdQ;g3G5Yc; z=tWRSfHt*?!SH=GD1Vkt>iAQ#MVC-4@)-@S+uwi7_IQ5ztPkJ+>Z!(-+hR?CLET9= zDr@dw+!Av-+g$ccA%8GbFO$L{=~nHT+OZaDX#D1u@fLmi|?F$Lq5dW4UZmBgWr0=JViod;j`j+=o{t zs~JzP64eO1uzQ%=Tk?pxJ6&Sjl-(<~iaBiOkLaKL_ed`W|H}R^oxYmnKrV z76|eRw|*;Vy3<4GnqCFAZ$KozXgIzA-2Jqj_lh161o#A4f@voxQ82?D$|&oQLF)Oj22St^#2{epR#V zdEP$NOa4-iUaF7@VDr!5{(ufpR^4~hItP|=C@5!2`;ej-;Zy!prwlr-zWI|h%yfB_ z*GuDh;}hu1gD>+FCt03cBm{_hgqmxUC$45PG*h%0zU-I3#0Y&+mrf%i?s^d*$M2uZ zC|O?^i9ZV6djE`1U5NW{D+va zl*ia>L_9dX#~B(uQ#47Fg3?B(Wv2txz25u_ZUe$8TgD9{lfXBxiFTbY=i_dSx_ zDfu#%z{M4Q%F7?mo^X=F5AQ2`=x>@oF$|1N3N5{uj+_s!?#0v4#!ArA^Dd8UmRn6M zcyNL#XzG#$+cD7U`+b{cR{S)w*CfZc7iY+&-814k(f^~E3%vuBuPJ=Lh@cZJqU^_>U>^}njTXR>rFd`UDfNJVHP>^%Cx7F2m@N_y2ppuZ zJ?-ao);Vz^Yt$HA_6;Bs@KLM1jpMtxbeinlI*2E{uE!D$!9kBLK zPjtJZS!s{B?6jI61`*>&lBHhuo3VMY!`ol-thbW!^I*-s$09 zCqJZ$P3inQ3c)0=st`!h}rtpl{h~hp&*| z=7An-ZImSgFON^-;TgN0cx0F|mc3V^^_akhjXor5hy>;I`k)U+GNe==`Ny**uS~*m z@hU;Hm|MR2?1P)0Rzh!D_sQ$?F;`U6t?94KeEWu9i^=N~c~UoGz@b}f)M9Kp)STC0 zU+S~+hw1T4-2t5&(b5;)QDu4VM&2SU1pV$ z=|87_WnWQK0{^N2ql2!@nYYpJ7y~{D_fHLyR3b2!Yw2;NX|}($Ia$s_FjsR z8kh~x305}+mZq4(bPX4AHuiv@gTf|xaO%T`WF-c@$us~r^kCOEKKB1m6>20-}`uu|Q`Y^U!*KJd` z<=^&`V-a%8=B&-c%&;HX^akxJekK$8}Me}fi2y`4HM(v;$Pam-VR@bQNoTr5g`_6KQAk2zH9U*6?8-s7TV+4)frOv`J8*ai>`J@UMgeYD;~h+QxkQ*LB0RcGWO zYro8GGrfSux=-8M`iD^mKpb{KY=Kb>d%#iJiE2g zrRbtUYXie^<1d(tw)ORKY+p2}2^e+L zVceIa1gKv}BH#mZSXV!vy;u}J67|~4aTk+bhn*3E87LZ*Hb-<|k%q$K?Q$}kh@l=p zvBL2-7$D@$C@{$`9?QwF_WkAkfIRUZ6Z1>^a5De~IU|&YdHPm8WqjqV5lM9=;!Ka%T_J z{ef;BjOwHUymiPyw~(j`b9vppn#@i}ey6})0P+b=YNQW4Whu%SEUdq1dcEue(JD1( zy${64Flb@}!$FBN2q8N#9tuJ~cymlyr359?pPcHn9%JBr2WJ0%Nf< z&s`NZ(W5p^SWg*s;aNhrZP-7QO7yN{9Q*s@VZ{rVjyDGcQ+;`;DAC?|NVWx(Ph4IK z!&s4s2U_Of{o7tj`3=jO8M^18*Jbt6%>Jabv@sWt`$0PvaFr^603{05OU!vp#6sU~5490G)bIePW45#|I?j{Jdh4Qk{Z8Cp8kiTuH(z{eZI*5vT@u&m-+ zjd9(+{x*Axl^z`EVjM??B&j(yeeLH&V;o)ud&MuHB9UL2lS&;lm9gREc6eWO!OifAf{7mb)d?zAs;=y3Yf;s9XJ~u zD*Ta7qY97xd~3gKUsnKk_AL(*kMT!5L&sr8w;;IwpTA6dz1ALA@=R>ALw&^iVW&g(6mqzZv=pc(= zMiE(M2H=EJ2FY#1*2CukPqv>E(-f@0VgF3msAae4hPFzV1^*>sz(kRkQ&Hr> zc&)Iv-pRe^<%kPRoJh5qTO6`AN;0$)o!m)dW?n$oN4^nn))YbZ80ZC^q5GvePS%zQA7@%i0kJc2!)Td3)pah_-@UUS4H_H^rfrVW z@)-PdNEL)w5u^u3zI3hUGg4WaUv`K}{@aop%9^YA0C_^l$>-r5|GESFKG2bqB#XvR zx^u?mheZ#u_C4_akdVUa@;f=PBR{(I0-@wRKFA_42r+!iKOA!&<#NcK;@Y$8=ZcJd z^dh!$o<>!PiNC&VUUE8#$j3p}Y3HacgEae>%$zD8`{@w) zip?LPyYMO>Dr#S2e4%n`Y_=MLqAdurVsLAXW-P+q&|;yxeSfU3Tp~fui6*WjDq5IK7<{)zn}+KA^C(u5T9QJ zyK&~&s&7YC5=7C9qvSF|dZefZ)(ulK26Vtg%=9d*;Yc2E4Jskvy|kF0W*GyA3o zDo35>@fnX1Fk|6o_YUf6WS2AN!CnU^S}_so8I=(beGT5k-4oC}zkT zaQVO{WL{+|x>5XJjhuB*6@A;s58F(}s z6;QffeBQZVz3=yS=K1c-&hE@!pZ(3w?*4UMA5nYfNpP>Spa&^J)JBSYgn7GT4SHy} zTyqR(`_b+J8|6YLTfX*enf@eWCS=G_o~eQE70oPV`*|~b?t*E(Zw}*rzHt>lV^ucp z=Ie8XOkb2di#UxsUAKz*p5`+1XPeT+%O6w^XGZewR3^iI;1U# zA`21q6zV+(w>&@Mzo|6N?NeJWH_v$h0aU-n9@7-#hZK0MZOF5Jp_i)mCZ({r+TrM! z5;7m#8N9_RO8PhghJlMzT6kzi(D-a0YKc53JQ-5i2hA3?ryF^Kx5R>5VCFIW5wJQw@FI zPhSmrI<(jr?~^Bg-O3l0XEun95X%Axfhg{kOh~pPdBg%!I1V_8>*2Stw#M?hWV1=l z+Su&*Fb0vIdVjTJ_S-k`vQ0_Z_yoLZ4GzxYCtEbE!(y4gOEDruiXY}UP^1|SUk74( z^T@uh_hpfx`-TWN2@~bRp&#&o9(aVHV1v{5A%us@G@8F`j@1wcR+X`3>F}G7x5k_3 z?a81Dv4ZW!t_e9aOesvvHSW02bwpP_t~xoSuFH)L9s_$<14LxIm9pGJn1ws;>eTeHdq`c6G zzjf7%@2=4D>_;L2`|;A)VX@FTUhDh8#q2x#5dLP3PXQ#XvVeZ2BApv?gBtiT&(>XN z=&q;4>^=|2cxC1nbl5rd?%N)r^V2)anrnm-%=K6;;gf;yX5kUSaFJ>~oadlOUu~*M zFAm2(goW#QRvp~~3CwV6@)42^81x~MYAi%pD^ZVpr5^qQbPb&eGu=Y^V4d%P3;EbeTy5q!owdbYE)tY)u@@I_DngtH%=)2_;7Ww%z;uF zKU$4ad<~;w#np3g_X3Nc9>Mi*UhbLJc6+CfdfUSS39Mke?SawP5TY)OTJkXg4YMb) zH;|CI_;?)(et*LPcUz9}Nb{beUy+3jy$qAWk}(wa?H;>~c?-G8jml+5R$n ztgSju{;Q}rUphn_1iDximp7ypSk|bm<721Tcp3d=T~Z0HC&rIfm1Z8QeO8{Kj9)KB zFaR;Ajc9S4M$JFp;(_9#3vnV2D-?}h&?%_pw8HpHA9Q53>lX29Q*XUgVr*Is&julT zJWI2O7HYO<&&KQ0%g*-aC{4O)PnU`_DhYpi7a}@cim596!fC>d%jWd$i0jvD(Y|Mh zF}Le@QZ#Z?(X(4DgfGyfx!1+hkr{R{eFs1WeNPBpA2COoc7nx8g%zb_(n+Yo#kW%e z=NsUMMBz{LK#>nQ6VM^STv=z`r^;ab_Q?4YL3Mcf1S!?D$c+)VL6LU~)v&CDz{J8euYy+z;jMkV(S~q|x6eKx%*79Y zFn?ruA8PuX2Oujh2TBmL)xds-u~Y`Fx(u6$OT zNUxoXA5KTqrt&-1O?D~2pYGaa z+*TOgyC^q@EM5s_fygLV$Zxr1p@kgsGw^KFZzZb=F2+b%0XpY!dWZA>IZ^=i$jh0(j{jr zukZE&c#13}?U>34%zhUzs2xv9jyF55=T<~pD7 zxr#=87s$AIk#b?B0%FZ(l7m+&g zuOYgyp43tj0(r3FN_0_6i~$Ra-9trO*4a!kEDj!H2{;^Nn;N8#>s55G{t-=0k=;f` zp^_IVG5%q`{%*Zf?x9yS#{F`Uw8E_rfv^is1K|V`3gzvy-4H3>P63{@X8=!7+1R@5 zV4|!W6J-#RUoiOu<~B}=*YduA0}&^{@Bw{6}$9RW6zXra_(&re`r$3FeGGN_a zG#B=EW&?|v&%Hr3-e`8e<-T~!1!QpDl$as%>a++~tANwES=M#?)Y6!oth;{5o^^jI zR|lL@q00>cMK`n{;EcM%$9u3MQc!2*<#OBb(j^<8`&XiKkx5#~9Qj6=Y90^~PKyZe zmoz=VI}M`20p6um>wfhd_~=wrxbh18rpD5N^Si!f?yN_p?_@{4 zr3p84)HZjpV;HcS*BS^48wW7+vPr>_vPK*$``f^ufOT}b(0$pZVohtw0sM-`G;wX6=rO zK37hkkQUV8F*76xBj3LjvF=kqTBnTuVJ9>`sXtUL2I;6AVKg8+1#%qV)_{_R6eLms z1n7ob`WGpO{UGR84*9iLx)@Y^pLBU7DO+srzg0%yyAH zH~j949i*4wmsKH&?)9#Y-5ciW7SVf~(W!OArh}aWj?^g z<3%y1JQF1&^CwpE{wiINMGERR;o=;8>OV)B2lC(UA4-4{QzBQ51eIoEzHD?%XBFI# z7*b=@`+R9Dt9K6aq~H9iGpBXCbhyU!0n+1yl#d(f+sC(a8uK34rZX)vHm#f(ar^qo z5;LknW7W6R7Rdy1`?8Yfjp;s#{*?rpOW)w394A7c13fSALc7b8 zDTJao@8?jT)R*P6_RGry^Wk`b^OnL`N#RMd&n2n-=I3rJspj&_O^*-A`i>B3r*N6` zoM=lwq{UC>M-{55CE*q8G{GRIvH_B7t#xM;H5#*^)U{C4x#sl%ob+R-@f`cUWXHHK zhj;uW4C*fstWw4Cs1zU^g8sYwSR4Ivd$h5cNBReqxE=-qGNJTAX?M^0-U+y{{z)Ixft8Q}}>bJ*_-uft@dx6GFpdKxvykvs4lW@})0E}pV!&u=Feo-wF^ zuPTG4(8T`Y+A5-ekF&J#Zm2ME`FxGnpbrUicLL&B)PfEAx8*VwyWxXM1 z45MNVz&<5aa?s5E)|=)NIPwBwAy&&8`+IM&=}E=kzj)I(U#}W=S4;ZO2&o zt`~d8fmcEc{3xp;11`KkT|>W&yHg&)G7^o4yM37vOr3i9L~?s0w;hd+I*?6&>a>rJ zLA3itNN?5_K|WZ(I`}9Hp;}v+3(b8EGD;S1i^cJpCbL9h3A|IWdNw|PhE+qq#i;7( zXFA;>cv_2Ef0g8z-llJ^+pmvobCV>f6hbwT%r%E5aZ$4nnQNTd?R4;Mnh?c9KHJmT zni|*{GK|?<=4vul z%7}z4>^_UiDUsfC7MXt;Cl~TPdF3EO{Qk0;aCE|e{XvYEmcV0EEnHeWW76iE#jR>? zb4npR=g^d~k+7UB?a_sXo-mH`4^@^Wk^bQ8a%)Oz)9uuuV6eUSC}c4l5#1>0G?PwL zxDBqg51lu9@?(aW>0BRm9EIqP`HloyAg}9Pp;&v0~&3OZ_GAB*#G54}h zu8+U)f#D?ycU)^X`K)~xXe<5HDM!QN)|nKzmvXBT;fi zHXK#iCTkyIu~uVzCzGE0v&{=HThFUi?#U54t-3seEod69R%r{#!w3jo{g^eqecl1QlzAV%dBQb*}D-)9PPysM6I z3xa{Kp-0K95w7{y#(DjSp1K?>pI>94Cgl|QNy;KUx8U0VYU+Bn(`np&$24!M@zKH5 zUkjX$#YMf#^sz_D?*T&wXQ3FU)VP@j4|tgds^8@345*`9!ToEKuPulD4l!WInCK`_%acZK7Y7{$b0`d?~X3S z-VP{^yJRsOz6vNniwpKY#W zdCvsfD!tGdw{SdXa$dEU47I*+#5=S#g{tJuCCtk`$__mgvZ_wC7Ws#hLQ?ivxrM4p zFBo`R@_0#`WD~!<1zp2MDU+qU_XQp6_v>9lp%uhj*q4gN+Md|XYBtK9>YTQVoP4Tj z1(Fr^-Pw7J9SOEWY_jX-*t^;>!}VU+l%!w33Y?79ak zd+I<``~!o^7j`!I%|T`CDm?gg8lK`ofP=P1i zDVL<=SitAIFTGvgvn*4ijzPhFxmOp@;}zG>mjc91gK;~LAbumOS~UP$cDJXU$5{` zs+YA%=?F=U?uXBy1j+e&yCmc=b-5p!A<1J9+RwnpwD9r}Sy@k??8Cg^TRk&?3n3M! zR^4X_m8i}-rz$oLa;x z1-@R3IZPfy*wX}xxz`UM-X?^*S-PbboC}r-$`CC?-r}!u4SI^|^%wFhiJOL#pO!QV z`?p+(G1e!;W2bjqT(%3Pjo(G`$4pSURT53*=S_X+i)<*C=nuGG$tI(;luwMAAb%B~ z=o3i!QL6vE!@4f6G|fGne!F1|txC%o8E8@a?PN00#ch4Fx9RKFTtp=4gz8xDrzi

xl;jj(q3 z&Q4q7A(diIy3su!-l96(06guRh3PvF*T3D>wk`C{2gVBqMg`!1^bsYI272 zZ&yY;vN|2g_adq>RbD*L{y6?RCj*m3mv1|5)VtQoPcddZt@TX@V0eyaE z!4*Z^BDRVGrO)47sX2U#>DkNj*%V4|+;!*NexN>jke?YN=BR%m(}t7D6x;946RGsJ zOP1V?=5t2j?CS03Z}XqBE|KM7V^w3m%bkAu&d%I|V94X~;QDaGof;SCB^I&0e@|a1 z(d_M== z6Ws1656*6eRY{c#kXK1XbLvNWK6miyLU4<_)3P@J-LbF zwrwd)nTdhww9|*A=jM8YIDGV$1PL-w!Rt8v-k-q4U6PYAlc=qJ5 zEx_xQK7~-M;1RSG%>HqAO&c8YqfRmY>U)tb4`!>>bc^!GpF7-qc*GK;AolQWZp9Dx zm9882o$Z1uq{*URzBh%Ls~o|q7s28$`yXf(t^C{}3sN8osf)wE?F1}8dw0+#bEuX~ zHzMFBU7&Pc$$6W5b+CW@dDq~SPE)pLbkMnu5%`b`m5U+<~IfIf8>GxSEKNF^r%q&kMsDSRlmy%f9i36 zMfOoX_`59kpOrta{L4@HQ*QVx1CO`=r<3pxs>8pae^ML%f|5VBc>fdnyXf%mkMLLB z{~GpwovZ%~=a=~K&y=6)!(WN~7s~G<#Q&Dl`!Af|#fX3A{8S_U%EP~K{w_-VGwG)) z@mIJXCm#R#od2IV@z1cII>TRa{1@0Sk>Y<#Dg779FR|j^DgRU}{#wh%Bjrz`#lK#| zzrLm4X9^z6ev1#q@AIJltoyw!{^!KOuSmT3!~DTN5x@T!e)iA*N*lx9y6NSmo;*rN S0RYU$jpMNm|3U5N+5Z3s#lwpL literal 18007 zcmajH1C%Ar(k{H)Hm5yp+qN}r+qP}nwr$(CHH~SzXWHHW%(>tB-*e7A?{}ltuANn@ z;>oD2T$vG78IQaa2q-E500{ta=?h8Zg3u080RVtMzu%_-*5=m6PVRQb`gV3!=7#!C z=C(F;E;dHAw)&3djo|39$*wDW&X z{^Jkx_x?X&O{{G7os1pm{$30Jzi63T>zf)o(g~V7S?k+5{x8Z{|3%r!*4D~S-^SSL zf6@DIrvA}0(Er~|{cp87I_Wz(JO1x_|E;#)kIMOf*TVTP_h#o{YwBR^`2VQyzu9SL zYv*kDr_%pvmH*Z+pnuos=;Ur??D*%=Z)@s09<(6&UYG8XC-(FgeLUrQxRi8DmzW<| zlJz*1$bAU|CE1Dxg#e|Q)Gdg8_jx8>2ZMx863^hA{59;#)DpA}=Hk1it(W@Hd407w z)M?nTN7#uKEf)|!+8{5?t*I03^ZW$Tj_K|CUg}C1<39Ey9&$3tI0Drov2Fn5dsQ0t z{&HE;y4G9(#(h*tRa&JTtwV}>e)}ZPs#H1>&$_(*^o1mqOKk`=k*!b)ZAfiZy7sB~ z!cTeqLojLWTQyj;9y(-X7~}}%(uiv_``nw0`(?9YCEmyPpB~-~IXRH+52LB6sXZkt z(4j24IupWaT$`cb#{fByHWd)v{8B!_$n6yYA4l6>{ zlJB~%ll?o+trvbATHiR1$V%e z@(-W&nBZEe=z_|WMSxH)G`1xWr@ZdBzw10U)RL%5b6)7vd5S02iE0&D=K<}mLBJ1O z%^g{GD(1kpV7YN+1(|r!Ka6!AS0MLaH{C3yQe!AA`wa~y(pE8+D~?DOOSn$(R~Tzg zkXsmoIhXJjAwcRljIO_Aemh`3aSfWp6`L#n$$&I{_+v-M-+qrs4!36yYco)g_Pmf9 z-55RHn;y{cnk5lKoMBe)ZV2w$ihK*EsB)akBy=FZM_r#@z*_E%^;KL0F-O2J%= ztwH6RwDRUJ@3(a^tbP1!Z{?>>)!xdbeqs-NM5{e}6DHs&iO zL%HU(5xo~T>HRw0MdH6ttl}nXdHw*Z=uf&O*p9m;NqWukF8HK)WpH!@FH^FD9$qLt zc?dMB68akYkcG81XC;xC^3!slv1Uey0vd_xy7%Ea%JpgzemC-B8X`aF7wGjn-^b~8 z&!dmYBx8Pq;yA5;q4~KhNu|Y>67}l!jl3UK^4nBVDIa#=2W7Du_T??}Yi3(77MRYS z46rR$n!WfWn<1vu7$$^j;vNvjRoacGtYW+fjiPKI!OhHGDAT36AIiC|IO((;DBn+- zUr2UiDNwJ@$Zr}Z406cg$su3&MRMlg%o#m2Yh2z-x?ifJL`}R{xgf+xj!k!rJUF$# z$A(sa3R9ODD=0^~AW*HUNoLf#u{U%ryCw{Vj}-08>lc6rC+sr?QmGQ=l|7koa&Z+m zPH;CBlw`@#9v~uHQva|_iwN6FLP#fg@~%{pC0UMSNm(G~MuehCCU=Msp@~hSuVNlo zUFy)+G0CTNPFZmb%)DbBeGShSS|1)0WP~pMaxyDqOyO14IZeBkZ$g|bGl-EzJ|wgG61Peim0B{;}%+iL~2F8;v5pB zh^2cEfj@W3p?DP8t|5U|Kg`&JAc!hHq3)=6>uOqR@0VobHj~+8pra;Uqac`lb67D? z6?L)3H1dw`nl8%qHB_gs(Q-zm^L9qZekavx5k6m3%j;qoSSN#x9%dSh$oCKO!ujvt z3k(N_V5W$Wb->)v1TFFOX}$!Vp3ILis)IWPE3}|dz8gTOmYbDw}X~h?rmx3YLei z9AVWMFervwBN6Y>;y`T#qQW|zND)H<_hS;@W2a0be3W6%7?Poy-|#`&^SuGyo|bf* zm#*RBJ!~M6e;^G>rg_;KtI*wvDR|>9PrzUP9;N}S_ z6B{zO_Vk=7Wac6qM7u*QSSluD=D_Hg2$_uwmw%c$bo}P_)HToTZOE6tV5M$HQj8@`Y%U zIE-!t$Z-N+XfimAwYJ>+6Rfr)BdcG)p-y>CFqY%o3+vWl#aX*%nyAxc@KcXzI{5jV z<<1YfKpn8wU_pSRMN*rA8Nz;Ne_SAKu1tQXUO!OI#d7T+EQyB#zBeUric zS7Tcb4cO+fLo`p=0ex53TgNXw@wiN77i=)HgBTS78MF0Wx^wh2nOr=lv?|PKo!1El z$QS78^&*&=aW{6+P_U5U6J>?vbN?l)v^DrqLE`!Eb3BR~&ldTa%?}xE0R#xS5oQx+ z-zHMSR^Rtu$w^!`GfAr*aIZ_*JuMYEi60mbDk^3}ujY$rAfC8vNmO)iyP4h!{o>RD zhTX>@mz?FtSY1wc+4{?ZOGkfl#)N0{RxAB7wK7sda9eoHFQ-;ii6P{J z4mbJECHj;j;(pjvAEnJ-X0v_kNhPW5ts`U?fQ#U2JNAdc^3jp>Qt#cU430y>i;PtX z@HzHT4oX6od_6RIU7)}W7J4$$f8F+uTvXHU{n0MAd&U(s+Qb)Ovx1nV6j2mfC<&aV z&XU?|846U)h|aL%Uar-9)gptg39;Q#D+9g>TfnyJUg!!T)%$LXRO<`z$Gx@)qslUY z0sto20l(ete+>gnxm{5aF#v!+zdv52l9{u$fsMYol_QHm2e7vSM)1SbrQi zI03KOgI4C%!X!sNX(g1)m07nP+iyAS&7yxDl24e*SW{d=7jDu*4h-#0D<&1^r zh6im-fNVyCX~j&UO+u(oNNmkX^QreB=6*Oy^tD%h$gf6ZqJlvud-Cj)KUAK8K1)GU;rQj4m}hRCk+6|MMU(& zp$Z@)3nFF?Wo9nG#4IBxuV!Yh=jDxL5eZXQPn6aQ_+}BR=@c)gm!)ePtFIqx;gV@$ zl5Fgr>E_lfCf2E{n&;rqqo+4uZa(Vloah}M5@H!0ZI_nj5fBg%6%`el9U7UCk(=w6 zn3$NIoSd1N8IazTnOzjn7!cMP7S$e=T9TU3oe)}Go>5qnS>6!e*iaf1Qx%a>os-j$ zon4+*SymQOUmxAkkzDv&N-HZ03aYBAYMN`xnmSsmt6Pd18=D&28ydR1yTi+8BC8hS zYZsCmmI^x8GY2xeM%rr!25Uwpnn%VPM|T^?=h}uAI;NL8rq_DsH=4%wrh|j$A|sbl zQu;b4hla8yCky(Hn*F8{Ae2-9PFDN>6;y&n3xzGUYcATn_OL7n4MkN9$Z{p zTwYyYS>M^*+?-lFUfelf{(iD?aJBp6=IDIn;%4mQWAfnO;Oy+|;Oy@7>f!SC*Y*9= z{r&yj&*xuHFCQNt{(A9k007FQgs^~;+s0)!jML_dyI*o5sf#Qp1J9m;LNin2rflO# zDHm6H!$thkx`CL576f7da_Cq~O%BCmP6GAjg|Z|!1p;r!Rh2fYU6G0A1yTTwV}o<; zO?|&iyk}OM%qB6J3!6m}B2zKD*yt;~1~m|k;-X3yel0|SeCgbw*P`7` zo93#DAO4sx13{39@gwANzz`c}tW*!mjsO?ZpmBY#WbjAbMinmWl11@eDTq6}W>N_t z?nqc!w6-(#)q8j*pSJ8L=#K^)e!ZP)$WuSZ%r4SN^m+m-M*t!W2|?uwFnXFkn?O7H z=;Y_2lOm7Ut5#5E!OND|wsEuHoEG3mg#a>{J&yf%9?X~natZeA7#?q0h*S9re-1$2 zGk`mP#VL6|P4j&rfO&7b_2FnRxe6$vvH%%*XT_ zh(i{$9H4FR>+GF-YqcIg?#;^iIlBP3X`7x7fIeh>9(WZTNvJiKo(Ji^3Jqp=v``$G zYC=1s#h8+wzCXWen^_93H-?81!{uoY32;2PtKrj02cVN~!dB-&26=5*=}Go>sd!#HvinZ%{~goBwhig+hz6_Ou+implGN8c(Ot?tQ5Gz{7HsPz z(wjDlU9ycq84L?z?~H8PvEKLtKy92W(}9)ewW>cX58-c6=bgVo8VzDs4eiGz4Paxn00^8*~Gy*CMQzDD}H+EM`ZVPpUgd>S)`t37-GA&ReOcycG$Py={yd~^I(45=n9NSf*71I$7Y&y6fYTzG&Ie;a;iM2r+2MM5+$ z92n_WoEFJAYr`vAM{J~kMKcd#Crf*=n0(~Rx#$Krw+h4r=yj_se@o~Auc9Yn_q==1 zwCyG;ZA~6_?cr&qpxb46mQtQNy4{MnBdefSViU>bF_jfR9EPPbt_U92LZwS~$b zV~BAcXFP0$0!Yi|=se-8Dg{u`lH3qv?++~l>p?N}8|>8*2Mb24XFRby7>+k?e!{{u2d#YsmwX-e%Z2y3Fz!C zj^Fmp+x8hhq_bv?Ew!QL(rQQx#rN&`3UIjm(f-t4F|7|{5LYad43I7s08n$rc1azg zE3eLCEXq+~3n}AAVFK4t1Nyj3OJ>YUT`h`%ishIlGJ{&jA>dNRBxnGk+B`Gy*NjF2 z5mX)kYBZXe_GwB$iN&%9c<$kTY20_ zME-R&@kj;*U?2-l&jaPi!;t+=Wv^5POTLZ4~rlKM*OE|<|Ma~H#R|KcC!Z3+=kz223Eg|%q<;LD0(v_Ece=qtvn2QMPLDF zS85svC8EUs%G)fQh5tfYVe!%p>lma4;^4*P!Uv2z9&X!s89G1g>+F|hVDw9h9|PqR zmcpX36CqEM?pAj}kPO9NXtTPCZ(*OKac+ zoUP#MWV1d;k-i>pPq+Ob5a95z-0u5Bv9ek0>KdT1NqHJbbxF=N;~1hv-9XkrLeRmL zFVNf%Vu2taU_5RLAD5=A855=xKvj)Rnj0_;vg*2}($754D6P7cT_^V)4%O&@qX9Z*=vD;hvJQ45I3e+aGx{ayoIMSlTzfq z<_-{cl`d2W+Q;aA51g{(j6r!oo#ANVD+K(*wJs%qWR#pcKCq699+YG}2add;7O08< z1T&UolnmtnWn37!W^mZa2Sa=n1R$~idt4EYOlWGtk+8$tgf&(MYQh1{6%Zp^8&M0 z(jz@uuiaFk;Uzp2-{%PdBGQH*4xsk1iFad2;nUptl!#9$K(CzSmzE`~Thk3!cj6&Z zk=bzzqx-ahL$|p6nhtbFaMD`^Wmn8=9zHpw z!shh%TylUx`pNgPc%p;Q!k^24qXzm`>{0T70s8XOK8mmrOJf>p7leY$|dcatIcPJPHd7G3ChW zE$}cbY^lz)C6V%R;o<*s5v5y~n5bQm-B) zK5q-RpXPvvqasuU6hPPSq5SZ<5t*(ej|+n5-F*{)AT2aFcuxTgC?eJYQ1q38v67Ss z#|3;4nf?AY$L{}~{VU!vdk7c-yjn=)=yyT!#&VA(V83V>ZWIVDOmRJCSiniZWw>1- z0P=>CUoAm_e_ye@ig;wP093D#He0IL;b$4xegQ!Um;`V+wRAwzbc6vbO^pF|-JXT~ zgcw)x{!QgV1#18dHLw*Mk)3`B%)M5`7u+Lu7f-fTCqNVn{}t~%u zhz|?!)1G&9R{`w|<>Kpz2qy%bRLwV`$|ZX{=N?bmFt$#E;urvLua66wI4+RZu1YbX zlmG@1yn;ENto_MTQ6?;ZO9n0ob=*5O>C6rF9y=fdmVY@_OmS{G0hC>?w2>cmU!f5Q zP&p5-)p3(=t`M6+v95eV((l`<_0eM>H?HZj+G9!kxk&)V~`gRrXu=$#iZ?@Jrofbz1 zk-dQmz+Q4;y92f5T57MokPvShPZjW(KtbflUJLo|*F-`@{^A2#p zN!9ccjvBA_fs0GI3qn53o3h~Z!s!^W8ot`HvM~l8qXh#bN*05DB_VHoOVDo#{vz5a zrR|__4>Si8@xx)=Nn@71YU&m=6O zX06%nTeeEiv&`kM-t?>!!K0@ys+M4T9FB4~6pgP+!(Uc@#l>-AI5eqHj?SqcojBC$B( zMx=$0_*k*jE;oKyAZ{H#Sb&KLegi@*31Q^=LnlIXY`G;Nkfo~`UKaUdbw6;oZXvwf zT`q3!L;pmKnY(YprpYmO>scEhH2^)#2}C#q{9o|TAUXQBNc=HJa9R-)`G;qDKuxZ~ zSC<#}5CLCx?SMq*u1i5a@&P=^(BLPEUw!Ejl>)ove0whO2%M}DTqz90UD02Z2k(PK zz5Ed`e?E2JlvZqQJ8N%t57!$$9kB&8(H)is-n^X7mABjd0#`r zQ|qCcJ2{*WU8jYUWZ{Q{^!RMnmdA4=-rHLSq__8f@@wr6vc@zL_Jyq8t@)ZDPB)#D|?M*{=+*^sLn zMfS8s2$X~ta*zLIY-v~&L=6v)J-7CKf-gPR!~$qKc-wyaCIMONP0feyD0{_}$qPCnC;1%(>SN!f1JBokj?kxg`C^pRD zs*3oyOCg5Eh>@+>qmAHScCYW_X{5HnVsE=urLA_De_TM|cPYaVqNpbm8TWo(D3e`O;-QM~LiQ zl$EgrI77&5fF7#AS*<>C59WOD0H#mq{A?un%pV#ofjxBMiSYBMrt)itu6JB77^tmw z2Gq6ys>}ekIf-Hj3$nH~JA`rGJ>;$#uR-pBW*nJWK;QcD^wBbBv&=PR8-B1)Nd>}> z;_itESbm$>%Fjw>hID)>D~eB@zLWVhuZ*pWsvjaNS$6@t+F-ypV1heH8oxpV{Eb?PIkvrY2;e#lL3G}t5FX08+-p_05&*|-kRamG7AVlA~8YTX%(b{X|WY$^0djUj%4X- z3^=or2H-!pm5Mh$AOk`LRXVn)KQFu`L}Z0)1oZ;`e(ej)@2d=LZJhpReaofxbwXiB z@@J2n07tXtdIcB}5U_`>+?fd}TW4naMfK^pN&+1PmSLtRLmQxT`;p|%ewR)zfyloe zbj&Z>b1NqYVUd*+_?GLlnCQ84_d+GU0d9d>djAz_;UOG($cGW;VEFw|PS;01d)#*C zebRN@3-6_WoBEcml3+%eFWm8|lK+~$lS<>7ER(rMs*8H+%D@)Q$hF-!wJOkU_dKSw z$8eUi#^~8`sgsN7hv#uW?e%8*0%Z@o>%VEP4`rhGj5u>|SagqATH2)o;KVl_O7ySWVb?@st1k1J9W5|WQy&Xb zo|9jQ&_#yReqx{*jy!6+KPwFAn!Z3`t^4Nk;vG_I|MW&LM$5ZfwA+4VfwAcEO(0&D zHxCtDU$?8T$z0s$5_@+_dZU25Cpa2=FFfqQct+bFuYhCD6H?5R52QhRmJBCHOfLe- zL7zsmXHDFBna$%&xc`s^__WxA={FE{oQ~X?`xLI^%3|uiGbDkV4PWiB1rk zuk>AzM8MsL??)bYmnttrq|9OQg1`i46zfL$lv?s?w@i+F>{aCjZ!((UC(JdgX5pFs z5m(Z4M8_|59Omho$fkMcc#m`AlWV%#?xd2ai%1Qpg8G>$DbvJv?@8J-ELiWjANp|= zPJlf=LaYs8C}Nb}1!qAg`nRg!4Cl7`Qrbl3H`GAW_=$DH3&cGmdf zss+P{0}>((ifI=PCA7i68$jV1Ux|Eann~0Y-g^Qb7vSW>S`=;YyeBQj63&NA^;$n7><2T#A!K3lU`Uv1p<5Ln9t0^SolQKPB!|rs*)HH8V4g^rqR+Em zhf}pU$O*UJ=U#`p1}s^dTzIcjddSm`74jz*Wfb=%hay`{o_;iKux(;?EZOBt@0<9s zYE>>1?EACpGIqNuvrkf+QO&*ZC()%xbx|gTiZi9bG1F5Iu(fFs+V7K5r|wCSQW$!y zV@N^Qd=>)(K0cC4(<&C zp|L~Mb4yNeOdbArbTz*i`>LXDdV86%UHR({Jo?d8)3=bGiVG6cOBf}G0J=p$Y3I=% zMMR(mHTr=wN>9OyCPS%X_1@<$_Vd#-_=_tFJjY7t50&^wNSfs-5Q@9X1G{VYdjQAo z;Qbjxd5eKn1KEU1Gr>VUQeb;c+Zjf@(~*$X^+;1V?1{4P!%Rqc_Y=nS-XVn-{ObAh%1hOMbr~ zKvb`tly{squHs3);-|sJOI&yS8C>F^-{tm*J9&B%vPjrTW#IUNEAy+eadue)X0uU4 zrOqgBs6M;m%*Xa|Yg3pxTp#p36ZfA(;V27kO zEkZw@uY~Hrtnj%r(0GV5mk=a*=V?82s%PE6bGpuZxCY6>QXk9`(>ov&l;cWgFJw-? z;P%Swy*rDj(i3_-f97D!TDnPMJ#kQJQP!%oYBVz0^HMb3k2ZALVISS7s(w_w#gE$y zVU3_6QjbC4iE-mhJHdV9o@y*UXw^aI_PDJ&)WiJrVmOW4dEHEi&bVxgvO<&@B}$7q zr0_vd8xa82A;8sT1!!7U+GNDJKYm0!lq7kyFOEnLTRPMXTIZA=rB-&5W0D}!5IBJRq>v*$!l&1|k;PCz^)567fx?z5Z zC{GU?D}XmtThjj(&Z!50ZqUqs;H1*=MyEWb@Ykg zAvh{%J0qeDMVKgdSPH1H6JXvSiE^w?%ey?IqESK{(%Uuj3zNN|yg?lP$^d0$w+Zk#P@?YO3c zKal~m=@KHwvL1_2ds)v^n&QL`7ZYuDyDUlB)A#lc8=8-L@v?Qsj_Mpci_K5}d4d}bMn(vh>9HDVezdZuovP8Xa!NF`$v6HMufK4D ziv~s(L#6&~|4Oaugo2WmpC}mN)N9NOph5${~_1hZa`BUSCftV4G${%HYJ>9-I_lp1bfm>33?@0$Eac%0e&F7)AlLPz+zH zv3?0tGVTAPPY)FWyiOQWQ*-De0ey|WgvrbeG?ge+wuyTmi~LLn)H-^0!z;!xI=7%p z#4L3o4Qn(4c62yRHC+RBm18~X@SBF^5joptlrtvWAtQZ2aRj8G=3w1=DXXQSUKHA( zo#-64`*ubd+de1EnT}b0ukGp@-9iDI@Dg^xI+66`MPPH}5^j$1N9{eIs)qRCf+1Xm!3N%+uj|lwxUQyV_PzM5k;1 zgy43_AcK~Uec$)ZC#zw11mJAa`OJx^D=T&zG@#7k=^NDBFBRpmb7RWjlatOH%Azg9 zs8Bj%fOk7RGpS|aUnjjPBL1xs0XZDWHwa47?yu5&*=xZtDUd~ zmm6!z^5p7rymQ5DTjqR9z56sgCfF;?`o$@Ij z;^JXGT@Xt5y!x8*-ZYE{0gCnuku`n3h(Zz zIu*D>^dKk_oWP4w`wU;IduF*eD%;i0YUQ-Uzu(J4Nt#mc7)PKSh)71W20P{DLqnD5 zXyoKvM5G6FcQ!gdrF}C4UA;@!(}c=n&3?quuze`nd9Y#cp4)zwg7q~=K0Fq0V9`JC zun5=wSN{Ue6rC!{blqhm({Mw#*@O9r0xp>bV0}IpPvmiW=2LsG4N`lJOqN+{tSrdI zMJ4?E4cdKkB4NaAq>g)t*OVA({%IP`G=H-|r@?Ikd3#!+*(5{E5@~AM`!P-;S#pzCYVLO?O%BkSO5Wj(5}_*q$!+VSkq-Y*g7jHodbhre1xFEYyaDAa&L zgSTs~sLp;>e{&T)yGKG3IiJd8nNu2@2;xu_a1x`d_%_3cmaW3ylSE5v9Ht!G)_GRH zIv+`jKi-hFYB!@h{&aI==$?cvW#NM%11<$ ztF%Hm7@Ske#{TMb--$Ciu)ndp+Ms4hW-=p6g_G@&R+dj&QCr>g1z3%8YdAz0JxoBD z(ZFrj#Cjjbh9 z7Nn7~vkSeNyt7|rl~;CF%8LEG>-sS8!Xk^$Q=rRnfqb+C(L z>3y7!o>#IzYdNEwD4%4I0f5b_f3D^HZ<#i*zu!VR8ap|e+nD|%*XB}7({aBI*@v&! zAYwzYSyS^1t&oYJMFPziD}t|}*arX>x&f6aj7v||39he;JI&}QOhtTP@{;B|}xvkz7!`DeE zU1;W`PE9It$MF0R58+b(J9dlMKXqJn?-SG$Ap&}$_oo}}LbY`GoUcbXYB+m$;6C;O z!GfqFCG}I4EVi&;_PN6Ki193=zVI}0u!T<}yF%zF=sCch0DtYt_zAtdxn_xdVQJWH z7ccq!WgS0fd++63gQIdX9fbkG`*84u)u4;gZ)}n*lmz>l;sWdO0CN!P8xXOc(((Pk z{h%2~XlM*RTFvZ?<*{yL?eiPW4(>0^OQVm|H>o4DD})#{8Q1z+B@2w)Sd(3Y7@lpY zUac3e+f(@uQ@re4j-Fxksg0E3gzXVz!@k{sqpxA=3oR{rA>RNw@@=BMF_i=iz7~HA&@t<_+#b%w9F`!xQGoK5JJvJBJ0`8~C9Prt{cOlzU8G6ohrNAYh&7 z2$WIuvyWSJeX0;Qq5^4nj2E59(^W(a6=IAhaNlXF-SzrfygpQK$Nv84%47H7XNb3J zXq-qW{&bbikpGyE?{QSf)bW)AVndz)k;h9D1qYI}l9(>0SvJox1m8QX=H^MN0edkJ z)l50e&AKn21m8OjNS*FVK%_osCM}j4z7VfOD0oDWpCAd^QhVregd1fENKzuYa)A@i zbJVnx9u{*}4o+k;GK^ z;OM(JJfDy?jYp53TBN%(-$33=0po@07xSpM6}`=Ttv{Igeq$!G?**PXuH`2@t&X0# zn##DtUBasYN&I9LLk{LT9Imh;3ju8wUKYv7$N`4I80X1WxoG}Xz*Q@yI>(6&rD>#R8jC?c`<{9{qDA`U1{1SZlOL-o&V?AkcBpO$yN{)L}b1SR9?)@$;`IhL}Msk z5|Io4@uJU#yE>TmcYOFp=!h?;Y@h?i(ThaCz zV&Pl$8@a_^uRk)We`SjyK2~d{dS>vRv?R~G4}L>_rH*Kg+{NbucUyYXjXVnGxb3L< zX~&E28A(>z)ut*H!fD4Ml-n+&T?04X8-!TMs`(IAY$VOy%80k~#^E7uRqf2w%5C^u za1Qd7@wW7rqb8}*#%LuNovZQXj<~3iIB_C#rBRYNtvTG-P|A-Y4nh<{p7@mT+3RpG zF=l0j@C8!vcvwPJ3EkujVcD=8aW1FdxfbbZMTxKw!@5x4j1^R|eV4i;f2ARY$H4$r zYfu(OEJ5;5L60hjkXKz?pq1FJiEk}xmG99h6nbr?vpO&2FAL^hDt4&D=vHNfFMNHq zY7;h#ZY`3!mt|S6mB386lLo=fWw96h378q9ah&UH#><*qladu>KP@>IM7Y|*h_%_8 zH0QxfKLf>$qY@vCw*nze$`qtCm<-k{+l)yUl9*A#tP-NEPjtUd12M!GI~-&rYnZ_` zKNl&GZ0|}i+6Z(dH#e+nwZV?42~pr6ls+alsN`^O1sR5Q7D7zoDs9c)=$s`uZ!&qk z$VYx%N(s9dF_|QXqm1hWR4`n0WT08QT%8{k z(}4RTOfZIW13>R}craejZ!2O955LS?oAW2Z6m0xnaEuW|su1Q;H}piAPF%)@q-tU5 zxo)W}q^Ny_fr;QjYp6p%e#R-zuS+941=4*U)95+BO~tM=!-|5!7g-=Tt^Or3)U)^Mjy}@X z@2AXJgOv{(p<3^IW2#dWgkz+rG*nwDlkHW{E!^1qF7DRrX4HNq-vas7Yi06z?>~~j z4VObx@_wg+SN%>R`TOr{|43FhcGCZoK^(0lQ=3GOy8TK8*%930wFs~^)iPtYkDA0V zn^X%(~wI7&ey8)fbNji>KX^dWTw};iv<|03!Bw5 z?$lyy+jtm|b>d9*!x47+Px`FEgyIJiq@uJgaMizGn*w{C#;+rgC)Q4*k$kQJq~ z*0(V?F?Re1p+L3bqz%Yd*sfzLl`V{oEXpJqgya{nQwaH$P66P+y6}WvI&6yA3dtmE z&&FFf6RX4JbKWNth;*$XS?)=)dBDaR7Y^5Ly}PaM(78uFH25}P=mnLz8R0a`rNC%u zK8`5*_e+QtPSQT!4P=12@925q0!Jrf9qPG=<2-g&Xv?qTK`rErKO(^Hc!#kWj z*ODWl0NJ#puEr25>C3uQvBA5IQk+H!O;d?74&EI?cpK_xaTzJ&Dli0=X!Z4k`ynkh zyTO&+CcLa+*!`U`cLBlV_CYp7haEVAKJc~TtPur@tWc}4FO z$?VMQ&!Our6#CV`=g($Py?cUF$@jMTpBR6dHC~bu4E1-j?)=W!l9vJkMg{ym8N~0l z{TKNsHN-!){x#nCCuzeU#)rS;+wbCk5;gn{|HD5)|BmnBFVM380{R#G!@u9qUs?db zAO45GWaoGBKTZ3;*dYEM><{z7UxNL+J^vHzU)dr49_LT4-Cv^kH#q-?zv1su{@t4J z|AO+b>=A#D^Y7O5{}-G;#18*2YyR*_{3Vb71?At_CH~%?f4?t=-<|9~TlrtvCjJl3 zKYQiB<~sgKSNu!Nf8+ecKJlL&?>{pk|I&l_9ZdgSUgSSf{#pCKM(2N4g8U^iME|xf z|n)f%a%l}N3|Ecw_!@-}i^e<`R|C^Cj WUJ4BS&t-_eH`d>SGmPM$qyG=h$Rxr5 diff --git a/plugins/channelmimo/beamsteeringcwmod/beamsteeringcwmodbaseband.cpp b/plugins/channelmimo/beamsteeringcwmod/beamsteeringcwmodbaseband.cpp new file mode 100644 index 000000000..c5931ec68 --- /dev/null +++ b/plugins/channelmimo/beamsteeringcwmod/beamsteeringcwmodbaseband.cpp @@ -0,0 +1,247 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspcommands.h" + +#include "beamsteeringcwmodbaseband.h" + + +MESSAGE_CLASS_DEFINITION(BeamSteeringCWModBaseband::MsgConfigureBeamSteeringCWModBaseband, Message) +MESSAGE_CLASS_DEFINITION(BeamSteeringCWModBaseband::MsgSignalNotification, Message) + +BeamSteeringCWModBaseband::BeamSteeringCWModBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleMOFifo.init(2, SampleMOFifo::getSizePolicy(48000)); + m_vbegin.resize(2); + + for (int i = 0; i < 2; i++) + { + m_streamSources[i].setStreamIndex(i); + m_channelizers[i] = new UpSampleChannelizer(&m_streamSources[i]); + m_sizes[i] = 0; + } + + QObject::connect( + &m_sampleMOFifo, + &SampleMOFifo::dataSyncRead, + this, + &BeamSteeringCWModBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_lastStream = 0; +} + +BeamSteeringCWModBaseband::~BeamSteeringCWModBaseband() +{ + for (int i = 0; i < 2; i++) { + delete m_channelizers[i]; + } +} + +void BeamSteeringCWModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleMOFifo.reset(); + + for (int i = 0; i < 2; i++) + { + m_streamSources[i].reset(); + m_sizes[i] = 0; + } +} + +void BeamSteeringCWModBaseband::pull(SampleVector::iterator& begin, unsigned int nbSamples, unsigned int streamIndex) +{ + if (streamIndex > 1) { + return; + } + + if (streamIndex == m_lastStream) { + qWarning("BeamSteeringCWModBaseband::pull: twice same stream in a row: %u", streamIndex); + } + + m_lastStream = streamIndex; + m_vbegin[streamIndex] = begin; + m_sizes[streamIndex] = nbSamples; + + if (streamIndex == 1) + { + unsigned int part1Begin, part1End, part2Begin, part2End, size; + + if (m_sizes[0] != m_sizes[1]) + { + qWarning("BeamSteeringCWModBaseband::pull: unequal sizes: [0]: %d [1]: %d", m_sizes[0], m_sizes[1]); + size = std::min(m_sizes[0], m_sizes[1]); + } + else + { + size = m_sizes[0]; + } + + std::vector& data = m_sampleMOFifo.getData(); + m_sampleMOFifo.readSync(size, part1Begin, part1End, part2Begin, part2End); + + if (part1Begin != part1End) + { + std::copy(data[0].begin() + part1Begin, data[0].begin() + part1End, m_vbegin[0]); + std::copy(data[1].begin() + part1Begin, data[1].begin() + part1End, m_vbegin[1]); + } + + if (part2Begin != part2End) + { + std::copy(data[0].begin() + part2Begin, data[0].begin() + part2End, m_vbegin[0]); + std::copy(data[1].begin() + part2Begin, data[1].begin() + part2End, m_vbegin[1]); + } + } +} + +void BeamSteeringCWModBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + + std::vector& data = m_sampleMOFifo.getData(); + + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + unsigned int remainder = m_sampleMOFifo.remainderSync(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleMOFifo.writeSync(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleMOFifo.remainderSync(); + } +} + +void BeamSteeringCWModBaseband::processFifo(std::vector& data, unsigned int ibegin, unsigned int iend) +{ + for (unsigned int stream = 0; stream < 2; stream++) { + m_channelizers[stream]->pull(data[stream].begin() + ibegin, iend - ibegin); + } +} + +void BeamSteeringCWModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool BeamSteeringCWModBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureBeamSteeringCWModBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureBeamSteeringCWModBaseband& cfg = (MsgConfigureBeamSteeringCWModBaseband&) cmd; + qDebug() << "BeamSteeringCWModBaseband::handleMessage: MsgConfigureBeamSteeringCWModBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgSignalNotification& cfg = (MsgSignalNotification&) cmd; + int basebandSampleRate = cfg.getBasebandSampleRate(); + + qDebug() << "BeamSteeringCWModBaseband::handleMessage: MsgSignalNotification:" + << " basebandSampleRate: " << basebandSampleRate; + + m_sampleMOFifo.resize(SampleMOFifo::getSizePolicy(basebandSampleRate)); + + for (int i = 0; i < 2; i++) + { + m_channelizers[i]->setBasebandSampleRate(basebandSampleRate, true); + m_streamSources[i].reset(); + } + + return true; + } + else + { + qDebug("BeamSteeringCWModBaseband::handleMessage: unhandled: %s", cmd.getIdentifier()); + return false; + } +} + +void BeamSteeringCWModBaseband::applySettings(const BeamSteeringCWModSettings& settings, bool force) +{ + if ((m_settings.m_filterChainHash != settings.m_filterChainHash) || (m_settings.m_log2Interp != settings.m_log2Interp) || force) + { + for (int i = 0; i < 2; i++) + { + m_channelizers[i]->setInterpolation(settings.m_log2Interp, settings.m_filterChainHash); + m_streamSources[i].reset(); + } + } + + if ((m_settings.m_steerDegrees != settings.m_steerDegrees) || force) + { + float steeringAngle = settings.m_steerDegrees / 180.0f; + steeringAngle = steeringAngle < -M_PI ? -M_PI : steeringAngle > M_PI ? M_PI : steeringAngle; + m_streamSources[1].setPhase(M_PI*cos(steeringAngle)); + } + + if ((m_settings.m_channelOutput != settings.m_channelOutput) || force) + { + if (settings.m_channelOutput == 0) + { + m_streamSources[0].muteChannel(false); + m_streamSources[1].muteChannel(false); + } + else if (settings.m_channelOutput == 1) + { + m_streamSources[0].muteChannel(false); + m_streamSources[1].muteChannel(true); + } + else if (settings.m_channelOutput == 2) + { + m_streamSources[0].muteChannel(true); + m_streamSources[1].muteChannel(false); + } + else + { + m_streamSources[0].muteChannel(false); + m_streamSources[1].muteChannel(false); + } + } + + m_settings = settings; +} \ No newline at end of file diff --git a/plugins/channeltx/filesource/CMakeLists.txt b/plugins/channeltx/filesource/CMakeLists.txt index 746cad56d..7268eab2c 100644 --- a/plugins/channeltx/filesource/CMakeLists.txt +++ b/plugins/channeltx/filesource/CMakeLists.txt @@ -10,7 +10,10 @@ else() endif() set(filesource_SOURCES - filesource.cpp + filesource.cpp + filesourcebaseband.cpp + filesourcereport.cpp + filesourcesource.cpp filesourceplugin.cpp filesourcesettings.cpp filesourcewebapiadapter.cpp @@ -18,6 +21,9 @@ set(filesource_SOURCES set(filesource_HEADERS filesource.h + filesourcebaseband.h + filesourcereport.h + filesourcesource.h filesourceplugin.h filesourcesettings.h filesourcewebapiadapter.h diff --git a/plugins/channeltx/filesource/filesource.cpp b/plugins/channeltx/filesource/filesource.cpp index d6c972742..e2a88bc0b 100644 --- a/plugins/channeltx/filesource/filesource.cpp +++ b/plugins/channeltx/filesource/filesource.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include "SWGChannelSettings.h" #include "SWGChannelReport.h" @@ -37,13 +38,12 @@ #include "device/deviceapi.h" #include "dsp/dspcommands.h" #include "dsp/devicesamplesink.h" -#include "dsp/upchannelizer.h" -#include "dsp/threadedbasebandsamplesource.h" #include "dsp/hbfilterchainconverter.h" #include "dsp/filerecord.h" #include "util/db.h" -MESSAGE_CLASS_DEFINITION(FileSource::MsgConfigureChannelizer, Message) +#include "filesourcebaseband.h" + MESSAGE_CLASS_DEFINITION(FileSource::MsgSampleRateNotification, Message) MESSAGE_CLASS_DEFINITION(FileSource::MsgConfigureFileSource, Message) MESSAGE_CLASS_DEFINITION(FileSource::MsgConfigureFileSourceName, Message) @@ -51,10 +51,6 @@ MESSAGE_CLASS_DEFINITION(FileSource::MsgConfigureFileSourceWork, Message) MESSAGE_CLASS_DEFINITION(FileSource::MsgConfigureFileSourceStreamTiming, Message) MESSAGE_CLASS_DEFINITION(FileSource::MsgConfigureFileSourceSeek, Message) MESSAGE_CLASS_DEFINITION(FileSource::MsgReportFileSourceAcquisition, Message) -MESSAGE_CLASS_DEFINITION(FileSource::MsgPlayPause, Message) -MESSAGE_CLASS_DEFINITION(FileSource::MsgReportFileSourceStreamData, Message) -MESSAGE_CLASS_DEFINITION(FileSource::MsgReportFileSourceStreamTiming, Message) -MESSAGE_CLASS_DEFINITION(FileSource::MsgReportHeaderCRC, Message) const QString FileSource::m_channelIdURI = "sdrangel.channeltx.filesource"; const QString FileSource::m_channelId ="FileSource"; @@ -62,33 +58,24 @@ const QString FileSource::m_channelId ="FileSource"; FileSource::FileSource(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), m_deviceAPI(deviceAPI), - m_fileName("..."), - m_sampleSize(0), - m_centerFrequency(0), - m_frequencyOffset(0), - m_fileSampleRate(0), - m_samplesCount(0), - m_sampleRate(0), - m_deviceSampleRate(0), - m_recordLength(0), - m_startingTimeStamp(0), - m_running(false) + m_settingsMutex(QMutex::Recursive), + m_frequencyOffset(0), + m_basebandSampleRate(0), + m_linearGain(0.0) { setObjectName(m_channelId); - m_channelizer = new UpChannelizer(this); - m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this); - m_deviceAPI->addChannelSource(m_threadedChannelizer); + m_thread = new QThread(this); + m_basebandSource = new FileSourceBaseband(); + m_basebandSource->moveToThread(m_thread); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSource(this); m_deviceAPI->addChannelSourceAPI(this); m_networkManager = new QNetworkAccessManager(); connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); - - m_linearGain = 1.0f; - m_magsq = 0.0f; - m_magsqSum = 0.0f; - m_magsqPeak = 0.0f; - m_magsqCount = 0; } FileSource::~FileSource() @@ -96,145 +83,33 @@ FileSource::~FileSource() disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); delete m_networkManager; m_deviceAPI->removeChannelSourceAPI(this); - m_deviceAPI->removeChannelSource(m_threadedChannelizer); - delete m_threadedChannelizer; - delete m_channelizer; -} - -void FileSource::pull(Sample& sample) -{ - Real re; - Real im; - - struct Sample16 - { - int16_t real; - int16_t imag; - }; - - struct Sample24 - { - int32_t real; - int32_t imag; - }; - - if (!m_running) - { - re = 0; - im = 0; - } - else if (m_sampleSize == 16) - { - Sample16 sample16; - m_ifstream.read(reinterpret_cast(&sample16), sizeof(Sample16)); - - if (m_ifstream.eof()) { - handleEOF(); - } else { - m_samplesCount++; - } - - // scale to +/-1.0 - re = (sample16.real * m_linearGain) / 32760.0f; - im = (sample16.imag * m_linearGain) / 32760.0f; - } - else if (m_sampleSize == 24) - { - Sample24 sample24; - m_ifstream.read(reinterpret_cast(&sample24), sizeof(Sample24)); - - if (m_ifstream.eof()) { - handleEOF(); - } else { - m_samplesCount++; - } - - // scale to +/-1.0 - re = (sample24.real * m_linearGain) / 8388608.0f; - im = (sample24.imag * m_linearGain) / 8388608.0f; - } - else - { - re = 0; - im = 0; - } - - - if (SDR_TX_SAMP_SZ == 16) - { - sample.setReal(re * 32768.0f); - sample.setImag(im * 32768.0f); - } - else if (SDR_TX_SAMP_SZ == 24) - { - sample.setReal(re * 8388608.0f); - sample.setImag(im * 8388608.0f); - } - else - { - sample.setReal(0); - sample.setImag(0); - } - - Real magsq = re*re + im*im; - m_movingAverage(magsq); - m_magsq = m_movingAverage.asDouble(); - m_magsqSum += magsq; - - if (magsq > m_magsqPeak) { - m_magsqPeak = magsq; - } - - m_magsqCount++; -} - -void FileSource::pullAudio(int nbSamples) -{ - (void) nbSamples; + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; } void FileSource::start() { - qDebug("FileSource::start"); - m_running = true; - - if (getMessageQueueToGUI()) - { - MsgReportFileSourceAcquisition *report = MsgReportFileSourceAcquisition::create(true); // acquisition on - getMessageQueueToGUI()->push(report); - } + qDebug("FileSource::start"); + m_basebandSource->reset(); + m_thread->start(); } void FileSource::stop() { qDebug("FileSource::stop"); - m_running = false; + m_thread->exit(); + m_thread->wait(); +} - if (getMessageQueueToGUI()) - { - MsgReportFileSourceAcquisition *report = MsgReportFileSourceAcquisition::create(false); // acquisition off - getMessageQueueToGUI()->push(report); - } +void FileSource::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); } bool FileSource::handleMessage(const Message& cmd) { - if (UpChannelizer::MsgChannelizerNotification::match(cmd)) - { - UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd; - int sampleRate = notif.getSampleRate(); - - qDebug() << "FileSource::handleMessage: MsgChannelizerNotification:" - << " channelSampleRate: " << sampleRate - << " offsetFrequency: " << notif.getFrequencyOffset(); - - if (sampleRate > 0) { - setSampleRate(sampleRate); - } - - return true; - } - else if (DSPSignalNotification::match(cmd)) + if (DSPSignalNotification::match(cmd)) { DSPSignalNotification& notif = (DSPSignalNotification&) cmd; @@ -242,17 +117,18 @@ bool FileSource::handleMessage(const Message& cmd) << " inputSampleRate: " << notif.getSampleRate() << " centerFrequency: " << notif.getCenterFrequency(); - setCenterFrequency(notif.getCenterFrequency()); - m_deviceSampleRate = notif.getSampleRate(); + m_basebandSampleRate = notif.getSampleRate(); calculateFrequencyOffset(); // This is when device sample rate changes + setCenterFrequency(notif.getCenterFrequency()); - // Redo the channelizer stuff with the new sample rate to re-synchronize everything - m_channelizer->set(m_channelizer->getInputMessageQueue(), - m_settings.m_log2Interp, - m_settings.m_filterChainHash); + // Notify source of input sample rate change + qDebug() << "FileSource::handleMessage: DSPSignalNotification: push to source"; + DSPSignalNotification *sig = new DSPSignalNotification(notif); + m_basebandSource->getInputMessageQueue()->push(sig); if (m_guiMessageQueue) { + qDebug() << "FileSource::handleMessage: DSPSignalNotification: push to GUI"; MsgSampleRateNotification *msg = MsgSampleRateNotification::create(notif.getSampleRate()); m_guiMessageQueue->push(msg); } @@ -266,58 +142,37 @@ bool FileSource::handleMessage(const Message& cmd) applySettings(cfg.getSettings(), cfg.getForce()); return true; } - else if (MsgConfigureChannelizer::match(cmd)) - { - MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; - m_settings.m_log2Interp = cfg.getLog2Interp(); - m_settings.m_filterChainHash = cfg.getFilterChainHash(); - - qDebug() << "FileSource::handleMessage: MsgConfigureChannelizer:" - << " log2Interp: " << m_settings.m_log2Interp - << " filterChainHash: " << m_settings.m_filterChainHash; - - m_channelizer->set(m_channelizer->getInputMessageQueue(), - m_settings.m_log2Interp, - m_settings.m_filterChainHash); - - calculateFrequencyOffset(); // This is when decimation or filter chain changes - - return true; - } else if (MsgConfigureFileSourceName::match(cmd)) { MsgConfigureFileSourceName& conf = (MsgConfigureFileSourceName&) cmd; - m_fileName = conf.getFileName(); - openFileStream(); + qDebug() << "FileSource::handleMessage: MsgConfigureFileSourceName:" << conf.getFileName(); + FileSourceBaseband::MsgConfigureFileSourceName *msg = FileSourceBaseband::MsgConfigureFileSourceName::create(conf.getFileName()); + m_basebandSource->getInputMessageQueue()->push(msg); + return true; } else if (MsgConfigureFileSourceWork::match(cmd)) { MsgConfigureFileSourceWork& conf = (MsgConfigureFileSourceWork&) cmd; - - if (conf.isWorking()) { - start(); - } else { - stop(); - } + FileSourceBaseband::MsgConfigureFileSourceWork *msg = FileSourceBaseband::MsgConfigureFileSourceWork::create(conf.isWorking()); + m_basebandSource->getInputMessageQueue()->push(msg); return true; } else if (MsgConfigureFileSourceSeek::match(cmd)) { MsgConfigureFileSourceSeek& conf = (MsgConfigureFileSourceSeek&) cmd; - int seekMillis = conf.getMillis(); - seekFileStream(seekMillis); + FileSourceBaseband::MsgConfigureFileSourceSeek *msg = FileSourceBaseband::MsgConfigureFileSourceSeek::create(conf.getMillis()); + m_basebandSource->getInputMessageQueue()->push(msg); return true; } else if (MsgConfigureFileSourceStreamTiming::match(cmd)) { - MsgReportFileSourceStreamTiming *report; - if (getMessageQueueToGUI()) { - report = MsgReportFileSourceStreamTiming::create(getSamplesCount()); + FileSourceReport::MsgReportFileSourceStreamTiming *report = + FileSourceReport::MsgReportFileSourceStreamTiming::create(m_basebandSource->getSamplesCount()); getMessageQueueToGUI()->push(report); } @@ -352,120 +207,22 @@ bool FileSource::deserialize(const QByteArray& data) } } -void FileSource::openFileStream() -{ - //stop(); - - if (m_ifstream.is_open()) { - m_ifstream.close(); - } - -#ifdef Q_OS_WIN - m_ifstream.open(m_fileName.toStdWString().c_str(), std::ios::binary | std::ios::ate); -#else - m_ifstream.open(m_fileName.toStdString().c_str(), std::ios::binary | std::ios::ate); -#endif - quint64 fileSize = m_ifstream.tellg(); - m_samplesCount = 0; - - if (fileSize > sizeof(FileRecord::Header)) - { - FileRecord::Header header; - m_ifstream.seekg(0,std::ios_base::beg); - bool crcOK = FileRecord::readHeader(m_ifstream, header); - m_fileSampleRate = header.sampleRate; - m_centerFrequency = header.centerFrequency; - m_startingTimeStamp = header.startTimeStamp; - m_sampleSize = header.sampleSize; - QString crcHex = QString("%1").arg(header.crc32 , 0, 16); - - if (crcOK) - { - qDebug("FileSource::openFileStream: CRC32 OK for header: %s", qPrintable(crcHex)); - m_recordLength = (fileSize - sizeof(FileRecord::Header)) / ((m_sampleSize == 24 ? 8 : 4) * m_fileSampleRate); - } - else - { - qCritical("FileSource::openFileStream: bad CRC32 for header: %s", qPrintable(crcHex)); - m_recordLength = 0; - } - - if (getMessageQueueToGUI()) { - MsgReportHeaderCRC *report = MsgReportHeaderCRC::create(crcOK); - getMessageQueueToGUI()->push(report); - } - } - else - { - m_recordLength = 0; - } - - qDebug() << "FileSource::openFileStream: " << m_fileName.toStdString().c_str() - << " fileSize: " << fileSize << " bytes" - << " length: " << m_recordLength << " seconds" - << " sample rate: " << m_fileSampleRate << " S/s" - << " center frequency: " << m_centerFrequency << " Hz" - << " sample size: " << m_sampleSize << " bits" - << " starting TS: " << m_startingTimeStamp << "s"; - - if (getMessageQueueToGUI()) { - MsgReportFileSourceStreamData *report = MsgReportFileSourceStreamData::create(m_fileSampleRate, - m_sampleSize, - m_centerFrequency, - m_startingTimeStamp, - m_recordLength); // file stream data - getMessageQueueToGUI()->push(report); - } - - if (m_recordLength == 0) { - m_ifstream.close(); - } -} - -void FileSource::seekFileStream(int seekMillis) -{ - QMutexLocker mutexLocker(&m_mutex); - - if ((m_ifstream.is_open()) && !m_running) - { - quint64 seekPoint = ((m_recordLength * seekMillis) / 1000) * m_fileSampleRate; - m_samplesCount = seekPoint; - seekPoint *= (m_sampleSize == 24 ? 8 : 4); // + sizeof(FileSink::Header) - m_ifstream.clear(); - m_ifstream.seekg(seekPoint + sizeof(FileRecord::Header), std::ios::beg); - } -} - -void FileSource::handleEOF() -{ - if (getMessageQueueToGUI()) - { - MsgReportFileSourceStreamTiming *report = MsgReportFileSourceStreamTiming::create(getSamplesCount()); - getMessageQueueToGUI()->push(report); - } - - if (m_settings.m_loop) - { - stop(); - seekFileStream(0); - start(); - } - else - { - stop(); - - if (getMessageQueueToGUI()) - { - MsgPlayPause *report = MsgPlayPause::create(false); - getMessageQueueToGUI()->push(report); - } - } -} - void FileSource::applySettings(const FileSourceSettings& settings, bool force) { qDebug() << "FileSource::applySettings:" - << " force: " << force; + << "m_fileName:" << settings.m_fileName + << "m_loop:" << settings.m_loop + << "m_gainDB:" << settings.m_gainDB + << "m_log2Interp:" << settings.m_log2Interp + << "m_filterChainHash:" << settings.m_filterChainHash + << "m_useReverseAPI:" << settings.m_useReverseAPI + << "m_reverseAPIAddress:" << settings.m_reverseAPIAddress + << "m_reverseAPIChannelIndex:" << settings.m_reverseAPIChannelIndex + << "m_reverseAPIDeviceIndex:" << settings.m_reverseAPIDeviceIndex + << "m_reverseAPIPort:" << settings.m_reverseAPIPort + << "m_rgbColor:" << settings.m_rgbColor + << "m_title:" << settings.m_title + << " force: " << force; QList reverseAPIKeys; @@ -475,13 +232,15 @@ void FileSource::applySettings(const FileSourceSettings& settings, bool force) if ((m_settings.m_fileName != settings.m_fileName) || force) { reverseAPIKeys.append("fileName"); } - if ((m_settings.m_gainDB != settings.m_gainDB) || force) { m_linearGain = CalcDb::powerFromdB(settings.m_gainDB); reverseAPIKeys.append("gainDB"); } + FileSourceBaseband::MsgConfigureFileSourceBaseband *msg = FileSourceBaseband::MsgConfigureFileSourceBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); + if (settings.m_useReverseAPI) { bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || @@ -509,7 +268,7 @@ void FileSource::validateFilterChainHash(FileSourceSettings& settings) void FileSource::calculateFrequencyOffset() { double shiftFactor = HBFilterChainConverter::getShiftFactor(m_settings.m_log2Interp, m_settings.m_filterChainHash); - m_frequencyOffset = m_deviceSampleRate * shiftFactor; + m_frequencyOffset = m_basebandSampleRate * shiftFactor; } int FileSource::webapiSettingsGet( @@ -630,12 +389,16 @@ void FileSource::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& respon { qint64 t_sec = 0; qint64 t_msec = 0; - quint64 samplesCount = getSamplesCount(); + quint64 samplesCount = m_basebandSource->getSamplesCount(); + uint32_t fileSampleRate = m_basebandSource->getFileSampleRate(); + quint64 startingTimeStamp = m_basebandSource->getStartingTimeStamp(); + quint64 fileRecordLength = m_basebandSource->getRecordLength(); + quint32 fileSampleSize = m_basebandSource->getFileSampleSize(); - if (m_fileSampleRate > 0) + if (fileSampleRate > 0) { - t_sec = samplesCount / m_fileSampleRate; - t_msec = (samplesCount - (t_sec * m_fileSampleRate)) * 1000 / m_fileSampleRate; + t_sec = samplesCount / fileSampleRate; + t_msec = (samplesCount - (t_sec * fileSampleRate)) * 1000 / fileSampleRate; } QTime t(0, 0, 0, 0); @@ -643,20 +406,20 @@ void FileSource::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& respon t = t.addMSecs(t_msec); response.getFileSourceReport()->setElapsedTime(new QString(t.toString("HH:mm:ss.zzz"))); - qint64 startingTimeStampMsec = m_startingTimeStamp * 1000LL; + qint64 startingTimeStampMsec = startingTimeStamp * 1000LL; QDateTime dt = QDateTime::fromMSecsSinceEpoch(startingTimeStampMsec); dt = dt.addSecs(t_sec); dt = dt.addMSecs(t_msec); response.getFileSourceReport()->setAbsoluteTime(new QString(dt.toString("yyyy-MM-dd HH:mm:ss.zzz"))); QTime recordLength(0, 0, 0, 0); - recordLength = recordLength.addSecs(m_recordLength); + recordLength = recordLength.addSecs(fileRecordLength); response.getFileSourceReport()->setDurationTime(new QString(recordLength.toString("HH:mm:ss"))); response.getFileSourceReport()->setFileName(new QString(m_settings.m_fileName)); - response.getFileSourceReport()->setFileSampleRate(m_fileSampleRate); - response.getFileSourceReport()->setFileSampleSize(m_sampleSize); - response.getFileSourceReport()->setSampleRate(m_sampleRate); + response.getFileSourceReport()->setFileSampleRate(fileSampleRate); + response.getFileSourceReport()->setFileSampleSize(fileSampleSize); + response.getFileSourceReport()->setSampleRate(m_basebandSampleRate); response.getFileSourceReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); } @@ -696,13 +459,14 @@ void FileSource::webapiReverseSendSettings(QList& channelSettingsKeys, m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -717,10 +481,23 @@ void FileSource::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("FileSource::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("FileSource::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } + +void FileSource::getMagSqLevels(double& avg, double& peak, int& nbSamples) const +{ + m_basebandSource->getMagSqLevels(avg, peak, nbSamples); +} + +void FileSource::propagateMessageQueueToGUI() +{ + m_basebandSource->setMessageQueueToGUI(getMessageQueueToGUI()); +} \ No newline at end of file diff --git a/plugins/channeltx/filesource/filesource.h b/plugins/channeltx/filesource/filesource.h index 70817aab6..2e6edc469 100644 --- a/plugins/channeltx/filesource/filesource.h +++ b/plugins/channeltx/filesource/filesource.h @@ -33,40 +33,18 @@ #include "util/message.h" #include "util/movingaverage.h" #include "filesourcesettings.h" +#include "filesourcereport.h" -class ThreadedBasebandSampleSource; -class UpChannelizer; -class DeviceAPI; -class FileSourceThread; class QNetworkAccessManager; class QNetworkReply; +class DeviceAPI; +class FileSourceBaseband; + class FileSource : public BasebandSampleSource, public ChannelAPI { Q_OBJECT public: - class MsgConfigureChannelizer : public Message { - MESSAGE_CLASS_DECLARATION - - public: - int getLog2Interp() const { return m_log2Interp; } - int getFilterChainHash() const { return m_filterChainHash; } - - static MsgConfigureChannelizer* create(unsigned int m_log2Interp, unsigned int m_filterChainHash) { - return new MsgConfigureChannelizer(m_log2Interp, m_filterChainHash); - } - - private: - unsigned int m_log2Interp; - unsigned int m_filterChainHash; - - MsgConfigureChannelizer(unsigned int log2Interp, unsigned int filterChainHash) : - Message(), - m_log2Interp(log2Interp), - m_filterChainHash(filterChainHash) - { } - }; - class MsgConfigureFileSource : public Message { MESSAGE_CLASS_DECLARATION @@ -207,113 +185,13 @@ public: { } }; - class MsgPlayPause : public Message { - MESSAGE_CLASS_DECLARATION - - public: - bool getPlayPause() const { return m_playPause; } - - static MsgPlayPause* create(bool playPause) { - return new MsgPlayPause(playPause); - } - - protected: - bool m_playPause; - - MsgPlayPause(bool playPause) : - Message(), - m_playPause(playPause) - { } - }; - - class MsgReportFileSourceStreamData : public Message { - MESSAGE_CLASS_DECLARATION - - public: - int getSampleRate() const { return m_sampleRate; } - quint32 getSampleSize() const { return m_sampleSize; } - quint64 getCenterFrequency() const { return m_centerFrequency; } - quint64 getStartingTimeStamp() const { return m_startingTimeStamp; } - quint64 getRecordLength() const { return m_recordLength; } - - static MsgReportFileSourceStreamData* create(int sampleRate, - quint32 sampleSize, - quint64 centerFrequency, - quint64 startingTimeStamp, - quint64 recordLength) - { - return new MsgReportFileSourceStreamData(sampleRate, sampleSize, centerFrequency, startingTimeStamp, recordLength); - } - - protected: - int m_sampleRate; - quint32 m_sampleSize; - quint64 m_centerFrequency; - quint64 m_startingTimeStamp; - quint64 m_recordLength; - - MsgReportFileSourceStreamData(int sampleRate, - quint32 sampleSize, - quint64 centerFrequency, - quint64 startingTimeStamp, - quint64 recordLength) : - Message(), - m_sampleRate(sampleRate), - m_sampleSize(sampleSize), - m_centerFrequency(centerFrequency), - m_startingTimeStamp(startingTimeStamp), - m_recordLength(recordLength) - { } - }; - - class MsgReportFileSourceStreamTiming : public Message { - MESSAGE_CLASS_DECLARATION - - public: - quint64 getSamplesCount() const { return m_samplesCount; } - - static MsgReportFileSourceStreamTiming* create(quint64 samplesCount) - { - return new MsgReportFileSourceStreamTiming(samplesCount); - } - - protected: - quint64 m_samplesCount; - - MsgReportFileSourceStreamTiming(quint64 samplesCount) : - Message(), - m_samplesCount(samplesCount) - { } - }; - - class MsgReportHeaderCRC : public Message { - MESSAGE_CLASS_DECLARATION - - public: - bool isOK() const { return m_ok; } - - static MsgReportHeaderCRC* create(bool ok) { - return new MsgReportHeaderCRC(ok); - } - - protected: - bool m_ok; - - MsgReportHeaderCRC(bool ok) : - Message(), - m_ok(ok) - { } - }; - FileSource(DeviceAPI *deviceAPI); ~FileSource(); - virtual void destroy() { delete this; } - virtual void pull(Sample& sample); - virtual void pullAudio(int nbSamples); virtual void start(); virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); virtual bool handleMessage(const Message& cmd); virtual void getIdentifier(QString& id) { id = objectName(); } @@ -357,78 +235,33 @@ public: SWGSDRangel::SWGChannelSettings& response); /** Set center frequency given in Hz */ - void setCenterFrequency(uint64_t centerFrequency) { m_centerFrequency = centerFrequency; } + void setCenterFrequency(uint64_t centerFrequency) { m_frequencyOffset = centerFrequency; } /** Set sample rate given in Hz */ - void setSampleRate(uint32_t sampleRate) { m_sampleRate = sampleRate; } + void setSampleRate(uint32_t sampleRate) { m_basebandSampleRate = sampleRate; } - quint64 getSamplesCount() const { return m_samplesCount; } - double getMagSq() const { return m_magsq; } - - void getMagSqLevels(double& avg, double& peak, int& nbSamples) - { - if (m_magsqCount > 0) - { - m_magsq = m_magsqSum / m_magsqCount; - m_magSqLevelStore.m_magsq = m_magsq; - m_magSqLevelStore.m_magsqPeak = m_magsqPeak; - } - - avg = m_magSqLevelStore.m_magsq; - peak = m_magSqLevelStore.m_magsqPeak; - nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount; - - m_magsqSum = 0.0f; - m_magsqPeak = 0.0f; - m_magsqCount = 0; - } + double getMagSq() const; + void getMagSqLevels(double& avg, double& peak, int& nbSamples) const; + void propagateMessageQueueToGUI(); static const QString m_channelIdURI; static const QString m_channelId; private: - struct MagSqLevelsStore - { - MagSqLevelsStore() : - m_magsq(1e-12), - m_magsqPeak(1e-12) - {} - double m_magsq; - double m_magsqPeak; - }; - DeviceAPI* m_deviceAPI; - QMutex m_mutex; - ThreadedBasebandSampleSource* m_threadedChannelizer; - UpChannelizer* m_channelizer; + QThread *m_thread; + FileSourceBaseband* m_basebandSource; FileSourceSettings m_settings; - std::ifstream m_ifstream; - QString m_fileName; - quint32 m_sampleSize; - quint64 m_centerFrequency; - int64_t m_frequencyOffset; - uint32_t m_fileSampleRate; - quint64 m_samplesCount; - uint32_t m_sampleRate; - uint32_t m_deviceSampleRate; - quint64 m_recordLength; //!< record length in seconds computed from file size - quint64 m_startingTimeStamp; - QTimer m_masterTimer; - bool m_running; + + SampleVector m_sampleBuffer; + QMutex m_settingsMutex; + uint64_t m_frequencyOffset; + uint32_t m_basebandSampleRate; + double m_linearGain; + QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; - double m_linearGain; - double m_magsq; - double m_magsqSum; - double m_magsqPeak; - int m_magsqCount; - MagSqLevelsStore m_magSqLevelStore; - MovingAverageUtil m_movingAverage; - - void openFileStream(); - void seekFileStream(int seekMillis); - void handleEOF(); void applySettings(const FileSourceSettings& settings, bool force = false); static void validateFilterChainHash(FileSourceSettings& settings); void calculateFrequencyOffset(); diff --git a/plugins/channeltx/filesource/filesourcebaseband.cpp b/plugins/channeltx/filesource/filesourcebaseband.cpp new file mode 100644 index 000000000..08fbed0a8 --- /dev/null +++ b/plugins/channeltx/filesource/filesourcebaseband.cpp @@ -0,0 +1,234 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "filesourcebaseband.h" + +MESSAGE_CLASS_DEFINITION(FileSourceBaseband::MsgConfigureFileSourceBaseband, Message) +MESSAGE_CLASS_DEFINITION(FileSourceBaseband::MsgConfigureChannelizer, Message) +MESSAGE_CLASS_DEFINITION(FileSourceBaseband::MsgConfigureFileSourceName, Message) +MESSAGE_CLASS_DEFINITION(FileSourceBaseband::MsgConfigureFileSourceWork, Message) +MESSAGE_CLASS_DEFINITION(FileSourceBaseband::MsgConfigureFileSourceSeek, Message) + +FileSourceBaseband::FileSourceBaseband() : + m_avg(0.0), + m_peak(0.0), + m_nbSamples(1), + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpSampleChannelizer(&m_source); + + qDebug("FileSourceBaseband::FileSourceBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &FileSourceBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +FileSourceBaseband::~FileSourceBaseband() +{ + delete m_channelizer; +} + +void FileSourceBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void FileSourceBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void FileSourceBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getMagSqLevels(m_avg, m_peak, m_nbSamples); +} + +void FileSourceBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void FileSourceBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool FileSourceBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureFileSourceBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureFileSourceBaseband& cfg = (MsgConfigureFileSourceBaseband&) cmd; + qDebug() << "FileSourceBaseband::handleMessage: MsgConfigureFileSourceBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureChannelizer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; + m_settings.m_log2Interp = cfg.getLog2Interp(); + m_settings.m_filterChainHash = cfg.getFilterChainHash(); + + qDebug() << "FileSourceBaseband::handleMessage: MsgConfigureChannelizer:" + << " log2Interp: " << m_settings.m_log2Interp + << " filterChainHash: " << m_settings.m_filterChainHash; + m_channelizer->setInterpolation(m_settings.m_log2Interp, m_settings.m_filterChainHash); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "FileSourceBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + + return true; + } + else if (MsgConfigureFileSourceName::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureFileSourceName& conf = (MsgConfigureFileSourceName&) cmd; + qDebug() << "FileSourceBaseband::handleMessage: MsgConfigureFileSourceName: " << conf.getFileName(); + m_source.openFileStream(conf.getFileName()); + + return true; + } + else if (MsgConfigureFileSourceWork::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureFileSourceWork& conf = (MsgConfigureFileSourceWork&) cmd; + qDebug() << "FileSourceBaseband::handleMessage: MsgConfigureFileSourceWork: " << conf.isWorking(); + m_source.setRunning(conf.isWorking()); + + return true; + } + else if (MsgConfigureFileSourceSeek::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureFileSourceSeek& conf = (MsgConfigureFileSourceSeek&) cmd; + m_source.seekFileStream(conf.getMillis()); + + return true; + } + else + { + return false; + } +} + +void FileSourceBaseband::applySettings(const FileSourceSettings& settings, bool force) +{ + qDebug() << "FileSourceBaseband::applySettings:" + << "m_fileName:" << settings.m_fileName + << "m_loop:" << settings.m_loop + << "m_gainDB:" << settings.m_gainDB + << "m_log2Interp:" << settings.m_log2Interp + << "m_filterChainHash:" << settings.m_filterChainHash + << " force: " << force; + + if ((m_settings.m_filterChainHash != settings.m_filterChainHash) + || (m_settings.m_log2Interp != settings.m_log2Interp) || force) + { + m_channelizer->setInterpolation(settings.m_log2Interp, settings.m_filterChainHash); + } + + m_source.applySettings(settings, force); + m_settings = settings; +} + +int FileSourceBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} + +quint64 FileSourceBaseband::getSamplesCount() const +{ + return m_source.getSamplesCount(); +} \ No newline at end of file diff --git a/plugins/channeltx/filesource/filesourcebaseband.h b/plugins/channeltx/filesource/filesourcebaseband.h new file mode 100644 index 000000000..fbc5160c4 --- /dev/null +++ b/plugins/channeltx/filesource/filesourcebaseband.h @@ -0,0 +1,184 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FILESOURCEBASEBAND_H +#define INCLUDE_FILESOURCEBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "filesourcesource.h" + +class UpSampleChannelizer; + +class FileSourceBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureFileSourceBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const FileSourceSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureFileSourceBaseband* create(const FileSourceSettings& settings, bool force) + { + return new MsgConfigureFileSourceBaseband(settings, force); + } + + private: + FileSourceSettings m_settings; + bool m_force; + + MsgConfigureFileSourceBaseband(const FileSourceSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureChannelizer : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getLog2Interp() const { return m_log2Interp; } + int getFilterChainHash() const { return m_filterChainHash; } + + static MsgConfigureChannelizer* create(unsigned int m_log2Interp, unsigned int m_filterChainHash) { + return new MsgConfigureChannelizer(m_log2Interp, m_filterChainHash); + } + + private: + unsigned int m_log2Interp; + unsigned int m_filterChainHash; + + MsgConfigureChannelizer(unsigned int log2Interp, unsigned int filterChainHash) : + Message(), + m_log2Interp(log2Interp), + m_filterChainHash(filterChainHash) + { } + }; + + class MsgConfigureFileSourceName : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const QString& getFileName() const { return m_fileName; } + + static MsgConfigureFileSourceName* create(const QString& fileName) + { + return new MsgConfigureFileSourceName(fileName); + } + + private: + QString m_fileName; + + MsgConfigureFileSourceName(const QString& fileName) : + Message(), + m_fileName(fileName) + { } + }; + + class MsgConfigureFileSourceWork : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool isWorking() const { return m_working; } + + static MsgConfigureFileSourceWork* create(bool working) + { + return new MsgConfigureFileSourceWork(working); + } + + private: + bool m_working; + + MsgConfigureFileSourceWork(bool working) : + Message(), + m_working(working) + { } + }; + + class MsgConfigureFileSourceSeek : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getMillis() const { return m_seekMillis; } + + static MsgConfigureFileSourceSeek* create(int seekMillis) + { + return new MsgConfigureFileSourceSeek(seekMillis); + } + + protected: + int m_seekMillis; //!< millis of seek position from the beginning 0..1000 + + MsgConfigureFileSourceSeek(int seekMillis) : + Message(), + m_seekMillis(seekMillis) + { } + }; + + FileSourceBaseband(); + ~FileSourceBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_source.setMessageQueueToGUI(messageQueue); } + double getMagSq() const { return m_source.getMagSq(); } + int getChannelSampleRate() const; + quint64 getSamplesCount() const; + + uint32_t getFileSampleRate() const { return m_source.getFileSampleRate(); } + quint64 getStartingTimeStamp() const { return m_source.getStartingTimeStamp(); } + quint64 getRecordLength() const { return m_source.getRecordLength(); } + quint32 getFileSampleSize() const { return m_source.getFileSampleSize(); } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) const + { + avg = m_avg; + peak = m_peak; + nbSamples = m_nbSamples; + } + +private: + SampleSourceFifo m_sampleFifo; + UpSampleChannelizer *m_channelizer; + FileSourceSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + FileSourceSettings m_settings; + double m_avg; + double m_peak; + int m_nbSamples; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const FileSourceSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + + +#endif // INCLUDE_FILESOURCEBASEBAND_H diff --git a/plugins/channeltx/filesource/filesourcegui.cpp b/plugins/channeltx/filesource/filesourcegui.cpp index c7390bbba..f13b01d6f 100644 --- a/plugins/channeltx/filesource/filesourcegui.cpp +++ b/plugins/channeltx/filesource/filesourcegui.cpp @@ -19,8 +19,6 @@ #include #include -#include "filesourcegui.h" - #include "device/deviceapi.h" #include "device/deviceuiset.h" #include "dsp/hbfilterchainconverter.h" @@ -29,6 +27,8 @@ #include "mainwindow.h" +#include "filesourcereport.h" +#include "filesourcegui.h" #include "filesource.h" #include "ui_filesourcegui.h" @@ -110,24 +110,24 @@ bool FileSourceGUI::handleMessage(const Message& message) updateWithAcquisition(); return true; } - else if (FileSource::MsgReportFileSourceStreamData::match(message)) + else if (FileSourceReport::MsgReportFileSourceStreamData::match(message)) { - m_fileSampleRate = ((FileSource::MsgReportFileSourceStreamData&)message).getSampleRate(); - m_fileSampleSize = ((FileSource::MsgReportFileSourceStreamData&)message).getSampleSize(); - m_startingTimeStamp = ((FileSource::MsgReportFileSourceStreamData&)message).getStartingTimeStamp(); - m_recordLength = ((FileSource::MsgReportFileSourceStreamData&)message).getRecordLength(); + m_fileSampleRate = ((FileSourceReport::MsgReportFileSourceStreamData&)message).getSampleRate(); + m_fileSampleSize = ((FileSourceReport::MsgReportFileSourceStreamData&)message).getSampleSize(); + m_startingTimeStamp = ((FileSourceReport::MsgReportFileSourceStreamData&)message).getStartingTimeStamp(); + m_recordLength = ((FileSourceReport::MsgReportFileSourceStreamData&)message).getRecordLength(); updateWithStreamData(); return true; } - else if (FileSource::MsgReportFileSourceStreamTiming::match(message)) + else if (FileSourceReport::MsgReportFileSourceStreamTiming::match(message)) { - m_samplesCount = ((FileSource::MsgReportFileSourceStreamTiming&)message).getSamplesCount(); + m_samplesCount = ((FileSourceReport::MsgReportFileSourceStreamTiming&)message).getSamplesCount(); updateWithStreamTime(); return true; } - else if (FileSource::MsgPlayPause::match(message)) + else if (FileSourceReport::MsgPlayPause::match(message)) { - FileSource::MsgPlayPause& notif = (FileSource::MsgPlayPause&) message; + FileSourceReport::MsgPlayPause& notif = (FileSourceReport::MsgPlayPause&) message; bool checked = notif.getPlayPause(); ui->play->setChecked(checked); ui->navTime->setEnabled(!checked); @@ -135,9 +135,9 @@ bool FileSourceGUI::handleMessage(const Message& message) return true; } - else if (FileSource::MsgReportHeaderCRC::match(message)) + else if (FileSourceReport::MsgReportHeaderCRC::match(message)) { - FileSource::MsgReportHeaderCRC& notif = (FileSource::MsgReportHeaderCRC&) message; + FileSourceReport::MsgReportHeaderCRC& notif = (FileSourceReport::MsgReportHeaderCRC&) message; if (notif.isOK()) { ui->crcLabel->setStyleSheet("QLabel { background-color : green; }"); @@ -181,6 +181,7 @@ FileSourceGUI::FileSourceGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Bas m_fileSource = (FileSource*) channelTx; m_fileSource->setMessageQueueToGUI(getInputMessageQueue()); + m_fileSource->propagateMessageQueueToGUI(); connect(&(m_deviceUISet->m_deviceAPI->getMasterTimer()), SIGNAL(timeout()), this, SLOT(tick())); @@ -231,17 +232,6 @@ void FileSourceGUI::applySettings(bool force) } } -void FileSourceGUI::applyChannelSettings() -{ - if (m_doApplySettings) - { - FileSource::MsgConfigureChannelizer *msgChan = FileSource::MsgConfigureChannelizer::create( - m_settings.m_log2Interp, - m_settings.m_filterChainHash); - m_fileSource->getInputMessageQueue()->push(msgChan); - } -} - void FileSourceGUI::configureFileName() { qDebug() << "FileSourceGui::configureFileName: " << m_fileName.toStdString().c_str(); @@ -473,7 +463,7 @@ void FileSourceGUI::applyPosition() ui->filterChainText->setText(s); displayRateAndShift(); - applyChannelSettings(); + applySettings(); } void FileSourceGUI::tick() diff --git a/plugins/channeltx/filesource/filesourcegui.h b/plugins/channeltx/filesource/filesourcegui.h index 710f6709d..27a2c723a 100644 --- a/plugins/channeltx/filesource/filesourcegui.h +++ b/plugins/channeltx/filesource/filesourcegui.h @@ -86,7 +86,6 @@ private: void blockApplySettings(bool block); void applySettings(bool force = false); - void applyChannelSettings(); void configureFileName(); void updateWithAcquisition(); void updateWithStreamData(); diff --git a/plugins/channeltx/filesource/filesourceplugin.cpp b/plugins/channeltx/filesource/filesourceplugin.cpp index 8ae86bb42..cd6b94013 100644 --- a/plugins/channeltx/filesource/filesourceplugin.cpp +++ b/plugins/channeltx/filesource/filesourceplugin.cpp @@ -27,7 +27,7 @@ const PluginDescriptor FileSourcePlugin::m_pluginDescriptor = { QString("File channel source"), - QString("4.11.6"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/channeltx/filesource/filesourcereport.cpp b/plugins/channeltx/filesource/filesourcereport.cpp new file mode 100644 index 000000000..734505186 --- /dev/null +++ b/plugins/channeltx/filesource/filesourcereport.cpp @@ -0,0 +1,29 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "filesourcereport.h" + +MESSAGE_CLASS_DEFINITION(FileSourceReport::MsgPlayPause, Message) +MESSAGE_CLASS_DEFINITION(FileSourceReport::MsgReportFileSourceStreamData, Message) +MESSAGE_CLASS_DEFINITION(FileSourceReport::MsgReportFileSourceStreamTiming, Message) +MESSAGE_CLASS_DEFINITION(FileSourceReport::MsgReportHeaderCRC, Message) + +FileSourceReport::FileSourceReport() +{} + +FileSourceReport::~FileSourceReport() +{} \ No newline at end of file diff --git a/plugins/channeltx/filesource/filesourcereport.h b/plugins/channeltx/filesource/filesourcereport.h new file mode 100644 index 000000000..c63f2dcf3 --- /dev/null +++ b/plugins/channeltx/filesource/filesourcereport.h @@ -0,0 +1,130 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_FILESOURCE_FILESOURCEREPORT_H_ +#define PLUGINS_CHANNELTX_FILESOURCE_FILESOURCEREPORT_H_ + +#include +#include "util/message.h" + +class FileSourceReport : public QObject +{ + Q_OBJECT +public: + class MsgReportHeaderCRC : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool isOK() const { return m_ok; } + + static MsgReportHeaderCRC* create(bool ok) { + return new MsgReportHeaderCRC(ok); + } + + protected: + bool m_ok; + + MsgReportHeaderCRC(bool ok) : + Message(), + m_ok(ok) + { } + }; + + class MsgReportFileSourceStreamData : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSampleRate() const { return m_sampleRate; } + quint32 getSampleSize() const { return m_sampleSize; } + quint64 getCenterFrequency() const { return m_centerFrequency; } + quint64 getStartingTimeStamp() const { return m_startingTimeStamp; } + quint64 getRecordLength() const { return m_recordLength; } + + static MsgReportFileSourceStreamData* create(int sampleRate, + quint32 sampleSize, + quint64 centerFrequency, + quint64 startingTimeStamp, + quint64 recordLength) + { + return new MsgReportFileSourceStreamData(sampleRate, sampleSize, centerFrequency, startingTimeStamp, recordLength); + } + + protected: + int m_sampleRate; + quint32 m_sampleSize; + quint64 m_centerFrequency; + quint64 m_startingTimeStamp; + quint64 m_recordLength; + + MsgReportFileSourceStreamData(int sampleRate, + quint32 sampleSize, + quint64 centerFrequency, + quint64 startingTimeStamp, + quint64 recordLength) : + Message(), + m_sampleRate(sampleRate), + m_sampleSize(sampleSize), + m_centerFrequency(centerFrequency), + m_startingTimeStamp(startingTimeStamp), + m_recordLength(recordLength) + { } + }; + + class MsgReportFileSourceStreamTiming : public Message { + MESSAGE_CLASS_DECLARATION + + public: + quint64 getSamplesCount() const { return m_samplesCount; } + + static MsgReportFileSourceStreamTiming* create(quint64 samplesCount) + { + return new MsgReportFileSourceStreamTiming(samplesCount); + } + + protected: + quint64 m_samplesCount; + + MsgReportFileSourceStreamTiming(quint64 samplesCount) : + Message(), + m_samplesCount(samplesCount) + { } + }; + + class MsgPlayPause : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getPlayPause() const { return m_playPause; } + + static MsgPlayPause* create(bool playPause) { + return new MsgPlayPause(playPause); + } + + protected: + bool m_playPause; + + MsgPlayPause(bool playPause) : + Message(), + m_playPause(playPause) + { } + }; + + FileSourceReport(); + ~FileSourceReport(); +}; + +#endif // PLUGINS_CHANNELTX_FILESOURCE_FILESOURCEREPORT_H_ \ No newline at end of file diff --git a/plugins/channeltx/filesource/filesourcesource.cpp b/plugins/channeltx/filesource/filesourcesource.cpp new file mode 100644 index 000000000..0263cf2cf --- /dev/null +++ b/plugins/channeltx/filesource/filesourcesource.cpp @@ -0,0 +1,283 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "filesourcesource.h" +#include "filesourcereport.h" + +#if (defined _WIN32_) || (defined _MSC_VER) +#include "windows_time.h" +#include +#else +#include +#include +#endif + +#include + +#include "dsp/dspcommands.h" +#include "dsp/devicesamplesink.h" +#include "dsp/hbfilterchainconverter.h" +#include "dsp/filerecord.h" +#include "util/db.h" + +FileSourceSource::FileSourceSource() : + m_fileName("..."), + m_sampleSize(0), + m_centerFrequency(0), + m_frequencyOffset(0), + m_fileSampleRate(0), + m_samplesCount(0), + m_sampleRate(0), + m_deviceSampleRate(0), + m_recordLength(0), + m_startingTimeStamp(0), + m_running(false), + m_guiMessageQueue(nullptr) +{ + m_linearGain = 1.0f; + m_magsq = 0.0f; + m_magsqSum = 0.0f; + m_magsqPeak = 0.0f; + m_magsqCount = 0; +} + +FileSourceSource::~FileSourceSource() +{ +} + +void FileSourceSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void FileSourceSource::pullOne(Sample& sample) +{ + Real re; + Real im; + + struct Sample16 + { + int16_t real; + int16_t imag; + }; + + struct Sample24 + { + int32_t real; + int32_t imag; + }; + + if (!m_running) + { + re = 0; + im = 0; + } + else if (m_sampleSize == 16) + { + Sample16 sample16; + m_ifstream.read(reinterpret_cast(&sample16), sizeof(Sample16)); + + if (m_ifstream.eof()) { + handleEOF(); + } else { + m_samplesCount++; + } + + // scale to +/-1.0 + re = (sample16.real * m_linearGain) / 32760.0f; + im = (sample16.imag * m_linearGain) / 32760.0f; + } + else if (m_sampleSize == 24) + { + Sample24 sample24; + m_ifstream.read(reinterpret_cast(&sample24), sizeof(Sample24)); + + if (m_ifstream.eof()) { + handleEOF(); + } else { + m_samplesCount++; + } + + // scale to +/-1.0 + re = (sample24.real * m_linearGain) / 8388608.0f; + im = (sample24.imag * m_linearGain) / 8388608.0f; + } + else + { + re = 0; + im = 0; + } + + + if (SDR_TX_SAMP_SZ == 16) + { + sample.setReal(re * 32768.0f); + sample.setImag(im * 32768.0f); + } + else if (SDR_TX_SAMP_SZ == 24) + { + sample.setReal(re * 8388608.0f); + sample.setImag(im * 8388608.0f); + } + else + { + sample.setReal(0); + sample.setImag(0); + } + + Real magsq = re*re + im*im; + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + m_magsqSum += magsq; + + if (magsq > m_magsqPeak) { + m_magsqPeak = magsq; + } + + m_magsqCount++; +} + +void FileSourceSource::openFileStream(const QString& fileName) +{ + m_fileName = fileName; + + if (m_ifstream.is_open()) { + m_ifstream.close(); + } + +#ifdef Q_OS_WIN + m_ifstream.open(m_fileName.toStdWString().c_str(), std::ios::binary | std::ios::ate); +#else + m_ifstream.open(m_fileName.toStdString().c_str(), std::ios::binary | std::ios::ate); +#endif + quint64 fileSize = m_ifstream.tellg(); + m_samplesCount = 0; + + if (fileSize > sizeof(FileRecord::Header)) + { + FileRecord::Header header; + m_ifstream.seekg(0,std::ios_base::beg); + bool crcOK = FileRecord::readHeader(m_ifstream, header); + m_fileSampleRate = header.sampleRate; + m_centerFrequency = header.centerFrequency; + m_startingTimeStamp = header.startTimeStamp; + m_sampleSize = header.sampleSize; + QString crcHex = QString("%1").arg(header.crc32 , 0, 16); + + if (crcOK) + { + qDebug("FileSourceSource::openFileStream: CRC32 OK for header: %s", qPrintable(crcHex)); + m_recordLength = (fileSize - sizeof(FileRecord::Header)) / ((m_sampleSize == 24 ? 8 : 4) * m_fileSampleRate); + } + else + { + qCritical("FileSourceSource::openFileStream: bad CRC32 for header: %s", qPrintable(crcHex)); + m_recordLength = 0; + } + + if (getMessageQueueToGUI()) + { + FileSourceReport::MsgReportHeaderCRC *report = FileSourceReport::MsgReportHeaderCRC::create(crcOK); + getMessageQueueToGUI()->push(report); + } + } + else + { + m_recordLength = 0; + } + + qDebug() << "FileSourceSource::openFileStream: " << m_fileName.toStdString().c_str() + << " fileSize: " << fileSize << " bytes" + << " length: " << m_recordLength << " seconds" + << " sample rate: " << m_fileSampleRate << " S/s" + << " center frequency: " << m_centerFrequency << " Hz" + << " sample size: " << m_sampleSize << " bits" + << " starting TS: " << m_startingTimeStamp << "s"; + + if (getMessageQueueToGUI()) + { + FileSourceReport::MsgReportFileSourceStreamData *report = FileSourceReport::MsgReportFileSourceStreamData::create(m_fileSampleRate, + m_sampleSize, + m_centerFrequency, + m_startingTimeStamp, + m_recordLength); // file stream data + getMessageQueueToGUI()->push(report); + } + + if (m_recordLength == 0) { + m_ifstream.close(); + } +} + +void FileSourceSource::seekFileStream(int seekMillis) +{ + if ((m_ifstream.is_open()) && !m_running) + { + quint64 seekPoint = ((m_recordLength * seekMillis) / 1000) * m_fileSampleRate; + m_samplesCount = seekPoint; + seekPoint *= (m_sampleSize == 24 ? 8 : 4); // + sizeof(FileSink::Header) + m_ifstream.clear(); + m_ifstream.seekg(seekPoint + sizeof(FileRecord::Header), std::ios::beg); + } +} + +void FileSourceSource::handleEOF() +{ + if (!m_ifstream.is_open()) { + return; + } + + if (getMessageQueueToGUI()) + { + FileSourceReport::MsgReportFileSourceStreamTiming *report = FileSourceReport::MsgReportFileSourceStreamTiming::create(getSamplesCount()); + getMessageQueueToGUI()->push(report); + } + + if (m_settings.m_loop) + { + m_ifstream.clear(); + m_ifstream.seekg(0, std::ios::beg); + m_samplesCount = 0; + } + else + { + if (getMessageQueueToGUI()) + { + FileSourceReport::MsgPlayPause *report = FileSourceReport::MsgPlayPause::create(false); + getMessageQueueToGUI()->push(report); + } + } +} + +void FileSourceSource::applySettings(const FileSourceSettings& settings, bool force) +{ + qDebug() << "FileSourceSource::applySettings:" + << "m_fileName:" << settings.m_fileName + << "m_loop:" << settings.m_loop + << "m_gainDB:" << settings.m_gainDB + << "m_log2Interp:" << settings.m_log2Interp + << "m_filterChainHash:" << settings.m_filterChainHash + << " force: " << force; + + m_settings = settings; +} diff --git a/plugins/channeltx/filesource/filesourcesource.h b/plugins/channeltx/filesource/filesourcesource.h new file mode 100644 index 000000000..4ad7720ea --- /dev/null +++ b/plugins/channeltx/filesource/filesourcesource.h @@ -0,0 +1,127 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_FILESOURCE_FILESOURCESOURCE_H_ +#define PLUGINS_CHANNELTX_FILESOURCE_FILESOURCESOURCE_H_ + +#include +#include +#include + +#include +#include +#include +#include + +#include "dsp/channelsamplesource.h" +#include "util/movingaverage.h" +#include "filesourcesettings.h" + +class MessageQueue; + +class FileSourceSource : public ChannelSampleSource { +public: + FileSourceSource(); + ~FileSourceSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples) {} + + /** Set center frequency given in Hz */ + void setCenterFrequency(uint64_t centerFrequency) { m_centerFrequency = centerFrequency; } + + /** Set sample rate given in Hz */ + void setSampleRate(uint32_t sampleRate) { m_sampleRate = sampleRate; } + + quint64 getSamplesCount() const { return m_samplesCount; } + double getMagSq() const { return m_magsq; } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) + { + if (m_magsqCount > 0) + { + m_magsq = m_magsqSum / m_magsqCount; + m_magSqLevelStore.m_magsq = m_magsq; + m_magSqLevelStore.m_magsqPeak = m_magsqPeak; + } + + avg = m_magSqLevelStore.m_magsq; + peak = m_magSqLevelStore.m_magsqPeak; + nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount; + + m_magsqSum = 0.0f; + m_magsqPeak = 0.0f; + m_magsqCount = 0; + } + + void applySettings(const FileSourceSettings& settings, bool force = false); + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_guiMessageQueue = messageQueue; } + + void openFileStream(const QString& fileName); + void seekFileStream(int seekMillis); + void setRunning(bool running) { m_running = running; } + + uint32_t getFileSampleRate() const { return m_fileSampleRate; } + quint64 getStartingTimeStamp() const { return m_startingTimeStamp; } + quint64 getRecordLength() const { return m_recordLength; } + quint32 getFileSampleSize() const { return m_sampleSize; } + +private: + struct MagSqLevelsStore + { + MagSqLevelsStore() : + m_magsq(1e-12), + m_magsqPeak(1e-12) + {} + double m_magsq; + double m_magsqPeak; + }; + + int m_channelSampleRate; + int m_channelFrequencyOffset; + FileSourceSettings m_settings; + + std::ifstream m_ifstream; + QString m_fileName; + quint32 m_sampleSize; + quint64 m_centerFrequency; + int64_t m_frequencyOffset; + uint32_t m_fileSampleRate; + quint64 m_samplesCount; + uint32_t m_sampleRate; + uint32_t m_deviceSampleRate; + quint64 m_recordLength; //!< record length in seconds computed from file size + quint64 m_startingTimeStamp; + QTimer m_masterTimer; + bool m_running; + + double m_linearGain; + double m_magsq; + double m_magsqSum; + double m_magsqPeak; + int m_magsqCount; + MagSqLevelsStore m_magSqLevelStore; + MovingAverageUtil m_movingAverage; + + MessageQueue *m_guiMessageQueue; + + void handleEOF(); + MessageQueue *getMessageQueueToGUI() { return m_guiMessageQueue; } +}; + +#endif // PLUGINS_CHANNELTX_FILESOURCE_FILESOURCE_H_ diff --git a/plugins/channeltx/localsource/CMakeLists.txt b/plugins/channeltx/localsource/CMakeLists.txt index 176c14f60..7e25980de 100644 --- a/plugins/channeltx/localsource/CMakeLists.txt +++ b/plugins/channeltx/localsource/CMakeLists.txt @@ -1,7 +1,9 @@ project(localsource) set(localsource_SOURCES - localsource.cpp + localsource.cpp + localsourcebaseband.cpp + localsourcesource.cpp localsourcethread.cpp localsourceplugin.cpp localsourcesettings.cpp @@ -9,7 +11,9 @@ set(localsource_SOURCES ) set(localsource_HEADERS - localsource.h + localsource.h + localsourcebaseband.h + localsourcesource.h localsourcethread.h localsourceplugin.h localsourcesettings.h diff --git a/plugins/channeltx/localsource/localsource.cpp b/plugins/channeltx/localsource/localsource.cpp index c9993e80b..7f6802d2e 100644 --- a/plugins/channeltx/localsource/localsource.cpp +++ b/plugins/channeltx/localsource/localsource.cpp @@ -17,18 +17,14 @@ #include "localsource.h" -#include -#include - #include #include #include +#include #include "SWGChannelSettings.h" #include "util/simpleserializer.h" -#include "dsp/threadedbasebandsamplesource.h" -#include "dsp/upchannelizer.h" #include "dsp/dspcommands.h" #include "dsp/dspdevicesinkengine.h" #include "dsp/dspengine.h" @@ -36,11 +32,10 @@ #include "dsp/hbfilterchainconverter.h" #include "device/deviceapi.h" -#include "localsourcethread.h" +#include "localsourcebaseband.h" MESSAGE_CLASS_DEFINITION(LocalSource::MsgConfigureLocalSource, Message) -MESSAGE_CLASS_DEFINITION(LocalSource::MsgSampleRateNotification, Message) -MESSAGE_CLASS_DEFINITION(LocalSource::MsgConfigureChannelizer, Message) +MESSAGE_CLASS_DEFINITION(LocalSource::MsgBasebandSampleRateNotification, Message) const QString LocalSource::m_channelIdURI = "sdrangel.channel.localsource"; const QString LocalSource::m_channelId = "LocalSource"; @@ -48,23 +43,20 @@ const QString LocalSource::m_channelId = "LocalSource"; LocalSource::LocalSource(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), m_deviceAPI(deviceAPI), - m_running(false), - m_sinkThread(nullptr), - m_localSampleSourceFifo(nullptr), - m_chunkSize(0), - m_localSamplesIndex(0), - m_localSamplesIndexOffset(0), m_centerFrequency(0), m_frequencyOffset(0), - m_sampleRate(48000), - m_deviceSampleRate(48000), + m_basebandSampleRate(48000), m_settingsMutex(QMutex::Recursive) { setObjectName(m_channelId); - m_channelizer = new UpChannelizer(this); - m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this); - m_deviceAPI->addChannelSource(m_threadedChannelizer); + m_thread = new QThread(this); + m_basebandSource = new LocalSourceBaseband(); + m_basebandSource->moveToThread(m_thread); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSource(this); m_deviceAPI->addChannelSourceAPI(this); m_networkManager = new QNetworkAccessManager(); @@ -76,167 +68,57 @@ LocalSource::~LocalSource() disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); delete m_networkManager; m_deviceAPI->removeChannelSourceAPI(this); - m_deviceAPI->removeChannelSource(m_threadedChannelizer); - delete m_threadedChannelizer; - delete m_channelizer; -} - -void LocalSource::pull(Sample& sample) -{ - if (m_localSampleSourceFifo) - { - QMutexLocker mutexLocker(&m_settingsMutex); - sample = m_localSamples[m_localSamplesIndex + m_localSamplesIndexOffset]; - - if (m_localSamplesIndex < m_chunkSize - 1) - { - m_localSamplesIndex++; - } - else - { - m_localSamplesIndex = 0; - - if (m_localSamplesIndexOffset == 0) { - m_localSamplesIndexOffset = m_chunkSize; - } else { - m_localSamplesIndexOffset = 0; - } - - emit pullSamples(m_chunkSize); - } - } - else - { - sample = Sample{0, 0}; - } -} - -void LocalSource::processSamples(int offset) -{ - if (m_localSampleSourceFifo) - { - int destOffset = (m_localSamplesIndexOffset == 0 ? m_chunkSize : 0); - SampleVector::iterator beginSource; - SampleVector::iterator beginDestination = m_localSamples.begin() + destOffset; - m_localSampleSourceFifo->setIteratorFromOffset(beginSource, offset); - std::copy(beginSource, beginSource + m_chunkSize, beginDestination); - } -} - -void LocalSource::pullAudio(int nbSamples) -{ - (void) nbSamples; + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; } void LocalSource::start() { - qDebug("LocalSource::start"); - - if (m_running) { - stop(); - } - - m_sinkThread = new LocalSourceThread(); - DeviceSampleSink *deviceSink = getLocalDevice(m_settings.m_localDeviceIndex); - - if (deviceSink) - { - m_localSampleSourceFifo = deviceSink->getSampleFifo(); - m_chunkSize = m_localSampleSourceFifo->size() / 16; - m_localSamples.resize(2*m_chunkSize); - m_localSamplesIndex = 0; - m_sinkThread->setSampleFifo(m_localSampleSourceFifo); - } - else - { - m_localSampleSourceFifo = nullptr; - } - - connect(this, - SIGNAL(pullSamples(unsigned int)), - m_sinkThread, - SLOT(pullSamples(unsigned int)), - Qt::QueuedConnection); - - connect(m_sinkThread, - SIGNAL(samplesAvailable(int)), - this, - SLOT(processSamples(int)), - Qt::QueuedConnection); - - m_sinkThread->startStop(true); - m_running = true; + qDebug("LocalSource::start"); + m_basebandSource->reset(); + m_thread->start(); } void LocalSource::stop() { qDebug("LocalSource::stop"); + m_thread->exit(); + m_thread->wait(); +} - if (m_sinkThread != 0) - { - m_sinkThread->startStop(false); - m_sinkThread->deleteLater(); - m_sinkThread = 0; - } - - m_running = false; +void LocalSource::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); } bool LocalSource::handleMessage(const Message& cmd) { - if (UpChannelizer::MsgChannelizerNotification::match(cmd)) - { - UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd; - int sampleRate = notif.getSampleRate(); - - qDebug() << "LocalSource::handleMessage: MsgChannelizerNotification:" - << " channelSampleRate: " << sampleRate - << " offsetFrequency: " << notif.getFrequencyOffset(); - - if (sampleRate > 0) - { - if (m_localSampleSourceFifo) - { - QMutexLocker mutexLocker(&m_settingsMutex); - m_localSampleSourceFifo->resize(sampleRate); - m_chunkSize = sampleRate / 8; - m_localSamplesIndex = 0; - m_localSamplesIndexOffset = 0; - m_localSamples.resize(2*m_chunkSize); - } - - setSampleRate(sampleRate); - } - - return true; - } - else if (DSPSignalNotification::match(cmd)) + if (DSPSignalNotification::match(cmd)) { - DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification& cfg = (DSPSignalNotification&) cmd; + qDebug() << "LocalSource::handleMessage: DSPSignalNotification: " + << "basband sample rate: " << cfg.getSampleRate() + << "center frequency: " << cfg.getCenterFrequency(); - qDebug() << "LocalSource::handleMessage: DSPSignalNotification:" - << " inputSampleRate: " << notif.getSampleRate() - << " centerFrequency: " << notif.getCenterFrequency(); + m_basebandSampleRate = cfg.getSampleRate(); + m_centerFrequency = cfg.getCenterFrequency(); - setCenterFrequency(notif.getCenterFrequency()); - m_deviceSampleRate = notif.getSampleRate(); - calculateFrequencyOffset(); // This is when device sample rate changes - propagateSampleRateAndFrequency(m_settings.m_localDeviceIndex); + calculateFrequencyOffset(m_settings.m_log2Interp, m_settings.m_filterChainHash); + propagateSampleRateAndFrequency(m_settings.m_localDeviceIndex, m_settings.m_log2Interp); - // Redo the channelizer stuff with the new sample rate to re-synchronize everything - m_channelizer->set(m_channelizer->getInputMessageQueue(), - m_settings.m_log2Interp, - m_settings.m_filterChainHash); + MsgBasebandSampleRateNotification *msg = MsgBasebandSampleRateNotification::create(cfg.getSampleRate()); + m_basebandSource->getInputMessageQueue()->push(msg); if (m_guiMessageQueue) { - MsgSampleRateNotification *msg = MsgSampleRateNotification::create(notif.getSampleRate()); + MsgBasebandSampleRateNotification *msg = MsgBasebandSampleRateNotification::create(cfg.getSampleRate()); m_guiMessageQueue->push(msg); } return true; } - else if (MsgConfigureLocalSource::match(cmd)) + if (MsgConfigureLocalSource::match(cmd)) { MsgConfigureLocalSource& cfg = (MsgConfigureLocalSource&) cmd; qDebug() << "LocalSource::handleMessage: MsgConfigureLocalSink"; @@ -244,25 +126,6 @@ bool LocalSource::handleMessage(const Message& cmd) return true; } - else if (MsgConfigureChannelizer::match(cmd)) - { - MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; - m_settings.m_log2Interp = cfg.getLog2Interp(); - m_settings.m_filterChainHash = cfg.getFilterChainHash(); - - qDebug() << "LocalSource::handleMessage: MsgConfigureChannelizer:" - << " log2Interp: " << m_settings.m_log2Interp - << " filterChainHash: " << m_settings.m_filterChainHash; - - m_channelizer->set(m_channelizer->getInputMessageQueue(), - m_settings.m_log2Interp, - m_settings.m_filterChainHash); - - calculateFrequencyOffset(); // This is when decimation or filter chain changes - propagateSampleRateAndFrequency(m_settings.m_localDeviceIndex); - - return true; - } else { return false; @@ -302,9 +165,15 @@ void LocalSource::getLocalDevices(std::vector& indexes) DSPDeviceSinkEngine *deviceSinkEngine = dspEngine->getDeviceSinkEngineByIndex(i); DeviceSampleSink *deviceSink = deviceSinkEngine->getSink(); - if (deviceSink->getDeviceDescription() == "LocalOutput") { + if (deviceSink->getDeviceDescription() == "LocalOutput") + { + qDebug("LocalSource::getLocalDevices: index: %u: LocalOutput found", i); indexes.push_back(i); } + else + { + qDebug("LocalSource::getLocalDevices: index: %u: %s", i, qPrintable(deviceSink->getDeviceDescription())); + } } } @@ -340,42 +209,81 @@ DeviceSampleSink *LocalSource::getLocalDevice(uint32_t index) return nullptr; } -void LocalSource::propagateSampleRateAndFrequency(uint32_t index) +void LocalSource::propagateSampleRateAndFrequency(uint32_t index, uint32_t log2Interp) { + qDebug() << "LocalSource::propagateSampleRateAndFrequency:" + << " index: " << index + << " baseband_freq: " << m_basebandSampleRate + << " log2interp: " << log2Interp + << " frequency: " << m_centerFrequency + m_frequencyOffset; + DeviceSampleSink *deviceSink = getLocalDevice(index); if (deviceSink) { - deviceSink->setSampleRate(m_deviceSampleRate / (1<setSampleRate(m_basebandSampleRate / (1 << log2Interp)); deviceSink->setCenterFrequency(m_centerFrequency + m_frequencyOffset); } + else + { + qDebug("LocalSource::propagateSampleRateAndFrequency: no suitable device at index %u", index); + } } void LocalSource::applySettings(const LocalSourceSettings& settings, bool force) { qDebug() << "LocalSource::applySettings:" - << " m_localDeviceIndex: " << settings.m_localDeviceIndex - << " force: " << force; + << "m_localDeviceIndex:" << settings.m_localDeviceIndex + << "m_log2Interp:" << settings.m_log2Interp + << "m_filterChainHash:" << settings.m_filterChainHash + << "m_play:" << settings.m_play + << "m_rgbColor:" << settings.m_rgbColor + << "m_title:" << settings.m_title + << "m_useReverseAPI:" << settings.m_useReverseAPI + << "m_reverseAPIAddress:" << settings.m_reverseAPIAddress + << "m_reverseAPIChannelIndex:" << settings.m_reverseAPIChannelIndex + << "m_reverseAPIDeviceIndex:" << settings.m_reverseAPIDeviceIndex + << "m_reverseAPIPort:" << settings.m_reverseAPIPort + << " force: " << force; QList reverseAPIKeys; + if ((settings.m_log2Interp != m_settings.m_log2Interp) || force) { + reverseAPIKeys.append("log2Interp"); + } + if ((settings.m_filterChainHash != m_settings.m_filterChainHash) || force) { + reverseAPIKeys.append("filterChainHash"); + } if ((settings.m_localDeviceIndex != m_settings.m_localDeviceIndex) || force) { reverseAPIKeys.append("localDeviceIndex"); - DeviceSampleSink *deviceSink = getLocalDevice(settings.m_localDeviceIndex); + calculateFrequencyOffset(settings.m_log2Interp, settings.m_filterChainHash); + propagateSampleRateAndFrequency(settings.m_localDeviceIndex, settings.m_log2Interp); + DeviceSampleSink *deviceSampleSink = getLocalDevice(settings.m_localDeviceIndex); + LocalSourceBaseband::MsgConfigureLocalDeviceSampleSink *msg = + LocalSourceBaseband::MsgConfigureLocalDeviceSampleSink::create(deviceSampleSink); + m_basebandSource->getInputMessageQueue()->push(msg); + } - if (deviceSink) - { - if (m_sinkThread) { - m_sinkThread->setSampleFifo(deviceSink->getSampleFifo()); - } + if ((settings.m_log2Interp != m_settings.m_log2Interp) + || (settings.m_filterChainHash != m_settings.m_filterChainHash) || force) + { + calculateFrequencyOffset(settings.m_log2Interp, settings.m_filterChainHash); + propagateSampleRateAndFrequency(m_settings.m_localDeviceIndex, settings.m_log2Interp); + LocalSourceBaseband::MsgConfigureChannelizer *msg = LocalSourceBaseband::MsgConfigureChannelizer::create( + settings.m_log2Interp, + settings.m_filterChainHash + ); + m_basebandSource->getInputMessageQueue()->push(msg); + } - propagateSampleRateAndFrequency(settings.m_localDeviceIndex); - } - else - { - qWarning("LocalSource::applySettings: invalid local device for index %u", settings.m_localDeviceIndex); - } + if ((settings.m_play != m_settings.m_play) || force) + { + reverseAPIKeys.append("play"); + LocalSourceBaseband::MsgConfigureLocalSourceWork *msg = LocalSourceBaseband::MsgConfigureLocalSourceWork::create( + settings.m_play + ); + m_basebandSource->getInputMessageQueue()->push(msg); } if ((settings.m_useReverseAPI) && (reverseAPIKeys.size() != 0)) @@ -402,10 +310,10 @@ void LocalSource::validateFilterChainHash(LocalSourceSettings& settings) settings.m_filterChainHash = settings.m_filterChainHash >= s ? s-1 : settings.m_filterChainHash; } -void LocalSource::calculateFrequencyOffset() +void LocalSource::calculateFrequencyOffset(uint32_t log2Interp, uint32_t filterChainHash) { - double shiftFactor = HBFilterChainConverter::getShiftFactor(m_settings.m_log2Interp, m_settings.m_filterChainHash); - m_frequencyOffset = m_deviceSampleRate * shiftFactor; + double shiftFactor = HBFilterChainConverter::getShiftFactor(log2Interp, filterChainHash); + m_frequencyOffset = m_basebandSampleRate * shiftFactor; } int LocalSource::webapiSettingsGet( @@ -432,12 +340,6 @@ int LocalSource::webapiSettingsPutPatch( MsgConfigureLocalSource *msg = MsgConfigureLocalSource::create(settings, force); m_inputMessageQueue.push(msg); - if ((settings.m_log2Interp != m_settings.m_log2Interp) || (settings.m_filterChainHash != m_settings.m_filterChainHash) || force) - { - MsgConfigureChannelizer *msg = MsgConfigureChannelizer::create(settings.m_log2Interp, settings.m_filterChainHash); - m_inputMessageQueue.push(msg); - } - qDebug("LocalSource::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); if (m_guiMessageQueue) // forward to GUI if any { @@ -553,13 +455,14 @@ void LocalSource::webapiReverseSendSettings(QList& channelSettingsKeys, m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -574,10 +477,13 @@ void LocalSource::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("LocalSource::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("LocalSource::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } diff --git a/plugins/channeltx/localsource/localsource.h b/plugins/channeltx/localsource/localsource.h index afa2c05f0..ecaeb3dba 100644 --- a/plugins/channeltx/localsource/localsource.h +++ b/plugins/channeltx/localsource/localsource.h @@ -27,13 +27,13 @@ #include "channel/channelapi.h" #include "localsourcesettings.h" -class DeviceAPI; -class DeviceSampleSink; -class ThreadedBasebandSampleSource; -class UpChannelizer; -class LocalSourceThread; class QNetworkAccessManager; class QNetworkReply; +class QThread; + +class DeviceAPI; +class DeviceSampleSink; +class LocalSourceBaseband; class LocalSource : public BasebandSampleSource, public ChannelAPI { Q_OBJECT @@ -61,19 +61,19 @@ public: { } }; - class MsgSampleRateNotification : public Message { + class MsgBasebandSampleRateNotification : public Message { MESSAGE_CLASS_DECLARATION public: - static MsgSampleRateNotification* create(int sampleRate) { - return new MsgSampleRateNotification(sampleRate); + static MsgBasebandSampleRateNotification* create(int sampleRate) { + return new MsgBasebandSampleRateNotification(sampleRate); } - int getSampleRate() const { return m_sampleRate; } + int getBasebandSampleRate() const { return m_sampleRate; } private: - MsgSampleRateNotification(int sampleRate) : + MsgBasebandSampleRateNotification(int sampleRate) : Message(), m_sampleRate(sampleRate) { } @@ -81,36 +81,13 @@ public: int m_sampleRate; }; - class MsgConfigureChannelizer : public Message { - MESSAGE_CLASS_DECLARATION - - public: - int getLog2Interp() const { return m_log2Interp; } - int getFilterChainHash() const { return m_filterChainHash; } - - static MsgConfigureChannelizer* create(unsigned int m_log2Interp, unsigned int m_filterChainHash) { - return new MsgConfigureChannelizer(m_log2Interp, m_filterChainHash); - } - - private: - unsigned int m_log2Interp; - unsigned int m_filterChainHash; - - MsgConfigureChannelizer(unsigned int log2Interp, unsigned int filterChainHash) : - Message(), - m_log2Interp(log2Interp), - m_filterChainHash(filterChainHash) - { } - }; - LocalSource(DeviceAPI *deviceAPI); virtual ~LocalSource(); virtual void destroy() { delete this; } - virtual void pull(Sample& sample); - virtual void pullAudio(int nbSamples); virtual void start(); virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); virtual bool handleMessage(const Message& cmd); virtual void getIdentifier(QString& id) { id = objectName(); } @@ -149,39 +126,20 @@ public: const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response); - /** Set center frequency given in Hz */ - void setCenterFrequency(uint64_t centerFrequency) { m_centerFrequency = centerFrequency; } - - /** Set sample rate given in Hz */ - void setSampleRate(uint32_t sampleRate) { m_sampleRate = sampleRate; } - - void setChannelizer(unsigned int log2Interp, unsigned int filterChainHash); void getLocalDevices(std::vector& indexes); static const QString m_channelIdURI; static const QString m_channelId; -signals: - void pullSamples(unsigned int count); - private: DeviceAPI *m_deviceAPI; - ThreadedBasebandSampleSource* m_threadedChannelizer; - UpChannelizer* m_channelizer; - bool m_running; - + QThread *m_thread; + LocalSourceBaseband *m_basebandSource; LocalSourceSettings m_settings; - LocalSourceThread *m_sinkThread; - SampleSourceFifoDB *m_localSampleSourceFifo; - int m_chunkSize; - SampleVector m_localSamples; - int m_localSamplesIndex; - int m_localSamplesIndexOffset; uint64_t m_centerFrequency; int64_t m_frequencyOffset; - uint32_t m_sampleRate; - uint32_t m_deviceSampleRate; + uint32_t m_basebandSampleRate; QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; @@ -189,15 +147,15 @@ private: QMutex m_settingsMutex; void applySettings(const LocalSourceSettings& settings, bool force = false); - DeviceSampleSink *getLocalDevice(uint32_t index); - void propagateSampleRateAndFrequency(uint32_t index); + void propagateSampleRateAndFrequency(uint32_t index, uint32_t log2Interp); static void validateFilterChainHash(LocalSourceSettings& settings); - void calculateFrequencyOffset(); + void calculateFrequencyOffset(uint32_t log2Interp, uint32_t filterChainHash); + DeviceSampleSink *getLocalDevice(uint32_t index); + void webapiReverseSendSettings(QList& channelSettingsKeys, const LocalSourceSettings& settings, bool force); private slots: void networkManagerFinished(QNetworkReply *reply); - void processSamples(int offset); }; #endif /* INCLUDE_LOCALSOURCE_H_ */ diff --git a/plugins/channeltx/localsource/localsource.pro b/plugins/channeltx/localsource/localsource.pro deleted file mode 100644 index 33d25f77b..000000000 --- a/plugins/channeltx/localsource/localsource.pro +++ /dev/null @@ -1,52 +0,0 @@ -#-------------------------------------------------------- -# -# Pro file for Android and Windows builds with Qt Creator -# -#-------------------------------------------------------- - -TEMPLATE = lib -CONFIG += plugin - -QT += core gui widgets multimedia network opengl - -TARGET = localsource - -INCLUDEPATH += $$PWD -INCLUDEPATH += ../../../exports -INCLUDEPATH += ../../../sdrbase -INCLUDEPATH += ../../../sdrgui -INCLUDEPATH += ../../../swagger/sdrangel/code/qt5/client -macx:INCLUDEPATH += /opt/local/include - -CONFIG(Release):build_subdir = release -CONFIG(Debug):build_subdir = debug - -CONFIG(MINGW32):INCLUDEPATH += "C:\softs\boost_1_66_0" -CONFIG(MSVC):INCLUDEPATH += "C:\softs\boost_1_66_0" -CONFIG(macx):INCLUDEPATH += "../../../../../boost_1_69_0" - -SOURCES += localsource.cpp\ - localsourcegui.cpp\ - localsourcesettings.cpp\ - localsourceplugin.cpp\ - localsourcethread.cpp - -HEADERS += localsource.h\ - localsourcegui.h\ - localsourcesettings.h\ - localsourceplugin.h\ - localsourcethread.h - -FORMS += localsourcegui.ui - -LIBS += -L../../../sdrbase/$${build_subdir} -lsdrbase -LIBS += -L../../../sdrgui/$${build_subdir} -lsdrgui -LIBS += -L../../../swagger/$${build_subdir} -lswagger - -macx { - QMAKE_LFLAGS_SONAME = -Wl,-install_name,@rpath/ -} - -RESOURCES = ../../../sdrgui/resources/res.qrc - -CONFIG(MINGW32):DEFINES += USE_INTERNAL_TIMER=1 diff --git a/plugins/channeltx/localsource/localsourcebaseband.cpp b/plugins/channeltx/localsource/localsourcebaseband.cpp new file mode 100644 index 000000000..0a88334a9 --- /dev/null +++ b/plugins/channeltx/localsource/localsourcebaseband.cpp @@ -0,0 +1,219 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "localsourcebaseband.h" + +MESSAGE_CLASS_DEFINITION(LocalSourceBaseband::MsgConfigureLocalSourceBaseband, Message) +MESSAGE_CLASS_DEFINITION(LocalSourceBaseband::MsgConfigureChannelizer, Message) +MESSAGE_CLASS_DEFINITION(LocalSourceBaseband::MsgConfigureLocalSourceWork, Message) +MESSAGE_CLASS_DEFINITION(LocalSourceBaseband::MsgBasebandSampleRateNotification, Message) +MESSAGE_CLASS_DEFINITION(LocalSourceBaseband::MsgConfigureLocalDeviceSampleSink, Message) + +LocalSourceBaseband::LocalSourceBaseband() : + m_localSampleSink(nullptr), + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpSampleChannelizer(&m_source); + + qDebug("FileSourceBaseband::FileSourceBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &LocalSourceBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +LocalSourceBaseband::~LocalSourceBaseband() +{ + delete m_channelizer; +} + +void LocalSourceBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void LocalSourceBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void LocalSourceBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } +} + +void LocalSourceBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void LocalSourceBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool LocalSourceBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureLocalSourceBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureLocalSourceBaseband& cfg = (MsgConfigureLocalSourceBaseband&) cmd; + qDebug() << "LocalSourceBaseband::handleMessage: MsgConfigureLocalSourceBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureChannelizer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; + m_settings.m_log2Interp = cfg.getLog2Interp(); + m_settings.m_filterChainHash = cfg.getFilterChainHash(); + + qDebug() << "LocalSourceBaseband::handleMessage: MsgConfigureChannelizer:" + << " log2Interp: " << m_settings.m_log2Interp + << " filterChainHash: " << m_settings.m_filterChainHash; + m_channelizer->setInterpolation(m_settings.m_log2Interp, m_settings.m_filterChainHash); + + return true; + } + else if (MsgBasebandSampleRateNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgBasebandSampleRateNotification& notif = (MsgBasebandSampleRateNotification&) cmd; + qDebug() << "LocalSourceBaseband::handleMessage: MsgBasebandSampleRateNotification: basebandSampleRate: " << notif.getBasebandSampleRate(); + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getBasebandSampleRate())); + m_channelizer->setBasebandSampleRate(notif.getBasebandSampleRate()); + + return true; + } + else if (MsgConfigureLocalSourceWork::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureLocalSourceWork& conf = (MsgConfigureLocalSourceWork&) cmd; + qDebug() << "LocalSourceBaseband::handleMessage: MsgConfigureLocalSourceWork: " << conf.isWorking(); + + if (conf.isWorking()) { + m_source.start(m_localSampleSink); + } else { + m_source.stop(); + } + + return true; + } + else if (MsgConfigureLocalDeviceSampleSink::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureLocalDeviceSampleSink& notif = (MsgConfigureLocalDeviceSampleSink&) cmd; + qDebug() << "LocalSourceBaseband::handleMessage: MsgConfigureLocalDeviceSampleSink: " << notif.getDeviceSampleSink(); + m_localSampleSink = notif.getDeviceSampleSink(); + + if (m_source.isRunning()) { + m_source.start(m_localSampleSink); + } + + return true; + } + else + { + return false; + } +} + +void LocalSourceBaseband::applySettings(const LocalSourceSettings& settings, bool force) +{ + qDebug() << "LocalSourceBaseband::applySettings:" + << "m_localDeviceIndex:" << settings.m_localDeviceIndex + << "m_log2Interp:" << settings.m_log2Interp + << "m_filterChainHash:" << settings.m_filterChainHash + << "m_play:" << settings.m_play + << " force: " << force; + + //m_source.applySettings(settings, force); + m_settings = settings; +} + +int LocalSourceBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} diff --git a/plugins/channeltx/localsource/localsourcebaseband.h b/plugins/channeltx/localsource/localsourcebaseband.h new file mode 100644 index 000000000..79f30dcbd --- /dev/null +++ b/plugins/channeltx/localsource/localsourcebaseband.h @@ -0,0 +1,170 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FILESOURCEBASEBAND_H +#define INCLUDE_FILESOURCEBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "localsourcesource.h" +#include "localsourcesettings.h" + +class UpSampleChannelizer; + +class LocalSourceBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureLocalSourceBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const LocalSourceSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureLocalSourceBaseband* create(const LocalSourceSettings& settings, bool force) + { + return new MsgConfigureLocalSourceBaseband(settings, force); + } + + private: + LocalSourceSettings m_settings; + bool m_force; + + MsgConfigureLocalSourceBaseband(const LocalSourceSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureChannelizer : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getLog2Interp() const { return m_log2Interp; } + int getFilterChainHash() const { return m_filterChainHash; } + + static MsgConfigureChannelizer* create(unsigned int m_log2Interp, unsigned int m_filterChainHash) { + return new MsgConfigureChannelizer(m_log2Interp, m_filterChainHash); + } + + private: + unsigned int m_log2Interp; + unsigned int m_filterChainHash; + + MsgConfigureChannelizer(unsigned int log2Interp, unsigned int filterChainHash) : + Message(), + m_log2Interp(log2Interp), + m_filterChainHash(filterChainHash) + { } + }; + + class MsgConfigureLocalSourceWork : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool isWorking() const { return m_working; } + + static MsgConfigureLocalSourceWork* create(bool working) + { + return new MsgConfigureLocalSourceWork(working); + } + + private: + bool m_working; + + MsgConfigureLocalSourceWork(bool working) : + Message(), + m_working(working) + { } + }; + + class MsgBasebandSampleRateNotification : public Message { + MESSAGE_CLASS_DECLARATION + + public: + static MsgBasebandSampleRateNotification* create(int sampleRate) { + return new MsgBasebandSampleRateNotification(sampleRate); + } + + int getBasebandSampleRate() const { return m_sampleRate; } + + private: + + MsgBasebandSampleRateNotification(int sampleRate) : + Message(), + m_sampleRate(sampleRate) + { } + + int m_sampleRate; + }; + + class MsgConfigureLocalDeviceSampleSink : public Message { + MESSAGE_CLASS_DECLARATION + + public: + static MsgConfigureLocalDeviceSampleSink* create(DeviceSampleSink *deviceSampleSink) { + return new MsgConfigureLocalDeviceSampleSink(deviceSampleSink); + } + + DeviceSampleSink *getDeviceSampleSink() const { return m_deviceSampleSink; } + + private: + + MsgConfigureLocalDeviceSampleSink(DeviceSampleSink *deviceSampleSink) : + Message(), + m_deviceSampleSink(deviceSampleSink) + { } + + DeviceSampleSink *m_deviceSampleSink; + }; + + LocalSourceBaseband(); + ~LocalSourceBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + int getChannelSampleRate() const; + void startSource() { m_source.start(m_localSampleSink); } + void stopSource() { m_source.stop(); } + +private: + SampleSourceFifo m_sampleFifo; + UpSampleChannelizer *m_channelizer; + LocalSourceSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + LocalSourceSettings m_settings; + DeviceSampleSink *m_localSampleSink; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const LocalSourceSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + + +#endif // INCLUDE_FILESOURCEBASEBAND_H diff --git a/plugins/channeltx/localsource/localsourcegui.cpp b/plugins/channeltx/localsource/localsourcegui.cpp index 31cf92009..2995c788b 100644 --- a/plugins/channeltx/localsource/localsourcegui.cpp +++ b/plugins/channeltx/localsource/localsourcegui.cpp @@ -82,11 +82,10 @@ bool LocalSourceGUI::deserialize(const QByteArray& data) bool LocalSourceGUI::handleMessage(const Message& message) { - if (LocalSource::MsgSampleRateNotification::match(message)) + if (LocalSource::MsgBasebandSampleRateNotification::match(message)) { - LocalSource::MsgSampleRateNotification& notif = (LocalSource::MsgSampleRateNotification&) message; - //m_channelMarker.setBandwidth(notif.getSampleRate()); - m_sampleRate = notif.getSampleRate(); + LocalSource::MsgBasebandSampleRateNotification& notif = (LocalSource::MsgBasebandSampleRateNotification&) message; + m_basebandSampleRate = notif.getBasebandSampleRate(); displayRateAndShift(); return true; } @@ -110,7 +109,7 @@ LocalSourceGUI::LocalSourceGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, B ui(new Ui::LocalSourceGUI), m_pluginAPI(pluginAPI), m_deviceUISet(deviceUISet), - m_sampleRate(0), + m_basebandSampleRate(0), m_tickCount(0) { ui->setupUi(this); @@ -168,23 +167,12 @@ void LocalSourceGUI::applySettings(bool force) } } -void LocalSourceGUI::applyChannelSettings() -{ - if (m_doApplySettings) - { - LocalSource::MsgConfigureChannelizer *msgChan = LocalSource::MsgConfigureChannelizer::create( - m_settings.m_log2Interp, - m_settings.m_filterChainHash); - m_localSource->getInputMessageQueue()->push(msgChan); - } -} - void LocalSourceGUI::displaySettings() { m_channelMarker.blockSignals(true); m_channelMarker.setCenterFrequency(0); m_channelMarker.setTitle(m_settings.m_title); - m_channelMarker.setBandwidth(m_sampleRate); // TODO + m_channelMarker.setBandwidth(m_basebandSampleRate / (1<offsetFrequencyText->setText(tr("%1 Hz").arg(loc.toString(shift))); ui->channelRateText->setText(tr("%1k").arg(QString::number(channelSampleRate / 1000.0, 'g', 5))); @@ -305,6 +293,12 @@ void LocalSourceGUI::on_localDevicesRefresh_clicked(bool checked) updateLocalDevices(); } +void LocalSourceGUI::on_localDevicePlay_toggled(bool checked) +{ + m_settings.m_play = checked; + applySettings(); +} + void LocalSourceGUI::applyInterpolation() { uint32_t maxHash = 1; @@ -327,7 +321,7 @@ void LocalSourceGUI::applyPosition() ui->filterChainText->setText(s); displayRateAndShift(); - applyChannelSettings(); + applySettings(); } void LocalSourceGUI::tick() diff --git a/plugins/channeltx/localsource/localsourcegui.h b/plugins/channeltx/localsource/localsourcegui.h index 789160e2a..804d342f7 100644 --- a/plugins/channeltx/localsource/localsourcegui.h +++ b/plugins/channeltx/localsource/localsourcegui.h @@ -62,7 +62,7 @@ private: DeviceUISet* m_deviceUISet; ChannelMarker m_channelMarker; LocalSourceSettings m_settings; - int m_sampleRate; + int m_basebandSampleRate; quint64 m_deviceCenterFrequency; //!< Center frequency in device double m_shiftFrequencyFactor; //!< Channel frequency shift factor bool m_doApplySettings; @@ -78,7 +78,6 @@ private: void blockApplySettings(bool block); void applySettings(bool force = false); - void applyChannelSettings(); void displaySettings(); void displayRateAndShift(); void updateLocalDevices(); @@ -95,6 +94,7 @@ private slots: void on_position_valueChanged(int value); void on_localDevice_currentIndexChanged(int index); void on_localDevicesRefresh_clicked(bool checked); + void on_localDevicePlay_toggled(bool checked); void onWidgetRolled(QWidget* widget, bool rollDown); void onMenuDialogCalled(const QPoint& p); void tick(); diff --git a/plugins/channeltx/localsource/localsourcegui.ui b/plugins/channeltx/localsource/localsourcegui.ui index 6224aabaf..9ca20eccf 100644 --- a/plugins/channeltx/localsource/localsourcegui.ui +++ b/plugins/channeltx/localsource/localsourcegui.ui @@ -298,6 +298,21 @@ + + + + Start/Stop source + + + + + + + :/play.png + :/pause.png:/play.png + + + @@ -323,6 +338,11 @@

gui/rollupwidget.h
1 + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
diff --git a/plugins/channeltx/localsource/localsourceplugin.cpp b/plugins/channeltx/localsource/localsourceplugin.cpp index c021d7c6d..5e2b579f0 100644 --- a/plugins/channeltx/localsource/localsourceplugin.cpp +++ b/plugins/channeltx/localsource/localsourceplugin.cpp @@ -29,7 +29,7 @@ const PluginDescriptor LocalSourcePlugin::m_pluginDescriptor = { QString("Local channel source"), - QString("4.11.6"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/channeltx/localsource/localsourcesettings.cpp b/plugins/channeltx/localsource/localsourcesettings.cpp index ff6427ba4..e08419e0f 100644 --- a/plugins/channeltx/localsource/localsourcesettings.cpp +++ b/plugins/channeltx/localsource/localsourcesettings.cpp @@ -36,6 +36,7 @@ void LocalSourceSettings::resetToDefaults() m_log2Interp = 0; m_filterChainHash = 0; m_channelMarker = nullptr; + m_play = false; m_useReverseAPI = false; m_reverseAPIAddress = "127.0.0.1"; m_reverseAPIPort = 8888; diff --git a/plugins/channeltx/localsource/localsourcesettings.h b/plugins/channeltx/localsource/localsourcesettings.h index c883f9f26..f9ab4c45a 100644 --- a/plugins/channeltx/localsource/localsourcesettings.h +++ b/plugins/channeltx/localsource/localsourcesettings.h @@ -30,6 +30,7 @@ struct LocalSourceSettings QString m_title; uint32_t m_log2Interp; uint32_t m_filterChainHash; + bool m_play; bool m_useReverseAPI; QString m_reverseAPIAddress; uint16_t m_reverseAPIPort; diff --git a/plugins/channeltx/localsource/localsourcesource.cpp b/plugins/channeltx/localsource/localsourcesource.cpp new file mode 100644 index 000000000..d721845fe --- /dev/null +++ b/plugins/channeltx/localsource/localsourcesource.cpp @@ -0,0 +1,156 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "dsp/devicesamplesink.h" +#include "localsourcethread.h" +#include "localsourcesource.h" + +LocalSourceSource::LocalSourceSource() : + m_running(false), + m_sinkThread(nullptr) +{} + +LocalSourceSource::~LocalSourceSource() +{} + +void LocalSourceSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void LocalSourceSource::start(DeviceSampleSink *deviceSink) +{ + qDebug("LocalSourceSource::start: %p", deviceSink); + + if (m_running) { + stop(); + } + + if (!deviceSink) { + return; + } + + m_sinkThread = new LocalSourceThread(); + + if (deviceSink) + { + m_localSampleSourceFifo = deviceSink->getSampleFifo(); + m_chunkSize = deviceSink->getSampleRate() / 10; + m_localSamples.resize(2*m_chunkSize); + m_localSamplesIndex = 0; + m_localSamplesIndexOffset = m_chunkSize; + m_sinkThread->setSampleFifo(m_localSampleSourceFifo); + } + else + { + m_localSampleSourceFifo = nullptr; + } + + connect(this, + SIGNAL(pullSamples(unsigned int)), + m_sinkThread, + SLOT(pullSamples(unsigned int)), + Qt::QueuedConnection); + + connect(m_sinkThread, + SIGNAL(samplesAvailable(unsigned int, unsigned int, unsigned int, unsigned int)), + this, + SLOT(processSamples(unsigned int, unsigned int, unsigned int, unsigned int)), + Qt::QueuedConnection); + + m_sinkThread->startStop(true); + m_running = true; +} + +void LocalSourceSource::stop() +{ + qDebug("LocalSourceSource::stop"); + + if (m_sinkThread) + { + m_sinkThread->startStop(false); + m_sinkThread->deleteLater(); + m_sinkThread = nullptr; + } + + m_running = false; +} + +void LocalSourceSource::pullOne(Sample& sample) +{ + if (m_localSampleSourceFifo) + { + sample = m_localSamples[m_localSamplesIndex + m_localSamplesIndexOffset]; + + if (m_localSamplesIndex < m_chunkSize - 1) + { + m_localSamplesIndex++; + } + else + { + m_localSamplesIndex = 0; + + if (m_localSamplesIndexOffset == 0) { + m_localSamplesIndexOffset = m_chunkSize; + } else { + m_localSamplesIndexOffset = 0; + } + + emit pullSamples(m_chunkSize); + } + } + else + { + sample = Sample{0, 0}; + } +} + +void LocalSourceSource::processSamples(unsigned int iPart1Begin, unsigned int iPart1End, unsigned int iPart2Begin, unsigned int iPart2End) +{ + int destOffset = (m_localSamplesIndexOffset == 0 ? m_chunkSize : 0); + SampleVector::iterator beginDestination = m_localSamples.begin() + destOffset; + SampleVector& data = m_localSampleSourceFifo->getData(); + + // qDebug("LocalSourceSource::processSamples: m_localSamplesIndexOffset: %d m_chunkSize: %d part1: %u part2: %u", + // m_localSamplesIndexOffset, m_chunkSize, (iPart1End - iPart1Begin), (iPart2End - iPart2Begin)); + + if (iPart1Begin != iPart1End) + { + std::copy( + data.begin() + iPart1Begin, + data.begin() + iPart1End, + beginDestination + ); + } + + beginDestination += (iPart1End - iPart1Begin); + + if (iPart2Begin != iPart2End) + { + std::copy( + data.begin() + iPart2Begin, + data.begin() + iPart2End, + beginDestination + ); + } +} diff --git a/plugins/channeltx/localsource/localsourcesource.h b/plugins/channeltx/localsource/localsourcesource.h new file mode 100644 index 000000000..a42440cdc --- /dev/null +++ b/plugins/channeltx/localsource/localsourcesource.h @@ -0,0 +1,61 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_LOCALSOURCE_LOCALSOURCESOURCE_H_ +#define PLUGINS_CHANNELTX_LOCALSOURCE_LOCALSOURCESOURCE_H_ + +#include + +#include "dsp/channelsamplesource.h" +#include "localsourcesettings.h" + +class DeviceSampleSink; +class SampleSourceFifo; +class LocalSourceThread; + +class LocalSourceSource : public QObject, public ChannelSampleSource { + Q_OBJECT +public: + LocalSourceSource(); + ~LocalSourceSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples) {} + + void start(DeviceSampleSink *deviceSink); + void stop(); + bool isRunning() const { return m_running; } + +signals: + void pullSamples(unsigned int count); + +private: + bool m_running; + LocalSourceThread *m_sinkThread; + SampleSourceFifo *m_localSampleSourceFifo; + int m_chunkSize; + SampleVector m_localSamples; + int m_localSamplesIndex; + int m_localSamplesIndexOffset; + +private slots: + void processSamples(unsigned int iPart1Begin, unsigned int iPart1End, unsigned int iPart2Begin, unsigned int iPart2End); +}; + + +#endif // PLUGINS_CHANNELTX_LOCALSOURCE_LOCALSOURCESOURCE_H_ diff --git a/plugins/channeltx/localsource/localsourcethread.cpp b/plugins/channeltx/localsource/localsourcethread.cpp index d16210c71..195c85b51 100644 --- a/plugins/channeltx/localsource/localsourcethread.cpp +++ b/plugins/channeltx/localsource/localsourcethread.cpp @@ -15,7 +15,7 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// -#include "dsp/samplesourcefifodb.h" +#include "dsp/samplesourcefifo.h" #include "localsourcethread.h" @@ -24,7 +24,7 @@ MESSAGE_CLASS_DEFINITION(LocalSourceThread::MsgStartStop, Message) LocalSourceThread::LocalSourceThread(QObject* parent) : QThread(parent), m_running(false), - m_sampleFifo(0) + m_sampleFifo(nullptr) { connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()), Qt::QueuedConnection); } @@ -40,16 +40,16 @@ void LocalSourceThread::startStop(bool start) m_inputMessageQueue.push(msg); } -void LocalSourceThread::setSampleFifo(SampleSourceFifoDB *sampleFifo) +void LocalSourceThread::setSampleFifo(SampleSourceFifo *sampleFifo) { m_sampleFifo = sampleFifo; } void LocalSourceThread::pullSamples(unsigned int count) { - SampleVector::iterator beginRead; - m_sampleFifo->readAdvance(beginRead, count); - emit samplesAvailable(m_sampleFifo->getIteratorOffset(beginRead)); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_sampleFifo->read(count, iPart1Begin, iPart1End, iPart2Begin, iPart2End); + emit samplesAvailable(iPart1Begin, iPart1End, iPart2Begin, iPart2End); } void LocalSourceThread::startWork() diff --git a/plugins/channeltx/localsource/localsourcethread.h b/plugins/channeltx/localsource/localsourcethread.h index f59240ae0..ff5a7b54e 100644 --- a/plugins/channeltx/localsource/localsourcethread.h +++ b/plugins/channeltx/localsource/localsourcethread.h @@ -26,7 +26,7 @@ #include "util/message.h" #include "util/messagequeue.h" -class SampleSourceFifoDB; +class SampleSourceFifo; class LocalSourceThread : public QThread { Q_OBJECT @@ -51,23 +51,23 @@ public: { } }; - LocalSourceThread(QObject* parent = 0); + LocalSourceThread(QObject* parent = nullptr); ~LocalSourceThread(); void startStop(bool start); - void setSampleFifo(SampleSourceFifoDB *sampleFifo); + void setSampleFifo(SampleSourceFifo *sampleFifo); public slots: void pullSamples(unsigned int count); signals: - void samplesAvailable(int offset); + void samplesAvailable(unsigned int iPart1Begin, unsigned int iPart1End, unsigned int iPart2Begin, unsigned int iPart2End); private: QMutex m_startWaitMutex; QWaitCondition m_startWaiter; volatile bool m_running; - SampleSourceFifoDB *m_sampleFifo; + SampleSourceFifo *m_sampleFifo; MessageQueue m_inputMessageQueue; diff --git a/plugins/channeltx/modam/CMakeLists.txt b/plugins/channeltx/modam/CMakeLists.txt index c7a0efd5b..5634eefd9 100644 --- a/plugins/channeltx/modam/CMakeLists.txt +++ b/plugins/channeltx/modam/CMakeLists.txt @@ -4,6 +4,8 @@ set(modam_SOURCES ammod.cpp ammodplugin.cpp ammodsettings.cpp + ammodbaseband.cpp + ammodsource.cpp ammodwebapiadapter.cpp ) @@ -11,6 +13,8 @@ set(modam_HEADERS ammod.h ammodplugin.h ammodsettings.h + ammodbaseband.h + ammodsource.h ammodwebapiadapter.h ) diff --git a/plugins/channeltx/modam/ammod.cpp b/plugins/channeltx/modam/ammod.cpp index 369cfd048..689019fc9 100644 --- a/plugins/channeltx/modam/ammod.cpp +++ b/plugins/channeltx/modam/ammod.cpp @@ -24,21 +24,22 @@ #include #include #include +#include #include "SWGChannelSettings.h" #include "SWGChannelReport.h" #include "SWGAMModReport.h" -#include "ammod.h" - -#include "dsp/upchannelizer.h" #include "dsp/dspengine.h" -#include "dsp/threadedbasebandsamplesource.h" #include "dsp/dspcommands.h" #include "dsp/devicesamplemimo.h" +#include "dsp/cwkeyer.h" #include "device/deviceapi.h" #include "util/db.h" +#include "ammodbaseband.h" +#include "ammod.h" + MESSAGE_CLASS_DEFINITION(AMMod::MsgConfigureAMMod, Message) MESSAGE_CLASS_DEFINITION(AMMod::MsgConfigureChannelizer, Message) MESSAGE_CLASS_DEFINITION(AMMod::MsgConfigureFileSourceName, Message) @@ -49,51 +50,25 @@ MESSAGE_CLASS_DEFINITION(AMMod::MsgReportFileSourceStreamTiming, Message) const QString AMMod::m_channelIdURI = "sdrangel.channeltx.modam"; const QString AMMod::m_channelId ="AMMod"; -const int AMMod::m_levelNbSamples = 480; // every 10ms AMMod::AMMod(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), m_deviceAPI(deviceAPI), - m_basebandSampleRate(48000), - m_outputSampleRate(48000), - m_inputFrequencyOffset(0), - m_audioFifo(4800), - m_feedbackAudioFifo(48000), m_settingsMutex(QMutex::Recursive), m_fileSize(0), m_recordLength(0), - m_sampleRate(48000), - m_levelCalcCount(0), - m_peakLevel(0.0f), - m_levelSum(0.0f) + m_sampleRate(48000) { setObjectName(m_channelId); - m_audioBuffer.resize(1<<14); - m_audioBufferFill = 0; + m_thread = new QThread(this); + m_basebandSource = new AMModBaseband(); + m_basebandSource->setInputFileStream(&m_ifstream); + m_basebandSource->moveToThread(m_thread); - m_feedbackAudioBuffer.resize(1<<14); - m_feedbackAudioBufferFill = 0; - - m_magsq = 0.0; - - DSPEngine::instance()->getAudioDeviceManager()->addAudioSource(&m_audioFifo, getInputMessageQueue()); - m_audioSampleRate = DSPEngine::instance()->getAudioDeviceManager()->getInputSampleRate(); - - DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(&m_feedbackAudioFifo, getInputMessageQueue()); - m_feedbackAudioSampleRate = DSPEngine::instance()->getAudioDeviceManager()->getOutputSampleRate(); - applyFeedbackAudioSampleRate(m_feedbackAudioSampleRate); - - m_toneNco.setFreq(1000.0, m_audioSampleRate); - m_cwKeyer.setSampleRate(m_audioSampleRate); - m_cwKeyer.reset(); - - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); applySettings(m_settings, true); - m_channelizer = new UpChannelizer(this); - m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this); - m_deviceAPI->addChannelSource(m_threadedChannelizer); + m_deviceAPI->addChannelSource(this); m_deviceAPI->addChannelSourceAPI(this); m_networkManager = new QNetworkAccessManager(); @@ -105,11 +80,9 @@ AMMod::~AMMod() disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); delete m_networkManager; m_deviceAPI->removeChannelSourceAPI(this); - m_deviceAPI->removeChannelSource(m_threadedChannelizer); - delete m_threadedChannelizer; - delete m_channelizer; - DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(&m_feedbackAudioFifo); - DSPEngine::instance()->getAudioDeviceManager()->removeAudioSource(&m_audioFifo); + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; } uint32_t AMMod::getNumberOfDeviceStreams() const @@ -117,249 +90,37 @@ uint32_t AMMod::getNumberOfDeviceStreams() const return m_deviceAPI->getNbSinkStreams(); } -void AMMod::pull(Sample& sample) -{ - if (m_settings.m_channelMute) - { - sample.m_real = 0.0f; - sample.m_imag = 0.0f; - return; - } - - Complex ci; - - m_settingsMutex.lock(); - - if (m_interpolatorDistance > 1.0f) // decimate - { - modulateSample(); - - while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - else - { - if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - - m_interpolatorDistanceRemain += m_interpolatorDistance; - - ci *= m_carrierNco.nextIQ(); // shift to carrier frequency - - m_settingsMutex.unlock(); - - double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); - magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); - m_movingAverage(magsq); - m_magsq = m_movingAverage.asDouble(); - - sample.m_real = (FixReal) ci.real(); - sample.m_imag = (FixReal) ci.imag(); -} - -void AMMod::pullAudio(int nbSamples) -{ -// qDebug("AMMod::pullAudio: %d", nbSamples); - unsigned int nbAudioSamples = nbSamples * ((Real) m_audioSampleRate / (Real) m_basebandSampleRate); - - if (nbAudioSamples > m_audioBuffer.size()) - { - m_audioBuffer.resize(nbAudioSamples); - } - - m_audioFifo.read(reinterpret_cast(&m_audioBuffer[0]), nbAudioSamples); - m_audioBufferFill = 0; -} - -void AMMod::modulateSample() -{ - Real t; - - pullAF(t); - - if (m_settings.m_feedbackAudioEnable) { - pushFeedback(t * m_settings.m_feedbackVolumeFactor * 16384.0f); - } - - calculateLevel(t); - m_audioBufferFill++; - - m_modSample.real((t*m_settings.m_modFactor + 1.0f) * 16384.0f); // modulate and scale zero frequency carrier - m_modSample.imag(0.0f); -} - -void AMMod::pullAF(Real& sample) -{ - switch (m_settings.m_modAFInput) - { - case AMModSettings::AMModInputTone: - sample = m_toneNco.next(); - break; - case AMModSettings::AMModInputFile: - // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw - // ffplay -f f32le -ar 48k -ac 1 f4exb_call.raw - if (m_ifstream.is_open()) - { - if (m_ifstream.eof()) - { - if (m_settings.m_playLoop) - { - m_ifstream.clear(); - m_ifstream.seekg(0, std::ios::beg); - } - } - - if (m_ifstream.eof()) - { - sample = 0.0f; - } - else - { - m_ifstream.read(reinterpret_cast(&sample), sizeof(Real)); - sample *= m_settings.m_volumeFactor; - } - } - else - { - sample = 0.0f; - } - break; - case AMModSettings::AMModInputAudio: - sample = ((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f) * m_settings.m_volumeFactor; - break; - case AMModSettings::AMModInputCWTone: - Real fadeFactor; - - if (m_cwKeyer.getSample()) - { - m_cwKeyer.getCWSmoother().getFadeSample(true, fadeFactor); - sample = m_toneNco.next() * fadeFactor; - } - else - { - if (m_cwKeyer.getCWSmoother().getFadeSample(false, fadeFactor)) - { - sample = m_toneNco.next() * fadeFactor; - } - else - { - sample = 0.0f; - m_toneNco.setPhase(0); - } - } - break; - case AMModSettings::AMModInputNone: - default: - sample = 0.0f; - break; - } -} - -void AMMod::pushFeedback(Real sample) -{ - Complex c(sample, sample); - Complex ci; - - if (m_feedbackInterpolatorDistance < 1.0f) // interpolate - { - while (!m_feedbackInterpolator.interpolate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) - { - processOneSample(ci); - m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; - } - } - else // decimate - { - if (m_feedbackInterpolator.decimate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) - { - processOneSample(ci); - m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; - } - } -} - -void AMMod::processOneSample(Complex& ci) -{ - m_feedbackAudioBuffer[m_feedbackAudioBufferFill].l = ci.real(); - m_feedbackAudioBuffer[m_feedbackAudioBufferFill].r = ci.imag(); - ++m_feedbackAudioBufferFill; - - if (m_feedbackAudioBufferFill >= m_feedbackAudioBuffer.size()) - { - uint res = m_feedbackAudioFifo.write((const quint8*)&m_feedbackAudioBuffer[0], m_feedbackAudioBufferFill); - - if (res != m_feedbackAudioBufferFill) - { - qDebug("AMDemod::pushFeedback: %u/%u audio samples written m_feedbackInterpolatorDistance: %f", - res, m_feedbackAudioBufferFill, m_feedbackInterpolatorDistance); - m_feedbackAudioFifo.clear(); - } - - m_feedbackAudioBufferFill = 0; - } -} - -void AMMod::calculateLevel(Real& sample) -{ - if (m_levelCalcCount < m_levelNbSamples) - { - m_peakLevel = std::max(std::fabs(m_peakLevel), sample); - m_levelSum += sample * sample; - m_levelCalcCount++; - } - else - { - qreal rmsLevel = sqrt(m_levelSum / m_levelNbSamples); - //qDebug("NFMMod::calculateLevel: %f %f", rmsLevel, m_peakLevel); - emit levelChanged(rmsLevel, m_peakLevel, m_levelNbSamples); - m_peakLevel = 0.0f; - m_levelSum = 0.0f; - m_levelCalcCount = 0; - } -} - void AMMod::start() { - qDebug() << "AMMod::start: m_outputSampleRate: " << m_outputSampleRate - << " m_inputFrequencyOffset: " << m_settings.m_inputFrequencyOffset; - - m_audioFifo.clear(); - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); + qDebug("AMMod::start"); + m_basebandSource->reset(); + m_thread->start(); } void AMMod::stop() { + qDebug("AMMod::stop"); + m_thread->exit(); + m_thread->wait(); +} + +void AMMod::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); } bool AMMod::handleMessage(const Message& cmd) { - if (UpChannelizer::MsgChannelizerNotification::match(cmd)) - { - UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd; - qDebug() << "AMMod::handleMessage: MsgChannelizerNotification:" - << " basebandSampleRate: " << notif.getBasebandSampleRate() - << " outputSampleRate: " << notif.getSampleRate() - << " inputFrequencyOffset: " << notif.getFrequencyOffset(); - - applyChannelSettings(notif.getBasebandSampleRate(), notif.getSampleRate(), notif.getFrequencyOffset()); - - return true; - } - else if (MsgConfigureChannelizer::match(cmd)) + if (MsgConfigureChannelizer::match(cmd)) { MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; qDebug() << "AMMod::handleMessage: MsgConfigureChannelizer:" - << " getSampleRate: " << cfg.getSampleRate() - << " getCenterFrequency: " << cfg.getCenterFrequency(); + << " getSourceSampleRate: " << cfg.getSourceSampleRate() + << " getSourceCenterFrequency: " << cfg.getSourceCenterFrequency(); - m_channelizer->configure(m_channelizer->getInputMessageQueue(), - cfg.getSampleRate(), - cfg.getCenterFrequency()); + AMModBaseband::MsgConfigureChannelizer *msg + = AMModBaseband::MsgConfigureChannelizer::create(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_basebandSource->getInputMessageQueue()->push(msg); return true; } @@ -376,6 +137,7 @@ bool AMMod::handleMessage(const Message& cmd) { MsgConfigureFileSourceName& conf = (MsgConfigureFileSourceName&) cmd; m_fileName = conf.getFileName(); + qDebug() << "AMMod::handleMessage: MsgConfigureFileSourceName"; openFileStream(); return true; } @@ -383,6 +145,7 @@ bool AMMod::handleMessage(const Message& cmd) { MsgConfigureFileSourceSeek& conf = (MsgConfigureFileSourceSeek&) cmd; int seekPercentage = conf.getPercentage(); + qDebug() << "AMMod::handleMessage: MsgConfigureFileSourceSeek"; seekFileStream(seekPercentage); return true; @@ -406,6 +169,7 @@ bool AMMod::handleMessage(const Message& cmd) else if (CWKeyer::MsgConfigureCWKeyer::match(cmd)) { const CWKeyer::MsgConfigureCWKeyer& cfg = (CWKeyer::MsgConfigureCWKeyer&) cmd; + qDebug() << "AMMod::handleMessage: MsgConfigureCWKeyer"; if (m_settings.m_useReverseAPI) { webapiReverseSendCWSettings(cfg.getSettings()); @@ -413,33 +177,14 @@ bool AMMod::handleMessage(const Message& cmd) return true; } - else if (DSPConfigureAudio::match(cmd)) - { - DSPConfigureAudio& cfg = (DSPConfigureAudio&) cmd; - uint32_t sampleRate = cfg.getSampleRate(); - DSPConfigureAudio::AudioType audioType = cfg.getAudioType(); - - qDebug() << "AMMod::handleMessage: DSPConfigureAudio:" - << " sampleRate: " << sampleRate - << " audioType: " << audioType; - - if (audioType == DSPConfigureAudio::AudioInput) - { - if (sampleRate != m_audioSampleRate) { - applyAudioSampleRate(sampleRate); - } - } - else if (audioType == DSPConfigureAudio::AudioOutput) - { - if (sampleRate != m_audioSampleRate) { - applyFeedbackAudioSampleRate(sampleRate); - } - } - - return true; - } else if (DSPSignalNotification::match(cmd)) { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "AMMod::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + return true; } else @@ -483,76 +228,6 @@ void AMMod::seekFileStream(int seekPercentage) } } -void AMMod::applyAudioSampleRate(int sampleRate) -{ - qDebug("AMMod::applyAudioSampleRate: %d", sampleRate); - - MsgConfigureChannelizer* channelConfigMsg = MsgConfigureChannelizer::create( - sampleRate, m_settings.m_inputFrequencyOffset); - m_inputMessageQueue.push(channelConfigMsg); - - m_settingsMutex.lock(); - - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) sampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, sampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); - m_toneNco.setFreq(m_settings.m_toneFrequency, sampleRate); - m_cwKeyer.setSampleRate(sampleRate); - - m_settingsMutex.unlock(); - - m_audioSampleRate = sampleRate; - applyFeedbackAudioSampleRate(m_feedbackAudioSampleRate); -} - -void AMMod::applyFeedbackAudioSampleRate(unsigned int sampleRate) -{ - qDebug("AMMod::applyFeedbackAudioSampleRate: %u", sampleRate); - - m_settingsMutex.lock(); - - m_feedbackInterpolatorDistanceRemain = 0; - m_feedbackInterpolatorConsumed = false; - m_feedbackInterpolatorDistance = (Real) sampleRate / (Real) m_audioSampleRate; - Real cutoff = std::min(sampleRate, m_audioSampleRate) / 2.2f; - m_feedbackInterpolator.create(48, sampleRate, cutoff, 3.0); - - m_settingsMutex.unlock(); - - m_feedbackAudioSampleRate = sampleRate; -} - -void AMMod::applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force) -{ - qDebug() << "AMMod::applyChannelSettings:" - << " basebandSampleRate: " << basebandSampleRate - << " outputSampleRate: " << outputSampleRate - << " inputFrequencyOffset: " << inputFrequencyOffset; - - if ((inputFrequencyOffset != m_inputFrequencyOffset) || - (outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_carrierNco.setFreq(inputFrequencyOffset, outputSampleRate); - m_settingsMutex.unlock(); - } - - if ((outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_audioSampleRate / (Real) outputSampleRate; - m_interpolator.create(48, m_audioSampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); - m_settingsMutex.unlock(); - } - - m_basebandSampleRate = basebandSampleRate; - m_outputSampleRate = outputSampleRate; - m_inputFrequencyOffset = inputFrequencyOffset; -} - void AMMod::applySettings(const AMModSettings& settings, bool force) { qDebug() << "AMMod::applySettings:" @@ -599,23 +274,12 @@ void AMMod::applySettings(const AMModSettings& settings, bool force) reverseAPIKeys.append("modAFInput"); } - if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) - { + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { reverseAPIKeys.append("rfBandwidth"); - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_audioSampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, m_audioSampleRate, settings.m_rfBandwidth / 2.2, 3.0); - m_settingsMutex.unlock(); } - if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) - { + if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) { reverseAPIKeys.append("toneFrequency"); - m_settingsMutex.lock(); - m_toneNco.setFreq(settings.m_toneFrequency, m_audioSampleRate); - m_settingsMutex.unlock(); } if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force) @@ -623,12 +287,14 @@ void AMMod::applySettings(const AMModSettings& settings, bool force) reverseAPIKeys.append("audioDeviceName"); AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); int audioDeviceIndex = audioDeviceManager->getInputDeviceIndex(settings.m_audioDeviceName); - audioDeviceManager->addAudioSource(&m_audioFifo, getInputMessageQueue(), audioDeviceIndex); + audioDeviceManager->addAudioSource(m_basebandSource->getAudioFifo(), getInputMessageQueue(), audioDeviceIndex); uint32_t audioSampleRate = audioDeviceManager->getInputSampleRate(audioDeviceIndex); - if (m_audioSampleRate != audioSampleRate) { + if (m_basebandSource->getAudioSampleRate() != audioSampleRate) + { reverseAPIKeys.append("audioSampleRate"); - applyAudioSampleRate(audioSampleRate); + DSPConfigureAudio *msg = new DSPConfigureAudio(audioSampleRate, DSPConfigureAudio::AudioInput); + m_basebandSource->getInputMessageQueue()->push(msg); } } @@ -637,12 +303,14 @@ void AMMod::applySettings(const AMModSettings& settings, bool force) reverseAPIKeys.append("feedbackAudioDeviceName"); AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); int audioDeviceIndex = audioDeviceManager->getOutputDeviceIndex(settings.m_feedbackAudioDeviceName); - audioDeviceManager->addAudioSink(&m_feedbackAudioFifo, getInputMessageQueue(), audioDeviceIndex); + audioDeviceManager->addAudioSink(m_basebandSource->getFeedbackAudioFifo(), getInputMessageQueue(), audioDeviceIndex); uint32_t audioSampleRate = audioDeviceManager->getOutputSampleRate(audioDeviceIndex); - if (m_feedbackAudioSampleRate != audioSampleRate) { + if (m_basebandSource->getFeedbackAudioSampleRate() != audioSampleRate) + { reverseAPIKeys.append("feedbackAudioSampleRate"); - applyFeedbackAudioSampleRate(audioSampleRate); + DSPConfigureAudio *msg = new DSPConfigureAudio(audioSampleRate, DSPConfigureAudio::AudioOutput); + m_basebandSource->getInputMessageQueue()->push(msg); } } @@ -651,20 +319,17 @@ void AMMod::applySettings(const AMModSettings& settings, bool force) if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only { m_deviceAPI->removeChannelSourceAPI(this, m_settings.m_streamIndex); - m_deviceAPI->removeChannelSource(m_threadedChannelizer, m_settings.m_streamIndex); - m_deviceAPI->addChannelSource(m_threadedChannelizer, settings.m_streamIndex); + m_deviceAPI->removeChannelSource(this, m_settings.m_streamIndex); + m_deviceAPI->addChannelSource(this, settings.m_streamIndex); m_deviceAPI->addChannelSourceAPI(this, settings.m_streamIndex); - // apply stream sample rate to itself - applyChannelSettings( - m_basebandSampleRate, - m_deviceAPI->getSampleMIMO()->getSinkSampleRate(settings.m_streamIndex), - m_inputFrequencyOffset - ); } reverseAPIKeys.append("streamIndex"); } + AMModBaseband::MsgConfigureAMModBaseband *msg = AMModBaseband::MsgConfigureAMModBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); + if (settings.m_useReverseAPI) { bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || @@ -710,7 +375,7 @@ int AMMod::webapiSettingsGet( webapiFormatChannelSettings(response, m_settings); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = response.getAmModSettings()->getCwKeyer(); - const CWKeyerSettings& cwKeyerSettings = m_cwKeyer.getSettings(); + const CWKeyerSettings& cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); CWKeyer::webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); return 200; @@ -729,11 +394,11 @@ int AMMod::webapiSettingsPutPatch( if (channelSettingsKeys.contains("cwKeyer")) { SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = response.getAmModSettings()->getCwKeyer(); - CWKeyerSettings cwKeyerSettings = m_cwKeyer.getSettings(); + CWKeyerSettings cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); CWKeyer::webapiSettingsPutPatch(channelSettingsKeys, cwKeyerSettings, apiCwKeyerSettings); CWKeyer::MsgConfigureCWKeyer *msgCwKeyer = CWKeyer::MsgConfigureCWKeyer::create(cwKeyerSettings, force); - m_cwKeyer.getInputMessageQueue()->push(msgCwKeyer); + m_basebandSource->getCWKeyer().getInputMessageQueue()->push(msgCwKeyer); if (m_guiMessageQueue) // forward to GUI if any { @@ -744,8 +409,8 @@ int AMMod::webapiSettingsPutPatch( if (m_settings.m_inputFrequencyOffset != settings.m_inputFrequencyOffset) { - AMMod::MsgConfigureChannelizer *msgChan = AMMod::MsgConfigureChannelizer::create( - m_audioSampleRate, settings.m_inputFrequencyOffset); + AMModBaseband::MsgConfigureChannelizer *msgChan = AMModBaseband::MsgConfigureChannelizer::create( + m_basebandSource->getAudioSampleRate(), settings.m_inputFrequencyOffset); m_inputMessageQueue.push(msgChan); } @@ -874,8 +539,8 @@ void AMMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& respons void AMMod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) { response.getAmModReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); - response.getAmModReport()->setAudioSampleRate(m_audioSampleRate); - response.getAmModReport()->setChannelSampleRate(m_outputSampleRate); + response.getAmModReport()->setAudioSampleRate(m_basebandSource->getAudioSampleRate()); + response.getAmModReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); } void AMMod::webapiReverseSendSettings(QList& channelSettingsKeys, const AMModSettings& settings, bool force) @@ -926,10 +591,10 @@ void AMMod::webapiReverseSendSettings(QList& channelSettingsKeys, const if (force) { - const CWKeyerSettings& cwKeyerSettings = m_cwKeyer.getSettings(); + const CWKeyerSettings& cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); swgAMModSettings->setCwKeyer(new SWGSDRangel::SWGCWKeyerSettings()); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = swgAMModSettings->getCwKeyer(); - m_cwKeyer.webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); + m_basebandSource->getCWKeyer().webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); } QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") @@ -940,13 +605,14 @@ void AMMod::webapiReverseSendSettings(QList& channelSettingsKeys, const m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -961,7 +627,7 @@ void AMMod::webapiReverseSendCWSettings(const CWKeyerSettings& cwKeyerSettings) swgAMModSettings->setCwKeyer(new SWGSDRangel::SWGCWKeyerSettings()); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = swgAMModSettings->getCwKeyer(); - m_cwKeyer.webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); + m_basebandSource->getCWKeyer().webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") .arg(m_settings.m_reverseAPIAddress) @@ -971,13 +637,14 @@ void AMMod::webapiReverseSendCWSettings(const CWKeyerSettings& cwKeyerSettings) m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -992,10 +659,28 @@ void AMMod::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("AMMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("AMMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } + +double AMMod::getMagSq() const +{ + return m_basebandSource->getMagSq(); +} + +CWKeyer *AMMod::getCWKeyer() +{ + return &m_basebandSource->getCWKeyer(); +} + +void AMMod::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); +} \ No newline at end of file diff --git a/plugins/channeltx/modam/ammod.h b/plugins/channeltx/modam/ammod.h index d83f0441c..b0e60c28a 100644 --- a/plugins/channeltx/modam/ammod.h +++ b/plugins/channeltx/modam/ammod.h @@ -27,22 +27,16 @@ #include "dsp/basebandsamplesource.h" #include "channel/channelapi.h" -#include "dsp/nco.h" -#include "dsp/ncof.h" -#include "dsp/interpolator.h" -#include "util/movingaverage.h" -#include "dsp/agc.h" -#include "dsp/cwkeyer.h" -#include "audio/audiofifo.h" #include "util/message.h" #include "ammodsettings.h" class QNetworkAccessManager; class QNetworkReply; -class ThreadedBasebandSampleSource; -class UpChannelizer; +class QThread; +class AMModBaseband; class DeviceAPI; +class CWKeyer; class AMMod : public BasebandSampleSource, public ChannelAPI { Q_OBJECT @@ -71,26 +65,33 @@ public: { } }; + /** + * |<------ Baseband from device (before device soft interpolation) -------------------------->| + * |<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->| + * | ^-------------------------------| + * | | Source CF + * | | Source SR | + */ class MsgConfigureChannelizer : public Message { MESSAGE_CLASS_DECLARATION public: - int getSampleRate() const { return m_sampleRate; } - int getCenterFrequency() const { return m_centerFrequency; } + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } - static MsgConfigureChannelizer* create(int sampleRate, int centerFrequency) + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) { - return new MsgConfigureChannelizer(sampleRate, centerFrequency); + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); } private: - int m_sampleRate; - int m_centerFrequency; + int m_sourceSampleRate; + int m_sourceCenterFrequency; - MsgConfigureChannelizer(int sampleRate, int centerFrequency) : + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : Message(), - m_sampleRate(sampleRate), - m_centerFrequency(centerFrequency) + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) { } }; @@ -205,10 +206,9 @@ public: ~AMMod(); virtual void destroy() { delete this; } - virtual void pull(Sample& sample); - virtual void pullAudio(int nbSamples); virtual void start(); virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); virtual bool handleMessage(const Message& cmd); virtual void getIdentifier(QString& id) { id = objectName(); } @@ -251,24 +251,14 @@ public: const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response); - double getMagSq() const { return m_magsq; } uint32_t getNumberOfDeviceStreams() const; - - CWKeyer *getCWKeyer() { return &m_cwKeyer; } + double getMagSq() const; + CWKeyer *getCWKeyer(); + void setLevelMeter(QObject *levelMeter); static const QString m_channelIdURI; static const QString m_channelId; -signals: - /** - * Level changed - * \param rmsLevel RMS level in range 0.0 - 1.0 - * \param peakLevel Peak level in range 0.0 - 1.0 - * \param numSamples Number of audio samples analyzed - */ - void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); - - private: enum RateState { RSInitialFill, @@ -276,41 +266,10 @@ private: }; DeviceAPI* m_deviceAPI; - ThreadedBasebandSampleSource* m_threadedChannelizer; - UpChannelizer* m_channelizer; - - int m_basebandSampleRate; - int m_outputSampleRate; - int m_inputFrequencyOffset; + QThread *m_thread; + AMModBaseband* m_basebandSource; AMModSettings m_settings; - NCO m_carrierNco; - NCOF m_toneNco; - Complex m_modSample; - - Interpolator m_interpolator; - Real m_interpolatorDistance; - Real m_interpolatorDistanceRemain; - bool m_interpolatorConsumed; - - Interpolator m_feedbackInterpolator; - Real m_feedbackInterpolatorDistance; - Real m_feedbackInterpolatorDistanceRemain; - bool m_feedbackInterpolatorConsumed; - - double m_magsq; - MovingAverageUtil m_movingAverage; - - quint32 m_audioSampleRate; - AudioVector m_audioBuffer; - uint m_audioBufferFill; - AudioFifo m_audioFifo; - - quint32 m_feedbackAudioSampleRate; - AudioVector m_feedbackAudioBuffer; - uint m_feedbackAudioBufferFill; - AudioFifo m_feedbackAudioFifo; - SampleVector m_sampleBuffer; QMutex m_settingsMutex; @@ -320,25 +279,10 @@ private: quint32 m_recordLength; //!< record length in seconds computed from file size int m_sampleRate; - quint32 m_levelCalcCount; - Real m_peakLevel; - Real m_levelSum; - CWKeyer m_cwKeyer; - - static const int m_levelNbSamples; - QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; - void applyAudioSampleRate(int sampleRate); - void applyFeedbackAudioSampleRate(unsigned int sampleRate); - void processOneSample(Complex& ci); - void applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force = false); void applySettings(const AMModSettings& settings, bool force = false); - void pullAF(Real& sample); - void pushFeedback(Real sample); - void calculateLevel(Real& sample); - void modulateSample(); void openFileStream(); void seekFileStream(int seekPercentage); void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); diff --git a/plugins/channeltx/modam/ammodbaseband.cpp b/plugins/channeltx/modam/ammodbaseband.cpp new file mode 100644 index 000000000..1a3b6c458 --- /dev/null +++ b/plugins/channeltx/modam/ammodbaseband.cpp @@ -0,0 +1,229 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "ammodbaseband.h" + +MESSAGE_CLASS_DEFINITION(AMModBaseband::MsgConfigureAMModBaseband, Message) +MESSAGE_CLASS_DEFINITION(AMModBaseband::MsgConfigureChannelizer, Message) + +AMModBaseband::AMModBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpSampleChannelizer(&m_source); + + qDebug("AMModBaseband::AMModBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &AMModBaseband::handleData, + Qt::QueuedConnection + ); + + DSPEngine::instance()->getAudioDeviceManager()->addAudioSource(m_source.getAudioFifo(), getInputMessageQueue()); + m_source.applyAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getInputSampleRate()); + + DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(m_source.getFeedbackAudioFifo(), getInputMessageQueue()); + m_source.applyFeedbackAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getOutputSampleRate()); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +AMModBaseband::~AMModBaseband() +{ + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(m_source.getFeedbackAudioFifo()); + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSource(m_source.getAudioFifo()); + delete m_channelizer; +} + +void AMModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void AMModBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void AMModBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + Real rmsLevel, peakLevel, numSamples; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); +} + +void AMModBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void AMModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool AMModBaseband::handleMessage(const Message& cmd) +{ + if (DSPConfigureAudio::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPConfigureAudio& cfg = (DSPConfigureAudio&) cmd; + uint32_t sampleRate = cfg.getSampleRate(); + DSPConfigureAudio::AudioType audioType = cfg.getAudioType(); + + qDebug() << "AMModBaseband::handleMessage: DSPConfigureAudio:" + << " sampleRate: " << sampleRate + << " audioType: " << audioType; + + if (audioType == DSPConfigureAudio::AudioInput) + { + if (sampleRate != m_source.getAudioSampleRate()) { + m_source.applyAudioSampleRate(sampleRate); + } + } + else if (audioType == DSPConfigureAudio::AudioOutput) + { + if (sampleRate != m_source.getFeedbackAudioSampleRate()) { + m_source.applyFeedbackAudioSampleRate(sampleRate); + } + } + + return true; + } + else if (MsgConfigureAMModBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureAMModBaseband& cfg = (MsgConfigureAMModBaseband&) cmd; + qDebug() << "AMModBaseband::handleMessage: MsgConfigureAMModBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureChannelizer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; + qDebug() << "AMModBaseband::handleMessage: MsgConfigureChannelizer" + << "(requested) sourceSampleRate: " << cfg.getSourceSampleRate() + << "(requested) sourceCenterFrequency: " << cfg.getSourceCenterFrequency(); + m_channelizer->setChannelization(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "AMModBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (CWKeyer::MsgConfigureCWKeyer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + qDebug() << "AMModBaseband::handleMessage: MsgConfigureCWKeyer"; + const CWKeyer::MsgConfigureCWKeyer& cfg = (CWKeyer::MsgConfigureCWKeyer&) cmd; + CWKeyer::MsgConfigureCWKeyer *notif = new CWKeyer::MsgConfigureCWKeyer(cfg); + CWKeyer& cwKeyer = m_source.getCWKeyer(); + cwKeyer.getInputMessageQueue()->push(notif); + + return true; + } + else + { + return false; + } +} + +void AMModBaseband::applySettings(const AMModSettings& settings, bool force) +{ + m_source.applySettings(settings, force); + m_settings = settings; +} + +int AMModBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} \ No newline at end of file diff --git a/plugins/channeltx/modam/ammodbaseband.h b/plugins/channeltx/modam/ammodbaseband.h new file mode 100644 index 000000000..9a391cb39 --- /dev/null +++ b/plugins/channeltx/modam/ammodbaseband.h @@ -0,0 +1,123 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AMMODBASEBAND_H +#define INCLUDE_AMMODBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "ammodsource.h" + +class UpSampleChannelizer; + +class AMModBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureAMModBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const AMModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAMModBaseband* create(const AMModSettings& settings, bool force) + { + return new MsgConfigureAMModBaseband(settings, force); + } + + private: + AMModSettings m_settings; + bool m_force; + + MsgConfigureAMModBaseband(const AMModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureChannelizer : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } + + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) + { + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); + } + + private: + int m_sourceSampleRate; + int m_sourceCenterFrequency; + + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : + Message(), + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) + { } + }; + + AMModBaseband(); + ~AMModBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + CWKeyer& getCWKeyer() { return m_source.getCWKeyer(); } + double getMagSq() const { return m_source.getMagSq(); } + unsigned int getAudioSampleRate() const { return m_source.getAudioSampleRate(); } + unsigned int getFeedbackAudioSampleRate() const { return m_source.getFeedbackAudioSampleRate(); } + int getChannelSampleRate() const; + void setInputFileStream(std::ifstream *ifstream) { m_source.setInputFileStream(ifstream); } + AudioFifo *getAudioFifo() { return m_source.getAudioFifo(); } + AudioFifo *getFeedbackAudioFifo() { return m_source.getFeedbackAudioFifo(); } + +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + SampleSourceFifo m_sampleFifo; + UpSampleChannelizer *m_channelizer; + AMModSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + AMModSettings m_settings; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const AMModSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + + +#endif // INCLUDE_AMMODBASEBAND_H diff --git a/plugins/channeltx/modam/ammodgui.cpp b/plugins/channeltx/modam/ammodgui.cpp index 440cadc99..13b9981d4 100644 --- a/plugins/channeltx/modam/ammodgui.cpp +++ b/plugins/channeltx/modam/ammodgui.cpp @@ -22,12 +22,12 @@ #include #include "device/deviceuiset.h" -#include "dsp/upchannelizer.h" #include "plugin/pluginapi.h" #include "util/simpleserializer.h" #include "util/db.h" #include "dsp/dspengine.h" +#include "dsp/cwkeyer.h" #include "gui/crightclickenabler.h" #include "gui/audioselectdialog.h" #include "gui/basicchannelsettingsdialog.h" @@ -396,7 +396,7 @@ AMModGUI::AMModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampl ui->cwKeyerGUI->setCWKeyer(m_amMod->getCWKeyer()); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); - connect(m_amMod, SIGNAL(levelChanged(qreal, qreal, int)), ui->volumeMeter, SLOT(levelChanged(qreal, qreal, int))); + m_amMod->setLevelMeter(ui->volumeMeter); displaySettings(); applySettings(true); diff --git a/plugins/channeltx/modam/ammodplugin.cpp b/plugins/channeltx/modam/ammodplugin.cpp index 7f73a29df..94326f4ef 100644 --- a/plugins/channeltx/modam/ammodplugin.cpp +++ b/plugins/channeltx/modam/ammodplugin.cpp @@ -27,7 +27,7 @@ const PluginDescriptor AMModPlugin::m_pluginDescriptor = { QString("AM Modulator"), - QString("4.11.6"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/channeltx/modam/ammodsource.cpp b/plugins/channeltx/modam/ammodsource.cpp new file mode 100644 index 000000000..7d0610887 --- /dev/null +++ b/plugins/channeltx/modam/ammodsource.cpp @@ -0,0 +1,330 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "ammodsource.h" + +const int AMModSource::m_levelNbSamples = 480; // every 10ms + +AMModSource::AMModSource() : + m_channelSampleRate(48000), + m_channelFrequencyOffset(0), + m_audioFifo(4800), + m_feedbackAudioFifo(48000), + m_levelCalcCount(0), + m_peakLevel(0.0f), + m_levelSum(0.0f), + m_ifstream(nullptr), + m_audioSampleRate(48000) +{ + m_audioBuffer.resize(1<<14); + m_audioBufferFill = 0; + + m_feedbackAudioBuffer.resize(1<<14); + m_feedbackAudioBufferFill = 0; + + m_magsq = 0.0; + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +AMModSource::~AMModSource() +{ +} + +void AMModSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void AMModSource::pullOne(Sample& sample) +{ + if (m_settings.m_channelMute) + { + sample.m_real = 0.0f; + sample.m_imag = 0.0f; + return; + } + + Complex ci; + + if (m_interpolatorDistance > 1.0f) // decimate + { + modulateSample(); + + while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + else + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + + m_interpolatorDistanceRemain += m_interpolatorDistance; + + ci *= m_carrierNco.nextIQ(); // shift to carrier frequency + double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); + magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + + sample.m_real = (FixReal) ci.real(); + sample.m_imag = (FixReal) ci.imag(); +} + +void AMModSource::prefetch(unsigned int nbSamples) +{ + unsigned int nbSamplesAudio = nbSamples * ((Real) m_audioSampleRate / (Real) m_channelSampleRate); + pullAudio(nbSamplesAudio); +} + +void AMModSource::pullAudio(unsigned int nbSamples) +{ + + if (nbSamples > m_audioBuffer.size()) { + m_audioBuffer.resize(nbSamples); + } + + m_audioFifo.read(reinterpret_cast(&m_audioBuffer[0]), nbSamples); + m_audioBufferFill = 0; +} + +void AMModSource::modulateSample() +{ + Real t; + + pullAF(t); + + if (m_settings.m_feedbackAudioEnable) { + pushFeedback(t * m_settings.m_feedbackVolumeFactor * 16384.0f); + } + + calculateLevel(t); + m_audioBufferFill++; + + m_modSample.real((t*m_settings.m_modFactor + 1.0f) * 16384.0f); // modulate and scale zero frequency carrier + m_modSample.imag(0.0f); +} + +void AMModSource::pullAF(Real& sample) +{ + switch (m_settings.m_modAFInput) + { + case AMModSettings::AMModInputTone: + sample = m_toneNco.next(); + break; + case AMModSettings::AMModInputFile: + // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw + // ffplay -f f32le -ar 48k -ac 1 f4exb_call.raw + if (m_ifstream && m_ifstream->is_open()) + { + if (m_ifstream->eof()) + { + if (m_settings.m_playLoop) + { + m_ifstream->clear(); + m_ifstream->seekg(0, std::ios::beg); + } + } + + if (m_ifstream->eof()) + { + sample = 0.0f; + } + else + { + m_ifstream->read(reinterpret_cast(&sample), sizeof(Real)); + sample *= m_settings.m_volumeFactor; + } + } + else + { + sample = 0.0f; + } + break; + case AMModSettings::AMModInputAudio: + sample = ((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f) * m_settings.m_volumeFactor; + break; + case AMModSettings::AMModInputCWTone: + Real fadeFactor; + + if (m_cwKeyer.getSample()) + { + m_cwKeyer.getCWSmoother().getFadeSample(true, fadeFactor); + sample = m_toneNco.next() * fadeFactor; + } + else + { + if (m_cwKeyer.getCWSmoother().getFadeSample(false, fadeFactor)) + { + sample = m_toneNco.next() * fadeFactor; + } + else + { + sample = 0.0f; + m_toneNco.setPhase(0); + } + } + break; + case AMModSettings::AMModInputNone: + default: + sample = 0.0f; + break; + } +} + +void AMModSource::pushFeedback(Real sample) +{ + Complex c(sample, sample); + Complex ci; + + if (m_feedbackInterpolatorDistance < 1.0f) // interpolate + { + while (!m_feedbackInterpolator.interpolate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; + } + } + else // decimate + { + if (m_feedbackInterpolator.decimate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; + } + } +} + +void AMModSource::processOneSample(Complex& ci) +{ + m_feedbackAudioBuffer[m_feedbackAudioBufferFill].l = ci.real(); + m_feedbackAudioBuffer[m_feedbackAudioBufferFill].r = ci.imag(); + ++m_feedbackAudioBufferFill; + + if (m_feedbackAudioBufferFill >= m_feedbackAudioBuffer.size()) + { + uint res = m_feedbackAudioFifo.write((const quint8*)&m_feedbackAudioBuffer[0], m_feedbackAudioBufferFill); + + if (res != m_feedbackAudioBufferFill) + { + qDebug("AMModChannelSource::pushFeedback: %u/%u audio samples written m_feedbackInterpolatorDistance: %f", + res, m_feedbackAudioBufferFill, m_feedbackInterpolatorDistance); + m_feedbackAudioFifo.clear(); + } + + m_feedbackAudioBufferFill = 0; + } +} + +void AMModSource::calculateLevel(Real& sample) +{ + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), sample); + m_levelSum += sample * sample; + m_levelCalcCount++; + } + else + { + m_rmsLevel = sqrt(m_levelSum / m_levelNbSamples); + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void AMModSource::applyAudioSampleRate(unsigned int sampleRate) +{ + qDebug("AMModSource::applyAudioSampleRate: %d", sampleRate); + + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) sampleRate / (Real) m_channelSampleRate; + m_interpolator.create(48, sampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); + m_toneNco.setFreq(m_settings.m_toneFrequency, sampleRate); + m_cwKeyer.setSampleRate(sampleRate); + m_cwKeyer.reset(); + m_audioSampleRate = sampleRate; + applyFeedbackAudioSampleRate(m_feedbackAudioSampleRate); +} + +void AMModSource::applyFeedbackAudioSampleRate(unsigned int sampleRate) +{ + qDebug("AMModSource::applyFeedbackAudioSampleRate: %u", sampleRate); + + m_feedbackInterpolatorDistanceRemain = 0; + m_feedbackInterpolatorConsumed = false; + m_feedbackInterpolatorDistance = (Real) sampleRate / (Real) m_audioSampleRate; + Real cutoff = std::min(sampleRate, m_audioSampleRate) / 2.2f; + m_feedbackInterpolator.create(48, sampleRate, cutoff, 3.0); + m_feedbackAudioSampleRate = sampleRate; +} + +void AMModSource::applySettings(const AMModSettings& settings, bool force) +{ + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) + { + m_settings.m_rfBandwidth = settings.m_rfBandwidth; + applyAudioSampleRate(m_audioSampleRate); + } + + if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) + { + m_toneNco.setFreq(settings.m_toneFrequency, m_audioSampleRate); + } + + m_settings = settings; +} + +void AMModSource::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "AMModSource::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((channelFrequencyOffset != m_channelFrequencyOffset) + || (channelSampleRate != m_channelSampleRate) || force) + { + m_carrierNco.setFreq(channelFrequencyOffset, channelSampleRate); + } + + if ((channelSampleRate != m_channelSampleRate) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_audioSampleRate / (Real) channelSampleRate; + m_interpolator.create(48, m_audioSampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} diff --git a/plugins/channeltx/modam/ammodsource.h b/plugins/channeltx/modam/ammodsource.h new file mode 100644 index 000000000..bf0f5472d --- /dev/null +++ b/plugins/channeltx/modam/ammodsource.h @@ -0,0 +1,117 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AMMODSOURCE_H +#define INCLUDE_AMMODSOURCE_H + +#include + +#include +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/nco.h" +#include "dsp/ncof.h" +#include "dsp/interpolator.h" +#include "util/movingaverage.h" +#include "dsp/cwkeyer.h" +#include "audio/audiofifo.h" + +#include "ammodsettings.h" + +class AMModSource : public ChannelSampleSource +{ +public: + AMModSource(); + virtual ~AMModSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples); + + void setInputFileStream(std::ifstream *ifstream) { m_ifstream = ifstream; } + AudioFifo *getAudioFifo() { return &m_audioFifo; } + AudioFifo *getFeedbackAudioFifo() { return &m_feedbackAudioFifo; } + void applyAudioSampleRate(unsigned int sampleRate); + void applyFeedbackAudioSampleRate(unsigned int sampleRate); + unsigned int getAudioSampleRate() const { return m_audioSampleRate; } + unsigned int getFeedbackAudioSampleRate() const { return m_feedbackAudioSampleRate; } + CWKeyer& getCWKeyer() { return m_cwKeyer; } + double getMagSq() const { return m_magsq; } + void getLevels(Real& rmsLevel, Real& peakLevel, Real& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevel; + numSamples = m_levelNbSamples; + } + void applySettings(const AMModSettings& settings, bool force = false); + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + +private: + int m_channelSampleRate; + int m_channelFrequencyOffset; + AMModSettings m_settings; + + NCO m_carrierNco; + NCOF m_toneNco; + Complex m_modSample; + + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + bool m_interpolatorConsumed; + + Interpolator m_feedbackInterpolator; + Real m_feedbackInterpolatorDistance; + Real m_feedbackInterpolatorDistanceRemain; + bool m_feedbackInterpolatorConsumed; + + double m_magsq; + MovingAverageUtil m_movingAverage; + + quint32 m_audioSampleRate; + AudioVector m_audioBuffer; + uint m_audioBufferFill; + AudioFifo m_audioFifo; + + quint32 m_feedbackAudioSampleRate; + AudioVector m_feedbackAudioBuffer; + uint m_feedbackAudioBufferFill; + AudioFifo m_feedbackAudioFifo; + + quint32 m_levelCalcCount; + Real m_rmsLevel; + Real m_peakLevelOut; + Real m_peakLevel; + Real m_levelSum; + + std::ifstream *m_ifstream; + CWKeyer m_cwKeyer; + + static const int m_levelNbSamples; + + void processOneSample(Complex& ci); + void pullAF(Real& sample); + void pullAudio(unsigned int nbSamples); + void pushFeedback(Real sample); + void calculateLevel(Real& sample); + void modulateSample(); +}; + + + +#endif // INCLUDE_AMMODSOURCE_H diff --git a/plugins/channeltx/modam/ammodwebapiadapter.cpp b/plugins/channeltx/modam/ammodwebapiadapter.cpp index 0a954cb85..e6a6a8443 100644 --- a/plugins/channeltx/modam/ammodwebapiadapter.cpp +++ b/plugins/channeltx/modam/ammodwebapiadapter.cpp @@ -16,6 +16,7 @@ /////////////////////////////////////////////////////////////////////////////////// #include "SWGChannelSettings.h" +#include "dsp/cwkeyer.h" #include "ammod.h" #include "ammodwebapiadapter.h" diff --git a/plugins/channeltx/modatv/CMakeLists.txt b/plugins/channeltx/modatv/CMakeLists.txt index 0c1eab90e..876314997 100644 --- a/plugins/channeltx/modatv/CMakeLists.txt +++ b/plugins/channeltx/modatv/CMakeLists.txt @@ -1,7 +1,10 @@ project(modatv) set(modatv_SOURCES - atvmod.cpp + atvmod.cpp + atvmodbaseband.cpp + atvmodreport.cpp + atvmodsource.cpp atvmodplugin.cpp atvmodsettings.cpp atvmodwebapiadapter.cpp @@ -9,6 +12,9 @@ set(modatv_SOURCES set(modatv_HEADERS atvmod.h + atvmodbaseband.h + atvmodreport.h + atvmodsource.h atvmodplugin.h atvmodsettings.h atvmodwebapiadapter.h diff --git a/plugins/channeltx/modatv/atvmod.cpp b/plugins/channeltx/modatv/atvmod.cpp index 8892fcc21..846079aca 100644 --- a/plugins/channeltx/modatv/atvmod.cpp +++ b/plugins/channeltx/modatv/atvmod.cpp @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2017 Edouard Griffiths, F4EXB // +// Copyright (C) 2016 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // @@ -15,95 +15,56 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// -#include +#include +#include +#include #include +#include #include #include #include +#include #include "SWGChannelSettings.h" #include "SWGChannelReport.h" #include "SWGATVModReport.h" -#include "opencv2/imgproc/imgproc.hpp" - -#include "dsp/upchannelizer.h" -#include "dsp/threadedbasebandsamplesource.h" +#include "dsp/dspengine.h" #include "dsp/dspcommands.h" +#include "dsp/devicesamplemimo.h" #include "device/deviceapi.h" #include "util/db.h" +#include "atvmodbaseband.h" #include "atvmod.h" MESSAGE_CLASS_DEFINITION(ATVMod::MsgConfigureATVMod, Message) MESSAGE_CLASS_DEFINITION(ATVMod::MsgConfigureChannelizer, Message) +MESSAGE_CLASS_DEFINITION(ATVMod::MsgConfigureSourceCenterFrequency, Message) MESSAGE_CLASS_DEFINITION(ATVMod::MsgConfigureImageFileName, Message) MESSAGE_CLASS_DEFINITION(ATVMod::MsgConfigureVideoFileName, Message) MESSAGE_CLASS_DEFINITION(ATVMod::MsgConfigureVideoFileSourceSeek, Message) MESSAGE_CLASS_DEFINITION(ATVMod::MsgConfigureVideoFileSourceStreamTiming, Message) -MESSAGE_CLASS_DEFINITION(ATVMod::MsgReportVideoFileSourceStreamTiming, Message) -MESSAGE_CLASS_DEFINITION(ATVMod::MsgReportVideoFileSourceStreamData, Message) MESSAGE_CLASS_DEFINITION(ATVMod::MsgConfigureCameraIndex, Message) MESSAGE_CLASS_DEFINITION(ATVMod::MsgConfigureCameraData, Message) -MESSAGE_CLASS_DEFINITION(ATVMod::MsgReportCameraData, Message) -MESSAGE_CLASS_DEFINITION(ATVMod::MsgReportEffectiveSampleRate, Message) const QString ATVMod::m_channelIdURI = "sdrangel.channeltx.modatv"; const QString ATVMod::m_channelId = "ATVMod"; -const float ATVMod::m_blackLevel = 0.3f; -const float ATVMod::m_spanLevel = 0.7f; -const int ATVMod::m_levelNbSamples = 10000; // every 10ms -const int ATVMod::m_nbBars = 6; -const int ATVMod::m_cameraFPSTestNbFrames = 100; -const int ATVMod::m_ssbFftLen = 1024; ATVMod::ATVMod(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), - m_deviceAPI(deviceAPI), - m_outputSampleRate(1000000), - m_inputFrequencyOffset(0), - m_modPhasor(0.0f), - m_tvSampleRate(1000000), - m_evenImage(true), - m_settingsMutex(QMutex::Recursive), - m_horizontalCount(0), - m_lineCount(0), - m_imageOK(false), - m_videoFPSq(1.0f), - m_videoFPSCount(0.0f), - m_videoPrevFPSCount(0), - m_videoEOF(false), - m_videoOK(false), - m_cameraIndex(-1), - //m_showOverlayText(false), - m_SSBFilter(0), - m_SSBFilterBuffer(0), - m_SSBFilterBufferIndex(0), - m_DSBFilter(0), - m_DSBFilterBuffer(0), - m_DSBFilterBufferIndex(0) + m_deviceAPI(deviceAPI) { - setObjectName(m_channelId); - scanCameras(); + setObjectName(m_channelId); - m_SSBFilter = new fftfilt(0, m_settings.m_rfBandwidth / m_outputSampleRate, m_ssbFftLen); - m_SSBFilterBuffer = new Complex[m_ssbFftLen>>1]; // filter returns data exactly half of its size - memset(m_SSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen>>1)); + m_thread = new QThread(this); + m_basebandSource = new ATVModBaseband(); + m_basebandSource->moveToThread(m_thread); - m_DSBFilter = new fftfilt((2.0f * m_settings.m_rfBandwidth) / m_outputSampleRate, 2 * m_ssbFftLen); - m_DSBFilterBuffer = new Complex[m_ssbFftLen]; - memset(m_DSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen)); + applySettings(m_settings, true); - m_interpolatorDistanceRemain = 0.0f; - m_interpolatorDistance = 1.0f; - - applyChannelSettings(m_outputSampleRate, m_inputFrequencyOffset, true); - applySettings(m_settings, true); // does applyStandard() too; - - m_channelizer = new UpChannelizer(this); - m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this); - m_deviceAPI->addChannelSource(m_threadedChannelizer); + m_deviceAPI->addChannelSource(this); m_deviceAPI->addChannelSourceAPI(this); m_networkManager = new QNetworkAccessManager(); @@ -114,431 +75,60 @@ ATVMod::~ATVMod() { disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); delete m_networkManager; - - if (m_video.isOpened()) { - m_video.release(); - } - - releaseCameras(); - m_deviceAPI->removeChannelSourceAPI(this); - m_deviceAPI->removeChannelSource(m_threadedChannelizer); - delete m_threadedChannelizer; - delete m_channelizer; - delete m_SSBFilter; - delete m_DSBFilter; - delete[] m_SSBFilterBuffer; - delete[] m_DSBFilterBuffer; + m_deviceAPI->removeChannelSourceAPI(this); + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; } -void ATVMod::pullAudio(int nbSamples) +uint32_t ATVMod::getNumberOfDeviceStreams() const { - (void) nbSamples; -} - -void ATVMod::pull(Sample& sample) -{ - if (m_settings.m_channelMute) - { - sample.m_real = 0.0f; - sample.m_imag = 0.0f; - return; - } - - Complex ci; - - m_settingsMutex.lock(); - - if ((m_tvSampleRate == m_outputSampleRate) && (!m_settings.m_forceDecimator)) // no interpolation nor decimation - { - modulateSample(); - pullFinalize(m_modSample, sample); - } - else - { - if (m_interpolatorDistance > 1.0f) // decimate - { - modulateSample(); - - while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - else - { - if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - - m_interpolatorDistanceRemain += m_interpolatorDistance; - pullFinalize(ci, sample); - } -} - -void ATVMod::pullFinalize(Complex& ci, Sample& sample) -{ - ci *= m_carrierNco.nextIQ(); // shift to carrier frequency - - m_settingsMutex.unlock(); - - double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); - magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); - m_movingAverage(magsq); - - sample.m_real = (FixReal) ci.real(); - sample.m_imag = (FixReal) ci.imag(); -} - -void ATVMod::modulateSample() -{ - Real t; - - pullVideo(t); - calculateLevel(t); - - t = m_settings.m_invertedVideo ? 1.0f - t : t; - - switch (m_settings.m_atvModulation) - { - case ATVModSettings::ATVModulationFM: // FM half bandwidth deviation - m_modPhasor += (t - 0.5f) * m_settings.m_fmExcursion * 2.0f * M_PI; - if (m_modPhasor > 2.0f * M_PI) m_modPhasor -= 2.0f * M_PI; // limit growth - if (m_modPhasor < 2.0f * M_PI) m_modPhasor += 2.0f * M_PI; // limit growth - m_modSample.real(cos(m_modPhasor) * m_settings.m_rfScalingFactor); // -1 dB - m_modSample.imag(sin(m_modPhasor) * m_settings.m_rfScalingFactor); - break; - case ATVModSettings::ATVModulationLSB: - case ATVModSettings::ATVModulationUSB: - m_modSample = modulateSSB(t); - m_modSample *= m_settings.m_rfScalingFactor; - break; - case ATVModSettings::ATVModulationVestigialLSB: - case ATVModSettings::ATVModulationVestigialUSB: - m_modSample = modulateVestigialSSB(t); - m_modSample *= m_settings.m_rfScalingFactor; - break; - case ATVModSettings::ATVModulationAM: // AM 90% - default: - m_modSample.real((t*1.8f + 0.1f) * (m_settings.m_rfScalingFactor/2.0f)); // modulate and scale zero frequency carrier - m_modSample.imag(0.0f); - } -} - -Complex& ATVMod::modulateSSB(Real& sample) -{ - int n_out; - Complex ci(sample, 0.0f); - fftfilt::cmplx *filtered; - - n_out = m_SSBFilter->runSSB(ci, &filtered, m_settings.m_atvModulation == ATVModSettings::ATVModulationUSB); - - if (n_out > 0) - { - memcpy((void *) m_SSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); - m_SSBFilterBufferIndex = 0; - } - - m_SSBFilterBufferIndex++; - - return m_SSBFilterBuffer[m_SSBFilterBufferIndex-1]; -} - -Complex& ATVMod::modulateVestigialSSB(Real& sample) -{ - int n_out; - Complex ci(sample, 0.0f); - fftfilt::cmplx *filtered; - - n_out = m_DSBFilter->runAsym(ci, &filtered, m_settings.m_atvModulation == ATVModSettings::ATVModulationVestigialUSB); - - if (n_out > 0) - { - memcpy((void *) m_DSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); - m_DSBFilterBufferIndex = 0; - } - - m_DSBFilterBufferIndex++; - - return m_DSBFilterBuffer[m_DSBFilterBufferIndex-1]; -} - -void ATVMod::pullVideo(Real& sample) -{ - if ((m_settings.m_atvStd == ATVModSettings::ATVStdHSkip) && (m_lineCount == m_nbLines2)) // last line in skip mode - { - pullImageLine(sample, true); // pull image line without sync - } - else if (m_lineCount < m_nbLines2 + 1) // even image or non interlaced - { - int iLine = m_lineCount; - - if (iLine < m_nbSyncLinesHeadE + m_nbBlankLines) - { - pullVSyncLine(sample); - } - else if (iLine > m_nbLines2 - m_nbSyncLinesBottom) - { - pullVSyncLine(sample); - } - else - { - pullImageLine(sample); - } - } - else // odd image - { - int iLine = m_lineCount - m_nbLines2 - 1; - - if (iLine < m_nbSyncLinesHeadO + m_nbBlankLines) - { - pullVSyncLine(sample); - } - else if (iLine > m_nbLines2 - 1 - m_nbSyncLinesBottom) - { - pullVSyncLine(sample); - } - else - { - pullImageLine(sample); - } - } - - if (m_horizontalCount < m_nbHorizPoints - 1) - { - m_horizontalCount++; - } - else - { - if (m_lineCount < m_nbLines - 1) - { - m_lineCount++; - if (m_lineCount > (m_nbLines/2)) m_evenImage = !m_evenImage; - } - else // new image - { - m_lineCount = 0; - m_evenImage = !m_evenImage; - - if ((m_settings.m_atvModInput == ATVModSettings::ATVModInputVideo) && m_videoOK && (m_settings.m_videoPlay) && !m_videoEOF) - { - int grabOK = 0; - int fpsIncrement = (int) m_videoFPSCount - m_videoPrevFPSCount; - - // move a number of frames according to increment - // use grab to test for EOF then retrieve to preserve last valid frame as the current original frame - // TODO: handle pause (no move) - for (int i = 0; i < fpsIncrement; i++) - { - grabOK = m_video.grab(); - if (!grabOK) break; - } - - if (grabOK) - { - cv::Mat colorFrame; - m_video.retrieve(colorFrame); - - if (!colorFrame.empty()) // some frames may not come out properly - { - if (m_settings.m_showOverlayText) { - mixImageAndText(colorFrame); - } - - cv::cvtColor(colorFrame, m_videoframeOriginal, CV_BGR2GRAY); - resizeVideo(); - } - } - else - { - if (m_settings.m_videoPlayLoop) { // play loop - seekVideoFileStream(0); - } else { // stops - m_videoEOF = true; - } - } - - if (m_videoFPSCount < m_videoFPS) - { - m_videoPrevFPSCount = (int) m_videoFPSCount; - m_videoFPSCount += m_videoFPSq; - } - else - { - m_videoPrevFPSCount = 0; - m_videoFPSCount = m_videoFPSq; - } - } - else if ((m_settings.m_atvModInput == ATVModSettings::ATVModInputCamera) && (m_settings.m_cameraPlay)) - { - ATVCamera& camera = m_cameras[m_cameraIndex]; // currently selected canera - - if (camera.m_videoFPS < 0.0f) // default frame rate when it could not be obtained via get - { - time_t start, end; - cv::Mat frame; - - if (getMessageQueueToGUI()) - { - MsgReportCameraData *report; - report = MsgReportCameraData::create( - camera.m_cameraNumber, - 0.0f, - camera.m_videoFPSManual, - camera.m_videoFPSManualEnable, - camera.m_videoWidth, - camera.m_videoHeight, - 1); // open splash screen on GUI side - getMessageQueueToGUI()->push(report); - } - - int nbFrames = 0; - - time(&start); - - for (int i = 0; i < m_cameraFPSTestNbFrames; i++) - { - camera.m_camera >> frame; - if (!frame.empty()) nbFrames++; - } - - time(&end); - - double seconds = difftime (end, start); - // take a 10% guard and divide bandwidth between all cameras as a hideous hack - camera.m_videoFPS = ((nbFrames / seconds) * 0.9) / m_cameras.size(); - camera.m_videoFPSq = camera.m_videoFPS / m_fps; - camera.m_videoFPSCount = camera.m_videoFPSq; - camera.m_videoPrevFPSCount = 0; - - if (getMessageQueueToGUI()) - { - MsgReportCameraData *report; - report = MsgReportCameraData::create( - camera.m_cameraNumber, - camera.m_videoFPS, - camera.m_videoFPSManual, - camera.m_videoFPSManualEnable, - camera.m_videoWidth, - camera.m_videoHeight, - 2); // close splash screen on GUI side - getMessageQueueToGUI()->push(report); - } - } - else if (camera.m_videoFPS == 0.0f) // Hideous hack for windows - { - camera.m_videoFPS = 5.0f; - camera.m_videoFPSq = camera.m_videoFPS / m_fps; - camera.m_videoFPSCount = camera.m_videoFPSq; - camera.m_videoPrevFPSCount = 0; - - if (getMessageQueueToGUI()) - { - MsgReportCameraData *report; - report = MsgReportCameraData::create( - camera.m_cameraNumber, - camera.m_videoFPS, - camera.m_videoFPSManual, - camera.m_videoFPSManualEnable, - camera.m_videoWidth, - camera.m_videoHeight, - 0); - getMessageQueueToGUI()->push(report); - } - } - - int fpsIncrement = (int) camera.m_videoFPSCount - camera.m_videoPrevFPSCount; - - // move a number of frames according to increment - // use grab to test for EOF then retrieve to preserve last valid frame as the current original frame - cv::Mat colorFrame; - - for (int i = 0; i < fpsIncrement; i++) - { - camera.m_camera >> colorFrame; - if (colorFrame.empty()) break; - } - - if (!colorFrame.empty()) // some frames may not come out properly - { - if (m_settings.m_showOverlayText) { - mixImageAndText(colorFrame); - } - - cv::cvtColor(colorFrame, camera.m_videoframeOriginal, CV_BGR2GRAY); - resizeCamera(); - } - - if (camera.m_videoFPSCount < (camera.m_videoFPSManualEnable ? camera.m_videoFPSManual : camera.m_videoFPS)) - { - camera.m_videoPrevFPSCount = (int) camera.m_videoFPSCount; - camera.m_videoFPSCount += (camera.m_videoFPSManualEnable ? camera.m_videoFPSqManual : camera.m_videoFPSq); - } - else - { - camera.m_videoPrevFPSCount = 0; - camera.m_videoFPSCount = (camera.m_videoFPSManualEnable ? camera.m_videoFPSqManual : camera.m_videoFPSq); - } - } - } - - m_horizontalCount = 0; - } -} - -void ATVMod::calculateLevel(Real& sample) -{ - if (m_levelCalcCount < m_levelNbSamples) - { - m_peakLevel = std::max(std::fabs(m_peakLevel), sample); - m_levelSum += sample * sample; - m_levelCalcCount++; - } - else - { - qreal rmsLevel = std::sqrt(m_levelSum / m_levelNbSamples); - //qDebug("NFMMod::calculateLevel: %f %f", rmsLevel, m_peakLevel); - emit levelChanged(rmsLevel, m_peakLevel, m_levelNbSamples); - m_peakLevel = 0.0f; - m_levelSum = 0.0f; - m_levelCalcCount = 0; - } + return m_deviceAPI->getNbSinkStreams(); } void ATVMod::start() { - qDebug() << "ATVMod::start: m_outputSampleRate: " << m_outputSampleRate - << " m_inputFrequencyOffset: " << m_settings.m_inputFrequencyOffset; - applyChannelSettings(m_outputSampleRate, m_inputFrequencyOffset, true); + qDebug("ATVMod::start"); + m_basebandSource->reset(); + m_thread->start(); } void ATVMod::stop() { + qDebug("ATVMod::stop"); + m_thread->exit(); + m_thread->wait(); +} + +void ATVMod::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); } bool ATVMod::handleMessage(const Message& cmd) { - if (UpChannelizer::MsgChannelizerNotification::match(cmd)) + if (MsgConfigureChannelizer::match(cmd)) { - UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd; - qDebug() << "ATVMod::handleMessage: MsgChannelizerNotification:" - << " outputSampleRate: " << notif.getSampleRate() - << " inputFrequencyOffset: " << notif.getFrequencyOffset(); + MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; + qDebug() << "ATVMod::handleMessage: MsgConfigureChannelizer:" + << " getSourceSampleRate: " << cfg.getSourceSampleRate() + << " getSourceCenterFrequency: " << cfg.getSourceCenterFrequency(); - applyChannelSettings(notif.getSampleRate(), notif.getFrequencyOffset()); + ATVModBaseband::MsgConfigureChannelizer *msg + = ATVModBaseband::MsgConfigureChannelizer::create(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_basebandSource->getInputMessageQueue()->push(msg); return true; } - else if (MsgConfigureChannelizer::match(cmd)) + else if (MsgConfigureSourceCenterFrequency::match(cmd)) { - MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; - qDebug() << "SSBMod::handleMessage: MsgConfigureChannelizer: sampleRate: " << m_channelizer->getOutputSampleRate() - << " centerFrequency: " << cfg.getCenterFrequency(); + MsgConfigureSourceCenterFrequency& cfg = (MsgConfigureSourceCenterFrequency&) cmd; + qDebug() << "ATVMod::handleMessage: MsgConfigureSourceCenterFrequency:" + << " getSourceCenterFrequency: " << cfg.getSourceCenterFrequency(); - m_channelizer->configure(m_channelizer->getInputMessageQueue(), - m_channelizer->getOutputSampleRate(), - cfg.getCenterFrequency()); + ATVModBaseband::MsgConfigureChannelizer *msg + = ATVModBaseband::MsgConfigureChannelizer::create(m_basebandSource->getChannelSampleRate(), cfg.getSourceCenterFrequency()); + m_basebandSource->getInputMessageQueue()->push(msg); return true; } @@ -551,538 +141,72 @@ bool ATVMod::handleMessage(const Message& cmd) return true; } + else if (DSPSignalNotification::match(cmd)) + { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "ATVMod::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + + return true; + } else if (MsgConfigureImageFileName::match(cmd)) { - MsgConfigureImageFileName& conf = (MsgConfigureImageFileName&) cmd; - openImage(conf.getFileName()); + MsgConfigureImageFileName& cfg = (MsgConfigureImageFileName&) cmd; + ATVModBaseband::MsgConfigureImageFileName *msg = ATVModBaseband::MsgConfigureImageFileName::create( + cfg.getFileName()); + m_basebandSource->getInputMessageQueue()->push(msg); + return true; } else if (MsgConfigureVideoFileName::match(cmd)) { - MsgConfigureVideoFileName& conf = (MsgConfigureVideoFileName&) cmd; - openVideo(conf.getFileName()); + MsgConfigureVideoFileName& cfg = (MsgConfigureVideoFileName&) cmd; + ATVModBaseband::MsgConfigureVideoFileName *msg = ATVModBaseband::MsgConfigureVideoFileName::create( + cfg.getFileName()); + m_basebandSource->getInputMessageQueue()->push(msg); + return true; } else if (MsgConfigureVideoFileSourceSeek::match(cmd)) { - MsgConfigureVideoFileSourceSeek& conf = (MsgConfigureVideoFileSourceSeek&) cmd; - int seekPercentage = conf.getPercentage(); - seekVideoFileStream(seekPercentage); + MsgConfigureVideoFileSourceSeek& cfg = (MsgConfigureVideoFileSourceSeek&) cmd; + ATVModBaseband::MsgConfigureVideoFileSourceSeek *rep = ATVModBaseband::MsgConfigureVideoFileSourceSeek::create(cfg.getPercentage()); + m_basebandSource->getInputMessageQueue()->push(rep); + return true; } else if (MsgConfigureVideoFileSourceStreamTiming::match(cmd)) { - int framesCount; - - if (m_videoOK && m_video.isOpened()) - { - framesCount = m_video.get(CV_CAP_PROP_POS_FRAMES);; - } else { - framesCount = 0; - } - - if (getMessageQueueToGUI()) - { - MsgReportVideoFileSourceStreamTiming *report; - report = MsgReportVideoFileSourceStreamTiming::create(framesCount); - getMessageQueueToGUI()->push(report); - } + ATVModBaseband::MsgConfigureVideoFileSourceStreamTiming *rep = ATVModBaseband::MsgConfigureVideoFileSourceStreamTiming::create(); + m_basebandSource->getInputMessageQueue()->push(rep); return true; } else if (MsgConfigureCameraIndex::match(cmd)) { - MsgConfigureCameraIndex& cfg = (MsgConfigureCameraIndex&) cmd; - uint32_t index = cfg.getIndex() & 0x7FFFFFF; + MsgConfigureCameraIndex& cfg = (MsgConfigureCameraIndex&) cmd; + ATVModBaseband::MsgConfigureCameraIndex *rep = ATVModBaseband::MsgConfigureCameraIndex::create(cfg.getIndex()); + m_basebandSource->getInputMessageQueue()->push(rep); - if (index < m_cameras.size()) - { - m_cameraIndex = index; - - if (getMessageQueueToGUI()) - { - MsgReportCameraData *report; - report = MsgReportCameraData::create( - m_cameras[m_cameraIndex].m_cameraNumber, - m_cameras[m_cameraIndex].m_videoFPS, - m_cameras[m_cameraIndex].m_videoFPSManual, - m_cameras[m_cameraIndex].m_videoFPSManualEnable, - m_cameras[m_cameraIndex].m_videoWidth, - m_cameras[m_cameraIndex].m_videoHeight, - 0); - getMessageQueueToGUI()->push(report); - } - } - - return true; + return true; } else if (MsgConfigureCameraData::match(cmd)) { - MsgConfigureCameraData& cfg = (MsgConfigureCameraData&) cmd; - uint32_t index = cfg.getIndex() & 0x7FFFFFF; - float mnaualFPS = cfg.getManualFPS(); - bool manualFPSEnable = cfg.getManualFPSEnable(); + MsgConfigureCameraData& cfg = (MsgConfigureCameraData&) cmd; + ATVModBaseband::MsgConfigureCameraData *rep = ATVModBaseband::MsgConfigureCameraData::create( + cfg.getIndex(), cfg.getManualFPS(), cfg.getManualFPSEnable() + ); - if (index < m_cameras.size()) - { - m_cameras[index].m_videoFPSManual = mnaualFPS; - m_cameras[index].m_videoFPSManualEnable = manualFPSEnable; - } - - return true; - } - else if (DSPSignalNotification::match(cmd)) - { return true; } - else - { - return false; - } -} - -void ATVMod::getBaseValues(int outputSampleRate, int linesPerSecond, int& sampleRateUnits, uint32_t& nbPointsPerRateUnit) -{ - int maxPoints = outputSampleRate / linesPerSecond; - int i = maxPoints; - - for (; i > 0; i--) - { - if ((i * linesPerSecond) % 10 == 0) - break; - } - - nbPointsPerRateUnit = i == 0 ? maxPoints : i; - sampleRateUnits = nbPointsPerRateUnit * linesPerSecond; -} - -float ATVMod::getRFBandwidthDivisor(ATVModSettings::ATVModulation modulation) -{ - switch(modulation) - { - case ATVModSettings::ATVModulationLSB: - case ATVModSettings::ATVModulationUSB: - case ATVModSettings::ATVModulationVestigialLSB: - case ATVModSettings::ATVModulationVestigialUSB: - return 1.05f; - break; - case ATVModSettings::ATVModulationAM: - case ATVModSettings::ATVModulationFM: - default: - return 2.2f; - } -} - -void ATVMod::applyStandard() -{ - m_pointsPerSync = (uint32_t) ((4.7f / 64.0f) * m_pointsPerLine); - m_pointsPerBP = (uint32_t) ((4.7f / 64.0f) * m_pointsPerLine); - m_pointsPerFP = (uint32_t) ((2.6f / 64.0f) * m_pointsPerLine); - m_pointsPerFSync = (uint32_t) ((2.3f / 64.0f) * m_pointsPerLine); - - m_pointsPerImgLine = m_pointsPerLine - m_pointsPerSync - m_pointsPerBP - m_pointsPerFP; - m_nbHorizPoints = m_pointsPerLine; - - m_pointsPerHBar = m_pointsPerImgLine / m_nbBars; - m_hBarIncrement = m_spanLevel / (float) m_nbBars; - m_vBarIncrement = m_spanLevel / (float) m_nbBars; - - m_nbLines = m_settings.m_nbLines; - m_nbLines2 = m_nbLines / 2; - m_fps = m_settings.m_fps * 1.0f; - -// qDebug() << "ATVMod::applyStandard: " -// << " m_nbLines: " << m_config.m_nbLines -// << " m_fps: " << m_config.m_fps -// << " rateUnits: " << rateUnits -// << " nbPointsPerRateUnit: " << nbPointsPerRateUnit -// << " m_tvSampleRate: " << m_tvSampleRate -// << " m_pointsPerTU: " << m_pointsPerTU; - - switch(m_settings.m_atvStd) - { - case ATVModSettings::ATVStdHSkip: - m_nbImageLines = m_nbLines; // lines less the total number of sync lines - m_nbImageLines2 = m_nbImageLines; // force non interleaved for vbars - m_interleaved = false; - m_nbSyncLinesHeadE = 0; // number of sync lines on the top of a frame even - m_nbSyncLinesHeadO = 0; // number of sync lines on the top of a frame odd - m_nbSyncLinesBottom = -1; // force no vsync in even block - m_nbLongSyncLines = 0; - m_nbHalfLongSync = 0; - m_nbWholeEqLines = 0; - m_singleLongSync = true; - m_nbBlankLines = 0; - m_blankLineLvel = 0.7f; - m_nbLines2 = m_nbLines - 1; - break; - case ATVModSettings::ATVStdShort: - m_nbImageLines = m_nbLines - 2; // lines less the total number of sync lines - m_nbImageLines2 = m_nbImageLines; // force non interleaved for vbars - m_interleaved = false; - m_nbSyncLinesHeadE = 1; // number of sync lines on the top of a frame even - m_nbSyncLinesHeadO = 1; // number of sync lines on the top of a frame odd - m_nbSyncLinesBottom = 0; - m_nbLongSyncLines = 1; - m_nbHalfLongSync = 0; - m_nbWholeEqLines = 0; - m_singleLongSync = true; - m_nbBlankLines = 1; - m_blankLineLvel = 0.7f; - m_nbLines2 = m_nbLines; // force non interleaved => treated as even for all lines - break; - case ATVModSettings::ATVStdShortInterleaved: - m_nbImageLines = m_nbLines - 2; // lines less the total number of sync lines - m_nbImageLines2 = m_nbImageLines / 2; - m_interleaved = true; - m_nbSyncLinesHeadE = 1; // number of sync lines on the top of a frame even - m_nbSyncLinesHeadO = 1; // number of sync lines on the top of a frame odd - m_nbSyncLinesBottom = 0; - m_nbLongSyncLines = 1; - m_nbHalfLongSync = 0; - m_nbWholeEqLines = 0; - m_singleLongSync = true; - m_nbBlankLines = 1; - m_blankLineLvel = 0.7f; - break; - case ATVModSettings::ATVStd405: // Follows loosely the 405 lines standard - m_nbImageLines = m_nbLines - 15; // lines less the total number of sync lines - m_nbImageLines2 = m_nbImageLines / 2; - m_interleaved = true; - m_nbSyncLinesHeadE = 5; // number of sync lines on the top of a frame even - m_nbSyncLinesHeadO = 4; // number of sync lines on the top of a frame odd - m_nbSyncLinesBottom = 3; - m_nbLongSyncLines = 2; - m_nbHalfLongSync = 1; - m_nbWholeEqLines = 2; - m_singleLongSync = false; - m_nbBlankLines = 7; // yields 376 lines (195 - 7) * 2 - m_blankLineLvel = m_blackLevel; - break; - case ATVModSettings::ATVStdPAL525: // Follows PAL-M standard - m_nbImageLines = m_nbLines - 15; - m_nbImageLines2 = m_nbImageLines / 2; - m_interleaved = true; - m_nbSyncLinesHeadE = 5; - m_nbSyncLinesHeadO = 4; // number of sync lines on the top of a frame odd - m_nbSyncLinesBottom = 3; - m_nbLongSyncLines = 2; - m_nbHalfLongSync = 1; - m_nbWholeEqLines = 2; - m_singleLongSync = false; - m_nbBlankLines = 15; // yields 480 lines (255 - 15) * 2 - m_blankLineLvel = m_blackLevel; - break; - case ATVModSettings::ATVStdPAL625: // Follows PAL-B/G/H standard - default: - m_nbImageLines = m_nbLines - 15; - m_nbImageLines2 = m_nbImageLines / 2; - m_interleaved = true; - m_nbSyncLinesHeadE = 5; - m_nbSyncLinesHeadO = 4; // number of sync lines on the top of a frame odd - m_nbSyncLinesBottom = 3; - m_nbLongSyncLines = 2; - m_nbHalfLongSync = 1; - m_nbWholeEqLines = 2; - m_singleLongSync = false; - m_nbBlankLines = 17; // yields 576 lines (305 - 17) * 2 - m_blankLineLvel = m_blackLevel; - } - - m_linesPerVBar = m_nbImageLines2 / m_nbBars; - - if (m_imageOK) - { - resizeImage(); - } - - if (m_videoOK) - { - calculateVideoSizes(); - resizeVideo(); - } - - calculateCamerasSizes(); -} - -void ATVMod::openImage(const QString& fileName) -{ - m_imageFromFile = cv::imread(qPrintable(fileName), CV_LOAD_IMAGE_GRAYSCALE); - m_imageOK = m_imageFromFile.data != 0; - - if (m_imageOK) - { - m_settings.m_imageFileName = fileName; - m_imageFromFile.copyTo(m_imageOriginal); - - if (m_settings.m_showOverlayText) { - mixImageAndText(m_imageOriginal); - } - - resizeImage(); - } else { - m_settings.m_imageFileName.clear(); - qDebug("ATVMod::openImage: cannot open image file %s", qPrintable(fileName)); + return false; } } -void ATVMod::openVideo(const QString& fileName) -{ - //if (m_videoOK && m_video.isOpened()) m_video.release(); should be done by OpenCV in open method - - m_videoOK = m_video.open(qPrintable(fileName)); - - if (m_videoOK) - { - m_settings.m_videoFileName = fileName; - m_videoFPS = m_video.get(CV_CAP_PROP_FPS); - m_videoWidth = (int) m_video.get(CV_CAP_PROP_FRAME_WIDTH); - m_videoHeight = (int) m_video.get(CV_CAP_PROP_FRAME_HEIGHT); - m_videoLength = (int) m_video.get(CV_CAP_PROP_FRAME_COUNT); - int ex = static_cast(m_video.get(CV_CAP_PROP_FOURCC)); - char ext[] = {(char)(ex & 0XFF),(char)((ex & 0XFF00) >> 8),(char)((ex & 0XFF0000) >> 16),(char)((ex & 0XFF000000) >> 24),0}; - - qDebug("ATVMod::openVideo: %s FPS: %f size: %d x %d #frames: %d codec: %s", - m_video.isOpened() ? "OK" : "KO", - m_videoFPS, - m_videoWidth, - m_videoHeight, - m_videoLength, - ext); - - calculateVideoSizes(); - m_videoEOF = false; - - if (getMessageQueueToGUI()) - { - MsgReportVideoFileSourceStreamData *report; - report = MsgReportVideoFileSourceStreamData::create(m_videoFPS, m_videoLength); - getMessageQueueToGUI()->push(report); - } - } - else - { - m_settings.m_videoFileName.clear(); - qDebug("ATVMod::openVideo: cannot open video file %s", qPrintable(fileName)); - } -} - -void ATVMod::resizeImage() -{ - float fy = (m_nbImageLines - 2*m_nbBlankLines) / (float) m_imageOriginal.rows; - float fx = m_pointsPerImgLine / (float) m_imageOriginal.cols; - cv::resize(m_imageOriginal, m_image, cv::Size(), fx, fy); - qDebug("ATVMod::resizeImage: %d x %d -> %d x %d", m_imageOriginal.cols, m_imageOriginal.rows, m_image.cols, m_image.rows); -} - -void ATVMod::calculateVideoSizes() -{ - m_videoFy = (m_nbImageLines - 2*m_nbBlankLines) / (float) m_videoHeight; - m_videoFx = m_pointsPerImgLine / (float) m_videoWidth; - m_videoFPSq = m_videoFPS / m_fps; - m_videoFPSCount = m_videoFPSq; - m_videoPrevFPSCount = 0; - - qDebug("ATVMod::calculateVideoSizes: factors: %f x %f FPSq: %f", m_videoFx, m_videoFy, m_videoFPSq); -} - -void ATVMod::resizeVideo() -{ - if (!m_videoframeOriginal.empty()) { - cv::resize(m_videoframeOriginal, m_videoFrame, cv::Size(), m_videoFx, m_videoFy); // resize current frame - } -} - -void ATVMod::calculateCamerasSizes() -{ - for (std::vector::iterator it = m_cameras.begin(); it != m_cameras.end(); ++it) - { - it->m_videoFy = (m_nbImageLines - 2*m_nbBlankLines) / (float) it->m_videoHeight; - it->m_videoFx = m_pointsPerImgLine / (float) it->m_videoWidth; - it->m_videoFPSq = it->m_videoFPS / m_fps; - it->m_videoFPSqManual = it->m_videoFPSManual / m_fps; - it->m_videoFPSCount = 0; //it->m_videoFPSq; - it->m_videoPrevFPSCount = 0; - - qDebug("ATVMod::calculateCamerasSizes: [%d] factors: %f x %f FPSq: %f", (int) (it - m_cameras.begin()), it->m_videoFx, it->m_videoFy, it->m_videoFPSq); - } -} - -void ATVMod::resizeCameras() -{ - for (std::vector::iterator it = m_cameras.begin(); it != m_cameras.end(); ++it) - { - if (!it->m_videoframeOriginal.empty()) { - cv::resize(it->m_videoframeOriginal, it->m_videoFrame, cv::Size(), it->m_videoFx, it->m_videoFy); // resize current frame - } - } -} - -void ATVMod::resizeCamera() -{ - ATVCamera& camera = m_cameras[m_cameraIndex]; - - if (!camera.m_videoframeOriginal.empty()) { - cv::resize(camera.m_videoframeOriginal, camera.m_videoFrame, cv::Size(), camera.m_videoFx, camera.m_videoFy); // resize current frame - } -} - -void ATVMod::seekVideoFileStream(int seekPercentage) -{ - QMutexLocker mutexLocker(&m_settingsMutex); - - if ((m_videoOK) && m_video.isOpened()) - { - int seekPoint = ((m_videoLength * seekPercentage) / 100); - m_video.set(CV_CAP_PROP_POS_FRAMES, seekPoint); - m_videoFPSCount = m_videoFPSq; - m_videoPrevFPSCount = 0; - m_videoEOF = false; - } -} - -void ATVMod::scanCameras() -{ - for (int i = 0; i < 4; i++) - { - ATVCamera newCamera; - m_cameras.push_back(newCamera); - m_cameras.back().m_cameraNumber = i; - m_cameras.back().m_camera.open(i); - - if (m_cameras.back().m_camera.isOpened()) - { - m_cameras.back().m_videoFPS = m_cameras.back().m_camera.get(CV_CAP_PROP_FPS); - m_cameras.back().m_videoWidth = (int) m_cameras.back().m_camera.get(CV_CAP_PROP_FRAME_WIDTH); - m_cameras.back().m_videoHeight = (int) m_cameras.back().m_camera.get(CV_CAP_PROP_FRAME_HEIGHT); - - //m_cameras.back().m_videoFPS = m_cameras.back().m_videoFPS < 0 ? 16.3f : m_cameras.back().m_videoFPS; - - qDebug("ATVMod::scanCameras: [%d] FPS: %f %dx%d", - i, - m_cameras.back().m_videoFPS, - m_cameras.back().m_videoWidth , - m_cameras.back().m_videoHeight); - } - else - { - m_cameras.pop_back(); - } - } - - if (m_cameras.size() > 0) - { - calculateCamerasSizes(); - m_cameraIndex = 0; - } -} - -void ATVMod::releaseCameras() -{ - for (std::vector::iterator it = m_cameras.begin(); it != m_cameras.end(); ++it) - { - if (it->m_camera.isOpened()) it->m_camera.release(); - } -} - -void ATVMod::getCameraNumbers(std::vector& numbers) -{ - for (std::vector::iterator it = m_cameras.begin(); it != m_cameras.end(); ++it) { - numbers.push_back(it->m_cameraNumber); - } - - if (m_cameras.size() > 0) - { - m_cameraIndex = 0; - - if (getMessageQueueToGUI()) - { - MsgReportCameraData *report; - report = MsgReportCameraData::create( - m_cameras[0].m_cameraNumber, - m_cameras[0].m_videoFPS, - m_cameras[0].m_videoFPSManual, - m_cameras[0].m_videoFPSManualEnable, - m_cameras[0].m_videoWidth, - m_cameras[0].m_videoHeight, - 0); - getMessageQueueToGUI()->push(report); - } - } -} - -void ATVMod::mixImageAndText(cv::Mat& image) -{ - int fontFace = cv::FONT_HERSHEY_PLAIN; - double fontScale = image.rows / 100.0; - int thickness = image.cols / 160; - int baseline=0; - - fontScale = fontScale < 4.0f ? 4.0f : fontScale; // minimum size - cv::Size textSize = cv::getTextSize(m_settings.m_overlayText.toStdString(), fontFace, fontScale, thickness, &baseline); - baseline += thickness; - - // position the text in the top left corner - cv::Point textOrg(6, textSize.height+10); - // then put the text itself - cv::putText(image, m_settings.m_overlayText.toStdString(), textOrg, fontFace, fontScale, cv::Scalar::all(255*m_settings.m_uniformLevel), thickness, CV_AA); -} - -void ATVMod::applyChannelSettings(int outputSampleRate, int inputFrequencyOffset, bool force) -{ - qDebug() << "AMMod::applyChannelSettings:" - << " outputSampleRate: " << outputSampleRate - << " inputFrequencyOffset: " << inputFrequencyOffset; - - if ((inputFrequencyOffset != m_inputFrequencyOffset) || - (outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_carrierNco.setFreq(inputFrequencyOffset, outputSampleRate); - m_settingsMutex.unlock(); - } - - if ((outputSampleRate != m_outputSampleRate) || force) - { - getBaseValues(outputSampleRate, m_settings.m_nbLines * m_settings.m_fps, m_tvSampleRate, m_pointsPerLine); - - m_settingsMutex.lock(); - - if (m_tvSampleRate > 0) - { - m_interpolatorDistanceRemain = 0; - m_interpolatorDistance = (Real) m_tvSampleRate / (Real) outputSampleRate; - m_interpolator.create(32, - m_tvSampleRate, - m_settings.m_rfBandwidth / getRFBandwidthDivisor(m_settings.m_atvModulation), - 3.0); - } - else - { - m_tvSampleRate = outputSampleRate; - } - - m_SSBFilter->create_filter(0, m_settings.m_rfBandwidth / m_tvSampleRate); - memset(m_SSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen>>1)); - m_SSBFilterBufferIndex = 0; - - applyStandard(); // set all timings - m_settingsMutex.unlock(); - - if (getMessageQueueToGUI()) - { - MsgReportEffectiveSampleRate *report; - report = MsgReportEffectiveSampleRate::create(m_tvSampleRate, m_pointsPerLine); - getMessageQueueToGUI()->push(report); - } - } - - m_outputSampleRate = outputSampleRate; - m_inputFrequencyOffset = inputFrequencyOffset; -} - void ATVMod::applySettings(const ATVModSettings& settings, bool force) { qDebug() << "ATVMod::applySettings:" @@ -1170,72 +294,8 @@ void ATVMod::applySettings(const ATVModSettings& settings, bool force) reverseAPIKeys.append("overlayText"); } - if ((settings.m_atvStd != m_settings.m_atvStd) - || (settings.m_nbLines != m_settings.m_nbLines) - || (settings.m_fps != m_settings.m_fps) - || (settings.m_rfBandwidth != m_settings.m_rfBandwidth) - || (settings.m_atvModulation != m_settings.m_atvModulation) || force) - { - getBaseValues(m_outputSampleRate, settings.m_nbLines * settings.m_fps, m_tvSampleRate, m_pointsPerLine); - - m_settingsMutex.lock(); - - if (m_tvSampleRate > 0) - { - m_interpolatorDistanceRemain = 0; - m_interpolatorDistance = (Real) m_tvSampleRate / (Real) m_outputSampleRate; - m_interpolator.create(32, - m_tvSampleRate, - settings.m_rfBandwidth / getRFBandwidthDivisor(settings.m_atvModulation), - 3.0); - } - - m_SSBFilter->create_filter(0, settings.m_rfBandwidth / m_tvSampleRate); - memset(m_SSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen>>1)); - m_SSBFilterBufferIndex = 0; - - applyStandard(); // set all timings - m_settingsMutex.unlock(); - - if (getMessageQueueToGUI()) - { - MsgReportEffectiveSampleRate *report; - report = MsgReportEffectiveSampleRate::create(m_tvSampleRate, m_pointsPerLine); - getMessageQueueToGUI()->push(report); - } - } - - if ((settings.m_rfOppBandwidth != m_settings.m_rfOppBandwidth) - || (settings.m_rfBandwidth != m_settings.m_rfBandwidth) - || (settings.m_nbLines != m_settings.m_nbLines) // difference in line period may have changed TV sample rate - || (settings.m_fps != m_settings.m_fps) // - || force) - { - m_settingsMutex.lock(); - - m_DSBFilter->create_asym_filter(settings.m_rfOppBandwidth / m_tvSampleRate, settings.m_rfBandwidth / m_tvSampleRate); - memset(m_DSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen)); - m_DSBFilterBufferIndex = 0; - - m_settingsMutex.unlock(); - } - - if ((settings.m_showOverlayText != m_settings.m_showOverlayText) || force) - { - if (!m_imageFromFile.empty()) - { - m_imageFromFile.copyTo(m_imageOriginal); - - if (settings.m_showOverlayText) { - qDebug("ATVMod::applySettings: set overlay text"); - mixImageAndText(m_imageOriginal); - } else{ - qDebug("ATVMod::applySettings: clear overlay text"); - } - - resizeImage(); - } - } + ATVModBaseband::MsgConfigureATVModBaseband *msg = ATVModBaseband::MsgConfigureATVModBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); if (settings.m_useReverseAPI) { @@ -1296,7 +356,7 @@ int ATVMod::webapiSettingsPutPatch( if (m_settings.m_inputFrequencyOffset != settings.m_inputFrequencyOffset) { ATVMod::MsgConfigureChannelizer *msgChan = ATVMod::MsgConfigureChannelizer::create( - settings.m_inputFrequencyOffset); + m_basebandSource->getChannelSampleRate(), settings.m_inputFrequencyOffset); m_inputMessageQueue.push(msgChan); } @@ -1311,9 +371,9 @@ int ATVMod::webapiSettingsPutPatch( if (channelSettingsKeys.contains("imageFileName")) { - MsgConfigureImageFileName *msg = MsgConfigureImageFileName::create( + ATVModBaseband::MsgConfigureImageFileName *msg = ATVModBaseband::MsgConfigureImageFileName::create( *response.getAtvModSettings()->getImageFileName()); - m_inputMessageQueue.push(msg); + m_basebandSource->getInputMessageQueue()->push(msg); if (m_guiMessageQueue) // forward to GUI if any { @@ -1325,7 +385,7 @@ int ATVMod::webapiSettingsPutPatch( if (channelSettingsKeys.contains("videoFileName")) { - MsgConfigureVideoFileName *msg = MsgConfigureVideoFileName::create( + ATVModBaseband::MsgConfigureVideoFileName *msg = ATVModBaseband::MsgConfigureVideoFileName::create( *response.getAtvModSettings()->getVideoFileName()); m_inputMessageQueue.push(msg); @@ -1501,7 +561,7 @@ void ATVMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& respon void ATVMod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) { response.getAtvModReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); - response.getAtvModReport()->setChannelSampleRate(m_outputSampleRate); + response.getAtvModReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); } void ATVMod::webapiReverseSendSettings(QList& channelSettingsKeys, const ATVModSettings& settings, bool force) @@ -1588,13 +648,14 @@ void ATVMod::webapiReverseSendSettings(QList& channelSettingsKeys, cons m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -1609,10 +670,38 @@ void ATVMod::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("AMMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("ATVMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } + +double ATVMod::getMagSq() const +{ + return m_basebandSource->getMagSq(); +} + +void ATVMod::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); +} + +int ATVMod::getEffectiveSampleRate() const +{ + return m_basebandSource->getEffectiveSampleRate(); +} + +void ATVMod::getCameraNumbers(std::vector& numbers) +{ + m_basebandSource->getCameraNumbers(numbers); +} + +void ATVMod::propagateMessageQueueToGUI() +{ + m_basebandSource->setMessageQueueToGUI(getMessageQueueToGUI()); +} \ No newline at end of file diff --git a/plugins/channeltx/modatv/atvmod.h b/plugins/channeltx/modatv/atvmod.h index c0a55fcac..b47847b20 100644 --- a/plugins/channeltx/modatv/atvmod.h +++ b/plugins/channeltx/modatv/atvmod.h @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2017 Edouard Griffiths, F4EXB // +// Copyright (C) 2019 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // @@ -19,32 +19,23 @@ #define PLUGINS_CHANNELTX_MODATV_ATVMOD_H_ #include +#include +#include -#include #include #include -#include -#include -#include - -#include - #include "dsp/basebandsamplesource.h" #include "channel/channelapi.h" -#include "dsp/nco.h" -#include "dsp/interpolator.h" -#include "util/movingaverage.h" -#include "dsp/fftfilt.h" #include "util/message.h" #include "atvmodsettings.h" class QNetworkAccessManager; class QNetworkReply; +class QThread; +class ATVModBaseband; class DeviceAPI; -class ThreadedBasebandSampleSource; -class UpChannelizer; class ATVMod : public BasebandSampleSource, public ChannelAPI { Q_OBJECT @@ -73,23 +64,50 @@ public: { } }; + /** + * |<------ Baseband from device (before device soft interpolation) -------------------------->| + * |<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->| + * | ^-------------------------------| + * | | Source CF + * | | Source SR | + */ class MsgConfigureChannelizer : public Message { MESSAGE_CLASS_DECLARATION public: - int getCenterFrequency() const { return m_centerFrequency; } + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } - static MsgConfigureChannelizer* create(int centerFrequency) - { - return new MsgConfigureChannelizer(centerFrequency); + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) { + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); } private: - int m_centerFrequency; + int m_sourceSampleRate; + int m_sourceCenterFrequency; - MsgConfigureChannelizer(int centerFrequency) : + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : Message(), - m_centerFrequency(centerFrequency) + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) + { } + }; + + class MsgConfigureSourceCenterFrequency : public Message { + MESSAGE_CLASS_DECLARATION + public: + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } + + static MsgConfigureSourceCenterFrequency *create(int sourceCenterFrequency) { + return new MsgConfigureSourceCenterFrequency(sourceCenterFrequency); + } + + private: + int m_sourceCenterFrequency; + + MsgConfigureSourceCenterFrequency(int sourceCenterFrequency) : + Message(), + m_sourceCenterFrequency(sourceCenterFrequency) { } }; @@ -173,52 +191,6 @@ public: { } }; - class MsgReportVideoFileSourceStreamTiming : public Message - { - MESSAGE_CLASS_DECLARATION - - public: - int getFrameCount() const { return m_frameCount; } - - static MsgReportVideoFileSourceStreamTiming* create(int frameCount) - { - return new MsgReportVideoFileSourceStreamTiming(frameCount); - } - - protected: - int m_frameCount; - - MsgReportVideoFileSourceStreamTiming(int frameCount) : - Message(), - m_frameCount(frameCount) - { } - }; - - class MsgReportVideoFileSourceStreamData : public Message { - MESSAGE_CLASS_DECLARATION - - public: - int getFrameRate() const { return m_frameRate; } - quint32 getVideoLength() const { return m_videoLength; } - - static MsgReportVideoFileSourceStreamData* create(int frameRate, - quint32 recordLength) - { - return new MsgReportVideoFileSourceStreamData(frameRate, recordLength); - } - - protected: - int m_frameRate; - int m_videoLength; //!< Video length in frames - - MsgReportVideoFileSourceStreamData(int frameRate, - int videoLength) : - Message(), - m_frameRate(frameRate), - m_videoLength(videoLength) - { } - }; - class MsgConfigureCameraIndex : public Message { MESSAGE_CLASS_DECLARATION @@ -270,99 +242,15 @@ public: { } }; - class MsgReportCameraData : public Message { - MESSAGE_CLASS_DECLARATION - - public: - int getdeviceNumber() const { return m_deviceNumber; } - float getFPS() const { return m_fps; } - float getFPSManual() const { return m_fpsManual; } - bool getFPSManualEnable() const { return m_fpsManualEnable; } - int getWidth() const { return m_width; } - int getHeight() const { return m_height; } - int getStatus() const { return m_status; } - - static MsgReportCameraData* create( - int deviceNumber, - float fps, - float fpsManual, - bool fpsManualEnable, - int width, - int height, - int status) - { - return new MsgReportCameraData( - deviceNumber, - fps, - fpsManual, - fpsManualEnable, - width, - height, - status); - } - - protected: - int m_deviceNumber; - float m_fps; - float m_fpsManual; - bool m_fpsManualEnable; - int m_width; - int m_height; - int m_status; - - MsgReportCameraData( - int deviceNumber, - float fps, - float fpsManual, - bool fpsManualEnable, - int width, - int height, - int status) : - Message(), - m_deviceNumber(deviceNumber), - m_fps(fps), - m_fpsManual(fpsManual), - m_fpsManualEnable(fpsManualEnable), - m_width(width), - m_height(height), - m_status(status) - { } - }; - - class MsgReportEffectiveSampleRate : public Message - { - MESSAGE_CLASS_DECLARATION - - public: - int getSampleRate() const { return m_sampleRate; } - uint32_t gatNbPointsPerLine() const { return m_nbPointsPerLine; } - - static MsgReportEffectiveSampleRate* create(int sampleRate, uint32_t nbPointsPerLine) - { - return new MsgReportEffectiveSampleRate(sampleRate, nbPointsPerLine); - } - - protected: - int m_sampleRate; - uint32_t m_nbPointsPerLine; - - MsgReportEffectiveSampleRate( - int sampleRate, - uint32_t nbPointsPerLine) : - Message(), - m_sampleRate(sampleRate), - m_nbPointsPerLine(nbPointsPerLine) - { } - }; + //================================================================= ATVMod(DeviceAPI *deviceAPI); ~ATVMod(); virtual void destroy() { delete this; } - virtual void pull(Sample& sample); - virtual void pullAudio(int nbSamples); // this is used for video signal actually virtual void start(); virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); virtual bool handleMessage(const Message& cmd); virtual void getIdentifier(QString& id) { id = objectName(); } @@ -405,441 +293,32 @@ public: const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response); - int getEffectiveSampleRate() const { return m_tvSampleRate; }; - double getMagSq() const { return m_movingAverage.asDouble(); } - void getCameraNumbers(std::vector& numbers); - static void getBaseValues(int outputSampleRate, int linesPerSecond, int& sampleRateUnits, uint32_t& nbPointsPerRateUnit); - static float getRFBandwidthDivisor(ATVModSettings::ATVModulation modulation); + uint32_t getNumberOfDeviceStreams() const; + double getMagSq() const; + void setLevelMeter(QObject *levelMeter); + int getEffectiveSampleRate() const; + void getCameraNumbers(std::vector& numbers); + void propagateMessageQueueToGUI(); static const QString m_channelIdURI; static const QString m_channelId; -signals: - /** - * Level changed - * \param rmsLevel RMS level in range 0.0 - 1.0 - * \param peakLevel Peak level in range 0.0 - 1.0 - * \param numSamples Number of audio samples analyzed - */ - void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); - -private slots: - void networkManagerFinished(QNetworkReply *reply); - private: - struct ATVCamera - { - cv::VideoCapture m_camera; //!< camera object - cv::Mat m_videoframeOriginal; //!< camera non resized image - cv::Mat m_videoFrame; //!< displayable camera frame - int m_cameraNumber; //!< camera device number - float m_videoFPS; //!< camera FPS rate - float m_videoFPSManual; //!< camera FPS rate manually set - bool m_videoFPSManualEnable; //!< Enable camera FPS rate manual set value - int m_videoWidth; //!< camera frame width - int m_videoHeight; //!< camera frame height - float m_videoFx; //!< camera horizontal scaling factor - float m_videoFy; //!< camera vertictal scaling factor - float m_videoFPSq; //!< camera FPS sacaling factor - float m_videoFPSqManual; //!< camera FPS sacaling factor manually set - float m_videoFPSCount; //!< camera FPS fractional counter - int m_videoPrevFPSCount; //!< camera FPS previous integer counter - - ATVCamera() : - m_cameraNumber(-1), - m_videoFPS(25.0f), - m_videoFPSManual(20.0f), - m_videoFPSManualEnable(false), - m_videoWidth(1), - m_videoHeight(1), - m_videoFx(1.0f), - m_videoFy(1.0f), - m_videoFPSq(1.0f), - m_videoFPSqManual(1.0f), - m_videoFPSCount(0.0f), - m_videoPrevFPSCount(0) - {} - }; - DeviceAPI* m_deviceAPI; - ThreadedBasebandSampleSource* m_threadedChannelizer; - UpChannelizer* m_channelizer; - - int m_outputSampleRate; - int m_inputFrequencyOffset; + QThread *m_thread; + ATVModBaseband* m_basebandSource; ATVModSettings m_settings; - NCO m_carrierNco; - Complex m_modSample; - float m_modPhasor; //!< For FM modulation - Interpolator m_interpolator; - Real m_interpolatorDistance; - Real m_interpolatorDistanceRemain; - int m_tvSampleRate; //!< sample rate for generating signal - uint32_t m_pointsPerLine; //!< Number of points per full line - int m_pointsPerSync; //!< number of line points for the horizontal sync - int m_pointsPerBP; //!< number of line points for the back porch - int m_pointsPerImgLine; //!< number of line points for the image line - uint32_t m_pointsPerFP; //!< number of line points for the front porch - int m_pointsPerFSync; //!< number of line points for the field first sync - uint32_t m_pointsPerHBar; //!< number of line points for a bar of the bar chart - uint32_t m_linesPerVBar; //!< number of lines for a bar of the bar chart - uint32_t m_pointsPerTU; //!< number of line points per time unit - int m_nbLines; //!< number of lines per complete frame - int m_nbLines2; //!< same number as above (non interlaced) or half the number above (interlaced) - uint32_t m_nbImageLines; //!< number of image lines excluding synchronization lines - uint32_t m_nbImageLines2; //!< same number as above (non interlaced) or half the number above (interlaced) - int m_nbHorizPoints; //!< number of line points per horizontal line - int m_nbSyncLinesHeadE; //!< number of header sync lines on even frame - int m_nbSyncLinesHeadO; //!< number of header sync lines on odd frame - int m_nbSyncLinesBottom;//!< number of sync lines at bottom - int m_nbLongSyncLines; //!< number of whole long sync lines for vertical synchronization - int m_nbHalfLongSync; //!< number of half long sync / equalization lines - int m_nbWholeEqLines; //!< number of whole equalizing lines - bool m_singleLongSync; //!< single or double long sync per long sync line - int m_nbBlankLines; //!< number of lines in a frame (full or half) that are blanked (black) at the top of the image - float m_blankLineLvel; //!< video level of blank lines - float m_hBarIncrement; //!< video level increment at each horizontal bar increment - float m_vBarIncrement; //!< video level increment at each vertical bar increment - bool m_interleaved; //!< true if image is interlaced (2 half frames per frame) - bool m_evenImage; //!< in interlaced mode true if this is an even image - QMutex m_settingsMutex; - int m_horizontalCount; //!< current point index on line - int m_lineCount; //!< current line index in frame - float m_fps; //!< resulting frames per second - - MovingAverageUtil m_movingAverage; - quint32 m_levelCalcCount; - Real m_peakLevel; - Real m_levelSum; - - cv::Mat m_imageFromFile; //!< original image not resized not overlaid by text - cv::Mat m_imageOriginal; //!< original not resized image - cv::Mat m_image; //!< resized image for transmission at given rate - bool m_imageOK; - - cv::VideoCapture m_video; //!< current video capture - cv::Mat m_videoframeOriginal; //!< current frame from video - cv::Mat m_videoFrame; //!< current displayable video frame - float m_videoFPS; //!< current video FPS rate - int m_videoWidth; //!< current video frame width - int m_videoHeight; //!< current video frame height - float m_videoFx; //!< current video horizontal scaling factor - float m_videoFy; //!< current video vertictal scaling factor - float m_videoFPSq; //!< current video FPS sacaling factor - float m_videoFPSCount; //!< current video FPS fractional counter - int m_videoPrevFPSCount; //!< current video FPS previous integer counter - int m_videoLength; //!< current video length in frames - bool m_videoEOF; //!< current video has reached end of file - bool m_videoOK; - - std::vector m_cameras; //!< vector of available cameras - int m_cameraIndex; //!< curent camera index in list of available cameras - - std::string m_overlayText; - QString m_imageFileName; - QString m_videoFileName; - - // Used for standard SSB - fftfilt* m_SSBFilter; - Complex* m_SSBFilterBuffer; - int m_SSBFilterBufferIndex; - - // Used for vestigial SSB with asymmetrical filtering (needs double sideband scheme) - fftfilt* m_DSBFilter; - Complex* m_DSBFilterBuffer; - int m_DSBFilterBufferIndex; - QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; - static const int m_ssbFftLen; - static const float m_blackLevel; - static const float m_spanLevel; - static const int m_levelNbSamples; - static const int m_nbBars; //!< number of bars in bar or chessboard patterns - static const int m_cameraFPSTestNbFrames; //!< number of frames for camera FPS test - - void applyChannelSettings(int outputSampleRate, int inputFrequencyOffset, bool force = false); void applySettings(const ATVModSettings& settings, bool force = false); - void pullFinalize(Complex& ci, Sample& sample); - void pullVideo(Real& sample); - void calculateLevel(Real& sample); - void modulateSample(); - Complex& modulateSSB(Real& sample); - Complex& modulateVestigialSSB(Real& sample); - void applyStandard(); - void openImage(const QString& fileName); - void openVideo(const QString& fileName); - void resizeImage(); - void calculateVideoSizes(); - void resizeVideo(); - void seekVideoFileStream(int seekPercentage); - void scanCameras(); - void releaseCameras(); - void calculateCamerasSizes(); - void resizeCameras(); - void resizeCamera(); - void mixImageAndText(cv::Mat& image); - void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); void webapiReverseSendSettings(QList& channelSettingsKeys, const ATVModSettings& settings, bool force); - inline void pullImageLine(Real& sample, bool noHSync = false) - { - if (m_horizontalCount < m_pointsPerSync) // sync pulse - { - sample = noHSync ? m_blackLevel : 0.0f; // ultra-black - } - else if (m_horizontalCount < m_pointsPerSync + m_pointsPerBP) // back porch - { - sample = m_blackLevel; // black - } - else if (m_horizontalCount < m_pointsPerSync + m_pointsPerBP + m_pointsPerImgLine) - { - int pointIndex = m_horizontalCount - (m_pointsPerSync + m_pointsPerBP); - int oddity = m_lineCount < m_nbLines2 + 1 ? 0 : 1; - int iLine = oddity == 0 ? m_lineCount : m_lineCount - m_nbLines2 - 1; - int iLineImage = iLine - m_nbBlankLines - (oddity == 0 ? m_nbSyncLinesHeadE : m_nbSyncLinesHeadO); - - switch(m_settings.m_atvModInput) - { - case ATVModSettings::ATVModInputHBars: - sample = (((float)pointIndex) / m_pointsPerHBar) * m_hBarIncrement + m_blackLevel; - break; - case ATVModSettings::ATVModInputVBars: - sample = (((float)iLine) / m_linesPerVBar) * m_vBarIncrement + m_blackLevel; - break; - case ATVModSettings::ATVModInputChessboard: - sample = (((iLine / m_linesPerVBar)*5 + (pointIndex / m_pointsPerHBar)) % 2) * m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; - break; - case ATVModSettings::ATVModInputHGradient: - sample = (pointIndex / (float) m_pointsPerImgLine) * m_spanLevel + m_blackLevel; - break; - case ATVModSettings::ATVModInputVGradient: - sample = ((iLine -5) / (float) m_nbImageLines2) * m_spanLevel + m_blackLevel; - break; - case ATVModSettings::ATVModInputImage: - if (!m_imageOK || (iLineImage < -oddity) || m_image.empty()) - { - sample = m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; - } - else - { - unsigned char pixv; - - if (m_interleaved) { - pixv = m_image.at(2*iLineImage + oddity, pointIndex); // row (y), col (x) - } else { - pixv = m_image.at(iLineImage, pointIndex); // row (y), col (x) - } - - sample = (pixv / 256.0f) * m_spanLevel + m_blackLevel; - } - break; - case ATVModSettings::ATVModInputVideo: - if (!m_videoOK || (iLineImage < -oddity) || m_videoFrame.empty()) - { - sample = m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; - } - else - { - unsigned char pixv; - - if (m_interleaved) { - pixv = m_videoFrame.at(2*iLineImage + oddity, pointIndex); // row (y), col (x) - } else { - pixv = m_videoFrame.at(iLineImage, pointIndex); // row (y), col (x) - } - - sample = (pixv / 256.0f) * m_spanLevel + m_blackLevel; - } - break; - case ATVModSettings::ATVModInputCamera: - if ((iLineImage < -oddity) || (m_cameraIndex < 0)) - { - sample = m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; - } - else - { - ATVCamera& camera = m_cameras[m_cameraIndex]; - - if (camera.m_videoFrame.empty()) - { - sample = m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; - } - else - { - unsigned char pixv; - - if (m_interleaved) { - pixv = camera.m_videoFrame.at(2*iLineImage + oddity, pointIndex); // row (y), col (x) - } else { - pixv = camera.m_videoFrame.at(iLineImage, pointIndex); // row (y), col (x) - } - - sample = (pixv / 256.0f) * m_spanLevel + m_blackLevel; - } - } - break; - case ATVModSettings::ATVModInputUniform: - default: - sample = m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; - } - } - else // front porch - { - sample = m_blackLevel; // black - } - } - - inline void pullVSyncLineLongPulses(Real& sample) - { - int halfIndex = m_horizontalCount % (m_nbHorizPoints/2); - - if (halfIndex < (m_nbHorizPoints/2) - m_pointsPerSync) // ultra-black - { - sample = 0.0f; - } - else // black - { - if (m_singleLongSync && (m_horizontalCount < m_nbHorizPoints/2)) { - sample = 0.0f; - } else { - sample = m_blackLevel; - } - } - } - - inline void pullVSyncLineEqualizingPulses(Real& sample) - { - if (m_horizontalCount < m_pointsPerSync) - { - sample = 0.0f; // ultra-black - } - else if (m_horizontalCount < (m_nbHorizPoints/2)) - { - sample = m_blackLevel; // black - } - else if (m_horizontalCount < (m_nbHorizPoints/2) + m_pointsPerFSync) - { - sample = 0.0f; // ultra-black - } - else - { - sample = m_blackLevel; // black - } - } - - inline void pullVSyncLineEqualizingThenLongPulses(Real& sample) - { - if (m_horizontalCount < m_pointsPerSync) - { - sample = 0.0f; // ultra-black - } - else if (m_horizontalCount < (m_nbHorizPoints/2)) - { - sample = m_blackLevel; // black - } - else if (m_horizontalCount < m_nbHorizPoints - m_pointsPerSync) - { - sample = 0.0f; // ultra-black - } - else - { - sample = m_blackLevel; // black - } - } - - inline void pullVSyncLineLongThenEqualizingPulses(Real& sample) - { - if (m_horizontalCount < (m_nbHorizPoints/2) - m_pointsPerSync) - { - sample = 0.0f; // ultra-black - } - else if (m_horizontalCount < (m_nbHorizPoints/2)) - { - sample = m_blackLevel; // black - } - else if (m_horizontalCount < (m_nbHorizPoints/2) + m_pointsPerFSync) - { - sample = 0.0f; // ultra-black - } - else - { - sample = m_blackLevel; // black - } - } - - inline void pullVSyncLine(Real& sample) - { - if (m_lineCount < m_nbLines2 + 1) // even - { - int fieldLine = m_lineCount; - - if (fieldLine < m_nbLongSyncLines) // 0,1: Whole line "long" pulses - { - pullVSyncLineLongPulses(sample); - } - else if (fieldLine < m_nbLongSyncLines + m_nbHalfLongSync) // long pulse then equalizing pulse - { - pullVSyncLineLongThenEqualizingPulses(sample); - } - else if (fieldLine < m_nbLongSyncLines + m_nbHalfLongSync + m_nbWholeEqLines) // Whole line equalizing pulses - { - pullVSyncLineEqualizingPulses(sample); - } - else if (fieldLine > m_nbLines2 - m_nbHalfLongSync) // equalizing pulse then long pulse - { - pullVSyncLineEqualizingThenLongPulses(sample); - } - else if (fieldLine > m_nbLines2 - m_nbHalfLongSync - m_nbWholeEqLines) // Whole line equalizing pulses - { - pullVSyncLineEqualizingPulses(sample); - } - else // black images - { - if (m_horizontalCount < m_pointsPerSync) - { - sample = 0.0f; - } - else - { - sample = m_blankLineLvel; - } - } - } - else // odd - { - int fieldLine = m_lineCount - m_nbLines2 - 1; - - if (fieldLine < m_nbLongSyncLines) // 0,1: Whole line "long" pulses - { - pullVSyncLineLongPulses(sample); - } - else if (fieldLine < m_nbLongSyncLines + m_nbWholeEqLines) // Whole line equalizing pulses - { - pullVSyncLineEqualizingPulses(sample); - } - else if (fieldLine > m_nbLines2 - 1 - m_nbWholeEqLines - m_nbHalfLongSync) // Whole line equalizing pulses - { - pullVSyncLineEqualizingPulses(sample); - } - else // black images - { - if (m_horizontalCount < m_pointsPerSync) - { - sample = 0.0f; - } - else - { - sample = m_blankLineLvel; - } - } - } - } +private slots: + void networkManagerFinished(QNetworkReply *reply); }; - -#endif /* PLUGINS_CHANNELTX_MODATV_ATVMOD_H_ */ +#endif /* PLUGINS_CHANNELTX_MODAM_AMMOD_H_ */ diff --git a/plugins/channeltx/modatv/atvmodbaseband.cpp b/plugins/channeltx/modatv/atvmodbaseband.cpp new file mode 100644 index 000000000..54fb59aa1 --- /dev/null +++ b/plugins/channeltx/modatv/atvmodbaseband.cpp @@ -0,0 +1,255 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "atvmodbaseband.h" + +MESSAGE_CLASS_DEFINITION(ATVModBaseband::MsgConfigureATVModBaseband, Message) +MESSAGE_CLASS_DEFINITION(ATVModBaseband::MsgConfigureChannelizer, Message) +MESSAGE_CLASS_DEFINITION(ATVModBaseband::MsgConfigureImageFileName, Message) +MESSAGE_CLASS_DEFINITION(ATVModBaseband::MsgConfigureVideoFileName, Message) +MESSAGE_CLASS_DEFINITION(ATVModBaseband::MsgConfigureVideoFileSourceSeek, Message) +MESSAGE_CLASS_DEFINITION(ATVModBaseband::MsgConfigureVideoFileSourceStreamTiming, Message) +MESSAGE_CLASS_DEFINITION(ATVModBaseband::MsgConfigureCameraIndex, Message) +MESSAGE_CLASS_DEFINITION(ATVModBaseband::MsgConfigureCameraData, Message) + +ATVModBaseband::ATVModBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpSampleChannelizer(&m_source); + + qDebug("AMModBaseband::AMModBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &ATVModBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +ATVModBaseband::~ATVModBaseband() +{ + delete m_channelizer; +} + +void ATVModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void ATVModBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void ATVModBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + Real rmsLevel, peakLevel, numSamples; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); +} + +void ATVModBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void ATVModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool ATVModBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureATVModBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureATVModBaseband& cfg = (MsgConfigureATVModBaseband&) cmd; + qDebug() << "AMModBaseband::handleMessage: MsgConfigureATVModBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureChannelizer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; + qDebug() << "ATVModBaseband::handleMessage: MsgConfigureChannelizer" + << "(requested) sourceSampleRate: " << cfg.getSourceSampleRate() + << "(requested) sourceCenterFrequency: " << cfg.getSourceCenterFrequency(); + m_channelizer->setChannelization(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "ATVModBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_sampleFifo.resize(4*SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (MsgConfigureImageFileName::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureImageFileName& cfg = (MsgConfigureImageFileName&) cmd; + qDebug() << "ATVModBaseband::handleMessage: MsgConfigureImageFileName: fileNaem: " << cfg.getFileName(); + m_source.openImage(cfg.getFileName()); + } + else if (MsgConfigureVideoFileName::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureVideoFileName& cfg = (MsgConfigureVideoFileName&) cmd; + qDebug() << "ATVModBaseband::handleMessage: MsgConfigureVideoFileName: fileName: " << cfg.getFileName(); + m_source.openVideo( cfg.getFileName()); + } + else if (MsgConfigureVideoFileSourceSeek::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureVideoFileSourceSeek& cfg = (MsgConfigureVideoFileSourceSeek&) cmd; + qDebug() << "ATVModBaseband::handleMessage: MsgConfigureVideoFileName: precnetage: " << cfg.getPercentage(); + m_source.seekVideoFileStream(cfg.getPercentage()); + } + else if (MsgConfigureVideoFileSourceStreamTiming::match(cmd)) + { + m_source.reportVideoFileSourceStreamTiming(); + } + else if (MsgConfigureCameraIndex::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureCameraIndex& cfg = (MsgConfigureCameraIndex&) cmd; + uint32_t index = cfg.getIndex() & 0x7FFFFFF; + m_source.configureCameraIndex(index); + } + else if (MsgConfigureCameraData::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureCameraData& cfg = (MsgConfigureCameraData&) cmd; + m_source.configureCameraData(cfg.getIndex(), cfg.getManualFPS(), cfg.getManualFPSEnable()); + } + else + { + return false; + } +} + +void ATVModBaseband::applySettings(const ATVModSettings& settings, bool force) +{ + qDebug() << "ATVModBaseband::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " m_rfOppBandwidth: " << settings.m_rfOppBandwidth + << " m_atvStd: " << (int) settings.m_atvStd + << " m_nbLines: " << settings.m_nbLines + << " m_fps: " << settings.m_fps + << " m_atvModInput: " << (int) settings.m_atvModInput + << " m_uniformLevel: " << settings.m_uniformLevel + << " m_atvModulation: " << (int) settings.m_atvModulation + << " m_videoPlayLoop: " << settings.m_videoPlayLoop + << " m_videoPlay: " << settings.m_videoPlay + << " m_cameraPlay: " << settings.m_cameraPlay + << " m_channelMute: " << settings.m_channelMute + << " m_invertedVideo: " << settings.m_invertedVideo + << " m_rfScalingFactor: " << settings.m_rfScalingFactor + << " m_fmExcursion: " << settings.m_fmExcursion + << " m_forceDecimator: " << settings.m_forceDecimator + << " m_showOverlayText: " << settings.m_showOverlayText + << " m_overlayText: " << settings.m_overlayText + << " force: " << force; + + m_source.applySettings(settings, force); + m_settings = settings; +} + +int ATVModBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} + +void ATVModBaseband::getCameraNumbers(std::vector& numbers) +{ + m_source.getCameraNumbers(numbers); +} \ No newline at end of file diff --git a/plugins/channeltx/modatv/atvmodbaseband.h b/plugins/channeltx/modatv/atvmodbaseband.h new file mode 100644 index 000000000..1358a0cca --- /dev/null +++ b/plugins/channeltx/modatv/atvmodbaseband.h @@ -0,0 +1,252 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_ATVMODBASEBAND_H +#define INCLUDE_ATVMODBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "atvmodsource.h" + +class UpSampleChannelizer; + +class ATVModBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureATVModBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const ATVModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureATVModBaseband* create(const ATVModSettings& settings, bool force) + { + return new MsgConfigureATVModBaseband(settings, force); + } + + private: + ATVModSettings m_settings; + bool m_force; + + MsgConfigureATVModBaseband(const ATVModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureChannelizer : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } + + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) + { + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); + } + + private: + int m_sourceSampleRate; + int m_sourceCenterFrequency; + + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : + Message(), + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) + { } + }; + + class MsgConfigureImageFileName : public Message + { + MESSAGE_CLASS_DECLARATION + + public: + const QString& getFileName() const { return m_fileName; } + + static MsgConfigureImageFileName* create(const QString& fileName) + { + return new MsgConfigureImageFileName(fileName); + } + + private: + QString m_fileName; + + MsgConfigureImageFileName(const QString& fileName) : + Message(), + m_fileName(fileName) + { } + }; + + class MsgConfigureVideoFileName : public Message + { + MESSAGE_CLASS_DECLARATION + + public: + const QString& getFileName() const { return m_fileName; } + + static MsgConfigureVideoFileName* create(const QString& fileName) + { + return new MsgConfigureVideoFileName(fileName); + } + + private: + QString m_fileName; + + MsgConfigureVideoFileName(const QString& fileName) : + Message(), + m_fileName(fileName) + { } + }; + + class MsgConfigureVideoFileSourceSeek : public Message + { + MESSAGE_CLASS_DECLARATION + + public: + int getPercentage() const { return m_seekPercentage; } + + static MsgConfigureVideoFileSourceSeek* create(int seekPercentage) + { + return new MsgConfigureVideoFileSourceSeek(seekPercentage); + } + + protected: + int m_seekPercentage; //!< percentage of seek position from the beginning 0..100 + + MsgConfigureVideoFileSourceSeek(int seekPercentage) : + Message(), + m_seekPercentage(seekPercentage) + { } + }; + + class MsgConfigureVideoFileSourceStreamTiming : public Message { + MESSAGE_CLASS_DECLARATION + + public: + + static MsgConfigureVideoFileSourceStreamTiming* create() + { + return new MsgConfigureVideoFileSourceStreamTiming(); + } + + private: + + MsgConfigureVideoFileSourceStreamTiming() : + Message() + { } + }; + + class MsgConfigureCameraIndex : public Message + { + MESSAGE_CLASS_DECLARATION + + public: + int getIndex() const { return m_index; } + + static MsgConfigureCameraIndex* create(int index) + { + return new MsgConfigureCameraIndex(index); + } + + private: + int m_index; + + MsgConfigureCameraIndex(int index) : + Message(), + m_index(index) + { } + }; + + class MsgConfigureCameraData : public Message + { + MESSAGE_CLASS_DECLARATION + + public: + int getIndex() const { return m_index; } + float getManualFPS() const { return m_manualFPS; } + bool getManualFPSEnable() const { return m_manualFPSEnable; } + + static MsgConfigureCameraData* create( + int index, + float manualFPS, + bool manualFPSEnable) + { + return new MsgConfigureCameraData(index, manualFPS, manualFPSEnable); + } + + private: + int m_index; + float m_manualFPS; + bool m_manualFPSEnable; + + MsgConfigureCameraData(int index, float manualFPS, bool manualFPSEnable) : + Message(), + m_index(index), + m_manualFPS(manualFPS), + m_manualFPSEnable(manualFPSEnable) + { } + }; + + ATVModBaseband(); + ~ATVModBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_source.setMessageQueueToGUI(messageQueue); } + double getMagSq() const { return m_source.getMagSq(); } + int getChannelSampleRate() const; + void getCameraNumbers(std::vector& numbers); + + int getEffectiveSampleRate() const { return m_source.getEffectiveSampleRate(); } + +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + SampleSourceFifo m_sampleFifo; + UpSampleChannelizer *m_channelizer; + ATVModSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + ATVModSettings m_settings; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const ATVModSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + + +#endif // INCLUDE_ATVMODBASEBAND_H diff --git a/plugins/channeltx/modatv/atvmodgui.cpp b/plugins/channeltx/modatv/atvmodgui.cpp index 04f1f9a0f..87a911fe0 100644 --- a/plugins/channeltx/modatv/atvmodgui.cpp +++ b/plugins/channeltx/modatv/atvmodgui.cpp @@ -34,6 +34,7 @@ #include "ui_atvmodgui.h" #include "atvmodgui.h" +#include "atvmodreport.h" ATVModGUI* ATVModGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx) { @@ -46,6 +47,80 @@ void ATVModGUI::destroy() delete this; } +ATVModGUI::ATVModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent) : + RollupWidget(parent), + ui(new Ui::ATVModGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_doApplySettings(true), + m_videoLength(0), + m_videoFrameRate(48000), + m_frameCount(0), + m_tickCount(0), + m_enableNavTime(false), + m_camBusyFPSMessageBox(0), + m_rfSliderDivisor(100000) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose, true); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_atvMod = (ATVMod*) channelTx; //new ATVMod(m_deviceUISet->m_deviceSinkAPI); + m_atvMod->setMessageQueueToGUI(getInputMessageQueue()); + m_atvMod->propagateMessageQueueToGUI(); + + connect(&MainWindow::getInstance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(m_settings.m_rgbColor); + m_channelMarker.setBandwidth(5000); + m_channelMarker.setCenterFrequency(0); + m_channelMarker.setTitle("ATV Modulator"); + m_channelMarker.setSourceOrSinkStream(false); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + setTitleColor(m_channelMarker.getColor()); + m_settings.setChannelMarker(&m_channelMarker); + + m_deviceUISet->registerTxChannelInstance(ATVMod::m_channelIdURI, this); + m_deviceUISet->addChannelMarker(&m_channelMarker); + m_deviceUISet->addRollupWidget(this); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + + resetToDefaults(); + + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); + m_atvMod->setLevelMeter(ui->volumeMeter); + + std::vector cameraNumbers; + m_atvMod->getCameraNumbers(cameraNumbers); + + for (std::vector::iterator it = cameraNumbers.begin(); it != cameraNumbers.end(); ++it) { + ui->camSelect->addItem(tr("%1").arg(*it)); + } + + QChar delta = QChar(0x94, 0x03); + ui->fmExcursionLabel->setText(delta); + + displaySettings(); + applySettings(true); +} + +ATVModGUI::~ATVModGUI() +{ + m_deviceUISet->removeTxChannelInstance(this); + delete m_atvMod; // TODO: check this: when the GUI closes it has to delete the modulator + delete ui; +} + void ATVModGUI::setName(const QString& name) { setObjectName(name); @@ -95,23 +170,23 @@ bool ATVModGUI::deserialize(const QByteArray& data) bool ATVModGUI::handleMessage(const Message& message) { - if (ATVMod::MsgReportVideoFileSourceStreamData::match(message)) + if (ATVModReport::MsgReportVideoFileSourceStreamData::match(message)) { - m_videoFrameRate = ((ATVMod::MsgReportVideoFileSourceStreamData&)message).getFrameRate(); - m_videoLength = ((ATVMod::MsgReportVideoFileSourceStreamData&)message).getVideoLength(); + m_videoFrameRate = ((ATVModReport::MsgReportVideoFileSourceStreamData&)message).getFrameRate(); + m_videoLength = ((ATVModReport::MsgReportVideoFileSourceStreamData&)message).getVideoLength(); m_frameCount = 0; updateWithStreamData(); return true; } - else if (ATVMod::MsgReportVideoFileSourceStreamTiming::match(message)) + else if (ATVModReport::MsgReportVideoFileSourceStreamTiming::match(message)) { - m_frameCount = ((ATVMod::MsgReportVideoFileSourceStreamTiming&)message).getFrameCount(); + m_frameCount = ((ATVModReport::MsgReportVideoFileSourceStreamTiming&)message).getFrameCount(); updateWithStreamTime(); return true; } - else if (ATVMod::MsgReportCameraData::match(message)) + else if (ATVModReport::MsgReportCameraData::match(message)) { - ATVMod::MsgReportCameraData& rpt = (ATVMod::MsgReportCameraData&) message; + ATVModReport::MsgReportCameraData& rpt = (ATVModReport::MsgReportCameraData&) message; ui->cameraDeviceNumber->setText(tr("#%1").arg(rpt.getdeviceNumber())); ui->camerFPS->setText(tr("%1 FPS").arg(rpt.getFPS(), 0, 'f', 2)); ui->cameraImageSize->setText(tr("%1x%2").arg(rpt.getWidth()).arg(rpt.getHeight())); @@ -141,10 +216,10 @@ bool ATVModGUI::handleMessage(const Message& message) return true; } - else if (ATVMod::MsgReportEffectiveSampleRate::match(message)) + else if (ATVModReport::MsgReportEffectiveSampleRate::match(message)) { - int sampleRate = ((ATVMod::MsgReportEffectiveSampleRate&)message).getSampleRate(); - uint32_t nbPointsPerLine = ((ATVMod::MsgReportEffectiveSampleRate&)message).gatNbPointsPerLine(); + int sampleRate = ((ATVModReport::MsgReportEffectiveSampleRate&)message).getSampleRate(); + uint32_t nbPointsPerLine = ((ATVModReport::MsgReportEffectiveSampleRate&)message).gatNbPointsPerLine(); ui->channelSampleRateText->setText(tr("%1k").arg(sampleRate/1000.0f, 0, 'f', 2)); ui->nbPointsPerLineText->setText(tr("%1p").arg(nbPointsPerLine)); setRFFiltersSlidersRange(sampleRate); @@ -644,79 +719,6 @@ void ATVModGUI::onMenuDialogCalled(const QPoint &p) resetContextMenuType(); } -ATVModGUI::ATVModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent) : - RollupWidget(parent), - ui(new Ui::ATVModGUI), - m_pluginAPI(pluginAPI), - m_deviceUISet(deviceUISet), - m_channelMarker(this), - m_doApplySettings(true), - m_videoLength(0), - m_videoFrameRate(48000), - m_frameCount(0), - m_tickCount(0), - m_enableNavTime(false), - m_camBusyFPSMessageBox(0), - m_rfSliderDivisor(100000) -{ - ui->setupUi(this); - setAttribute(Qt::WA_DeleteOnClose, true); - connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); - connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); - - m_atvMod = (ATVMod*) channelTx; //new ATVMod(m_deviceUISet->m_deviceSinkAPI); - m_atvMod->setMessageQueueToGUI(getInputMessageQueue()); - - connect(&MainWindow::getInstance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); - - ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); - ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); - ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); - - m_channelMarker.blockSignals(true); - m_channelMarker.setColor(m_settings.m_rgbColor); - m_channelMarker.setBandwidth(5000); - m_channelMarker.setCenterFrequency(0); - m_channelMarker.setTitle("ATV Modulator"); - m_channelMarker.setSourceOrSinkStream(false); - m_channelMarker.blockSignals(false); - m_channelMarker.setVisible(true); // activate signal on the last setting only - - setTitleColor(m_channelMarker.getColor()); - m_settings.setChannelMarker(&m_channelMarker); - - m_deviceUISet->registerTxChannelInstance(ATVMod::m_channelIdURI, this); - m_deviceUISet->addChannelMarker(&m_channelMarker); - m_deviceUISet->addRollupWidget(this); - - connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); - - resetToDefaults(); - - connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); - connect(m_atvMod, SIGNAL(levelChanged(qreal, qreal, int)), ui->volumeMeter, SLOT(levelChanged(qreal, qreal, int))); - - std::vector cameraNumbers; - m_atvMod->getCameraNumbers(cameraNumbers); - - for (std::vector::iterator it = cameraNumbers.begin(); it != cameraNumbers.end(); ++it) { - ui->camSelect->addItem(tr("%1").arg(*it)); - } - - QChar delta = QChar(0x94, 0x03); - ui->fmExcursionLabel->setText(delta); - - displaySettings(); - applySettings(true); -} - -ATVModGUI::~ATVModGUI() -{ - m_deviceUISet->removeTxChannelInstance(this); - delete m_atvMod; // TODO: check this: when the GUI closes it has to delete the modulator - delete ui; -} - void ATVModGUI::blockApplySettings(bool block) { m_doApplySettings = !block; @@ -726,7 +728,7 @@ void ATVModGUI::applySettings(bool force) { if (m_doApplySettings) { - ATVMod::MsgConfigureChannelizer *msgChan = ATVMod::MsgConfigureChannelizer::create( + ATVMod::MsgConfigureSourceCenterFrequency *msgChan = ATVMod::MsgConfigureSourceCenterFrequency::create( m_channelMarker.getCenterFrequency()); m_atvMod->getInputMessageQueue()->push(msgChan); diff --git a/plugins/channeltx/modatv/atvmodplugin.cpp b/plugins/channeltx/modatv/atvmodplugin.cpp index 4ca13b678..0f77b0fca 100644 --- a/plugins/channeltx/modatv/atvmodplugin.cpp +++ b/plugins/channeltx/modatv/atvmodplugin.cpp @@ -27,7 +27,7 @@ const PluginDescriptor ATVModPlugin::m_pluginDescriptor = { QString("ATV Modulator"), - QString("4.11.6"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/channeltx/modatv/atvmodreport.cpp b/plugins/channeltx/modatv/atvmodreport.cpp new file mode 100644 index 000000000..d358ce935 --- /dev/null +++ b/plugins/channeltx/modatv/atvmodreport.cpp @@ -0,0 +1,29 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "atvmodreport.h" + +MESSAGE_CLASS_DEFINITION(ATVModReport::MsgReportVideoFileSourceStreamTiming, Message) +MESSAGE_CLASS_DEFINITION(ATVModReport::MsgReportVideoFileSourceStreamData, Message) +MESSAGE_CLASS_DEFINITION(ATVModReport::MsgReportCameraData, Message) +MESSAGE_CLASS_DEFINITION(ATVModReport::MsgReportEffectiveSampleRate, Message) + +ATVModReport::ATVModReport() +{ } + +ATVModReport::~ATVModReport() +{ } \ No newline at end of file diff --git a/plugins/channeltx/modatv/atvmodreport.h b/plugins/channeltx/modatv/atvmodreport.h new file mode 100644 index 000000000..ec3a6594c --- /dev/null +++ b/plugins/channeltx/modatv/atvmodreport.h @@ -0,0 +1,165 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODATV_ATVMODREPORT_H_ +#define PLUGINS_CHANNELTX_MODATV_ATVMODREPORT_H_ + +#include +#include +#include "util/message.h" + +class ATVModReport : public QObject +{ + Q_OBJECT +public: + class MsgReportVideoFileSourceStreamTiming : public Message + { + MESSAGE_CLASS_DECLARATION + + public: + int getFrameCount() const { return m_frameCount; } + + static MsgReportVideoFileSourceStreamTiming* create(int frameCount) + { + return new MsgReportVideoFileSourceStreamTiming(frameCount); + } + + protected: + int m_frameCount; + + MsgReportVideoFileSourceStreamTiming(int frameCount) : + Message(), + m_frameCount(frameCount) + { } + }; + + class MsgReportVideoFileSourceStreamData : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getFrameRate() const { return m_frameRate; } + quint32 getVideoLength() const { return m_videoLength; } + + static MsgReportVideoFileSourceStreamData* create(int frameRate, + quint32 recordLength) + { + return new MsgReportVideoFileSourceStreamData(frameRate, recordLength); + } + + protected: + int m_frameRate; + int m_videoLength; //!< Video length in frames + + MsgReportVideoFileSourceStreamData(int frameRate, + int videoLength) : + Message(), + m_frameRate(frameRate), + m_videoLength(videoLength) + { } + }; + + class MsgReportCameraData : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getdeviceNumber() const { return m_deviceNumber; } + float getFPS() const { return m_fps; } + float getFPSManual() const { return m_fpsManual; } + bool getFPSManualEnable() const { return m_fpsManualEnable; } + int getWidth() const { return m_width; } + int getHeight() const { return m_height; } + int getStatus() const { return m_status; } + + static MsgReportCameraData* create( + int deviceNumber, + float fps, + float fpsManual, + bool fpsManualEnable, + int width, + int height, + int status) + { + return new MsgReportCameraData( + deviceNumber, + fps, + fpsManual, + fpsManualEnable, + width, + height, + status); + } + + protected: + int m_deviceNumber; + float m_fps; + float m_fpsManual; + bool m_fpsManualEnable; + int m_width; + int m_height; + int m_status; + + MsgReportCameraData( + int deviceNumber, + float fps, + float fpsManual, + bool fpsManualEnable, + int width, + int height, + int status) : + Message(), + m_deviceNumber(deviceNumber), + m_fps(fps), + m_fpsManual(fpsManual), + m_fpsManualEnable(fpsManualEnable), + m_width(width), + m_height(height), + m_status(status) + { } + }; + + class MsgReportEffectiveSampleRate : public Message + { + MESSAGE_CLASS_DECLARATION + + public: + int getSampleRate() const { return m_sampleRate; } + uint32_t gatNbPointsPerLine() const { return m_nbPointsPerLine; } + + static MsgReportEffectiveSampleRate* create(int sampleRate, uint32_t nbPointsPerLine) + { + return new MsgReportEffectiveSampleRate(sampleRate, nbPointsPerLine); + } + + protected: + int m_sampleRate; + uint32_t m_nbPointsPerLine; + + MsgReportEffectiveSampleRate( + int sampleRate, + uint32_t nbPointsPerLine) : + Message(), + m_sampleRate(sampleRate), + m_nbPointsPerLine(nbPointsPerLine) + { } + }; + +public: + ATVModReport(); + ~ATVModReport(); +}; + +#endif // PLUGINS_CHANNELTX_MODATV_ATVMODREPORT_H_ diff --git a/plugins/channeltx/modatv/atvmodsource.cpp b/plugins/channeltx/modatv/atvmodsource.cpp new file mode 100644 index 000000000..5f798ab34 --- /dev/null +++ b/plugins/channeltx/modatv/atvmodsource.cpp @@ -0,0 +1,1073 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include +#include + +#include "opencv2/imgproc/imgproc.hpp" + +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "util/db.h" +#include "util/messagequeue.h" + +#include "atvmodreport.h" +#include "atvmodsource.h" + +const float ATVModSource::m_blackLevel = 0.3f; +const float ATVModSource::m_spanLevel = 0.7f; +const int ATVModSource::m_levelNbSamples = 10000; // every 10ms +const int ATVModSource::m_nbBars = 6; +const int ATVModSource::m_cameraFPSTestNbFrames = 100; +const int ATVModSource::m_ssbFftLen = 1024; + +ATVModSource::ATVModSource() : + m_outputSampleRate(1000000), + m_inputFrequencyOffset(0), + m_modPhasor(0.0f), + m_tvSampleRate(1000000), + m_evenImage(true), + m_settingsMutex(QMutex::Recursive), + m_horizontalCount(0), + m_lineCount(0), + m_imageOK(false), + m_videoFPSq(1.0f), + m_videoFPSCount(0.0f), + m_videoPrevFPSCount(0), + m_videoEOF(false), + m_videoOK(false), + m_cameraIndex(-1), + //m_showOverlayText(false), + m_SSBFilter(nullptr), + m_SSBFilterBuffer(nullptr), + m_SSBFilterBufferIndex(0), + m_DSBFilter(nullptr), + m_DSBFilterBuffer(nullptr), + m_DSBFilterBufferIndex(0), + m_messageQueueToGUI(nullptr) +{ + scanCameras(); + + m_SSBFilter = new fftfilt(0, m_settings.m_rfBandwidth / m_outputSampleRate, m_ssbFftLen); + m_SSBFilterBuffer = new Complex[m_ssbFftLen>>1]; // filter returns data exactly half of its size + memset(m_SSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen>>1)); + + m_DSBFilter = new fftfilt((2.0f * m_settings.m_rfBandwidth) / m_outputSampleRate, 2 * m_ssbFftLen); + m_DSBFilterBuffer = new Complex[m_ssbFftLen]; + memset(m_DSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen)); + + m_interpolatorDistanceRemain = 0.0f; + m_interpolatorDistance = 1.0f; + + applyChannelSettings(m_outputSampleRate, m_inputFrequencyOffset, true); + applySettings(m_settings, true); // does applyStandard() too; +} + +ATVModSource::~ATVModSource() +{ + if (m_video.isOpened()) { + m_video.release(); + } + + releaseCameras(); + delete m_SSBFilter; + delete m_DSBFilter; + delete[] m_SSBFilterBuffer; + delete[] m_DSBFilterBuffer; +} + +void ATVModSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void ATVModSource::prefetch(unsigned int nbSamples) +{ + (void) nbSamples; +} + +void ATVModSource::pullOne(Sample& sample) +{ + if (m_settings.m_channelMute) + { + sample.m_real = 0.0f; + sample.m_imag = 0.0f; + return; + } + + Complex ci; + + m_settingsMutex.lock(); + + if ((m_tvSampleRate == m_outputSampleRate) && (!m_settings.m_forceDecimator)) // no interpolation nor decimation + { + modulateSample(); + pullFinalize(m_modSample, sample); + } + else + { + if (m_interpolatorDistance > 1.0f) // decimate + { + modulateSample(); + + while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + else + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + + m_interpolatorDistanceRemain += m_interpolatorDistance; + pullFinalize(ci, sample); + } +} + +void ATVModSource::pullFinalize(Complex& ci, Sample& sample) +{ + ci *= m_carrierNco.nextIQ(); // shift to carrier frequency + + m_settingsMutex.unlock(); + + double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); + magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); + m_movingAverage(magsq); + + sample.m_real = (FixReal) ci.real(); + sample.m_imag = (FixReal) ci.imag(); +} + +void ATVModSource::modulateSample() +{ + Real t; + + pullVideo(t); + calculateLevel(t); + + t = m_settings.m_invertedVideo ? 1.0f - t : t; + + switch (m_settings.m_atvModulation) + { + case ATVModSettings::ATVModulationFM: // FM half bandwidth deviation + m_modPhasor += (t - 0.5f) * m_settings.m_fmExcursion * 2.0f * M_PI; + if (m_modPhasor > 2.0f * M_PI) m_modPhasor -= 2.0f * M_PI; // limit growth + if (m_modPhasor < 2.0f * M_PI) m_modPhasor += 2.0f * M_PI; // limit growth + m_modSample.real(cos(m_modPhasor) * m_settings.m_rfScalingFactor); // -1 dB + m_modSample.imag(sin(m_modPhasor) * m_settings.m_rfScalingFactor); + break; + case ATVModSettings::ATVModulationLSB: + case ATVModSettings::ATVModulationUSB: + m_modSample = modulateSSB(t); + m_modSample *= m_settings.m_rfScalingFactor; + break; + case ATVModSettings::ATVModulationVestigialLSB: + case ATVModSettings::ATVModulationVestigialUSB: + m_modSample = modulateVestigialSSB(t); + m_modSample *= m_settings.m_rfScalingFactor; + break; + case ATVModSettings::ATVModulationAM: // AM 90% + default: + m_modSample.real((t*1.8f + 0.1f) * (m_settings.m_rfScalingFactor/2.0f)); // modulate and scale zero frequency carrier + m_modSample.imag(0.0f); + } +} + +Complex& ATVModSource::modulateSSB(Real& sample) +{ + int n_out; + Complex ci(sample, 0.0f); + fftfilt::cmplx *filtered; + + n_out = m_SSBFilter->runSSB(ci, &filtered, m_settings.m_atvModulation == ATVModSettings::ATVModulationUSB); + + if (n_out > 0) + { + memcpy((void *) m_SSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); + m_SSBFilterBufferIndex = 0; + } + + m_SSBFilterBufferIndex++; + + return m_SSBFilterBuffer[m_SSBFilterBufferIndex-1]; +} + +Complex& ATVModSource::modulateVestigialSSB(Real& sample) +{ + int n_out; + Complex ci(sample, 0.0f); + fftfilt::cmplx *filtered; + + n_out = m_DSBFilter->runAsym(ci, &filtered, m_settings.m_atvModulation == ATVModSettings::ATVModulationVestigialUSB); + + if (n_out > 0) + { + memcpy((void *) m_DSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); + m_DSBFilterBufferIndex = 0; + } + + m_DSBFilterBufferIndex++; + + return m_DSBFilterBuffer[m_DSBFilterBufferIndex-1]; +} + +void ATVModSource::pullVideo(Real& sample) +{ + if ((m_settings.m_atvStd == ATVModSettings::ATVStdHSkip) && (m_lineCount == m_nbLines2)) // last line in skip mode + { + pullImageLine(sample, true); // pull image line without sync + } + else if (m_lineCount < m_nbLines2 + 1) // even image or non interlaced + { + int iLine = m_lineCount; + + if (iLine < m_nbSyncLinesHeadE + m_nbBlankLines) + { + pullVSyncLine(sample); + } + else if (iLine > m_nbLines2 - m_nbSyncLinesBottom) + { + pullVSyncLine(sample); + } + else + { + pullImageLine(sample); + } + } + else // odd image + { + int iLine = m_lineCount - m_nbLines2 - 1; + + if (iLine < m_nbSyncLinesHeadO + m_nbBlankLines) + { + pullVSyncLine(sample); + } + else if (iLine > m_nbLines2 - 1 - m_nbSyncLinesBottom) + { + pullVSyncLine(sample); + } + else + { + pullImageLine(sample); + } + } + + if (m_horizontalCount < m_nbHorizPoints - 1) + { + m_horizontalCount++; + } + else + { + if (m_lineCount < m_nbLines - 1) + { + m_lineCount++; + if (m_lineCount > (m_nbLines/2)) m_evenImage = !m_evenImage; + } + else // new image + { + m_lineCount = 0; + m_evenImage = !m_evenImage; + + if ((m_settings.m_atvModInput == ATVModSettings::ATVModInputVideo) && m_videoOK && (m_settings.m_videoPlay) && !m_videoEOF) + { + int grabOK = 0; + int fpsIncrement = (int) m_videoFPSCount - m_videoPrevFPSCount; + + // move a number of frames according to increment + // use grab to test for EOF then retrieve to preserve last valid frame as the current original frame + // TODO: handle pause (no move) + for (int i = 0; i < fpsIncrement; i++) + { + grabOK = m_video.grab(); + if (!grabOK) break; + } + + if (grabOK) + { + cv::Mat colorFrame; + m_video.retrieve(colorFrame); + + if (!colorFrame.empty()) // some frames may not come out properly + { + if (m_settings.m_showOverlayText) { + mixImageAndText(colorFrame); + } + + cv::cvtColor(colorFrame, m_videoframeOriginal, CV_BGR2GRAY); + resizeVideo(); + } + } + else + { + if (m_settings.m_videoPlayLoop) { // play loop + seekVideoFileStream(0); + } else { // stops + m_videoEOF = true; + } + } + + if (m_videoFPSCount < m_videoFPS) + { + m_videoPrevFPSCount = (int) m_videoFPSCount; + m_videoFPSCount += m_videoFPSq; + } + else + { + m_videoPrevFPSCount = 0; + m_videoFPSCount = m_videoFPSq; + } + } + else if ((m_settings.m_atvModInput == ATVModSettings::ATVModInputCamera) && (m_settings.m_cameraPlay)) + { + ATVCamera& camera = m_cameras[m_cameraIndex]; + + if (camera.m_videoFPS < 0.0f) // default frame rate when it could not be obtained via get + { + time_t start, end; + cv::Mat frame; + + if (getMessageQueueToGUI()) + { + ATVModReport::MsgReportCameraData *report; + report = ATVModReport::MsgReportCameraData::create( + camera.m_cameraNumber, + 0.0f, + camera.m_videoFPSManual, + camera.m_videoFPSManualEnable, + camera.m_videoWidth, + camera.m_videoHeight, + 1); // open splash screen on GUI side + getMessageQueueToGUI()->push(report); + } + + int nbFrames = 0; + + time(&start); + + for (int i = 0; i < m_cameraFPSTestNbFrames; i++) + { + camera.m_camera >> frame; + if (!frame.empty()) nbFrames++; + } + + time(&end); + + double seconds = difftime (end, start); + // take a 10% guard and divide bandwidth between all cameras as a hideous hack + camera.m_videoFPS = ((nbFrames / seconds) * 0.9) / m_cameras.size(); + camera.m_videoFPSq = camera.m_videoFPS / m_fps; + camera.m_videoFPSCount = camera.m_videoFPSq; + camera.m_videoPrevFPSCount = 0; + + if (getMessageQueueToGUI()) + { + ATVModReport::MsgReportCameraData *report; + report = ATVModReport::MsgReportCameraData::create( + camera.m_cameraNumber, + camera.m_videoFPS, + camera.m_videoFPSManual, + camera.m_videoFPSManualEnable, + camera.m_videoWidth, + camera.m_videoHeight, + 2); // close splash screen on GUI side + getMessageQueueToGUI()->push(report); + } + } + else if (camera.m_videoFPS == 0.0f) // Hideous hack for windows + { + camera.m_videoFPS = 5.0f; + camera.m_videoFPSq = camera.m_videoFPS / m_fps; + camera.m_videoFPSCount = camera.m_videoFPSq; + camera.m_videoPrevFPSCount = 0; + + if (getMessageQueueToGUI()) + { + ATVModReport::MsgReportCameraData *report; + report = ATVModReport::MsgReportCameraData::create( + camera.m_cameraNumber, + camera.m_videoFPS, + camera.m_videoFPSManual, + camera.m_videoFPSManualEnable, + camera.m_videoWidth, + camera.m_videoHeight, + 0); + getMessageQueueToGUI()->push(report); + } + } + + int fpsIncrement = (int) camera.m_videoFPSCount - camera.m_videoPrevFPSCount; + + // move a number of frames according to increment + // use grab to test for EOF then retrieve to preserve last valid frame as the current original frame + cv::Mat colorFrame; + int grabOK = 0; + + for (int i = 0; i < fpsIncrement; i++) + { + grabOK = camera.m_camera.grab(); + if (!grabOK) break; + // camera.m_camera >> colorFrame; + // if (colorFrame.empty()) break; + } + + if (grabOK) { + camera.m_camera.retrieve(colorFrame); + } + + if (!colorFrame.empty()) // some frames may not come out properly + { + if (m_settings.m_showOverlayText) { + mixImageAndText(colorFrame); + } + + cv::cvtColor(colorFrame, camera.m_videoframeOriginal, CV_BGR2GRAY); + resizeCamera(); + } + + if (camera.m_videoFPSCount < (camera.m_videoFPSManualEnable ? camera.m_videoFPSManual : camera.m_videoFPS)) + { + camera.m_videoPrevFPSCount = (int) camera.m_videoFPSCount; + camera.m_videoFPSCount += (camera.m_videoFPSManualEnable ? camera.m_videoFPSqManual : camera.m_videoFPSq); + } + else + { + camera.m_videoPrevFPSCount = 0; + camera.m_videoFPSCount = (camera.m_videoFPSManualEnable ? camera.m_videoFPSqManual : camera.m_videoFPSq); + } + } + } + + m_horizontalCount = 0; + } +} + +void ATVModSource::calculateLevel(Real& sample) +{ + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), sample); + m_levelSum += sample * sample; + m_levelCalcCount++; + } + else + { + m_rmsLevel = std::sqrt(m_levelSum / m_levelNbSamples); + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void ATVModSource::getBaseValues(int outputSampleRate, int linesPerSecond, int& sampleRateUnits, uint32_t& nbPointsPerRateUnit) +{ + int maxPoints = outputSampleRate / linesPerSecond; + int i = maxPoints; + + for (; i > 0; i--) + { + if ((i * linesPerSecond) % 10 == 0) + break; + } + + nbPointsPerRateUnit = i == 0 ? maxPoints : i; + sampleRateUnits = nbPointsPerRateUnit * linesPerSecond; +} + +float ATVModSource::getRFBandwidthDivisor(ATVModSettings::ATVModulation modulation) +{ + switch(modulation) + { + case ATVModSettings::ATVModulationLSB: + case ATVModSettings::ATVModulationUSB: + case ATVModSettings::ATVModulationVestigialLSB: + case ATVModSettings::ATVModulationVestigialUSB: + return 1.05f; + break; + case ATVModSettings::ATVModulationAM: + case ATVModSettings::ATVModulationFM: + default: + return 2.2f; + } +} + +void ATVModSource::applyStandard() +{ + m_pointsPerSync = (uint32_t) ((4.7f / 64.0f) * m_pointsPerLine); + m_pointsPerBP = (uint32_t) ((4.7f / 64.0f) * m_pointsPerLine); + m_pointsPerFP = (uint32_t) ((2.6f / 64.0f) * m_pointsPerLine); + m_pointsPerFSync = (uint32_t) ((2.3f / 64.0f) * m_pointsPerLine); + + m_pointsPerImgLine = m_pointsPerLine - m_pointsPerSync - m_pointsPerBP - m_pointsPerFP; + m_nbHorizPoints = m_pointsPerLine; + + m_pointsPerHBar = m_pointsPerImgLine / m_nbBars; + m_hBarIncrement = m_spanLevel / (float) m_nbBars; + m_vBarIncrement = m_spanLevel / (float) m_nbBars; + + m_nbLines = m_settings.m_nbLines; + m_nbLines2 = m_nbLines / 2; + m_fps = m_settings.m_fps * 1.0f; + +// qDebug() << "ATVMod::applyStandard: " +// << " m_nbLines: " << m_config.m_nbLines +// << " m_fps: " << m_config.m_fps +// << " rateUnits: " << rateUnits +// << " nbPointsPerRateUnit: " << nbPointsPerRateUnit +// << " m_tvSampleRate: " << m_tvSampleRate +// << " m_pointsPerTU: " << m_pointsPerTU; + + switch(m_settings.m_atvStd) + { + case ATVModSettings::ATVStdHSkip: + m_nbImageLines = m_nbLines; // lines less the total number of sync lines + m_nbImageLines2 = m_nbImageLines; // force non interleaved for vbars + m_interleaved = false; + m_nbSyncLinesHeadE = 0; // number of sync lines on the top of a frame even + m_nbSyncLinesHeadO = 0; // number of sync lines on the top of a frame odd + m_nbSyncLinesBottom = -1; // force no vsync in even block + m_nbLongSyncLines = 0; + m_nbHalfLongSync = 0; + m_nbWholeEqLines = 0; + m_singleLongSync = true; + m_nbBlankLines = 0; + m_blankLineLvel = 0.7f; + m_nbLines2 = m_nbLines - 1; + break; + case ATVModSettings::ATVStdShort: + m_nbImageLines = m_nbLines - 2; // lines less the total number of sync lines + m_nbImageLines2 = m_nbImageLines; // force non interleaved for vbars + m_interleaved = false; + m_nbSyncLinesHeadE = 1; // number of sync lines on the top of a frame even + m_nbSyncLinesHeadO = 1; // number of sync lines on the top of a frame odd + m_nbSyncLinesBottom = 0; + m_nbLongSyncLines = 1; + m_nbHalfLongSync = 0; + m_nbWholeEqLines = 0; + m_singleLongSync = true; + m_nbBlankLines = 1; + m_blankLineLvel = 0.7f; + m_nbLines2 = m_nbLines; // force non interleaved => treated as even for all lines + break; + case ATVModSettings::ATVStdShortInterleaved: + m_nbImageLines = m_nbLines - 2; // lines less the total number of sync lines + m_nbImageLines2 = m_nbImageLines / 2; + m_interleaved = true; + m_nbSyncLinesHeadE = 1; // number of sync lines on the top of a frame even + m_nbSyncLinesHeadO = 1; // number of sync lines on the top of a frame odd + m_nbSyncLinesBottom = 0; + m_nbLongSyncLines = 1; + m_nbHalfLongSync = 0; + m_nbWholeEqLines = 0; + m_singleLongSync = true; + m_nbBlankLines = 1; + m_blankLineLvel = 0.7f; + break; + case ATVModSettings::ATVStd405: // Follows loosely the 405 lines standard + m_nbImageLines = m_nbLines - 15; // lines less the total number of sync lines + m_nbImageLines2 = m_nbImageLines / 2; + m_interleaved = true; + m_nbSyncLinesHeadE = 5; // number of sync lines on the top of a frame even + m_nbSyncLinesHeadO = 4; // number of sync lines on the top of a frame odd + m_nbSyncLinesBottom = 3; + m_nbLongSyncLines = 2; + m_nbHalfLongSync = 1; + m_nbWholeEqLines = 2; + m_singleLongSync = false; + m_nbBlankLines = 7; // yields 376 lines (195 - 7) * 2 + m_blankLineLvel = m_blackLevel; + break; + case ATVModSettings::ATVStdPAL525: // Follows PAL-M standard + m_nbImageLines = m_nbLines - 15; + m_nbImageLines2 = m_nbImageLines / 2; + m_interleaved = true; + m_nbSyncLinesHeadE = 5; + m_nbSyncLinesHeadO = 4; // number of sync lines on the top of a frame odd + m_nbSyncLinesBottom = 3; + m_nbLongSyncLines = 2; + m_nbHalfLongSync = 1; + m_nbWholeEqLines = 2; + m_singleLongSync = false; + m_nbBlankLines = 15; // yields 480 lines (255 - 15) * 2 + m_blankLineLvel = m_blackLevel; + break; + case ATVModSettings::ATVStdPAL625: // Follows PAL-B/G/H standard + default: + m_nbImageLines = m_nbLines - 15; + m_nbImageLines2 = m_nbImageLines / 2; + m_interleaved = true; + m_nbSyncLinesHeadE = 5; + m_nbSyncLinesHeadO = 4; // number of sync lines on the top of a frame odd + m_nbSyncLinesBottom = 3; + m_nbLongSyncLines = 2; + m_nbHalfLongSync = 1; + m_nbWholeEqLines = 2; + m_singleLongSync = false; + m_nbBlankLines = 17; // yields 576 lines (305 - 17) * 2 + m_blankLineLvel = m_blackLevel; + } + + m_linesPerVBar = m_nbImageLines2 / m_nbBars; + + if (m_imageOK) + { + resizeImage(); + } + + if (m_videoOK) + { + calculateVideoSizes(); + resizeVideo(); + } + + calculateCamerasSizes(); +} + +void ATVModSource::openImage(const QString& fileName) +{ + m_imageFromFile = cv::imread(qPrintable(fileName), CV_LOAD_IMAGE_GRAYSCALE); + m_imageOK = m_imageFromFile.data != 0; + + if (m_imageOK) + { + m_settings.m_imageFileName = fileName; + m_imageFromFile.copyTo(m_imageOriginal); + + if (m_settings.m_showOverlayText) { + mixImageAndText(m_imageOriginal); + } + + resizeImage(); + } + else + { + m_settings.m_imageFileName.clear(); + qDebug("ATVModSource::openImage: cannot open image file %s", qPrintable(fileName)); + } +} + +void ATVModSource::openVideo(const QString& fileName) +{ + //if (m_videoOK && m_video.isOpened()) m_video.release(); should be done by OpenCV in open method + + m_videoOK = m_video.open(qPrintable(fileName)); + + if (m_videoOK) + { + m_settings.m_videoFileName = fileName; + m_videoFPS = m_video.get(CV_CAP_PROP_FPS); + m_videoWidth = (int) m_video.get(CV_CAP_PROP_FRAME_WIDTH); + m_videoHeight = (int) m_video.get(CV_CAP_PROP_FRAME_HEIGHT); + m_videoLength = (int) m_video.get(CV_CAP_PROP_FRAME_COUNT); + int ex = static_cast(m_video.get(CV_CAP_PROP_FOURCC)); + char ext[] = {(char)(ex & 0XFF),(char)((ex & 0XFF00) >> 8),(char)((ex & 0XFF0000) >> 16),(char)((ex & 0XFF000000) >> 24),0}; + + qDebug("ATVModSource::openVideo: %s FPS: %f size: %d x %d #frames: %d codec: %s", + m_video.isOpened() ? "OK" : "KO", + m_videoFPS, + m_videoWidth, + m_videoHeight, + m_videoLength, + ext); + + calculateVideoSizes(); + m_videoEOF = false; + + if (getMessageQueueToGUI()) + { + ATVModReport::MsgReportVideoFileSourceStreamData *report; + report = ATVModReport::MsgReportVideoFileSourceStreamData::create(m_videoFPS, m_videoLength); + getMessageQueueToGUI()->push(report); + } + } + else + { + m_settings.m_videoFileName.clear(); + qDebug("ATVModSource::openVideo: cannot open video file %s", qPrintable(fileName)); + } +} + +void ATVModSource::resizeImage() +{ + float fy = (m_nbImageLines - 2*m_nbBlankLines) / (float) m_imageOriginal.rows; + float fx = m_pointsPerImgLine / (float) m_imageOriginal.cols; + cv::resize(m_imageOriginal, m_image, cv::Size(), fx, fy); + qDebug("ATVModSource::resizeImage: %d x %d -> %d x %d", m_imageOriginal.cols, m_imageOriginal.rows, m_image.cols, m_image.rows); +} + +void ATVModSource::calculateVideoSizes() +{ + m_videoFy = (m_nbImageLines - 2*m_nbBlankLines) / (float) m_videoHeight; + m_videoFx = m_pointsPerImgLine / (float) m_videoWidth; + m_videoFPSq = m_videoFPS / m_fps; + m_videoFPSCount = m_videoFPSq; + m_videoPrevFPSCount = 0; + + qDebug("ATVModSource::calculateVideoSizes: factors: %f x %f FPSq: %f", m_videoFx, m_videoFy, m_videoFPSq); +} + +void ATVModSource::resizeVideo() +{ + if (!m_videoframeOriginal.empty()) { + cv::resize(m_videoframeOriginal, m_videoFrame, cv::Size(), m_videoFx, m_videoFy); // resize current frame + } +} + +void ATVModSource::calculateCamerasSizes() +{ + for (std::vector::iterator it = m_cameras.begin(); it != m_cameras.end(); ++it) + { + it->m_videoFy = (m_nbImageLines - 2*m_nbBlankLines) / (float) it->m_videoHeight; + it->m_videoFx = m_pointsPerImgLine / (float) it->m_videoWidth; + it->m_videoFPSq = it->m_videoFPS / m_fps; + it->m_videoFPSqManual = it->m_videoFPSManual / m_fps; + it->m_videoFPSCount = 0; //it->m_videoFPSq; + it->m_videoPrevFPSCount = 0; + + qDebug("ATVModSource::calculateCamerasSizes: [%d] factors: %f x %f FPSq: %f", + (int) (it - m_cameras.begin()), it->m_videoFx, it->m_videoFy, it->m_videoFPSq); + } +} + +void ATVModSource::resizeCameras() +{ + for (std::vector::iterator it = m_cameras.begin(); it != m_cameras.end(); ++it) + { + if (!it->m_videoframeOriginal.empty()) { + cv::resize(it->m_videoframeOriginal, it->m_videoFrame, cv::Size(), it->m_videoFx, it->m_videoFy); // resize current frame + } + } +} + +void ATVModSource::resizeCamera() +{ + if (!m_cameras[m_cameraIndex].m_videoframeOriginal.empty()) { + cv::resize(m_cameras[m_cameraIndex].m_videoframeOriginal, m_cameras[m_cameraIndex].m_videoFrame, cv::Size(), m_cameras[m_cameraIndex].m_videoFx, m_cameras[m_cameraIndex].m_videoFy); // resize current frame + } +} + +void ATVModSource::seekVideoFileStream(int seekPercentage) +{ + QMutexLocker mutexLocker(&m_settingsMutex); + + if ((m_videoOK) && m_video.isOpened()) + { + int seekPoint = ((m_videoLength * seekPercentage) / 100); + m_video.set(CV_CAP_PROP_POS_FRAMES, seekPoint); + m_videoFPSCount = m_videoFPSq; + m_videoPrevFPSCount = 0; + m_videoEOF = false; + } +} + +void ATVModSource::scanCameras() +{ + for (int i = 0; i < 4; i++) + { + ATVCamera newCamera; + m_cameras.push_back(newCamera); + m_cameras.back().m_cameraNumber = i; + m_cameras.back().m_camera.open(i); + + if (m_cameras.back().m_camera.isOpened()) + { + m_cameras.back().m_videoFPS = m_cameras.back().m_camera.get(CV_CAP_PROP_FPS); + m_cameras.back().m_videoWidth = (int) m_cameras.back().m_camera.get(CV_CAP_PROP_FRAME_WIDTH); + m_cameras.back().m_videoHeight = (int) m_cameras.back().m_camera.get(CV_CAP_PROP_FRAME_HEIGHT); + + //m_cameras.back().m_videoFPS = m_cameras.back().m_videoFPS < 0 ? 16.3f : m_cameras.back().m_videoFPS; + + qDebug("ATVModSource::scanCameras: [%d] FPS: %f %dx%d", + i, + m_cameras.back().m_videoFPS, + m_cameras.back().m_videoWidth , + m_cameras.back().m_videoHeight); + } + else + { + m_cameras.pop_back(); + } + } + + if (m_cameras.size() > 0) + { + calculateCamerasSizes(); + m_cameraIndex = 0; + } +} + +void ATVModSource::releaseCameras() +{ + for (std::vector::iterator it = m_cameras.begin(); it != m_cameras.end(); ++it) + { + if (it->m_camera.isOpened()) it->m_camera.release(); + } +} + +void ATVModSource::getCameraNumbers(std::vector& numbers) +{ + for (std::vector::iterator it = m_cameras.begin(); it != m_cameras.end(); ++it) { + numbers.push_back(it->m_cameraNumber); + } + + if (m_cameras.size() > 0) + { + m_cameraIndex = 0; + + if (getMessageQueueToGUI()) + { + ATVModReport::MsgReportCameraData *report; + report = ATVModReport::MsgReportCameraData::create( + m_cameras[0].m_cameraNumber, + m_cameras[0].m_videoFPS, + m_cameras[0].m_videoFPSManual, + m_cameras[0].m_videoFPSManualEnable, + m_cameras[0].m_videoWidth, + m_cameras[0].m_videoHeight, + 0); + getMessageQueueToGUI()->push(report); + } + } +} + +void ATVModSource::mixImageAndText(cv::Mat& image) +{ + int fontFace = cv::FONT_HERSHEY_PLAIN; + double fontScale = image.rows / 100.0; + int thickness = image.cols / 160; + int baseline=0; + + fontScale = fontScale < 4.0f ? 4.0f : fontScale; // minimum size + cv::Size textSize = cv::getTextSize(m_settings.m_overlayText.toStdString(), fontFace, fontScale, thickness, &baseline); + baseline += thickness; + + // position the text in the top left corner + cv::Point textOrg(6, textSize.height+10); + // then put the text itself + cv::putText(image, m_settings.m_overlayText.toStdString(), textOrg, fontFace, fontScale, cv::Scalar::all(255*m_settings.m_uniformLevel), thickness, CV_AA); +} + +void ATVModSource::applyChannelSettings(int outputSampleRate, int inputFrequencyOffset, bool force) +{ + qDebug() << "ATVModSource::applyChannelSettings:" + << " outputSampleRate: " << outputSampleRate + << " inputFrequencyOffset: " << inputFrequencyOffset; + + if ((inputFrequencyOffset != m_inputFrequencyOffset) || + (outputSampleRate != m_outputSampleRate) || force) + { + m_settingsMutex.lock(); + m_carrierNco.setFreq(inputFrequencyOffset, outputSampleRate); + m_settingsMutex.unlock(); + } + + if ((outputSampleRate != m_outputSampleRate) || force) + { + getBaseValues(outputSampleRate, m_settings.m_nbLines * m_settings.m_fps, m_tvSampleRate, m_pointsPerLine); + + m_settingsMutex.lock(); + + if (m_tvSampleRate > 0) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorDistance = (Real) m_tvSampleRate / (Real) outputSampleRate; + m_interpolator.create(32, + m_tvSampleRate, + m_settings.m_rfBandwidth / getRFBandwidthDivisor(m_settings.m_atvModulation), + 3.0); + } + else + { + m_tvSampleRate = outputSampleRate; + } + + m_SSBFilter->create_filter(0, m_settings.m_rfBandwidth / m_tvSampleRate); + memset(m_SSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen>>1)); + m_SSBFilterBufferIndex = 0; + + applyStandard(); // set all timings + m_settingsMutex.unlock(); + + if (getMessageQueueToGUI()) + { + ATVModReport::MsgReportEffectiveSampleRate *report; + report = ATVModReport::MsgReportEffectiveSampleRate::create(m_tvSampleRate, m_pointsPerLine); + getMessageQueueToGUI()->push(report); + } + } + + m_outputSampleRate = outputSampleRate; + m_inputFrequencyOffset = inputFrequencyOffset; +} + +void ATVModSource::applySettings(const ATVModSettings& settings, bool force) +{ + qDebug() << "ATVModSource::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " m_rfOppBandwidth: " << settings.m_rfOppBandwidth + << " m_atvStd: " << (int) settings.m_atvStd + << " m_nbLines: " << settings.m_nbLines + << " m_fps: " << settings.m_fps + << " m_atvModInput: " << (int) settings.m_atvModInput + << " m_uniformLevel: " << settings.m_uniformLevel + << " m_atvModulation: " << (int) settings.m_atvModulation + << " m_videoPlayLoop: " << settings.m_videoPlayLoop + << " m_videoPlay: " << settings.m_videoPlay + << " m_cameraPlay: " << settings.m_cameraPlay + << " m_channelMute: " << settings.m_channelMute + << " m_invertedVideo: " << settings.m_invertedVideo + << " m_rfScalingFactor: " << settings.m_rfScalingFactor + << " m_fmExcursion: " << settings.m_fmExcursion + << " m_forceDecimator: " << settings.m_forceDecimator + << " m_showOverlayText: " << settings.m_showOverlayText + << " m_overlayText: " << settings.m_overlayText + << " force: " << force; + + if ((settings.m_atvStd != m_settings.m_atvStd) + || (settings.m_nbLines != m_settings.m_nbLines) + || (settings.m_fps != m_settings.m_fps) + || (settings.m_rfBandwidth != m_settings.m_rfBandwidth) + || (settings.m_atvModulation != m_settings.m_atvModulation) || force) + { + getBaseValues(m_outputSampleRate, settings.m_nbLines * settings.m_fps, m_tvSampleRate, m_pointsPerLine); + + m_settingsMutex.lock(); + + if (m_tvSampleRate > 0) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorDistance = (Real) m_tvSampleRate / (Real) m_outputSampleRate; + m_interpolator.create(32, + m_tvSampleRate, + settings.m_rfBandwidth / getRFBandwidthDivisor(settings.m_atvModulation), + 3.0); + } + + m_SSBFilter->create_filter(0, settings.m_rfBandwidth / m_tvSampleRate); + memset(m_SSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen>>1)); + m_SSBFilterBufferIndex = 0; + + applyStandard(); // set all timings + m_settingsMutex.unlock(); + + if (getMessageQueueToGUI()) + { + ATVModReport::MsgReportEffectiveSampleRate *report; + report = ATVModReport::MsgReportEffectiveSampleRate::create(m_tvSampleRate, m_pointsPerLine); + getMessageQueueToGUI()->push(report); + } + } + + if ((settings.m_rfOppBandwidth != m_settings.m_rfOppBandwidth) + || (settings.m_rfBandwidth != m_settings.m_rfBandwidth) + || (settings.m_nbLines != m_settings.m_nbLines) // difference in line period may have changed TV sample rate + || (settings.m_fps != m_settings.m_fps) // + || force) + { + m_settingsMutex.lock(); + + m_DSBFilter->create_asym_filter(settings.m_rfOppBandwidth / m_tvSampleRate, settings.m_rfBandwidth / m_tvSampleRate); + memset(m_DSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen)); + m_DSBFilterBufferIndex = 0; + + m_settingsMutex.unlock(); + } + + if ((settings.m_showOverlayText != m_settings.m_showOverlayText) || force) + { + if (!m_imageFromFile.empty()) + { + m_imageFromFile.copyTo(m_imageOriginal); + + if (settings.m_showOverlayText) { + qDebug("ATVModSource::applySettings: set overlay text"); + mixImageAndText(m_imageOriginal); + } else{ + qDebug("ATVModSource::applySettings: clear overlay text"); + } + + resizeImage(); + } + } + + m_settings = settings; +} + +void ATVModSource::reportVideoFileSourceStreamTiming() +{ + int framesCount; + + if (m_videoOK && m_video.isOpened()) + { + framesCount = m_video.get(CV_CAP_PROP_POS_FRAMES);; + } else { + framesCount = 0; + } + + if (getMessageQueueToGUI()) + { + ATVModReport::MsgReportVideoFileSourceStreamTiming *report; + report = ATVModReport::MsgReportVideoFileSourceStreamTiming::create(framesCount); + getMessageQueueToGUI()->push(report); + } +} + +void ATVModSource::configureCameraIndex(int index) +{ + if (index < m_cameras.size()) + { + m_cameraIndex = index; + + if (getMessageQueueToGUI()) + { + ATVModReport::MsgReportCameraData *report; + report = ATVModReport::MsgReportCameraData::create( + m_cameras[m_cameraIndex].m_cameraNumber, + m_cameras[m_cameraIndex].m_videoFPS, + m_cameras[m_cameraIndex].m_videoFPSManual, + m_cameras[m_cameraIndex].m_videoFPSManualEnable, + m_cameras[m_cameraIndex].m_videoWidth, + m_cameras[m_cameraIndex].m_videoHeight, + 0); + getMessageQueueToGUI()->push(report); + } + } +} + +void ATVModSource::configureCameraData(uint32_t index, float mnaualFPS, bool manualFPSEnable) +{ + if (index < m_cameras.size()) + { + m_cameras[index].m_videoFPSManual = mnaualFPS; + m_cameras[index].m_videoFPSManualEnable = manualFPSEnable; + } +} \ No newline at end of file diff --git a/plugins/channeltx/modatv/atvmodsource.h b/plugins/channeltx/modatv/atvmodsource.h new file mode 100644 index 000000000..508c2c8ef --- /dev/null +++ b/plugins/channeltx/modatv/atvmodsource.h @@ -0,0 +1,502 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODATV_ATVMODSOURCE_H_ +#define PLUGINS_CHANNELTX_MODATV_ATVMODSOURCE_H_ + +#include + +#include +#include + +#include +#include +#include + +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "util/movingaverage.h" +#include "dsp/fftfilt.h" +#include "util/message.h" + +#include "atvmodsettings.h" + +class MessageQueue; + +class ATVModSource : public ChannelSampleSource +{ +public: + ATVModSource(); + ~ATVModSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples); + + int getEffectiveSampleRate() const { return m_tvSampleRate; }; + double getMagSq() const { return m_movingAverage.asDouble(); } + void getCameraNumbers(std::vector& numbers); + + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_messageQueueToGUI = messageQueue; } + void getLevels(Real& rmsLevel, Real& peakLevel, Real& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevel; + numSamples = m_levelNbSamples; + } + + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const ATVModSettings& settings, bool force = false); + void openImage(const QString& fileName); + void openVideo(const QString& fileName); + void seekVideoFileStream(int seekPercentage); + void reportVideoFileSourceStreamTiming(); + void configureCameraIndex(int index); + void configureCameraData(uint32_t index, float mnaualFPS, bool manualFPSEnable); + + static void getBaseValues(int outputSampleRate, int linesPerSecond, int& sampleRateUnits, uint32_t& nbPointsPerRateUnit); + static float getRFBandwidthDivisor(ATVModSettings::ATVModulation modulation); + +private: + class ATVCamera + { + public: + cv::VideoCapture m_camera; //!< camera object + cv::Mat m_videoframeOriginal; //!< camera non resized image + cv::Mat m_videoFrame; //!< displayable camera frame + int m_cameraNumber; //!< camera device number + float m_videoFPS; //!< camera FPS rate + float m_videoFPSManual; //!< camera FPS rate manually set + bool m_videoFPSManualEnable; //!< Enable camera FPS rate manual set value + int m_videoWidth; //!< camera frame width + int m_videoHeight; //!< camera frame height + float m_videoFx; //!< camera horizontal scaling factor + float m_videoFy; //!< camera vertictal scaling factor + float m_videoFPSq; //!< camera FPS sacaling factor + float m_videoFPSqManual; //!< camera FPS sacaling factor manually set + float m_videoFPSCount; //!< camera FPS fractional counter + int m_videoPrevFPSCount; //!< camera FPS previous integer counter + + ATVCamera() : + m_cameraNumber(-1), + m_videoFPS(25.0f), + m_videoFPSManual(20.0f), + m_videoFPSManualEnable(false), + m_videoWidth(1), + m_videoHeight(1), + m_videoFx(1.0f), + m_videoFy(1.0f), + m_videoFPSq(1.0f), + m_videoFPSqManual(1.0f), + m_videoFPSCount(0.0f), + m_videoPrevFPSCount(0) + {} + + ATVCamera(const ATVCamera& camera) : + m_camera(camera.m_camera), + m_videoframeOriginal(camera.m_videoframeOriginal), + m_videoFrame(camera.m_videoFrame), + m_cameraNumber(camera.m_cameraNumber), + m_videoFPS(camera.m_videoFPS), + m_videoFPSManual(camera.m_videoFPSManual), + m_videoFPSManualEnable(camera.m_videoFPSManualEnable), + m_videoWidth(camera.m_videoWidth), + m_videoHeight(camera.m_videoHeight), + m_videoFx(camera.m_videoFx), + m_videoFy(camera.m_videoFy), + m_videoFPSq(camera.m_videoFPSq), + m_videoFPSqManual(camera.m_videoFPSqManual), + m_videoFPSCount(camera.m_videoFPSCount), + m_videoPrevFPSCount(camera.m_videoPrevFPSCount) + {} + }; + + int m_outputSampleRate; + int m_inputFrequencyOffset; + ATVModSettings m_settings; + + NCO m_carrierNco; + Complex m_modSample; + float m_modPhasor; //!< For FM modulation + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + int m_tvSampleRate; //!< sample rate for generating signal + uint32_t m_pointsPerLine; //!< Number of points per full line + int m_pointsPerSync; //!< number of line points for the horizontal sync + int m_pointsPerBP; //!< number of line points for the back porch + int m_pointsPerImgLine; //!< number of line points for the image line + uint32_t m_pointsPerFP; //!< number of line points for the front porch + int m_pointsPerFSync; //!< number of line points for the field first sync + uint32_t m_pointsPerHBar; //!< number of line points for a bar of the bar chart + uint32_t m_linesPerVBar; //!< number of lines for a bar of the bar chart + uint32_t m_pointsPerTU; //!< number of line points per time unit + int m_nbLines; //!< number of lines per complete frame + int m_nbLines2; //!< same number as above (non interlaced) or half the number above (interlaced) + uint32_t m_nbImageLines; //!< number of image lines excluding synchronization lines + uint32_t m_nbImageLines2; //!< same number as above (non interlaced) or half the number above (interlaced) + int m_nbHorizPoints; //!< number of line points per horizontal line + int m_nbSyncLinesHeadE; //!< number of header sync lines on even frame + int m_nbSyncLinesHeadO; //!< number of header sync lines on odd frame + int m_nbSyncLinesBottom;//!< number of sync lines at bottom + int m_nbLongSyncLines; //!< number of whole long sync lines for vertical synchronization + int m_nbHalfLongSync; //!< number of half long sync / equalization lines + int m_nbWholeEqLines; //!< number of whole equalizing lines + bool m_singleLongSync; //!< single or double long sync per long sync line + int m_nbBlankLines; //!< number of lines in a frame (full or half) that are blanked (black) at the top of the image + float m_blankLineLvel; //!< video level of blank lines + float m_hBarIncrement; //!< video level increment at each horizontal bar increment + float m_vBarIncrement; //!< video level increment at each vertical bar increment + bool m_interleaved; //!< true if image is interlaced (2 half frames per frame) + bool m_evenImage; //!< in interlaced mode true if this is an even image + QMutex m_settingsMutex; + int m_horizontalCount; //!< current point index on line + int m_lineCount; //!< current line index in frame + float m_fps; //!< resulting frames per second + + MovingAverageUtil m_movingAverage; + quint32 m_levelCalcCount; + Real m_rmsLevel; + Real m_peakLevelOut; + Real m_peakLevel; + Real m_levelSum; + + cv::Mat m_imageFromFile; //!< original image not resized not overlaid by text + cv::Mat m_imageOriginal; //!< original not resized image + cv::Mat m_image; //!< resized image for transmission at given rate + bool m_imageOK; + + cv::VideoCapture m_video; //!< current video capture + cv::Mat m_videoframeOriginal; //!< current frame from video + cv::Mat m_videoFrame; //!< current displayable video frame + float m_videoFPS; //!< current video FPS rate + int m_videoWidth; //!< current video frame width + int m_videoHeight; //!< current video frame height + float m_videoFx; //!< current video horizontal scaling factor + float m_videoFy; //!< current video vertictal scaling factor + float m_videoFPSq; //!< current video FPS sacaling factor + float m_videoFPSCount; //!< current video FPS fractional counter + int m_videoPrevFPSCount; //!< current video FPS previous integer counter + int m_videoLength; //!< current video length in frames + bool m_videoEOF; //!< current video has reached end of file + bool m_videoOK; + + std::vector m_cameras; //!< vector of available cameras + int m_cameraIndex; //!< curent camera index in list of available cameras + + std::string m_overlayText; + QString m_imageFileName; + QString m_videoFileName; + + // Used for standard SSB + fftfilt* m_SSBFilter; + Complex* m_SSBFilterBuffer; + int m_SSBFilterBufferIndex; + + // Used for vestigial SSB with asymmetrical filtering (needs double sideband scheme) + fftfilt* m_DSBFilter; + Complex* m_DSBFilterBuffer; + int m_DSBFilterBufferIndex; + + MessageQueue *m_messageQueueToGUI; + + static const int m_ssbFftLen; + static const float m_blackLevel; + static const float m_spanLevel; + static const int m_levelNbSamples; + static const int m_nbBars; //!< number of bars in bar or chessboard patterns + static const int m_cameraFPSTestNbFrames; //!< number of frames for camera FPS test + + void pullFinalize(Complex& ci, Sample& sample); + void pullVideo(Real& sample); + void calculateLevel(Real& sample); + void modulateSample(); + Complex& modulateSSB(Real& sample); + Complex& modulateVestigialSSB(Real& sample); + void applyStandard(); + void resizeImage(); + void calculateVideoSizes(); + void resizeVideo(); + void scanCameras(); + void releaseCameras(); + void calculateCamerasSizes(); + void resizeCameras(); + void resizeCamera(); + void mixImageAndText(cv::Mat& image); + + MessageQueue *getMessageQueueToGUI() { return m_messageQueueToGUI; } + + inline void pullImageLine(Real& sample, bool noHSync = false) + { + if (m_horizontalCount < m_pointsPerSync) // sync pulse + { + sample = noHSync ? m_blackLevel : 0.0f; // ultra-black + } + else if (m_horizontalCount < m_pointsPerSync + m_pointsPerBP) // back porch + { + sample = m_blackLevel; // black + } + else if (m_horizontalCount < m_pointsPerSync + m_pointsPerBP + m_pointsPerImgLine) + { + int pointIndex = m_horizontalCount - (m_pointsPerSync + m_pointsPerBP); + int oddity = m_lineCount < m_nbLines2 + 1 ? 0 : 1; + int iLine = oddity == 0 ? m_lineCount : m_lineCount - m_nbLines2 - 1; + int iLineImage = iLine - m_nbBlankLines - (oddity == 0 ? m_nbSyncLinesHeadE : m_nbSyncLinesHeadO); + + switch(m_settings.m_atvModInput) + { + case ATVModSettings::ATVModInputHBars: + sample = (((float)pointIndex) / m_pointsPerHBar) * m_hBarIncrement + m_blackLevel; + break; + case ATVModSettings::ATVModInputVBars: + sample = (((float)iLine) / m_linesPerVBar) * m_vBarIncrement + m_blackLevel; + break; + case ATVModSettings::ATVModInputChessboard: + sample = (((iLine / m_linesPerVBar)*5 + (pointIndex / m_pointsPerHBar)) % 2) * m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; + break; + case ATVModSettings::ATVModInputHGradient: + sample = (pointIndex / (float) m_pointsPerImgLine) * m_spanLevel + m_blackLevel; + break; + case ATVModSettings::ATVModInputVGradient: + sample = ((iLine -5) / (float) m_nbImageLines2) * m_spanLevel + m_blackLevel; + break; + case ATVModSettings::ATVModInputImage: + if (!m_imageOK || (iLineImage < -oddity) || m_image.empty()) + { + sample = m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; + } + else + { + unsigned char pixv; + + if (m_interleaved) { + pixv = m_image.at(2*iLineImage + oddity, pointIndex); // row (y), col (x) + } else { + pixv = m_image.at(iLineImage, pointIndex); // row (y), col (x) + } + + sample = (pixv / 256.0f) * m_spanLevel + m_blackLevel; + } + break; + case ATVModSettings::ATVModInputVideo: + if (!m_videoOK || (iLineImage < -oddity) || m_videoFrame.empty()) + { + sample = m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; + } + else + { + unsigned char pixv; + + if (m_interleaved) { + pixv = m_videoFrame.at(2*iLineImage + oddity, pointIndex); // row (y), col (x) + } else { + pixv = m_videoFrame.at(iLineImage, pointIndex); // row (y), col (x) + } + + sample = (pixv / 256.0f) * m_spanLevel + m_blackLevel; + } + break; + case ATVModSettings::ATVModInputCamera: + if ((iLineImage < -oddity) || (m_cameraIndex < 0)) + { + sample = m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; + } + else + { + ATVCamera& camera = m_cameras[m_cameraIndex]; + + if (camera.m_videoFrame.empty()) + { + sample = m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; + } + else + { + unsigned char pixv; + + if (m_interleaved) { + pixv = camera.m_videoFrame.at(2*iLineImage + oddity, pointIndex); // row (y), col (x) + } else { + pixv = camera.m_videoFrame.at(iLineImage, pointIndex); // row (y), col (x) + } + + sample = (pixv / 256.0f) * m_spanLevel + m_blackLevel; + } + } + break; + case ATVModSettings::ATVModInputUniform: + default: + sample = m_spanLevel * m_settings.m_uniformLevel + m_blackLevel; + } + } + else // front porch + { + sample = m_blackLevel; // black + } + } + + inline void pullVSyncLineLongPulses(Real& sample) + { + int halfIndex = m_horizontalCount % (m_nbHorizPoints/2); + + if (halfIndex < (m_nbHorizPoints/2) - m_pointsPerSync) // ultra-black + { + sample = 0.0f; + } + else // black + { + if (m_singleLongSync && (m_horizontalCount < m_nbHorizPoints/2)) { + sample = 0.0f; + } else { + sample = m_blackLevel; + } + } + } + + inline void pullVSyncLineEqualizingPulses(Real& sample) + { + if (m_horizontalCount < m_pointsPerSync) + { + sample = 0.0f; // ultra-black + } + else if (m_horizontalCount < (m_nbHorizPoints/2)) + { + sample = m_blackLevel; // black + } + else if (m_horizontalCount < (m_nbHorizPoints/2) + m_pointsPerFSync) + { + sample = 0.0f; // ultra-black + } + else + { + sample = m_blackLevel; // black + } + } + + inline void pullVSyncLineEqualizingThenLongPulses(Real& sample) + { + if (m_horizontalCount < m_pointsPerSync) + { + sample = 0.0f; // ultra-black + } + else if (m_horizontalCount < (m_nbHorizPoints/2)) + { + sample = m_blackLevel; // black + } + else if (m_horizontalCount < m_nbHorizPoints - m_pointsPerSync) + { + sample = 0.0f; // ultra-black + } + else + { + sample = m_blackLevel; // black + } + } + + inline void pullVSyncLineLongThenEqualizingPulses(Real& sample) + { + if (m_horizontalCount < (m_nbHorizPoints/2) - m_pointsPerSync) + { + sample = 0.0f; // ultra-black + } + else if (m_horizontalCount < (m_nbHorizPoints/2)) + { + sample = m_blackLevel; // black + } + else if (m_horizontalCount < (m_nbHorizPoints/2) + m_pointsPerFSync) + { + sample = 0.0f; // ultra-black + } + else + { + sample = m_blackLevel; // black + } + } + + inline void pullVSyncLine(Real& sample) + { + if (m_lineCount < m_nbLines2 + 1) // even + { + int fieldLine = m_lineCount; + + if (fieldLine < m_nbLongSyncLines) // 0,1: Whole line "long" pulses + { + pullVSyncLineLongPulses(sample); + } + else if (fieldLine < m_nbLongSyncLines + m_nbHalfLongSync) // long pulse then equalizing pulse + { + pullVSyncLineLongThenEqualizingPulses(sample); + } + else if (fieldLine < m_nbLongSyncLines + m_nbHalfLongSync + m_nbWholeEqLines) // Whole line equalizing pulses + { + pullVSyncLineEqualizingPulses(sample); + } + else if (fieldLine > m_nbLines2 - m_nbHalfLongSync) // equalizing pulse then long pulse + { + pullVSyncLineEqualizingThenLongPulses(sample); + } + else if (fieldLine > m_nbLines2 - m_nbHalfLongSync - m_nbWholeEqLines) // Whole line equalizing pulses + { + pullVSyncLineEqualizingPulses(sample); + } + else // black images + { + if (m_horizontalCount < m_pointsPerSync) + { + sample = 0.0f; + } + else + { + sample = m_blankLineLvel; + } + } + } + else // odd + { + int fieldLine = m_lineCount - m_nbLines2 - 1; + + if (fieldLine < m_nbLongSyncLines) // 0,1: Whole line "long" pulses + { + pullVSyncLineLongPulses(sample); + } + else if (fieldLine < m_nbLongSyncLines + m_nbWholeEqLines) // Whole line equalizing pulses + { + pullVSyncLineEqualizingPulses(sample); + } + else if (fieldLine > m_nbLines2 - 1 - m_nbWholeEqLines - m_nbHalfLongSync) // Whole line equalizing pulses + { + pullVSyncLineEqualizingPulses(sample); + } + else // black images + { + if (m_horizontalCount < m_pointsPerSync) + { + sample = 0.0f; + } + else + { + sample = m_blankLineLvel; + } + } + } + } +}; + + +#endif /* PLUGINS_CHANNELTX_MODATV_ATVMOD_H_ */ diff --git a/plugins/channeltx/modfreedv/CMakeLists.txt b/plugins/channeltx/modfreedv/CMakeLists.txt index 49738672a..1e7af9440 100644 --- a/plugins/channeltx/modfreedv/CMakeLists.txt +++ b/plugins/channeltx/modfreedv/CMakeLists.txt @@ -2,6 +2,8 @@ project(modfreedv) set(modfreedv_SOURCES freedvmod.cpp + freedvmodbaseband.cpp + freedvmodsource.cpp freedvmodplugin.cpp freedvmodsettings.cpp freedvmodwebapiadapter.cpp @@ -9,6 +11,8 @@ set(modfreedv_SOURCES set(modfreedv_HEADERS freedvmod.h + freedvmodbaseband.h + freedvmodsource.h freedvmodplugin.h freedvmodsettings.h freedvmodwebapiadapter.h diff --git a/plugins/channeltx/modfreedv/freedvmod.cpp b/plugins/channeltx/modfreedv/freedvmod.cpp index 16f1eb633..28018617a 100644 --- a/plugins/channeltx/modfreedv/freedvmod.cpp +++ b/plugins/channeltx/modfreedv/freedvmod.cpp @@ -15,32 +15,30 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// -#include "freedvmod.h" - #include #include #include #include #include #include +#include #include #include #include -#include "codec2/freedv_api.h" - #include "SWGChannelSettings.h" #include "SWGChannelReport.h" #include "SWGFreeDVModReport.h" -#include "dsp/upchannelizer.h" #include "dsp/dspengine.h" -#include "dsp/threadedbasebandsamplesource.h" #include "dsp/dspcommands.h" #include "device/deviceapi.h" #include "util/db.h" +#include "freedvmodbaseband.h" +#include "freedvmod.h" + MESSAGE_CLASS_DEFINITION(FreeDVMod::MsgConfigureFreeDVMod, Message) MESSAGE_CLASS_DEFINITION(FreeDVMod::MsgConfigureChannelizer, Message) MESSAGE_CLASS_DEFINITION(FreeDVMod::MsgConfigureFileSourceName, Message) @@ -51,69 +49,26 @@ MESSAGE_CLASS_DEFINITION(FreeDVMod::MsgReportFileSourceStreamTiming, Message) const QString FreeDVMod::m_channelIdURI = "sdrangel.channeltx.freedvmod"; const QString FreeDVMod::m_channelId = "FreeDVMod"; -const int FreeDVMod::m_levelNbSamples = 80; // every 10ms -const int FreeDVMod::m_ssbFftLen = 1024; FreeDVMod::FreeDVMod(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), m_deviceAPI(deviceAPI), - m_basebandSampleRate(48000), - m_outputSampleRate(48000), - m_modemSampleRate(48000), // // default 2400A mode - m_inputFrequencyOffset(0), - m_lowCutoff(0.0), - m_hiCutoff(6000.0), - m_SSBFilter(0), - m_SSBFilterBuffer(0), - m_SSBFilterBufferIndex(0), - m_sampleSink(0), - m_audioFifo(4800), m_settingsMutex(QMutex::Recursive), m_fileSize(0), m_recordLength(0), - m_inputSampleRate(8000), // all modes take 8000 S/s input - m_levelCalcCount(0), - m_peakLevel(0.0f), - m_levelSum(0.0f), - m_freeDV(0), - m_nSpeechSamples(0), - m_nNomModemSamples(0), - m_iSpeech(0), - m_iModem(0), - m_speechIn(0), - m_modOut(0), - m_scaleFactor(SDR_TX_SCALEF) + m_fileSampleRate(8000) // all modes take 8000 S/s input { setObjectName(m_channelId); - DSPEngine::instance()->getAudioDeviceManager()->addAudioSource(&m_audioFifo, getInputMessageQueue()); - m_audioSampleRate = DSPEngine::instance()->getAudioDeviceManager()->getInputSampleRate(); - - m_SSBFilter = new fftfilt(m_lowCutoff / m_audioSampleRate, m_hiCutoff / m_audioSampleRate, m_ssbFftLen); - m_SSBFilterBuffer = new Complex[m_ssbFftLen>>1]; // filter returns data exactly half of its size - std::fill(m_SSBFilterBuffer, m_SSBFilterBuffer+(m_ssbFftLen>>1), Complex{0,0}); - - m_audioBuffer.resize(1<<14); - m_audioBufferFill = 0; - - m_sum.real(0.0f); - m_sum.imag(0.0f); - m_undersampleCount = 0; - m_sumCount = 0; - - m_magsq = 0.0; - - m_toneNco.setFreq(1000.0, m_inputSampleRate); - m_cwKeyer.setSampleRate(m_inputSampleRate); - m_cwKeyer.reset(); - - m_channelizer = new UpChannelizer(this); - m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this); - m_deviceAPI->addChannelSource(m_threadedChannelizer); - m_deviceAPI->addChannelSourceAPI(this); + m_thread = new QThread(this); + m_basebandSource = new FreeDVModBaseband(); + m_basebandSource->setInputFileStream(&m_ifstream); + m_basebandSource->moveToThread(m_thread); applySettings(m_settings, true); - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); + + m_deviceAPI->addChannelSource(this); + m_deviceAPI->addChannelSourceAPI(this); m_networkManager = new QNetworkAccessManager(); connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); @@ -123,330 +78,43 @@ FreeDVMod::~FreeDVMod() { disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); delete m_networkManager; - - DSPEngine::instance()->getAudioDeviceManager()->removeAudioSource(&m_audioFifo); - m_deviceAPI->removeChannelSourceAPI(this); - m_deviceAPI->removeChannelSource(m_threadedChannelizer); - delete m_threadedChannelizer; - delete m_channelizer; - - delete m_SSBFilter; - delete[] m_SSBFilterBuffer; - - if (m_freeDV) { - freedv_close(m_freeDV); - } -} - -void FreeDVMod::pull(Sample& sample) -{ - Complex ci; - - m_settingsMutex.lock(); - - if (m_interpolatorDistance > 1.0f) // decimate - { - modulateSample(); - - while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - else - { - if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - - m_interpolatorDistanceRemain += m_interpolatorDistance; - - ci *= m_carrierNco.nextIQ(); // shift to carrier frequency - ci *= 0.891235351562f * SDR_TX_SCALEF; //scaling at -1 dB to account for possible filter overshoot - - m_settingsMutex.unlock(); - - double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); - magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); - m_movingAverage(magsq); - m_magsq = m_movingAverage.asDouble(); - - sample.m_real = (FixReal) ci.real(); - sample.m_imag = (FixReal) ci.imag(); -} - -void FreeDVMod::pullAudio(int nbSamples) -{ - unsigned int nbSamplesAudio = nbSamples * ((Real) m_audioSampleRate / (Real) m_modemSampleRate); - - if (nbSamplesAudio > m_audioBuffer.size()) - { - m_audioBuffer.resize(nbSamplesAudio); - } - - m_audioFifo.read(reinterpret_cast(&m_audioBuffer[0]), nbSamplesAudio); - m_audioBufferFill = 0; -} - -void FreeDVMod::modulateSample() -{ - pullAF(m_modSample); - if (!m_settings.m_gaugeInputElseModem) { - calculateLevel(m_modSample); - } - m_audioBufferFill++; -} - -void FreeDVMod::pullAF(Complex& sample) -{ - if (m_settings.m_audioMute) - { - sample.real(0.0f); - sample.imag(0.0f); - return; - } - - Complex ci; - fftfilt::cmplx *filtered; - int n_out = 0; - - int decim = 1<<(m_settings.m_spanLog2 - 1); - unsigned char decim_mask = decim - 1; // counter LSB bit mask for decimation by 2^(m_scaleLog2 - 1) - - if (m_iModem >= m_nNomModemSamples) - { - switch (m_settings.m_modAFInput) - { - case FreeDVModSettings::FreeDVModInputTone: - for (int i = 0; i < m_nSpeechSamples; i++) - { - m_speechIn[i] = m_toneNco.next() * 32768.0f * m_settings.m_volumeFactor; - if (m_settings.m_gaugeInputElseModem) { - calculateLevel(m_speechIn[i]); - } - } - freedv_tx(m_freeDV, m_modOut, m_speechIn); - break; - case FreeDVModSettings::FreeDVModInputFile: - if (m_iModem >= m_nNomModemSamples) - { - if (m_ifstream.is_open()) - { - std::fill(m_speechIn, m_speechIn + m_nSpeechSamples, 0); - - if (m_ifstream.eof()) - { - if (m_settings.m_playLoop) - { - m_ifstream.clear(); - m_ifstream.seekg(0, std::ios::beg); - } - } - - if (m_ifstream.eof()) - { - std::fill(m_modOut, m_modOut + m_nNomModemSamples, 0); - } - else - { - - m_ifstream.read(reinterpret_cast(m_speechIn), sizeof(int16_t) * m_nSpeechSamples); - - if ((m_settings.m_volumeFactor != 1.0) || m_settings.m_gaugeInputElseModem) - { - for (int i = 0; i < m_nSpeechSamples; i++) - { - if (m_settings.m_volumeFactor != 1.0) { - m_speechIn[i] *= m_settings.m_volumeFactor; - } - if (m_settings.m_gaugeInputElseModem) { - calculateLevel(m_speechIn[i]); - } - } - } - - freedv_tx(m_freeDV, m_modOut, m_speechIn); - } - } - else - { - std::fill(m_modOut, m_modOut + m_nNomModemSamples, 0); - } - } - break; - case FreeDVModSettings::FreeDVModInputAudio: - for (int i = 0; i < m_nSpeechSamples; i++) - { - qint16 audioSample = (m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) * (m_settings.m_volumeFactor / 2.0f); - m_audioBufferFill++; - - while (!m_audioResampler.downSample(audioSample, m_speechIn[i])) - { - audioSample = (m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) * (m_settings.m_volumeFactor / 2.0f); - m_audioBufferFill++; - } - - if (m_settings.m_gaugeInputElseModem) { - calculateLevel(m_speechIn[i]); - } - } - freedv_tx(m_freeDV, m_modOut, m_speechIn); - break; - case FreeDVModSettings::FreeDVModInputCWTone: - for (int i = 0; i < m_nSpeechSamples; i++) - { - Real fadeFactor; - - if (m_cwKeyer.getSample()) - { - m_cwKeyer.getCWSmoother().getFadeSample(true, fadeFactor); - m_speechIn[i] = m_toneNco.next() * 32768.0f * fadeFactor * m_settings.m_volumeFactor; - } - else - { - if (m_cwKeyer.getCWSmoother().getFadeSample(false, fadeFactor)) - { - m_speechIn[i] = m_toneNco.next() * 32768.0f * fadeFactor * m_settings.m_volumeFactor; - } - else - { - m_speechIn[i] = 0; - m_toneNco.setPhase(0); - } - } - - if (m_settings.m_gaugeInputElseModem) { - calculateLevel(m_speechIn[i]); - } - } - freedv_tx(m_freeDV, m_modOut, m_speechIn); - break; - case FreeDVModSettings::FreeDVModInputNone: - default: - std::fill(m_speechIn, m_speechIn + m_nSpeechSamples, 0); - freedv_tx(m_freeDV, m_modOut, m_speechIn); - break; - } - - m_iModem = 0; - } - - ci.real(m_modOut[m_iModem++] / m_scaleFactor); - ci.imag(0.0f); - - n_out = m_SSBFilter->runSSB(ci, &filtered, true); // USB - - if (n_out > 0) - { - memcpy((void *) m_SSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); - m_SSBFilterBufferIndex = 0; - - for (int i = 0; i < n_out; i++) - { - // Downsample by 2^(m_scaleLog2 - 1) for SSB band spectrum display - // smart decimation with bit gain using float arithmetic (23 bits significand) - - m_sum += filtered[i]; - - if (!(m_undersampleCount++ & decim_mask)) - { - Real avgr = (m_sum.real() / decim) * 0.891235351562f * SDR_TX_SCALEF; //scaling at -1 dB to account for possible filter overshoot - Real avgi = (m_sum.imag() / decim) * 0.891235351562f * SDR_TX_SCALEF; - m_sampleBuffer.push_back(Sample(avgr, avgi)); - m_sum.real(0.0); - m_sum.imag(0.0); - } - } - - if (m_sampleSink != 0) - { - m_sampleSink->feed(m_sampleBuffer.begin(), m_sampleBuffer.end(), true); // SSB - } - - m_sampleBuffer.clear(); - } - - sample = m_SSBFilterBuffer[m_SSBFilterBufferIndex++]; -} - -void FreeDVMod::calculateLevel(Complex& sample) -{ - Real t = sample.real(); // TODO: possibly adjust depending on sample type - - if (m_levelCalcCount < m_levelNbSamples) - { - m_peakLevel = std::max(std::fabs(m_peakLevel), t); - m_levelSum += t * t; - m_levelCalcCount++; - } - else - { - qreal rmsLevel = sqrt(m_levelSum / m_levelNbSamples); - //qDebug("NFMMod::calculateLevel: %f %f", rmsLevel, m_peakLevel); - emit levelChanged(rmsLevel, m_peakLevel, m_levelNbSamples); - m_peakLevel = 0.0f; - m_levelSum = 0.0f; - m_levelCalcCount = 0; - } -} - -void FreeDVMod::calculateLevel(qint16& sample) -{ - Real t = sample / SDR_TX_SCALEF; - - if (m_levelCalcCount < m_levelNbSamples) - { - m_peakLevel = std::max(std::fabs(m_peakLevel), t); - m_levelSum += t * t; - m_levelCalcCount++; - } - else - { - qreal rmsLevel = sqrt(m_levelSum / m_levelNbSamples); - //qDebug("FreeDVMod::calculateLevel: %f %f", rmsLevel, m_peakLevel); - emit levelChanged(rmsLevel, m_peakLevel, m_levelNbSamples); - m_peakLevel = 0.0f; - m_levelSum = 0.0f; - m_levelCalcCount = 0; - } + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; } void FreeDVMod::start() { - qDebug() << "FreeDVMod::start: m_outputSampleRate: " << m_outputSampleRate - << " m_inputFrequencyOffset: " << m_settings.m_inputFrequencyOffset; - - m_audioFifo.clear(); - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); + qDebug("FreeDVMod::start"); + m_basebandSource->reset(); + m_thread->start(); } void FreeDVMod::stop() { + qDebug("FreeDVMod::stop"); + m_thread->exit(); + m_thread->wait(); +} + +void FreeDVMod::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); } bool FreeDVMod::handleMessage(const Message& cmd) { - if (UpChannelizer::MsgChannelizerNotification::match(cmd)) - { - UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd; - qDebug() << "FreeDVMod::handleMessage: MsgChannelizerNotification"; - - applyChannelSettings(notif.getBasebandSampleRate(), notif.getSampleRate(), notif.getFrequencyOffset()); - - return true; - } - else if (MsgConfigureChannelizer::match(cmd)) + if (MsgConfigureChannelizer::match(cmd)) { MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; - qDebug() << "FreeDVMod::handleMessage: MsgConfigureChannelizer: sampleRate: " << cfg.getSampleRate() - << " centerFrequency: " << cfg.getCenterFrequency(); + qDebug() << "FreeDVMod::handleMessage: MsgConfigureChannelizer:" + << " getSourceSampleRate: " << cfg.getSourceSampleRate() + << " getSourceCenterFrequency: " << cfg.getSourceCenterFrequency(); - m_channelizer->configure(m_channelizer->getInputMessageQueue(), - cfg.getSampleRate(), - cfg.getCenterFrequency()); + FreeDVModBaseband::MsgConfigureChannelizer *msg + = FreeDVModBaseband::MsgConfigureChannelizer::create(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_basebandSource->getInputMessageQueue()->push(msg); return true; } @@ -503,22 +171,14 @@ bool FreeDVMod::handleMessage(const Message& cmd) return true; } - else if (DSPConfigureAudio::match(cmd)) - { - DSPConfigureAudio& cfg = (DSPConfigureAudio&) cmd; - uint32_t sampleRate = cfg.getSampleRate(); - - qDebug() << "FreeDVMod::handleMessage: DSPConfigureAudio:" - << " sampleRate: " << sampleRate; - - if (sampleRate != m_audioSampleRate) { - applyAudioSampleRate(sampleRate); - } - - return true; - } else if (DSPSignalNotification::match(cmd)) { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "FreeDVMod::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + return true; } else @@ -537,7 +197,7 @@ void FreeDVMod::openFileStream() m_fileSize = m_ifstream.tellg(); m_ifstream.seekg(0,std::ios_base::beg); - m_recordLength = m_fileSize / (sizeof(int16_t) * m_inputSampleRate); + m_recordLength = m_fileSize / (sizeof(int16_t) * m_fileSampleRate); qDebug() << "FreeDVMod::openFileStream: " << m_fileName.toStdString().c_str() << " fileSize: " << m_fileSize << "bytes" @@ -546,7 +206,7 @@ void FreeDVMod::openFileStream() if (getMessageQueueToGUI()) { MsgReportFileSourceStreamData *report; - report = MsgReportFileSourceStreamData::create(m_inputSampleRate, m_recordLength); + report = MsgReportFileSourceStreamData::create(m_fileSampleRate, m_recordLength); getMessageQueueToGUI()->push(report); } } @@ -557,184 +217,13 @@ void FreeDVMod::seekFileStream(int seekPercentage) if (m_ifstream.is_open()) { - int seekPoint = ((m_recordLength * seekPercentage) / 100) * m_inputSampleRate; + int seekPoint = ((m_recordLength * seekPercentage) / 100) * m_fileSampleRate; seekPoint *= sizeof(Real); m_ifstream.clear(); m_ifstream.seekg(seekPoint, std::ios::beg); } } -void FreeDVMod::applyAudioSampleRate(int sampleRate) -{ - qDebug("FreeDVMod::applyAudioSampleRate: %d", sampleRate); - // TODO: put up simple IIR interpolator when sampleRate < m_modemSampleRate - - m_settingsMutex.lock(); - m_audioResampler.setDecimation(sampleRate / m_inputSampleRate); - m_audioResampler.setAudioFilters(sampleRate, sampleRate, 250, 3300); - m_settingsMutex.unlock(); - - m_audioSampleRate = sampleRate; -} - -void FreeDVMod::applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force) -{ - qDebug() << "FreeDVMod::applyChannelSettings:" - << " basebandSampleRate: " << basebandSampleRate - << " outputSampleRate: " << outputSampleRate - << " inputFrequencyOffset: " << inputFrequencyOffset; - - if ((inputFrequencyOffset != m_inputFrequencyOffset) || - (outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_carrierNco.setFreq(inputFrequencyOffset, outputSampleRate); - m_settingsMutex.unlock(); - } - - if ((outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_modemSampleRate / (Real) outputSampleRate; - m_interpolator.create(48, m_modemSampleRate, m_hiCutoff, 3.0); - m_settingsMutex.unlock(); - } - - m_basebandSampleRate = basebandSampleRate; - m_outputSampleRate = outputSampleRate; - m_inputFrequencyOffset = inputFrequencyOffset; -} - -void FreeDVMod::applyFreeDVMode(FreeDVModSettings::FreeDVMode mode) -{ - m_hiCutoff = FreeDVModSettings::getHiCutoff(mode); - m_lowCutoff = FreeDVModSettings::getLowCutoff(mode); - int modemSampleRate = FreeDVModSettings::getModSampleRate(mode); - - m_settingsMutex.lock(); - m_SSBFilter->create_filter(m_lowCutoff / modemSampleRate, m_hiCutoff / modemSampleRate); - - // baseband interpolator and filter - if (modemSampleRate != m_modemSampleRate) - { - MsgConfigureChannelizer* channelConfigMsg = MsgConfigureChannelizer::create( - modemSampleRate, m_settings.m_inputFrequencyOffset); - m_inputMessageQueue.push(channelConfigMsg); - - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) modemSampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, modemSampleRate, m_hiCutoff, 3.0); - m_modemSampleRate = modemSampleRate; - - if (getMessageQueueToGUI()) - { - DSPConfigureAudio *cfg = new DSPConfigureAudio(m_modemSampleRate, DSPConfigureAudio::AudioInput); - getMessageQueueToGUI()->push(cfg); - } - } - - // FreeDV object - - if (m_freeDV) { - freedv_close(m_freeDV); - } - - int fdv_mode = -1; - - switch(mode) - { - case FreeDVModSettings::FreeDVMode700C: - fdv_mode = FREEDV_MODE_700C; - m_scaleFactor = SDR_TX_SCALEF / 3.2f; - break; - case FreeDVModSettings::FreeDVMode700D: - fdv_mode = FREEDV_MODE_700D; - m_scaleFactor = SDR_TX_SCALEF / 3.2f; - break; - case FreeDVModSettings::FreeDVMode800XA: - fdv_mode = FREEDV_MODE_800XA; - m_scaleFactor = SDR_TX_SCALEF / 8.2f; - break; - case FreeDVModSettings::FreeDVMode1600: - fdv_mode = FREEDV_MODE_1600; - m_scaleFactor = SDR_TX_SCALEF / 3.2f; - break; - case FreeDVModSettings::FreeDVMode2400A: - default: - fdv_mode = FREEDV_MODE_2400A; - m_scaleFactor = SDR_TX_SCALEF / 8.2f; - break; - } - - if (fdv_mode == FREEDV_MODE_700D) - { - struct freedv_advanced adv; - adv.interleave_frames = 1; - m_freeDV = freedv_open_advanced(fdv_mode, &adv); - } - else - { - m_freeDV = freedv_open(fdv_mode); - } - - if (m_freeDV) - { - freedv_set_test_frames(m_freeDV, 0); - freedv_set_snr_squelch_thresh(m_freeDV, -100.0); - freedv_set_squelch_en(m_freeDV, 1); - freedv_set_clip(m_freeDV, 0); - freedv_set_tx_bpf(m_freeDV, 1); - freedv_set_ext_vco(m_freeDV, 0); - - freedv_set_callback_txt(m_freeDV, nullptr, nullptr, nullptr); - freedv_set_callback_protocol(m_freeDV, nullptr, nullptr, nullptr); - freedv_set_callback_data(m_freeDV, nullptr, nullptr, nullptr); - - int nSpeechSamples = freedv_get_n_speech_samples(m_freeDV); - int nNomModemSamples = freedv_get_n_nom_modem_samples(m_freeDV); - int Fs = freedv_get_modem_sample_rate(m_freeDV); - int Rs = freedv_get_modem_symbol_rate(m_freeDV); - - if (nSpeechSamples != m_nSpeechSamples) - { - if (m_speechIn) { - delete[] m_speechIn; - } - - m_speechIn = new int16_t[nSpeechSamples]; - m_nSpeechSamples = nSpeechSamples; - } - - if (nNomModemSamples != m_nNomModemSamples) - { - if (m_modOut) { - delete[] m_modOut; - } - - m_modOut = new int16_t[nNomModemSamples]; - m_nNomModemSamples = nNomModemSamples; - } - - m_iSpeech = 0; - m_iModem = 0; - - qDebug() << "FreeDVMod::applyFreeDVMode:" - << " fdv_mode: " << fdv_mode - << " m_modemSampleRate: " << m_modemSampleRate - << " m_lowCutoff: " << m_lowCutoff - << " m_hiCutoff: " << m_hiCutoff - << " Fs: " << Fs - << " Rs: " << Rs - << " m_nSpeechSamples: " << m_nSpeechSamples - << " m_nNomModemSamples: " << m_nNomModemSamples; - } - - m_settingsMutex.unlock(); -} - void FreeDVMod::applySettings(const FreeDVModSettings& settings, bool force) { QList reverseAPIKeys; @@ -775,29 +264,30 @@ void FreeDVMod::applySettings(const FreeDVModSettings& settings, bool force) if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force) { reverseAPIKeys.append("audioDeviceName"); } - - if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) - { - m_settingsMutex.lock(); - m_toneNco.setFreq(settings.m_toneFrequency, m_inputSampleRate); - m_settingsMutex.unlock(); + if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) { + reverseAPIKeys.append("toneFrequency"); + } + if ((m_settings.m_freeDVMode != settings.m_freeDVMode) || force) { + reverseAPIKeys.append("freeDVMode"); } if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force) { AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); int audioDeviceIndex = audioDeviceManager->getInputDeviceIndex(settings.m_audioDeviceName); - audioDeviceManager->addAudioSource(&m_audioFifo, getInputMessageQueue(), audioDeviceIndex); + audioDeviceManager->addAudioSource(m_basebandSource->getAudioFifo(), getInputMessageQueue(), audioDeviceIndex); uint32_t audioSampleRate = audioDeviceManager->getInputSampleRate(audioDeviceIndex); - if (m_audioSampleRate != audioSampleRate) { - applyAudioSampleRate(audioSampleRate); + if (m_basebandSource->getAudioSampleRate() != audioSampleRate) + { + reverseAPIKeys.append("audioSampleRate"); + DSPConfigureAudio *msg = new DSPConfigureAudio(audioSampleRate, DSPConfigureAudio::AudioInput); + m_basebandSource->getInputMessageQueue()->push(msg); } } - if ((m_settings.m_freeDVMode != settings.m_freeDVMode) || force) { - applyFreeDVMode(settings.m_freeDVMode); - } + FreeDVModBaseband::MsgConfigureFreeDVModBaseband *msg = FreeDVModBaseband::MsgConfigureFreeDVModBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); if (settings.m_useReverseAPI) { @@ -844,7 +334,7 @@ int FreeDVMod::webapiSettingsGet( webapiFormatChannelSettings(response, m_settings); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = response.getFreeDvModSettings()->getCwKeyer(); - const CWKeyerSettings& cwKeyerSettings = m_cwKeyer.getSettings(); + const CWKeyerSettings& cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); CWKeyer::webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); return 200; @@ -863,11 +353,11 @@ int FreeDVMod::webapiSettingsPutPatch( if (channelSettingsKeys.contains("cwKeyer")) { SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = response.getFreeDvModSettings()->getCwKeyer(); - CWKeyerSettings cwKeyerSettings = m_cwKeyer.getSettings(); + CWKeyerSettings cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); CWKeyer::webapiSettingsPutPatch(channelSettingsKeys, cwKeyerSettings, apiCwKeyerSettings); CWKeyer::MsgConfigureCWKeyer *msgCwKeyer = CWKeyer::MsgConfigureCWKeyer::create(cwKeyerSettings, force); - m_cwKeyer.getInputMessageQueue()->push(msgCwKeyer); + m_basebandSource->getCWKeyer().getInputMessageQueue()->push(msgCwKeyer); if (m_guiMessageQueue) // forward to GUI if any { @@ -879,7 +369,7 @@ int FreeDVMod::webapiSettingsPutPatch( if (m_settings.m_inputFrequencyOffset != settings.m_inputFrequencyOffset) { FreeDVMod::MsgConfigureChannelizer *msgChan = FreeDVMod::MsgConfigureChannelizer::create( - m_audioSampleRate, settings.m_inputFrequencyOffset); + m_basebandSource->getAudioSampleRate(), settings.m_inputFrequencyOffset); m_inputMessageQueue.push(msgChan); } @@ -1012,8 +502,8 @@ void FreeDVMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& res void FreeDVMod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) { response.getFreeDvModReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); - response.getFreeDvModReport()->setAudioSampleRate(m_audioSampleRate); - response.getFreeDvModReport()->setChannelSampleRate(m_outputSampleRate); + response.getFreeDvModReport()->setAudioSampleRate(m_basebandSource->getAudioSampleRate()); + response.getFreeDvModReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); } void FreeDVMod::webapiReverseSendSettings(QList& channelSettingsKeys, const FreeDVModSettings& settings, bool force) @@ -1067,10 +557,10 @@ void FreeDVMod::webapiReverseSendSettings(QList& channelSettingsKeys, c if (force) { - const CWKeyerSettings& cwKeyerSettings = m_cwKeyer.getSettings(); + const CWKeyerSettings& cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); swgFreeDVModSettings->setCwKeyer(new SWGSDRangel::SWGCWKeyerSettings()); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = swgFreeDVModSettings->getCwKeyer(); - m_cwKeyer.webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); + m_basebandSource->getCWKeyer().webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); } QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") @@ -1081,13 +571,14 @@ void FreeDVMod::webapiReverseSendSettings(QList& channelSettingsKeys, c m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -1102,7 +593,7 @@ void FreeDVMod::webapiReverseSendCWSettings(const CWKeyerSettings& cwKeyerSettin swgFreeDVModSettings->setCwKeyer(new SWGSDRangel::SWGCWKeyerSettings()); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = swgFreeDVModSettings->getCwKeyer(); - m_cwKeyer.webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); + m_basebandSource->getCWKeyer().webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") .arg(m_settings.m_reverseAPIAddress) @@ -1112,13 +603,14 @@ void FreeDVMod::webapiReverseSendCWSettings(const CWKeyerSettings& cwKeyerSettin m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -1133,10 +625,53 @@ void FreeDVMod::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("FreeDVMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("FreeDVMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); +} + +void FreeDVMod::setSpectrumSampleSink(BasebandSampleSink* sampleSink) +{ + m_basebandSource->setSpectrumSampleSink(sampleSink); +} + +uint32_t FreeDVMod::getAudioSampleRate() const +{ + return m_basebandSource->getAudioSampleRate(); +} + +uint32_t FreeDVMod::getModemSampleRate() const +{ + return m_basebandSource->getModemSampleRate(); +} + +Real FreeDVMod::getLowCutoff() const +{ + return m_basebandSource->getLowCutoff(); +} + +Real FreeDVMod::getHiCutoff() const +{ + return m_basebandSource->getHiCutoff(); +} + +double FreeDVMod::getMagSq() const +{ + return m_basebandSource->getMagSq(); +} + +CWKeyer *FreeDVMod::getCWKeyer() +{ + return &m_basebandSource->getCWKeyer(); +} + +void FreeDVMod::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); } diff --git a/plugins/channeltx/modfreedv/freedvmod.h b/plugins/channeltx/modfreedv/freedvmod.h index a03ae0fc1..5df6108f4 100644 --- a/plugins/channeltx/modfreedv/freedvmod.h +++ b/plugins/channeltx/modfreedv/freedvmod.h @@ -28,23 +28,16 @@ #include "dsp/basebandsamplesource.h" #include "channel/channelapi.h" #include "dsp/basebandsamplesink.h" -#include "dsp/ncof.h" -#include "dsp/interpolator.h" -#include "util/movingaverage.h" -#include "dsp/agc.h" -#include "dsp/fftfilt.h" -#include "dsp/cwkeyer.h" -#include "audio/audiofifo.h" -#include "audio/audioresampler.h" #include "util/message.h" #include "freedvmodsettings.h" class QNetworkAccessManager; class QNetworkReply; +class QThread; class DeviceAPI; -class ThreadedBasebandSampleSource; -class UpChannelizer; +class CWKeyer; +class FreeDVModBaseband; struct freedv; @@ -75,26 +68,33 @@ public: { } }; + /** + * |<------ Baseband from device (before device soft interpolation) -------------------------->| + * |<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->| + * | ^-------------------------------| + * | | Source CF + * | | Source SR | + */ class MsgConfigureChannelizer : public Message { MESSAGE_CLASS_DECLARATION public: - int getSampleRate() const { return m_sampleRate; } - int getCenterFrequency() const { return m_centerFrequency; } + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } - static MsgConfigureChannelizer* create(int sampleRate, int centerFrequency) + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) { - return new MsgConfigureChannelizer(sampleRate, centerFrequency); + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); } private: - int m_sampleRate; - int m_centerFrequency; + int m_sourceSampleRate; + int m_sourceCenterFrequency; - MsgConfigureChannelizer(int sampleRate, int centerFrequency) : + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : Message(), - m_sampleRate(sampleRate), - m_centerFrequency(centerFrequency) + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) { } }; @@ -209,12 +209,9 @@ public: ~FreeDVMod(); virtual void destroy() { delete this; } - void setSpectrumSampleSink(BasebandSampleSink* sampleSink) { m_sampleSink = sampleSink; } - - virtual void pull(Sample& sample); - virtual void pullAudio(int nbSamples); virtual void start(); virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); virtual bool handleMessage(const Message& cmd); virtual void getIdentifier(QString& id) { id = objectName(); } @@ -257,27 +254,18 @@ public: const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response); - uint32_t getAudioSampleRate() const { return m_audioSampleRate; } - uint32_t getModemSampleRate() const { return m_modemSampleRate; } - double getMagSq() const { return m_magsq; } - Real getLowCutoff() const { return m_lowCutoff; } - Real getHiCutoff() const { return m_hiCutoff; } - - CWKeyer *getCWKeyer() { return &m_cwKeyer; } + uint32_t getAudioSampleRate() const; + uint32_t getModemSampleRate() const; + Real getLowCutoff() const; + Real getHiCutoff() const; + double getMagSq() const; + CWKeyer *getCWKeyer(); + void setLevelMeter(QObject *levelMeter); + void setSpectrumSampleSink(BasebandSampleSink* sampleSink); static const QString m_channelIdURI; static const QString m_channelId; -signals: - /** - * Level changed - * \param rmsLevel RMS level in range 0.0 - 1.0 - * \param peakLevel Peak level in range 0.0 - 1.0 - * \param numSamples Number of audio samples analyzed - */ - void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); - - private: enum RateState { RSInitialFill, @@ -285,80 +273,23 @@ private: }; DeviceAPI* m_deviceAPI; - ThreadedBasebandSampleSource* m_threadedChannelizer; - UpChannelizer* m_channelizer; - - int m_basebandSampleRate; - int m_outputSampleRate; - int m_modemSampleRate; - int m_inputFrequencyOffset; - Real m_lowCutoff; - Real m_hiCutoff; + QThread *m_thread; + FreeDVModBaseband* m_basebandSource; FreeDVModSettings m_settings; - NCOF m_carrierNco; - NCOF m_toneNco; - Complex m_modSample; - Interpolator m_interpolator; - Real m_interpolatorDistance; - Real m_interpolatorDistanceRemain; - bool m_interpolatorConsumed; - fftfilt* m_SSBFilter; - Complex* m_SSBFilterBuffer; - int m_SSBFilterBufferIndex; - static const int m_ssbFftLen; - - BasebandSampleSink* m_sampleSink; SampleVector m_sampleBuffer; - - fftfilt::cmplx m_sum; - int m_undersampleCount; - int m_sumCount; - - double m_magsq; - MovingAverageUtil m_movingAverage; - - quint32 m_audioSampleRate; - AudioVector m_audioBuffer; - uint m_audioBufferFill; - AudioFifo m_audioFifo; - QMutex m_settingsMutex; std::ifstream m_ifstream; QString m_fileName; quint64 m_fileSize; //!< raw file size (bytes) quint32 m_recordLength; //!< record length in seconds computed from file size - int m_inputSampleRate; //!< speech (input) sample rate (fixed 8000 S/s) - - quint32 m_levelCalcCount; - Real m_peakLevel; - Real m_levelSum; - CWKeyer m_cwKeyer; + int m_fileSampleRate; //!< speech (input) sample rate (fixed 8000 S/s) QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; - struct freedv *m_freeDV; - int m_nSpeechSamples; - int m_nNomModemSamples; - int m_iSpeech; - int m_iModem; - int16_t *m_speechIn; - int16_t *m_modOut; - float m_scaleFactor; //!< divide by this amount to scale from int16 to float in [-1.0, 1.0] interval - AudioResampler m_audioResampler; - - static const int m_levelNbSamples; - - void applyAudioSampleRate(int sampleRate); - void applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force = false); void applySettings(const FreeDVModSettings& settings, bool force = false); - void applyFreeDVMode(FreeDVModSettings::FreeDVMode mode); - void pullAF(Complex& sample); - void calculateLevel(Complex& sample); - void calculateLevel(qint16& sample); - void modulateSample(); void openFileStream(); void seekFileStream(int seekPercentage); void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); diff --git a/plugins/channeltx/modfreedv/freedvmodbaseband.cpp b/plugins/channeltx/modfreedv/freedvmodbaseband.cpp new file mode 100644 index 000000000..d20e9f20e --- /dev/null +++ b/plugins/channeltx/modfreedv/freedvmodbaseband.cpp @@ -0,0 +1,218 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "freedvmodbaseband.h" + +MESSAGE_CLASS_DEFINITION(FreeDVModBaseband::MsgConfigureFreeDVModBaseband, Message) +MESSAGE_CLASS_DEFINITION(FreeDVModBaseband::MsgConfigureChannelizer, Message) + +FreeDVModBaseband::FreeDVModBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpSampleChannelizer(&m_source); + + qDebug("FreeDVModBaseband::FreeDVModBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &FreeDVModBaseband::handleData, + Qt::QueuedConnection + ); + + DSPEngine::instance()->getAudioDeviceManager()->addAudioSource(m_source.getAudioFifo(), getInputMessageQueue()); + m_source.applyAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getInputSampleRate()); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +FreeDVModBaseband::~FreeDVModBaseband() +{ + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSource(m_source.getAudioFifo()); + delete m_channelizer; +} + +void FreeDVModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void FreeDVModBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void FreeDVModBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + Real rmsLevel, peakLevel, numSamples; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); +} + +void FreeDVModBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void FreeDVModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool FreeDVModBaseband::handleMessage(const Message& cmd) +{ + if (DSPConfigureAudio::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPConfigureAudio& cfg = (DSPConfigureAudio&) cmd; + uint32_t sampleRate = cfg.getSampleRate(); + DSPConfigureAudio::AudioType audioType = cfg.getAudioType(); + + qDebug() << "FreeDVModBaseband::handleMessage: DSPConfigureAudio:" + << " sampleRate: " << sampleRate + << " audioType: " << audioType; + + if (audioType == DSPConfigureAudio::AudioInput) + { + if (sampleRate != m_source.getAudioSampleRate()) { + m_source.applyAudioSampleRate(sampleRate); + } + } + + return true; + } + else if (MsgConfigureFreeDVModBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureFreeDVModBaseband& cfg = (MsgConfigureFreeDVModBaseband&) cmd; + qDebug() << "FreeDVModBaseband::handleMessage: MsgConfigureFreeDVModBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureChannelizer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; + qDebug() << "FreeDVModBaseband::handleMessage: MsgConfigureChannelizer" + << "(requested) sourceSampleRate: " << cfg.getSourceSampleRate() + << "(requested) sourceCenterFrequency: " << cfg.getSourceCenterFrequency(); + m_channelizer->setChannelization(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "FreeDVModBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (CWKeyer::MsgConfigureCWKeyer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + const CWKeyer::MsgConfigureCWKeyer& cfg = (CWKeyer::MsgConfigureCWKeyer&) cmd; + CWKeyer::MsgConfigureCWKeyer *notif = new CWKeyer::MsgConfigureCWKeyer(cfg); + CWKeyer& cwKeyer = m_source.getCWKeyer(); + cwKeyer.getInputMessageQueue()->push(notif); + + return true; + } + else + { + return false; + } +} + +void FreeDVModBaseband::applySettings(const FreeDVModSettings& settings, bool force) +{ + m_source.applySettings(settings, force); + m_settings = settings; +} + +int FreeDVModBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} \ No newline at end of file diff --git a/plugins/channeltx/modfreedv/freedvmodbaseband.h b/plugins/channeltx/modfreedv/freedvmodbaseband.h new file mode 100644 index 000000000..f0580a1b3 --- /dev/null +++ b/plugins/channeltx/modfreedv/freedvmodbaseband.h @@ -0,0 +1,124 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FREEDVMODBASEBAND_H +#define INCLUDE_FREEDVMODBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "freedvmodsource.h" + +class UpSampleChannelizer; + +class FreeDVModBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureFreeDVModBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const FreeDVModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureFreeDVModBaseband* create(const FreeDVModSettings& settings, bool force) + { + return new MsgConfigureFreeDVModBaseband(settings, force); + } + + private: + FreeDVModSettings m_settings; + bool m_force; + + MsgConfigureFreeDVModBaseband(const FreeDVModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureChannelizer : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } + + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) + { + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); + } + + private: + int m_sourceSampleRate; + int m_sourceCenterFrequency; + + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : + Message(), + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) + { } + }; + + FreeDVModBaseband(); + ~FreeDVModBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + CWKeyer& getCWKeyer() { return m_source.getCWKeyer(); } + double getMagSq() const { return m_source.getMagSq(); } + int getChannelSampleRate() const; + void setInputFileStream(std::ifstream *ifstream) { m_source.setInputFileStream(ifstream); } + AudioFifo *getAudioFifo() { return m_source.getAudioFifo(); } + void setSpectrumSampleSink(BasebandSampleSink* sampleSink) { m_source.setSpectrumSink(sampleSink); } + unsigned int getAudioSampleRate() const { return m_source.getAudioSampleRate(); } + unsigned int getModemSampleRate() const { return m_source.getModemSampleRate(); } + Real getLowCutoff() const { return m_source.getLowCutoff(); } + Real getHiCutoff() const { return m_source.getHiCutoff(); } + +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + SampleSourceFifo m_sampleFifo; + UpSampleChannelizer *m_channelizer; + FreeDVModSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + FreeDVModSettings m_settings; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const FreeDVModSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_FREEDVMODBASEBAND_H diff --git a/plugins/channeltx/modfreedv/freedvmodgui.cpp b/plugins/channeltx/modfreedv/freedvmodgui.cpp index 4129837e4..d1d36c104 100644 --- a/plugins/channeltx/modfreedv/freedvmodgui.cpp +++ b/plugins/channeltx/modfreedv/freedvmodgui.cpp @@ -30,6 +30,7 @@ #include "util/db.h" #include "dsp/dspengine.h" #include "dsp/dspcommands.h" +#include "dsp/cwkeyer.h" #include "gui/crightclickenabler.h" #include "gui/audioselectdialog.h" #include "gui/basicchannelsettingsdialog.h" @@ -393,7 +394,7 @@ FreeDVModGUI::FreeDVModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb m_settings.setCWKeyerGUI(ui->cwKeyerGUI); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); - connect(m_freeDVMod, SIGNAL(levelChanged(qreal, qreal, int)), ui->volumeMeter, SLOT(levelChanged(qreal, qreal, int))); + m_freeDVMod->setLevelMeter(ui->volumeMeter); displaySettings(); applyBandwidths(5 - ui->spanLog2->value(), true); // does applySettings(true) diff --git a/plugins/channeltx/modfreedv/freedvmodplugin.cpp b/plugins/channeltx/modfreedv/freedvmodplugin.cpp index 9e5e5fec1..38acf5490 100644 --- a/plugins/channeltx/modfreedv/freedvmodplugin.cpp +++ b/plugins/channeltx/modfreedv/freedvmodplugin.cpp @@ -27,7 +27,7 @@ const PluginDescriptor FreeDVModPlugin::m_pluginDescriptor = { QString("FreeDV Modulator"), - QString("4.11.6"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/channeltx/modfreedv/freedvmodsource.cpp b/plugins/channeltx/modfreedv/freedvmodsource.cpp new file mode 100644 index 000000000..4dffd7149 --- /dev/null +++ b/plugins/channeltx/modfreedv/freedvmodsource.cpp @@ -0,0 +1,525 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "codec2/freedv_api.h" + +#include "dsp/basebandsamplesink.h" +#include "freedvmodsource.h" + +const int FreeDVModSource::m_levelNbSamples = 80; // every 10ms +const int FreeDVModSource::m_ssbFftLen = 1024; + +FreeDVModSource::FreeDVModSource() : + m_channelSampleRate(48000), + m_channelFrequencyOffset(0), + m_modemSampleRate(48000), // // default 2400A mode + m_lowCutoff(0.0), + m_hiCutoff(6000.0), + m_SSBFilter(nullptr), + m_SSBFilterBuffer(0), + m_SSBFilterBufferIndex(0), + m_audioFifo(4800), + m_levelCalcCount(0), + m_peakLevel(0.0f), + m_levelSum(0.0f), + m_freeDV(nullptr), + m_nSpeechSamples(0), + m_nNomModemSamples(0), + m_iSpeech(0), + m_iModem(0), + m_speechIn(nullptr), + m_modOut(0), + m_scaleFactor(SDR_TX_SCALEF) +{ + m_SSBFilter = new fftfilt(m_lowCutoff / m_audioSampleRate, m_hiCutoff / m_audioSampleRate, m_ssbFftLen); + m_SSBFilterBuffer = new Complex[m_ssbFftLen>>1]; // filter returns data exactly half of its size + std::fill(m_SSBFilterBuffer, m_SSBFilterBuffer+(m_ssbFftLen>>1), Complex{0,0}); + + m_audioBuffer.resize(1<<14); + m_audioBufferFill = 0; + + m_sum.real(0.0f); + m_sum.imag(0.0f); + m_undersampleCount = 0; + m_sumCount = 0; + + m_magsq = 0.0; + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +FreeDVModSource::~FreeDVModSource() +{ + + delete m_SSBFilter; + delete[] m_SSBFilterBuffer; + + if (m_freeDV) { + freedv_close(m_freeDV); + } +} + +void FreeDVModSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void FreeDVModSource::pullOne(Sample& sample) +{ + Complex ci; + + if (m_interpolatorDistance > 1.0f) // decimate + { + modulateSample(); + + while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + else + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + + m_interpolatorDistanceRemain += m_interpolatorDistance; + + ci *= m_carrierNco.nextIQ(); // shift to carrier frequency + ci *= 0.891235351562f * SDR_TX_SCALEF; //scaling at -1 dB to account for possible filter overshoot + + double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); + magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + + sample.m_real = (FixReal) ci.real(); + sample.m_imag = (FixReal) ci.imag(); +} + +void FreeDVModSource::prefetch(unsigned int nbSamples) +{ + unsigned int nbSamplesAudio = nbSamples * ((Real) m_audioSampleRate / (Real) m_channelSampleRate); + pullAudio(nbSamplesAudio); +} + +void FreeDVModSource::pullAudio(unsigned int nbSamples) +{ + unsigned int nbSamplesAudio = nbSamples * ((Real) m_audioSampleRate / (Real) m_modemSampleRate); + + if (nbSamplesAudio > m_audioBuffer.size()) + { + m_audioBuffer.resize(nbSamplesAudio); + } + + m_audioFifo.read(reinterpret_cast(&m_audioBuffer[0]), nbSamplesAudio); + m_audioBufferFill = 0; +} + +void FreeDVModSource::modulateSample() +{ + pullAF(m_modSample); + if (!m_settings.m_gaugeInputElseModem) { + calculateLevel(m_modSample); + } + m_audioBufferFill++; +} + +void FreeDVModSource::pullAF(Complex& sample) +{ + if (m_settings.m_audioMute) + { + sample.real(0.0f); + sample.imag(0.0f); + return; + } + + Complex ci; + fftfilt::cmplx *filtered; + int n_out = 0; + + int decim = 1<<(m_settings.m_spanLog2 - 1); + unsigned char decim_mask = decim - 1; // counter LSB bit mask for decimation by 2^(m_scaleLog2 - 1) + + if (m_iModem >= m_nNomModemSamples) + { + switch (m_settings.m_modAFInput) + { + case FreeDVModSettings::FreeDVModInputTone: + for (int i = 0; i < m_nSpeechSamples; i++) + { + m_speechIn[i] = m_toneNco.next() * 32768.0f * m_settings.m_volumeFactor; + if (m_settings.m_gaugeInputElseModem) { + calculateLevel(m_speechIn[i]); + } + } + freedv_tx(m_freeDV, m_modOut, m_speechIn); + break; + case FreeDVModSettings::FreeDVModInputFile: + if (m_iModem >= m_nNomModemSamples) + { + if (m_ifstream && m_ifstream->is_open()) + { + std::fill(m_speechIn, m_speechIn + m_nSpeechSamples, 0); + + if (m_ifstream->eof()) + { + if (m_settings.m_playLoop) + { + m_ifstream->clear(); + m_ifstream->seekg(0, std::ios::beg); + } + } + + if (m_ifstream->eof()) + { + std::fill(m_modOut, m_modOut + m_nNomModemSamples, 0); + } + else + { + + m_ifstream->read(reinterpret_cast(m_speechIn), sizeof(int16_t) * m_nSpeechSamples); + + if ((m_settings.m_volumeFactor != 1.0) || m_settings.m_gaugeInputElseModem) + { + for (int i = 0; i < m_nSpeechSamples; i++) + { + if (m_settings.m_volumeFactor != 1.0) { + m_speechIn[i] *= m_settings.m_volumeFactor; + } + if (m_settings.m_gaugeInputElseModem) { + calculateLevel(m_speechIn[i]); + } + } + } + + freedv_tx(m_freeDV, m_modOut, m_speechIn); + } + } + else + { + std::fill(m_modOut, m_modOut + m_nNomModemSamples, 0); + } + } + break; + case FreeDVModSettings::FreeDVModInputAudio: + for (int i = 0; i < m_nSpeechSamples; i++) + { + qint16 audioSample = (m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) * (m_settings.m_volumeFactor / 2.0f); + m_audioBufferFill++; + + while (!m_audioResampler.downSample(audioSample, m_speechIn[i])) + { + audioSample = (m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) * (m_settings.m_volumeFactor / 2.0f); + m_audioBufferFill++; + } + + if (m_settings.m_gaugeInputElseModem) { + calculateLevel(m_speechIn[i]); + } + } + freedv_tx(m_freeDV, m_modOut, m_speechIn); + break; + case FreeDVModSettings::FreeDVModInputCWTone: + for (int i = 0; i < m_nSpeechSamples; i++) + { + Real fadeFactor; + + if (m_cwKeyer.getSample()) + { + m_cwKeyer.getCWSmoother().getFadeSample(true, fadeFactor); + m_speechIn[i] = m_toneNco.next() * 32768.0f * fadeFactor * m_settings.m_volumeFactor; + } + else + { + if (m_cwKeyer.getCWSmoother().getFadeSample(false, fadeFactor)) + { + m_speechIn[i] = m_toneNco.next() * 32768.0f * fadeFactor * m_settings.m_volumeFactor; + } + else + { + m_speechIn[i] = 0; + m_toneNco.setPhase(0); + } + } + + if (m_settings.m_gaugeInputElseModem) { + calculateLevel(m_speechIn[i]); + } + } + freedv_tx(m_freeDV, m_modOut, m_speechIn); + break; + case FreeDVModSettings::FreeDVModInputNone: + default: + std::fill(m_speechIn, m_speechIn + m_nSpeechSamples, 0); + freedv_tx(m_freeDV, m_modOut, m_speechIn); + break; + } + + m_iModem = 0; + } + + ci.real(m_modOut[m_iModem++] / m_scaleFactor); + ci.imag(0.0f); + + n_out = m_SSBFilter->runSSB(ci, &filtered, true); // USB + + if (n_out > 0) + { + memcpy((void *) m_SSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); + m_SSBFilterBufferIndex = 0; + + for (int i = 0; i < n_out; i++) + { + // Downsample by 2^(m_scaleLog2 - 1) for SSB band spectrum display + // smart decimation with bit gain using float arithmetic (23 bits significand) + + m_sum += filtered[i]; + + if (!(m_undersampleCount++ & decim_mask)) + { + Real avgr = (m_sum.real() / decim) * 0.891235351562f * SDR_TX_SCALEF; //scaling at -1 dB to account for possible filter overshoot + Real avgi = (m_sum.imag() / decim) * 0.891235351562f * SDR_TX_SCALEF; + m_sampleBuffer.push_back(Sample(avgr, avgi)); + m_sum.real(0.0); + m_sum.imag(0.0); + } + } + + if (m_spectrumSink) { + m_spectrumSink->feed(m_sampleBuffer.begin(), m_sampleBuffer.end(), true); // SSB + } + + m_sampleBuffer.clear(); + } + + sample = m_SSBFilterBuffer[m_SSBFilterBufferIndex++]; +} + +void FreeDVModSource::calculateLevel(Complex& sample) +{ + Real t = sample.real(); // TODO: possibly adjust depending on sample type + + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), t); + m_levelSum += t * t; + m_levelCalcCount++; + } + else + { + m_rmsLevel = sqrt(m_levelSum / m_levelNbSamples); + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void FreeDVModSource::calculateLevel(qint16& sample) +{ + Real t = sample / SDR_TX_SCALEF; + + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), t); + m_levelSum += t * t; + m_levelCalcCount++; + } + else + { + m_rmsLevel = sqrt(m_levelSum / m_levelNbSamples); + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void FreeDVModSource::applyAudioSampleRate(unsigned int sampleRate) +{ + qDebug("FreeDVModSource::applyAudioSampleRate: %d", sampleRate); + // TODO: put up simple IIR interpolator when sampleRate < m_modemSampleRate + + m_audioResampler.setDecimation(sampleRate / m_channelSampleRate); + m_audioResampler.setAudioFilters(sampleRate, sampleRate, 250, 3300); + + m_audioSampleRate = sampleRate; +} + +void FreeDVModSource::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "FreeDVMod::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((channelFrequencyOffset != m_channelFrequencyOffset) || + (channelSampleRate != m_channelSampleRate) || force) + { + m_carrierNco.setFreq(channelFrequencyOffset, channelSampleRate); + } + + if ((channelSampleRate != m_channelSampleRate) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_modemSampleRate / (Real) channelSampleRate; + m_interpolator.create(48, m_modemSampleRate, m_hiCutoff, 3.0); + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void FreeDVModSource::applyFreeDVMode(FreeDVModSettings::FreeDVMode mode) +{ + m_hiCutoff = FreeDVModSettings::getHiCutoff(mode); + m_lowCutoff = FreeDVModSettings::getLowCutoff(mode); + int modemSampleRate = FreeDVModSettings::getModSampleRate(mode); + + m_SSBFilter->create_filter(m_lowCutoff / modemSampleRate, m_hiCutoff / modemSampleRate); + + // baseband interpolator and filter + if (modemSampleRate != m_modemSampleRate) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) modemSampleRate / (Real) m_channelSampleRate; + m_interpolator.create(48, modemSampleRate, m_hiCutoff, 3.0); + m_modemSampleRate = modemSampleRate; + } + + // FreeDV object + + if (m_freeDV) { + freedv_close(m_freeDV); + } + + int fdv_mode = -1; + + switch(mode) + { + case FreeDVModSettings::FreeDVMode700C: + fdv_mode = FREEDV_MODE_700C; + m_scaleFactor = SDR_TX_SCALEF / 3.2f; + break; + case FreeDVModSettings::FreeDVMode700D: + fdv_mode = FREEDV_MODE_700D; + m_scaleFactor = SDR_TX_SCALEF / 3.2f; + break; + case FreeDVModSettings::FreeDVMode800XA: + fdv_mode = FREEDV_MODE_800XA; + m_scaleFactor = SDR_TX_SCALEF / 8.2f; + break; + case FreeDVModSettings::FreeDVMode1600: + fdv_mode = FREEDV_MODE_1600; + m_scaleFactor = SDR_TX_SCALEF / 3.2f; + break; + case FreeDVModSettings::FreeDVMode2400A: + default: + fdv_mode = FREEDV_MODE_2400A; + m_scaleFactor = SDR_TX_SCALEF / 8.2f; + break; + } + + if (fdv_mode == FREEDV_MODE_700D) + { + struct freedv_advanced adv; + adv.interleave_frames = 1; + m_freeDV = freedv_open_advanced(fdv_mode, &adv); + } + else + { + m_freeDV = freedv_open(fdv_mode); + } + + if (m_freeDV) + { + freedv_set_test_frames(m_freeDV, 0); + freedv_set_snr_squelch_thresh(m_freeDV, -100.0); + freedv_set_squelch_en(m_freeDV, 1); + freedv_set_clip(m_freeDV, 0); + freedv_set_tx_bpf(m_freeDV, 1); + freedv_set_ext_vco(m_freeDV, 0); + + freedv_set_callback_txt(m_freeDV, nullptr, nullptr, nullptr); + freedv_set_callback_protocol(m_freeDV, nullptr, nullptr, nullptr); + freedv_set_callback_data(m_freeDV, nullptr, nullptr, nullptr); + + int nSpeechSamples = freedv_get_n_speech_samples(m_freeDV); + int nNomModemSamples = freedv_get_n_nom_modem_samples(m_freeDV); + int Fs = freedv_get_modem_sample_rate(m_freeDV); + int Rs = freedv_get_modem_symbol_rate(m_freeDV); + + if (nSpeechSamples != m_nSpeechSamples) + { + if (m_speechIn) { + delete[] m_speechIn; + } + + m_speechIn = new int16_t[nSpeechSamples]; + m_nSpeechSamples = nSpeechSamples; + } + + if (nNomModemSamples != m_nNomModemSamples) + { + if (m_modOut) { + delete[] m_modOut; + } + + m_modOut = new int16_t[nNomModemSamples]; + m_nNomModemSamples = nNomModemSamples; + } + + m_iSpeech = 0; + m_iModem = 0; + + qDebug() << "FreeDVMod::applyFreeDVMode:" + << " fdv_mode: " << fdv_mode + << " m_modemSampleRate: " << m_modemSampleRate + << " m_lowCutoff: " << m_lowCutoff + << " m_hiCutoff: " << m_hiCutoff + << " Fs: " << Fs + << " Rs: " << Rs + << " m_nSpeechSamples: " << m_nSpeechSamples + << " m_nNomModemSamples: " << m_nNomModemSamples; + } +} + +void FreeDVModSource::applySettings(const FreeDVModSettings& settings, bool force) +{ + if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) { + m_toneNco.setFreq(settings.m_toneFrequency, m_channelSampleRate); + } + + if ((m_settings.m_freeDVMode != settings.m_freeDVMode) || force) { + applyFreeDVMode(settings.m_freeDVMode); + } + + m_settings = settings; +} diff --git a/plugins/channeltx/modfreedv/freedvmodsource.h b/plugins/channeltx/modfreedv/freedvmodsource.h new file mode 100644 index 000000000..eedaa6492 --- /dev/null +++ b/plugins/channeltx/modfreedv/freedvmodsource.h @@ -0,0 +1,140 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FREEDVMODSOURCE_H +#define INCLUDE_FREEDVMODSOURCE_H + +#include + +#include +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/nco.h" +#include "dsp/ncof.h" +#include "dsp/interpolator.h" +#include "dsp/fftfilt.h" +#include "util/movingaverage.h" +#include "dsp/cwkeyer.h" +#include "audio/audiofifo.h" +#include "audio/audioresampler.h" + +#include "freedvmodsettings.h" + +class BasebandSampleSink; + +class FreeDVModSource : public ChannelSampleSource +{ +public: + FreeDVModSource(); + virtual ~FreeDVModSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples); + + void setInputFileStream(std::ifstream *ifstream) { m_ifstream = ifstream; } + AudioFifo *getAudioFifo() { return &m_audioFifo; } + void applyAudioSampleRate(unsigned int sampleRate); + CWKeyer& getCWKeyer() { return m_cwKeyer; } + double getMagSq() const { return m_magsq; } + void getLevels(Real& rmsLevel, Real& peakLevel, Real& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevel; + numSamples = m_levelNbSamples; + } + unsigned int getAudioSampleRate() const { return m_audioSampleRate; } + unsigned int getModemSampleRate() const { return m_modemSampleRate; } + Real getLowCutoff() const { return m_lowCutoff; } + Real getHiCutoff() const { return m_hiCutoff; } + void setSpectrumSink(BasebandSampleSink *sampleSink) { m_spectrumSink = sampleSink; } + + void applySettings(const FreeDVModSettings& settings, bool force = false); + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applyFreeDVMode(FreeDVModSettings::FreeDVMode mode); + +private: + int m_channelSampleRate; + int m_channelFrequencyOffset; + int m_modemSampleRate; + Real m_lowCutoff; + Real m_hiCutoff; + FreeDVModSettings m_settings; + + NCOF m_carrierNco; + NCOF m_toneNco; + Complex m_modSample; + + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + bool m_interpolatorConsumed; + + fftfilt* m_SSBFilter; + Complex* m_SSBFilterBuffer; + int m_SSBFilterBufferIndex; + static const int m_ssbFftLen; + + BasebandSampleSink* m_spectrumSink; + SampleVector m_sampleBuffer; + + fftfilt::cmplx m_sum; + int m_undersampleCount; + int m_sumCount; + + double m_magsq; + MovingAverageUtil m_movingAverage; + + quint32 m_audioSampleRate; + AudioVector m_audioBuffer; + uint m_audioBufferFill; + AudioFifo m_audioFifo; + + quint32 m_levelCalcCount; + Real m_rmsLevel; + Real m_peakLevelOut; + Real m_peakLevel; + Real m_levelSum; + + std::ifstream *m_ifstream; + CWKeyer m_cwKeyer; + + struct freedv *m_freeDV; + int m_nSpeechSamples; + int m_nNomModemSamples; + int m_iSpeech; + int m_iModem; + int16_t *m_speechIn; + int16_t *m_modOut; + float m_scaleFactor; //!< divide by this amount to scale from int16 to float in [-1.0, 1.0] interval + AudioResampler m_audioResampler; + + static const int m_levelNbSamples; + + void processOneSample(Complex& ci); + void pullAF(Complex& sample); + void pullAudio(unsigned int nbSamples); + void pushFeedback(Real sample); + void calculateLevel(Complex& sample); + void calculateLevel(qint16& sample); + void modulateSample(); +}; + + + +#endif // INCLUDE_FREEDVMODSOURCE_H diff --git a/plugins/channeltx/modfreedv/freedvmodwebapiadapter.cpp b/plugins/channeltx/modfreedv/freedvmodwebapiadapter.cpp index 0f9e81b22..5695e8f7d 100644 --- a/plugins/channeltx/modfreedv/freedvmodwebapiadapter.cpp +++ b/plugins/channeltx/modfreedv/freedvmodwebapiadapter.cpp @@ -16,6 +16,7 @@ /////////////////////////////////////////////////////////////////////////////////// #include "SWGChannelSettings.h" +#include "dsp/cwkeyer.h" #include "freedvmod.h" #include "freedvmodwebapiadapter.h" diff --git a/plugins/channeltx/modnfm/CMakeLists.txt b/plugins/channeltx/modnfm/CMakeLists.txt index 6281c05d4..a0907f12e 100644 --- a/plugins/channeltx/modnfm/CMakeLists.txt +++ b/plugins/channeltx/modnfm/CMakeLists.txt @@ -1,7 +1,9 @@ project(modnfm) set(modnfm_SOURCES - nfmmod.cpp + nfmmod.cpp + nfmmodbaseband.cpp + nfmmodsource.cpp nfmmodplugin.cpp nfmmodsettings.cpp nfmmodwebapiadapter.cpp @@ -9,6 +11,8 @@ set(modnfm_SOURCES set(modnfm_HEADERS nfmmod.h + nfmmodbaseband.h + nfmmodsource.h nfmmodplugin.h nfmmodsettings.h nfmmodwebapiadapter.h diff --git a/plugins/channeltx/modnfm/nfmmod.cpp b/plugins/channeltx/modnfm/nfmmod.cpp index 5b0cb0437..ebe22247a 100644 --- a/plugins/channeltx/modnfm/nfmmod.cpp +++ b/plugins/channeltx/modnfm/nfmmod.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "SWGChannelSettings.h" #include "SWGCWKeyerSettings.h" @@ -31,13 +32,13 @@ #include #include -#include "dsp/upchannelizer.h" #include "dsp/dspengine.h" #include "dsp/dspcommands.h" +#include "dsp/cwkeyer.h" #include "device/deviceapi.h" -#include "dsp/threadedbasebandsamplesource.h" #include "util/db.h" +#include "nfmmodbaseband.h" #include "nfmmod.h" MESSAGE_CLASS_DEFINITION(NFMMod::MsgConfigureNFMMod, Message) @@ -50,54 +51,25 @@ MESSAGE_CLASS_DEFINITION(NFMMod::MsgReportFileSourceStreamTiming, Message) const QString NFMMod::m_channelIdURI = "sdrangel.channeltx.modnfm"; const QString NFMMod::m_channelId = "NFMMod"; -const int NFMMod::m_levelNbSamples = 480; // every 10ms NFMMod::NFMMod(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), m_deviceAPI(deviceAPI), - m_basebandSampleRate(48000), - m_outputSampleRate(48000), - m_inputFrequencyOffset(0), - m_modPhasor(0.0f), - m_audioFifo(4800), - m_feedbackAudioFifo(48000), m_settingsMutex(QMutex::Recursive), m_fileSize(0), m_recordLength(0), - m_sampleRate(48000), - m_levelCalcCount(0), - m_peakLevel(0.0f), - m_levelSum(0.0f) + m_sampleRate(48000) { setObjectName(m_channelId); - m_audioBuffer.resize(1<<14); - m_audioBufferFill = 0; + m_thread = new QThread(this); + m_basebandSource = new NFMModBaseband(); + m_basebandSource->setInputFileStream(&m_ifstream); + m_basebandSource->moveToThread(m_thread); - m_feedbackAudioBuffer.resize(1<<14); - m_feedbackAudioBufferFill = 0; - - m_magsq = 0.0; - - DSPEngine::instance()->getAudioDeviceManager()->addAudioSource(&m_audioFifo, getInputMessageQueue()); - m_audioSampleRate = DSPEngine::instance()->getAudioDeviceManager()->getInputSampleRate(); - - DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(&m_feedbackAudioFifo, getInputMessageQueue()); - m_feedbackAudioSampleRate = DSPEngine::instance()->getAudioDeviceManager()->getOutputSampleRate(); - applyFeedbackAudioSampleRate(m_feedbackAudioSampleRate); - - m_lowpass.create(301, m_audioSampleRate, 250.0); - m_toneNco.setFreq(1000.0, m_audioSampleRate); - m_ctcssNco.setFreq(88.5, m_audioSampleRate); - m_cwKeyer.setSampleRate(m_audioSampleRate); - m_cwKeyer.reset(); - - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); applySettings(m_settings, true); - m_channelizer = new UpChannelizer(this); - m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this); - m_deviceAPI->addChannelSource(m_threadedChannelizer); + m_deviceAPI->addChannelSource(this); m_deviceAPI->addChannelSourceAPI(this); m_networkManager = new QNetworkAccessManager(); @@ -108,263 +80,43 @@ NFMMod::~NFMMod() { disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); delete m_networkManager; - DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(&m_feedbackAudioFifo); - DSPEngine::instance()->getAudioDeviceManager()->removeAudioSource(&m_audioFifo); m_deviceAPI->removeChannelSourceAPI(this); - m_deviceAPI->removeChannelSource(m_threadedChannelizer); - delete m_threadedChannelizer; - delete m_channelizer; -} - -void NFMMod::pull(Sample& sample) -{ - if (m_settings.m_channelMute) - { - sample.m_real = 0.0f; - sample.m_imag = 0.0f; - return; - } - - Complex ci; - - m_settingsMutex.lock(); - - if (m_interpolatorDistance > 1.0f) // decimate - { - modulateSample(); - - while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - else - { - if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - - m_interpolatorDistanceRemain += m_interpolatorDistance; - - ci *= m_carrierNco.nextIQ(); // shift to carrier frequency - - m_settingsMutex.unlock(); - - double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); - magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); - m_movingAverage(magsq); - m_magsq = m_movingAverage.asDouble(); - - sample.m_real = (FixReal) ci.real(); - sample.m_imag = (FixReal) ci.imag(); -} - -void NFMMod::pullAudio(int nbSamples) -{ - unsigned int nbSamplesAudio = nbSamples * ((Real) m_audioSampleRate / (Real) m_basebandSampleRate); - - if (nbSamplesAudio > m_audioBuffer.size()) - { - m_audioBuffer.resize(nbSamplesAudio); - } - - m_audioFifo.read(reinterpret_cast(&m_audioBuffer[0]), nbSamplesAudio); - m_audioBufferFill = 0; -} - -void NFMMod::modulateSample() -{ - Real t; - - pullAF(t); - - if (m_settings.m_feedbackAudioEnable) { - pushFeedback(t * m_settings.m_feedbackVolumeFactor * 16384.0f); - } - - calculateLevel(t); - m_audioBufferFill++; - - if (m_settings.m_ctcssOn) - { - m_modPhasor += (m_settings.m_fmDeviation / (float) m_audioSampleRate) * (0.85f * m_bandpass.filter(t) + 0.15f * 378.0f * m_ctcssNco.next()) * (M_PI / 378.0f); - } - else - { - // 378 = 302 * 1.25; 302 = number of filter taps (established experimentally) - m_modPhasor += (m_settings.m_fmDeviation / (float) m_audioSampleRate) * m_bandpass.filter(t) * (M_PI / 378.0f); - } - - m_modSample.real(cos(m_modPhasor) * 0.891235351562f * SDR_TX_SCALEF); // -1 dB - m_modSample.imag(sin(m_modPhasor) * 0.891235351562f * SDR_TX_SCALEF); -} - -void NFMMod::pullAF(Real& sample) -{ - switch (m_settings.m_modAFInput) - { - case NFMModSettings::NFMModInputTone: - sample = m_toneNco.next(); - break; - case NFMModSettings::NFMModInputFile: - // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw - // ffplay -f f32le -ar 48k -ac 1 f4exb_call.raw - if (m_ifstream.is_open()) - { - if (m_ifstream.eof()) - { - if (m_settings.m_playLoop) - { - m_ifstream.clear(); - m_ifstream.seekg(0, std::ios::beg); - } - } - - if (m_ifstream.eof()) - { - sample = 0.0f; - } - else - { - m_ifstream.read(reinterpret_cast(&sample), sizeof(Real)); - sample *= m_settings.m_volumeFactor; - } - } - else - { - sample = 0.0f; - } - break; - case NFMModSettings::NFMModInputAudio: - sample = ((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f) * m_settings.m_volumeFactor; - break; - case NFMModSettings::NFMModInputCWTone: - Real fadeFactor; - - if (m_cwKeyer.getSample()) - { - m_cwKeyer.getCWSmoother().getFadeSample(true, fadeFactor); - sample = m_toneNco.next() * fadeFactor; - } - else - { - if (m_cwKeyer.getCWSmoother().getFadeSample(false, fadeFactor)) - { - sample = m_toneNco.next() * fadeFactor; - } - else - { - sample = 0.0f; - m_toneNco.setPhase(0); - } - } - break; - case NFMModSettings::NFMModInputNone: - default: - sample = 0.0f; - break; - } -} - -void NFMMod::pushFeedback(Real sample) -{ - Complex c(sample, sample); - Complex ci; - - if (m_feedbackInterpolatorDistance < 1.0f) // interpolate - { - while (!m_feedbackInterpolator.interpolate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) - { - processOneSample(ci); - m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; - } - } - else // decimate - { - if (m_feedbackInterpolator.decimate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) - { - processOneSample(ci); - m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; - } - } -} - -void NFMMod::processOneSample(Complex& ci) -{ - m_feedbackAudioBuffer[m_feedbackAudioBufferFill].l = ci.real(); - m_feedbackAudioBuffer[m_feedbackAudioBufferFill].r = ci.imag(); - ++m_feedbackAudioBufferFill; - - if (m_feedbackAudioBufferFill >= m_feedbackAudioBuffer.size()) - { - uint res = m_feedbackAudioFifo.write((const quint8*)&m_feedbackAudioBuffer[0], m_feedbackAudioBufferFill); - - if (res != m_feedbackAudioBufferFill) - { - qDebug("AMDemod::pushFeedback: %u/%u audio samples written m_feedbackInterpolatorDistance: %f", - res, m_feedbackAudioBufferFill, m_feedbackInterpolatorDistance); - m_feedbackAudioFifo.clear(); - } - - m_feedbackAudioBufferFill = 0; - } -} - -void NFMMod::calculateLevel(Real& sample) -{ - if (m_levelCalcCount < m_levelNbSamples) - { - m_peakLevel = std::max(std::fabs(m_peakLevel), sample); - m_levelSum += sample * sample; - m_levelCalcCount++; - } - else - { - qreal rmsLevel = sqrt(m_levelSum / m_levelNbSamples); - //qDebug("NFMMod::calculateLevel: %f %f", rmsLevel, m_peakLevel); - emit levelChanged(rmsLevel, m_peakLevel, m_levelNbSamples); - m_peakLevel = 0.0f; - m_levelSum = 0.0f; - m_levelCalcCount = 0; - } + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; } void NFMMod::start() { - qDebug() << "NFMMod::start: m_outputSampleRate: " << m_outputSampleRate - << " m_inputFrequencyOffset: " << m_inputFrequencyOffset; - - m_audioFifo.clear(); - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); + qDebug("NFMMod::start"); + m_basebandSource->reset(); + m_thread->start(); } void NFMMod::stop() { + qDebug("NFMMod::stop"); + m_thread->exit(); + m_thread->wait(); +} + +void NFMMod::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); } bool NFMMod::handleMessage(const Message& cmd) { - if (UpChannelizer::MsgChannelizerNotification::match(cmd)) - { - UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd; - qDebug() << "NFMMod::handleMessage: UpChannelizer::MsgChannelizerNotification"; - - applyChannelSettings(notif.getBasebandSampleRate(), notif.getSampleRate(), notif.getFrequencyOffset()); - - return true; - } - else if (MsgConfigureChannelizer::match(cmd)) + if (MsgConfigureChannelizer::match(cmd)) { MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; qDebug() << "NFMMod::handleMessage: MsgConfigureChannelizer:" - << " getSampleRate: " << cfg.getSampleRate() - << " getCenterFrequency: " << cfg.getCenterFrequency(); + << " getSourceSampleRate: " << cfg.getSourceSampleRate() + << " getSourceCenterFrequency: " << cfg.getSourceCenterFrequency(); - m_channelizer->configure(m_channelizer->getInputMessageQueue(), - cfg.getSampleRate(), - cfg.getCenterFrequency()); + NFMModBaseband::MsgConfigureChannelizer *msg + = NFMModBaseband::MsgConfigureChannelizer::create(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_basebandSource->getInputMessageQueue()->push(msg); return true; } @@ -412,6 +164,41 @@ bool NFMMod::handleMessage(const Message& cmd) return true; } + else if (MsgConfigureFileSourceName::match(cmd)) + { + MsgConfigureFileSourceName& conf = (MsgConfigureFileSourceName&) cmd; + m_fileName = conf.getFileName(); + openFileStream(); + qDebug() << "NFMMod::handleMessage: MsgConfigureFileSourceName:" + << " m_fileName: " << m_fileName; + return true; + } + else if (MsgConfigureFileSourceSeek::match(cmd)) + { + MsgConfigureFileSourceSeek& conf = (MsgConfigureFileSourceSeek&) cmd; + int seekPercentage = conf.getPercentage(); + seekFileStream(seekPercentage); + qDebug() << "NFMMod::handleMessage: MsgConfigureFileSourceSeek:" + << " seekPercentage: " << seekPercentage; + + return true; + } + else if (MsgConfigureFileSourceStreamTiming::match(cmd)) + { + std::size_t samplesCount; + + if (m_ifstream.eof()) { + samplesCount = m_fileSize / sizeof(Real); + } else { + samplesCount = m_ifstream.tellg() / sizeof(Real); + } + + MsgReportFileSourceStreamTiming *report; + report = MsgReportFileSourceStreamTiming::create(samplesCount); + getMessageQueueToGUI()->push(report); + + return true; + } else if (CWKeyer::MsgConfigureCWKeyer::match(cmd)) { const CWKeyer::MsgConfigureCWKeyer& cfg = (CWKeyer::MsgConfigureCWKeyer&) cmd; @@ -422,33 +209,14 @@ bool NFMMod::handleMessage(const Message& cmd) return true; } - else if (DSPConfigureAudio::match(cmd)) - { - DSPConfigureAudio& cfg = (DSPConfigureAudio&) cmd; - uint32_t sampleRate = cfg.getSampleRate(); - DSPConfigureAudio::AudioType audioType = cfg.getAudioType(); - - qDebug() << "NFMMod::handleMessage: DSPConfigureAudio:" - << " sampleRate: " << sampleRate - << " audioType: " << audioType; - - if (audioType == DSPConfigureAudio::AudioInput) - { - if (sampleRate != m_audioSampleRate) { - applyAudioSampleRate(sampleRate); - } - } - else if (audioType == DSPConfigureAudio::AudioOutput) - { - if (sampleRate != m_audioSampleRate) { - applyFeedbackAudioSampleRate(sampleRate); - } - } - - return true; - } else if (DSPSignalNotification::match(cmd)) { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "NFMMod::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + return true; } else @@ -492,79 +260,6 @@ void NFMMod::seekFileStream(int seekPercentage) } } -void NFMMod::applyAudioSampleRate(int sampleRate) -{ - qDebug("NFMMod::applyAudioSampleRate: %d", sampleRate); - - MsgConfigureChannelizer* channelConfigMsg = MsgConfigureChannelizer::create( - sampleRate, m_settings.m_inputFrequencyOffset); - m_inputMessageQueue.push(channelConfigMsg); - - m_settingsMutex.lock(); - - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) sampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, sampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); - m_lowpass.create(301, sampleRate, 250.0); - m_bandpass.create(301, sampleRate, 300.0, m_settings.m_afBandwidth); - m_toneNco.setFreq(m_settings.m_toneFrequency, sampleRate); - m_ctcssNco.setFreq(NFMModSettings::getCTCSSFreq(m_settings.m_ctcssIndex), sampleRate); - m_cwKeyer.setSampleRate(sampleRate); - - m_settingsMutex.unlock(); - - m_audioSampleRate = sampleRate; - applyFeedbackAudioSampleRate(m_feedbackAudioSampleRate); -} - -void NFMMod::applyFeedbackAudioSampleRate(unsigned int sampleRate) -{ - qDebug("NFMMod::applyFeedbackAudioSampleRate: %u", sampleRate); - - m_settingsMutex.lock(); - - m_feedbackInterpolatorDistanceRemain = 0; - m_feedbackInterpolatorConsumed = false; - m_feedbackInterpolatorDistance = (Real) sampleRate / (Real) m_audioSampleRate; - Real cutoff = std::min(sampleRate, m_audioSampleRate) / 2.2f; - m_feedbackInterpolator.create(48, sampleRate, cutoff, 3.0); - - m_settingsMutex.unlock(); - - m_feedbackAudioSampleRate = sampleRate; -} - -void NFMMod::applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force) -{ - qDebug() << "NFMMod::applyChannelSettings:" - << " basebandSampleRate: " << basebandSampleRate - << " outputSampleRate: " << outputSampleRate - << " inputFrequencyOffset: " << inputFrequencyOffset; - - if ((inputFrequencyOffset != m_inputFrequencyOffset) || - (outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_carrierNco.setFreq(inputFrequencyOffset, outputSampleRate); - m_settingsMutex.unlock(); - } - - if ((outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_audioSampleRate / (Real) outputSampleRate; - m_interpolator.create(48, m_audioSampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); - m_settingsMutex.unlock(); - } - - m_basebandSampleRate = basebandSampleRate; - m_outputSampleRate = outputSampleRate; - m_inputFrequencyOffset = inputFrequencyOffset; -} - void NFMMod::applySettings(const NFMModSettings& settings, bool force) { qDebug() << "NFMMod::applySettings:" @@ -617,40 +312,20 @@ void NFMMod::applySettings(const NFMModSettings& settings, bool force) reverseAPIKeys.append("modAFInput"); } - if((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) - { + if((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { reverseAPIKeys.append("rfBandwidth"); - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_audioSampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, m_audioSampleRate, settings.m_rfBandwidth / 2.2, 3.0); - m_settingsMutex.unlock(); } - if ((settings.m_afBandwidth != m_settings.m_afBandwidth) || force) - { + if ((settings.m_afBandwidth != m_settings.m_afBandwidth) || force) { reverseAPIKeys.append("afBandwidth"); - m_settingsMutex.lock(); - m_lowpass.create(301, m_audioSampleRate, 250.0); - m_bandpass.create(301, m_audioSampleRate, 300.0, settings.m_afBandwidth); - m_settingsMutex.unlock(); } - if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) - { + if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) { reverseAPIKeys.append("toneFrequency"); - m_settingsMutex.lock(); - m_toneNco.setFreq(settings.m_toneFrequency, m_audioSampleRate); - m_settingsMutex.unlock(); } - if ((settings.m_ctcssIndex != m_settings.m_ctcssIndex) || force) - { + if ((settings.m_ctcssIndex != m_settings.m_ctcssIndex) || force) { reverseAPIKeys.append("ctcssIndex"); - m_settingsMutex.lock(); - m_ctcssNco.setFreq(NFMModSettings::getCTCSSFreq(settings.m_ctcssIndex), m_audioSampleRate); - m_settingsMutex.unlock(); } if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force) @@ -658,12 +333,14 @@ void NFMMod::applySettings(const NFMModSettings& settings, bool force) reverseAPIKeys.append("audioDeviceName"); AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); int audioDeviceIndex = audioDeviceManager->getInputDeviceIndex(settings.m_audioDeviceName); - audioDeviceManager->addAudioSource(&m_audioFifo, getInputMessageQueue(), audioDeviceIndex); + audioDeviceManager->addAudioSource(m_basebandSource->getAudioFifo(), getInputMessageQueue(), audioDeviceIndex); uint32_t audioSampleRate = audioDeviceManager->getInputSampleRate(audioDeviceIndex); - if (m_audioSampleRate != audioSampleRate) { + if (m_basebandSource->getAudioSampleRate() != audioSampleRate) + { reverseAPIKeys.append("audioSampleRate"); - applyAudioSampleRate(audioSampleRate); + DSPConfigureAudio *msg = new DSPConfigureAudio(audioSampleRate, DSPConfigureAudio::AudioInput); + m_basebandSource->getInputMessageQueue()->push(msg); } } @@ -672,15 +349,20 @@ void NFMMod::applySettings(const NFMModSettings& settings, bool force) reverseAPIKeys.append("feedbackAudioDeviceName"); AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); int audioDeviceIndex = audioDeviceManager->getOutputDeviceIndex(settings.m_feedbackAudioDeviceName); - audioDeviceManager->addAudioSink(&m_feedbackAudioFifo, getInputMessageQueue(), audioDeviceIndex); + audioDeviceManager->addAudioSink(m_basebandSource->getFeedbackAudioFifo(), getInputMessageQueue(), audioDeviceIndex); uint32_t audioSampleRate = audioDeviceManager->getOutputSampleRate(audioDeviceIndex); - if (m_feedbackAudioSampleRate != audioSampleRate) { + if (m_basebandSource->getFeedbackAudioSampleRate() != audioSampleRate) + { reverseAPIKeys.append("feedbackAudioSampleRate"); - applyFeedbackAudioSampleRate(audioSampleRate); + DSPConfigureAudio *msg = new DSPConfigureAudio(audioSampleRate, DSPConfigureAudio::AudioOutput); + m_basebandSource->getInputMessageQueue()->push(msg); } } + NFMModBaseband::MsgConfigureNFMModBaseband *msg = NFMModBaseband::MsgConfigureNFMModBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); + if (settings.m_useReverseAPI) { bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || @@ -729,7 +411,7 @@ int NFMMod::webapiSettingsGet( webapiFormatChannelSettings(response, m_settings); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = response.getNfmModSettings()->getCwKeyer(); - const CWKeyerSettings& cwKeyerSettings = m_cwKeyer.getSettings(); + const CWKeyerSettings& cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); CWKeyer::webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); return 200; @@ -748,11 +430,11 @@ int NFMMod::webapiSettingsPutPatch( if (channelSettingsKeys.contains("cwKeyer")) { SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = response.getNfmModSettings()->getCwKeyer(); - CWKeyerSettings cwKeyerSettings = m_cwKeyer.getSettings(); + CWKeyerSettings cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); CWKeyer::webapiSettingsPutPatch(channelSettingsKeys, cwKeyerSettings, apiCwKeyerSettings); CWKeyer::MsgConfigureCWKeyer *msgCwKeyer = CWKeyer::MsgConfigureCWKeyer::create(cwKeyerSettings, force); - m_cwKeyer.getInputMessageQueue()->push(msgCwKeyer); + m_basebandSource->getCWKeyer().getInputMessageQueue()->push(msgCwKeyer); if (m_guiMessageQueue) // forward to GUI if any { @@ -763,8 +445,8 @@ int NFMMod::webapiSettingsPutPatch( if (m_settings.m_inputFrequencyOffset != settings.m_inputFrequencyOffset) { - NFMMod::MsgConfigureChannelizer *msgChan = NFMMod::MsgConfigureChannelizer::create( - m_audioSampleRate, settings.m_inputFrequencyOffset); + NFMModBaseband::MsgConfigureChannelizer *msgChan = NFMModBaseband::MsgConfigureChannelizer::create( + m_basebandSource->getAudioSampleRate(), settings.m_inputFrequencyOffset); m_inputMessageQueue.push(msgChan); } @@ -902,8 +584,8 @@ void NFMMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& respon void NFMMod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) { response.getNfmModReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); - response.getNfmModReport()->setAudioSampleRate(m_audioSampleRate); - response.getNfmModReport()->setChannelSampleRate(m_outputSampleRate); + response.getNfmModReport()->setAudioSampleRate(m_basebandSource->getAudioSampleRate()); + response.getNfmModReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); } void NFMMod::webapiReverseSendSettings(QList& channelSettingsKeys, const NFMModSettings& settings, bool force) @@ -963,10 +645,10 @@ void NFMMod::webapiReverseSendSettings(QList& channelSettingsKeys, cons if (force) { - const CWKeyerSettings& cwKeyerSettings = m_cwKeyer.getSettings(); + const CWKeyerSettings& cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); swgNFMModSettings->setCwKeyer(new SWGSDRangel::SWGCWKeyerSettings()); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = swgNFMModSettings->getCwKeyer(); - m_cwKeyer.webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); + m_basebandSource->getCWKeyer().webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); } QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") @@ -977,13 +659,14 @@ void NFMMod::webapiReverseSendSettings(QList& channelSettingsKeys, cons m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -998,7 +681,7 @@ void NFMMod::webapiReverseSendCWSettings(const CWKeyerSettings& cwKeyerSettings) swgNFModSettings->setCwKeyer(new SWGSDRangel::SWGCWKeyerSettings()); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = swgNFModSettings->getCwKeyer(); - m_cwKeyer.webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); + m_basebandSource->getCWKeyer().webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") .arg(m_settings.m_reverseAPIAddress) @@ -1008,13 +691,14 @@ void NFMMod::webapiReverseSendCWSettings(const CWKeyerSettings& cwKeyerSettings) m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -1029,11 +713,28 @@ void NFMMod::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("NFMMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("NFMMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } +double NFMMod::getMagSq() const +{ + return m_basebandSource->getMagSq(); +} + +CWKeyer *NFMMod::getCWKeyer() +{ + return &m_basebandSource->getCWKeyer(); +} + +void NFMMod::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); +} diff --git a/plugins/channeltx/modnfm/nfmmod.h b/plugins/channeltx/modnfm/nfmmod.h index fcdc3acc3..13f8da675 100644 --- a/plugins/channeltx/modnfm/nfmmod.h +++ b/plugins/channeltx/modnfm/nfmmod.h @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2016-2019 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // @@ -27,24 +27,16 @@ #include "dsp/basebandsamplesource.h" #include "channel/channelapi.h" -#include "dsp/nco.h" -#include "dsp/ncof.h" -#include "dsp/interpolator.h" -#include "dsp/lowpass.h" -#include "dsp/bandpass.h" -#include "util/movingaverage.h" -#include "dsp/agc.h" -#include "dsp/cwkeyer.h" -#include "audio/audiofifo.h" #include "util/message.h" #include "nfmmodsettings.h" -class DeviceAPI; -class ThreadedBasebandSampleSource; -class UpChannelizer; class QNetworkAccessManager; class QNetworkReply; +class QThread; +class DeviceAPI; +class CWKeyer; +class NFMModBaseband; class NFMMod : public BasebandSampleSource, public ChannelAPI { Q_OBJECT @@ -73,26 +65,33 @@ public: { } }; + /** + * |<------ Baseband from device (before device soft interpolation) -------------------------->| + * |<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->| + * | ^-------------------------------| + * | | Source CF + * | | Source SR | + */ class MsgConfigureChannelizer : public Message { MESSAGE_CLASS_DECLARATION public: - int getSampleRate() const { return m_sampleRate; } - int getCenterFrequency() const { return m_centerFrequency; } + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } - static MsgConfigureChannelizer* create(int sampleRate, int centerFrequency) + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) { - return new MsgConfigureChannelizer(sampleRate, centerFrequency); + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); } private: - int m_sampleRate; - int m_centerFrequency; + int m_sourceSampleRate; + int m_sourceCenterFrequency; - MsgConfigureChannelizer(int sampleRate, int centerFrequency) : + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : Message(), - m_sampleRate(sampleRate), - m_centerFrequency(centerFrequency) + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) { } }; @@ -207,10 +206,9 @@ public: ~NFMMod(); virtual void destroy() { delete this; } - virtual void pull(Sample& sample); - virtual void pullAudio(int nbSamples); virtual void start(); virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); virtual bool handleMessage(const Message& cmd); virtual void getIdentifier(QString& id) { id = objectName(); } @@ -253,23 +251,13 @@ public: const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response); - double getMagSq() const { return m_magsq; } - - CWKeyer *getCWKeyer() { return &m_cwKeyer; } + double getMagSq() const; + CWKeyer *getCWKeyer(); + void setLevelMeter(QObject *levelMeter); static const QString m_channelIdURI; static const QString m_channelId; -signals: - /** - * Level changed - * \param rmsLevel RMS level in range 0.0 - 1.0 - * \param peakLevel Peak level in range 0.0 - 1.0 - * \param numSamples Number of audio samples analyzed - */ - void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); - - private: enum RateState { RSInitialFill, @@ -277,46 +265,10 @@ private: }; DeviceAPI* m_deviceAPI; - ThreadedBasebandSampleSource* m_threadedChannelizer; - UpChannelizer* m_channelizer; - - int m_basebandSampleRate; - int m_outputSampleRate; - int m_inputFrequencyOffset; + QThread *m_thread; + NFMModBaseband* m_basebandSource; NFMModSettings m_settings; - NCO m_carrierNco; - NCOF m_toneNco; - NCOF m_ctcssNco; - float m_modPhasor; //!< baseband modulator phasor - Complex m_modSample; - - Interpolator m_interpolator; - Real m_interpolatorDistance; - Real m_interpolatorDistanceRemain; - bool m_interpolatorConsumed; - - Interpolator m_feedbackInterpolator; - Real m_feedbackInterpolatorDistance; - Real m_feedbackInterpolatorDistanceRemain; - bool m_feedbackInterpolatorConsumed; - - Lowpass m_lowpass; - Bandpass m_bandpass; - - double m_magsq; - MovingAverageUtil m_movingAverage; - - quint32 m_audioSampleRate; - AudioVector m_audioBuffer; - uint m_audioBufferFill; - AudioFifo m_audioFifo; - - quint32 m_feedbackAudioSampleRate; - AudioVector m_feedbackAudioBuffer; - uint m_feedbackAudioBufferFill; - AudioFifo m_feedbackAudioFifo; - SampleVector m_sampleBuffer; QMutex m_settingsMutex; @@ -326,26 +278,10 @@ private: quint32 m_recordLength; //!< record length in seconds computed from file size int m_sampleRate; - NFMModSettings::NFMModInputAF m_afInput; - quint32 m_levelCalcCount; - Real m_peakLevel; - Real m_levelSum; - CWKeyer m_cwKeyer; - QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; - static const int m_levelNbSamples; - - void applyAudioSampleRate(int sampleRate); - void applyFeedbackAudioSampleRate(unsigned int sampleRate); - void processOneSample(Complex& ci); - void applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force = false); void applySettings(const NFMModSettings& settings, bool force = false); - void pullAF(Real& sample); - void pushFeedback(Real sample); - void calculateLevel(Real& sample); - void modulateSample(); void openFileStream(); void seekFileStream(int seekPercentage); void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); diff --git a/plugins/channeltx/modnfm/nfmmodbaseband.cpp b/plugins/channeltx/modnfm/nfmmodbaseband.cpp new file mode 100644 index 000000000..7d4e4fbb4 --- /dev/null +++ b/plugins/channeltx/modnfm/nfmmodbaseband.cpp @@ -0,0 +1,228 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "nfmmodbaseband.h" + +MESSAGE_CLASS_DEFINITION(NFMModBaseband::MsgConfigureNFMModBaseband, Message) +MESSAGE_CLASS_DEFINITION(NFMModBaseband::MsgConfigureChannelizer, Message) + +NFMModBaseband::NFMModBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpSampleChannelizer(&m_source); + + qDebug("NFMModBaseband::NFMModBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &NFMModBaseband::handleData, + Qt::QueuedConnection + ); + + DSPEngine::instance()->getAudioDeviceManager()->addAudioSource(m_source.getAudioFifo(), getInputMessageQueue()); + m_source.applyAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getInputSampleRate()); + + DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(m_source.getFeedbackAudioFifo(), getInputMessageQueue()); + m_source.applyFeedbackAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getOutputSampleRate()); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +NFMModBaseband::~NFMModBaseband() +{ + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(m_source.getFeedbackAudioFifo()); + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSource(m_source.getAudioFifo()); + delete m_channelizer; +} + +void NFMModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void NFMModBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void NFMModBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + Real rmsLevel, peakLevel, numSamples; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); +} + +void NFMModBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void NFMModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool NFMModBaseband::handleMessage(const Message& cmd) +{ + if (DSPConfigureAudio::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPConfigureAudio& cfg = (DSPConfigureAudio&) cmd; + uint32_t sampleRate = cfg.getSampleRate(); + DSPConfigureAudio::AudioType audioType = cfg.getAudioType(); + + qDebug() << "NFMModBaseband::handleMessage: DSPConfigureAudio:" + << " sampleRate: " << sampleRate + << " audioType: " << audioType; + + if (audioType == DSPConfigureAudio::AudioInput) + { + if (sampleRate != m_source.getAudioSampleRate()) { + m_source.applyAudioSampleRate(sampleRate); + } + } + else if (audioType == DSPConfigureAudio::AudioOutput) + { + if (sampleRate != m_source.getFeedbackAudioSampleRate()) { + m_source.applyFeedbackAudioSampleRate(sampleRate); + } + } + + return true; + } + else if (MsgConfigureNFMModBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureNFMModBaseband& cfg = (MsgConfigureNFMModBaseband&) cmd; + qDebug() << "NFMModBaseband::handleMessage: MsgConfigureNFMModBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureChannelizer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; + qDebug() << "NFMModBaseband::handleMessage: MsgConfigureChannelizer" + << "(requested) sourceSampleRate: " << cfg.getSourceSampleRate() + << "(requested) sourceCenterFrequency: " << cfg.getSourceCenterFrequency(); + m_channelizer->setChannelization(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "NFMModBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (CWKeyer::MsgConfigureCWKeyer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + const CWKeyer::MsgConfigureCWKeyer& cfg = (CWKeyer::MsgConfigureCWKeyer&) cmd; + CWKeyer::MsgConfigureCWKeyer *notif = new CWKeyer::MsgConfigureCWKeyer(cfg); + CWKeyer& cwKeyer = m_source.getCWKeyer(); + cwKeyer.getInputMessageQueue()->push(notif); + + return true; + } + else + { + return false; + } +} + +void NFMModBaseband::applySettings(const NFMModSettings& settings, bool force) +{ + m_source.applySettings(settings, force); + m_settings = settings; +} + +int NFMModBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} \ No newline at end of file diff --git a/plugins/channeltx/modnfm/nfmmodbaseband.h b/plugins/channeltx/modnfm/nfmmodbaseband.h new file mode 100644 index 000000000..eebbac663 --- /dev/null +++ b/plugins/channeltx/modnfm/nfmmodbaseband.h @@ -0,0 +1,123 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_NFMMODBASEBAND_H +#define INCLUDE_NFMMODBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "nfmmodsource.h" + +class UpSampleChannelizer; + +class NFMModBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureNFMModBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const NFMModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureNFMModBaseband* create(const NFMModSettings& settings, bool force) + { + return new MsgConfigureNFMModBaseband(settings, force); + } + + private: + NFMModSettings m_settings; + bool m_force; + + MsgConfigureNFMModBaseband(const NFMModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureChannelizer : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } + + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) + { + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); + } + + private: + int m_sourceSampleRate; + int m_sourceCenterFrequency; + + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : + Message(), + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) + { } + }; + + NFMModBaseband(); + ~NFMModBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + CWKeyer& getCWKeyer() { return m_source.getCWKeyer(); } + double getMagSq() const { return m_source.getMagSq(); } + unsigned int getAudioSampleRate() const { return m_source.getAudioSampleRate(); } + unsigned int getFeedbackAudioSampleRate() const { return m_source.getFeedbackAudioSampleRate(); } + int getChannelSampleRate() const; + void setInputFileStream(std::ifstream *ifstream) { m_source.setInputFileStream(ifstream); } + AudioFifo *getAudioFifo() { return m_source.getAudioFifo(); } + AudioFifo *getFeedbackAudioFifo() { return m_source.getFeedbackAudioFifo(); } + +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + SampleSourceFifo m_sampleFifo; + UpSampleChannelizer *m_channelizer; + NFMModSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + NFMModSettings m_settings; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const NFMModSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + + +#endif // INCLUDE_NFMMODBASEBAND_H diff --git a/plugins/channeltx/modnfm/nfmmodgui.cpp b/plugins/channeltx/modnfm/nfmmodgui.cpp index 4a2d988a6..b36345ae0 100644 --- a/plugins/channeltx/modnfm/nfmmodgui.cpp +++ b/plugins/channeltx/modnfm/nfmmodgui.cpp @@ -26,6 +26,7 @@ #include "util/simpleserializer.h" #include "util/db.h" #include "dsp/dspengine.h" +#include "dsp/cwkeyer.h" #include "gui/crightclickenabler.h" #include "gui/audioselectdialog.h" #include "gui/basicchannelsettingsdialog.h" @@ -410,7 +411,7 @@ NFMModGUI::NFMModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSam ui->cwKeyerGUI->setCWKeyer(m_nfmMod->getCWKeyer()); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); - connect(m_nfmMod, SIGNAL(levelChanged(qreal, qreal, int)), ui->volumeMeter, SLOT(levelChanged(qreal, qreal, int))); + m_nfmMod->setLevelMeter(ui->volumeMeter); m_settings.setChannelMarker(&m_channelMarker); m_settings.setCWKeyerGUI(ui->cwKeyerGUI); diff --git a/plugins/channeltx/modnfm/nfmmodplugin.cpp b/plugins/channeltx/modnfm/nfmmodplugin.cpp index 7ae2d4666..77539e87c 100644 --- a/plugins/channeltx/modnfm/nfmmodplugin.cpp +++ b/plugins/channeltx/modnfm/nfmmodplugin.cpp @@ -27,7 +27,7 @@ const PluginDescriptor NFMModPlugin::m_pluginDescriptor = { QString("NFM Modulator"), - QString("4.11.6"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/channeltx/modnfm/nfmmodsettings.cpp b/plugins/channeltx/modnfm/nfmmodsettings.cpp index 8050acd75..8b1f0b7d5 100644 --- a/plugins/channeltx/modnfm/nfmmodsettings.cpp +++ b/plugins/channeltx/modnfm/nfmmodsettings.cpp @@ -49,7 +49,7 @@ void NFMModSettings::resetToDefaults() m_afBandwidth = 3000; m_inputFrequencyOffset = 0; m_rfBandwidth = 12500.0f; - m_fmDeviation = 5000.0f; + m_fmDeviation = 3000.0f; m_toneFrequency = 1000.0f; m_volumeFactor = 1.0f; m_channelMute = false; @@ -129,7 +129,7 @@ bool NFMModSettings::deserialize(const QByteArray& data) m_inputFrequencyOffset = tmp; d.readReal(2, &m_rfBandwidth, 12500.0); d.readReal(3, &m_afBandwidth, 1000.0); - d.readReal(4, &m_fmDeviation, 5000.0); + d.readReal(4, &m_fmDeviation, 3000.0); d.readU32(5, &m_rgbColor); d.readReal(6, &m_toneFrequency, 1000.0); d.readReal(7, &m_volumeFactor, 1.0); diff --git a/plugins/channeltx/modnfm/nfmmodsource.cpp b/plugins/channeltx/modnfm/nfmmodsource.cpp new file mode 100644 index 000000000..67f4bba69 --- /dev/null +++ b/plugins/channeltx/modnfm/nfmmodsource.cpp @@ -0,0 +1,359 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "nfmmodsource.h" + +const int NFMModSource::m_levelNbSamples = 480; // every 10ms +const float NFMModSource::m_preemphasis = 120.0e-6; // 120us + +NFMModSource::NFMModSource() : + m_channelSampleRate(48000), + m_channelFrequencyOffset(0), + m_modPhasor(0.0f), + m_audioFifo(4800), + m_feedbackAudioFifo(48000), + m_levelCalcCount(0), + m_peakLevel(0.0f), + m_levelSum(0.0f), + m_ifstream(nullptr), + m_preemphasisFilter(m_preemphasis*48000), + m_audioSampleRate(48000) +{ + m_audioBuffer.resize(1<<14); + m_audioBufferFill = 0; + + m_feedbackAudioBuffer.resize(1<<14); + m_feedbackAudioBufferFill = 0; + + m_magsq = 0.0; + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +NFMModSource::~NFMModSource() +{ +} + +void NFMModSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void NFMModSource::pullOne(Sample& sample) +{ + if (m_settings.m_channelMute) + { + sample.m_real = 0.0f; + sample.m_imag = 0.0f; + return; + } + + Complex ci; + + if (m_interpolatorDistance > 1.0f) // decimate + { + modulateSample(); + + while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + else + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + + m_interpolatorDistanceRemain += m_interpolatorDistance; + + ci *= m_carrierNco.nextIQ(); // shift to carrier frequency + + double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); + magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + + sample.m_real = (FixReal) ci.real(); + sample.m_imag = (FixReal) ci.imag(); +} + +void NFMModSource::prefetch(unsigned int nbSamples) +{ + unsigned int nbSamplesAudio = nbSamples * ((Real) m_audioSampleRate / (Real) m_channelSampleRate); + pullAudio(nbSamplesAudio); +} + +void NFMModSource::pullAudio(unsigned int nbSamplesAudio) +{ + if (nbSamplesAudio > m_audioBuffer.size()) + { + m_audioBuffer.resize(nbSamplesAudio); + } + + m_audioFifo.read(reinterpret_cast(&m_audioBuffer[0]), nbSamplesAudio); + m_audioBufferFill = 0; +} + +void NFMModSource::modulateSample() +{ + Real t0, t; + + pullAF(t0); + m_preemphasisFilter.process(t0, t); + + if (m_settings.m_feedbackAudioEnable) { + pushFeedback(t * m_settings.m_feedbackVolumeFactor * 16384.0f); + } + + calculateLevel(t); + m_audioBufferFill++; + + if (m_settings.m_ctcssOn) + { + m_modPhasor += (m_settings.m_fmDeviation / (float) m_audioSampleRate) * (0.85f * m_bandpass.filter(t) + 0.15f * 189.0f * m_ctcssNco.next()) * (M_PI / 189.0f); + } + else + { + // 378 = 302 * 1.25; 302 = number of filter taps (established experimentally) and 189 = 378/2 for 2*PI + m_modPhasor += (m_settings.m_fmDeviation / (float) m_audioSampleRate) * m_bandpass.filter(t) * (M_PI / 189.0f); + } + + // limit phasor range to ]-pi,pi] + if (m_modPhasor > M_PI) { + m_modPhasor -= (2.0f * M_PI); + } + + m_modSample.real(cos(m_modPhasor) * 0.891235351562f * SDR_TX_SCALEF); // -1 dB + m_modSample.imag(sin(m_modPhasor) * 0.891235351562f * SDR_TX_SCALEF); +} + +void NFMModSource::pullAF(Real& sample) +{ + switch (m_settings.m_modAFInput) + { + case NFMModSettings::NFMModInputTone: + sample = m_toneNco.next(); + break; + case NFMModSettings::NFMModInputFile: + // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw + // ffplay -f f32le -ar 48k -ac 1 f4exb_call.raw + if (m_ifstream && m_ifstream->is_open()) + { + if (m_ifstream->eof()) + { + if (m_settings.m_playLoop) + { + m_ifstream->clear(); + m_ifstream->seekg(0, std::ios::beg); + } + } + + if (m_ifstream->eof()) + { + sample = 0.0f; + } + else + { + m_ifstream->read(reinterpret_cast(&sample), sizeof(Real)); + sample *= m_settings.m_volumeFactor; + } + } + else + { + sample = 0.0f; + } + break; + case NFMModSettings::NFMModInputAudio: + sample = ((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f) * m_settings.m_volumeFactor; + break; + case NFMModSettings::NFMModInputCWTone: + Real fadeFactor; + + if (m_cwKeyer.getSample()) + { + m_cwKeyer.getCWSmoother().getFadeSample(true, fadeFactor); + sample = m_toneNco.next() * fadeFactor; + } + else + { + if (m_cwKeyer.getCWSmoother().getFadeSample(false, fadeFactor)) + { + sample = m_toneNco.next() * fadeFactor; + } + else + { + sample = 0.0f; + m_toneNco.setPhase(0); + } + } + break; + case NFMModSettings::NFMModInputNone: + default: + sample = 0.0f; + break; + } +} + +void NFMModSource::pushFeedback(Real sample) +{ + Complex c(sample, sample); + Complex ci; + + if (m_feedbackInterpolatorDistance < 1.0f) // interpolate + { + while (!m_feedbackInterpolator.interpolate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; + } + } + else // decimate + { + if (m_feedbackInterpolator.decimate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; + } + } +} + +void NFMModSource::processOneSample(Complex& ci) +{ + m_feedbackAudioBuffer[m_feedbackAudioBufferFill].l = ci.real(); + m_feedbackAudioBuffer[m_feedbackAudioBufferFill].r = ci.imag(); + ++m_feedbackAudioBufferFill; + + if (m_feedbackAudioBufferFill >= m_feedbackAudioBuffer.size()) + { + uint res = m_feedbackAudioFifo.write((const quint8*)&m_feedbackAudioBuffer[0], m_feedbackAudioBufferFill); + + if (res != m_feedbackAudioBufferFill) + { + qDebug("NFMModSource::pushFeedback: %u/%u audio samples written m_feedbackInterpolatorDistance: %f", + res, m_feedbackAudioBufferFill, m_feedbackInterpolatorDistance); + m_feedbackAudioFifo.clear(); + } + + m_feedbackAudioBufferFill = 0; + } +} + +void NFMModSource::calculateLevel(Real& sample) +{ + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), sample); + m_levelSum += sample * sample; + m_levelCalcCount++; + } + else + { + qreal rmsLevel = sqrt(m_levelSum / m_levelNbSamples); + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void NFMModSource::applyAudioSampleRate(unsigned int sampleRate) +{ + qDebug("NFMModSource::applyAudioSampleRate: %u", sampleRate); + + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) sampleRate / (Real) m_channelSampleRate; + m_interpolator.create(48, sampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); + m_lowpass.create(301, sampleRate, 250.0); + m_bandpass.create(301, sampleRate, 300.0, m_settings.m_afBandwidth); + m_toneNco.setFreq(m_settings.m_toneFrequency, sampleRate); + m_ctcssNco.setFreq(NFMModSettings::getCTCSSFreq(m_settings.m_ctcssIndex), sampleRate); + m_cwKeyer.setSampleRate(sampleRate); + m_cwKeyer.reset(); + m_preemphasisFilter.configure(m_preemphasis*sampleRate); + m_audioSampleRate = sampleRate; + applyFeedbackAudioSampleRate(m_feedbackAudioSampleRate); +} + +void NFMModSource::applyFeedbackAudioSampleRate(unsigned int sampleRate) +{ + qDebug("NFMModSource::applyFeedbackAudioSampleRate: %u", sampleRate); + + m_feedbackInterpolatorDistanceRemain = 0; + m_feedbackInterpolatorConsumed = false; + m_feedbackInterpolatorDistance = (Real) sampleRate / (Real) m_audioSampleRate; + Real cutoff = std::min(sampleRate, m_audioSampleRate) / 2.2f; + m_feedbackInterpolator.create(48, sampleRate, cutoff, 3.0); + m_feedbackAudioSampleRate = sampleRate; +} + +void NFMModSource::applySettings(const NFMModSettings& settings, bool force) +{ + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) + || (settings.m_afBandwidth != m_settings.m_afBandwidth) || force) + { + m_settings.m_rfBandwidth = settings.m_rfBandwidth; + m_settings.m_afBandwidth = settings.m_afBandwidth; + applyAudioSampleRate(m_audioSampleRate); + } + + if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) { + m_toneNco.setFreq(settings.m_toneFrequency, m_audioSampleRate); + } + + if ((settings.m_ctcssIndex != m_settings.m_ctcssIndex) || force) { + m_ctcssNco.setFreq(NFMModSettings::getCTCSSFreq(settings.m_ctcssIndex), m_audioSampleRate); + } + + m_settings = settings; +} + +void NFMModSource::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "NFMModSource::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((channelFrequencyOffset != m_channelFrequencyOffset) + || (channelSampleRate != m_channelSampleRate) || force) + { + m_carrierNco.setFreq(channelFrequencyOffset, channelSampleRate); + } + + if ((channelSampleRate != m_channelSampleRate) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_audioSampleRate / (Real) channelSampleRate; + m_interpolator.create(48, m_audioSampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} \ No newline at end of file diff --git a/plugins/channeltx/modnfm/nfmmodsource.h b/plugins/channeltx/modnfm/nfmmodsource.h new file mode 100644 index 000000000..fa5a252d5 --- /dev/null +++ b/plugins/channeltx/modnfm/nfmmodsource.h @@ -0,0 +1,127 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_NFMMODSOURCE_H +#define INCLUDE_NFMMODSOURCE_H + +#include + +#include +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/nco.h" +#include "dsp/ncof.h" +#include "dsp/interpolator.h" +#include "dsp/lowpass.h" +#include "dsp/bandpass.h" +#include "dsp/filterrc.h" +#include "util/movingaverage.h" +#include "dsp/cwkeyer.h" +#include "audio/audiofifo.h" + +#include "nfmmodsettings.h" + +class NFMModSource : public ChannelSampleSource +{ +public: + NFMModSource(); + virtual ~NFMModSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples); + + void setInputFileStream(std::ifstream *ifstream) { m_ifstream = ifstream; } + AudioFifo *getAudioFifo() { return &m_audioFifo; } + AudioFifo *getFeedbackAudioFifo() { return &m_feedbackAudioFifo; } + void applyAudioSampleRate(unsigned int sampleRate); + void applyFeedbackAudioSampleRate(unsigned int sampleRate); + unsigned int getAudioSampleRate() const { return m_audioSampleRate; } + unsigned int getFeedbackAudioSampleRate() const { return m_feedbackAudioSampleRate; } + CWKeyer& getCWKeyer() { return m_cwKeyer; } + double getMagSq() const { return m_magsq; } + void getLevels(Real& rmsLevel, Real& peakLevel, Real& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevel; + numSamples = m_levelNbSamples; + } + void applySettings(const NFMModSettings& settings, bool force = false); + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + +private: + int m_channelSampleRate; + int m_channelFrequencyOffset; + NFMModSettings m_settings; + + NCO m_carrierNco; + NCOF m_toneNco; + NCOF m_ctcssNco; + float m_modPhasor; //!< baseband modulator phasor + Complex m_modSample; + + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + bool m_interpolatorConsumed; + + Interpolator m_feedbackInterpolator; + Real m_feedbackInterpolatorDistance; + Real m_feedbackInterpolatorDistanceRemain; + bool m_feedbackInterpolatorConsumed; + + Lowpass m_lowpass; + Bandpass m_bandpass; + HighPassFilterRC m_preemphasisFilter; + + double m_magsq; + MovingAverageUtil m_movingAverage; + + quint32 m_audioSampleRate; + AudioVector m_audioBuffer; + uint m_audioBufferFill; + AudioFifo m_audioFifo; + + quint32 m_feedbackAudioSampleRate; + AudioVector m_feedbackAudioBuffer; + uint m_feedbackAudioBufferFill; + AudioFifo m_feedbackAudioFifo; + + quint32 m_levelCalcCount; + Real m_rmsLevel; + Real m_peakLevelOut; + Real m_peakLevel; + Real m_levelSum; + + std::ifstream *m_ifstream; + CWKeyer m_cwKeyer; + + static const int m_levelNbSamples; + static const float m_preemphasis; + + void processOneSample(Complex& ci); + void pullAF(Real& sample); + void pullAudio(unsigned int nbSamples); + void pushFeedback(Real sample); + void calculateLevel(Real& sample); + void modulateSample(); +}; + + + +#endif // INCLUDE_NFMMODSOURCE_H diff --git a/plugins/channeltx/modnfm/nfmmodwebapiadapter.cpp b/plugins/channeltx/modnfm/nfmmodwebapiadapter.cpp index f2b303fc8..646d21f52 100644 --- a/plugins/channeltx/modnfm/nfmmodwebapiadapter.cpp +++ b/plugins/channeltx/modnfm/nfmmodwebapiadapter.cpp @@ -16,6 +16,7 @@ /////////////////////////////////////////////////////////////////////////////////// #include "SWGChannelSettings.h" +#include "dsp/cwkeyer.h" #include "nfmmod.h" #include "nfmmodwebapiadapter.h" diff --git a/plugins/channeltx/modssb/CMakeLists.txt b/plugins/channeltx/modssb/CMakeLists.txt index a010be184..47ef58163 100644 --- a/plugins/channeltx/modssb/CMakeLists.txt +++ b/plugins/channeltx/modssb/CMakeLists.txt @@ -2,6 +2,8 @@ project(modssb) set(modssb_SOURCES ssbmod.cpp + ssbmodbaseband.cpp + ssbmodsource.cpp ssbmodplugin.cpp ssbmodsettings.cpp ssbmodwebapiadapter.cpp @@ -9,6 +11,8 @@ set(modssb_SOURCES set(modssb_HEADERS ssbmod.h + ssbmodbaseband.h + ssbmodsource.h ssbmodplugin.h ssbmodsettings.h ssbmodwebapiadapter.h diff --git a/plugins/channeltx/modssb/ssbmod.cpp b/plugins/channeltx/modssb/ssbmod.cpp index babe750f1..43b1ac245 100644 --- a/plugins/channeltx/modssb/ssbmod.cpp +++ b/plugins/channeltx/modssb/ssbmod.cpp @@ -15,14 +15,13 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// -#include "ssbmod.h" - #include #include #include #include #include #include +#include #include #include @@ -32,13 +31,15 @@ #include "SWGChannelReport.h" #include "SWGSSBModReport.h" -#include "dsp/upchannelizer.h" #include "dsp/dspengine.h" -#include "dsp/threadedbasebandsamplesource.h" #include "dsp/dspcommands.h" +#include "dsp/cwkeyer.h" #include "device/deviceapi.h" #include "util/db.h" +#include "ssbmodbaseband.h" +#include "ssbmod.h" + MESSAGE_CLASS_DEFINITION(SSBMod::MsgConfigureSSBMod, Message) MESSAGE_CLASS_DEFINITION(SSBMod::MsgConfigureChannelizer, Message) MESSAGE_CLASS_DEFINITION(SSBMod::MsgConfigureFileSourceName, Message) @@ -49,84 +50,25 @@ MESSAGE_CLASS_DEFINITION(SSBMod::MsgReportFileSourceStreamTiming, Message) const QString SSBMod::m_channelIdURI = "sdrangel.channeltx.modssb"; const QString SSBMod::m_channelId = "SSBMod"; -const int SSBMod::m_levelNbSamples = 480; // every 10ms -const int SSBMod::m_ssbFftLen = 1024; SSBMod::SSBMod(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), m_deviceAPI(deviceAPI), - m_basebandSampleRate(48000), - m_outputSampleRate(48000), - m_inputFrequencyOffset(0), - m_SSBFilter(0), - m_DSBFilter(0), - m_SSBFilterBuffer(0), - m_DSBFilterBuffer(0), - m_SSBFilterBufferIndex(0), - m_DSBFilterBufferIndex(0), - m_sampleSink(0), - m_audioFifo(4800), - m_feedbackAudioFifo(48000), m_settingsMutex(QMutex::Recursive), m_fileSize(0), m_recordLength(0), - m_sampleRate(48000), - m_levelCalcCount(0), - m_peakLevel(0.0f), - m_levelSum(0.0f), - m_agcStepLength(2400) + m_sampleRate(48000) { setObjectName(m_channelId); - DSPEngine::instance()->getAudioDeviceManager()->addAudioSource(&m_audioFifo, getInputMessageQueue()); - m_audioSampleRate = DSPEngine::instance()->getAudioDeviceManager()->getInputSampleRate(); + m_thread = new QThread(this); + m_basebandSource = new SSBModBaseband(); + m_basebandSource->setInputFileStream(&m_ifstream); + m_basebandSource->moveToThread(m_thread); - DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(&m_feedbackAudioFifo, getInputMessageQueue()); - m_feedbackAudioSampleRate = DSPEngine::instance()->getAudioDeviceManager()->getOutputSampleRate(); - applyFeedbackAudioSampleRate(m_feedbackAudioSampleRate); - - m_SSBFilter = new fftfilt(m_settings.m_lowCutoff / m_audioSampleRate, m_settings.m_bandwidth / m_audioSampleRate, m_ssbFftLen); - m_DSBFilter = new fftfilt((2.0f * m_settings.m_bandwidth) / m_audioSampleRate, 2 * m_ssbFftLen); - m_SSBFilterBuffer = new Complex[m_ssbFftLen>>1]; // filter returns data exactly half of its size - m_DSBFilterBuffer = new Complex[m_ssbFftLen]; - std::fill(m_SSBFilterBuffer, m_SSBFilterBuffer+(m_ssbFftLen>>1), Complex{0,0}); - std::fill(m_DSBFilterBuffer, m_DSBFilterBuffer+m_ssbFftLen, Complex{0,0}); -// memset(m_SSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen>>1)); -// memset(m_DSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen)); - - m_audioBuffer.resize(1<<14); - m_audioBufferFill = 0; - - m_feedbackAudioBuffer.resize(1<<14); - m_feedbackAudioBufferFill = 0; - - m_sum.real(0.0f); - m_sum.imag(0.0f); - m_undersampleCount = 0; - m_sumCount = 0; - - m_magsq = 0.0; - - m_toneNco.setFreq(1000.0, m_audioSampleRate); - m_cwKeyer.setSampleRate(48000); - m_cwKeyer.reset(); - - m_audioCompressor.initSimple( - m_audioSampleRate, - 50, // pregain (dB) - -30, // threshold (dB) - 20, // knee (dB) - 12, // ratio (dB) - 0.003, // attack (s) - 0.25 // release (s) - ); - - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); applySettings(m_settings, true); - m_channelizer = new UpChannelizer(this); - m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this); - m_deviceAPI->addChannelSource(m_threadedChannelizer); + m_deviceAPI->addChannelSource(this); m_deviceAPI->addChannelSourceAPI(this); m_networkManager = new QNetworkAccessManager(); @@ -137,482 +79,50 @@ SSBMod::~SSBMod() { disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); delete m_networkManager; - - DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(&m_feedbackAudioFifo); - DSPEngine::instance()->getAudioDeviceManager()->removeAudioSource(&m_audioFifo); - m_deviceAPI->removeChannelSourceAPI(this); - m_deviceAPI->removeChannelSource(m_threadedChannelizer); - delete m_threadedChannelizer; - delete m_channelizer; - - delete m_SSBFilter; - delete m_DSBFilter; - delete[] m_SSBFilterBuffer; - delete[] m_DSBFilterBuffer; -} - -void SSBMod::pull(Sample& sample) -{ - Complex ci; - - m_settingsMutex.lock(); - - if (m_interpolatorDistance > 1.0f) // decimate - { - modulateSample(); - - while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - else - { - if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - - m_interpolatorDistanceRemain += m_interpolatorDistance; - - ci *= m_carrierNco.nextIQ(); // shift to carrier frequency - ci *= 0.891235351562f * SDR_TX_SCALEF; //scaling at -1 dB to account for possible filter overshoot - - m_settingsMutex.unlock(); - - double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); - magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); - m_movingAverage(magsq); - m_magsq = m_movingAverage.asDouble(); - - sample.m_real = (FixReal) ci.real(); - sample.m_imag = (FixReal) ci.imag(); -} - -void SSBMod::pullAudio(int nbSamples) -{ - unsigned int nbSamplesAudio = nbSamples * ((Real) m_audioSampleRate / (Real) m_basebandSampleRate); - - if (nbSamplesAudio > m_audioBuffer.size()) - { - m_audioBuffer.resize(nbSamplesAudio); - } - - m_audioFifo.read(reinterpret_cast(&m_audioBuffer[0]), nbSamplesAudio); - m_audioBufferFill = 0; -} - -void SSBMod::modulateSample() -{ - pullAF(m_modSample); - - if (m_settings.m_feedbackAudioEnable) { - pushFeedback(m_modSample * m_settings.m_feedbackVolumeFactor * 16384.0f); - } - - calculateLevel(m_modSample); - m_audioBufferFill++; -} - -void SSBMod::pullAF(Complex& sample) -{ - if (m_settings.m_audioMute) - { - sample.real(0.0f); - sample.imag(0.0f); - return; - } - - Complex ci; - fftfilt::cmplx *filtered; - int n_out = 0; - - int decim = 1<<(m_settings.m_spanLog2 - 1); - unsigned char decim_mask = decim - 1; // counter LSB bit mask for decimation by 2^(m_scaleLog2 - 1) - - switch (m_settings.m_modAFInput) - { - case SSBModSettings::SSBModInputTone: - if (m_settings.m_dsb) - { - Real t = m_toneNco.next()/1.25; - sample.real(t); - sample.imag(t); - } - else - { - if (m_settings.m_usb) { - sample = m_toneNco.nextIQ(); - } else { - sample = m_toneNco.nextQI(); - } - } - break; - case SSBModSettings::SSBModInputFile: - // Monaural (mono): - // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw - // ffplay -f f32le -ar 48k -ac 1 f4exb_call.raw - // Binaural (stereo): - // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw - // ffplay -f f32le -ar 48k -ac 2 f4exb_call.raw - if (m_ifstream.is_open()) - { - if (m_ifstream.eof()) - { - if (m_settings.m_playLoop) - { - m_ifstream.clear(); - m_ifstream.seekg(0, std::ios::beg); - } - } - - if (m_ifstream.eof()) - { - ci.real(0.0f); - ci.imag(0.0f); - } - else - { - if (m_settings.m_audioBinaural) - { - Complex c; - m_ifstream.read(reinterpret_cast(&c), sizeof(Complex)); - - if (m_settings.m_audioFlipChannels) - { - ci.real(c.imag() * m_settings.m_volumeFactor); - ci.imag(c.real() * m_settings.m_volumeFactor); - } - else - { - ci = c * m_settings.m_volumeFactor; - } - } - else - { - Real real; - m_ifstream.read(reinterpret_cast(&real), sizeof(Real)); - - if (m_settings.m_agc) - { - real = m_audioCompressor.compress(real); - ci.real(real); - ci.imag(0.0f); - ci *= m_settings.m_volumeFactor; - } - else - { - ci.real(real * m_settings.m_volumeFactor); - ci.imag(0.0f); - } - } - } - } - else - { - ci.real(0.0f); - ci.imag(0.0f); - } - break; - case SSBModSettings::SSBModInputAudio: - if (m_settings.m_audioBinaural) - { - if (m_settings.m_audioFlipChannels) - { - ci.real((m_audioBuffer[m_audioBufferFill].r / SDR_TX_SCALEF) * m_settings.m_volumeFactor); - ci.imag((m_audioBuffer[m_audioBufferFill].l / SDR_TX_SCALEF) * m_settings.m_volumeFactor); - } - else - { - ci.real((m_audioBuffer[m_audioBufferFill].l / SDR_TX_SCALEF) * m_settings.m_volumeFactor); - ci.imag((m_audioBuffer[m_audioBufferFill].r / SDR_TX_SCALEF) * m_settings.m_volumeFactor); - } - } - else - { - if (m_settings.m_agc) - { - ci.real(((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f)); - ci.real(m_audioCompressor.compress(ci.real())); - ci.imag(0.0f); - ci *= m_settings.m_volumeFactor; - } - else - { - ci.real(((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f) * m_settings.m_volumeFactor); - ci.imag(0.0f); - } - } - - break; - case SSBModSettings::SSBModInputCWTone: - Real fadeFactor; - - if (m_cwKeyer.getSample()) - { - m_cwKeyer.getCWSmoother().getFadeSample(true, fadeFactor); - - if (m_settings.m_dsb) - { - Real t = m_toneNco.next() * fadeFactor; - sample.real(t); - sample.imag(t); - } - else - { - if (m_settings.m_usb) { - sample = m_toneNco.nextIQ() * fadeFactor; - } else { - sample = m_toneNco.nextQI() * fadeFactor; - } - } - } - else - { - if (m_cwKeyer.getCWSmoother().getFadeSample(false, fadeFactor)) - { - if (m_settings.m_dsb) - { - Real t = (m_toneNco.next() * fadeFactor)/1.25; - sample.real(t); - sample.imag(t); - } - else - { - if (m_settings.m_usb) { - sample = m_toneNco.nextIQ() * fadeFactor; - } else { - sample = m_toneNco.nextQI() * fadeFactor; - } - } - } - else - { - sample.real(0.0f); - sample.imag(0.0f); - m_toneNco.setPhase(0); - } - } - - break; - case SSBModSettings::SSBModInputNone: - default: - sample.real(0.0f); - sample.imag(0.0f); - break; - } - - if ((m_settings.m_modAFInput == SSBModSettings::SSBModInputFile) - || (m_settings.m_modAFInput == SSBModSettings::SSBModInputAudio)) // real audio - { - if (m_settings.m_dsb) - { - n_out = m_DSBFilter->runDSB(ci, &filtered); - - if (n_out > 0) - { - memcpy((void *) m_DSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); - m_DSBFilterBufferIndex = 0; - } - - sample = m_DSBFilterBuffer[m_DSBFilterBufferIndex]; - m_DSBFilterBufferIndex++; - } - else - { - n_out = m_SSBFilter->runSSB(ci, &filtered, m_settings.m_usb); - - if (n_out > 0) - { - memcpy((void *) m_SSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); - m_SSBFilterBufferIndex = 0; - } - - sample = m_SSBFilterBuffer[m_SSBFilterBufferIndex]; - m_SSBFilterBufferIndex++; - } - - if (n_out > 0) - { - for (int i = 0; i < n_out; i++) - { - // Downsample by 2^(m_scaleLog2 - 1) for SSB band spectrum display - // smart decimation with bit gain using float arithmetic (23 bits significand) - - m_sum += filtered[i]; - - if (!(m_undersampleCount++ & decim_mask)) - { - Real avgr = (m_sum.real() / decim) * 0.891235351562f * SDR_TX_SCALEF; //scaling at -1 dB to account for possible filter overshoot - Real avgi = (m_sum.imag() / decim) * 0.891235351562f * SDR_TX_SCALEF; - - if (!m_settings.m_dsb & !m_settings.m_usb) - { // invert spectrum for LSB - m_sampleBuffer.push_back(Sample(avgi, avgr)); - } - else - { - m_sampleBuffer.push_back(Sample(avgr, avgi)); - } - - m_sum.real(0.0); - m_sum.imag(0.0); - } - } - } - } // Real audio - else if ((m_settings.m_modAFInput == SSBModSettings::SSBModInputTone) - || (m_settings.m_modAFInput == SSBModSettings::SSBModInputCWTone)) // tone - { - m_sum += sample; - - if (!(m_undersampleCount++ & decim_mask)) - { - Real avgr = (m_sum.real() / decim) * 0.891235351562f * SDR_TX_SCALEF; //scaling at -1 dB to account for possible filter overshoot - Real avgi = (m_sum.imag() / decim) * 0.891235351562f * SDR_TX_SCALEF; - - if (!m_settings.m_dsb & !m_settings.m_usb) - { // invert spectrum for LSB - m_sampleBuffer.push_back(Sample(avgi, avgr)); - } - else - { - m_sampleBuffer.push_back(Sample(avgr, avgi)); - } - - m_sum.real(0.0); - m_sum.imag(0.0); - } - - if (m_sumCount < (m_settings.m_dsb ? m_ssbFftLen : m_ssbFftLen>>1)) - { - n_out = 0; - m_sumCount++; - } - else - { - n_out = m_sumCount; - m_sumCount = 0; - } - } - - if (n_out > 0) - { - if (m_sampleSink != 0) - { - m_sampleSink->feed(m_sampleBuffer.begin(), m_sampleBuffer.end(), !m_settings.m_dsb); - } - - m_sampleBuffer.clear(); - } -} - -void SSBMod::pushFeedback(Complex c) -{ - Complex ci; - - if (m_feedbackInterpolatorDistance < 1.0f) // interpolate - { - while (!m_feedbackInterpolator.interpolate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) - { - processOneSample(ci); - m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; - } - } - else // decimate - { - if (m_feedbackInterpolator.decimate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) - { - processOneSample(ci); - m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; - } - } -} - -void SSBMod::processOneSample(Complex& ci) -{ - m_feedbackAudioBuffer[m_feedbackAudioBufferFill].l = ci.real(); - m_feedbackAudioBuffer[m_feedbackAudioBufferFill].r = ci.imag(); - ++m_feedbackAudioBufferFill; - - if (m_feedbackAudioBufferFill >= m_feedbackAudioBuffer.size()) - { - uint res = m_feedbackAudioFifo.write((const quint8*)&m_feedbackAudioBuffer[0], m_feedbackAudioBufferFill); - - if (res != m_feedbackAudioBufferFill) - { - qDebug("AMDemod::pushFeedback: %u/%u audio samples written m_feedbackInterpolatorDistance: %f", - res, m_feedbackAudioBufferFill, m_feedbackInterpolatorDistance); - m_feedbackAudioFifo.clear(); - } - - m_feedbackAudioBufferFill = 0; - } -} - -void SSBMod::calculateLevel(Complex& sample) -{ - Real t = sample.real(); // TODO: possibly adjust depending on sample type - - if (m_levelCalcCount < m_levelNbSamples) - { - m_peakLevel = std::max(std::fabs(m_peakLevel), t); - m_levelSum += t * t; - m_levelCalcCount++; - } - else - { - qreal rmsLevel = sqrt(m_levelSum / m_levelNbSamples); - //qDebug("NFMMod::calculateLevel: %f %f", rmsLevel, m_peakLevel); - emit levelChanged(rmsLevel, m_peakLevel, m_levelNbSamples); - m_peakLevel = 0.0f; - m_levelSum = 0.0f; - m_levelCalcCount = 0; - } + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; } void SSBMod::start() { - qDebug() << "SSBMod::start: m_outputSampleRate: " << m_outputSampleRate - << " m_inputFrequencyOffset: " << m_settings.m_inputFrequencyOffset; - - m_audioFifo.clear(); - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); + qDebug("SSBMod::start"); + m_basebandSource->reset(); + m_thread->start(); } void SSBMod::stop() { + qDebug("SSBMod::stop"); + m_thread->exit(); + m_thread->wait(); +} + +void SSBMod::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); } bool SSBMod::handleMessage(const Message& cmd) { - if (UpChannelizer::MsgChannelizerNotification::match(cmd)) - { - UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd; - qDebug() << "SSBMod::handleMessage: MsgChannelizerNotification"; - - applyChannelSettings(notif.getBasebandSampleRate(), notif.getSampleRate(), notif.getFrequencyOffset()); - - return true; - } - else if (MsgConfigureChannelizer::match(cmd)) + if (MsgConfigureChannelizer::match(cmd)) { MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; - qDebug() << "SSBMod::handleMessage: MsgConfigureChannelizer: sampleRate: " << cfg.getSampleRate() - << " centerFrequency: " << cfg.getCenterFrequency(); + qDebug() << "SSBMod::handleMessage: MsgConfigureChannelizer:" + << " getSourceSampleRate: " << cfg.getSourceSampleRate() + << " getSourceCenterFrequency: " << cfg.getSourceCenterFrequency(); - m_channelizer->configure(m_channelizer->getInputMessageQueue(), - cfg.getSampleRate(), - cfg.getCenterFrequency()); + SSBModBaseband::MsgConfigureChannelizer *msg + = SSBModBaseband::MsgConfigureChannelizer::create(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_basebandSource->getInputMessageQueue()->push(msg); return true; } else if (MsgConfigureSSBMod::match(cmd)) { MsgConfigureSSBMod& cfg = (MsgConfigureSSBMod&) cmd; - qDebug() << "SSBMod::handleMessage: MsgConfigureSSBMod"; + qDebug() << "NFMMod::handleMessage: MsgConfigureSSBMod"; applySettings(cfg.getSettings(), cfg.getForce()); @@ -662,33 +172,14 @@ bool SSBMod::handleMessage(const Message& cmd) return true; } - else if (DSPConfigureAudio::match(cmd)) - { - DSPConfigureAudio& cfg = (DSPConfigureAudio&) cmd; - uint32_t sampleRate = cfg.getSampleRate(); - DSPConfigureAudio::AudioType audioType = cfg.getAudioType(); - - qDebug() << "SSBMod::handleMessage: DSPConfigureAudio:" - << " sampleRate: " << sampleRate - << " audioType: " << audioType; - - if (audioType == DSPConfigureAudio::AudioInput) - { - if (sampleRate != m_audioSampleRate) { - applyAudioSampleRate(sampleRate); - } - } - else if (audioType == DSPConfigureAudio::AudioOutput) - { - if (sampleRate != m_audioSampleRate) { - applyFeedbackAudioSampleRate(sampleRate); - } - } - - return true; - } else if (DSPSignalNotification::match(cmd)) { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "NFMMod::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + return true; } else @@ -735,109 +226,6 @@ void SSBMod::seekFileStream(int seekPercentage) } } -void SSBMod::applyAudioSampleRate(int sampleRate) -{ - qDebug("SSBMod::applyAudioSampleRate: %d", sampleRate); - - - MsgConfigureChannelizer* channelConfigMsg = MsgConfigureChannelizer::create( - sampleRate, m_settings.m_inputFrequencyOffset); - m_inputMessageQueue.push(channelConfigMsg); - - m_settingsMutex.lock(); - - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) sampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, sampleRate, m_settings.m_bandwidth, 3.0); - - float band = m_settings.m_bandwidth; - float lowCutoff = m_settings.m_lowCutoff; - bool usb = m_settings.m_usb; - - if (band < 100.0f) // at least 100 Hz - { - band = 100.0f; - lowCutoff = 0; - } - - if (band - lowCutoff < 100.0f) { - lowCutoff = band - 100.0f; - } - - m_SSBFilter->create_filter(lowCutoff / sampleRate, band / sampleRate); - m_DSBFilter->create_dsb_filter((2.0f * band) / sampleRate); - - m_settings.m_bandwidth = band; - m_settings.m_lowCutoff = lowCutoff; - m_settings.m_usb = usb; - - m_toneNco.setFreq(m_settings.m_toneFrequency, sampleRate); - m_cwKeyer.setSampleRate(sampleRate); - - m_audioCompressor.m_rate = sampleRate; - m_audioCompressor.initState(); - - m_settingsMutex.unlock(); - - m_audioSampleRate = sampleRate; - - if (getMessageQueueToGUI()) - { - DSPConfigureAudio *cfg = new DSPConfigureAudio(m_audioSampleRate, DSPConfigureAudio::AudioInput); - getMessageQueueToGUI()->push(cfg); - } - - applyFeedbackAudioSampleRate(m_feedbackAudioSampleRate); -} - -void SSBMod::applyFeedbackAudioSampleRate(unsigned int sampleRate) -{ - qDebug("SSBMod::applyFeedbackAudioSampleRate: %u", sampleRate); - - m_settingsMutex.lock(); - - m_feedbackInterpolatorDistanceRemain = 0; - m_feedbackInterpolatorConsumed = false; - m_feedbackInterpolatorDistance = (Real) sampleRate / (Real) m_audioSampleRate; - Real cutoff = std::min(sampleRate, m_audioSampleRate) / 2.2f; - m_feedbackInterpolator.create(48, sampleRate, cutoff, 3.0); - - m_settingsMutex.unlock(); - - m_feedbackAudioSampleRate = sampleRate; -} - -void SSBMod::applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force) -{ - qDebug() << "SSBMod::applyChannelSettings:" - << " basebandSampleRate: " << basebandSampleRate - << " outputSampleRate: " << outputSampleRate - << " inputFrequencyOffset: " << inputFrequencyOffset; - - if ((inputFrequencyOffset != m_inputFrequencyOffset) || - (outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_carrierNco.setFreq(inputFrequencyOffset, outputSampleRate); - m_settingsMutex.unlock(); - } - - if ((outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_audioSampleRate / (Real) outputSampleRate; - m_interpolator.create(48, m_audioSampleRate, m_settings.m_bandwidth, 3.0); - m_settingsMutex.unlock(); - } - - m_basebandSampleRate = basebandSampleRate; - m_outputSampleRate = outputSampleRate; - m_inputFrequencyOffset = inputFrequencyOffset; -} - void SSBMod::applySettings(const SSBModSettings& settings, bool force) { float band = settings.m_bandwidth; @@ -897,61 +285,18 @@ void SSBMod::applySettings(const SSBModSettings& settings, bool force) reverseAPIKeys.append("audioDeviceName"); } - if ((settings.m_bandwidth != m_settings.m_bandwidth) || - (settings.m_lowCutoff != m_settings.m_lowCutoff) || force) - { - if (band < 100.0f) // at least 100 Hz - { - band = 100.0f; - lowCutoff = 0; - } - - if (band - lowCutoff < 100.0f) { - lowCutoff = band - 100.0f; - } - - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_audioSampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, m_audioSampleRate, band, 3.0); - m_SSBFilter->create_filter(lowCutoff / m_audioSampleRate, band / m_audioSampleRate); - m_DSBFilter->create_dsb_filter((2.0f * band) / m_audioSampleRate); - m_settingsMutex.unlock(); - } - - if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) - { - m_settingsMutex.lock(); - m_toneNco.setFreq(settings.m_toneFrequency, m_audioSampleRate); - m_settingsMutex.unlock(); - } - - if ((settings.m_dsb != m_settings.m_dsb) || force) - { - if (settings.m_dsb) - { - std::fill(m_DSBFilterBuffer, m_DSBFilterBuffer+m_ssbFftLen, Complex{0,0}); - //memset(m_DSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen)); - m_DSBFilterBufferIndex = 0; - } - else - { - std::fill(m_SSBFilterBuffer, m_SSBFilterBuffer+(m_ssbFftLen>>1), Complex{0,0}); - //memset(m_SSBFilterBuffer, 0, sizeof(Complex)*(m_ssbFftLen>>1)); - m_SSBFilterBufferIndex = 0; - } - } - if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force) { AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); int audioDeviceIndex = audioDeviceManager->getInputDeviceIndex(settings.m_audioDeviceName); - audioDeviceManager->addAudioSource(&m_audioFifo, getInputMessageQueue(), audioDeviceIndex); + audioDeviceManager->addAudioSource(m_basebandSource->getAudioFifo(), getInputMessageQueue(), audioDeviceIndex); uint32_t audioSampleRate = audioDeviceManager->getInputSampleRate(audioDeviceIndex); - if (m_audioSampleRate != audioSampleRate) { - applyAudioSampleRate(audioSampleRate); + if (m_basebandSource->getAudioSampleRate() != audioSampleRate) + { + reverseAPIKeys.append("audioSampleRate"); + DSPConfigureAudio *msg = new DSPConfigureAudio(audioSampleRate, DSPConfigureAudio::AudioInput); + m_basebandSource->getInputMessageQueue()->push(msg); } } @@ -960,15 +305,19 @@ void SSBMod::applySettings(const SSBModSettings& settings, bool force) reverseAPIKeys.append("feedbackAudioDeviceName"); AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); int audioDeviceIndex = audioDeviceManager->getOutputDeviceIndex(settings.m_feedbackAudioDeviceName); - audioDeviceManager->addAudioSink(&m_feedbackAudioFifo, getInputMessageQueue(), audioDeviceIndex); + audioDeviceManager->addAudioSink(m_basebandSource->getFeedbackAudioFifo(), getInputMessageQueue(), audioDeviceIndex); uint32_t audioSampleRate = audioDeviceManager->getOutputSampleRate(audioDeviceIndex); - if (m_feedbackAudioSampleRate != audioSampleRate) { + if (m_basebandSource->getFeedbackAudioSampleRate() != audioSampleRate) { reverseAPIKeys.append("feedbackAudioSampleRate"); - applyFeedbackAudioSampleRate(audioSampleRate); + DSPConfigureAudio *msg = new DSPConfigureAudio(audioSampleRate, DSPConfigureAudio::AudioOutput); + m_basebandSource->getInputMessageQueue()->push(msg); } } + SSBModBaseband::MsgConfigureSSBModBaseband *msg = SSBModBaseband::MsgConfigureSSBModBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); + if (settings.m_useReverseAPI) { bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || @@ -1017,7 +366,7 @@ int SSBMod::webapiSettingsGet( webapiFormatChannelSettings(response, m_settings); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = response.getSsbModSettings()->getCwKeyer(); - const CWKeyerSettings& cwKeyerSettings = m_cwKeyer.getSettings(); + const CWKeyerSettings& cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); CWKeyer::webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); return 200; @@ -1036,11 +385,11 @@ int SSBMod::webapiSettingsPutPatch( if (channelSettingsKeys.contains("cwKeyer")) { SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = response.getSsbModSettings()->getCwKeyer(); - CWKeyerSettings cwKeyerSettings = m_cwKeyer.getSettings(); + CWKeyerSettings cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); CWKeyer::webapiSettingsPutPatch(channelSettingsKeys, cwKeyerSettings, apiCwKeyerSettings); CWKeyer::MsgConfigureCWKeyer *msgCwKeyer = CWKeyer::MsgConfigureCWKeyer::create(cwKeyerSettings, force); - m_cwKeyer.getInputMessageQueue()->push(msgCwKeyer); + m_basebandSource->getCWKeyer().getInputMessageQueue()->push(msgCwKeyer); if (m_guiMessageQueue) // forward to GUI if any { @@ -1052,7 +401,7 @@ int SSBMod::webapiSettingsPutPatch( if (m_settings.m_inputFrequencyOffset != settings.m_inputFrequencyOffset) { SSBMod::MsgConfigureChannelizer *msgChan = SSBMod::MsgConfigureChannelizer::create( - m_audioSampleRate, settings.m_inputFrequencyOffset); + m_basebandSource->getAudioSampleRate(), settings.m_inputFrequencyOffset); m_inputMessageQueue.push(msgChan); } @@ -1205,8 +554,8 @@ void SSBMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& respon void SSBMod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) { response.getSsbModReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); - response.getSsbModReport()->setAudioSampleRate(m_audioSampleRate); - response.getSsbModReport()->setChannelSampleRate(m_outputSampleRate); + response.getSsbModReport()->setAudioSampleRate(m_basebandSource->getAudioSampleRate()); + response.getSsbModReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); } void SSBMod::webapiReverseSendSettings(QList& channelSettingsKeys, const SSBModSettings& settings, bool force) @@ -1275,10 +624,10 @@ void SSBMod::webapiReverseSendSettings(QList& channelSettingsKeys, cons if (force) { - const CWKeyerSettings& cwKeyerSettings = m_cwKeyer.getSettings(); + const CWKeyerSettings& cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); swgSSBModSettings->setCwKeyer(new SWGSDRangel::SWGCWKeyerSettings()); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = swgSSBModSettings->getCwKeyer(); - m_cwKeyer.webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); + m_basebandSource->getCWKeyer().webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); } QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") @@ -1289,13 +638,14 @@ void SSBMod::webapiReverseSendSettings(QList& channelSettingsKeys, cons m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -1310,7 +660,7 @@ void SSBMod::webapiReverseSendCWSettings(const CWKeyerSettings& cwKeyerSettings) swgSSBModSettings->setCwKeyer(new SWGSDRangel::SWGCWKeyerSettings()); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = swgSSBModSettings->getCwKeyer(); - m_cwKeyer.webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); + m_basebandSource->getCWKeyer().webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") .arg(m_settings.m_reverseAPIAddress) @@ -1320,13 +670,14 @@ void SSBMod::webapiReverseSendCWSettings(const CWKeyerSettings& cwKeyerSettings) m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -1341,10 +692,38 @@ void SSBMod::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("SSBMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("SSBMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } + +double SSBMod::getMagSq() const +{ + return m_basebandSource->getMagSq(); +} + +CWKeyer *SSBMod::getCWKeyer() +{ + return &m_basebandSource->getCWKeyer(); +} + +void SSBMod::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); +} + +unsigned int SSBMod::getAudioSampleRate() const +{ + return m_basebandSource->getAudioSampleRate(); +} + +void SSBMod::setSpectrumSink(BasebandSampleSink *sampleSink) +{ + m_basebandSource->setSpectrumSink(sampleSink); +} \ No newline at end of file diff --git a/plugins/channeltx/modssb/ssbmod.h b/plugins/channeltx/modssb/ssbmod.h index 2c6a137ac..2a31cfde8 100644 --- a/plugins/channeltx/modssb/ssbmod.h +++ b/plugins/channeltx/modssb/ssbmod.h @@ -28,23 +28,16 @@ #include "dsp/basebandsamplesource.h" #include "channel/channelapi.h" #include "dsp/basebandsamplesink.h" -#include "dsp/ncof.h" -#include "dsp/interpolator.h" -#include "util/movingaverage.h" -#include "dsp/agc.h" -#include "dsp/fftfilt.h" -#include "dsp/cwkeyer.h" -#include "audio/audiofifo.h" -#include "audio/audiocompressorsnd.h" #include "util/message.h" #include "ssbmodsettings.h" class QNetworkAccessManager; class QNetworkReply; +class QThread; class DeviceAPI; -class ThreadedBasebandSampleSource; -class UpChannelizer; +class CWKeyer; +class SSBModBaseband; class SSBMod : public BasebandSampleSource, public ChannelAPI { Q_OBJECT @@ -73,26 +66,33 @@ public: { } }; + /** + * |<------ Baseband from device (before device soft interpolation) -------------------------->| + * |<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->| + * | ^-------------------------------| + * | | Source CF + * | | Source SR | + */ class MsgConfigureChannelizer : public Message { MESSAGE_CLASS_DECLARATION public: - int getSampleRate() const { return m_sampleRate; } - int getCenterFrequency() const { return m_centerFrequency; } + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } - static MsgConfigureChannelizer* create(int sampleRate, int centerFrequency) + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) { - return new MsgConfigureChannelizer(sampleRate, centerFrequency); + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); } private: - int m_sampleRate; - int m_centerFrequency; + int m_sourceSampleRate; + int m_sourceCenterFrequency; - MsgConfigureChannelizer(int sampleRate, int centerFrequency) : + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : Message(), - m_sampleRate(sampleRate), - m_centerFrequency(centerFrequency) + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) { } }; @@ -207,12 +207,9 @@ public: ~SSBMod(); virtual void destroy() { delete this; } - void setSpectrumSampleSink(BasebandSampleSink* sampleSink) { m_sampleSink = sampleSink; } - - virtual void pull(Sample& sample); - virtual void pullAudio(int nbSamples); virtual void start(); virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); virtual bool handleMessage(const Message& cmd); virtual void getIdentifier(QString& id) { id = objectName(); } @@ -255,24 +252,15 @@ public: const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response); - uint32_t getAudioSampleRate() const { return m_audioSampleRate; } - double getMagSq() const { return m_magsq; } - - CWKeyer *getCWKeyer() { return &m_cwKeyer; } + double getMagSq() const; + CWKeyer *getCWKeyer(); + void setLevelMeter(QObject *levelMeter); + unsigned int getAudioSampleRate() const; + void setSpectrumSink(BasebandSampleSink *sampleSink); static const QString m_channelIdURI; static const QString m_channelId; -signals: - /** - * Level changed - * \param rmsLevel RMS level in range 0.0 - 1.0 - * \param peakLevel Peak level in range 0.0 - 1.0 - * \param numSamples Number of audio samples analyzed - */ - void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); - - private: enum RateState { RSInitialFill, @@ -280,56 +268,11 @@ private: }; DeviceAPI* m_deviceAPI; - ThreadedBasebandSampleSource* m_threadedChannelizer; - UpChannelizer* m_channelizer; - - int m_basebandSampleRate; - int m_outputSampleRate; - int m_inputFrequencyOffset; + QThread *m_thread; + SSBModBaseband* m_basebandSource; SSBModSettings m_settings; - NCOF m_carrierNco; - NCOF m_toneNco; - Complex m_modSample; - - Interpolator m_interpolator; - Real m_interpolatorDistance; - Real m_interpolatorDistanceRemain; - bool m_interpolatorConsumed; - - Interpolator m_feedbackInterpolator; - Real m_feedbackInterpolatorDistance; - Real m_feedbackInterpolatorDistanceRemain; - bool m_feedbackInterpolatorConsumed; - - fftfilt* m_SSBFilter; - fftfilt* m_DSBFilter; - Complex* m_SSBFilterBuffer; - Complex* m_DSBFilterBuffer; - int m_SSBFilterBufferIndex; - int m_DSBFilterBufferIndex; - static const int m_ssbFftLen; - - BasebandSampleSink* m_sampleSink; SampleVector m_sampleBuffer; - - fftfilt::cmplx m_sum; - int m_undersampleCount; - int m_sumCount; - - double m_magsq; - MovingAverageUtil m_movingAverage; - - quint32 m_audioSampleRate; - AudioVector m_audioBuffer; - uint m_audioBufferFill; - AudioFifo m_audioFifo; - - quint32 m_feedbackAudioSampleRate; - AudioVector m_feedbackAudioBuffer; - uint m_feedbackAudioBufferFill; - AudioFifo m_feedbackAudioFifo; - QMutex m_settingsMutex; std::ifstream m_ifstream; @@ -338,28 +281,10 @@ private: quint32 m_recordLength; //!< record length in seconds computed from file size int m_sampleRate; - quint32 m_levelCalcCount; - Real m_peakLevel; - Real m_levelSum; - CWKeyer m_cwKeyer; - - AudioCompressorSnd m_audioCompressor; - int m_agcStepLength; - QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; - static const int m_levelNbSamples; - - void applyAudioSampleRate(int sampleRate); - void applyFeedbackAudioSampleRate(unsigned int sampleRate); - void processOneSample(Complex& ci); - void applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force = false); void applySettings(const SSBModSettings& settings, bool force = false); - void pullAF(Complex& sample); - void pushFeedback(Complex sample); - void calculateLevel(Complex& sample); - void modulateSample(); void openFileStream(); void seekFileStream(int seekPercentage); void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); diff --git a/plugins/channeltx/modssb/ssbmodbaseband.cpp b/plugins/channeltx/modssb/ssbmodbaseband.cpp new file mode 100644 index 000000000..269837320 --- /dev/null +++ b/plugins/channeltx/modssb/ssbmodbaseband.cpp @@ -0,0 +1,227 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "ssbmodbaseband.h" + +MESSAGE_CLASS_DEFINITION(SSBModBaseband::MsgConfigureSSBModBaseband, Message) +MESSAGE_CLASS_DEFINITION(SSBModBaseband::MsgConfigureChannelizer, Message) + +SSBModBaseband::SSBModBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpSampleChannelizer(&m_source); + + qDebug("SSBModBaseband::SSBModBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &SSBModBaseband::handleData, + Qt::QueuedConnection + ); + + DSPEngine::instance()->getAudioDeviceManager()->addAudioSource(m_source.getAudioFifo(), getInputMessageQueue()); + m_source.applyAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getInputSampleRate()); + + DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(m_source.getFeedbackAudioFifo(), getInputMessageQueue()); + m_source.applyFeedbackAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getOutputSampleRate()); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +SSBModBaseband::~SSBModBaseband() +{ + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(m_source.getFeedbackAudioFifo()); + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSource(m_source.getAudioFifo()); + delete m_channelizer; +} + +void SSBModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void SSBModBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void SSBModBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + Real rmsLevel, peakLevel, numSamples; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); +} + +void SSBModBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void SSBModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool SSBModBaseband::handleMessage(const Message& cmd) +{ + if (DSPConfigureAudio::match(cmd)) + { + DSPConfigureAudio& cfg = (DSPConfigureAudio&) cmd; + uint32_t sampleRate = cfg.getSampleRate(); + DSPConfigureAudio::AudioType audioType = cfg.getAudioType(); + + qDebug() << "SSBModBaseband::handleMessage: DSPConfigureAudio:" + << " sampleRate: " << sampleRate + << " audioType: " << audioType; + + if (audioType == DSPConfigureAudio::AudioInput) + { + if (sampleRate != m_source.getAudioSampleRate()) { + m_source.applyAudioSampleRate(sampleRate); + } + } + else if (audioType == DSPConfigureAudio::AudioOutput) + { + if (sampleRate != m_source.getFeedbackAudioSampleRate()) { + m_source.applyFeedbackAudioSampleRate(sampleRate); + } + } + + return true; + } + else if (MsgConfigureSSBModBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureSSBModBaseband& cfg = (MsgConfigureSSBModBaseband&) cmd; + qDebug() << "SSBModBaseband::handleMessage: MsgConfigureSSBModBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureChannelizer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; + qDebug() << "SSBModBaseband::handleMessage: MsgConfigureChannelizer" + << "(requested) sourceSampleRate: " << cfg.getSourceSampleRate() + << "(requested) sourceCenterFrequency: " << cfg.getSourceCenterFrequency(); + m_channelizer->setChannelization(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "SSBModBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (CWKeyer::MsgConfigureCWKeyer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + const CWKeyer::MsgConfigureCWKeyer& cfg = (CWKeyer::MsgConfigureCWKeyer&) cmd; + CWKeyer::MsgConfigureCWKeyer *notif = new CWKeyer::MsgConfigureCWKeyer(cfg); + CWKeyer& cwKeyer = m_source.getCWKeyer(); + cwKeyer.getInputMessageQueue()->push(notif); + + return true; + } + else + { + return false; + } +} + +void SSBModBaseband::applySettings(const SSBModSettings& settings, bool force) +{ + m_source.applySettings(settings, force); + m_settings = settings; +} + +int SSBModBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} \ No newline at end of file diff --git a/plugins/channeltx/modssb/ssbmodbaseband.h b/plugins/channeltx/modssb/ssbmodbaseband.h new file mode 100644 index 000000000..31294b59d --- /dev/null +++ b/plugins/channeltx/modssb/ssbmodbaseband.h @@ -0,0 +1,124 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_SSBMODBASEBAND_H +#define INCLUDE_SSBMODBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "ssbmodsource.h" + +class UpSampleChannelizer; +class BasebandSampleSink; + +class SSBModBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureSSBModBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const SSBModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureSSBModBaseband* create(const SSBModSettings& settings, bool force) + { + return new MsgConfigureSSBModBaseband(settings, force); + } + + private: + SSBModSettings m_settings; + bool m_force; + + MsgConfigureSSBModBaseband(const SSBModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureChannelizer : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } + + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) + { + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); + } + + private: + int m_sourceSampleRate; + int m_sourceCenterFrequency; + + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : + Message(), + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) + { } + }; + + SSBModBaseband(); + ~SSBModBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + CWKeyer& getCWKeyer() { return m_source.getCWKeyer(); } + double getMagSq() const { return m_source.getMagSq(); } + unsigned int getAudioSampleRate() const { return m_source.getAudioSampleRate(); } + unsigned int getFeedbackAudioSampleRate() const { return m_source.getFeedbackAudioSampleRate(); } + int getChannelSampleRate() const; + void setInputFileStream(std::ifstream *ifstream) { m_source.setInputFileStream(ifstream); } + AudioFifo *getAudioFifo() { return m_source.getAudioFifo(); } + AudioFifo *getFeedbackAudioFifo() { return m_source.getFeedbackAudioFifo(); } + void setSpectrumSink(BasebandSampleSink *sampleSink) { m_source.setSpectrumSink(sampleSink); } + +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + SampleSourceFifo m_sampleFifo; + UpSampleChannelizer *m_channelizer; + SSBModSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + SSBModSettings m_settings; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const SSBModSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_SSBMODBASEBAND_H diff --git a/plugins/channeltx/modssb/ssbmodgui.cpp b/plugins/channeltx/modssb/ssbmodgui.cpp index a8c31aedc..aff4d602d 100644 --- a/plugins/channeltx/modssb/ssbmodgui.cpp +++ b/plugins/channeltx/modssb/ssbmodgui.cpp @@ -31,6 +31,7 @@ #include "util/db.h" #include "dsp/dspengine.h" #include "dsp/dspcommands.h" +#include "dsp/cwkeyer.h" #include "gui/crightclickenabler.h" #include "gui/audioselectdialog.h" #include "gui/basicchannelsettingsdialog.h" @@ -405,7 +406,7 @@ SSBModGUI::SSBModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSam m_spectrumVis = new SpectrumVis(SDR_TX_SCALEF, ui->glSpectrum); m_ssbMod = (SSBMod*) channelTx; //new SSBMod(m_deviceUISet->m_deviceSinkAPI); - m_ssbMod->setSpectrumSampleSink(m_spectrumVis); + m_ssbMod->setSpectrumSink(m_spectrumVis); m_ssbMod->setMessageQueueToGUI(getInputMessageQueue()); resetToDefaults(); @@ -455,7 +456,7 @@ SSBModGUI::SSBModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSam m_settings.setCWKeyerGUI(ui->cwKeyerGUI); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); - connect(m_ssbMod, SIGNAL(levelChanged(qreal, qreal, int)), ui->volumeMeter, SLOT(levelChanged(qreal, qreal, int))); + m_ssbMod->setLevelMeter(ui->volumeMeter); m_iconDSBUSB.addPixmap(QPixmap("://dsb.png"), QIcon::Normal, QIcon::On); m_iconDSBUSB.addPixmap(QPixmap("://usb.png"), QIcon::Normal, QIcon::Off); diff --git a/plugins/channeltx/modssb/ssbmodplugin.cpp b/plugins/channeltx/modssb/ssbmodplugin.cpp index 3ead9f6cf..23e0b7452 100644 --- a/plugins/channeltx/modssb/ssbmodplugin.cpp +++ b/plugins/channeltx/modssb/ssbmodplugin.cpp @@ -27,7 +27,7 @@ const PluginDescriptor SSBModPlugin::m_pluginDescriptor = { QString("SSB Modulator"), - QString("4.11.6"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/channeltx/modssb/ssbmodsource.cpp b/plugins/channeltx/modssb/ssbmodsource.cpp new file mode 100644 index 000000000..89e831f20 --- /dev/null +++ b/plugins/channeltx/modssb/ssbmodsource.cpp @@ -0,0 +1,638 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/basebandsamplesink.h" +#include "ssbmodsource.h" + +const int SSBModSource::m_ssbFftLen = 1024; +const int SSBModSource::m_levelNbSamples = 480; // every 10ms + +SSBModSource::SSBModSource() : + m_channelSampleRate(48000), + m_channelFrequencyOffset(0), + m_audioFifo(4800), + m_feedbackAudioFifo(48000), + m_levelCalcCount(0), + m_peakLevel(0.0f), + m_levelSum(0.0f), + m_ifstream(nullptr), + m_audioSampleRate(48000) +{ + m_SSBFilter = new fftfilt(m_settings.m_lowCutoff / m_audioSampleRate, m_settings.m_bandwidth / m_audioSampleRate, m_ssbFftLen); + m_DSBFilter = new fftfilt((2.0f * m_settings.m_bandwidth) / m_audioSampleRate, 2 * m_ssbFftLen); + m_SSBFilterBuffer = new Complex[m_ssbFftLen>>1]; // filter returns data exactly half of its size + m_DSBFilterBuffer = new Complex[m_ssbFftLen]; + std::fill(m_SSBFilterBuffer, m_SSBFilterBuffer+(m_ssbFftLen>>1), Complex{0,0}); + std::fill(m_DSBFilterBuffer, m_DSBFilterBuffer+m_ssbFftLen, Complex{0,0}); + + m_audioBuffer.resize(1<<14); + m_audioBufferFill = 0; + + m_feedbackAudioBuffer.resize(1<<14); + m_feedbackAudioBufferFill = 0; + + m_sum.real(0.0f); + m_sum.imag(0.0f); + m_undersampleCount = 0; + m_sumCount = 0; + + m_magsq = 0.0; + m_toneNco.setFreq(1000.0, m_audioSampleRate); + + m_cwKeyer.setSampleRate(m_audioSampleRate); + m_cwKeyer.reset(); + + m_audioCompressor.initSimple( + m_audioSampleRate, + 50, // pregain (dB) + -30, // threshold (dB) + 20, // knee (dB) + 12, // ratio (dB) + 0.003, // attack (s) + 0.25 // release (s) + ); + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +SSBModSource::~SSBModSource() +{ + delete m_SSBFilter; + delete m_DSBFilter; + delete[] m_SSBFilterBuffer; + delete[] m_DSBFilterBuffer; +} + +void SSBModSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void SSBModSource::pullOne(Sample& sample) +{ + Complex ci; + + if (m_interpolatorDistance > 1.0f) // decimate + { + modulateSample(); + + while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + else + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + + m_interpolatorDistanceRemain += m_interpolatorDistance; + + ci *= m_carrierNco.nextIQ(); // shift to carrier frequency + ci *= 0.891235351562f * SDR_TX_SCALEF; //scaling at -1 dB to account for possible filter overshoot + + double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); + magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + + sample.m_real = (FixReal) ci.real(); + sample.m_imag = (FixReal) ci.imag(); +} + +void SSBModSource::prefetch(unsigned int nbSamples) +{ + unsigned int nbSamplesAudio = nbSamples * ((Real) m_audioSampleRate / (Real) m_channelSampleRate); + pullAudio(nbSamplesAudio); +} + +void SSBModSource::pullAudio(unsigned int nbSamplesAudio) +{ + if (nbSamplesAudio > m_audioBuffer.size()) + { + m_audioBuffer.resize(nbSamplesAudio); + } + + m_audioFifo.read(reinterpret_cast(&m_audioBuffer[0]), nbSamplesAudio); + m_audioBufferFill = 0; +} + +void SSBModSource::modulateSample() +{ + pullAF(m_modSample); + + if (m_settings.m_feedbackAudioEnable) { + pushFeedback(m_modSample * m_settings.m_feedbackVolumeFactor * 16384.0f); + } + + calculateLevel(m_modSample); + m_audioBufferFill++; +} + +void SSBModSource::pullAF(Complex& sample) +{ + if (m_settings.m_audioMute) + { + sample.real(0.0f); + sample.imag(0.0f); + return; + } + + Complex ci; + fftfilt::cmplx *filtered; + int n_out = 0; + + int decim = 1<<(m_settings.m_spanLog2 - 1); + unsigned char decim_mask = decim - 1; // counter LSB bit mask for decimation by 2^(m_scaleLog2 - 1) + + switch (m_settings.m_modAFInput) + { + case SSBModSettings::SSBModInputTone: + if (m_settings.m_dsb) + { + Real t = m_toneNco.next()/1.25; + sample.real(t); + sample.imag(t); + } + else + { + if (m_settings.m_usb) { + sample = m_toneNco.nextIQ(); + } else { + sample = m_toneNco.nextQI(); + } + } + break; + case SSBModSettings::SSBModInputFile: + // Monaural (mono): + // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw + // ffplay -f f32le -ar 48k -ac 1 f4exb_call.raw + // Binaural (stereo): + // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw + // ffplay -f f32le -ar 48k -ac 2 f4exb_call.raw + if (m_ifstream && m_ifstream->is_open()) + { + if (m_ifstream->eof()) + { + if (m_settings.m_playLoop) + { + m_ifstream->clear(); + m_ifstream->seekg(0, std::ios::beg); + } + } + + if (m_ifstream->eof()) + { + ci.real(0.0f); + ci.imag(0.0f); + } + else + { + if (m_settings.m_audioBinaural) + { + Complex c; + m_ifstream->read(reinterpret_cast(&c), sizeof(Complex)); + + if (m_settings.m_audioFlipChannels) + { + ci.real(c.imag() * m_settings.m_volumeFactor); + ci.imag(c.real() * m_settings.m_volumeFactor); + } + else + { + ci = c * m_settings.m_volumeFactor; + } + } + else + { + Real real; + m_ifstream->read(reinterpret_cast(&real), sizeof(Real)); + + if (m_settings.m_agc) + { + real = m_audioCompressor.compress(real); + ci.real(real); + ci.imag(0.0f); + ci *= m_settings.m_volumeFactor; + } + else + { + ci.real(real * m_settings.m_volumeFactor); + ci.imag(0.0f); + } + } + } + } + else + { + ci.real(0.0f); + ci.imag(0.0f); + } + break; + case SSBModSettings::SSBModInputAudio: + if (m_settings.m_audioBinaural) + { + if (m_settings.m_audioFlipChannels) + { + ci.real((m_audioBuffer[m_audioBufferFill].r / SDR_TX_SCALEF) * m_settings.m_volumeFactor); + ci.imag((m_audioBuffer[m_audioBufferFill].l / SDR_TX_SCALEF) * m_settings.m_volumeFactor); + } + else + { + ci.real((m_audioBuffer[m_audioBufferFill].l / SDR_TX_SCALEF) * m_settings.m_volumeFactor); + ci.imag((m_audioBuffer[m_audioBufferFill].r / SDR_TX_SCALEF) * m_settings.m_volumeFactor); + } + } + else + { + if (m_settings.m_agc) + { + ci.real(((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f)); + ci.real(m_audioCompressor.compress(ci.real())); + ci.imag(0.0f); + ci *= m_settings.m_volumeFactor; + } + else + { + ci.real(((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f) * m_settings.m_volumeFactor); + ci.imag(0.0f); + } + } + + break; + case SSBModSettings::SSBModInputCWTone: + Real fadeFactor; + + if (m_cwKeyer.getSample()) + { + m_cwKeyer.getCWSmoother().getFadeSample(true, fadeFactor); + + if (m_settings.m_dsb) + { + Real t = m_toneNco.next() * fadeFactor; + sample.real(t); + sample.imag(t); + } + else + { + if (m_settings.m_usb) { + sample = m_toneNco.nextIQ() * fadeFactor; + } else { + sample = m_toneNco.nextQI() * fadeFactor; + } + } + } + else + { + if (m_cwKeyer.getCWSmoother().getFadeSample(false, fadeFactor)) + { + if (m_settings.m_dsb) + { + Real t = (m_toneNco.next() * fadeFactor)/1.25; + sample.real(t); + sample.imag(t); + } + else + { + if (m_settings.m_usb) { + sample = m_toneNco.nextIQ() * fadeFactor; + } else { + sample = m_toneNco.nextQI() * fadeFactor; + } + } + } + else + { + sample.real(0.0f); + sample.imag(0.0f); + m_toneNco.setPhase(0); + } + } + + break; + case SSBModSettings::SSBModInputNone: + default: + sample.real(0.0f); + sample.imag(0.0f); + break; + } + + if ((m_settings.m_modAFInput == SSBModSettings::SSBModInputFile) + || (m_settings.m_modAFInput == SSBModSettings::SSBModInputAudio)) // real audio + { + if (m_settings.m_dsb) + { + n_out = m_DSBFilter->runDSB(ci, &filtered); + + if (n_out > 0) + { + memcpy((void *) m_DSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); + m_DSBFilterBufferIndex = 0; + } + + sample = m_DSBFilterBuffer[m_DSBFilterBufferIndex]; + m_DSBFilterBufferIndex++; + } + else + { + n_out = m_SSBFilter->runSSB(ci, &filtered, m_settings.m_usb); + + if (n_out > 0) + { + memcpy((void *) m_SSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); + m_SSBFilterBufferIndex = 0; + } + + sample = m_SSBFilterBuffer[m_SSBFilterBufferIndex]; + m_SSBFilterBufferIndex++; + } + + if (n_out > 0) + { + for (int i = 0; i < n_out; i++) + { + // Downsample by 2^(m_scaleLog2 - 1) for SSB band spectrum display + // smart decimation with bit gain using float arithmetic (23 bits significand) + + m_sum += filtered[i]; + + if (!(m_undersampleCount++ & decim_mask)) + { + Real avgr = (m_sum.real() / decim) * 0.891235351562f * SDR_TX_SCALEF; //scaling at -1 dB to account for possible filter overshoot + Real avgi = (m_sum.imag() / decim) * 0.891235351562f * SDR_TX_SCALEF; + + if (!m_settings.m_dsb & !m_settings.m_usb) + { // invert spectrum for LSB + m_sampleBuffer.push_back(Sample(avgi, avgr)); + } + else + { + m_sampleBuffer.push_back(Sample(avgr, avgi)); + } + + m_sum.real(0.0); + m_sum.imag(0.0); + } + } + } + } // Real audio + else if ((m_settings.m_modAFInput == SSBModSettings::SSBModInputTone) + || (m_settings.m_modAFInput == SSBModSettings::SSBModInputCWTone)) // tone + { + m_sum += sample; + + if (!(m_undersampleCount++ & decim_mask)) + { + Real avgr = (m_sum.real() / decim) * 0.891235351562f * SDR_TX_SCALEF; //scaling at -1 dB to account for possible filter overshoot + Real avgi = (m_sum.imag() / decim) * 0.891235351562f * SDR_TX_SCALEF; + + if (!m_settings.m_dsb & !m_settings.m_usb) + { // invert spectrum for LSB + m_sampleBuffer.push_back(Sample(avgi, avgr)); + } + else + { + m_sampleBuffer.push_back(Sample(avgr, avgi)); + } + + m_sum.real(0.0); + m_sum.imag(0.0); + } + + if (m_sumCount < (m_settings.m_dsb ? m_ssbFftLen : m_ssbFftLen>>1)) + { + n_out = 0; + m_sumCount++; + } + else + { + n_out = m_sumCount; + m_sumCount = 0; + } + } + + if (n_out > 0) + { + if (m_spectrumSink) { + m_spectrumSink->feed(m_sampleBuffer.begin(), m_sampleBuffer.end(), !m_settings.m_dsb); + } + + m_sampleBuffer.clear(); + } +} + +void SSBModSource::pushFeedback(Complex c) +{ + Complex ci; + + if (m_feedbackInterpolatorDistance < 1.0f) // interpolate + { + while (!m_feedbackInterpolator.interpolate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; + } + } + else // decimate + { + if (m_feedbackInterpolator.decimate(&m_feedbackInterpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_feedbackInterpolatorDistanceRemain += m_feedbackInterpolatorDistance; + } + } +} + +void SSBModSource::processOneSample(Complex& ci) +{ + m_feedbackAudioBuffer[m_feedbackAudioBufferFill].l = ci.real(); + m_feedbackAudioBuffer[m_feedbackAudioBufferFill].r = ci.imag(); + ++m_feedbackAudioBufferFill; + + if (m_feedbackAudioBufferFill >= m_feedbackAudioBuffer.size()) + { + uint res = m_feedbackAudioFifo.write((const quint8*)&m_feedbackAudioBuffer[0], m_feedbackAudioBufferFill); + + if (res != m_feedbackAudioBufferFill) + { + qDebug("SSBModSource::pushFeedback: %u/%u audio samples written m_feedbackInterpolatorDistance: %f", + res, m_feedbackAudioBufferFill, m_feedbackInterpolatorDistance); + m_feedbackAudioFifo.clear(); + } + + m_feedbackAudioBufferFill = 0; + } +} + +void SSBModSource::calculateLevel(Complex& sample) +{ + Real t = sample.real(); // TODO: possibly adjust depending on sample type + + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), t); + m_levelSum += t * t; + m_levelCalcCount++; + } + else + { + m_rmsLevel = sqrt(m_levelSum / m_levelNbSamples); + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void SSBModSource::applyAudioSampleRate(unsigned int sampleRate) +{ + qDebug("SSBModSource::applyAudioSampleRate: %u", sampleRate); + + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) sampleRate / (Real) m_channelSampleRate; + m_interpolator.create(48, sampleRate, m_settings.m_bandwidth, 3.0); + + float band = m_settings.m_bandwidth; + float lowCutoff = m_settings.m_lowCutoff; + bool usb = m_settings.m_usb; + + if (band < 100.0f) // at least 100 Hz + { + band = 100.0f; + lowCutoff = 0; + } + + if (band - lowCutoff < 100.0f) { + lowCutoff = band - 100.0f; + } + + m_SSBFilter->create_filter(lowCutoff / sampleRate, band / sampleRate); + m_DSBFilter->create_dsb_filter((2.0f * band) / sampleRate); + + m_settings.m_bandwidth = band; + m_settings.m_lowCutoff = lowCutoff; + m_settings.m_usb = usb; + + m_toneNco.setFreq(m_settings.m_toneFrequency, sampleRate); + m_cwKeyer.setSampleRate(sampleRate); + m_cwKeyer.reset(); + + m_audioCompressor.m_rate = sampleRate; + m_audioCompressor.initState(); + m_audioSampleRate = sampleRate; + + applyFeedbackAudioSampleRate(m_feedbackAudioSampleRate); +} + +void SSBModSource::applyFeedbackAudioSampleRate(unsigned int sampleRate) +{ + qDebug("SSBModSource::applyFeedbackAudioSampleRate: %u", sampleRate); + + m_feedbackInterpolatorDistanceRemain = 0; + m_feedbackInterpolatorConsumed = false; + m_feedbackInterpolatorDistance = (Real) sampleRate / (Real) m_audioSampleRate; + Real cutoff = std::min(sampleRate, m_audioSampleRate) / 2.2f; + m_feedbackInterpolator.create(48, sampleRate, cutoff, 3.0); + m_feedbackAudioSampleRate = sampleRate; +} + +void SSBModSource::applySettings(const SSBModSettings& settings, bool force) +{ + float band = settings.m_bandwidth; + float lowCutoff = settings.m_lowCutoff; + bool usb = settings.m_usb; + + if ((settings.m_bandwidth != m_settings.m_bandwidth) || + (settings.m_lowCutoff != m_settings.m_lowCutoff) || force) + { + if (band < 100.0f) // at least 100 Hz + { + band = 100.0f; + lowCutoff = 0; + } + + if (band - lowCutoff < 100.0f) { + lowCutoff = band - 100.0f; + } + + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_audioSampleRate / (Real) m_channelSampleRate; + m_interpolator.create(48, m_audioSampleRate, band, 3.0); + m_SSBFilter->create_filter(lowCutoff / m_audioSampleRate, band / m_audioSampleRate); + m_DSBFilter->create_dsb_filter((2.0f * band) / m_audioSampleRate); + } + + if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) { + m_toneNco.setFreq(settings.m_toneFrequency, m_audioSampleRate); + } + + if ((settings.m_dsb != m_settings.m_dsb) || force) + { + if (settings.m_dsb) + { + std::fill(m_DSBFilterBuffer, m_DSBFilterBuffer+m_ssbFftLen, Complex{0,0}); + m_DSBFilterBufferIndex = 0; + } + else + { + std::fill(m_SSBFilterBuffer, m_SSBFilterBuffer+(m_ssbFftLen>>1), Complex{0,0}); + m_SSBFilterBufferIndex = 0; + } + } + + m_settings = settings; + m_settings.m_bandwidth = band; + m_settings.m_lowCutoff = lowCutoff; + m_settings.m_usb = usb; +} + +void SSBModSource::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "SSBModSource::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((channelFrequencyOffset != m_channelFrequencyOffset) + || (channelSampleRate != m_channelSampleRate) || force) { + m_carrierNco.setFreq(channelFrequencyOffset, channelSampleRate); + } + + if ((channelSampleRate != m_channelSampleRate) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_audioSampleRate / (Real) channelSampleRate; + m_interpolator.create(48, m_audioSampleRate, m_settings.m_bandwidth, 3.0); + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} diff --git a/plugins/channeltx/modssb/ssbmodsource.h b/plugins/channeltx/modssb/ssbmodsource.h new file mode 100644 index 000000000..2146e7b71 --- /dev/null +++ b/plugins/channeltx/modssb/ssbmodsource.h @@ -0,0 +1,137 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_SSBMODSOURCE_H +#define INCLUDE_SSBMODSOURCE_H + +#include + +#include +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/ncof.h" +#include "dsp/interpolator.h" +#include "dsp/fftfilt.h" +#include "dsp/cwkeyer.h" +#include "util/movingaverage.h" +#include "audio/audiocompressorsnd.h" +#include "audio/audiofifo.h" + +#include "ssbmodsettings.h" + +class BasebandSampleSink; + +class SSBModSource : public ChannelSampleSource +{ +public: + SSBModSource(); + virtual ~SSBModSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples); + + void setInputFileStream(std::ifstream *ifstream) { m_ifstream = ifstream; } + AudioFifo *getAudioFifo() { return &m_audioFifo; } + AudioFifo *getFeedbackAudioFifo() { return &m_feedbackAudioFifo; } + void applyAudioSampleRate(unsigned int sampleRate); + void applyFeedbackAudioSampleRate(unsigned int sampleRate); + unsigned int getAudioSampleRate() const { return m_audioSampleRate; } + unsigned int getFeedbackAudioSampleRate() const { return m_feedbackAudioSampleRate; } + CWKeyer& getCWKeyer() { return m_cwKeyer; } + double getMagSq() const { return m_magsq; } + void getLevels(Real& rmsLevel, Real& peakLevel, Real& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevel; + numSamples = m_levelNbSamples; + } + void applySettings(const SSBModSettings& settings, bool force = false); + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = 0); + void setSpectrumSink(BasebandSampleSink *sampleSink) { m_spectrumSink = sampleSink; } + +private: + int m_channelSampleRate; + int m_channelFrequencyOffset; + SSBModSettings m_settings; + + NCOF m_carrierNco; + NCOF m_toneNco; + Complex m_modSample; + + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + bool m_interpolatorConsumed; + + Interpolator m_feedbackInterpolator; + Real m_feedbackInterpolatorDistance; + Real m_feedbackInterpolatorDistanceRemain; + bool m_feedbackInterpolatorConsumed; + + fftfilt* m_SSBFilter; + fftfilt* m_DSBFilter; + Complex* m_SSBFilterBuffer; + Complex* m_DSBFilterBuffer; + int m_SSBFilterBufferIndex; + int m_DSBFilterBufferIndex; + static const int m_ssbFftLen; + + BasebandSampleSink* m_spectrumSink; + SampleVector m_sampleBuffer; + + fftfilt::cmplx m_sum; + int m_undersampleCount; + int m_sumCount; + + double m_magsq; + MovingAverageUtil m_movingAverage; + + quint32 m_audioSampleRate; + AudioVector m_audioBuffer; + uint m_audioBufferFill; + AudioFifo m_audioFifo; + + quint32 m_feedbackAudioSampleRate; + AudioVector m_feedbackAudioBuffer; + uint m_feedbackAudioBufferFill; + AudioFifo m_feedbackAudioFifo; + + quint32 m_levelCalcCount; + Real m_rmsLevel; + Real m_peakLevelOut; + Real m_peakLevel; + Real m_levelSum; + + std::ifstream *m_ifstream; + CWKeyer m_cwKeyer; + + AudioCompressorSnd m_audioCompressor; + int m_agcStepLength; + + static const int m_levelNbSamples; + + void processOneSample(Complex& ci); + void pullAF(Complex& sample); + void pullAudio(unsigned int nbSamples); + void pushFeedback(Complex sample); + void calculateLevel(Complex& sample); + void modulateSample(); +}; + +#endif // INCLUDE_SSBMODSOURCE_H diff --git a/plugins/channeltx/modssb/ssbmodwebapiadapter.cpp b/plugins/channeltx/modssb/ssbmodwebapiadapter.cpp index e34d72c16..902327cf3 100644 --- a/plugins/channeltx/modssb/ssbmodwebapiadapter.cpp +++ b/plugins/channeltx/modssb/ssbmodwebapiadapter.cpp @@ -16,6 +16,7 @@ /////////////////////////////////////////////////////////////////////////////////// #include "SWGChannelSettings.h" +#include "dsp/cwkeyer.h" #include "ssbmod.h" #include "ssbmodwebapiadapter.h" diff --git a/plugins/channeltx/modwfm/CMakeLists.txt b/plugins/channeltx/modwfm/CMakeLists.txt index 0bac9ed41..7ebc3310c 100644 --- a/plugins/channeltx/modwfm/CMakeLists.txt +++ b/plugins/channeltx/modwfm/CMakeLists.txt @@ -1,7 +1,9 @@ project(modwfm) set(modwfm_SOURCES - wfmmod.cpp + wfmmod.cpp + wfmmodbaseband.cpp + wfmmodsource.cpp wfmmodplugin.cpp wfmmodsettings.cpp wfmmodwebapiadapter.cpp @@ -9,6 +11,8 @@ set(modwfm_SOURCES set(modwfm_HEADERS wfmmod.h + wfmmodbaseband.h + wfmmodsource.h wfmmodplugin.h wfmmodsettings.h wfmmodwebapiadapter.h diff --git a/plugins/channeltx/modwfm/wfmmod.cpp b/plugins/channeltx/modwfm/wfmmod.cpp index fa1b9261f..8f2bc8248 100644 --- a/plugins/channeltx/modwfm/wfmmod.cpp +++ b/plugins/channeltx/modwfm/wfmmod.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -30,13 +31,12 @@ #include "SWGChannelReport.h" #include "SWGAMModReport.h" -#include "dsp/upchannelizer.h" #include "dsp/dspengine.h" -#include "dsp/threadedbasebandsamplesource.h" #include "dsp/dspcommands.h" #include "device/deviceapi.h" #include "util/db.h" +#include "wfmmodbaseband.h" #include "wfmmod.h" MESSAGE_CLASS_DEFINITION(WFMMod::MsgConfigureWFMMod, Message) @@ -49,51 +49,25 @@ MESSAGE_CLASS_DEFINITION(WFMMod::MsgReportFileSourceStreamTiming, Message) const QString WFMMod::m_channelIdURI = "sdrangel.channeltx.modwfm"; const QString WFMMod::m_channelId = "WFMMod"; -const int WFMMod::m_levelNbSamples = 480; // every 10ms -const int WFMMod::m_rfFilterFFTLength = 1024; WFMMod::WFMMod(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), m_deviceAPI(deviceAPI), - m_basebandSampleRate(384000), - m_outputSampleRate(384000), - m_inputFrequencyOffset(0), - m_modPhasor(0.0f), - m_audioFifo(4800), m_settingsMutex(QMutex::Recursive), m_fileSize(0), m_recordLength(0), - m_sampleRate(48000), - m_levelCalcCount(0), - m_peakLevel(0.0f), - m_levelSum(0.0f) + m_sampleRate(48000) { setObjectName(m_channelId); - m_rfFilter = new fftfilt(-62500.0 / 384000.0, 62500.0 / 384000.0, m_rfFilterFFTLength); - m_rfFilterBuffer = new Complex[m_rfFilterFFTLength]; - std::fill(m_rfFilterBuffer, m_rfFilterBuffer+m_rfFilterFFTLength, Complex{0,0}); - //memset(m_rfFilterBuffer, 0, sizeof(Complex)*(m_rfFilterFFTLength)); - m_rfFilterBufferIndex = 0; + m_thread = new QThread(this); + m_basebandSource = new WFMModBaseband(); + m_basebandSource->setInputFileStream(&m_ifstream); + m_basebandSource->moveToThread(m_thread); - m_audioBuffer.resize(1<<14); - m_audioBufferFill = 0; - - m_magsq = 0.0; - - DSPEngine::instance()->getAudioDeviceManager()->addAudioSource(&m_audioFifo, getInputMessageQueue()); - m_audioSampleRate = DSPEngine::instance()->getAudioDeviceManager()->getInputSampleRate(); - - m_toneNcoRF.setFreq(1000.0, m_outputSampleRate); - m_cwKeyer.setSampleRate(m_outputSampleRate); - m_cwKeyer.reset(); - - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); applySettings(m_settings, true); - m_channelizer = new UpChannelizer(this); - m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this); - m_deviceAPI->addChannelSource(m_threadedChannelizer); + m_deviceAPI->addChannelSource(this); m_deviceAPI->addChannelSourceAPI(this); m_networkManager = new QNetworkAccessManager(); @@ -104,227 +78,50 @@ WFMMod::~WFMMod() { disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); delete m_networkManager; - DSPEngine::instance()->getAudioDeviceManager()->removeAudioSource(&m_audioFifo); m_deviceAPI->removeChannelSourceAPI(this); - m_deviceAPI->removeChannelSource(m_threadedChannelizer); - delete m_threadedChannelizer; - delete m_channelizer; - delete m_rfFilter; - delete[] m_rfFilterBuffer; -} - -void WFMMod::pull(Sample& sample) -{ - if (m_settings.m_channelMute) - { - sample.m_real = 0.0f; - sample.m_imag = 0.0f; - return; - } - - Complex ci, ri; - fftfilt::cmplx *rf; - int rf_out; - - m_settingsMutex.lock(); - - if ((m_settings.m_modAFInput == WFMModSettings::WFMModInputFile) - || (m_settings.m_modAFInput == WFMModSettings::WFMModInputAudio)) - { - if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ri)) - { - pullAF(m_modSample); - calculateLevel(m_modSample.real()); - m_audioBufferFill++; - } - - m_interpolatorDistanceRemain += m_interpolatorDistance; - } - else - { - pullAF(ri); - } - - m_modPhasor += (m_settings.m_fmDeviation / (float) m_outputSampleRate) * ri.real() * M_PI * 2.0f; - ci.real(cos(m_modPhasor) * 0.891235351562f * SDR_TX_SCALEF); // -1 dB - ci.imag(sin(m_modPhasor) * 0.891235351562f * SDR_TX_SCALEF); - - // RF filtering - rf_out = m_rfFilter->runFilt(ci, &rf); - - if (rf_out > 0) - { - memcpy((void *) m_rfFilterBuffer, (const void *) rf, rf_out*sizeof(Complex)); - m_rfFilterBufferIndex = 0; - - } - - ci = m_rfFilterBuffer[m_rfFilterBufferIndex] * m_carrierNco.nextIQ(); // shift to carrier frequency - m_rfFilterBufferIndex++; - - m_settingsMutex.unlock(); - - double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); - magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); - m_movingAverage(magsq); - m_magsq = m_movingAverage.asDouble(); - - sample.m_real = (FixReal) ci.real(); - sample.m_imag = (FixReal) ci.imag(); -} - -void WFMMod::pullAudio(int nbSamples) -{ - unsigned int nbSamplesAudio = nbSamples * ((Real) m_audioSampleRate / (Real) m_basebandSampleRate); - - if (nbSamplesAudio > m_audioBuffer.size()) - { - m_audioBuffer.resize(nbSamplesAudio); - } - - m_audioFifo.read(reinterpret_cast(&m_audioBuffer[0]), nbSamplesAudio); - m_audioBufferFill = 0; -} - -void WFMMod::pullAF(Complex& sample) -{ - switch (m_settings.m_modAFInput) - { - case WFMModSettings::WFMModInputTone: - sample.real(m_toneNcoRF.next() * m_settings.m_volumeFactor); - sample.imag(0.0f); - break; - case WFMModSettings::WFMModInputFile: - // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw - // ffplay -f f32le -ar 48k -ac 1 f4exb_call.raw - if (m_ifstream.is_open()) - { - if (m_ifstream.eof()) - { - if (m_settings.m_playLoop) - { - m_ifstream.clear(); - m_ifstream.seekg(0, std::ios::beg); - } - } - - if (m_ifstream.eof()) - { - sample.real(0.0f); - sample.imag(0.0f); - } - else - { - Real s; - m_ifstream.read(reinterpret_cast(&s), sizeof(Real)); - sample.real(s * m_settings.m_volumeFactor); - sample.imag(0.0f); - } - } - else - { - sample.real(0.0f); - sample.imag(0.0f); - } - break; - case WFMModSettings::WFMModInputAudio: - { - sample.real(((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f) * m_settings.m_volumeFactor); - sample.imag(0.0f); - } - break; - case WFMModSettings::WFMModInputCWTone: - Real fadeFactor; - - if (m_cwKeyer.getSample()) - { - m_cwKeyer.getCWSmoother().getFadeSample(true, fadeFactor); - sample.real(m_toneNcoRF.next() * m_settings.m_volumeFactor * fadeFactor); - sample.imag(0.0f); - } - else - { - if (m_cwKeyer.getCWSmoother().getFadeSample(false, fadeFactor)) - { - sample.real(m_toneNcoRF.next() * m_settings.m_volumeFactor * fadeFactor); - sample.imag(0.0f); - } - else - { - sample.real(0.0f); - sample.imag(0.0f); - m_toneNcoRF.setPhase(0); - } - } - break; - case WFMModSettings::WFMModInputNone: - default: - sample.real(0.0f); - sample.imag(0.0f); - break; - } -} - -void WFMMod::calculateLevel(const Real& sample) -{ - if (m_levelCalcCount < m_levelNbSamples) - { - m_peakLevel = std::max(std::fabs(m_peakLevel), sample); - m_levelSum += sample * sample; - m_levelCalcCount++; - } - else - { - qreal rmsLevel = sqrt(m_levelSum / m_levelNbSamples); - //qDebug("WFMMod::calculateLevel: %f %f", rmsLevel, m_peakLevel); - emit levelChanged(rmsLevel, m_peakLevel, m_levelNbSamples); - m_peakLevel = 0.0f; - m_levelSum = 0.0f; - m_levelCalcCount = 0; - } + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; } void WFMMod::start() { - qDebug() << "WFMMod::start: m_outputSampleRate: " << m_outputSampleRate - << " m_inputFrequencyOffset: " << m_inputFrequencyOffset; - - m_audioFifo.clear(); - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); + qDebug("WFMMod::start"); + m_basebandSource->reset(); + m_thread->start(); } void WFMMod::stop() { + qDebug("WFMMod::stop"); + m_thread->exit(); + m_thread->wait(); +} + +void WFMMod::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); } bool WFMMod::handleMessage(const Message& cmd) { - if (UpChannelizer::MsgChannelizerNotification::match(cmd)) - { - UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd; - qDebug() << "WFMMod::handleMessage: MsgChannelizerNotification"; - - applyChannelSettings(notif.getBasebandSampleRate(), notif.getSampleRate(), notif.getFrequencyOffset()); - - return true; - } - else if (MsgConfigureChannelizer::match(cmd)) + if (MsgConfigureChannelizer::match(cmd)) { MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; qDebug() << "WFMMod::handleMessage: MsgConfigureChannelizer:" - << " getSampleRate: " << cfg.getSampleRate() - << " getCenterFrequency: " << cfg.getCenterFrequency(); + << " getSourceSampleRate: " << cfg.getSourceSampleRate() + << " getSourceCenterFrequency: " << cfg.getSourceCenterFrequency(); - m_channelizer->configure(m_channelizer->getInputMessageQueue(), - cfg.getSampleRate(), - cfg.getCenterFrequency()); + WFMModBaseband::MsgConfigureChannelizer *msg + = WFMModBaseband::MsgConfigureChannelizer::create(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_basebandSource->getInputMessageQueue()->push(msg); return true; } else if (MsgConfigureWFMMod::match(cmd)) { MsgConfigureWFMMod& cfg = (MsgConfigureWFMMod&) cmd; - qDebug() << "NFWFMMod::handleMessage: MsgConfigureWFMMod"; + qDebug() << "WFMMod::handleMessage: MsgConfigureWFMMod"; WFMModSettings settings = cfg.getSettings(); @@ -373,22 +170,14 @@ bool WFMMod::handleMessage(const Message& cmd) return true; } - else if (DSPConfigureAudio::match(cmd)) - { - DSPConfigureAudio& cfg = (DSPConfigureAudio&) cmd; - uint32_t sampleRate = cfg.getSampleRate(); - - qDebug() << "WFMMod::handleMessage: DSPConfigureAudio:" - << " sampleRate: " << sampleRate; - - if (sampleRate != m_audioSampleRate) { - applyAudioSampleRate(sampleRate); - } - - return true; - } else if (DSPSignalNotification::match(cmd)) { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "WFMMod::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + return true; } else @@ -432,56 +221,6 @@ void WFMMod::seekFileStream(int seekPercentage) } } -void WFMMod::applyAudioSampleRate(int sampleRate) -{ - qDebug("WFMMod::applyAudioSampleRate: %d", sampleRate); - - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) sampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, sampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); - m_settingsMutex.unlock(); - - m_audioSampleRate = sampleRate; -} - -void WFMMod::applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force) -{ - qDebug() << "WFMMod::applyChannelSettings:" - << " basebandSampleRate: " << basebandSampleRate - << " outputSampleRate: " << outputSampleRate - << " inputFrequencyOffset: " << inputFrequencyOffset; - - if ((inputFrequencyOffset != m_inputFrequencyOffset) || - (outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_carrierNco.setFreq(inputFrequencyOffset, outputSampleRate); - m_settingsMutex.unlock(); - } - - if ((outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_audioSampleRate / (Real) outputSampleRate; - m_interpolator.create(48, m_audioSampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); - Real lowCut = -(m_settings.m_rfBandwidth / 2.0) / outputSampleRate; - Real hiCut = (m_settings.m_rfBandwidth / 2.0) / outputSampleRate; - m_rfFilter->create_filter(lowCut, hiCut); - m_toneNcoRF.setFreq(m_settings.m_toneFrequency, outputSampleRate); - m_cwKeyer.setSampleRate(outputSampleRate); - m_cwKeyer.reset(); - m_settingsMutex.unlock(); - } - - m_basebandSampleRate = basebandSampleRate; - m_outputSampleRate = outputSampleRate; - m_inputFrequencyOffset = inputFrequencyOffset; -} - void WFMMod::applySettings(const WFMModSettings& settings, bool force) { qDebug() << "WFMMod::applySettings:" @@ -522,34 +261,14 @@ void WFMMod::applySettings(const WFMModSettings& settings, bool force) if ((settings.m_modAFInput != m_settings.m_modAFInput) || force) { reverseAPIKeys.append("modAFInput"); } - - if ((settings.m_afBandwidth != m_settings.m_afBandwidth) || force) - { + if ((settings.m_afBandwidth != m_settings.m_afBandwidth) || force) { reverseAPIKeys.append("afBandwidth"); - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_audioSampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, m_audioSampleRate, settings.m_rfBandwidth / 2.2, 3.0); - m_settingsMutex.unlock(); } - - if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) - { + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { reverseAPIKeys.append("rfBandwidth"); - m_settingsMutex.lock(); - Real lowCut = -(settings.m_rfBandwidth / 2.0) / m_outputSampleRate; - Real hiCut = (settings.m_rfBandwidth / 2.0) / m_outputSampleRate; - m_rfFilter->create_filter(lowCut, hiCut); - m_settingsMutex.unlock(); } - - if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) - { + if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) { reverseAPIKeys.append("toneFrequency"); - m_settingsMutex.lock(); - m_toneNcoRF.setFreq(settings.m_toneFrequency, m_outputSampleRate); - m_settingsMutex.unlock(); } if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force) @@ -557,14 +276,20 @@ void WFMMod::applySettings(const WFMModSettings& settings, bool force) reverseAPIKeys.append("audioDeviceName"); AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); int audioDeviceIndex = audioDeviceManager->getInputDeviceIndex(settings.m_audioDeviceName); - audioDeviceManager->addAudioSource(&m_audioFifo, getInputMessageQueue(), audioDeviceIndex); + audioDeviceManager->addAudioSource(m_basebandSource->getAudioFifo(), getInputMessageQueue(), audioDeviceIndex); uint32_t audioSampleRate = audioDeviceManager->getInputSampleRate(audioDeviceIndex); - if (m_audioSampleRate != audioSampleRate) { - applyAudioSampleRate(audioSampleRate); + if (m_basebandSource->getAudioSampleRate() != audioSampleRate) + { + reverseAPIKeys.append("audioSampleRate"); + DSPConfigureAudio *msg = new DSPConfigureAudio(audioSampleRate, DSPConfigureAudio::AudioInput); + m_basebandSource->getInputMessageQueue()->push(msg); } } + WFMModBaseband::MsgConfigureWFMModBaseband *msg = WFMModBaseband::MsgConfigureWFMModBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); + if (settings.m_useReverseAPI) { bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || @@ -610,7 +335,7 @@ int WFMMod::webapiSettingsGet( webapiFormatChannelSettings(response, m_settings); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = response.getWfmModSettings()->getCwKeyer(); - const CWKeyerSettings& cwKeyerSettings = m_cwKeyer.getSettings(); + const CWKeyerSettings& cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); CWKeyer::webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); return 200; @@ -629,11 +354,11 @@ int WFMMod::webapiSettingsPutPatch( if (channelSettingsKeys.contains("cwKeyer")) { SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = response.getWfmModSettings()->getCwKeyer(); - CWKeyerSettings cwKeyerSettings = m_cwKeyer.getSettings(); + CWKeyerSettings cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); CWKeyer::webapiSettingsPutPatch(channelSettingsKeys, cwKeyerSettings, apiCwKeyerSettings); CWKeyer::MsgConfigureCWKeyer *msgCwKeyer = CWKeyer::MsgConfigureCWKeyer::create(cwKeyerSettings, force); - m_cwKeyer.getInputMessageQueue()->push(msgCwKeyer); + m_basebandSource->getCWKeyer().getInputMessageQueue()->push(msgCwKeyer); if (m_guiMessageQueue) // forward to GUI if any { @@ -776,8 +501,8 @@ void WFMMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& respon void WFMMod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) { response.getWfmModReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); - response.getWfmModReport()->setAudioSampleRate(m_audioSampleRate); - response.getWfmModReport()->setChannelSampleRate(m_outputSampleRate); + response.getWfmModReport()->setAudioSampleRate(m_basebandSource->getAudioSampleRate()); + response.getWfmModReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); } void WFMMod::webapiReverseSendSettings(QList& channelSettingsKeys, const WFMModSettings& settings, bool force) @@ -831,10 +556,10 @@ void WFMMod::webapiReverseSendSettings(QList& channelSettingsKeys, cons if (force) { - const CWKeyerSettings& cwKeyerSettings = m_cwKeyer.getSettings(); + const CWKeyerSettings& cwKeyerSettings = m_basebandSource->getCWKeyer().getSettings(); swgWFMModSettings->setCwKeyer(new SWGSDRangel::SWGCWKeyerSettings()); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = swgWFMModSettings->getCwKeyer(); - m_cwKeyer.webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); + m_basebandSource->getCWKeyer().webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); } QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") @@ -845,13 +570,14 @@ void WFMMod::webapiReverseSendSettings(QList& channelSettingsKeys, cons m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -866,7 +592,7 @@ void WFMMod::webapiReverseSendCWSettings(const CWKeyerSettings& cwKeyerSettings) swgWFMModSettings->setCwKeyer(new SWGSDRangel::SWGCWKeyerSettings()); SWGSDRangel::SWGCWKeyerSettings *apiCwKeyerSettings = swgWFMModSettings->getCwKeyer(); - m_cwKeyer.webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); + m_basebandSource->getCWKeyer().webapiFormatChannelSettings(apiCwKeyerSettings, cwKeyerSettings); QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") .arg(m_settings.m_reverseAPIAddress) @@ -876,13 +602,14 @@ void WFMMod::webapiReverseSendCWSettings(const CWKeyerSettings& cwKeyerSettings) m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -897,10 +624,28 @@ void WFMMod::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("WFMMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("WFMMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); +} + +double WFMMod::getMagSq() const +{ + return m_basebandSource->getMagSq(); +} + +CWKeyer *WFMMod::getCWKeyer() +{ + return &m_basebandSource->getCWKeyer(); +} + +void WFMMod::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); } diff --git a/plugins/channeltx/modwfm/wfmmod.h b/plugins/channeltx/modwfm/wfmmod.h index 97c160172..be662a0c7 100644 --- a/plugins/channeltx/modwfm/wfmmod.h +++ b/plugins/channeltx/modwfm/wfmmod.h @@ -27,23 +27,16 @@ #include "dsp/basebandsamplesource.h" #include "channel/channelapi.h" -#include "dsp/nco.h" -#include "dsp/ncof.h" -#include "dsp/interpolator.h" -#include "dsp/fftfilt.h" -#include "util/movingaverage.h" -#include "dsp/agc.h" -#include "dsp/cwkeyer.h" -#include "audio/audiofifo.h" #include "util/message.h" #include "wfmmodsettings.h" class QNetworkAccessManager; class QNetworkReply; +class QThread; class DeviceAPI; -class ThreadedBasebandSampleSource; -class UpChannelizer; +class CWKeyer; +class WFMModBaseband; class WFMMod : public BasebandSampleSource, public ChannelAPI { Q_OBJECT @@ -72,26 +65,33 @@ public: { } }; + /** + * |<------ Baseband from device (before device soft interpolation) -------------------------->| + * |<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->| + * | ^-------------------------------| + * | | Source CF + * | | Source SR | + */ class MsgConfigureChannelizer : public Message { MESSAGE_CLASS_DECLARATION public: - int getSampleRate() const { return m_sampleRate; } - int getCenterFrequency() const { return m_centerFrequency; } + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } - static MsgConfigureChannelizer* create(int sampleRate, int centerFrequency) + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) { - return new MsgConfigureChannelizer(sampleRate, centerFrequency); + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); } private: - int m_sampleRate; - int m_centerFrequency; + int m_sourceSampleRate; + int m_sourceCenterFrequency; - MsgConfigureChannelizer(int sampleRate, int centerFrequency) : + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : Message(), - m_sampleRate(sampleRate), - m_centerFrequency(centerFrequency) + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) { } }; @@ -206,10 +206,9 @@ public: ~WFMMod(); virtual void destroy() { delete this; } - virtual void pull(Sample& sample); - virtual void pullAudio(int nbSamples); virtual void start(); virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); virtual bool handleMessage(const Message& cmd); virtual void getIdentifier(QString& id) { id = objectName(); } @@ -252,9 +251,9 @@ public: const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response); - double getMagSq() const { return m_magsq; } - - CWKeyer *getCWKeyer() { return &m_cwKeyer; } + double getMagSq() const; + CWKeyer *getCWKeyer(); + void setLevelMeter(QObject *levelMeter); static const QString m_channelIdURI; static const QString m_channelId; @@ -276,36 +275,10 @@ private: }; DeviceAPI* m_deviceAPI; - ThreadedBasebandSampleSource* m_threadedChannelizer; - UpChannelizer* m_channelizer; - - int m_basebandSampleRate; - int m_outputSampleRate; - int m_inputFrequencyOffset; + QThread *m_thread; + WFMModBaseband* m_basebandSource; WFMModSettings m_settings; - NCO m_carrierNco; - NCOF m_toneNcoRF; - float m_modPhasor; //!< baseband modulator phasor - Complex m_modSample; - Interpolator m_interpolator; - Real m_interpolatorDistance; - Real m_interpolatorDistanceRemain; - bool m_interpolatorConsumed; - - fftfilt* m_rfFilter; - static const int m_rfFilterFFTLength; - fftfilt::cmplx *m_rfFilterBuffer; - int m_rfFilterBufferIndex; - - double m_magsq; - MovingAverageUtil m_movingAverage; - - quint32 m_audioSampleRate; - AudioVector m_audioBuffer; - uint m_audioBufferFill; - AudioFifo m_audioFifo; - SampleVector m_sampleBuffer; QMutex m_settingsMutex; @@ -315,21 +288,12 @@ private: quint32 m_recordLength; //!< record length in seconds computed from file size int m_sampleRate; - quint32 m_levelCalcCount; - Real m_peakLevel; - Real m_levelSum; - CWKeyer m_cwKeyer; - QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; static const int m_levelNbSamples; - void applyAudioSampleRate(int sampleRate); - void applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force = false); void applySettings(const WFMModSettings& settings, bool force = false); - void pullAF(Complex& sample); - void calculateLevel(const Real& sample); void openFileStream(); void seekFileStream(int seekPercentage); void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); diff --git a/plugins/channeltx/modwfm/wfmmodbaseband.cpp b/plugins/channeltx/modwfm/wfmmodbaseband.cpp new file mode 100644 index 000000000..0582701c6 --- /dev/null +++ b/plugins/channeltx/modwfm/wfmmodbaseband.cpp @@ -0,0 +1,218 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "wfmmodbaseband.h" + +MESSAGE_CLASS_DEFINITION(WFMModBaseband::MsgConfigureWFMModBaseband, Message) +MESSAGE_CLASS_DEFINITION(WFMModBaseband::MsgConfigureChannelizer, Message) + +WFMModBaseband::WFMModBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpSampleChannelizer(&m_source); + + qDebug("WFMModBaseband::WFMModBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &WFMModBaseband::handleData, + Qt::QueuedConnection + ); + + DSPEngine::instance()->getAudioDeviceManager()->addAudioSource(m_source.getAudioFifo(), getInputMessageQueue()); + m_source.applyAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getInputSampleRate()); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +WFMModBaseband::~WFMModBaseband() +{ + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSource(m_source.getAudioFifo()); + delete m_channelizer; +} + +void WFMModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void WFMModBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void WFMModBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + Real rmsLevel, peakLevel, numSamples; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); +} + +void WFMModBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void WFMModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool WFMModBaseband::handleMessage(const Message& cmd) +{ + if (DSPConfigureAudio::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPConfigureAudio& cfg = (DSPConfigureAudio&) cmd; + uint32_t sampleRate = cfg.getSampleRate(); + DSPConfigureAudio::AudioType audioType = cfg.getAudioType(); + + qDebug() << "WFMModBaseband::handleMessage: DSPConfigureAudio:" + << " sampleRate: " << sampleRate + << " audioType: " << audioType; + + if (audioType == DSPConfigureAudio::AudioInput) + { + if (sampleRate != m_source.getAudioSampleRate()) { + m_source.applyAudioSampleRate(sampleRate); + } + } + + return true; + } + else if (MsgConfigureWFMModBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureWFMModBaseband& cfg = (MsgConfigureWFMModBaseband&) cmd; + qDebug() << "WFMModBaseband::handleMessage: MsgConfigureWFMModBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureChannelizer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; + qDebug() << "WFMModBaseband::handleMessage: MsgConfigureChannelizer" + << "(requested) sourceSampleRate: " << cfg.getSourceSampleRate() + << "(requested) sourceCenterFrequency: " << cfg.getSourceCenterFrequency(); + m_channelizer->setChannelization(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "WFMModBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (CWKeyer::MsgConfigureCWKeyer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + const CWKeyer::MsgConfigureCWKeyer& cfg = (CWKeyer::MsgConfigureCWKeyer&) cmd; + CWKeyer::MsgConfigureCWKeyer *notif = new CWKeyer::MsgConfigureCWKeyer(cfg); + CWKeyer& cwKeyer = m_source.getCWKeyer(); + cwKeyer.getInputMessageQueue()->push(notif); + + return true; + } + else + { + return false; + } +} + +void WFMModBaseband::applySettings(const WFMModSettings& settings, bool force) +{ + m_source.applySettings(settings, force); + m_settings = settings; +} + +int WFMModBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} \ No newline at end of file diff --git a/plugins/channeltx/modwfm/wfmmodbaseband.h b/plugins/channeltx/modwfm/wfmmodbaseband.h new file mode 100644 index 000000000..2b307a826 --- /dev/null +++ b/plugins/channeltx/modwfm/wfmmodbaseband.h @@ -0,0 +1,121 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_WFMMODBASEBAND_H +#define INCLUDE_WFMMODBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "wfmmodsource.h" + +class UpSampleChannelizer; + +class WFMModBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureWFMModBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const WFMModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureWFMModBaseband* create(const WFMModSettings& settings, bool force) + { + return new MsgConfigureWFMModBaseband(settings, force); + } + + private: + WFMModSettings m_settings; + bool m_force; + + MsgConfigureWFMModBaseband(const WFMModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureChannelizer : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } + + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) + { + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); + } + + private: + int m_sourceSampleRate; + int m_sourceCenterFrequency; + + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : + Message(), + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) + { } + }; + + WFMModBaseband(); + ~WFMModBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + CWKeyer& getCWKeyer() { return m_source.getCWKeyer(); } + double getMagSq() const { return m_source.getMagSq(); } + unsigned int getAudioSampleRate() const { return m_source.getAudioSampleRate(); } + int getChannelSampleRate() const; + void setInputFileStream(std::ifstream *ifstream) { m_source.setInputFileStream(ifstream); } + AudioFifo *getAudioFifo() { return m_source.getAudioFifo(); } + +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + SampleSourceFifo m_sampleFifo; + UpSampleChannelizer *m_channelizer; + WFMModSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + WFMModSettings m_settings; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const WFMModSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + + +#endif // INCLUDE_WFMMODBASEBAND_H diff --git a/plugins/channeltx/modwfm/wfmmodgui.cpp b/plugins/channeltx/modwfm/wfmmodgui.cpp index c3ae5d565..1b3d33a54 100644 --- a/plugins/channeltx/modwfm/wfmmodgui.cpp +++ b/plugins/channeltx/modwfm/wfmmodgui.cpp @@ -22,12 +22,11 @@ #include #include "device/deviceuiset.h" -#include "dsp/upchannelizer.h" -#include "dsp/threadedbasebandsamplesource.h" #include "plugin/pluginapi.h" #include "util/simpleserializer.h" #include "util/db.h" #include "dsp/dspengine.h" +#include "dsp/cwkeyer.h" #include "gui/crightclickenabler.h" #include "gui/audioselectdialog.h" #include "gui/basicchannelsettingsdialog.h" @@ -382,7 +381,7 @@ WFMModGUI::WFMModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSam m_settings.setCWKeyerGUI(ui->cwKeyerGUI); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); - connect(m_wfmMod, SIGNAL(levelChanged(qreal, qreal, int)), ui->volumeMeter, SLOT(levelChanged(qreal, qreal, int))); + m_wfmMod->setLevelMeter(ui->volumeMeter); displaySettings(); applySettings(true); diff --git a/plugins/channeltx/modwfm/wfmmodplugin.cpp b/plugins/channeltx/modwfm/wfmmodplugin.cpp index 39d074c6b..3deb38f36 100644 --- a/plugins/channeltx/modwfm/wfmmodplugin.cpp +++ b/plugins/channeltx/modwfm/wfmmodplugin.cpp @@ -27,7 +27,7 @@ const PluginDescriptor WFMModPlugin::m_pluginDescriptor = { QString("WFM Modulator"), - QString("4.11.6"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/channeltx/modwfm/wfmmodsource.cpp b/plugins/channeltx/modwfm/wfmmodsource.cpp new file mode 100644 index 000000000..cb370d7bb --- /dev/null +++ b/plugins/channeltx/modwfm/wfmmodsource.cpp @@ -0,0 +1,298 @@ +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "wfmmodsource.h" + +const int WFMModSource::m_rfFilterFFTLength = 1024; +const int WFMModSource::m_levelNbSamples = 480; // every 10ms + +WFMModSource::WFMModSource() : + m_channelSampleRate(384000), + m_channelFrequencyOffset(0), + m_modPhasor(0.0f), + m_audioFifo(4800), + m_levelCalcCount(0), + m_peakLevel(0.0f), + m_levelSum(0.0f), + m_ifstream(nullptr), + m_audioSampleRate(48000) +{ + m_rfFilter = new fftfilt(-62500.0 / 384000.0, 62500.0 / 384000.0, m_rfFilterFFTLength); + m_rfFilterBuffer = new Complex[m_rfFilterFFTLength]; + std::fill(m_rfFilterBuffer, m_rfFilterBuffer+m_rfFilterFFTLength, Complex{0,0}); + m_rfFilterBufferIndex = 0; + m_audioBuffer.resize(1<<14); + m_audioBufferFill = 0; + m_magsq = 0.0; + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +WFMModSource::~WFMModSource() +{ + delete m_rfFilter; + delete[] m_rfFilterBuffer; +} + +void WFMModSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void WFMModSource::pullOne(Sample& sample) +{ + if (m_settings.m_channelMute) + { + sample.m_real = 0.0f; + sample.m_imag = 0.0f; + return; + } + + Complex ci, ri; + fftfilt::cmplx *rf; + int rf_out; + Real t; + + if ((m_settings.m_modAFInput == WFMModSettings::WFMModInputFile) + || (m_settings.m_modAFInput == WFMModSettings::WFMModInputAudio)) + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ri)) + { + pullAF(t); + calculateLevel(t); + m_modSample.real(t); + m_modSample.imag(0.0f); + m_audioBufferFill++; + } + + t = ri.real(); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + else + { + pullAF(t); + calculateLevel(t); + } + + m_modPhasor += (m_settings.m_fmDeviation / (float) m_channelSampleRate) * t * M_PI * 2.0f; + + // limit phasor range to ]-pi,pi] + if (m_modPhasor > M_PI) { + m_modPhasor -= (2.0f * M_PI); + } + + ci.real(cos(m_modPhasor) * 0.891235351562f * SDR_TX_SCALEF); // -1 dB + ci.imag(sin(m_modPhasor) * 0.891235351562f * SDR_TX_SCALEF); + + // RF filtering + rf_out = m_rfFilter->runFilt(ci, &rf); + + if (rf_out > 0) + { + memcpy((void *) m_rfFilterBuffer, (const void *) rf, rf_out*sizeof(Complex)); + m_rfFilterBufferIndex = 0; + + } + + ci = m_rfFilterBuffer[m_rfFilterBufferIndex] * m_carrierNco.nextIQ(); // shift to carrier frequency + m_rfFilterBufferIndex++; + + double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); + magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + + sample.m_real = (FixReal) ci.real(); + sample.m_imag = (FixReal) ci.imag(); +} + +void WFMModSource::prefetch(unsigned int nbSamples) +{ + unsigned int nbSamplesAudio = nbSamples * ((Real) m_audioSampleRate / (Real) m_channelSampleRate); + pullAudio(nbSamplesAudio); +} + +void WFMModSource::pullAudio(unsigned int nbSamplesAudio) +{ + if (nbSamplesAudio > m_audioBuffer.size()) { + m_audioBuffer.resize(nbSamplesAudio); + } + + m_audioFifo.read(reinterpret_cast(&m_audioBuffer[0]), nbSamplesAudio); + m_audioBufferFill = 0; +} + +void WFMModSource::pullAF(Real& sample) +{ + switch (m_settings.m_modAFInput) + { + case WFMModSettings::WFMModInputTone: + sample = m_toneNco.next() * m_settings.m_volumeFactor; + break; + case WFMModSettings::WFMModInputFile: + // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw + // ffplay -f f32le -ar 48k -ac 1 f4exb_call.raw + if (m_ifstream && m_ifstream->is_open()) + { + if (m_ifstream->eof()) + { + if (m_settings.m_playLoop) + { + m_ifstream->clear(); + m_ifstream->seekg(0, std::ios::beg); + } + } + + if (m_ifstream->eof()) + { + sample = 0.0f; + } + else + { + Real s; + m_ifstream->read(reinterpret_cast(&s), sizeof(Real)); + sample = s * m_settings.m_volumeFactor; + } + } + else + { + sample = 0.0f; + } + break; + case WFMModSettings::WFMModInputAudio: + { + sample = ((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f) * m_settings.m_volumeFactor; + } + break; + case WFMModSettings::WFMModInputCWTone: + Real fadeFactor; + + if (m_cwKeyer.getSample()) + { + m_cwKeyer.getCWSmoother().getFadeSample(true, fadeFactor); + sample = m_toneNco.next() * m_settings.m_volumeFactor * fadeFactor; + } + else + { + if (m_cwKeyer.getCWSmoother().getFadeSample(false, fadeFactor)) + { + sample = m_toneNco.next() * m_settings.m_volumeFactor * fadeFactor; + } + else + { + sample = 0.0f; + m_toneNco.setPhase(0); + } + } + break; + case WFMModSettings::WFMModInputNone: + default: + sample = 0.0f; + break; + } +} + +void WFMModSource::calculateLevel(const Real& sample) +{ + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), sample); + m_levelSum += sample * sample; + m_levelCalcCount++; + } + else + { + m_rmsLevel = sqrt(m_levelSum / m_levelNbSamples); + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void WFMModSource::applyAudioSampleRate(unsigned int sampleRate) +{ + qDebug("WFMModSource::applyAudioSampleRate: %d", sampleRate); + + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) sampleRate / (Real) m_channelSampleRate; + m_interpolator.create(48, sampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); + + m_audioSampleRate = sampleRate; +} + +void WFMModSource::applySettings(const WFMModSettings& settings, bool force) +{ + if ((settings.m_afBandwidth != m_settings.m_afBandwidth) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_audioSampleRate / (Real) m_channelSampleRate; + m_interpolator.create(48, m_audioSampleRate, settings.m_afBandwidth / 2.2, 3.0); + } + + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) + { + Real lowCut = -(settings.m_rfBandwidth / 2.0) / m_channelSampleRate; + Real hiCut = (settings.m_rfBandwidth / 2.0) / m_channelSampleRate; + m_rfFilter->create_filter(lowCut, hiCut); + } + + if ((settings.m_toneFrequency != m_settings.m_toneFrequency) || force) { + m_toneNco.setFreq(settings.m_toneFrequency, m_channelSampleRate); + } + + m_settings = settings; +} + +void WFMModSource::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "WFMModSource::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((channelFrequencyOffset != m_channelFrequencyOffset) + || (channelSampleRate != m_channelSampleRate) || force) { + m_carrierNco.setFreq(channelFrequencyOffset, channelSampleRate); + } + + if ((channelSampleRate != m_channelSampleRate) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_audioSampleRate / (Real) channelSampleRate; + m_interpolator.create(48, m_audioSampleRate, m_settings.m_afBandwidth / 2.2, 3.0); + Real lowCut = -(m_settings.m_rfBandwidth / 2.0) / channelSampleRate; + Real hiCut = (m_settings.m_rfBandwidth / 2.0) / channelSampleRate; + m_rfFilter->create_filter(lowCut, hiCut); + m_toneNco.setFreq(m_settings.m_toneFrequency, channelSampleRate); + m_cwKeyer.setSampleRate(channelSampleRate); + m_cwKeyer.reset(); + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} \ No newline at end of file diff --git a/plugins/channeltx/modwfm/wfmmodsource.h b/plugins/channeltx/modwfm/wfmmodsource.h new file mode 100644 index 000000000..f75378ecd --- /dev/null +++ b/plugins/channeltx/modwfm/wfmmodsource.h @@ -0,0 +1,108 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_WFMMODSOURCE_H +#define INCLUDE_WFMMODSOURCE_H + +#include + +#include +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/nco.h" +#include "dsp/ncof.h" +#include "dsp/interpolator.h" +#include "dsp/fftfilt.h" +#include "util/movingaverage.h" +#include "dsp/cwkeyer.h" +#include "audio/audiofifo.h" + +#include "wfmmodsettings.h" + +class WFMModSource : public ChannelSampleSource +{ +public: + WFMModSource(); + virtual ~WFMModSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples); + + void setInputFileStream(std::ifstream *ifstream) { m_ifstream = ifstream; } + AudioFifo *getAudioFifo() { return &m_audioFifo; } + void applyAudioSampleRate(unsigned int sampleRate); + void applyFeedbackAudioSampleRate(unsigned int sampleRate); + unsigned int getAudioSampleRate() const { return m_audioSampleRate; } + CWKeyer& getCWKeyer() { return m_cwKeyer; } + double getMagSq() const { return m_magsq; } + void getLevels(Real& rmsLevel, Real& peakLevel, Real& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevel; + numSamples = m_levelNbSamples; + } + void applySettings(const WFMModSettings& settings, bool force = false); + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + +private: + int m_channelSampleRate; + int m_channelFrequencyOffset; + WFMModSettings m_settings; + + NCO m_carrierNco; + NCOF m_toneNco; + float m_modPhasor; //!< baseband modulator phasor + Complex m_modSample; + + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + bool m_interpolatorConsumed; + + fftfilt* m_rfFilter; + static const int m_rfFilterFFTLength; + fftfilt::cmplx *m_rfFilterBuffer; + int m_rfFilterBufferIndex; + + double m_magsq; + MovingAverageUtil m_movingAverage; + + quint32 m_audioSampleRate; + AudioVector m_audioBuffer; + uint m_audioBufferFill; + AudioFifo m_audioFifo; + + quint32 m_levelCalcCount; + Real m_rmsLevel; + Real m_peakLevelOut; + Real m_peakLevel; + Real m_levelSum; + + std::ifstream *m_ifstream; + CWKeyer m_cwKeyer; + + static const int m_levelNbSamples; + + void processOneSample(Complex& ci); + void pullAF(Real& sample); + void pullAudio(unsigned int nbSamples); + void calculateLevel(const Real& sample); +}; + +#endif // INCLUDE_WFMMODSOURCE_H diff --git a/plugins/channeltx/modwfm/wfmmodwebapiadapter.cpp b/plugins/channeltx/modwfm/wfmmodwebapiadapter.cpp index 2a0ccf0d6..1665e8309 100644 --- a/plugins/channeltx/modwfm/wfmmodwebapiadapter.cpp +++ b/plugins/channeltx/modwfm/wfmmodwebapiadapter.cpp @@ -16,6 +16,7 @@ /////////////////////////////////////////////////////////////////////////////////// #include "SWGChannelSettings.h" +#include "dsp/cwkeyer.h" #include "wfmmod.h" #include "wfmmodwebapiadapter.h" diff --git a/plugins/channeltx/remotesource/CMakeLists.txt b/plugins/channeltx/remotesource/CMakeLists.txt index 5533824db..245555290 100644 --- a/plugins/channeltx/remotesource/CMakeLists.txt +++ b/plugins/channeltx/remotesource/CMakeLists.txt @@ -10,7 +10,9 @@ else() endif() set(remotesource_SOURCES - remotesource.cpp + remotesource.cpp + remotesourcebaseband.cpp + remotesourcesource.cpp remotesourcethread.cpp remotesourceplugin.cpp remotesourcesettings.cpp @@ -18,7 +20,9 @@ set(remotesource_SOURCES ) set(remotesource_HEADERS - remotesource.h + remotesource.h + remotesourcebaseband.h + remotesourcesource.h remotesourcethread.h remotesourceplugin.h remotesourcesettings.h diff --git a/plugins/channeltx/remotesource/readme.md b/plugins/channeltx/remotesource/readme.md index 5dc96ff83..9675b79b5 100644 --- a/plugins/channeltx/remotesource/readme.md +++ b/plugins/channeltx/remotesource/readme.md @@ -24,11 +24,13 @@ Local port from where the I/Q samples are fetched via UDP

3: Validation button

-When the return key is hit within the address (1) or port (2) the changes are effective immediately. You can also use this button to set again these values. +When the return key is hit within the address (1) or port (2) the changes are effective immediately. You can also use this button to set again these values.

4: Stream sample rate

-Stream sample rate as specified in the stream meta data +Stream sample rate as specified in the stream meta data. Interpolation may occur to match the baseband sample rate. This will be done around the baseband center frequency (no NCO shift). Decimation is not provisionned so unpredictable results may occur if the remote stream sample rate is larger than the baseband sample rate. + +To minimize processing an exact match of baseband sample rate and remote stream sample rate is recommended. If this is not possible then a power of two ratio is still preferable.

5: Stream status

@@ -49,7 +51,7 @@ The color of the icon indicates stream status: - Green: all original blocks have been received for all frames during the last polling timeframe (ex: 136) - No color: some original blocks were reconstructed from FEC blocks for some frames during the last polling timeframe (ex: between 128 and 135) - Red: some original blocks were definitely lost for some frames during the last polling timeframe (ex: less than 128) - +

5.3: Actual stream sample rate

This is the sample rate calculated using the counter of samples between two consecutive polls diff --git a/plugins/channeltx/remotesource/remotesource.cpp b/plugins/channeltx/remotesource/remotesource.cpp index de9266655..f537eb9d2 100644 --- a/plugins/channeltx/remotesource/remotesource.cpp +++ b/plugins/channeltx/remotesource/remotesource.cpp @@ -17,20 +17,11 @@ #include "remotesource.h" -#if (defined _WIN32_) || (defined _MSC_VER) -#include "windows_time.h" -#include -#else -#include -#include -#endif -#include -#include - #include #include #include #include +#include #include "SWGChannelSettings.h" #include "SWGChannelReport.h" @@ -38,12 +29,10 @@ #include "dsp/devicesamplesink.h" #include "device/deviceapi.h" -#include "dsp/upchannelizer.h" -#include "dsp/threadedbasebandsamplesource.h" +#include "util/timeutil.h" -#include "remotesourcethread.h" +#include "remotesourcebaseband.h" -MESSAGE_CLASS_DEFINITION(RemoteSource::MsgSampleRateNotification, Message) MESSAGE_CLASS_DEFINITION(RemoteSource::MsgConfigureRemoteSource, Message) MESSAGE_CLASS_DEFINITION(RemoteSource::MsgQueryStreamData, Message) MESSAGE_CLASS_DEFINITION(RemoteSource::MsgReportStreamData, Message) @@ -53,21 +42,17 @@ const QString RemoteSource::m_channelId ="RemoteSource"; RemoteSource::RemoteSource(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), - m_deviceAPI(deviceAPI), - m_sourceThread(0), - m_running(false), - m_nbCorrectableErrors(0), - m_nbUncorrectableErrors(0) + m_deviceAPI(deviceAPI) { setObjectName(m_channelId); - connect(&m_dataQueue, SIGNAL(dataBlockEnqueued()), this, SLOT(handleData()), Qt::QueuedConnection); - m_cm256p = m_cm256.isInitialized() ? &m_cm256 : 0; - m_currentMeta.init(); + m_thread = new QThread(this); + m_basebandSource = new RemoteSourceBaseband(); + m_basebandSource->moveToThread(m_thread); - m_channelizer = new UpChannelizer(this); - m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this); - m_deviceAPI->addChannelSource(m_threadedChannelizer); + applySettings(m_settings, true); + + m_deviceAPI->addChannelSource(this); m_deviceAPI->addChannelSourceAPI(this); m_networkManager = new QNetworkAccessManager(); @@ -79,78 +64,35 @@ RemoteSource::~RemoteSource() disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); delete m_networkManager; m_deviceAPI->removeChannelSourceAPI(this); - m_deviceAPI->removeChannelSource(m_threadedChannelizer); - delete m_threadedChannelizer; - delete m_channelizer; -} - -void RemoteSource::pull(Sample& sample) -{ - m_dataReadQueue.readSample(sample, true); // true is scale for Tx -} - -void RemoteSource::pullAudio(int nbSamples) -{ - (void) nbSamples; + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; } void RemoteSource::start() { qDebug("RemoteSource::start"); - - if (m_running) { - stop(); - } - - m_sourceThread = new RemoteSourceThread(&m_dataQueue); - m_sourceThread->startStop(true); - m_sourceThread->dataBind(m_settings.m_dataAddress, m_settings.m_dataPort); - m_running = true; + m_basebandSource->reset(); + m_thread->start(); + RemoteSourceBaseband::MsgConfigureRemoteSourceWork *msg = RemoteSourceBaseband::MsgConfigureRemoteSourceWork::create(true); + m_basebandSource->getInputMessageQueue()->push(msg); } void RemoteSource::stop() { qDebug("RemoteSource::stop"); - - if (m_sourceThread != 0) - { - m_sourceThread->startStop(false); - m_sourceThread->deleteLater(); - m_sourceThread = 0; - } - - m_running = false; + m_thread->exit(); + m_thread->wait(); } -void RemoteSource::setDataLink(const QString& dataAddress, uint16_t dataPort) +void RemoteSource::pull(SampleVector::iterator& begin, unsigned int nbSamples) { - RemoteSourceSettings settings = m_settings; - settings.m_dataAddress = dataAddress; - settings.m_dataPort = dataPort; - - MsgConfigureRemoteSource *msg = MsgConfigureRemoteSource::create(settings, false); - m_inputMessageQueue.push(msg); + m_basebandSource->pull(begin, nbSamples); } bool RemoteSource::handleMessage(const Message& cmd) { - if (UpChannelizer::MsgChannelizerNotification::match(cmd)) - { - UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd; - qDebug() << "RemoteSource::handleMessage: MsgChannelizerNotification:" - << " basebandSampleRate: " << notif.getBasebandSampleRate() - << " outputSampleRate: " << notif.getSampleRate() - << " inputFrequencyOffset: " << notif.getFrequencyOffset(); - - if (m_guiMessageQueue) - { - MsgSampleRateNotification *msg = MsgSampleRateNotification::create(notif.getBasebandSampleRate()); - m_guiMessageQueue->push(msg); - } - - return true; - } - else if (MsgConfigureRemoteSource::match(cmd)) + if (MsgConfigureRemoteSource::match(cmd)) { MsgConfigureRemoteSource& cfg = (MsgConfigureRemoteSource&) cmd; qDebug() << "MsgConfigureRemoteSource::handleMessage: MsgConfigureRemoteSource"; @@ -162,21 +104,22 @@ bool RemoteSource::handleMessage(const Message& cmd) { if (m_guiMessageQueue) { - struct timeval tv; - gettimeofday(&tv, 0); + uint64_t nowus = TimeUtil::nowus(); + RemoteDataReadQueue& dataReadQueue = m_basebandSource->getDataQueue(); + const RemoteMetaDataFEC& currentMeta = m_basebandSource->getRemoteMetaDataFEC(); MsgReportStreamData *msg = MsgReportStreamData::create( - tv.tv_sec, - tv.tv_usec, - m_dataReadQueue.size(), - m_dataReadQueue.length(), - m_dataReadQueue.readSampleCount(), - m_nbCorrectableErrors, - m_nbUncorrectableErrors, - m_currentMeta.m_nbOriginalBlocks, - m_currentMeta.m_nbFECBlocks, - m_currentMeta.m_centerFrequency, - m_currentMeta.m_sampleRate); + nowus / 1000000U, + nowus % 1000000U, + dataReadQueue.size(), + dataReadQueue.length(), + dataReadQueue.readSampleCount(), + m_basebandSource->getNbCorrectableErrors(), + m_basebandSource->getNbUncorrectableErrors(), + currentMeta.m_nbOriginalBlocks, + currentMeta.m_nbFECBlocks, + currentMeta.m_centerFrequency, + currentMeta.m_sampleRate); m_guiMessageQueue->push(msg); } @@ -213,28 +156,28 @@ void RemoteSource::applySettings(const RemoteSourceSettings& settings, bool forc qDebug() << "RemoteSource::applySettings:" << " m_dataAddress: " << settings.m_dataAddress << " m_dataPort: " << settings.m_dataPort + << " m_rgbColor: " << settings.m_rgbColor + << " m_title: " << settings.m_title + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex + << " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex + << " m_reverseAPIPort: " << settings.m_reverseAPIPort << " force: " << force; bool change = false; QList reverseAPIKeys; - if ((m_settings.m_dataAddress != settings.m_dataAddress) || force) - { + if ((m_settings.m_dataAddress != settings.m_dataAddress) || force) { reverseAPIKeys.append("dataAddress"); - change = true; } - if ((m_settings.m_dataPort != settings.m_dataPort) || force) - { + if ((m_settings.m_dataPort != settings.m_dataPort) || force) { reverseAPIKeys.append("dataPort"); - change = true; } - if (change && m_sourceThread) - { - reverseAPIKeys.append("sourceThread"); - m_sourceThread->dataBind(settings.m_dataAddress, settings.m_dataPort); - } + RemoteSourceBaseband::MsgConfigureRemoteSourceBaseband *msg = RemoteSourceBaseband::MsgConfigureRemoteSourceBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); if (settings.m_useReverseAPI) { @@ -249,142 +192,6 @@ void RemoteSource::applySettings(const RemoteSourceSettings& settings, bool forc m_settings = settings; } -void RemoteSource::handleDataBlock(RemoteDataBlock* dataBlock) -{ - (void) dataBlock; - if (dataBlock->m_rxControlBlock.m_blockCount < RemoteNbOrginalBlocks) - { - qWarning("RemoteSource::handleDataBlock: incomplete data block: not processing"); - } - else - { - int blockCount = 0; - - for (int blockIndex = 0; blockIndex < 256; blockIndex++) - { - if ((blockIndex == 0) && (dataBlock->m_rxControlBlock.m_metaRetrieved)) - { - m_cm256DescriptorBlocks[blockCount].Index = 0; - m_cm256DescriptorBlocks[blockCount].Block = (void *) &(dataBlock->m_superBlocks[0].m_protectedBlock); - blockCount++; - } - else if (dataBlock->m_superBlocks[blockIndex].m_header.m_blockIndex != 0) - { - m_cm256DescriptorBlocks[blockCount].Index = dataBlock->m_superBlocks[blockIndex].m_header.m_blockIndex; - m_cm256DescriptorBlocks[blockCount].Block = (void *) &(dataBlock->m_superBlocks[blockIndex].m_protectedBlock); - blockCount++; - } - } - - //qDebug("RemoteSource::handleDataBlock: frame: %u blocks: %d", dataBlock.m_rxControlBlock.m_frameIndex, blockCount); - - // Need to use the CM256 recovery - if (m_cm256p &&(dataBlock->m_rxControlBlock.m_originalCount < RemoteNbOrginalBlocks)) - { - qDebug("RemoteSource::handleDataBlock: %d recovery blocks", dataBlock->m_rxControlBlock.m_recoveryCount); - CM256::cm256_encoder_params paramsCM256; - paramsCM256.BlockBytes = sizeof(RemoteProtectedBlock); // never changes - paramsCM256.OriginalCount = RemoteNbOrginalBlocks; // never changes - - if (m_currentMeta.m_tv_sec == 0) { - paramsCM256.RecoveryCount = dataBlock->m_rxControlBlock.m_recoveryCount; - } else { - paramsCM256.RecoveryCount = m_currentMeta.m_nbFECBlocks; - } - - // update counters - if (dataBlock->m_rxControlBlock.m_originalCount < RemoteNbOrginalBlocks - paramsCM256.RecoveryCount) { - m_nbUncorrectableErrors += RemoteNbOrginalBlocks - paramsCM256.RecoveryCount - dataBlock->m_rxControlBlock.m_originalCount; - } else { - m_nbCorrectableErrors += dataBlock->m_rxControlBlock.m_recoveryCount; - } - - if (m_cm256.cm256_decode(paramsCM256, m_cm256DescriptorBlocks)) // CM256 decode - { - qWarning() << "RemoteSource::handleDataBlock: decode CM256 error:" - << " m_originalCount: " << dataBlock->m_rxControlBlock.m_originalCount - << " m_recoveryCount: " << dataBlock->m_rxControlBlock.m_recoveryCount; - } - else - { - for (int ir = 0; ir < dataBlock->m_rxControlBlock.m_recoveryCount; ir++) // restore missing blocks - { - int recoveryIndex = RemoteNbOrginalBlocks - dataBlock->m_rxControlBlock.m_recoveryCount + ir; - int blockIndex = m_cm256DescriptorBlocks[recoveryIndex].Index; - RemoteProtectedBlock *recoveredBlock = - (RemoteProtectedBlock *) m_cm256DescriptorBlocks[recoveryIndex].Block; - memcpy((void *) &(dataBlock->m_superBlocks[blockIndex].m_protectedBlock), recoveredBlock, sizeof(RemoteProtectedBlock)); - if ((blockIndex == 0) && !dataBlock->m_rxControlBlock.m_metaRetrieved) { - dataBlock->m_rxControlBlock.m_metaRetrieved = true; - } - } - } - } - - // Validate block zero and retrieve its data - if (dataBlock->m_rxControlBlock.m_metaRetrieved) - { - RemoteMetaDataFEC *metaData = (RemoteMetaDataFEC *) &(dataBlock->m_superBlocks[0].m_protectedBlock); - boost::crc_32_type crc32; - crc32.process_bytes(metaData, sizeof(RemoteMetaDataFEC)-4); - - if (crc32.checksum() == metaData->m_crc32) - { - if (!(m_currentMeta == *metaData)) - { - printMeta("RemoteSource::handleDataBlock", metaData); - - if (m_currentMeta.m_sampleRate != metaData->m_sampleRate) - { - m_channelizer->configure(m_channelizer->getInputMessageQueue(), metaData->m_sampleRate, 0); - m_dataReadQueue.setSize(calculateDataReadQueueSize(metaData->m_sampleRate)); - } - } - - m_currentMeta = *metaData; - } - else - { - qWarning() << "RemoteSource::handleDataBlock: recovered meta: invalid CRC32"; - } - } - - m_dataReadQueue.push(dataBlock); // Push into R/W buffer - } -} - -void RemoteSource::handleData() -{ - RemoteDataBlock* dataBlock; - - while (m_running && ((dataBlock = m_dataQueue.pop()) != 0)) { - handleDataBlock(dataBlock); - } -} - -void RemoteSource::printMeta(const QString& header, RemoteMetaDataFEC *metaData) -{ - qDebug().noquote() << header << ": " - << "|" << metaData->m_centerFrequency - << ":" << metaData->m_sampleRate - << ":" << (int) (metaData->m_sampleBytes & 0xF) - << ":" << (int) metaData->m_sampleBits - << ":" << (int) metaData->m_nbOriginalBlocks - << ":" << (int) metaData->m_nbFECBlocks - << "|" << metaData->m_tv_sec - << ":" << metaData->m_tv_usec - << "|"; -} - -uint32_t RemoteSource::calculateDataReadQueueSize(int sampleRate) -{ - // scale for 20 blocks at 48 kS/s. Take next even number. - uint32_t maxSize = sampleRate / 2400; - maxSize = (maxSize % 2 == 0) ? maxSize : maxSize + 1; - qDebug("RemoteSource::calculateDataReadQueueSize: set max queue size to %u blocks", maxSize); - return maxSize; -} - int RemoteSource::webapiSettingsGet( SWGSDRangel::SWGChannelSettings& response, QString& errorMessage) @@ -505,20 +312,21 @@ void RemoteSource::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& void RemoteSource::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) { - struct timeval tv; - gettimeofday(&tv, 0); + uint64_t nowus = TimeUtil::nowus(); + RemoteDataReadQueue& dataReadQueue = m_basebandSource->getDataQueue(); + const RemoteMetaDataFEC& currentMeta = m_basebandSource->getRemoteMetaDataFEC(); - response.getRemoteSourceReport()->setTvSec(tv.tv_sec); - response.getRemoteSourceReport()->setTvUSec(tv.tv_usec); - response.getRemoteSourceReport()->setQueueSize(m_dataReadQueue.size()); - response.getRemoteSourceReport()->setQueueLength(m_dataReadQueue.length()); - response.getRemoteSourceReport()->setSamplesCount(m_dataReadQueue.readSampleCount()); - response.getRemoteSourceReport()->setCorrectableErrorsCount(m_nbCorrectableErrors); - response.getRemoteSourceReport()->setUncorrectableErrorsCount(m_nbUncorrectableErrors); - response.getRemoteSourceReport()->setNbOriginalBlocks(m_currentMeta.m_nbOriginalBlocks); - response.getRemoteSourceReport()->setNbFecBlocks(m_currentMeta.m_nbFECBlocks); - response.getRemoteSourceReport()->setCenterFreq(m_currentMeta.m_centerFrequency); - response.getRemoteSourceReport()->setSampleRate(m_currentMeta.m_sampleRate); + response.getRemoteSourceReport()->setTvSec(nowus / 1000000U); + response.getRemoteSourceReport()->setTvUSec(nowus % 1000000U); + response.getRemoteSourceReport()->setQueueSize(dataReadQueue.size()); + response.getRemoteSourceReport()->setQueueLength(dataReadQueue.length()); + response.getRemoteSourceReport()->setSamplesCount(dataReadQueue.readSampleCount()); + response.getRemoteSourceReport()->setCorrectableErrorsCount(m_basebandSource->getNbCorrectableErrors()); + response.getRemoteSourceReport()->setUncorrectableErrorsCount(m_basebandSource->getNbUncorrectableErrors()); + response.getRemoteSourceReport()->setNbOriginalBlocks(currentMeta.m_nbOriginalBlocks); + response.getRemoteSourceReport()->setNbFecBlocks(currentMeta.m_nbFECBlocks); + response.getRemoteSourceReport()->setCenterFreq(currentMeta.m_centerFrequency); + response.getRemoteSourceReport()->setSampleRate(currentMeta.m_sampleRate); response.getRemoteSourceReport()->setDeviceCenterFreq(m_deviceAPI->getSampleSink()->getCenterFrequency()/1000); response.getRemoteSourceReport()->setDeviceSampleRate(m_deviceAPI->getSampleSink()->getSampleRate()); } @@ -556,13 +364,14 @@ void RemoteSource::webapiReverseSendSettings(QList& channelSettingsKeys m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -577,10 +386,13 @@ void RemoteSource::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("RemoteSource::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("RemoteSource::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } diff --git a/plugins/channeltx/remotesource/remotesource.h b/plugins/channeltx/remotesource/remotesource.h index cb42b1d1c..40764bf43 100644 --- a/plugins/channeltx/remotesource/remotesource.h +++ b/plugins/channeltx/remotesource/remotesource.h @@ -18,28 +18,21 @@ #ifndef PLUGINS_CHANNELTX_REMOTESRC_REMOTESRC_H_ #define PLUGINS_CHANNELTX_REMOTESRC_REMOTESRC_H_ -#include "channel/remotedatablock.h" -#include "channel/remotedataqueue.h" -#include "channel/remotedatareadqueue.h" - #include #include -#include "cm256cc/cm256.h" - #include "dsp/basebandsamplesource.h" #include "channel/channelapi.h" #include "util/message.h" -#include "../remotesource/remotesourcesettings.h" +#include "remotesourcesettings.h" -class ThreadedBasebandSampleSource; -class UpChannelizer; -class DeviceAPI; -class RemoteSourceThread; -class RemoteDataBlock; class QNetworkAccessManager; class QNetworkReply; +class QThread; + +class DeviceAPI; +class RemoteSourceBaseband; class RemoteSource : public BasebandSampleSource, public ChannelAPI { Q_OBJECT @@ -68,26 +61,6 @@ public: { } }; - class MsgSampleRateNotification : public Message { - MESSAGE_CLASS_DECLARATION - - public: - static MsgSampleRateNotification* create(int sampleRate) { - return new MsgSampleRateNotification(sampleRate); - } - - int getSampleRate() const { return m_sampleRate; } - - private: - - MsgSampleRateNotification(int sampleRate) : - Message(), - m_sampleRate(sampleRate) - { } - - int m_sampleRate; - }; - class MsgQueryStreamData : public Message { MESSAGE_CLASS_DECLARATION public: @@ -186,10 +159,9 @@ public: virtual void destroy() { delete this; } - virtual void pull(Sample& sample); - virtual void pullAudio(int nbSamples); virtual void start(); virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); virtual bool handleMessage(const Message& cmd); virtual void getIdentifier(QString& id) { id = objectName(); } @@ -232,44 +204,24 @@ public: const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response); - void setDataLink(const QString& dataAddress, uint16_t dataPort); - static const QString m_channelIdURI; static const QString m_channelId; private: DeviceAPI* m_deviceAPI; - ThreadedBasebandSampleSource* m_threadedChannelizer; - UpChannelizer* m_channelizer; - RemoteDataQueue m_dataQueue; - RemoteSourceThread *m_sourceThread; - CM256 m_cm256; - CM256 *m_cm256p; - bool m_running; - + QThread *m_thread; + RemoteSourceBaseband *m_basebandSource; RemoteSourceSettings m_settings; - CM256::cm256_block m_cm256DescriptorBlocks[2*RemoteNbOrginalBlocks]; //!< CM256 decoder descriptors (block addresses and block indexes) - RemoteMetaDataFEC m_currentMeta; - - RemoteDataReadQueue m_dataReadQueue; - - uint32_t m_nbCorrectableErrors; //!< count of correctable errors in number of blocks - uint32_t m_nbUncorrectableErrors; //!< count of uncorrectable errors in number of blocks - QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; void applySettings(const RemoteSourceSettings& settings, bool force = false); - void handleDataBlock(RemoteDataBlock *dataBlock); - void printMeta(const QString& header, RemoteMetaDataFEC *metaData); - uint32_t calculateDataReadQueueSize(int sampleRate); void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); void webapiReverseSendSettings(QList& channelSettingsKeys, const RemoteSourceSettings& settings, bool force); private slots: void networkManagerFinished(QNetworkReply *reply); - void handleData(); }; #endif // PLUGINS_CHANNELTX_REMOTESRC_REMOTESRC_H_ diff --git a/plugins/channeltx/remotesource/remotesourcebaseband.cpp b/plugins/channeltx/remotesource/remotesourcebaseband.cpp new file mode 100644 index 000000000..fd6df2e53 --- /dev/null +++ b/plugins/channeltx/remotesource/remotesourcebaseband.cpp @@ -0,0 +1,187 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "remotesourcebaseband.h" + +MESSAGE_CLASS_DEFINITION(RemoteSourceBaseband::MsgConfigureRemoteSourceBaseband, Message) +MESSAGE_CLASS_DEFINITION(RemoteSourceBaseband::MsgConfigureRemoteSourceWork, Message) + +RemoteSourceBaseband::RemoteSourceBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpSampleChannelizer(&m_source); + + qDebug("RemoteSourceBaseband::RemoteSourceBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &RemoteSourceBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_source, SIGNAL(newChannelSampleRate(unsigned int)), this, SLOT(newChannelSampleRate(unsigned int))); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +RemoteSourceBaseband::~RemoteSourceBaseband() +{ + delete m_channelizer; +} + +void RemoteSourceBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void RemoteSourceBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + qDebug("RemoteSourceBaseband::pull: %u", nbSamples); + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void RemoteSourceBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } +} + +void RemoteSourceBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void RemoteSourceBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool RemoteSourceBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureRemoteSourceBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureRemoteSourceBaseband& cfg = (MsgConfigureRemoteSourceBaseband&) cmd; + qDebug() << "RemoteSourceBaseband::handleMessage: MsgConfigureRemoteSourceBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureRemoteSourceWork::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureRemoteSourceWork& conf = (MsgConfigureRemoteSourceWork&) cmd; + qDebug() << "RemoteSourceBaseband::handleMessage: MsgConfigureRemoteSourceWork: " << conf.isWorking(); + + if (conf.isWorking()) { + m_source.start(); + } else { + m_source.stop(); + } + + return true; + } + else + { + return false; + } +} + +void RemoteSourceBaseband::applySettings(const RemoteSourceSettings& settings, bool force) +{ + qDebug() << "RemoteSourceBaseband::applySettings:" + << " m_dataAddress: " << settings.m_dataAddress + << " m_dataPort: " << settings.m_dataPort + << " force: " << force; + + if ((settings.m_dataAddress != m_settings.m_dataAddress) + || (settings.m_dataPort != m_settings.m_dataPort) || force) { + m_source.dataBind(settings.m_dataAddress, settings.m_dataPort); + } + + m_settings = settings; +} + +int RemoteSourceBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} + +void RemoteSourceBaseband::newRemoteSampleRate(unsigned int sampleRate) +{ + m_channelizer->setChannelization(sampleRate, 0); // Adjust channelizer to match remote sample rate +} \ No newline at end of file diff --git a/plugins/channeltx/remotesource/remotesourcebaseband.h b/plugins/channeltx/remotesource/remotesourcebaseband.h new file mode 100644 index 000000000..29eedd5a1 --- /dev/null +++ b/plugins/channeltx/remotesource/remotesourcebaseband.h @@ -0,0 +1,110 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_REMOTESOURCEBASEBAND_H +#define INCLUDE_REMOTESOURCEBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "remotesourcesource.h" +#include "remotesourcesettings.h" + +class UpSampleChannelizer; + +class RemoteSourceBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureRemoteSourceBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const RemoteSourceSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureRemoteSourceBaseband* create(const RemoteSourceSettings& settings, bool force) + { + return new MsgConfigureRemoteSourceBaseband(settings, force); + } + + private: + RemoteSourceSettings m_settings; + bool m_force; + + MsgConfigureRemoteSourceBaseband(const RemoteSourceSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureRemoteSourceWork : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool isWorking() const { return m_working; } + + static MsgConfigureRemoteSourceWork* create(bool working) + { + return new MsgConfigureRemoteSourceWork(working); + } + + private: + bool m_working; + + MsgConfigureRemoteSourceWork(bool working) : + Message(), + m_working(working) + { } + }; + + RemoteSourceBaseband(); + ~RemoteSourceBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + int getChannelSampleRate() const; + RemoteDataReadQueue& getDataQueue() { return m_source.getDataQueue(); } + uint32_t getNbCorrectableErrors() const { return m_source.getNbCorrectableErrors(); } + uint32_t getNbUncorrectableErrors() const { return m_source.getNbUncorrectableErrors(); } + const RemoteMetaDataFEC& getRemoteMetaDataFEC() const { return m_source.getRemoteMetaDataFEC(); } + +private: + SampleSourceFifo m_sampleFifo; + UpSampleChannelizer *m_channelizer; + RemoteSourceSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + RemoteSourceSettings m_settings; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const RemoteSourceSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed + void newRemoteSampleRate(unsigned int sampleRate); +}; + + +#endif // INCLUDE_REMOTESOURCEBASEBAND_H diff --git a/plugins/channeltx/remotesource/remotesourcegui.cpp b/plugins/channeltx/remotesource/remotesourcegui.cpp index 26433ea69..3ad9a1b96 100644 --- a/plugins/channeltx/remotesource/remotesourcegui.cpp +++ b/plugins/channeltx/remotesource/remotesourcegui.cpp @@ -81,13 +81,7 @@ bool RemoteSourceGUI::deserialize(const QByteArray& data) bool RemoteSourceGUI::handleMessage(const Message& message) { - if (RemoteSource::MsgSampleRateNotification::match(message)) - { - RemoteSource::MsgSampleRateNotification& notif = (RemoteSource::MsgSampleRateNotification&) message; - m_channelMarker.setBandwidth(notif.getSampleRate()); - return true; - } - else if (RemoteSource::MsgConfigureRemoteSource::match(message)) + if (RemoteSource::MsgConfigureRemoteSource::match(message)) { const RemoteSource::MsgConfigureRemoteSource& cfg = (RemoteSource::MsgConfigureRemoteSource&) message; m_settings = cfg.getSettings(); @@ -99,7 +93,16 @@ bool RemoteSourceGUI::handleMessage(const Message& message) else if (RemoteSource::MsgReportStreamData::match(message)) { const RemoteSource::MsgReportStreamData& report = (RemoteSource::MsgReportStreamData&) message; - ui->sampleRate->setText(QString("%1").arg(report.get_sampleRate())); + + uint32_t remoteSampleRate = report.get_sampleRate(); + + if (remoteSampleRate != m_remoteSampleRate) + { + m_channelMarker.setBandwidth(remoteSampleRate); + m_remoteSampleRate = remoteSampleRate; + } + + ui->sampleRate->setText(QString("%1").arg(remoteSampleRate)); QString nominalNbBlocksText = QString("%1/%2") .arg(report.get_nbOriginalBlocks() + report.get_nbFECBlocks()) .arg(report.get_nbFECBlocks()); @@ -160,6 +163,7 @@ RemoteSourceGUI::RemoteSourceGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, ui(new Ui::RemoteSourceGUI), m_pluginAPI(pluginAPI), m_deviceUISet(deviceUISet), + m_remoteSampleRate(48000), m_countUnrecoverable(0), m_countRecovered(0), m_lastCountUnrecoverable(0), @@ -231,7 +235,7 @@ void RemoteSourceGUI::displaySettings() m_channelMarker.blockSignals(true); m_channelMarker.setCenterFrequency(0); m_channelMarker.setTitle(m_settings.m_title); - m_channelMarker.setBandwidth(5000); // TODO + m_channelMarker.setBandwidth(m_remoteSampleRate); m_channelMarker.blockSignals(false); m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only diff --git a/plugins/channeltx/remotesource/remotesourcegui.h b/plugins/channeltx/remotesource/remotesourcegui.h index cf6de88a9..67386326a 100644 --- a/plugins/channeltx/remotesource/remotesourcegui.h +++ b/plugins/channeltx/remotesource/remotesourcegui.h @@ -63,6 +63,7 @@ private: DeviceUISet* m_deviceUISet; ChannelMarker m_channelMarker; RemoteSourceSettings m_settings; + int m_remoteSampleRate; bool m_doApplySettings; RemoteSource* m_remoteSrc; diff --git a/plugins/channeltx/remotesource/remotesourceplugin.cpp b/plugins/channeltx/remotesource/remotesourceplugin.cpp index 427abf99c..35abfc675 100644 --- a/plugins/channeltx/remotesource/remotesourceplugin.cpp +++ b/plugins/channeltx/remotesource/remotesourceplugin.cpp @@ -27,7 +27,7 @@ const PluginDescriptor RemoteSourcePlugin::m_pluginDescriptor = { QString("Remote channel source"), - QString("4.11.6"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/channeltx/remotesource/remotesourcesource.cpp b/plugins/channeltx/remotesource/remotesourcesource.cpp new file mode 100644 index 000000000..d697347a9 --- /dev/null +++ b/plugins/channeltx/remotesource/remotesourcesource.cpp @@ -0,0 +1,265 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "remotesourcethread.h" +#include "remotesourcesource.h" + +RemoteSourceSource::RemoteSourceSource() : + m_running(false), + m_sourceThread(nullptr), + m_nbCorrectableErrors(0), + m_nbUncorrectableErrors(0), + m_channelSampleRate(48000) +{ + connect(&m_dataQueue, SIGNAL(dataBlockEnqueued()), this, SLOT(handleData()), Qt::QueuedConnection); + m_cm256p = m_cm256.isInitialized() ? &m_cm256 : 0; + m_currentMeta.init(); + applyChannelSettings(m_channelSampleRate, true); +} + +RemoteSourceSource::~RemoteSourceSource() +{} + +void RemoteSourceSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void RemoteSourceSource::pullOne(Sample& sample) +{ + m_dataReadQueue.readSample(sample, true); // true is scale for Tx + + Complex ci; + + if (m_interpolatorDistance > 1.0f) // decimate + { + getSample(); + + while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) { + getSample(); + } + } + else + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) { + getSample(); + } + } + + m_interpolatorDistanceRemain += m_interpolatorDistance; + sample.m_real = (FixReal) ci.real(); + sample.m_imag = (FixReal) ci.imag(); +} + +void RemoteSourceSource::getSample() +{ + Sample s; + m_dataReadQueue.readSample(s, true); // true is scale for Tx + m_modSample.real(s.m_real); + m_modSample.imag(s.m_imag); +} + +void RemoteSourceSource::start() +{ + qDebug("RemoteSourceSource::start"); + + if (m_running) { + stop(); + } + + m_sourceThread = new RemoteSourceThread(&m_dataQueue); + m_sourceThread->startStop(true); + m_sourceThread->dataBind(m_settings.m_dataAddress, m_settings.m_dataPort); + m_running = true; +} + +void RemoteSourceSource::stop() +{ + qDebug("RemoteSourceSource::stop"); + + if (m_sourceThread) + { + m_sourceThread->startStop(false); + m_sourceThread->deleteLater(); + m_sourceThread = 0; + } + + m_running = false; +} + +void RemoteSourceSource::handleData() +{ + RemoteDataBlock* dataBlock; + + while (m_running && ((dataBlock = m_dataQueue.pop()) != 0)) { + handleDataBlock(dataBlock); + } +} + +void RemoteSourceSource::handleDataBlock(RemoteDataBlock* dataBlock) +{ + (void) dataBlock; + if (dataBlock->m_rxControlBlock.m_blockCount < RemoteNbOrginalBlocks) + { + qWarning("RemoteSourceSource::handleDataBlock: incomplete data block: not processing"); + } + else + { + int blockCount = 0; + + for (int blockIndex = 0; blockIndex < 256; blockIndex++) + { + if ((blockIndex == 0) && (dataBlock->m_rxControlBlock.m_metaRetrieved)) + { + m_cm256DescriptorBlocks[blockCount].Index = 0; + m_cm256DescriptorBlocks[blockCount].Block = (void *) &(dataBlock->m_superBlocks[0].m_protectedBlock); + blockCount++; + } + else if (dataBlock->m_superBlocks[blockIndex].m_header.m_blockIndex != 0) + { + m_cm256DescriptorBlocks[blockCount].Index = dataBlock->m_superBlocks[blockIndex].m_header.m_blockIndex; + m_cm256DescriptorBlocks[blockCount].Block = (void *) &(dataBlock->m_superBlocks[blockIndex].m_protectedBlock); + blockCount++; + } + } + + //qDebug("RemoteSourceSource::handleDataBlock: frame: %u blocks: %d", dataBlock.m_rxControlBlock.m_frameIndex, blockCount); + + // Need to use the CM256 recovery + if (m_cm256p &&(dataBlock->m_rxControlBlock.m_originalCount < RemoteNbOrginalBlocks)) + { + qDebug("RemoteSourceSource::handleDataBlock: %d recovery blocks", dataBlock->m_rxControlBlock.m_recoveryCount); + CM256::cm256_encoder_params paramsCM256; + paramsCM256.BlockBytes = sizeof(RemoteProtectedBlock); // never changes + paramsCM256.OriginalCount = RemoteNbOrginalBlocks; // never changes + + if (m_currentMeta.m_tv_sec == 0) { + paramsCM256.RecoveryCount = dataBlock->m_rxControlBlock.m_recoveryCount; + } else { + paramsCM256.RecoveryCount = m_currentMeta.m_nbFECBlocks; + } + + // update counters + if (dataBlock->m_rxControlBlock.m_originalCount < RemoteNbOrginalBlocks - paramsCM256.RecoveryCount) { + m_nbUncorrectableErrors += RemoteNbOrginalBlocks - paramsCM256.RecoveryCount - dataBlock->m_rxControlBlock.m_originalCount; + } else { + m_nbCorrectableErrors += dataBlock->m_rxControlBlock.m_recoveryCount; + } + + if (m_cm256.cm256_decode(paramsCM256, m_cm256DescriptorBlocks)) // CM256 decode + { + qWarning() << "RemoteSourceSource::handleDataBlock: decode CM256 error:" + << " m_originalCount: " << dataBlock->m_rxControlBlock.m_originalCount + << " m_recoveryCount: " << dataBlock->m_rxControlBlock.m_recoveryCount; + } + else + { + for (int ir = 0; ir < dataBlock->m_rxControlBlock.m_recoveryCount; ir++) // restore missing blocks + { + int recoveryIndex = RemoteNbOrginalBlocks - dataBlock->m_rxControlBlock.m_recoveryCount + ir; + int blockIndex = m_cm256DescriptorBlocks[recoveryIndex].Index; + RemoteProtectedBlock *recoveredBlock = + (RemoteProtectedBlock *) m_cm256DescriptorBlocks[recoveryIndex].Block; + memcpy((void *) &(dataBlock->m_superBlocks[blockIndex].m_protectedBlock), recoveredBlock, sizeof(RemoteProtectedBlock)); + if ((blockIndex == 0) && !dataBlock->m_rxControlBlock.m_metaRetrieved) { + dataBlock->m_rxControlBlock.m_metaRetrieved = true; + } + } + } + } + + // Validate block zero and retrieve its data + if (dataBlock->m_rxControlBlock.m_metaRetrieved) + { + RemoteMetaDataFEC *metaData = (RemoteMetaDataFEC *) &(dataBlock->m_superBlocks[0].m_protectedBlock); + boost::crc_32_type crc32; + crc32.process_bytes(metaData, sizeof(RemoteMetaDataFEC)-4); + + if (crc32.checksum() == metaData->m_crc32) + { + if (!(m_currentMeta == *metaData)) + { + printMeta("RemoteSourceSource::handleDataBlock", metaData); + + if (m_currentMeta.m_sampleRate != metaData->m_sampleRate) { + emit newRemoteSampleRate(metaData->m_sampleRate); + // returns via applyChannelSettings to set interpolator + } + } + + m_currentMeta = *metaData; + } + else + { + qWarning() << "RemoteSource::handleDataBlock: recovered meta: invalid CRC32"; + } + } + + m_dataReadQueue.push(dataBlock); // Push into R/W buffer + } +} + +void RemoteSourceSource::printMeta(const QString& header, RemoteMetaDataFEC *metaData) +{ + qDebug().noquote() << header << ": " + << "|" << metaData->m_centerFrequency + << ":" << metaData->m_sampleRate + << ":" << (int) (metaData->m_sampleBytes & 0xF) + << ":" << (int) metaData->m_sampleBits + << ":" << (int) metaData->m_nbOriginalBlocks + << ":" << (int) metaData->m_nbFECBlocks + << "|" << metaData->m_tv_sec + << ":" << metaData->m_tv_usec + << "|"; +} + +void RemoteSourceSource::dataBind(const QString& dataAddress, uint16_t dataPort) +{ + if (m_sourceThread) { + m_sourceThread->dataBind(dataAddress, dataPort); + } + + m_settings.m_dataAddress = dataAddress; + m_settings.m_dataPort = dataPort; +} + +void RemoteSourceSource::applyChannelSettings(int channelSampleRate, bool force) +{ + qDebug() << "RemoteSourceSource::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " m_currentMeta.m_sampleRate: " << m_currentMeta.m_sampleRate + << " force: " << force; + + if ((channelSampleRate != m_channelSampleRate) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_currentMeta.m_sampleRate / (Real) channelSampleRate; + m_interpolator.create(48, m_currentMeta.m_sampleRate, m_currentMeta.m_sampleRate / 2.2, 3.0); + } + + m_channelSampleRate = channelSampleRate; +} \ No newline at end of file diff --git a/plugins/channeltx/remotesource/remotesourcesource.h b/plugins/channeltx/remotesource/remotesourcesource.h new file mode 100644 index 000000000..874593785 --- /dev/null +++ b/plugins/channeltx/remotesource/remotesourcesource.h @@ -0,0 +1,87 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_REMOTESOURCE_REMOTESOURCESOURCE_H_ +#define PLUGINS_CHANNELTX_REMOTESOURCE_REMOTESOURCESOURCE_H_ + +#include + +#include "cm256cc/cm256.h" + +#include "dsp/channelsamplesource.h" +#include "dsp/interpolator.h" +#include "channel/remotedatablock.h" +#include "channel/remotedataqueue.h" +#include "channel/remotedatareadqueue.h" + +#include "remotesourcesettings.h" + +class RemoteSourceThread; + +class RemoteSourceSource : public QObject, public ChannelSampleSource { + Q_OBJECT +public: + RemoteSourceSource(); + ~RemoteSourceSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples) {} + + void start(); + void stop(); + bool isRunning() const { return m_running; } + RemoteDataReadQueue& getDataQueue() { return m_dataReadQueue; } + void dataBind(const QString& m_dataAddress, uint16_t dataPort); + uint32_t getNbCorrectableErrors() const { return m_nbCorrectableErrors; } + uint32_t getNbUncorrectableErrors() const { return m_nbUncorrectableErrors; } + const RemoteMetaDataFEC& getRemoteMetaDataFEC() const { return m_currentMeta; } + + void applyChannelSettings(int channelSampleRate, bool force = false); + +signals: + void newRemoteSampleRate(unsigned int sampleRate); + +private: + bool m_running; + RemoteSourceThread *m_sourceThread; + RemoteDataQueue m_dataQueue; + RemoteDataReadQueue m_dataReadQueue; + CM256 m_cm256; + CM256 *m_cm256p; + RemoteSourceSettings m_settings; + CM256::cm256_block m_cm256DescriptorBlocks[2*RemoteNbOrginalBlocks]; //!< CM256 decoder descriptors (block addresses and block indexes) + RemoteMetaDataFEC m_currentMeta; + uint32_t m_nbCorrectableErrors; //!< count of correctable errors in number of blocks + uint32_t m_nbUncorrectableErrors; //!< count of uncorrectable errors in number of blocks + + int m_channelSampleRate; + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + bool m_interpolatorConsumed; + Complex m_modSample; + + void handleDataBlock(RemoteDataBlock *dataBlock); + void printMeta(const QString& header, RemoteMetaDataFEC *metaData); + void getSample(); + +private slots: + void handleData(); +}; + +#endif // PLUGINS_CHANNELTX_REMOTESOURCE_REMOTESOURCESOURCE_H_ diff --git a/plugins/channeltx/udpsource/CMakeLists.txt b/plugins/channeltx/udpsource/CMakeLists.txt index e9a5e1558..192b50f25 100644 --- a/plugins/channeltx/udpsource/CMakeLists.txt +++ b/plugins/channeltx/udpsource/CMakeLists.txt @@ -1,7 +1,9 @@ project(udpsource) set(udpsource_SOURCES - udpsource.cpp + udpsource.cpp + udpsourcebaseband.cpp + udpsourcesource.cpp udpsourceplugin.cpp udpsourceudphandler.cpp udpsourcemsg.cpp @@ -11,6 +13,8 @@ set(udpsource_SOURCES set(udpsource_HEADERS udpsource.h + udpsourcebaseband.h + udpsourcesource.h udpsourceplugin.h udpsourceudphandler.h udpsourcemsg.h diff --git a/plugins/channeltx/udpsource/udpsource.cpp b/plugins/channeltx/udpsource/udpsource.cpp index 8ea1c9735..c33edd911 100644 --- a/plugins/channeltx/udpsource/udpsource.cpp +++ b/plugins/channeltx/udpsource/udpsource.cpp @@ -19,24 +19,21 @@ #include #include #include +#include #include "SWGChannelSettings.h" #include "SWGChannelReport.h" #include "SWGUDPSourceReport.h" #include "device/deviceapi.h" -#include "dsp/upchannelizer.h" -#include "dsp/threadedbasebandsamplesource.h" #include "dsp/dspcommands.h" #include "util/db.h" +#include "udpsourcebaseband.h" #include "udpsource.h" -#include "udpsourcemsg.h" MESSAGE_CLASS_DEFINITION(UDPSource::MsgConfigureUDPSource, Message) MESSAGE_CLASS_DEFINITION(UDPSource::MsgConfigureChannelizer, Message) -MESSAGE_CLASS_DEFINITION(UDPSource::MsgUDPSourceSpectrum, Message) -MESSAGE_CLASS_DEFINITION(UDPSource::MsgResetReadIndex, Message) const QString UDPSource::m_channelIdURI = "sdrangel.channeltx.udpsource"; const QString UDPSource::m_channelId = "UDPSource"; @@ -44,43 +41,17 @@ const QString UDPSource::m_channelId = "UDPSource"; UDPSource::UDPSource(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), m_deviceAPI(deviceAPI), - m_basebandSampleRate(48000), - m_outputSampleRate(48000), - m_inputFrequencyOffset(0), - m_squelch(1e-6), - m_spectrum(0), - m_spectrumEnabled(false), - m_spectrumChunkSize(2160), - m_spectrumChunkCounter(0), - m_magsq(1e-10), - m_movingAverage(16, 1e-10), - m_inMovingAverage(480, 1e-10), - m_sampleRateSum(0), - m_sampleRateAvgCounter(0), - m_levelCalcCount(0), - m_peakLevel(0.0f), - m_levelSum(0.0f), - m_levelNbSamples(480), - m_squelchOpen(false), - m_squelchOpenCount(0), - m_squelchCloseCount(0), - m_squelchThreshold(4800), - m_modPhasor(0.0f), - m_SSBFilterBufferIndex(0), m_settingsMutex(QMutex::Recursive) { setObjectName(m_channelId); - m_udpHandler.setFeedbackMessageQueue(&m_inputMessageQueue); - m_SSBFilter = new fftfilt(m_settings.m_lowCutoff / m_settings.m_inputSampleRate, m_settings.m_rfBandwidth / m_settings.m_inputSampleRate, m_ssbFftLen); - m_SSBFilterBuffer = new Complex[m_ssbFftLen>>1]; // filter returns data exactly half of its size + m_thread = new QThread(this); + m_basebandSource = new UDPSourceBaseband(); + m_basebandSource->moveToThread(m_thread); - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); applySettings(m_settings, true); - m_channelizer = new UpChannelizer(this); - m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this); - m_deviceAPI->addChannelSource(m_threadedChannelizer); + m_deviceAPI->addChannelSource(this); m_deviceAPI->addChannelSourceAPI(this); m_networkManager = new QNetworkAccessManager(); @@ -92,262 +63,42 @@ UDPSource::~UDPSource() disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); delete m_networkManager; m_deviceAPI->removeChannelSourceAPI(this); - m_deviceAPI->removeChannelSource(m_threadedChannelizer); - delete m_threadedChannelizer; - delete m_channelizer; - delete m_SSBFilter; - delete[] m_SSBFilterBuffer; + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; } void UDPSource::start() { - m_udpHandler.start(); - applyChannelSettings(m_basebandSampleRate, m_outputSampleRate, m_inputFrequencyOffset, true); + qDebug("UDPSource::start"); + m_basebandSource->reset(); + m_thread->start(); } void UDPSource::stop() { - m_udpHandler.stop(); + qDebug("UDPSource::stop"); + m_thread->exit(); + m_thread->wait(); } -void UDPSource::pull(Sample& sample) +void UDPSource::pull(SampleVector::iterator& begin, unsigned int nbSamples) { - if (m_settings.m_channelMute) - { - sample.m_real = 0.0f; - sample.m_imag = 0.0f; - initSquelch(false); - return; - } - - Complex ci; - - m_settingsMutex.lock(); - - if (m_interpolatorDistance > 1.0f) // decimate - { - modulateSample(); - - while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - else - { - if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) - { - modulateSample(); - } - } - - m_interpolatorDistanceRemain += m_interpolatorDistance; - - ci *= m_carrierNco.nextIQ(); // shift to carrier frequency - - m_settingsMutex.unlock(); - - double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); - magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); - m_movingAverage.feed(magsq); - m_magsq = m_movingAverage.average(); - - sample.m_real = (FixReal) ci.real(); - sample.m_imag = (FixReal) ci.imag(); -} - -void UDPSource::modulateSample() -{ - if (m_settings.m_sampleFormat == UDPSourceSettings::FormatSnLE) // Linear I/Q transponding - { - Sample s; - - m_udpHandler.readSample(s); - - uint64_t magsq = s.m_real * s.m_real + s.m_imag * s.m_imag; - m_inMovingAverage.feed(magsq/(SDR_TX_SCALED*SDR_TX_SCALED)); - m_inMagsq = m_inMovingAverage.average(); - - calculateSquelch(m_inMagsq); - - if (m_squelchOpen) - { - m_modSample.real(s.m_real * m_settings.m_gainOut); - m_modSample.imag(s.m_imag * m_settings.m_gainOut); - calculateLevel(m_modSample); - } - else - { - m_modSample.real(0.0f); - m_modSample.imag(0.0f); - } - } - else if (m_settings.m_sampleFormat == UDPSourceSettings::FormatNFM) - { - qint16 t; - readMonoSample(t); - - m_inMovingAverage.feed((t*t)/1073741824.0); - m_inMagsq = m_inMovingAverage.average(); - - calculateSquelch(m_inMagsq); - - if (m_squelchOpen) - { - m_modPhasor += (m_settings.m_fmDeviation / m_settings.m_inputSampleRate) * (t / SDR_TX_SCALEF) * M_PI * 2.0f; - m_modSample.real(cos(m_modPhasor) * 0.3162292f * SDR_TX_SCALEF * m_settings.m_gainOut); - m_modSample.imag(sin(m_modPhasor) * 0.3162292f * SDR_TX_SCALEF * m_settings.m_gainOut); - calculateLevel(m_modSample); - } - else - { - m_modSample.real(0.0f); - m_modSample.imag(0.0f); - } - } - else if (m_settings.m_sampleFormat == UDPSourceSettings::FormatAM) - { - qint16 t; - readMonoSample(t); - m_inMovingAverage.feed((t*t)/(SDR_TX_SCALED*SDR_TX_SCALED)); - m_inMagsq = m_inMovingAverage.average(); - - calculateSquelch(m_inMagsq); - - if (m_squelchOpen) - { - m_modSample.real(((t / SDR_TX_SCALEF)*m_settings.m_amModFactor*m_settings.m_gainOut + 1.0f) * (SDR_TX_SCALEF/2)); // modulate and scale zero frequency carrier - m_modSample.imag(0.0f); - calculateLevel(m_modSample); - } - else - { - m_modSample.real(0.0f); - m_modSample.imag(0.0f); - } - } - else if ((m_settings.m_sampleFormat == UDPSourceSettings::FormatLSB) || (m_settings.m_sampleFormat == UDPSourceSettings::FormatUSB)) - { - qint16 t; - Complex c, ci; - fftfilt::cmplx *filtered; - int n_out = 0; - - readMonoSample(t); - m_inMovingAverage.feed((t*t)/(SDR_TX_SCALED*SDR_TX_SCALED)); - m_inMagsq = m_inMovingAverage.average(); - - calculateSquelch(m_inMagsq); - - if (m_squelchOpen) - { - ci.real((t / SDR_TX_SCALEF) * m_settings.m_gainOut); - ci.imag(0.0f); - - n_out = m_SSBFilter->runSSB(ci, &filtered, (m_settings.m_sampleFormat == UDPSourceSettings::FormatUSB)); - - if (n_out > 0) - { - memcpy((void *) m_SSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); - m_SSBFilterBufferIndex = 0; - } - - c = m_SSBFilterBuffer[m_SSBFilterBufferIndex]; - m_modSample.real(m_SSBFilterBuffer[m_SSBFilterBufferIndex].real() * SDR_TX_SCALEF); - m_modSample.imag(m_SSBFilterBuffer[m_SSBFilterBufferIndex].imag() * SDR_TX_SCALEF); - m_SSBFilterBufferIndex++; - - calculateLevel(m_modSample); - } - else - { - m_modSample.real(0.0f); - m_modSample.imag(0.0f); - } - } - else - { - m_modSample.real(0.0f); - m_modSample.imag(0.0f); - initSquelch(false); - } - - if (m_spectrum && m_spectrumEnabled && (m_spectrumChunkCounter < m_spectrumChunkSize - 1)) - { - Sample s; - s.m_real = (FixReal) m_modSample.real(); - s.m_imag = (FixReal) m_modSample.imag(); - m_sampleBuffer.push_back(s); - m_spectrumChunkCounter++; - } - else if (m_spectrum) - { - m_spectrum->feed(m_sampleBuffer.begin(), m_sampleBuffer.end(), false); - m_sampleBuffer.clear(); - m_spectrumChunkCounter = 0; - } -} - -void UDPSource::calculateLevel(Real sample) -{ - if (m_levelCalcCount < m_levelNbSamples) - { - m_peakLevel = std::max(std::fabs(m_peakLevel), sample); - m_levelSum += sample * sample; - m_levelCalcCount++; - } - else - { - qreal rmsLevel = m_levelSum > 0.0 ? sqrt(m_levelSum / m_levelNbSamples) : 0.0; - //qDebug("NFMMod::calculateLevel: %f %f", rmsLevel, m_peakLevel); - emit levelChanged(rmsLevel, m_peakLevel, m_levelNbSamples); - m_peakLevel = 0.0f; - m_levelSum = 0.0f; - m_levelCalcCount = 0; - } -} - -void UDPSource::calculateLevel(Complex sample) -{ - Real t = std::abs(sample); - - if (m_levelCalcCount < m_levelNbSamples) - { - m_peakLevel = std::max(std::fabs(m_peakLevel), t); - m_levelSum += (t * t); - m_levelCalcCount++; - } - else - { - qreal rmsLevel = m_levelSum > 0.0 ? sqrt((m_levelSum/(SDR_TX_SCALED*SDR_TX_SCALED)) / m_levelNbSamples) : 0.0; - emit levelChanged(rmsLevel, m_peakLevel / SDR_TX_SCALEF, m_levelNbSamples); - m_peakLevel = 0.0f; - m_levelSum = 0.0f; - m_levelCalcCount = 0; - } + m_basebandSource->pull(begin, nbSamples); } bool UDPSource::handleMessage(const Message& cmd) { - if (UpChannelizer::MsgChannelizerNotification::match(cmd)) - { - UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd; - qDebug() << "UDPSource::handleMessage: MsgChannelizerNotification"; - - applyChannelSettings(notif.getBasebandSampleRate(), notif.getSampleRate(), notif.getFrequencyOffset()); - - return true; - } - else if (MsgConfigureChannelizer::match(cmd)) + if (MsgConfigureChannelizer::match(cmd)) { MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; - qDebug() << "UDPSource::handleMessage: MsgConfigureChannelizer:" - << " sampleRate: " << cfg.getSampleRate() - << " centerFrequency: " << cfg.getCenterFrequency(); + qDebug() << "AMMod::handleMessage: MsgConfigureChannelizer:" + << " getSourceSampleRate: " << cfg.getSourceSampleRate() + << " getSourceCenterFrequency: " << cfg.getSourceCenterFrequency(); - m_channelizer->configure(m_channelizer->getInputMessageQueue(), - cfg.getSampleRate(), - cfg.getCenterFrequency()); + UDPSourceBaseband::MsgConfigureChannelizer *msg + = UDPSourceBaseband::MsgConfigureChannelizer::create(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_basebandSource->getInputMessageQueue()->push(msg); return true; } @@ -360,133 +111,37 @@ bool UDPSource::handleMessage(const Message& cmd) return true; } - else if (UDPSourceMessages::MsgSampleRateCorrection::match(cmd)) - { - UDPSourceMessages::MsgSampleRateCorrection& cfg = (UDPSourceMessages::MsgSampleRateCorrection&) cmd; - Real newSampleRate = m_actualInputSampleRate + cfg.getCorrectionFactor() * m_actualInputSampleRate; - - // exclude values too way out nominal sample rate (20%) - if ((newSampleRate < m_settings.m_inputSampleRate * 1.2) && (newSampleRate > m_settings.m_inputSampleRate * 0.8)) - { - m_actualInputSampleRate = newSampleRate; - - if ((cfg.getRawDeltaRatio() > -0.05) && (cfg.getRawDeltaRatio() < 0.05)) - { - if (m_sampleRateAvgCounter < m_sampleRateAverageItems) - { - m_sampleRateSum += m_actualInputSampleRate; - m_sampleRateAvgCounter++; - } - } - else - { - m_sampleRateSum = 0.0; - m_sampleRateAvgCounter = 0; - } - - if (m_sampleRateAvgCounter == m_sampleRateAverageItems) - { - float avgRate = m_sampleRateSum / m_sampleRateAverageItems; - qDebug("UDPSource::handleMessage: MsgSampleRateCorrection: corr: %+.6f new rate: %.0f: avg rate: %.0f", - cfg.getCorrectionFactor(), - m_actualInputSampleRate, - avgRate); - m_actualInputSampleRate = avgRate; - m_sampleRateSum = 0.0; - m_sampleRateAvgCounter = 0; - } -// else -// { -// qDebug("UDPSource::handleMessage: MsgSampleRateCorrection: corr: %+.6f new rate: %.0f", -// cfg.getCorrectionFactor(), -// m_actualInputSampleRate); -// } - - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_actualInputSampleRate / (Real) m_outputSampleRate; - //m_interpolator.create(48, m_actualInputSampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); // causes clicking: leaving at standard frequency - m_settingsMutex.unlock(); - } - - return true; - } - else if (MsgUDPSourceSpectrum::match(cmd)) - { - MsgUDPSourceSpectrum& spc = (MsgUDPSourceSpectrum&) cmd; - m_spectrumEnabled = spc.getEnabled(); - qDebug() << "UDPSource::handleMessage: MsgUDPSourceSpectrum: m_spectrumEnabled: " << m_spectrumEnabled; - - return true; - } - else if (MsgResetReadIndex::match(cmd)) - { - m_settingsMutex.lock(); - m_udpHandler.resetReadIndex(); - m_settingsMutex.unlock(); - - qDebug() << "UDPSource::handleMessage: MsgResetReadIndex"; - - return true; - } else if (DSPSignalNotification::match(cmd)) { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "UDPSource::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + return true; } else { - if(m_spectrum != 0) - { - return m_spectrum->handleMessage(cmd); - } - else - { - return false; - } + return false; } } +void UDPSource::setSpectrumSink(BasebandSampleSink* spectrum) +{ + m_basebandSource->setSpectrumSink(spectrum); +} + void UDPSource::setSpectrum(bool enabled) { - Message* cmd = MsgUDPSourceSpectrum::create(enabled); - getInputMessageQueue()->push(cmd); + Message* cmd = UDPSourceBaseband::MsgUDPSourceSpectrum::create(enabled); + m_basebandSource->getInputMessageQueue()->push(cmd); } void UDPSource::resetReadIndex() { - Message* cmd = MsgResetReadIndex::create(); - getInputMessageQueue()->push(cmd); -} - -void UDPSource::applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force) -{ - qDebug() << "UDPSource::applyChannelSettings:" - << " basebandSampleRate: " << basebandSampleRate - << " outputSampleRate: " << outputSampleRate - << " inputFrequencyOffset: " << inputFrequencyOffset; - - if ((inputFrequencyOffset != m_inputFrequencyOffset) || - (outputSampleRate != m_outputSampleRate) || force) - { - m_settingsMutex.lock(); - m_carrierNco.setFreq(inputFrequencyOffset, outputSampleRate); - m_settingsMutex.unlock(); - } - - if (((outputSampleRate != m_outputSampleRate) && (!m_settings.m_autoRWBalance)) || force) - { - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) m_settings.m_inputSampleRate / (Real) outputSampleRate; - m_interpolator.create(48, m_settings.m_inputSampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); - m_settingsMutex.unlock(); - } - - m_basebandSampleRate = basebandSampleRate; - m_outputSampleRate = outputSampleRate; - m_inputFrequencyOffset = inputFrequencyOffset; + Message* cmd = UDPSourceBaseband::MsgResetReadIndex::create(); + m_basebandSource->getInputMessageQueue()->push(cmd); } void UDPSource::applySettings(const UDPSourceSettings& settings, bool force) @@ -565,76 +220,8 @@ void UDPSource::applySettings(const UDPSourceSettings& settings, bool force) reverseAPIKeys.append("stereoInput"); } - if((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || - (settings.m_lowCutoff != m_settings.m_lowCutoff) || - (settings.m_inputSampleRate != m_settings.m_inputSampleRate) || force) - { - m_settingsMutex.lock(); - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) settings.m_inputSampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, settings.m_inputSampleRate, settings.m_rfBandwidth / 2.2, 3.0); - m_actualInputSampleRate = settings.m_inputSampleRate; - m_udpHandler.resetReadIndex(); - m_sampleRateSum = 0.0; - m_sampleRateAvgCounter = 0; - m_spectrumChunkSize = settings.m_inputSampleRate * 0.05; // 50 ms chunk - m_spectrumChunkCounter = 0; - m_levelNbSamples = settings.m_inputSampleRate * 0.01; // every 10 ms - m_levelCalcCount = 0; - m_peakLevel = 0.0f; - m_levelSum = 0.0f; - m_udpHandler.resizeBuffer(settings.m_inputSampleRate); - m_inMovingAverage.resize(settings.m_inputSampleRate * 0.01, 1e-10); // 10 ms - m_squelchThreshold = settings.m_inputSampleRate * settings.m_squelchGate; - initSquelch(m_squelchOpen); - m_SSBFilter->create_filter(settings.m_lowCutoff / settings.m_inputSampleRate, settings.m_rfBandwidth / settings.m_inputSampleRate); - m_settingsMutex.unlock(); - } - - if ((settings.m_squelch != m_settings.m_squelch) || force) - { - m_squelch = CalcDb::powerFromdB(settings.m_squelch); - } - - if ((settings.m_squelchGate != m_settings.m_squelchGate) || force) - { - m_squelchThreshold = m_outputSampleRate * settings.m_squelchGate; - initSquelch(m_squelchOpen); - } - - if ((settings.m_udpAddress != m_settings.m_udpAddress) || - (settings.m_udpPort != m_settings.m_udpPort) || force) - { - m_settingsMutex.lock(); - m_udpHandler.configureUDPLink(settings.m_udpAddress, settings.m_udpPort); - m_settingsMutex.unlock(); - } - - if ((settings.m_channelMute != m_settings.m_channelMute) || force) - { - if (!settings.m_channelMute) { - m_udpHandler.resetReadIndex(); - } - } - - if ((settings.m_autoRWBalance != m_settings.m_autoRWBalance) || force) - { - m_settingsMutex.lock(); - m_udpHandler.setAutoRWBalance(settings.m_autoRWBalance); - - if (!settings.m_autoRWBalance) - { - m_interpolatorDistanceRemain = 0; - m_interpolatorConsumed = false; - m_interpolatorDistance = (Real) settings.m_inputSampleRate / (Real) m_outputSampleRate; - m_interpolator.create(48, settings.m_inputSampleRate, settings.m_rfBandwidth / 2.2, 3.0); - m_actualInputSampleRate = settings.m_inputSampleRate; - m_udpHandler.resetReadIndex(); - } - - m_settingsMutex.unlock(); - } + UDPSourceBaseband::MsgConfigureUDPSourceBaseband *msg = UDPSourceBaseband::MsgConfigureUDPSourceBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); if (settings.m_useReverseAPI) { @@ -854,9 +441,9 @@ void UDPSource::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& respons { response.getUdpSourceReport()->setInputPowerDb(CalcDb::dbPower(getInMagSq())); response.getUdpSourceReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); - response.getUdpSourceReport()->setSquelch(m_squelchOpen ? 1 : 0); + response.getUdpSourceReport()->setSquelch(m_basebandSource->isSquelchOpen() ? 1 : 0); response.getUdpSourceReport()->setBufferGauge(getBufferGauge()); - response.getUdpSourceReport()->setChannelSampleRate(m_outputSampleRate); + response.getUdpSourceReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); } void UDPSource::webapiReverseSendSettings(QList& channelSettingsKeys, const UDPSourceSettings& settings, bool force) @@ -937,13 +524,14 @@ void UDPSource::webapiReverseSendSettings(QList& channelSettingsKeys, c m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgChannelSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgChannelSettings; } @@ -958,10 +546,18 @@ void UDPSource::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("UDPSource::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("UDPSource::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } + +void UDPSource::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); +} \ No newline at end of file diff --git a/plugins/channeltx/udpsource/udpsource.h b/plugins/channeltx/udpsource/udpsource.h index 56f422895..a7dba4052 100644 --- a/plugins/channeltx/udpsource/udpsource.h +++ b/plugins/channeltx/udpsource/udpsource.h @@ -24,20 +24,15 @@ #include "dsp/basebandsamplesource.h" #include "channel/channelapi.h" #include "dsp/basebandsamplesink.h" -#include "dsp/interpolator.h" -#include "dsp/movingaverage.h" -#include "dsp/nco.h" -#include "dsp/fftfilt.h" #include "util/message.h" #include "udpsourcesettings.h" -#include "udpsourceudphandler.h" class QNetworkAccessManager; class QNetworkReply; +class QThread; class DeviceAPI; -class ThreadedBasebandSampleSource; -class UpChannelizer; +class UDPSourceBaseband; class UDPSource : public BasebandSampleSource, public ChannelAPI { Q_OBJECT @@ -67,26 +62,33 @@ public: } }; + /** + * |<------ Baseband from device (before device soft interpolation) -------------------------->| + * |<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->|<- Channel SR ------->| + * | ^-------------------------------| + * | | Source CF + * | | Source SR | + */ class MsgConfigureChannelizer : public Message { MESSAGE_CLASS_DECLARATION public: - int getSampleRate() const { return m_sampleRate; } - int getCenterFrequency() const { return m_centerFrequency; } + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } - static MsgConfigureChannelizer* create(int sampleRate, int centerFrequency) + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) { - return new MsgConfigureChannelizer(sampleRate, centerFrequency); + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); } private: - int m_sampleRate; - int m_centerFrequency; + int m_sourceSampleRate; + int m_sourceCenterFrequency; - MsgConfigureChannelizer(int sampleRate, int centerFrequency) : + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : Message(), - m_sampleRate(sampleRate), - m_centerFrequency(centerFrequency) + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) { } }; @@ -94,11 +96,9 @@ public: virtual ~UDPSource(); virtual void destroy() { delete this; } - void setSpectrumSink(BasebandSampleSink* spectrum) { m_spectrum = spectrum; } - virtual void start(); virtual void stop(); - virtual void pull(Sample& sample); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); virtual bool handleMessage(const Message& cmd); virtual void getIdentifier(QString& id) { id = objectName(); } @@ -141,210 +141,38 @@ public: const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response); - double getMagSq() const { return m_magsq; } - double getInMagSq() const { return m_inMagsq; } - int32_t getBufferGauge() const { return m_udpHandler.getBufferGauge(); } - bool getSquelchOpen() const { return m_squelchOpen; } - + double getMagSq() const; + double getInMagSq() const; + int32_t getBufferGauge() const; + bool getSquelchOpen(); + void setSpectrumSink(BasebandSampleSink* spectrum); void setSpectrum(bool enabled); void resetReadIndex(); + void setLevelMeter(QObject *levelMeter); static const QString m_channelIdURI; static const QString m_channelId; -signals: - /** - * Level changed - * \param rmsLevel RMS level in range 0.0 - 1.0 - * \param peakLevel Peak level in range 0.0 - 1.0 - * \param numSamples Number of audio samples analyzed - */ - void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); - private slots: void networkManagerFinished(QNetworkReply *reply); private: - class MsgUDPSourceSpectrum : public Message { - MESSAGE_CLASS_DECLARATION - - public: - bool getEnabled() const { return m_enabled; } - - static MsgUDPSourceSpectrum* create(bool enabled) - { - return new MsgUDPSourceSpectrum(enabled); - } - - private: - bool m_enabled; - - MsgUDPSourceSpectrum(bool enabled) : - Message(), - m_enabled(enabled) - { } - }; - - class MsgResetReadIndex : public Message { - MESSAGE_CLASS_DECLARATION - - public: - - static MsgResetReadIndex* create() - { - return new MsgResetReadIndex(); - } - - private: - - MsgResetReadIndex() : - Message() - { } - }; - DeviceAPI* m_deviceAPI; - ThreadedBasebandSampleSource* m_threadedChannelizer; - UpChannelizer* m_channelizer; - - int m_basebandSampleRate; - Real m_outputSampleRate; - int m_inputFrequencyOffset; + QThread *m_thread; + UDPSourceBaseband* m_basebandSource; UDPSourceSettings m_settings; - Real m_squelch; - - NCO m_carrierNco; - Complex m_modSample; - - BasebandSampleSink* m_spectrum; - bool m_spectrumEnabled; SampleVector m_sampleBuffer; - int m_spectrumChunkSize; - int m_spectrumChunkCounter; - - Interpolator m_interpolator; - Real m_interpolatorDistance; - Real m_interpolatorDistanceRemain; - bool m_interpolatorConsumed; - - double m_magsq; - double m_inMagsq; - MovingAverage m_movingAverage; - MovingAverage m_inMovingAverage; - - UDPSourceUDPHandler m_udpHandler; - Real m_actualInputSampleRate; //!< sample rate with UDP buffer skew compensation - double m_sampleRateSum; - int m_sampleRateAvgCounter; - - int m_levelCalcCount; - Real m_peakLevel; - double m_levelSum; - int m_levelNbSamples; - - bool m_squelchOpen; - int m_squelchOpenCount; - int m_squelchCloseCount; - int m_squelchThreshold; - - float m_modPhasor; //!< Phasor for FM modulation - fftfilt* m_SSBFilter; //!< Complex filter for SSB modulation - Complex* m_SSBFilterBuffer; - int m_SSBFilterBufferIndex; + QMutex m_settingsMutex; QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; - QMutex m_settingsMutex; - - static const int m_sampleRateAverageItems = 17; - static const int m_ssbFftLen = 1024; - - void applyChannelSettings(int basebandSampleRate, int outputSampleRate, int inputFrequencyOffset, bool force = false); void applySettings(const UDPSourceSettings& settings, bool force = false); - void modulateSample(); - void calculateLevel(Real sample); - void calculateLevel(Complex sample); void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); void webapiReverseSendSettings(QList& channelSettingsKeys, const UDPSourceSettings& settings, bool force); - inline void calculateSquelch(double value) - { - if ((!m_settings.m_squelchEnabled) || (value > m_squelch)) - { - if (m_squelchThreshold == 0) - { - m_squelchOpen = true; - } - else - { - if (m_squelchOpenCount < m_squelchThreshold) - { - m_squelchOpenCount++; - } - else - { - m_squelchCloseCount = m_squelchThreshold; - m_squelchOpen = true; - } - } - } - else - { - if (m_squelchThreshold == 0) - { - m_squelchOpen = false; - } - else - { - if (m_squelchCloseCount > 0) - { - m_squelchCloseCount--; - } - else - { - m_squelchOpenCount = 0; - m_squelchOpen = false; - } - } - } - } - - inline void initSquelch(bool open) - { - if (open) - { - m_squelchOpen = true; - m_squelchOpenCount = m_squelchThreshold; - m_squelchCloseCount = m_squelchThreshold; - } - else - { - m_squelchOpen = false; - m_squelchOpenCount = 0; - m_squelchCloseCount = 0; - } - } - - inline void readMonoSample(qint16& t) - { - - if (m_settings.m_stereoInput) - { - AudioSample a; - m_udpHandler.readSample(a); - t = ((a.l + a.r) * m_settings.m_gainIn) / 2; - } - else - { - m_udpHandler.readSample(t); - t *= m_settings.m_gainIn; - } - } }; - - - #endif /* PLUGINS_CHANNELTX_UDPSINK_UDPSOURCE_H_ */ diff --git a/plugins/channeltx/udpsource/udpsourcebaseband.cpp b/plugins/channeltx/udpsource/udpsourcebaseband.cpp new file mode 100644 index 000000000..5eb5dbd5d --- /dev/null +++ b/plugins/channeltx/udpsource/udpsourcebaseband.cpp @@ -0,0 +1,204 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upsamplechannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "udpsourcebaseband.h" +#include "udpsourcemsg.h" + +MESSAGE_CLASS_DEFINITION(UDPSourceBaseband::MsgConfigureUDPSourceBaseband, Message) +MESSAGE_CLASS_DEFINITION(UDPSourceBaseband::MsgConfigureChannelizer, Message) +MESSAGE_CLASS_DEFINITION(UDPSourceBaseband::MsgUDPSourceSpectrum, Message) +MESSAGE_CLASS_DEFINITION(UDPSourceBaseband::MsgResetReadIndex, Message) + +UDPSourceBaseband::UDPSourceBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpSampleChannelizer(&m_source); + + qDebug("UDPSourceBaseband::UDPSourceBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &UDPSourceBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_source.setUDPFeedbackMessageQueue(&m_inputMessageQueue); +} + +UDPSourceBaseband::~UDPSourceBaseband() +{ + delete m_channelizer; +} + +void UDPSourceBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void UDPSourceBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void UDPSourceBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + Real rmsLevel, peakLevel, numSamples; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); +} + +void UDPSourceBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void UDPSourceBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool UDPSourceBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureUDPSourceBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureUDPSourceBaseband& cfg = (MsgConfigureUDPSourceBaseband&) cmd; + qDebug() << "UDPSourceBaseband::handleMessage: MsgConfigureAMModBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgConfigureChannelizer::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureChannelizer& cfg = (MsgConfigureChannelizer&) cmd; + qDebug() << "UDPSourceBaseband::handleMessage: MsgConfigureChannelizer" + << "(requested) sourceSampleRate: " << cfg.getSourceSampleRate() + << "(requested) sourceCenterFrequency: " << cfg.getSourceCenterFrequency(); + m_channelizer->setChannelization(cfg.getSourceSampleRate(), cfg.getSourceCenterFrequency()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "UDPSourceBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else if (UDPSourceMessages::MsgSampleRateCorrection::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + UDPSourceMessages::MsgSampleRateCorrection& notif = (UDPSourceMessages::MsgSampleRateCorrection&) cmd; + qDebug() << "UDPSourceBaseband::handleMessage: MsgSampleRateCorrection:" + << "corr: " << notif.getCorrectionFactor() + << "ratio: " << notif.getRawDeltaRatio(); + m_source.sampleRateCorrection(notif.getRawDeltaRatio(), notif.getCorrectionFactor()); + + return true; + } + else + { + return false; + } +} + +void UDPSourceBaseband::applySettings(const UDPSourceSettings& settings, bool force) +{ + m_source.applySettings(settings, force); + m_settings = settings; +} + +int UDPSourceBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} + +bool UDPSourceBaseband::isSquelchOpen() const +{ + return m_source.getSquelchOpen(); +} \ No newline at end of file diff --git a/plugins/channeltx/udpsource/udpsourcebaseband.h b/plugins/channeltx/udpsource/udpsourcebaseband.h new file mode 100644 index 000000000..fe38ed86a --- /dev/null +++ b/plugins/channeltx/udpsource/udpsourcebaseband.h @@ -0,0 +1,155 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_UDPSOURCEBASEBAND_H +#define INCLUDE_UDPSOURCEBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "udpsourcesource.h" + +class UpSampleChannelizer; + +class UDPSourceBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureUDPSourceBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const UDPSourceSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureUDPSourceBaseband* create(const UDPSourceSettings& settings, bool force) + { + return new MsgConfigureUDPSourceBaseband(settings, force); + } + + private: + UDPSourceSettings m_settings; + bool m_force; + + MsgConfigureUDPSourceBaseband(const UDPSourceSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureChannelizer : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSourceSampleRate() const { return m_sourceSampleRate; } + int getSourceCenterFrequency() const { return m_sourceCenterFrequency; } + + static MsgConfigureChannelizer* create(int sourceSampleRate, int sourceCenterFrequency) + { + return new MsgConfigureChannelizer(sourceSampleRate, sourceCenterFrequency); + } + + private: + int m_sourceSampleRate; + int m_sourceCenterFrequency; + + MsgConfigureChannelizer(int sourceSampleRate, int sourceCenterFrequency) : + Message(), + m_sourceSampleRate(sourceSampleRate), + m_sourceCenterFrequency(sourceCenterFrequency) + { } + }; + + class MsgUDPSourceSpectrum : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getEnabled() const { return m_enabled; } + + static MsgUDPSourceSpectrum* create(bool enabled) + { + return new MsgUDPSourceSpectrum(enabled); + } + + private: + bool m_enabled; + + MsgUDPSourceSpectrum(bool enabled) : + Message(), + m_enabled(enabled) + { } + }; + + class MsgResetReadIndex : public Message { + MESSAGE_CLASS_DECLARATION + + public: + + static MsgResetReadIndex* create() + { + return new MsgResetReadIndex(); + } + + private: + + MsgResetReadIndex() : + Message() + { } + }; + + UDPSourceBaseband(); + ~UDPSourceBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + double getMagSq() const { return m_source.getMagSq(); } + int getChannelSampleRate() const; + bool isSquelchOpen() const; + void setSpectrumSink(BasebandSampleSink *spectrumSink) { m_source.setSpectrumSink(spectrumSink); } + +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + SampleSourceFifo m_sampleFifo; + UpSampleChannelizer *m_channelizer; + UDPSourceSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + UDPSourceSettings m_settings; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const UDPSourceSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_UDPSOURCEBASEBAND_H diff --git a/plugins/channeltx/udpsource/udpsourcegui.cpp b/plugins/channeltx/udpsource/udpsourcegui.cpp index f21228cf2..724a9e525 100644 --- a/plugins/channeltx/udpsource/udpsourcegui.cpp +++ b/plugins/channeltx/udpsource/udpsourcegui.cpp @@ -172,7 +172,7 @@ UDPSourceGUI::UDPSourceGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb ui->spectrumGUI->setBuddies(m_spectrumVis->getInputMessageQueue(), m_spectrumVis, ui->glSpectrum); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); - connect(m_udpSource, SIGNAL(levelChanged(qreal, qreal, int)), ui->volumeMeter, SLOT(levelChanged(qreal, qreal, int))); + m_udpSource->setLevelMeter(ui->volumeMeter); displaySettings(); applySettings(true); diff --git a/plugins/channeltx/udpsource/udpsourceplugin.cpp b/plugins/channeltx/udpsource/udpsourceplugin.cpp index a02da15a8..6af51170a 100644 --- a/plugins/channeltx/udpsource/udpsourceplugin.cpp +++ b/plugins/channeltx/udpsource/udpsourceplugin.cpp @@ -30,7 +30,7 @@ const PluginDescriptor UDPSourcePlugin::m_pluginDescriptor = { QString("UDP Channel Source"), - QString("4.11.6"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/channeltx/udpsource/udpsourcesource.cpp b/plugins/channeltx/udpsource/udpsourcesource.cpp new file mode 100644 index 000000000..aa264fe1b --- /dev/null +++ b/plugins/channeltx/udpsource/udpsourcesource.cpp @@ -0,0 +1,457 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017-2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspcommands.h" +#include "dsp/basebandsamplesink.h" +#include "util/db.h" + +#include "udpsourcesource.h" +#include "udpsourcemsg.h" + +UDPSourceSource::UDPSourceSource() : + m_channelSampleRate(48000), + m_channelFrequencyOffset(0), + m_squelch(1e-6), + m_spectrumSink(nullptr), + m_spectrumEnabled(false), + m_spectrumChunkSize(2160), + m_spectrumChunkCounter(0), + m_magsq(1e-10), + m_movingAverage(16, 1e-10), + m_inMovingAverage(480, 1e-10), + m_sampleRateSum(0), + m_sampleRateAvgCounter(0), + m_levelCalcCount(0), + m_peakLevel(0.0f), + m_levelSum(0.0f), + m_levelNbSamples(480), + m_squelchOpen(false), + m_squelchOpenCount(0), + m_squelchCloseCount(0), + m_squelchThreshold(4800), + m_modPhasor(0.0f), + m_SSBFilterBufferIndex(0) +{ + m_SSBFilter = new fftfilt(m_settings.m_lowCutoff / m_settings.m_inputSampleRate, m_settings.m_rfBandwidth / m_settings.m_inputSampleRate, m_ssbFftLen); + m_SSBFilterBuffer = new Complex[m_ssbFftLen>>1]; // filter returns data exactly half of its size + m_magsq = 0.0; + + m_udpHandler.start(); + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +UDPSourceSource::~UDPSourceSource() +{ + m_udpHandler.stop(); + delete m_SSBFilter; + delete[] m_SSBFilterBuffer; +} + +void UDPSourceSource::setUDPFeedbackMessageQueue(MessageQueue *messageQueue) +{ + m_udpHandler.setFeedbackMessageQueue(messageQueue); +} + +void UDPSourceSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void UDPSourceSource::pullOne(Sample& sample) +{ + if (m_settings.m_channelMute) + { + sample.m_real = 0.0f; + sample.m_imag = 0.0f; + initSquelch(false); + return; + } + + Complex ci; + + if (m_interpolatorDistance > 1.0f) // decimate + { + modulateSample(); + + while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + else + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + + m_interpolatorDistanceRemain += m_interpolatorDistance; + + ci *= m_carrierNco.nextIQ(); // shift to carrier frequency + double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); + magsq /= (SDR_TX_SCALED*SDR_TX_SCALED); + m_movingAverage.feed(magsq); + m_magsq = m_movingAverage.average(); + + sample.m_real = (FixReal) ci.real(); + sample.m_imag = (FixReal) ci.imag(); +} + +void UDPSourceSource::modulateSample() +{ + if (m_settings.m_sampleFormat == UDPSourceSettings::FormatSnLE) // Linear I/Q transponding + { + Sample s; + + m_udpHandler.readSample(s); + + uint64_t magsq = s.m_real * s.m_real + s.m_imag * s.m_imag; + m_inMovingAverage.feed(magsq/(SDR_TX_SCALED*SDR_TX_SCALED)); + m_inMagsq = m_inMovingAverage.average(); + + calculateSquelch(m_inMagsq); + + if (m_squelchOpen) + { + m_modSample.real(s.m_real * m_settings.m_gainOut); + m_modSample.imag(s.m_imag * m_settings.m_gainOut); + calculateLevel(m_modSample); + } + else + { + m_modSample.real(0.0f); + m_modSample.imag(0.0f); + } + } + else if (m_settings.m_sampleFormat == UDPSourceSettings::FormatNFM) + { + qint16 t; + readMonoSample(t); + + m_inMovingAverage.feed((t*t)/1073741824.0); + m_inMagsq = m_inMovingAverage.average(); + + calculateSquelch(m_inMagsq); + + if (m_squelchOpen) + { + m_modPhasor += (m_settings.m_fmDeviation / m_settings.m_inputSampleRate) * (t / SDR_TX_SCALEF) * M_PI * 2.0f; + m_modSample.real(cos(m_modPhasor) * 0.3162292f * SDR_TX_SCALEF * m_settings.m_gainOut); + m_modSample.imag(sin(m_modPhasor) * 0.3162292f * SDR_TX_SCALEF * m_settings.m_gainOut); + calculateLevel(m_modSample); + } + else + { + m_modSample.real(0.0f); + m_modSample.imag(0.0f); + } + } + else if (m_settings.m_sampleFormat == UDPSourceSettings::FormatAM) + { + qint16 t; + readMonoSample(t); + m_inMovingAverage.feed((t*t)/(SDR_TX_SCALED*SDR_TX_SCALED)); + m_inMagsq = m_inMovingAverage.average(); + + calculateSquelch(m_inMagsq); + + if (m_squelchOpen) + { + m_modSample.real(((t / SDR_TX_SCALEF)*m_settings.m_amModFactor*m_settings.m_gainOut + 1.0f) * (SDR_TX_SCALEF/2)); // modulate and scale zero frequency carrier + m_modSample.imag(0.0f); + calculateLevel(m_modSample); + } + else + { + m_modSample.real(0.0f); + m_modSample.imag(0.0f); + } + } + else if ((m_settings.m_sampleFormat == UDPSourceSettings::FormatLSB) || (m_settings.m_sampleFormat == UDPSourceSettings::FormatUSB)) + { + qint16 t; + Complex c, ci; + fftfilt::cmplx *filtered; + int n_out = 0; + + readMonoSample(t); + m_inMovingAverage.feed((t*t)/(SDR_TX_SCALED*SDR_TX_SCALED)); + m_inMagsq = m_inMovingAverage.average(); + + calculateSquelch(m_inMagsq); + + if (m_squelchOpen) + { + ci.real((t / SDR_TX_SCALEF) * m_settings.m_gainOut); + ci.imag(0.0f); + + n_out = m_SSBFilter->runSSB(ci, &filtered, (m_settings.m_sampleFormat == UDPSourceSettings::FormatUSB)); + + if (n_out > 0) + { + memcpy((void *) m_SSBFilterBuffer, (const void *) filtered, n_out*sizeof(Complex)); + m_SSBFilterBufferIndex = 0; + } + + c = m_SSBFilterBuffer[m_SSBFilterBufferIndex]; + m_modSample.real(m_SSBFilterBuffer[m_SSBFilterBufferIndex].real() * SDR_TX_SCALEF); + m_modSample.imag(m_SSBFilterBuffer[m_SSBFilterBufferIndex].imag() * SDR_TX_SCALEF); + m_SSBFilterBufferIndex++; + + calculateLevel(m_modSample); + } + else + { + m_modSample.real(0.0f); + m_modSample.imag(0.0f); + } + } + else + { + m_modSample.real(0.0f); + m_modSample.imag(0.0f); + initSquelch(false); + } + + if (m_spectrumSink && m_spectrumEnabled && (m_spectrumChunkCounter < m_spectrumChunkSize - 1)) + { + Sample s; + s.m_real = (FixReal) m_modSample.real(); + s.m_imag = (FixReal) m_modSample.imag(); + m_sampleBuffer.push_back(s); + m_spectrumChunkCounter++; + } + else if (m_spectrumSink) + { + m_spectrumSink->feed(m_sampleBuffer.begin(), m_sampleBuffer.end(), false); + m_sampleBuffer.clear(); + m_spectrumChunkCounter = 0; + } +} + +void UDPSourceSource::calculateLevel(Real sample) +{ + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), sample); + m_levelSum += sample * sample; + m_levelCalcCount++; + } + else + { + m_rmsLevel = m_levelSum > 0.0 ? sqrt(m_levelSum / m_levelNbSamples) : 0.0; + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void UDPSourceSource::calculateLevel(Complex sample) +{ + Real t = std::abs(sample); + + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), t); + m_levelSum += (t * t); + m_levelCalcCount++; + } + else + { + m_rmsLevel = m_levelSum > 0.0 ? sqrt((m_levelSum/(SDR_TX_SCALED*SDR_TX_SCALED)) / m_levelNbSamples) : 0.0; + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void UDPSourceSource::setSpectrumEnabled(bool enabled) +{ + m_spectrumEnabled = enabled; +} + +void UDPSourceSource::resetReadIndex() +{ + m_udpHandler.resetReadIndex(); +} + +void UDPSourceSource::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "UDPSourceSource::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((channelFrequencyOffset != m_channelFrequencyOffset) || + (channelSampleRate != m_channelSampleRate) || force) + { + m_carrierNco.setFreq(channelFrequencyOffset, channelSampleRate); + } + + if (((channelSampleRate != m_channelSampleRate) && (!m_settings.m_autoRWBalance)) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_settings.m_inputSampleRate / (Real) channelSampleRate; + m_interpolator.create(48, m_settings.m_inputSampleRate, m_settings.m_rfBandwidth / 2.2, 3.0); + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void UDPSourceSource::applySettings(const UDPSourceSettings& settings, bool force) +{ + qDebug() << "UDPSourceSource::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_sampleFormat: " << settings.m_sampleFormat + << " m_inputSampleRate: " << settings.m_inputSampleRate + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " m_lowCutoff: " << settings.m_lowCutoff + << " m_fmDeviation: " << settings.m_fmDeviation + << " m_amModFactor: " << settings.m_amModFactor + << " m_udpAddressStr: " << settings.m_udpAddress + << " m_udpPort: " << settings.m_udpPort + << " m_channelMute: " << settings.m_channelMute + << " m_gainIn: " << settings.m_gainIn + << " m_gainOut: " << settings.m_gainOut + << " m_squelchGate: " << settings.m_squelchGate + << " m_squelch: " << settings.m_squelch << "dB" + << " m_squelchEnabled: " << settings.m_squelchEnabled + << " m_autoRWBalance: " << settings.m_autoRWBalance + << " m_stereoInput: " << settings.m_stereoInput + << " force: " << force; + + if((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || + (settings.m_lowCutoff != m_settings.m_lowCutoff) || + (settings.m_inputSampleRate != m_settings.m_inputSampleRate) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) settings.m_inputSampleRate / (Real) m_channelSampleRate; + m_interpolator.create(48, settings.m_inputSampleRate, settings.m_rfBandwidth / 2.2, 3.0); + m_actualInputSampleRate = settings.m_inputSampleRate; + m_udpHandler.resetReadIndex(); + m_sampleRateSum = 0.0; + m_sampleRateAvgCounter = 0; + m_spectrumChunkSize = settings.m_inputSampleRate * 0.05; // 50 ms chunk + m_spectrumChunkCounter = 0; + m_levelNbSamples = settings.m_inputSampleRate * 0.01; // every 10 ms + m_levelCalcCount = 0; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_udpHandler.resizeBuffer(settings.m_inputSampleRate); + m_inMovingAverage.resize(settings.m_inputSampleRate * 0.01, 1e-10); // 10 ms + m_squelchThreshold = settings.m_inputSampleRate * settings.m_squelchGate; + initSquelch(m_squelchOpen); + m_SSBFilter->create_filter(settings.m_lowCutoff / settings.m_inputSampleRate, settings.m_rfBandwidth / settings.m_inputSampleRate); + } + + if ((settings.m_squelch != m_settings.m_squelch) || force) + { + m_squelch = CalcDb::powerFromdB(settings.m_squelch); + } + + if ((settings.m_squelchGate != m_settings.m_squelchGate) || force) + { + m_squelchThreshold = m_channelSampleRate * settings.m_squelchGate; + initSquelch(m_squelchOpen); + } + + if ((settings.m_udpAddress != m_settings.m_udpAddress) || + (settings.m_udpPort != m_settings.m_udpPort) || force) + { + m_udpHandler.configureUDPLink(settings.m_udpAddress, settings.m_udpPort); + } + + if ((settings.m_channelMute != m_settings.m_channelMute) || force) + { + if (!settings.m_channelMute) { + m_udpHandler.resetReadIndex(); + } + } + + if ((settings.m_autoRWBalance != m_settings.m_autoRWBalance) || force) + { + m_udpHandler.setAutoRWBalance(settings.m_autoRWBalance); + + if (!settings.m_autoRWBalance) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) settings.m_inputSampleRate / (Real) m_channelSampleRate; + m_interpolator.create(48, settings.m_inputSampleRate, settings.m_rfBandwidth / 2.2, 3.0); + m_actualInputSampleRate = settings.m_inputSampleRate; + m_udpHandler.resetReadIndex(); + } + } + + m_settings = settings; +} + +void UDPSourceSource::sampleRateCorrection(float rawDeltaRatio, float correctionFactor) +{ + float newSampleRate = m_actualInputSampleRate + correctionFactor * m_actualInputSampleRate; + + // exclude values too way out nominal sample rate (20%) + if ((newSampleRate < m_settings.m_inputSampleRate * 1.2) && (newSampleRate > m_settings.m_inputSampleRate * 0.8)) + { + m_actualInputSampleRate = newSampleRate; + + if ((rawDeltaRatio > -0.05) && (rawDeltaRatio < 0.05)) + { + if (m_sampleRateAvgCounter < m_sampleRateAverageItems) + { + m_sampleRateSum += m_actualInputSampleRate; + m_sampleRateAvgCounter++; + } + } + else + { + m_sampleRateSum = 0.0; + m_sampleRateAvgCounter = 0; + } + + if (m_sampleRateAvgCounter == m_sampleRateAverageItems) + { + float avgRate = m_sampleRateSum / m_sampleRateAverageItems; + qDebug("UDPSourceSource::sampleRateCorrection: corr: %+.6f new rate: %.0f: avg rate: %.0f", + correctionFactor, + m_actualInputSampleRate, + avgRate); + m_actualInputSampleRate = avgRate; + m_sampleRateSum = 0.0; + m_sampleRateAvgCounter = 0; + } + + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_actualInputSampleRate / (Real) m_channelSampleRate; + } +} \ No newline at end of file diff --git a/plugins/channeltx/udpsource/udpsourcesource.h b/plugins/channeltx/udpsource/udpsourcesource.h new file mode 100644 index 000000000..b3bd96a80 --- /dev/null +++ b/plugins/channeltx/udpsource/udpsourcesource.h @@ -0,0 +1,197 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017-2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_UDPSINK_UDPSOURCESOURCE_H_ +#define PLUGINS_CHANNELTX_UDPSINK_UDPSOURCESOURCE_H_ + +#include +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/interpolator.h" +#include "dsp/movingaverage.h" +#include "dsp/nco.h" +#include "dsp/fftfilt.h" + +#include "udpsourcesettings.h" +#include "udpsourceudphandler.h" + +class BasebandSampleSink; +class MessageQueue; + +class UDPSourceSource : public ChannelSampleSource { +public: + UDPSourceSource(); + virtual ~UDPSourceSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples) {}; + + void setUDPFeedbackMessageQueue(MessageQueue *messageQueue); + void setSpectrumSink(BasebandSampleSink* spectrumSink) { m_spectrumSink = spectrumSink; } + double getMagSq() const { return m_magsq; } + double getInMagSq() const { return m_inMagsq; } + int32_t getBufferGauge() const { return m_udpHandler.getBufferGauge(); } + bool getSquelchOpen() const { return m_squelchOpen; } + + void setSpectrumEnabled(bool enabled); + void resetReadIndex(); + + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const UDPSourceSettings& settings, bool force = false); + + void getLevels(Real& rmsLevel, Real& peakLevel, Real& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevel; + numSamples = m_levelNbSamples; + } + + void sampleRateCorrection(float rawDeltaRatio, float correctionFactor); + +private: + int m_channelSampleRate; + int m_channelFrequencyOffset; + UDPSourceSettings m_settings; + + Real m_squelch; + + NCO m_carrierNco; + Complex m_modSample; + + BasebandSampleSink* m_spectrumSink; + bool m_spectrumEnabled; + SampleVector m_sampleBuffer; + int m_spectrumChunkSize; + int m_spectrumChunkCounter; + + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + bool m_interpolatorConsumed; + + double m_magsq; + double m_inMagsq; + MovingAverage m_movingAverage; + MovingAverage m_inMovingAverage; + + UDPSourceUDPHandler m_udpHandler; + Real m_actualInputSampleRate; //!< sample rate with UDP buffer skew compensation + double m_sampleRateSum; + int m_sampleRateAvgCounter; + + int m_levelCalcCount; + Real m_rmsLevel; + Real m_peakLevelOut; + Real m_peakLevel; + double m_levelSum; + int m_levelNbSamples; + + bool m_squelchOpen; + int m_squelchOpenCount; + int m_squelchCloseCount; + int m_squelchThreshold; + + float m_modPhasor; //!< Phasor for FM modulation + fftfilt* m_SSBFilter; //!< Complex filter for SSB modulation + Complex* m_SSBFilterBuffer; + int m_SSBFilterBufferIndex; + + static const int m_sampleRateAverageItems = 17; + static const int m_ssbFftLen = 1024; + + void modulateSample(); + void calculateLevel(Real sample); + void calculateLevel(Complex sample); + + inline void calculateSquelch(double value) + { + if ((!m_settings.m_squelchEnabled) || (value > m_squelch)) + { + if (m_squelchThreshold == 0) + { + m_squelchOpen = true; + } + else + { + if (m_squelchOpenCount < m_squelchThreshold) + { + m_squelchOpenCount++; + } + else + { + m_squelchCloseCount = m_squelchThreshold; + m_squelchOpen = true; + } + } + } + else + { + if (m_squelchThreshold == 0) + { + m_squelchOpen = false; + } + else + { + if (m_squelchCloseCount > 0) + { + m_squelchCloseCount--; + } + else + { + m_squelchOpenCount = 0; + m_squelchOpen = false; + } + } + } + } + + inline void initSquelch(bool open) + { + if (open) + { + m_squelchOpen = true; + m_squelchOpenCount = m_squelchThreshold; + m_squelchCloseCount = m_squelchThreshold; + } + else + { + m_squelchOpen = false; + m_squelchOpenCount = 0; + m_squelchCloseCount = 0; + } + } + + inline void readMonoSample(qint16& t) + { + + if (m_settings.m_stereoInput) + { + AudioSample a; + m_udpHandler.readSample(a); + t = ((a.l + a.r) * m_settings.m_gainIn) / 2; + } + else + { + m_udpHandler.readSample(t); + t *= m_settings.m_gainIn; + } + } +}; + +#endif /* PLUGINS_CHANNELTX_UDPSINK_UDPSOURCESOURCE_H_ */ diff --git a/plugins/channeltx/udpsource/udpsourceudphandler.cpp b/plugins/channeltx/udpsource/udpsourceudphandler.cpp index bec881ff1..218cbd576 100644 --- a/plugins/channeltx/udpsource/udpsourceudphandler.cpp +++ b/plugins/channeltx/udpsource/udpsourceudphandler.cpp @@ -26,7 +26,7 @@ MESSAGE_CLASS_DEFINITION(UDPSourceUDPHandler::MsgUDPAddressAndPort, Message) UDPSourceUDPHandler::UDPSourceUDPHandler() : - m_dataSocket(0), + m_dataSocket(nullptr), m_dataAddress(QHostAddress::LocalHost), m_remoteAddress(QHostAddress::LocalHost), m_dataPort(9999), @@ -41,7 +41,7 @@ UDPSourceUDPHandler::UDPSourceUDPHandler() : m_rwDelta(m_minNbUDPFrames/2), m_d(0), m_autoRWBalance(true), - m_feedbackMessageQueue(0) + m_feedbackMessageQueue(nullptr) { m_udpBuf = new udpBlk_t[m_minNbUDPFrames]; std::fill(m_udpDump, m_udpDump + m_udpBlockSize + 8192, 0); diff --git a/plugins/samplemimo/testmi/testmi.cpp b/plugins/samplemimo/testmi/testmi.cpp index 645a1b190..d15da58a5 100644 --- a/plugins/samplemimo/testmi/testmi.cpp +++ b/plugins/samplemimo/testmi/testmi.cpp @@ -816,13 +816,14 @@ void TestMI::webapiReverseSendSettings(const DeviceSettingsKeys& deviceSettingsK m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgDeviceSettings; } @@ -841,17 +842,19 @@ void TestMI::webapiReverseSendStartStop(bool start) m_networkRequest.setUrl(QUrl(channelSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); + QNetworkReply *reply; if (start) { - m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); } else { - m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); } + buffer->setParent(reply); delete swgDeviceSettings; } @@ -865,12 +868,15 @@ void TestMI::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("TestMI::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("TestMI::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } bool TestMI::isRecording(unsigned int istream) const diff --git a/plugins/samplesink/CMakeLists.txt b/plugins/samplesink/CMakeLists.txt index b979ae7b8..829f955b7 100644 --- a/plugins/samplesink/CMakeLists.txt +++ b/plugins/samplesink/CMakeLists.txt @@ -1,7 +1,7 @@ project(samplesink) -add_subdirectory(filesink) add_subdirectory(testsink) +add_subdirectory(filesink) add_subdirectory(localoutput) if(CM256CC_FOUND) diff --git a/plugins/samplesink/bladerf1output/bladerf1output.cpp b/plugins/samplesink/bladerf1output/bladerf1output.cpp index daf9588b1..dc172ad2d 100644 --- a/plugins/samplesink/bladerf1output/bladerf1output.cpp +++ b/plugins/samplesink/bladerf1output/bladerf1output.cpp @@ -41,12 +41,12 @@ MESSAGE_CLASS_DEFINITION(Bladerf1Output::MsgReportBladerf1, Message) Bladerf1Output::Bladerf1Output(DeviceAPI *deviceAPI) : m_deviceAPI(deviceAPI), m_settings(), - m_dev(0), - m_bladerfThread(0), + m_dev(nullptr), + m_bladerfThread(nullptr), m_deviceDescription("BladeRFOutput"), m_running(false) { - m_sampleSourceFifo.resize(16*BLADERFOUTPUT_BLOCKSIZE); + m_sampleSourceFifo.resize(SampleSourceFifo::getSizePolicy(m_settings.m_devSampleRate)); openDevice(); m_deviceAPI->setNbSinkStreams(1); m_deviceAPI->setBuddySharedPtr(&m_sharedParams); @@ -81,7 +81,7 @@ bool Bladerf1Output::openDevice() int res; - m_sampleSourceFifo.resize(m_settings.m_devSampleRate/(1<<(m_settings.m_log2Interp <= 4 ? m_settings.m_log2Interp : 4))); + m_sampleSourceFifo.resize(SampleSourceFifo::getSizePolicy(m_settings.m_devSampleRate)); if (m_deviceAPI->getSourceBuddies().size() > 0) { @@ -341,20 +341,10 @@ bool Bladerf1Output::applySettings(const BladeRF1OutputSettings& settings, bool if ((m_settings.m_devSampleRate != settings.m_devSampleRate) || (m_settings.m_log2Interp != settings.m_log2Interp) || force) { - int fifoSize; - - if (settings.m_log2Interp >= 5) - { - fifoSize = DeviceBladeRF1Shared::m_sampleFifoMinSize32; - } - else - { - fifoSize = (std::max)( - (int) ((settings.m_devSampleRate/(1<& deviceSettingsKey m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgDeviceSettings; } @@ -750,17 +741,19 @@ void Bladerf1Output::webapiReverseSendStartStop(bool start) m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); + QNetworkReply *reply; if (start) { - m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); } else { - m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); } + buffer->setParent(reply); delete swgDeviceSettings; } @@ -774,10 +767,13 @@ void Bladerf1Output::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("Bladerf1Output::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("Bladerf1Output::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } diff --git a/plugins/samplesink/bladerf1output/bladerf1outputplugin.cpp b/plugins/samplesink/bladerf1output/bladerf1outputplugin.cpp index c3b084363..ada25a971 100644 --- a/plugins/samplesink/bladerf1output/bladerf1outputplugin.cpp +++ b/plugins/samplesink/bladerf1output/bladerf1outputplugin.cpp @@ -31,7 +31,7 @@ const PluginDescriptor Bladerf1OutputPlugin::m_pluginDescriptor = { QString("BladeRF1 Output"), - QString("4.11.10"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/samplesink/bladerf1output/bladerf1outputthread.cpp b/plugins/samplesink/bladerf1output/bladerf1outputthread.cpp index 57c63d3de..a76a5c4c3 100644 --- a/plugins/samplesink/bladerf1output/bladerf1outputthread.cpp +++ b/plugins/samplesink/bladerf1output/bladerf1outputthread.cpp @@ -21,9 +21,9 @@ #include #include +#include "dsp/samplesourcefifo.h" - -Bladerf1OutputThread::Bladerf1OutputThread(struct bladerf* dev, SampleSourceFifoDB* sampleFifo, QObject* parent) : +Bladerf1OutputThread::Bladerf1OutputThread(struct bladerf* dev, SampleSourceFifo* sampleFifo, QObject* parent) : QThread(parent), m_running(false), m_dev(dev), @@ -82,35 +82,51 @@ void Bladerf1OutputThread::run() // Interpolate according to specified log2 (ex: log2=4 => decim=16) void Bladerf1OutputThread::callback(qint16* buf, qint32 len) { - SampleVector::iterator beginRead; - m_sampleFifo->readAdvance(beginRead, len/(1<getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_sampleFifo->read(len/(1< #include #include -#include "dsp/samplesourcefifodb.h" #include "dsp/interpolators.h" #define BLADERFOUTPUT_BLOCKSIZE (1<<16) +class SampleSourceFifo; + class Bladerf1OutputThread : public QThread { Q_OBJECT public: - Bladerf1OutputThread(struct bladerf* dev, SampleSourceFifoDB* sampleFifo, QObject* parent = NULL); + Bladerf1OutputThread(struct bladerf* dev, SampleSourceFifo* sampleFifo, QObject* parent = NULL); ~Bladerf1OutputThread(); void startWork(); @@ -46,7 +47,7 @@ private: struct bladerf* m_dev; qint16 m_buf[2*BLADERFOUTPUT_BLOCKSIZE]; - SampleSourceFifoDB* m_sampleFifo; + SampleSourceFifo* m_sampleFifo; unsigned int m_log2Interp; @@ -54,6 +55,7 @@ private: void run(); void callback(qint16* buf, qint32 len); + void callbackPart(qint16* buf, SampleVector& data, unsigned int iBegin, unsigned int iEnd); }; #endif // INCLUDE_BLADERFOUTPUTTHREAD_H diff --git a/plugins/samplesink/bladerf2output/bladerf2output.cpp b/plugins/samplesink/bladerf2output/bladerf2output.cpp index 440fddcd4..95a415488 100644 --- a/plugins/samplesink/bladerf2output/bladerf2output.cpp +++ b/plugins/samplesink/bladerf2output/bladerf2output.cpp @@ -45,8 +45,8 @@ MESSAGE_CLASS_DEFINITION(BladeRF2Output::MsgReportGainRange, Message) BladeRF2Output::BladeRF2Output(DeviceAPI *deviceAPI) : m_deviceAPI(deviceAPI), m_settings(), - m_dev(0), - m_thread(0), + m_dev(nullptr), + m_thread(nullptr), m_deviceDescription("BladeRF2Output"), m_running(false) { @@ -75,7 +75,7 @@ void BladeRF2Output::destroy() bool BladeRF2Output::openDevice() { - m_sampleSourceFifo.resize(m_settings.m_devSampleRate/(1<<(m_settings.m_log2Interp <= 4 ? m_settings.m_log2Interp : 4))); + m_sampleSourceFifo.resize(SampleSourceFifo::getSizePolicy(m_settings.m_devSampleRate)); // look for Tx buddies and get reference to the device object if (m_deviceAPI->getSinkBuddies().size() > 0) // look sink sibling first @@ -279,7 +279,7 @@ bool BladeRF2Output::start() { qDebug("BladeRF2Output::start: expand channels. Re-allocate thread and take ownership"); - SampleSourceFifoDB **fifos = new SampleSourceFifoDB*[nbOriginalChannels]; + SampleSourceFifo **fifos = new SampleSourceFifo*[nbOriginalChannels]; unsigned int *log2Interps = new unsigned int[nbOriginalChannels]; for (int i = 0; i < nbOriginalChannels; i++) // save original FIFO references and data @@ -407,7 +407,7 @@ void BladeRF2Output::stop() { qDebug("BladeRF2Output::stop: MO mode. Reduce by deleting and re-creating the thread"); bladeRF2OutputThread->stopWork(); - SampleSourceFifoDB **fifos = new SampleSourceFifoDB*[nbOriginalChannels-1]; + SampleSourceFifo **fifos = new SampleSourceFifo*[nbOriginalChannels-1]; unsigned int *log2Interps = new unsigned int[nbOriginalChannels-1]; bool stillActiveFIFO = false; @@ -714,7 +714,7 @@ bool BladeRF2Output::applySettings(const BladeRF2OutputSettings& settings, bool { reverseAPIKeys.append("devSampleRate"); BladeRF2OutputThread *bladeRF2OutputThread = findThread(); - SampleSourceFifoDB *fifo = 0; + SampleSourceFifo *fifo = nullptr; if (bladeRF2OutputThread) { @@ -722,20 +722,10 @@ bool BladeRF2Output::applySettings(const BladeRF2OutputSettings& settings, bool bladeRF2OutputThread->setFifo(requestedChannel, 0); } - int fifoSize; - - if (settings.m_log2Interp >= 5) - { - fifoSize = DeviceBladeRF2Shared::m_sampleFifoMinSize32; - } - else - { - fifoSize = (std::max)( - (int) ((settings.m_devSampleRate/(1<setFifo(requestedChannel, &m_sampleSourceFifo); @@ -1173,13 +1163,14 @@ void BladeRF2Output::webapiReverseSendSettings(QList& deviceSettingsKey m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgDeviceSettings; } @@ -1198,17 +1189,19 @@ void BladeRF2Output::webapiReverseSendStartStop(bool start) m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); + QNetworkReply *reply; if (start) { - m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); } else { - m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); } + buffer->setParent(reply); delete swgDeviceSettings; } @@ -1222,10 +1215,11 @@ void BladeRF2Output::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; } - - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("BladeRF2Output::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("BladeRF2Output::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } } diff --git a/plugins/samplesink/bladerf2output/bladerf2outputplugin.cpp b/plugins/samplesink/bladerf2output/bladerf2outputplugin.cpp index 6fc88535f..99f6bfa2e 100644 --- a/plugins/samplesink/bladerf2output/bladerf2outputplugin.cpp +++ b/plugins/samplesink/bladerf2output/bladerf2outputplugin.cpp @@ -31,7 +31,7 @@ const PluginDescriptor BladeRF2OutputPlugin::m_pluginDescriptor = { QString("BladeRF2 Output"), - QString("4.11.10"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/samplesink/bladerf2output/bladerf2outputthread.cpp b/plugins/samplesink/bladerf2output/bladerf2outputthread.cpp index 097654967..e8c345658 100644 --- a/plugins/samplesink/bladerf2output/bladerf2outputthread.cpp +++ b/plugins/samplesink/bladerf2output/bladerf2outputthread.cpp @@ -17,7 +17,7 @@ #include -#include "dsp/samplesourcefifodb.h" +#include "dsp/samplesourcefifo.h" #include "bladerf2outputthread.h" @@ -151,14 +151,14 @@ unsigned int BladeRF2OutputThread::getLog2Interpolation(unsigned int channel) co } } -void BladeRF2OutputThread::setFifo(unsigned int channel, SampleSourceFifoDB *sampleFifo) +void BladeRF2OutputThread::setFifo(unsigned int channel, SampleSourceFifo *sampleFifo) { if (channel < m_nbChannels) { m_channels[channel].m_sampleFifo = sampleFifo; } } -SampleSourceFifoDB *BladeRF2OutputThread::getFifo(unsigned int channel) +SampleSourceFifo *BladeRF2OutputThread::getFifo(unsigned int channel) { if (channel < m_nbChannels) { return m_channels[channel].m_sampleFifo; @@ -193,47 +193,18 @@ void BladeRF2OutputThread::callbackSO(qint16* buf, qint32 len, unsigned int chan { if (m_channels[channel].m_sampleFifo) { - float bal = m_channels[channel].m_sampleFifo->getRWBalance(); + SampleVector& data = m_channels[channel].m_sampleFifo->getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_channels[channel].m_sampleFifo->read(len/(1< 0.25) { - qDebug("BladeRF2OutputThread::callbackSO: read leads: %f", bal); + if (iPart1Begin != iPart1End) { + callbackPart(buf, data, iPart1Begin, iPart1End, channel); } - SampleVector::iterator beginRead; - m_channels[channel].m_sampleFifo->readAdvance(beginRead, len/(1< m_interpolators; @@ -73,6 +73,7 @@ private: unsigned int getNbFifos(); void callbackSO(qint16* buf, qint32 len, unsigned int channel = 0); void callbackMO(qint16* buf, qint32 samplesPerChannel); + void callbackPart(qint16* buf, SampleVector& data, unsigned int iBegin, unsigned int iEnd, unsigned int channel); }; diff --git a/plugins/samplesink/filesink/filesinkplugin.cpp b/plugins/samplesink/filesink/filesinkplugin.cpp index 86a56f7fe..8c8a58eff 100644 --- a/plugins/samplesink/filesink/filesinkplugin.cpp +++ b/plugins/samplesink/filesink/filesinkplugin.cpp @@ -29,7 +29,7 @@ const PluginDescriptor FileSinkPlugin::m_pluginDescriptor = { QString("File sink output"), - QString("4.11.10"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/samplesink/filesink/filesinkthread.cpp b/plugins/samplesink/filesink/filesinkthread.cpp index 11db6eff4..d7ff51989 100644 --- a/plugins/samplesink/filesink/filesinkthread.cpp +++ b/plugins/samplesink/filesink/filesinkthread.cpp @@ -21,10 +21,10 @@ #include #include -#include "dsp/samplesourcefifodb.h" +#include "dsp/samplesourcefifo.h" #include "filesinkthread.h" -FileSinkThread::FileSinkThread(std::ofstream *samplesStream, SampleSourceFifoDB* sampleFifo, QObject* parent) : +FileSinkThread::FileSinkThread(std::ofstream *samplesStream, SampleSourceFifo* sampleFifo, QObject* parent) : QThread(parent), m_running(false), m_ofstream(samplesStream), @@ -97,7 +97,7 @@ void FileSinkThread::setSamplerate(int samplerate) // resize sample FIFO if (m_sampleFifo) { - m_sampleFifo->resize(samplerate); // 1s buffer + m_sampleFifo->resize(SampleSourceFifo::getSizePolicy(samplerate)); // 1s buffer } // resize output buffer @@ -178,52 +178,56 @@ void FileSinkThread::tick() m_throttleToggle = !m_throttleToggle; } -// if (m_throttlems > m_maxThrottlems) -// { -// qDebug("FileSinkThread::tick: m_maxThrottlems: %d", m_maxThrottlems); -// m_maxThrottlems = m_throttlems; -// } - - SampleVector::iterator readUntil; - - m_sampleFifo->readAdvance(readUntil, m_samplesChunkSize); - SampleVector::iterator beginRead = readUntil - m_samplesChunkSize; + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + SampleVector& data = m_sampleFifo->getData(); + m_sampleFifo->read(m_samplesChunkSize, iPart1Begin, iPart1End, iPart2Begin, iPart2End); m_samplesCount += m_samplesChunkSize; - if (m_log2Interpolation == 0) - { - m_ofstream->write(reinterpret_cast(&(*beginRead)), m_samplesChunkSize*sizeof(Sample)); - } - else - { - int chunkSize = std::min((int) m_samplesChunkSize, m_samplerate); - - switch (m_log2Interpolation) - { - case 1: - m_interpolators.interpolate2_cen(&beginRead, m_buf, chunkSize*(1<write(reinterpret_cast(m_buf), m_samplesChunkSize*(1<write(reinterpret_cast(&(*beginRead)), chunkSize*sizeof(Sample)); + } + else + { + switch (m_log2Interpolation) + { + case 1: + m_interpolators.interpolate2_cen(&beginRead, m_buf, chunkSize*(1<write(reinterpret_cast(m_buf), chunkSize*(1<getSourceBuddies().size() > 0) { @@ -373,10 +373,10 @@ bool HackRFOutput::applySettings(const HackRFOutputSettings& settings, bool forc if ((m_settings.m_devSampleRate != settings.m_devSampleRate) || (m_settings.m_log2Interp != settings.m_log2Interp) || force) { forwardChange = true; - int fifoSize = std::max( - (int) ((settings.m_devSampleRate/(1<getLnaExt() != 0; } if (deviceSettingsKeys.contains("transverterDeltaFrequency")) { - settings.m_transverterDeltaFrequency = response.getHackRfInputSettings()->getTransverterDeltaFrequency(); + settings.m_transverterDeltaFrequency = response.getHackRfOutputSettings()->getTransverterDeltaFrequency(); } if (deviceSettingsKeys.contains("transverterMode")) { - settings.m_transverterMode = response.getHackRfInputSettings()->getTransverterMode() != 0; + settings.m_transverterMode = response.getHackRfOutputSettings()->getTransverterMode() != 0; } if (deviceSettingsKeys.contains("useReverseAPI")) { settings.m_useReverseAPI = response.getHackRfOutputSettings()->getUseReverseApi() != 0; @@ -746,13 +746,14 @@ void HackRFOutput::webapiReverseSendSettings(QList& deviceSettingsKeys, m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgDeviceSettings; } @@ -771,17 +772,19 @@ void HackRFOutput::webapiReverseSendStartStop(bool start) m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); + QNetworkReply *reply; if (start) { - m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); } else { - m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); } + buffer->setParent(reply); delete swgDeviceSettings; } @@ -795,10 +798,13 @@ void HackRFOutput::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("HackRFOutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("HackRFOutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } diff --git a/plugins/samplesink/hackrfoutput/hackrfoutputplugin.cpp b/plugins/samplesink/hackrfoutput/hackrfoutputplugin.cpp index 3d64e0eac..45d559f08 100644 --- a/plugins/samplesink/hackrfoutput/hackrfoutputplugin.cpp +++ b/plugins/samplesink/hackrfoutput/hackrfoutputplugin.cpp @@ -31,7 +31,7 @@ const PluginDescriptor HackRFOutputPlugin::m_pluginDescriptor = { QString("HackRF Output"), - QString("4.11.10"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/samplesink/hackrfoutput/hackrfoutputthread.cpp b/plugins/samplesink/hackrfoutput/hackrfoutputthread.cpp index 2c14b1813..977827a23 100644 --- a/plugins/samplesink/hackrfoutput/hackrfoutputthread.cpp +++ b/plugins/samplesink/hackrfoutput/hackrfoutputthread.cpp @@ -20,9 +20,9 @@ #include #include -#include "dsp/samplesourcefifodb.h" +#include "dsp/samplesourcefifo.h" -HackRFOutputThread::HackRFOutputThread(hackrf_device* dev, SampleSourceFifoDB* sampleFifo, QObject* parent) : +HackRFOutputThread::HackRFOutputThread(hackrf_device* dev, SampleSourceFifo* sampleFifo, QObject* parent) : QThread(parent), m_running(false), m_dev(dev), @@ -118,9 +118,25 @@ void HackRFOutputThread::run() // Interpolate according to specified log2 (ex: log2=4 => interp=16) void HackRFOutputThread::callback(qint8* buf, qint32 len) { - SampleVector::iterator beginRead; - m_sampleFifo->readAdvance(beginRead, len/(2*(1<getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_sampleFifo->read(len/(2*(1<setNbSinkStreams(1); - m_sampleSourceFifo.resize(16*LIMESDROUTPUT_BLOCKSIZE); + m_sampleSourceFifo.resize(SampleSourceFifo::getSizePolicy(m_settings.m_devSampleRate)); m_streamId.handle = 0; suspendRxBuddies(); suspendTxBuddies(); @@ -823,11 +823,11 @@ bool LimeSDROutput::applySettings(const LimeSDROutputSettings& settings, bool fo reverseAPIKeys.append("devSampleRate"); reverseAPIKeys.append("log2SoftInterp"); - int fifoSize = (std::max)( - (int) ((settings.m_devSampleRate/(1<& deviceSettingsKeys m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgDeviceSettings; } @@ -1543,17 +1544,19 @@ void LimeSDROutput::webapiReverseSendStartStop(bool start) m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); + QNetworkReply *reply; if (start) { - m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); } else { - m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); } + buffer->setParent(reply); delete swgDeviceSettings; } @@ -1567,10 +1570,13 @@ void LimeSDROutput::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("LimeSDROutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("LimeSDROutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } diff --git a/plugins/samplesink/limesdroutput/limesdroutputplugin.cpp b/plugins/samplesink/limesdroutput/limesdroutputplugin.cpp index 605bf15f7..f94483ef1 100644 --- a/plugins/samplesink/limesdroutput/limesdroutputplugin.cpp +++ b/plugins/samplesink/limesdroutput/limesdroutputplugin.cpp @@ -34,7 +34,7 @@ const PluginDescriptor LimeSDROutputPlugin::m_pluginDescriptor = { QString("LimeSDR Output"), - QString("4.11.10"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/samplesink/limesdroutput/limesdroutputthread.cpp b/plugins/samplesink/limesdroutput/limesdroutputthread.cpp index 1e252539f..fb0fc1556 100644 --- a/plugins/samplesink/limesdroutput/limesdroutputthread.cpp +++ b/plugins/samplesink/limesdroutput/limesdroutputthread.cpp @@ -18,10 +18,12 @@ #include #include +#include "dsp/samplesourcefifo.h" + #include "limesdroutputthread.h" #include "limesdroutputsettings.h" -LimeSDROutputThread::LimeSDROutputThread(lms_stream_t* stream, SampleSourceFifoDB* sampleFifo, QObject* parent) : +LimeSDROutputThread::LimeSDROutputThread(lms_stream_t* stream, SampleSourceFifo* sampleFifo, QObject* parent) : QThread(parent), m_running(false), m_stream(stream), @@ -108,39 +110,54 @@ void LimeSDROutputThread::run() // Interpolate according to specified log2 (ex: log2=4 => decim=16) void LimeSDROutputThread::callback(qint16* buf, qint32 len) { - SampleVector::iterator beginRead; - m_sampleFifo->readAdvance(beginRead, len/(1<getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_sampleFifo->read(len/(1<setNbSinkStreams(1); m_networkManager = new QNetworkAccessManager(); connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); @@ -125,6 +125,7 @@ int LocalOutput::getSampleRate() const void LocalOutput::setSampleRate(int sampleRate) { m_sampleRate = sampleRate; + m_sampleSourceFifo.resize(SampleSourceFifo::getSizePolicy(m_sampleRate)); DSPSignalNotification *notif = new DSPSignalNotification(m_sampleRate, m_centerFrequency); // Frequency in Hz for the DSP engine m_deviceAPI->getDeviceEngineInputMessageQueue()->push(notif); @@ -351,13 +352,14 @@ void LocalOutput::webapiReverseSendSettings(QList& deviceSettingsKeys, m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgDeviceSettings; } @@ -376,17 +378,19 @@ void LocalOutput::webapiReverseSendStartStop(bool start) m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); + QNetworkReply *reply; if (start) { - m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); } else { - m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); } + buffer->setParent(reply); delete swgDeviceSettings; } @@ -400,10 +404,13 @@ void LocalOutput::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("LocalOutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("LocalOutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } diff --git a/plugins/samplesink/localoutput/localoutputplugin.cpp b/plugins/samplesink/localoutput/localoutputplugin.cpp index 7563a2f65..89a5c5c5d 100644 --- a/plugins/samplesink/localoutput/localoutputplugin.cpp +++ b/plugins/samplesink/localoutput/localoutputplugin.cpp @@ -30,7 +30,7 @@ const PluginDescriptor LocalOutputPlugin::m_pluginDescriptor = { QString("Local device output"), - QString("4.11.10"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/samplesink/plutosdroutput/plutosdroutput.cpp b/plugins/samplesink/plutosdroutput/plutosdroutput.cpp index 7c161cee9..a46dd4939 100644 --- a/plugins/samplesink/plutosdroutput/plutosdroutput.cpp +++ b/plugins/samplesink/plutosdroutput/plutosdroutput.cpp @@ -233,7 +233,7 @@ bool PlutoSDROutput::handleMessage(const Message& message) bool PlutoSDROutput::openDevice() { - m_sampleSourceFifo.resize(32*PLUTOSDR_BLOCKSIZE_SAMPLES); + m_sampleSourceFifo.resize(SampleSourceFifo::getSizePolicy(m_settings.m_devSampleRate)); // look for Rx buddy and get reference to common parameters if (m_deviceAPI->getSourceBuddies().size() > 0) // then sink @@ -432,10 +432,17 @@ bool PlutoSDROutput::applySettings(const PlutoSDROutputSettings& settings, bool forwardChangeOwnDSP = (m_settings.m_devSampleRate != settings.m_devSampleRate) || force; } + if ((m_settings.m_devSampleRate != settings.m_devSampleRate) || (m_settings.m_log2Interp != settings.m_log2Interp) || force) + { + unsigned int fifoRate = std::max( + (unsigned int) settings.m_devSampleRate / (1<& deviceSettingsKey m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgDeviceSettings; } @@ -876,17 +884,19 @@ void PlutoSDROutput::webapiReverseSendStartStop(bool start) m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); + QNetworkReply *reply; if (start) { - m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); } else { - m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); } + buffer->setParent(reply); delete swgDeviceSettings; } @@ -900,10 +910,13 @@ void PlutoSDROutput::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("PlutoSDROutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("PlutoSDROutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } diff --git a/plugins/samplesink/plutosdroutput/plutosdroutputthread.cpp b/plugins/samplesink/plutosdroutput/plutosdroutputthread.cpp index a0ebbce2b..3a4b32699 100644 --- a/plugins/samplesink/plutosdroutput/plutosdroutputthread.cpp +++ b/plugins/samplesink/plutosdroutput/plutosdroutputthread.cpp @@ -15,12 +15,14 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// +#include "iio.h" + +#include "dsp/samplesourcefifo.h" #include "plutosdr/deviceplutosdrbox.h" #include "plutosdroutputsettings.h" -#include "iio.h" #include "plutosdroutputthread.h" -PlutoSDROutputThread::PlutoSDROutputThread(uint32_t blocksizeSamples, DevicePlutoSDRBox* plutoBox, SampleSourceFifoDB* sampleFifo, QObject* parent) : +PlutoSDROutputThread::PlutoSDROutputThread(uint32_t blocksizeSamples, DevicePlutoSDRBox* plutoBox, SampleSourceFifo* sampleFifo, QObject* parent) : QThread(parent), m_running(false), m_plutoBox(plutoBox), @@ -29,7 +31,6 @@ PlutoSDROutputThread::PlutoSDROutputThread(uint32_t blocksizeSamples, DevicePlut m_log2Interp(0) { m_buf = new qint16[blocksizeSamples*2]; -// m_bufConv = new qint16[blocksizeSamples*(sizeof(Sample)/sizeof(qint16))]; } PlutoSDROutputThread::~PlutoSDROutputThread() @@ -44,8 +45,11 @@ void PlutoSDROutputThread::startWork() m_startWaitMutex.lock(); start(); - while(!m_running) + + while(!m_running) { m_startWaiter.wait(&m_startWaitMutex, 100); + } + m_startWaitMutex.unlock(); } @@ -115,9 +119,25 @@ void PlutoSDROutputThread::run() void PlutoSDROutputThread::convert(qint16* buf, qint32 len) { // pull samples from baseband generator - SampleVector::iterator beginRead; - m_sampleFifo->readAdvance(beginRead, len/(2*(1<getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_sampleFifo->read(len/(2*(1< #include -#include "dsp/samplesourcefifodb.h" #include "dsp/interpolators.h" #include "plutosdr/deviceplutosdrshared.h" class DevicePlutoSDRBox; +class SampleSourceFifo; class PlutoSDROutputThread : public QThread, public DevicePlutoSDRShared::ThreadInterface { Q_OBJECT public: - PlutoSDROutputThread(uint32_t blocksize, DevicePlutoSDRBox* plutoBox, SampleSourceFifoDB* sampleFifo, QObject* parent = 0); + PlutoSDROutputThread(uint32_t blocksize, DevicePlutoSDRBox* plutoBox, SampleSourceFifo* sampleFifo, QObject* parent = nullptr); ~PlutoSDROutputThread(); virtual void startWork(); @@ -51,7 +51,7 @@ private: int16_t *m_buf; //!< holds I+Q values of each sample from devce // int16_t *m_bufConv; //!< holds I+Q values of each sample converted to host format via iio_channel_convert uint32_t m_blockSizeSamples; //!< buffer sizes in number of (I,Q) samples - SampleSourceFifoDB* m_sampleFifo; //!< DSP sample FIFO (I,Q) + SampleSourceFifo* m_sampleFifo; //!< DSP sample FIFO (I,Q) unsigned int m_log2Interp; // soft interpolation @@ -59,6 +59,7 @@ private: void run(); void convert(qint16* buf, qint32 len); + void convertPart(qint16* buf, SampleVector& data, unsigned int iBegin, unsigned int iEnd); }; diff --git a/plugins/samplesink/remoteoutput/remoteoutput.cpp b/plugins/samplesink/remoteoutput/remoteoutput.cpp index 16e30f46b..a2ac0d115 100644 --- a/plugins/samplesink/remoteoutput/remoteoutput.cpp +++ b/plugins/samplesink/remoteoutput/remoteoutput.cpp @@ -512,32 +512,35 @@ void RemoteOutput::networkManagerFinished(QNetworkReply *reply) if (reply->error()) { qInfo("RemoteOutput::networkManagerFinished: error: %s", qPrintable(reply->errorString())); - return; } - - QString answer = reply->readAll(); - - try + else { - QByteArray jsonBytes(answer.toStdString().c_str()); - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(jsonBytes, &error); + QString answer = reply->readAll(); - if (error.error == QJsonParseError::NoError) + try { - analyzeApiReply(doc.object(), answer); + QByteArray jsonBytes(answer.toStdString().c_str()); + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(jsonBytes, &error); + + if (error.error == QJsonParseError::NoError) + { + analyzeApiReply(doc.object(), answer); + } + else + { + QString errorMsg = QString("Reply JSON error: ") + error.errorString() + QString(" at offset ") + QString::number(error.offset); + qInfo().noquote() << "RemoteOutput::networkManagerFinished" << errorMsg; + } } - else + catch (const std::exception& ex) { - QString errorMsg = QString("Reply JSON error: ") + error.errorString() + QString(" at offset ") + QString::number(error.offset); + QString errorMsg = QString("Error parsing request: ") + ex.what(); qInfo().noquote() << "RemoteOutput::networkManagerFinished" << errorMsg; } } - catch (const std::exception& ex) - { - QString errorMsg = QString("Error parsing request: ") + ex.what(); - qInfo().noquote() << "RemoteOutput::networkManagerFinished" << errorMsg; - } + + reply->deleteLater(); } void RemoteOutput::analyzeApiReply(const QJsonObject& jsonObject, const QString& answer) @@ -675,13 +678,14 @@ void RemoteOutput::webapiReverseSendSettings(QList& deviceSettingsKeys, m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgDeviceSettings; } @@ -700,16 +704,18 @@ void RemoteOutput::webapiReverseSendStartStop(bool start) m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); + QNetworkReply *reply; if (start) { - m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); } else { - m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); } + buffer->setParent(reply); delete swgDeviceSettings; } diff --git a/plugins/samplesink/remoteoutput/remoteoutputgui.cpp b/plugins/samplesink/remoteoutput/remoteoutputgui.cpp index 1ffb3d3a6..2b99df43d 100644 --- a/plugins/samplesink/remoteoutput/remoteoutputgui.cpp +++ b/plugins/samplesink/remoteoutput/remoteoutputgui.cpp @@ -513,38 +513,41 @@ void RemoteOutputSinkGui::networkManagerFinished(QNetworkReply *reply) { ui->apiAddressLabel->setStyleSheet("QLabel { background:rgb(79,79,79); }"); ui->statusText->setText(reply->errorString()); - return; } - - QString answer = reply->readAll(); - - try + else { - QByteArray jsonBytes(answer.toStdString().c_str()); - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(jsonBytes, &error); + QString answer = reply->readAll(); - if (error.error == QJsonParseError::NoError) + try { - ui->apiAddressLabel->setStyleSheet("QLabel { background-color : green; }"); - ui->statusText->setText(QString("API OK")); - analyzeApiReply(doc.object()); + QByteArray jsonBytes(answer.toStdString().c_str()); + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(jsonBytes, &error); + + if (error.error == QJsonParseError::NoError) + { + ui->apiAddressLabel->setStyleSheet("QLabel { background-color : green; }"); + ui->statusText->setText(QString("API OK")); + analyzeApiReply(doc.object()); + } + else + { + ui->apiAddressLabel->setStyleSheet("QLabel { background:rgb(79,79,79); }"); + QString errorMsg = QString("Reply JSON error: ") + error.errorString() + QString(" at offset ") + QString::number(error.offset); + ui->statusText->setText(QString("JSON error. See log")); + qInfo().noquote() << "RemoteOutputSinkGui::networkManagerFinished" << errorMsg; + } } - else + catch (const std::exception& ex) { ui->apiAddressLabel->setStyleSheet("QLabel { background:rgb(79,79,79); }"); - QString errorMsg = QString("Reply JSON error: ") + error.errorString() + QString(" at offset ") + QString::number(error.offset); - ui->statusText->setText(QString("JSON error. See log")); + QString errorMsg = QString("Error parsing request: ") + ex.what(); + ui->statusText->setText("Error parsing request. See log for details"); qInfo().noquote() << "RemoteOutputSinkGui::networkManagerFinished" << errorMsg; } } - catch (const std::exception& ex) - { - ui->apiAddressLabel->setStyleSheet("QLabel { background:rgb(79,79,79); }"); - QString errorMsg = QString("Error parsing request: ") + ex.what(); - ui->statusText->setText("Error parsing request. See log for details"); - qInfo().noquote() << "RemoteOutputSinkGui::networkManagerFinished" << errorMsg; - } + + reply->deleteLater(); } void RemoteOutputSinkGui::analyzeApiReply(const QJsonObject& jsonObject) diff --git a/plugins/samplesink/remoteoutput/remoteoutputplugin.cpp b/plugins/samplesink/remoteoutput/remoteoutputplugin.cpp index 859e5cab8..2aa09ae6d 100644 --- a/plugins/samplesink/remoteoutput/remoteoutputplugin.cpp +++ b/plugins/samplesink/remoteoutput/remoteoutputplugin.cpp @@ -30,7 +30,7 @@ const PluginDescriptor RemoteOutputPlugin::m_pluginDescriptor = { QString("Remote device output"), - QString("4.11.10"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/samplesink/remoteoutput/remoteoutputthread.cpp b/plugins/samplesink/remoteoutput/remoteoutputthread.cpp index c0f99d347..26fe10fff 100644 --- a/plugins/samplesink/remoteoutput/remoteoutputthread.cpp +++ b/plugins/samplesink/remoteoutput/remoteoutputthread.cpp @@ -21,11 +21,11 @@ #include #include -#include "dsp/samplesourcefifodb.h" +#include "dsp/samplesourcefifo.h" #include "util/timeutil.h" #include "remoteoutputthread.h" -RemoteOutputThread::RemoteOutputThread(SampleSourceFifoDB* sampleFifo, QObject* parent) : +RemoteOutputThread::RemoteOutputThread(SampleSourceFifo* sampleFifo, QObject* parent) : QThread(parent), m_running(false), m_samplesChunkSize(0), @@ -84,8 +84,12 @@ void RemoteOutputThread::setSamplerate(int samplerate) } // resize sample FIFO - if (m_sampleFifo) { - m_sampleFifo->resize(samplerate); // 1s buffer + if (m_sampleFifo) + { + unsigned int fifoRate = std::max( + (unsigned int) samplerate, + 48000U); + m_sampleFifo->resize(SampleSourceFifo::getSizePolicy(fifoRate)); } m_samplerate = samplerate; @@ -133,11 +137,23 @@ void RemoteOutputThread::tick() SampleVector::iterator readUntil; - m_sampleFifo->readAdvance(readUntil, m_samplesChunkSize); // pull samples - SampleVector::iterator beginRead = readUntil - m_samplesChunkSize; - m_samplesCount += m_samplesChunkSize; + SampleVector& data = m_sampleFifo->getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_sampleFifo->read(m_samplesChunkSize, iPart1Begin, iPart1End, iPart2Begin, iPart2End); - m_udpSinkFEC.write(beginRead, m_samplesChunkSize); + if (iPart1Begin != iPart1End) + { + SampleVector::iterator beginRead = data.begin() + iPart1Begin; + unsigned int partSize = iPart1End - iPart1Begin; + m_udpSinkFEC.write(beginRead, partSize); + } + + if (iPart2Begin != iPart2End) + { + SampleVector::iterator beginRead = data.begin() + iPart2Begin; + unsigned int partSize = iPart2End - iPart2Begin; + m_udpSinkFEC.write(beginRead, partSize); + } } } diff --git a/plugins/samplesink/remoteoutput/remoteoutputthread.h b/plugins/samplesink/remoteoutput/remoteoutputthread.h index 3a1001432..ce992a59e 100644 --- a/plugins/samplesink/remoteoutput/remoteoutputthread.h +++ b/plugins/samplesink/remoteoutput/remoteoutputthread.h @@ -36,14 +36,14 @@ #define REMOTEOUTPUT_THROTTLE_MS 50 -class SampleSourceFifoDB; +class SampleSourceFifo; struct timeval; class RemoteOutputThread : public QThread { Q_OBJECT public: - RemoteOutputThread(SampleSourceFifoDB* sampleFifo, QObject* parent = 0); + RemoteOutputThread(SampleSourceFifo* sampleFifo, QObject* parent = 0); ~RemoteOutputThread(); void startWork(); @@ -68,7 +68,7 @@ private: volatile bool m_running; int m_samplesChunkSize; - SampleSourceFifoDB* m_sampleFifo; + SampleSourceFifo* m_sampleFifo; uint32_t m_samplesCount; int m_chunkCorrection; diff --git a/plugins/samplesink/soapysdroutput/soapysdroutput.cpp b/plugins/samplesink/soapysdroutput/soapysdroutput.cpp index 39717166a..90efa9c38 100644 --- a/plugins/samplesink/soapysdroutput/soapysdroutput.cpp +++ b/plugins/samplesink/soapysdroutput/soapysdroutput.cpp @@ -74,7 +74,7 @@ void SoapySDROutput::destroy() bool SoapySDROutput::openDevice() { - m_sampleSourceFifo.resize(m_settings.m_devSampleRate/(1<<(m_settings.m_log2Interp <= 4 ? m_settings.m_log2Interp : 4))); + m_sampleSourceFifo.resize(SampleSourceFifo::getSizePolicy(m_settings.m_devSampleRate)); // look for Tx buddies and get reference to the device object if (m_deviceAPI->getSinkBuddies().size() > 0) // look sink sibling first @@ -463,7 +463,7 @@ bool SoapySDROutput::start() { qDebug("SoapySDROutput::start: expand channels. Re-allocate thread and take ownership"); - SampleSourceFifoDB **fifos = new SampleSourceFifoDB*[nbOriginalChannels]; + SampleSourceFifo **fifos = new SampleSourceFifo*[nbOriginalChannels]; unsigned int *log2Interps = new unsigned int[nbOriginalChannels]; for (int i = 0; i < nbOriginalChannels; i++) // save original FIFO references and data @@ -574,7 +574,7 @@ void SoapySDROutput::stop() { qDebug("SoapySDROutput::stop: MO mode. Reduce by deleting and re-creating the thread"); soapySDROutputThread->stopWork(); - SampleSourceFifoDB **fifos = new SampleSourceFifoDB*[nbOriginalChannels-1]; + SampleSourceFifo **fifos = new SampleSourceFifo*[nbOriginalChannels-1]; unsigned int *log2Interps = new unsigned int[nbOriginalChannels-1]; int highestActiveChannelIndex = -1; @@ -871,7 +871,7 @@ bool SoapySDROutput::applySettings(const SoapySDROutputSettings& settings, bool if ((m_settings.m_devSampleRate != settings.m_devSampleRate) || (m_settings.m_log2Interp != settings.m_log2Interp) || force) { SoapySDROutputThread *soapySDROutputThread = findThread(); - SampleSourceFifoDB *fifo = 0; + SampleSourceFifo *fifo = nullptr; if (soapySDROutputThread) { @@ -879,20 +879,10 @@ bool SoapySDROutput::applySettings(const SoapySDROutputSettings& settings, bool soapySDROutputThread->setFifo(requestedChannel, 0); } - int fifoSize; - - if (settings.m_log2Interp >= 5) - { - fifoSize = DeviceSoapySDRShared::m_sampleFifoMinSize32; - } - else - { - fifoSize = std::max( - (int) ((settings.m_devSampleRate/(1<setFifo(requestedChannel, &m_sampleSourceFifo); @@ -1894,13 +1884,14 @@ void SoapySDROutput::webapiReverseSendSettings(QList& deviceSettingsKey m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgDeviceSettings; } @@ -1919,17 +1910,19 @@ void SoapySDROutput::webapiReverseSendStartStop(bool start) m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); + QNetworkReply *reply; if (start) { - m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); } else { - m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); } + buffer->setParent(reply); delete swgDeviceSettings; } @@ -1943,10 +1936,13 @@ void SoapySDROutput::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("SoapySDROutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("SoapySDROutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } diff --git a/plugins/samplesink/soapysdroutput/soapysdroutputplugin.cpp b/plugins/samplesink/soapysdroutput/soapysdroutputplugin.cpp index 856a8ef79..dddcf4ed6 100644 --- a/plugins/samplesink/soapysdroutput/soapysdroutputplugin.cpp +++ b/plugins/samplesink/soapysdroutput/soapysdroutputplugin.cpp @@ -32,7 +32,7 @@ const PluginDescriptor SoapySDROutputPlugin::m_pluginDescriptor = { QString("SoapySDR Output"), - QString("4.11.10"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/samplesink/soapysdroutput/soapysdroutputthread.cpp b/plugins/samplesink/soapysdroutput/soapysdroutputthread.cpp index 412d30c70..38d7e3568 100644 --- a/plugins/samplesink/soapysdroutput/soapysdroutputthread.cpp +++ b/plugins/samplesink/soapysdroutput/soapysdroutputthread.cpp @@ -20,7 +20,7 @@ #include #include -#include "dsp/samplesourcefifodb.h" +#include "dsp/samplesourcefifo.h" #include "soapysdroutputthread.h" @@ -218,14 +218,14 @@ unsigned int SoapySDROutputThread::getLog2Interpolation(unsigned int channel) co } } -void SoapySDROutputThread::setFifo(unsigned int channel, SampleSourceFifoDB *sampleFifo) +void SoapySDROutputThread::setFifo(unsigned int channel, SampleSourceFifo *sampleFifo) { if (channel < m_nbChannels) { m_channels[channel].m_sampleFifo = sampleFifo; } } -SampleSourceFifoDB *SoapySDROutputThread::getFifo(unsigned int channel) +SampleSourceFifo *SoapySDROutputThread::getFifo(unsigned int channel) { if (channel < m_nbChannels) { return m_channels[channel].m_sampleFifo; @@ -283,47 +283,18 @@ void SoapySDROutputThread::callbackSO8(qint8* buf, qint32 len, unsigned int chan { if (m_channels[channel].m_sampleFifo) { - float bal = m_channels[channel].m_sampleFifo->getRWBalance(); + SampleVector& data = m_channels[channel].m_sampleFifo->getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_channels[channel].m_sampleFifo->read(len/(1< 0.25) { - qDebug("SoapySDROutputThread::callbackSO8: read leads: %f", bal); + if (iPart1Begin != iPart1End) { + callbackPart8(buf, data, iPart1Begin, iPart1End, channel); } - SampleVector::iterator beginRead; - m_channels[channel].m_sampleFifo->readAdvance(beginRead, len/(1<getRWBalance(); + SampleVector& data = m_channels[channel].m_sampleFifo->getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_channels[channel].m_sampleFifo->read(len/(1< 0.25) { - qDebug("SoapySDROutputThread::callbackSO12: read leads: %f", bal); + if (iPart1Begin != iPart1End) { + callbackPart12(buf, data, iPart1Begin, iPart1End, channel); } - SampleVector::iterator beginRead; - m_channels[channel].m_sampleFifo->readAdvance(beginRead, len/(1<getRWBalance(); + SampleVector& data = m_channels[channel].m_sampleFifo->getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_channels[channel].m_sampleFifo->read(len/(1< 0.25) { - qDebug("SoapySDROutputThread::callbackSO16: read leads: %f", bal); + if (iPart1Begin != iPart1End) { + callbackPart16(buf, data, iPart1Begin, iPart1End, channel); } - SampleVector::iterator beginRead; - m_channels[channel].m_sampleFifo->readAdvance(beginRead, len/(1<getRWBalance(); + SampleVector& data = m_channels[channel].m_sampleFifo->getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_channels[channel].m_sampleFifo->read(len/(1< 0.25) { - qDebug("SoapySDROutputThread::callbackSO16: read leads: %f", bal); + if (iPart1Begin != iPart1End) { + callbackPartF(buf, data, iPart1Begin, iPart1End, channel); } - SampleVector::iterator beginRead; - m_channels[channel].m_sampleFifo->readAdvance(beginRead, len/(1< m_interpolators8; Interpolators m_interpolators12; @@ -94,6 +94,10 @@ private: void callbackSO16(qint16* buf, qint32 len, unsigned int channel = 0); void callbackSOIF(float* buf, qint32 len, unsigned int channel = 0); void callbackMO(std::vector& buffs, qint32 samplesPerChannel); + void callbackPart8(qint8* buf, SampleVector& data, unsigned int iBegin, unsigned int iEnd, unsigned int channel); + void callbackPart12(qint16* buf, SampleVector& data, unsigned int iBegin, unsigned int iEnd, unsigned int channel); + void callbackPart16(qint16* buf, SampleVector& data, unsigned int iBegin, unsigned int iEnd, unsigned int channel); + void callbackPartF(float* buf, SampleVector& data, unsigned int iBegin, unsigned int iEnd, unsigned int channel); }; diff --git a/plugins/samplesink/testsink/testsinkplugin.cpp b/plugins/samplesink/testsink/testsinkplugin.cpp index 3d835d604..6c3740d5a 100644 --- a/plugins/samplesink/testsink/testsinkplugin.cpp +++ b/plugins/samplesink/testsink/testsinkplugin.cpp @@ -29,7 +29,7 @@ const PluginDescriptor TestSinkPlugin::m_pluginDescriptor = { QString("Test Sink Output"), - QString("4.11.12"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/samplesink/testsink/testsinkthread.cpp b/plugins/samplesink/testsink/testsinkthread.cpp index 9950ac4c4..4dd2eaebc 100644 --- a/plugins/samplesink/testsink/testsinkthread.cpp +++ b/plugins/samplesink/testsink/testsinkthread.cpp @@ -21,11 +21,11 @@ #include #include -#include "dsp/samplesourcefifodb.h" +#include "dsp/samplesourcefifo.h" #include "dsp/basebandsamplesink.h" #include "testsinkthread.h" -TestSinkThread::TestSinkThread(SampleSourceFifoDB* sampleFifo, QObject* parent) : +TestSinkThread::TestSinkThread(SampleSourceFifo* sampleFifo, QObject* parent) : QThread(parent), m_running(false), m_bufsize(0), @@ -88,7 +88,7 @@ void TestSinkThread::setSamplerate(int samplerate) // resize sample FIFO if (m_sampleFifo) { - m_sampleFifo->resize(samplerate); // 1s buffer + m_sampleFifo->resize(SampleSourceFifo::getSizePolicy(samplerate)); } // resize output buffer @@ -168,52 +168,63 @@ void TestSinkThread::tick() m_throttleToggle = !m_throttleToggle; } - SampleVector::iterator readUntil; - - m_sampleFifo->readAdvance(readUntil, m_samplesChunkSize); - SampleVector::iterator beginRead = readUntil - m_samplesChunkSize; + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + SampleVector& data = m_sampleFifo->getData(); + m_sampleFifo->read(m_samplesChunkSize, iPart1Begin, iPart1End, iPart2Begin, iPart2End); m_samplesCount += m_samplesChunkSize; - int chunkSize = std::min((int) m_samplesChunkSize, m_samplerate); - if (m_log2Interpolation == 0) - { - m_interpolators.interpolate1(&beginRead, m_buf, 2*chunkSize); - feedSpectrum(m_buf, 2*chunkSize); - //m_ofstream->write(reinterpret_cast(&(*beginRead)), m_samplesChunkSize*sizeof(Sample)); + if (iPart1Begin != iPart1End) { + callbackPart(data, iPart1Begin, iPart1End); } - else - { - switch (m_log2Interpolation) - { - case 1: - m_interpolators.interpolate2_cen(&beginRead, m_buf, chunkSize*(1<write(reinterpret_cast(m_buf), m_samplesChunkSize*(1< m_samplesVector; void run(); + void callbackPart(SampleVector& data, unsigned int iBegin, unsigned int iEnd); void feedSpectrum(int16_t *buf, unsigned int bufSize); private slots: diff --git a/plugins/samplesink/xtrxoutput/xtrxoutput.cpp b/plugins/samplesink/xtrxoutput/xtrxoutput.cpp index d2ef8fe2e..3dcf5e49c 100644 --- a/plugins/samplesink/xtrxoutput/xtrxoutput.cpp +++ b/plugins/samplesink/xtrxoutput/xtrxoutput.cpp @@ -77,7 +77,7 @@ void XTRXOutput::destroy() bool XTRXOutput::openDevice() { - m_sampleSourceFifo.resize(m_settings.m_devSampleRate/(1<<(m_settings.m_log2SoftInterp <= 4 ? m_settings.m_log2SoftInterp : 4))); + m_sampleSourceFifo.resize(SampleSourceFifo::getSizePolicy(m_settings.m_devSampleRate)); // look for Tx buddies and get reference to the device object if (m_deviceAPI->getSinkBuddies().size() > 0) // then sink @@ -278,7 +278,7 @@ bool XTRXOutput::start() { qDebug("XTRXOutput::start: expand channels. Re-allocate thread and take ownership"); - SampleSourceFifoDB **fifos = new SampleSourceFifoDB*[2]; + SampleSourceFifo **fifos = new SampleSourceFifo*[2]; unsigned int *log2Interps = new unsigned int[2]; for (int i = 0; i < 2; i++) // save original FIFO references and data @@ -909,6 +909,15 @@ bool XTRXOutput::applySettings(const XTRXOutputSettings& settings, bool force, b } } + if ((m_settings.m_devSampleRate != settings.m_devSampleRate) + || (m_settings.m_log2SoftInterp != settings.m_log2SoftInterp) || force) + { + unsigned int fifoRate = std::max( + (unsigned int) settings.m_devSampleRate / (1<& deviceSettingsKeys, c m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); // Always use PATCH to avoid passing reverse API settings - m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); delete swgDeviceSettings; } @@ -1425,17 +1435,19 @@ void XTRXOutput::webapiReverseSendStartStop(bool start) m_networkRequest.setUrl(QUrl(deviceSettingsURL)); m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QBuffer *buffer=new QBuffer(); + QBuffer *buffer = new QBuffer(); buffer->open((QBuffer::ReadWrite)); buffer->write(swgDeviceSettings->asJson().toUtf8()); buffer->seek(0); + QNetworkReply *reply; if (start) { - m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); } else { - m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); } + buffer->setParent(reply); delete swgDeviceSettings; } @@ -1449,10 +1461,13 @@ void XTRXOutput::networkManagerFinished(QNetworkReply *reply) << " error(" << (int) replyError << "): " << replyError << ": " << reply->errorString(); - return; + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("XTRXOutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } - QString answer = reply->readAll(); - answer.chop(1); // remove last \n - qDebug("XTRXOutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + reply->deleteLater(); } diff --git a/plugins/samplesink/xtrxoutput/xtrxoutputplugin.cpp b/plugins/samplesink/xtrxoutput/xtrxoutputplugin.cpp index e10ac97ac..3d4e1a181 100644 --- a/plugins/samplesink/xtrxoutput/xtrxoutputplugin.cpp +++ b/plugins/samplesink/xtrxoutput/xtrxoutputplugin.cpp @@ -35,7 +35,7 @@ const PluginDescriptor XTRXOutputPlugin::m_pluginDescriptor = { QString("XTRX Output"), - QString("4.11.10"), + QString("4.12.0"), QString("(c) Edouard Griffiths, F4EXB"), QString("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/samplesink/xtrxoutput/xtrxoutputthread.cpp b/plugins/samplesink/xtrxoutput/xtrxoutputthread.cpp index ddd42a653..3171146a8 100644 --- a/plugins/samplesink/xtrxoutput/xtrxoutputthread.cpp +++ b/plugins/samplesink/xtrxoutput/xtrxoutputthread.cpp @@ -20,7 +20,7 @@ #include #include "xtrx/devicextrx.h" -#include "dsp/samplesourcefifodb.h" +#include "dsp/samplesourcefifo.h" #include "xtrxoutputthread.h" @@ -94,14 +94,14 @@ unsigned int XTRXOutputThread::getLog2Interpolation(unsigned int channel) const } } -void XTRXOutputThread::setFifo(unsigned int channel, SampleSourceFifoDB *sampleFifo) +void XTRXOutputThread::setFifo(unsigned int channel, SampleSourceFifo *sampleFifo) { if (channel < 2) { m_channels[channel].m_sampleFifo = sampleFifo; } } -SampleSourceFifoDB *XTRXOutputThread::getFifo(unsigned int channel) +SampleSourceFifo *XTRXOutputThread::getFifo(unsigned int channel) { if (channel < 2) { return m_channels[channel].m_sampleFifo; @@ -222,56 +222,40 @@ void XTRXOutputThread::run() m_running = false; } -void XTRXOutputThread::callback(qint16* buf, qint32 len) +void XTRXOutputThread::callbackPart(qint16* buf, SampleVector& data, unsigned int iBegin, unsigned int iEnd) { - if (m_channels[m_uniqueChannelIndex].m_sampleFifo) + SampleVector::iterator beginRead = data.begin() + iBegin; + int len = 2*(iEnd - iBegin)*(1<getRWBalance(); - - if (bal < -0.25) { - qDebug("XTRXOutputThread::callbackSO: read lags: %f", bal); - } else if (bal > 0.25) { - qDebug("XTRXOutputThread::callbackSO: read leads: %f", bal); - } - - SampleVector::iterator beginRead; - m_channels[m_uniqueChannelIndex].m_sampleFifo->readAdvance(beginRead, len/(1<getRWBalance(); + SampleVector& data = m_channels[m_uniqueChannelIndex].m_sampleFifo->getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + m_channels[m_uniqueChannelIndex].m_sampleFifo->read(len/(1< 0.25) { - qDebug("XTRXOutputThread::callbackSO: read leads: %f", bal); + if (iPart1Begin != iPart1End) { + callbackPart(buf, data, iPart1Begin, iPart1End); } - SampleVector::iterator beginRead; - m_channels[m_uniqueChannelIndex].m_sampleFifo->readAdvance(beginRead, len/(1< m_interpolators; @@ -74,9 +74,9 @@ private: void run(); unsigned int getNbFifos(); - void callback(qint16* buf, qint32 len); void callbackSO(qint16* buf, qint32 len); void callbackMO(qint16* buf0, qint16* buf1, qint32 len); + void callbackPart(qint16* buf, SampleVector& data, unsigned int iBegin, unsigned int iEnd); }; #endif /* PLUGINS_SAMPLESOURCE_XTRXOUTPUT_XTRXOUTPUTTHREAD_H_ */ diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index c7b8633e4..b7d87e1f9 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -63,7 +63,7 @@ set(sdrbase_SOURCES dsp/afsquelch.cpp dsp/agc.cpp dsp/downchannelizer.cpp - dsp/upchannelizer.cpp + #dsp/upchannelizer.cpp dsp/upsamplechannelizer.cpp dsp/channelmarker.cpp dsp/ctcssdetector.cpp @@ -101,13 +101,14 @@ set(sdrbase_SOURCES dsp/samplemififo.cpp dsp/samplemofifo.cpp dsp/samplesinkfifo.cpp + dsp/samplesourcefifo.cpp dsp/samplesourcefifodb.cpp dsp/basebandsamplesink.cpp dsp/basebandsamplesource.cpp dsp/nullsink.cpp dsp/recursivefilters.cpp dsp/threadedbasebandsamplesink.cpp - dsp/threadedbasebandsamplesource.cpp + #dsp/threadedbasebandsamplesource.cpp dsp/wfir.cpp dsp/devicesamplesource.cpp dsp/devicesamplesink.cpp @@ -179,7 +180,7 @@ set(sdrbase_HEADERS dsp/afsquelch.h dsp/autocorrector.h dsp/downchannelizer.h - dsp/upchannelizer.h + #dsp/upchannelizer.h dsp/upsamplechannelizer.h dsp/channelmarker.h dsp/channelsamplesource.h @@ -241,12 +242,13 @@ set(sdrbase_HEADERS dsp/samplemififo.h dsp/samplemofifo.h dsp/samplesinkfifo.h + dsp/samplesourcefifo.h dsp/samplesourcefifodb.h dsp/basebandsamplesink.h dsp/basebandsamplesource.h dsp/nullsink.h dsp/threadedbasebandsamplesink.h - dsp/threadedbasebandsamplesource.h + #dsp/threadedbasebandsamplesource.h dsp/wfir.h dsp/devicesamplesource.h dsp/devicesamplesink.h diff --git a/sdrbase/device/deviceapi.cpp b/sdrbase/device/deviceapi.cpp index e58101c23..8f7fb49f3 100644 --- a/sdrbase/device/deviceapi.cpp +++ b/sdrbase/device/deviceapi.cpp @@ -107,23 +107,23 @@ void DeviceAPI::removeChannelSink(ThreadedBasebandSampleSink* sink, int streamIn } } -void DeviceAPI::addChannelSource(ThreadedBasebandSampleSource* source, int streamIndex) +void DeviceAPI::addChannelSource(BasebandSampleSource* source, int streamIndex) { (void) streamIndex; if (m_deviceSinkEngine) { - m_deviceSinkEngine->addThreadedSource(source); + m_deviceSinkEngine->addChannelSource(source); } else if (m_deviceMIMOEngine) { m_deviceMIMOEngine->addChannelSource(source); } } -void DeviceAPI::removeChannelSource(ThreadedBasebandSampleSource* source, int streamIndex) +void DeviceAPI::removeChannelSource(BasebandSampleSource* source, int streamIndex) { (void) streamIndex; if (m_deviceSinkEngine) { - m_deviceSinkEngine->removeThreadedSource(source); + m_deviceSinkEngine->removeChannelSource(source); } else if (m_deviceMIMOEngine) { m_deviceMIMOEngine->removeChannelSource(source); } diff --git a/sdrbase/device/deviceapi.h b/sdrbase/device/deviceapi.h index b886a2f2b..d1d168edb 100644 --- a/sdrbase/device/deviceapi.h +++ b/sdrbase/device/deviceapi.h @@ -26,7 +26,7 @@ class BasebandSampleSink; class ThreadedBasebandSampleSink; -class ThreadedBasebandSampleSource; +class BasebandSampleSource; class MIMOChannel; class ChannelAPI; class DeviceSampleSink; @@ -73,8 +73,8 @@ public: void addChannelSink(ThreadedBasebandSampleSink* sink, int streamIndex = 0); //!< Add a channel sink (Rx) void removeChannelSink(ThreadedBasebandSampleSink* sink, int streamIndex = 0); //!< Remove a channel sink (Rx) - void addChannelSource(ThreadedBasebandSampleSource* sink, int streamIndex = 0); //!< Add a channel source (Tx) - void removeChannelSource(ThreadedBasebandSampleSource* sink, int streamIndex = 0); //!< Remove a channel source (Tx) + void addChannelSource(BasebandSampleSource* sink, int streamIndex = 0); //!< Add a channel source (Tx) + void removeChannelSource(BasebandSampleSource* sink, int streamIndex = 0); //!< Remove a channel source (Tx) void addMIMOChannel(MIMOChannel* channel); //!< Add a MIMO channel (n Rx and m Tx combination) void removeMIMOChannel(MIMOChannel* channe); //!< Remove a MIMO channel (n Rx and m Tx combination) diff --git a/sdrbase/dsp/bandpass.h b/sdrbase/dsp/bandpass.h index 2daf57b16..f46489fd7 100644 --- a/sdrbase/dsp/bandpass.h +++ b/sdrbase/dsp/bandpass.h @@ -5,8 +5,8 @@ #include #include "dsp/dsptypes.h" -#undef M_PI -#define M_PI 3.14159265358979323846 +// #undef M_PI +// #define M_PI 3.14159265358979323846 template class Bandpass { public: diff --git a/sdrbase/dsp/basebandsamplesource.cpp b/sdrbase/dsp/basebandsamplesource.cpp index e3ce8f2b1..ed1bb4720 100644 --- a/sdrbase/dsp/basebandsamplesource.cpp +++ b/sdrbase/dsp/basebandsamplesource.cpp @@ -20,12 +20,9 @@ #include "util/message.h" BasebandSampleSource::BasebandSampleSource() : - m_guiMessageQueue(0), - m_sampleFifo(48000), // arbitrary, will be adjusted to match device sink FIFO size - m_deviceSampleFifo(0) + m_guiMessageQueue(nullptr) { connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); - connect(&m_sampleFifo, SIGNAL(dataWrite(int)), this, SLOT(handleWriteToSampleFifo(int))); } BasebandSampleSource::~BasebandSampleSource() @@ -38,51 +35,8 @@ void BasebandSampleSource::handleInputMessages() while ((message = m_inputMessageQueue.pop()) != 0) { - if (handleMessage(*message)) - { + if (handleMessage(*message)) { delete message; } } } - -void BasebandSampleSource::handleWriteToSampleFifo(int nbSamples) -{ - handleWriteToFifo(&m_sampleFifo, nbSamples); -} - -void BasebandSampleSource::handleWriteToDeviceFifo(int nbSamples) -{ - handleWriteToFifo(m_deviceSampleFifo, nbSamples); -} - -void BasebandSampleSource::handleWriteToFifo(SampleSourceFifoDB *sampleFifo, int nbSamples) -{ - SampleVector::iterator writeAt; - sampleFifo->getWriteIterator(writeAt); - pullAudio(nbSamples); // Pre-fetch input audio samples this is mandatory to keep things running smoothly - - for (int i = 0; i < nbSamples; i++) - { - pull((*writeAt)); - sampleFifo->bumpIndex(writeAt); - } -} - - -void BasebandSampleSource::setDeviceSampleSourceFifo(SampleSourceFifoDB *deviceSampleFifo) -{ - if (m_deviceSampleFifo != deviceSampleFifo) - { - if (m_deviceSampleFifo) { - qDebug("BasebandSampleSource::setDeviceSampleSourceFifo: disconnect device FIFO %p", m_deviceSampleFifo); - disconnect(m_deviceSampleFifo, SIGNAL(dataWrite(int)), this, SLOT(handleWriteToDeviceFifo(int))); - } - - if (deviceSampleFifo) { - qDebug("BasebandSampleSource::setDeviceSampleSourceFifo: connect device FIFO %p", deviceSampleFifo); - connect(deviceSampleFifo, SIGNAL(dataWrite(int)), this, SLOT(handleWriteToDeviceFifo(int))); - } - - m_deviceSampleFifo = deviceSampleFifo; - } -} diff --git a/sdrbase/dsp/basebandsamplesource.h b/sdrbase/dsp/basebandsamplesource.h index 1f8288181..2a7eaa8db 100644 --- a/sdrbase/dsp/basebandsamplesource.h +++ b/sdrbase/dsp/basebandsamplesource.h @@ -35,44 +35,19 @@ public: virtual void start() = 0; virtual void stop() = 0; - virtual void pull(Sample& sample) = 0; - virtual void pullAudio(int nbSamples) { (void) nbSamples; } - - /** direct feeding of sample source FIFO */ - void feed(SampleSourceFifoDB* sampleFifo, int nbSamples) - { - SampleVector::iterator writeAt; - sampleFifo->getWriteIterator(writeAt); - pullAudio(nbSamples); // Pre-fetch input audio samples this is mandatory to keep things running smoothly - - for (int i = 0; i < nbSamples; i++) - { - pull((*writeAt)); - sampleFifo->bumpIndex(writeAt); - } - } - - SampleSourceFifoDB& getSampleSourceFifo() { return m_sampleFifo; } - + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples) = 0; virtual bool handleMessage(const Message& cmd) = 0; //!< Processing of a message. Returns true if message has actually been processed MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication virtual void setMessageQueueToGUI(MessageQueue *queue) { m_guiMessageQueue = queue; } MessageQueue *getMessageQueueToGUI() { return m_guiMessageQueue; } - void setDeviceSampleSourceFifo(SampleSourceFifoDB *deviceSampleFifo); protected: MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication MessageQueue *m_guiMessageQueue; //!< Input message queue to the GUI - SampleSourceFifoDB m_sampleFifo; //!< Internal FIFO for multi-channel processing - SampleSourceFifoDB *m_deviceSampleFifo; //!< Reference to the device FIFO for single channel processing - - void handleWriteToFifo(SampleSourceFifoDB *sampleFifo, int nbSamples); protected slots: void handleInputMessages(); - void handleWriteToSampleFifo(int nbSamples); - void handleWriteToDeviceFifo(int nbSamples); }; #endif /* SDRBASE_DSP_BASEBANDSAMPLESOURCE_H_ */ diff --git a/sdrbase/dsp/channelsamplesource.h b/sdrbase/dsp/channelsamplesource.h index 68c373d6a..4cdcda174 100644 --- a/sdrbase/dsp/channelsamplesource.h +++ b/sdrbase/dsp/channelsamplesource.h @@ -33,6 +33,7 @@ public: virtual void pull(SampleVector::iterator begin, unsigned int nbSamples) = 0; //!< pull nbSamples from the source and write them starting at begin virtual void pullOne(Sample& sample) = 0; //!< pull a single sample from the source + virtual void prefetch(unsigned int nbSamples) = 0; //!< Do operation(s) before pulling nbSamples }; #endif // SDRBASE_DSP_CHANNELSAMPLESOURCE_H_ diff --git a/sdrbase/dsp/cwkeyer.cpp b/sdrbase/dsp/cwkeyer.cpp index dcbc12b5b..f4913d99d 100644 --- a/sdrbase/dsp/cwkeyer.cpp +++ b/sdrbase/dsp/cwkeyer.cpp @@ -551,6 +551,18 @@ void CWKeyer::handleInputMessages() void CWKeyer::applySettings(const CWKeyerSettings& settings, bool force) { + qDebug() << "CWKeyer::applySettings: " + << " m_dashKey: " << settings.m_dashKey + << " m_dashKeyModifiers: " << settings.m_dashKeyModifiers + << " m_dotKey: " << settings.m_dotKey + << " m_dotKeyModifiers: " << settings.m_dotKeyModifiers + << " m_keyboardIambic: " << settings.m_keyboardIambic + << " m_loop: " << settings.m_loop + << " m_mode: " << settings.m_mode + << " m_sampleRate: " << settings.m_sampleRate + << " m_text: " << settings.m_text + << " m_wpm: " << settings.m_wpm; + if ((m_settings.m_wpm != settings.m_wpm) || (m_settings.m_sampleRate != settings.m_sampleRate) || force) { @@ -618,7 +630,7 @@ void CWKeyer::webapiSettingsPutPatch( cwKeyerSettings.m_wpm = apiCwKeyerSettings->getWpm(); } if (channelSettingsKeys.contains("cwKeyer.keyboardIambic")) { - cwKeyerSettings.m_wpm = apiCwKeyerSettings->getKeyboardIambic() != 0; + cwKeyerSettings.m_keyboardIambic = apiCwKeyerSettings->getKeyboardIambic() != 0; } if (channelSettingsKeys.contains("cwKeyer.dotKey")) { cwKeyerSettings.m_dotKey = (Qt::Key) apiCwKeyerSettings->getDotKey(); diff --git a/sdrbase/dsp/devicesamplesink.h b/sdrbase/dsp/devicesamplesink.h index ac370b5e7..98cdb89fa 100644 --- a/sdrbase/dsp/devicesamplesink.h +++ b/sdrbase/dsp/devicesamplesink.h @@ -21,7 +21,7 @@ #include -#include "samplesourcefifodb.h" +#include "samplesourcefifo.h" #include "util/message.h" #include "util/messagequeue.h" #include "export.h" @@ -114,7 +114,7 @@ public: MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } virtual void setMessageQueueToGUI(MessageQueue *queue) = 0; // pure virtual so that child classes must have to deal with this MessageQueue *getMessageQueueToGUI() { return m_guiMessageQueue; } - SampleSourceFifoDB* getSampleFifo() { return &m_sampleSourceFifo; } + SampleSourceFifo* getSampleFifo() { return &m_sampleSourceFifo; } static qint64 calculateDeviceCenterFrequency( quint64 centerFrequency, @@ -141,7 +141,7 @@ protected slots: void handleInputMessages(); protected: - SampleSourceFifoDB m_sampleSourceFifo; + SampleSourceFifo m_sampleSourceFifo; MessageQueue m_inputMessageQueue; //!< Input queue to the sink MessageQueue *m_guiMessageQueue; //!< Input message queue to the GUI }; diff --git a/sdrbase/dsp/dspcommands.cpp b/sdrbase/dsp/dspcommands.cpp index 32b8a8e9b..5ecd58e0c 100644 --- a/sdrbase/dsp/dspcommands.cpp +++ b/sdrbase/dsp/dspcommands.cpp @@ -36,9 +36,7 @@ MESSAGE_CLASS_DEFINITION(DSPRemoveBasebandSampleSink, Message) MESSAGE_CLASS_DEFINITION(DSPRemoveSpectrumSink, Message) MESSAGE_CLASS_DEFINITION(DSPRemoveBasebandSampleSource, Message) MESSAGE_CLASS_DEFINITION(DSPAddThreadedBasebandSampleSink, Message) -MESSAGE_CLASS_DEFINITION(DSPAddThreadedBasebandSampleSource, Message) MESSAGE_CLASS_DEFINITION(DSPRemoveThreadedBasebandSampleSink, Message) -MESSAGE_CLASS_DEFINITION(DSPRemoveThreadedBasebandSampleSource, Message) MESSAGE_CLASS_DEFINITION(DSPAddAudioSink, Message) MESSAGE_CLASS_DEFINITION(DSPRemoveAudioSink, Message) MESSAGE_CLASS_DEFINITION(DSPConfigureCorrection, Message) diff --git a/sdrbase/dsp/dspcommands.h b/sdrbase/dsp/dspcommands.h index 886630590..ebd038915 100644 --- a/sdrbase/dsp/dspcommands.h +++ b/sdrbase/dsp/dspcommands.h @@ -197,18 +197,6 @@ private: ThreadedBasebandSampleSink* m_threadedSampleSink; }; -class SDRBASE_API DSPAddThreadedBasebandSampleSource : public Message { - MESSAGE_CLASS_DECLARATION - -public: - DSPAddThreadedBasebandSampleSource(ThreadedBasebandSampleSource* threadedSampleSource) : Message(), m_threadedSampleSource(threadedSampleSource) { } - - ThreadedBasebandSampleSource* getThreadedSampleSource() const { return m_threadedSampleSource; } - -private: - ThreadedBasebandSampleSource* m_threadedSampleSource; -}; - class SDRBASE_API DSPRemoveThreadedBasebandSampleSink : public Message { MESSAGE_CLASS_DECLARATION @@ -221,18 +209,6 @@ private: ThreadedBasebandSampleSink* m_threadedSampleSink; }; -class SDRBASE_API DSPRemoveThreadedBasebandSampleSource : public Message { - MESSAGE_CLASS_DECLARATION - -public: - DSPRemoveThreadedBasebandSampleSource(ThreadedBasebandSampleSource* threadedSampleSource) : Message(), m_threadedSampleSource(threadedSampleSource) { } - - ThreadedBasebandSampleSource* getThreadedSampleSource() const { return m_threadedSampleSource; } - -private: - ThreadedBasebandSampleSource* m_threadedSampleSource; -}; - class SDRBASE_API DSPAddAudioSink : public Message { MESSAGE_CLASS_DECLARATION diff --git a/sdrbase/dsp/dspdevicemimoengine.cpp b/sdrbase/dsp/dspdevicemimoengine.cpp index a8f88b867..e86986c07 100644 --- a/sdrbase/dsp/dspdevicemimoengine.cpp +++ b/sdrbase/dsp/dspdevicemimoengine.cpp @@ -19,16 +19,16 @@ #include #include "dspcommands.h" -#include "threadedbasebandsamplesource.h" #include "threadedbasebandsamplesink.h" +#include "basebandsamplesource.h" #include "devicesamplemimo.h" #include "mimochannel.h" #include "dspdevicemimoengine.h" MESSAGE_CLASS_DEFINITION(DSPDeviceMIMOEngine::SetSampleMIMO, Message) -MESSAGE_CLASS_DEFINITION(DSPDeviceMIMOEngine::AddThreadedBasebandSampleSource, Message) -MESSAGE_CLASS_DEFINITION(DSPDeviceMIMOEngine::RemoveThreadedBasebandSampleSource, Message) +MESSAGE_CLASS_DEFINITION(DSPDeviceMIMOEngine::AddBasebandSampleSource, Message) +MESSAGE_CLASS_DEFINITION(DSPDeviceMIMOEngine::RemoveBasebandSampleSource, Message) MESSAGE_CLASS_DEFINITION(DSPDeviceMIMOEngine::AddThreadedBasebandSampleSink, Message) MESSAGE_CLASS_DEFINITION(DSPDeviceMIMOEngine::RemoveThreadedBasebandSampleSink, Message) MESSAGE_CLASS_DEFINITION(DSPDeviceMIMOEngine::AddMIMOChannel, Message) @@ -151,23 +151,23 @@ void DSPDeviceMIMOEngine::setMIMOSequence(int sequence) m_sampleMIMOSequence = sequence; } -void DSPDeviceMIMOEngine::addChannelSource(ThreadedBasebandSampleSource* source, int index) +void DSPDeviceMIMOEngine::addChannelSource(BasebandSampleSource* source, int index) { - qDebug() << "DSPDeviceMIMOEngine::addThreadedSource: " + qDebug() << "DSPDeviceMIMOEngine::addChannelSource: " << source->objectName().toStdString().c_str() << " at: " << index; - AddThreadedBasebandSampleSource cmd(source, index); + AddBasebandSampleSource cmd(source, index); m_syncMessenger.sendWait(cmd); } -void DSPDeviceMIMOEngine::removeChannelSource(ThreadedBasebandSampleSource* source, int index) +void DSPDeviceMIMOEngine::removeChannelSource(BasebandSampleSource* source, int index) { - qDebug() << "DSPDeviceMIMOEngine::removeThreadedSource: " + qDebug() << "DSPDeviceMIMOEngine::removeChannelSource: " << source->objectName().toStdString().c_str() << " at: " << index; - RemoveThreadedBasebandSampleSource cmd(source, index); + RemoveBasebandSampleSource cmd(source, index); m_syncMessenger.sendWait(cmd); } @@ -311,17 +311,30 @@ void DSPDeviceMIMOEngine::workSampleSourceFifos() } std::vector vbegin; - vbegin.resize(sampleFifo->getNbStreams()); + std::vector data = sampleFifo->getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; unsigned int remainder = sampleFifo->remainderSync(); while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) { + sampleFifo->writeSync(remainder, iPart1Begin, iPart1End, iPart2Begin, iPart2End); + // pull samples from the sources by stream - for (unsigned int streamIndex = 0; streamIndex < sampleFifo->getNbStreams(); streamIndex++) { - workSamplesSource(vbegin[streamIndex], remainder, streamIndex); + + if (iPart1Begin != iPart1End) + { + for (unsigned int streamIndex = 0; streamIndex < sampleFifo->getNbStreams(); streamIndex++) { + workSamplesSource(data[streamIndex], iPart1Begin, iPart1End, streamIndex); + } } - // write pulled samples to FIFO - sampleFifo->writeSync(vbegin, remainder); + + if (iPart2Begin != iPart2End) + { + for (unsigned int streamIndex = 0; streamIndex < sampleFifo->getNbStreams(); streamIndex++) { + workSamplesSource(data[streamIndex], iPart2Begin, iPart2End, streamIndex); + } + } + // get new remainder remainder = sampleFifo->remainderSync(); } @@ -364,15 +377,21 @@ void DSPDeviceMIMOEngine::workSampleSourceFifo(unsigned int streamIndex) return; } - SampleVector::iterator begin; + SampleVector& data = sampleFifo->getData(streamIndex); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; unsigned int amount = sampleFifo->remainderAsync(streamIndex); while ((amount > 0) && (m_inputMessageQueue.size() == 0)) { - // pull remainderAsync() samples from the sources stream - workSamplesSource(begin, amount, streamIndex); - // write pulled samples to FIFO's corresponding stream - sampleFifo->writeAsync(begin, amount, streamIndex); + sampleFifo->writeAsync(amount, iPart1Begin, iPart1End, iPart2Begin, iPart2End, streamIndex); + // part1 + if (iPart1Begin != iPart1End) { + workSamplesSource(data, iPart1Begin, iPart1End, streamIndex); + } + // part2 + if (iPart2Begin != iPart2End) { + workSamplesSource(data, iPart2Begin, iPart2End, streamIndex); + } // get new amount amount = sampleFifo->remainderAsync(streamIndex); } @@ -415,41 +434,56 @@ void DSPDeviceMIMOEngine::workSamplesSink(const SampleVector::const_iterator& vb } } -void DSPDeviceMIMOEngine::workSamplesSource(SampleVector::iterator& begin, unsigned int nbSamples, unsigned int streamIndex) +void DSPDeviceMIMOEngine::workSamplesSource(SampleVector& data, unsigned int iBegin, unsigned int iEnd, unsigned int streamIndex) { - if (m_threadedBasebandSampleSources[streamIndex].size() == 0) + unsigned int nbSamples = iEnd - iBegin; + SampleVector::iterator begin = data.begin() + iBegin; + + m_sourceZeroBuffers[streamIndex].allocate(nbSamples, Sample{16384,0}); + std::copy( + m_sourceZeroBuffers[streamIndex].m_vector.begin(), + m_sourceZeroBuffers[streamIndex].m_vector.begin() + nbSamples, + begin + ); + + m_spectrumSink->feed(begin, begin + nbSamples, false); + qDebug("DSPDeviceMIMOEngine::workSamplesSource: nbSamples: %u streamIndex: %u", nbSamples, streamIndex); + + return; + + if (m_basebandSampleSources[streamIndex].size() == 0) { m_sourceZeroBuffers[streamIndex].allocate(nbSamples, Sample{0,0}); - begin = m_sourceZeroBuffers[streamIndex].m_vector.begin(); + std::copy( + m_sourceZeroBuffers[streamIndex].m_vector.begin(), + m_sourceZeroBuffers[streamIndex].m_vector.begin() + nbSamples, + begin + ); } - else if (m_threadedBasebandSampleSources[streamIndex].size() == 1) + else if (m_basebandSampleSources[streamIndex].size() == 1) { - ThreadedBasebandSampleSource *sampleSource = m_threadedBasebandSampleSources[streamIndex].front(); - sampleSource->getSampleSourceFifo().readAdvance(begin, nbSamples); - begin -= nbSamples; + BasebandSampleSource *sampleSource = m_basebandSampleSources[streamIndex].front(); + sampleSource->pull(begin, nbSamples); } else { - SampleVector::const_iterator sourceBegin; - ThreadedBasebandSampleSources::const_iterator it = m_threadedBasebandSampleSources[streamIndex].begin(); - ThreadedBasebandSampleSource *sampleSource = *it; - sampleSource->getSampleSourceFifo().readAdvance(sourceBegin, nbSamples); - sourceBegin -= nbSamples; m_sourceSampleBuffers[streamIndex].allocate(nbSamples); - std::copy(sourceBegin, sourceBegin + nbSamples, m_sourceSampleBuffers[streamIndex].m_vector.begin()); - ++it; + BasebandSampleSources::const_iterator srcIt = m_basebandSampleSources[streamIndex].begin(); + BasebandSampleSource *sampleSource = *srcIt; + sampleSource->pull(begin, nbSamples); + ++srcIt; - for (; it != m_threadedBasebandSampleSources[streamIndex].end(); ++it) + for (; srcIt != m_basebandSampleSources[streamIndex].end(); ++srcIt) { - sampleSource = *it; - sampleSource->getSampleSourceFifo().readAdvance(sourceBegin, nbSamples); - sourceBegin -= nbSamples; + sampleSource = *srcIt; + SampleVector::iterator aBegin = m_sourceSampleBuffers[streamIndex].m_vector.begin(); + sampleSource->pull(aBegin, nbSamples); std::transform( - m_sourceSampleBuffers[streamIndex].m_vector.begin(), - m_sourceSampleBuffers[streamIndex].m_vector.begin() + nbSamples, - sourceBegin, - m_sourceSampleBuffers[streamIndex].m_vector.begin(), - [](Sample& a, const Sample& b) -> Sample { + aBegin, + aBegin + nbSamples, + begin, + begin, + [](Sample& a, const Sample& b) -> Sample { // TODO: scale by number of sources return Sample{a.real()+b.real(), a.imag()+b.imag()}; } ); @@ -459,7 +493,9 @@ void DSPDeviceMIMOEngine::workSamplesSource(SampleVector::iterator& begin, unsig } // pull data from MIMO channels - for (MIMOChannels::const_iterator it = m_mimoChannels.begin(); it != m_mimoChannels.end(); ++it) { + for (MIMOChannels::const_iterator it = m_mimoChannels.begin(); it != m_mimoChannels.end(); ++it) + { + //qDebug("DSPDeviceMIMOEngine::workSamplesSource: nbSamples: %u stream: %u", nbSamples, streamIndex); (*it)->pull(begin, nbSamples, streamIndex); } @@ -543,13 +579,13 @@ DSPDeviceMIMOEngine::State DSPDeviceMIMOEngine::gotoIdle(int subsystemIndex) m_deviceSampleMIMO->stopTx(); // stop everything - std::vector::const_iterator vtSourceIt = m_threadedBasebandSampleSources.begin(); + std::vector::const_iterator vSourceIt = m_basebandSampleSources.begin(); - for (; vtSourceIt != m_threadedBasebandSampleSources.end(); vtSourceIt++) + for (; vSourceIt != m_basebandSampleSources.end(); vSourceIt++) { - for (ThreadedBasebandSampleSources::const_iterator it = vtSourceIt->begin(); it != vtSourceIt->end(); ++it) + for (BasebandSampleSources::const_iterator it = vSourceIt->begin(); it != vSourceIt->end(); ++it) { - qDebug() << "DSPDeviceMIMOEngine::gotoIdle: stopping ThreadedBasebandSampleSource(" << (*it)->getSampleSourceObjectName().toStdString().c_str() << ")"; + qDebug() << "DSPDeviceMIMOEngine::gotoIdle: stopping BasebandSampleSource(" << (*it)->objectName().toStdString().c_str() << ")"; (*it)->stop(); } } @@ -664,12 +700,12 @@ DSPDeviceMIMOEngine::State DSPDeviceMIMOEngine::gotoInit(int subsystemIndex) DSPSignalNotification notif(sinkStreamSampleRate, sinkCenterFrequency); - if (isink < m_threadedBasebandSampleSources.size()) + if (isink < m_basebandSampleSources.size()) { - for (ThreadedBasebandSampleSources::const_iterator it = m_threadedBasebandSampleSources[isink].begin(); it != m_threadedBasebandSampleSources[isink].end(); ++it) + for (BasebandSampleSources::const_iterator it = m_basebandSampleSources[isink].begin(); it != m_basebandSampleSources[isink].end(); ++it) { - qDebug() << "DSPDeviceMIMOEngine::gotoInit: initializing ThreadedSampleSource(" << (*it)->getSampleSourceObjectName().toStdString().c_str() << ")"; - (*it)->handleSourceMessage(notif); + qDebug() << "DSPDeviceMIMOEngine::gotoInit: initializing BasebandSampleSource(" << (*it)->objectName().toStdString().c_str() << ")"; + (*it)->handleMessage(notif); } } } @@ -732,17 +768,6 @@ DSPDeviceMIMOEngine::State DSPDeviceMIMOEngine::gotoRunning(int subsystemIndex) } } - std::vector::const_iterator vtSourceIt = m_threadedBasebandSampleSources.begin(); - - for (; vtSourceIt != m_threadedBasebandSampleSources.end(); vtSourceIt++) - { - for (ThreadedBasebandSampleSources::const_iterator it = vtSourceIt->begin(); it != vtSourceIt->end(); ++it) - { - qDebug() << "DSPDeviceMIMOEngine::gotoRunning: starting ThreadedBasebandSampleSource(" << (*it)->getSampleSourceObjectName().toStdString().c_str() << ")"; - (*it)->start(); - } - } - for (MIMOChannels::const_iterator it = m_mimoChannels.begin(); it != m_mimoChannels.end(); ++it) { qDebug() << "DSPDeviceMIMOEngine::gotoRunning: starting MIMOChannel sinks: " << (*it)->objectName().toStdString().c_str(); @@ -771,6 +796,17 @@ DSPDeviceMIMOEngine::State DSPDeviceMIMOEngine::gotoRunning(int subsystemIndex) return gotoError(1, "Could not start sample sink"); } + std::vector::const_iterator vSourceIt = m_basebandSampleSources.begin(); + + for (; vSourceIt != m_basebandSampleSources.end(); vSourceIt++) + { + for (BasebandSampleSources::const_iterator it = vSourceIt->begin(); it != vSourceIt->end(); ++it) + { + qDebug() << "DSPDeviceMIMOEngine::gotoRunning: starting BasebandSampleSource(" << (*it)->objectName().toStdString().c_str() << ")"; + (*it)->start(); + } + } + for (MIMOChannels::const_iterator it = m_mimoChannels.begin(); it != m_mimoChannels.end(); ++it) { qDebug() << "DSPDeviceMIMOEngine::gotoRunning: starting MIMOChannel sources: " << (*it)->objectName().toStdString().c_str(); @@ -849,7 +885,7 @@ void DSPDeviceMIMOEngine::handleSetMIMO(DeviceSampleMIMO* mimo) for (int i = 0; i < m_deviceSampleMIMO->getNbSourceFifos(); i++) { - m_threadedBasebandSampleSources.push_back(ThreadedBasebandSampleSources()); + m_basebandSampleSources.push_back(BasebandSampleSources()); m_sourceSampleBuffers.push_back(IncrementalVector()); m_sourceZeroBuffers.push_back(IncrementalVector()); } @@ -1042,36 +1078,36 @@ void DSPDeviceMIMOEngine::handleSynchronousMessages() m_threadedBasebandSampleSinks[isource].remove(threadedSink); } } - else if (AddThreadedBasebandSampleSource::match(*message)) + else if (AddBasebandSampleSource::match(*message)) { - const AddThreadedBasebandSampleSource *msg = (AddThreadedBasebandSampleSource *) message; - ThreadedBasebandSampleSource *threadedSource = msg->getThreadedSampleSource(); + const AddBasebandSampleSource *msg = (AddBasebandSampleSource *) message; + BasebandSampleSource *sampleSource = msg->getSampleSource(); unsigned int isink = msg->getIndex(); - if (isink < m_threadedBasebandSampleSources.size()) + if (isink < m_basebandSampleSources.size()) { - m_threadedBasebandSampleSources[isink].push_back(threadedSource); + m_basebandSampleSources[isink].push_back(sampleSource); // initialize sample rate and center frequency in the sink: int sinkStreamSampleRate = m_deviceSampleMIMO->getSinkSampleRate(isink); quint64 sinkCenterFrequency = m_deviceSampleMIMO->getSinkCenterFrequency(isink); DSPSignalNotification msg(sinkStreamSampleRate, sinkCenterFrequency); - threadedSource->handleSourceMessage(msg); + sampleSource->handleMessage(msg); // start the sink: if (m_stateTx == StRunning) { - threadedSource->start(); + sampleSource->start(); } } } - else if (RemoveThreadedBasebandSampleSource::match(*message)) + else if (RemoveBasebandSampleSource::match(*message)) { - const RemoveThreadedBasebandSampleSource *msg = (RemoveThreadedBasebandSampleSource *) message; - ThreadedBasebandSampleSource* threadedSource = msg->getThreadedSampleSource(); + const RemoveBasebandSampleSource *msg = (RemoveBasebandSampleSource *) message; + BasebandSampleSource* sampleSource = msg->getSampleSource(); unsigned int isink = msg->getIndex(); - if (isink < m_threadedBasebandSampleSources.size()) + if (isink < m_basebandSampleSources.size()) { - threadedSource->stop(); - m_threadedBasebandSampleSources[isink].remove(threadedSource); + sampleSource->stop(); + m_basebandSampleSources[isink].remove(sampleSource); } } else if (AddMIMOChannel::match(*message)) @@ -1297,12 +1333,12 @@ void DSPDeviceMIMOEngine::handleInputMessages() DSPSignalNotification *message = new DSPSignalNotification(sampleRate, centerFrequency); // forward source changes to channel sources with immediate execution (no queuing) - if (istream < m_threadedBasebandSampleSources.size()) + if (istream < m_basebandSampleSources.size()) { - for (ThreadedBasebandSampleSources::const_iterator it = m_threadedBasebandSampleSources[istream].begin(); it != m_threadedBasebandSampleSources[istream].end(); ++it) + for (BasebandSampleSources::const_iterator it = m_basebandSampleSources[istream].begin(); it != m_basebandSampleSources[istream].end(); ++it) { - qDebug() << "DSPDeviceMIMOEngine::handleSinkMessages: forward message to ThreadedSampleSource(" << (*it)->getSampleSourceObjectName().toStdString().c_str() << ")"; - (*it)->handleSourceMessage(*message); + qDebug() << "DSPDeviceMIMOEngine::handleSinkMessages: forward message to BasebandSampleSource(" << (*it)->objectName().toStdString().c_str() << ")"; + (*it)->handleMessage(*message); } } diff --git a/sdrbase/dsp/dspdevicemimoengine.h b/sdrbase/dsp/dspdevicemimoengine.h index b0e5afc20..021b09bf9 100644 --- a/sdrbase/dsp/dspdevicemimoengine.h +++ b/sdrbase/dsp/dspdevicemimoengine.h @@ -48,34 +48,34 @@ public: DeviceSampleMIMO* m_sampleMIMO; }; - class AddThreadedBasebandSampleSource : public Message { + class AddBasebandSampleSource : public Message { MESSAGE_CLASS_DECLARATION public: - AddThreadedBasebandSampleSource(ThreadedBasebandSampleSource* threadedSampleSource, unsigned int index) : + AddBasebandSampleSource(BasebandSampleSource* sampleSource, unsigned int index) : Message(), - m_threadedSampleSource(threadedSampleSource), + m_sampleSource(sampleSource), m_index(index) { } - ThreadedBasebandSampleSource* getThreadedSampleSource() const { return m_threadedSampleSource; } + BasebandSampleSource* getSampleSource() const { return m_sampleSource; } unsigned int getIndex() const { return m_index; } private: - ThreadedBasebandSampleSource* m_threadedSampleSource; + BasebandSampleSource* m_sampleSource; unsigned int m_index; }; - class RemoveThreadedBasebandSampleSource : public Message { + class RemoveBasebandSampleSource : public Message { MESSAGE_CLASS_DECLARATION public: - RemoveThreadedBasebandSampleSource(ThreadedBasebandSampleSource* threadedSampleSource, unsigned int index) : + RemoveBasebandSampleSource(BasebandSampleSource* sampleSource, unsigned int index) : Message(), - m_threadedSampleSource(threadedSampleSource), + m_sampleSource(sampleSource), m_index(index) { } - ThreadedBasebandSampleSource* getThreadedSampleSource() const { return m_threadedSampleSource; } + BasebandSampleSource* getSampleSource() const { return m_sampleSource; } unsigned int getIndex() const { return m_index; } private: - ThreadedBasebandSampleSource* m_threadedSampleSource; + BasebandSampleSource* m_sampleSource; unsigned int m_index; }; @@ -261,8 +261,8 @@ public: void setMIMOSequence(int sequence); //!< Set the sample MIMO sequence in type uint getUID() const { return m_uid; } - void addChannelSource(ThreadedBasebandSampleSource* source, int index = 0); //!< Add a channel source that will run on its own thread - void removeChannelSource(ThreadedBasebandSampleSource* source, int index = 0); //!< Remove a channel source that runs on its own thread + void addChannelSource(BasebandSampleSource* source, int index = 0); //!< Add a channel source + void removeChannelSource(BasebandSampleSource* source, int index = 0); //!< Remove a channel source void addChannelSink(ThreadedBasebandSampleSink* sink, int index = 0); //!< Add a channel sink that will run on its own thread void removeChannelSink(ThreadedBasebandSampleSink* sink, int index = 0); //!< Remove a channel sink that runs on its own thread void addMIMOChannel(MIMOChannel *channel); //!< Add a MIMO channel @@ -358,12 +358,11 @@ private: typedef std::list BasebandSampleSinks; std::vector m_basebandSampleSinks; //!< ancillary sample sinks on main thread (per input stream) - typedef std::list ThreadedBasebandSampleSinks; std::vector m_threadedBasebandSampleSinks; //!< channel sample sinks on their own thread (per input stream) - typedef std::list ThreadedBasebandSampleSources; - std::vector m_threadedBasebandSampleSources; //!< channel sample sources on their own threads (per output stream) + typedef std::list BasebandSampleSources; + std::vector m_basebandSampleSources; //!< channel sample sources (per output stream) std::vector> m_sourceSampleBuffers; std::vector> m_sourceZeroBuffers; @@ -382,7 +381,7 @@ private: void workSamplesSink(const SampleVector::const_iterator& vbegin, const SampleVector::const_iterator& vend, unsigned int streamIndex); void workSampleSourceFifos(); //!< transfer samples of all source streams (sync mode) void workSampleSourceFifo(unsigned int streamIndex); //!< transfer samples of one source stream (async mode) - void workSamplesSource(SampleVector::iterator& begin, unsigned int nbSamples, unsigned int streamIndex); + void workSamplesSource(SampleVector& data, unsigned int iBegin, unsigned int iEnd, unsigned int streamIndex); State gotoIdle(int subsystemIndex); //!< Go to the idle state State gotoInit(int subsystemIndex); //!< Go to the acquisition init state from idle diff --git a/sdrbase/dsp/dspdevicesinkengine.cpp b/sdrbase/dsp/dspdevicesinkengine.cpp index f1d578cfb..ae87f184d 100644 --- a/sdrbase/dsp/dspdevicesinkengine.cpp +++ b/sdrbase/dsp/dspdevicesinkengine.cpp @@ -27,7 +27,6 @@ #include "dsp/devicesamplesink.h" #include "dsp/dspcommands.h" #include "samplesourcefifodb.h" -#include "threadedbasebandsamplesource.h" DSPDeviceSinkEngine::DSPDeviceSinkEngine(uint32_t uid, QObject* parent) : QThread(parent), @@ -113,17 +112,17 @@ void DSPDeviceSinkEngine::setSinkSequence(int sequence) m_sampleSinkSequence = sequence; } -void DSPDeviceSinkEngine::addThreadedSource(ThreadedBasebandSampleSource* source) +void DSPDeviceSinkEngine::addChannelSource(BasebandSampleSource* source) { - qDebug() << "DSPDeviceSinkEngine::addThreadedSource: " << source->objectName().toStdString().c_str(); - DSPAddThreadedBasebandSampleSource cmd(source); + qDebug() << "DSPDeviceSinkEngine::addChannelSource: " << source->objectName().toStdString().c_str(); + DSPAddBasebandSampleSource cmd(source); m_syncMessenger.sendWait(cmd); } -void DSPDeviceSinkEngine::removeThreadedSource(ThreadedBasebandSampleSource* source) +void DSPDeviceSinkEngine::removeChannelSource(BasebandSampleSource* source) { - qDebug() << "DSPDeviceSinkEngine::removeThreadedSource: " << source->objectName().toStdString().c_str(); - DSPRemoveThreadedBasebandSampleSource cmd(source); + qDebug() << "DSPDeviceSinkEngine::removeChannelSource: " << source->objectName().toStdString().c_str(); + DSPRemoveBasebandSampleSource cmd(source); m_syncMessenger.sendWait(cmd); } @@ -157,52 +156,88 @@ QString DSPDeviceSinkEngine::sinkDeviceDescription() return cmd.getDeviceDescription(); } -void DSPDeviceSinkEngine::work(int nbWriteSamples) +void DSPDeviceSinkEngine::workSampleFifo() { - // multiple channel sources handling - if ((m_threadedBasebandSampleSources.size() + m_basebandSampleSources.size()) > 1) - { -// qDebug("DSPDeviceSinkEngine::work: multiple channel sources handling: %u", m_multipleSourcesDivisionFactor); + SampleSourceFifo *sourceFifo = m_deviceSampleSink->getSampleFifo(); - SampleVector::iterator writeBegin; - SampleSourceFifoDB* sampleFifo = m_deviceSampleSink->getSampleFifo(); - sampleFifo->getWriteIterator(writeBegin); - SampleVector::iterator writeAt = writeBegin; - std::vector sampleSourceIterators; + if (!sourceFifo) { + return; + } - for (ThreadedBasebandSampleSources::iterator it = m_threadedBasebandSampleSources.begin(); it != m_threadedBasebandSampleSources.end(); ++it) - { - sampleSourceIterators.push_back(SampleVector::iterator()); - (*it)->getSampleSourceFifo().readAdvance(sampleSourceIterators.back(), nbWriteSamples); - sampleSourceIterators.back() -= nbWriteSamples; - } + SampleVector& data = sourceFifo->getData(); + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + unsigned int remainder = sourceFifo->remainder(); - for (BasebandSampleSources::iterator it = m_basebandSampleSources.begin(); it != m_basebandSampleSources.end(); ++it) - { - sampleSourceIterators.push_back(SampleVector::iterator()); - (*it)->getSampleSourceFifo().readAdvance(sampleSourceIterators.back(), nbWriteSamples); - sampleSourceIterators.back() -= nbWriteSamples; - } + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + sourceFifo->write(remainder, iPart1Begin, iPart1End, iPart2Begin, iPart2End); - for (int is = 0; is < nbWriteSamples; is++) - { - // pull data from sources FIFOs and merge them in the device sample FIFO - for (std::vector::iterator it = sampleSourceIterators.begin(); it != sampleSourceIterators.end(); ++it) - { - Sample s = (**it); - s /= m_multipleSourcesDivisionFactor; - ++(*it); + if (iPart1Begin != iPart1End) { + workSamples(data, iPart1Begin, iPart1End); + } - if (it == sampleSourceIterators.begin()) { - (*writeAt) = s; - } else { - (*writeAt) += s; - } - } + if (iPart2Begin != iPart2End) { + workSamples(data, iPart2Begin, iPart2End); + } - sampleFifo->bumpIndex(writeAt); - } - } + remainder = sourceFifo->remainder(); + } +} + +void DSPDeviceSinkEngine::workSamples(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + unsigned int nbSamples = iEnd - iBegin; + SampleVector::iterator begin = data.begin() + iBegin; + + if (m_basebandSampleSources.size() == 0) + { + m_sourceZeroBuffer.allocate(nbSamples, Sample{0,0}); + std::copy( + m_sourceZeroBuffer.m_vector.begin(), + m_sourceZeroBuffer.m_vector.begin() + nbSamples, + data.begin() + iBegin + ); + } + else if (m_basebandSampleSources.size() == 1) + { + BasebandSampleSource *source = m_basebandSampleSources.front(); + source->pull(begin, nbSamples); + } + else + { + m_sourceSampleBuffer.allocate(nbSamples); + SampleVector::iterator sBegin = m_sourceSampleBuffer.m_vector.begin(); + BasebandSampleSources::const_iterator srcIt = m_basebandSampleSources.begin(); + BasebandSampleSource *source = *srcIt; + source->pull(begin, nbSamples); + srcIt++; + m_sumIndex = 1; + + for (; srcIt != m_basebandSampleSources.end(); ++srcIt, m_sumIndex++) + { + source = *srcIt; + source->pull(sBegin, nbSamples); + std::transform( + sBegin, + sBegin + nbSamples, + data.begin() + iBegin, + data.begin() + iBegin, + [this](Sample& a, const Sample& b) -> Sample { + int den = m_sumIndex + 1; // at each stage scale sum by n/n+1 and input by 1/n+1 + int nom = m_sumIndex; // so that final sum is scaled by N (number of channels) + return Sample{ + a.real()/den + nom*(b.real()/den), + a.imag()/den + nom*(b.imag()/den) + }; + } + ); + } + } + + // possibly feed data to spectrum sink + if (m_spectrumSink) { + m_spectrumSink->feed(data.begin() + iBegin, data.begin() + iEnd, false); + } } // notStarted -> idle -> init -> running -+ @@ -240,18 +275,6 @@ DSPDeviceSinkEngine::State DSPDeviceSinkEngine::gotoIdle() (*it)->stop(); } - for(ThreadedBasebandSampleSources::const_iterator it = m_threadedBasebandSampleSources.begin(); it != m_threadedBasebandSampleSources.end(); it++) - { - qDebug() << "DSPDeviceSinkEngine::gotoIdle: stopping ThreadedSampleSource(" << (*it)->getSampleSourceObjectName().toStdString().c_str() << ")"; - (*it)->stop(); - } - - if (m_spectrumSink) - { - disconnect(m_deviceSampleSink->getSampleFifo(), SIGNAL(dataRead(int)), this, SLOT(handleForwardToSpectrumSink(int))); - m_spectrumSink->stop(); - } - m_deviceDescription.clear(); m_sampleRate = 0; @@ -299,12 +322,6 @@ DSPDeviceSinkEngine::State DSPDeviceSinkEngine::gotoInit() (*it)->handleMessage(notif); } - for (ThreadedBasebandSampleSources::const_iterator it = m_threadedBasebandSampleSources.begin(); it != m_threadedBasebandSampleSources.end(); ++it) - { - qDebug() << "DSPDeviceSinkEngine::gotoInit: initializing ThreadedSampleSource(" << (*it)->getSampleSourceObjectName().toStdString().c_str() << ")"; - (*it)->handleSourceMessage(notif); - } - if (m_spectrumSink) { m_spectrumSink->handleMessage(notif); } @@ -358,15 +375,8 @@ DSPDeviceSinkEngine::State DSPDeviceSinkEngine::gotoRunning() (*it)->start(); } - for (ThreadedBasebandSampleSources::const_iterator it = m_threadedBasebandSampleSources.begin(); it != m_threadedBasebandSampleSources.end(); ++it) - { - qDebug() << "DSPDeviceSinkEngine::gotoRunning: starting ThreadedSampleSource(" << (*it)->getSampleSourceObjectName().toStdString().c_str() << ")"; - (*it)->start(); - } - if (m_spectrumSink) { - connect(m_deviceSampleSink->getSampleFifo(), SIGNAL(dataRead(int)), this, SLOT(handleForwardToSpectrumSink(int))); m_spectrumSink->start(); } @@ -387,22 +397,28 @@ DSPDeviceSinkEngine::State DSPDeviceSinkEngine::gotoError(const QString& errorMe void DSPDeviceSinkEngine::handleSetSink(DeviceSampleSink* sink) { - gotoIdle(); - m_deviceSampleSink = sink; - if(m_deviceSampleSink != 0) { - qDebug("DSPDeviceSinkEngine::handleSetSink: set %s", qPrintable(sink->getDeviceDescription())); - } else { - qDebug("DSPDeviceSinkEngine::handleSetSource: set none"); - } + if (!m_deviceSampleSink) { // Early leave + return; + } + + qDebug("DSPDeviceSinkEngine::handleSetSink: set %s", qPrintable(sink->getDeviceDescription())); + + QObject::connect( + m_deviceSampleSink->getSampleFifo(), + &SampleSourceFifo::dataRead, + this, + &DSPDeviceSinkEngine::handleData, + Qt::QueuedConnection + ); + } -void DSPDeviceSinkEngine::handleData(int nbSamples) +void DSPDeviceSinkEngine::handleData() { - if(m_state == StRunning) - { - work(nbSamples); + if (m_state == StRunning) { + workSampleFifo(); } } @@ -452,7 +468,7 @@ void DSPDeviceSinkEngine::handleSynchronousMessages() spectrumSink->stop(); } - m_spectrumSink = 0; + m_spectrumSink = nullptr; } else if (DSPAddBasebandSampleSource::match(*message)) { @@ -460,7 +476,6 @@ void DSPDeviceSinkEngine::handleSynchronousMessages() m_basebandSampleSources.push_back(source); DSPSignalNotification notif(m_sampleRate, m_centerFrequency); source->handleMessage(notif); - checkNumberOfBasebandSources(); if (m_state == StRunning) { @@ -476,30 +491,6 @@ void DSPDeviceSinkEngine::handleSynchronousMessages() } m_basebandSampleSources.remove(source); - checkNumberOfBasebandSources(); - } - else if (DSPAddThreadedBasebandSampleSource::match(*message)) - { - ThreadedBasebandSampleSource *threadedSource = ((DSPAddThreadedBasebandSampleSource*) message)->getThreadedSampleSource(); - m_threadedBasebandSampleSources.push_back(threadedSource); - DSPSignalNotification notif(m_sampleRate, m_centerFrequency); - threadedSource->handleSourceMessage(notif); - checkNumberOfBasebandSources(); - - if (m_state == StRunning) - { - threadedSource->start(); - } - } - else if (DSPRemoveThreadedBasebandSampleSource::match(*message)) - { - ThreadedBasebandSampleSource* threadedSource = ((DSPRemoveThreadedBasebandSampleSource*) message)->getThreadedSampleSource(); - if (m_state == StRunning) { - threadedSource->stop(); - } - - m_threadedBasebandSampleSources.remove(threadedSource); - checkNumberOfBasebandSources(); } m_syncMessenger.done(m_state); @@ -534,12 +525,6 @@ void DSPDeviceSinkEngine::handleInputMessages() (*it)->handleMessage(*message); } - for (ThreadedBasebandSampleSources::const_iterator it = m_threadedBasebandSampleSources.begin(); it != m_threadedBasebandSampleSources.end(); ++it) - { - qDebug() << "DSPDeviceSinkEngine::handleSourceMessages: forward message to ThreadedSampleSource(" << (*it)->getSampleSourceObjectName().toStdString().c_str() << ")"; - (*it)->handleSourceMessage(*message); - } - // forward changes to listeners on DSP output queue MessageQueue *guiMessageQueue = m_deviceSampleSink->getMessageQueueToGUI(); @@ -555,65 +540,3 @@ void DSPDeviceSinkEngine::handleInputMessages() } } } - -void DSPDeviceSinkEngine::handleForwardToSpectrumSink(int nbSamples) -{ - if (m_spectrumSink) - { - SampleSourceFifoDB* sampleFifo = m_deviceSampleSink->getSampleFifo(); - SampleVector::iterator readUntil; - sampleFifo->getReadIterator(readUntil); - m_spectrumSink->feed(readUntil - nbSamples, readUntil, false); - } -} - -void DSPDeviceSinkEngine::checkNumberOfBasebandSources() -{ - SampleSourceFifoDB* sampleFifo = m_deviceSampleSink->getSampleFifo(); - - // single channel source handling - if ((m_threadedBasebandSampleSources.size() + m_basebandSampleSources.size()) == 1) - { - qDebug("DSPDeviceSinkEngine::checkNumberOfBasebandSources: single channel mode"); - disconnect(sampleFifo, SIGNAL(dataWrite(int)), this, SLOT(handleData(int))); - - if (m_threadedBasebandSampleSources.size() == 1) { - m_threadedBasebandSampleSources.back()->setDeviceSampleSourceFifo(sampleFifo); - } else if (m_basebandSampleSources.size() == 1) { - m_basebandSampleSources.back()->setDeviceSampleSourceFifo(sampleFifo); - } - - m_multipleSourcesDivisionFactor = 1; // for consistency but it is not used in this case - } - // null or multiple channel sources handling - else - { - int nbSources = 0; - - for (ThreadedBasebandSampleSources::iterator it = m_threadedBasebandSampleSources.begin(); it != m_threadedBasebandSampleSources.end(); ++it) - { - (*it)->setDeviceSampleSourceFifo(0); - nbSources++; - } - - for (BasebandSampleSources::iterator it = m_basebandSampleSources.begin(); it != m_basebandSampleSources.end(); ++it) - { - (*it)->setDeviceSampleSourceFifo(0); - nbSources++; - } - - if (nbSources == 0) { - m_multipleSourcesDivisionFactor = 1; - } else if (nbSources < 3) { - m_multipleSourcesDivisionFactor = nbSources; - } else { - m_multipleSourcesDivisionFactor = 1< 1) { - connect(sampleFifo, SIGNAL(dataWrite(int)), this, SLOT(handleData(int)), Qt::QueuedConnection); - } - - qDebug("DSPDeviceSinkEngine::checkNumberOfBasebandSources: handle %d channel(s)", nbSources); - } -} diff --git a/sdrbase/dsp/dspdevicesinkengine.h b/sdrbase/dsp/dspdevicesinkengine.h index be14385fc..d7044eb47 100644 --- a/sdrbase/dsp/dspdevicesinkengine.h +++ b/sdrbase/dsp/dspdevicesinkengine.h @@ -23,18 +23,20 @@ #include #include #include + #include #include #include + #include "dsp/dsptypes.h" #include "dsp/fftwindow.h" #include "util/messagequeue.h" #include "util/syncmessenger.h" +#include "util/incrementalvector.h" #include "export.h" class DeviceSampleSink; class BasebandSampleSource; -class ThreadedBasebandSampleSource; class BasebandSampleSink; class SDRBASE_API DSPDeviceSinkEngine : public QThread { @@ -67,8 +69,8 @@ public: DeviceSampleSink *getSink() { return m_deviceSampleSink; } void setSinkSequence(int sequence); //!< Set the sample sink sequence in type - void addThreadedSource(ThreadedBasebandSampleSource* source); //!< Add a baseband sample source that will run on its own thread - void removeThreadedSource(ThreadedBasebandSampleSource* source); //!< Remove a baseband sample source that runs on its own thread + void addChannelSource(BasebandSampleSource* source); //!< Add a baseband sample source + void removeChannelSource(BasebandSampleSource* source); //!< Remove a baseband sample source void addSpectrumSink(BasebandSampleSink* spectrumSink); //!< Add a spectrum vis baseband sample sink void removeSpectrumSink(BasebandSampleSink* spectrumSink); //!< Add a spectrum vis baseband sample sink @@ -95,25 +97,18 @@ private: typedef std::list BasebandSampleSources; BasebandSampleSources m_basebandSampleSources; //!< baseband sample sources within main thread (usually file input) - typedef std::list ThreadedBasebandSampleSources; - ThreadedBasebandSampleSources m_threadedBasebandSampleSources; //!< baseband sample sources on their own threads (usually channels) - - typedef std::map BasebandSampleSourcesIteratorMap; - typedef std::pair BasebandSampleSourcesIteratorMapKV; - BasebandSampleSourcesIteratorMap m_basebandSampleSourcesIteratorMap; - - typedef std::map ThreadedBasebandSampleSourcesIteratorMap; - typedef std::pair ThreadedBasebandSampleSourcesIteratorMapKV; - ThreadedBasebandSampleSourcesIteratorMap m_threadedBasebandSampleSourcesIteratorMap; - BasebandSampleSink *m_spectrumSink; + IncrementalVector m_sourceSampleBuffer; + IncrementalVector m_sourceZeroBuffer; uint32_t m_sampleRate; quint64 m_centerFrequency; uint32_t m_multipleSourcesDivisionFactor; + unsigned int m_sumIndex; //!< channel index when summing channels void run(); - void work(int nbWriteSamples); //!< transfer samples from beseband sources to sink if in running state + void workSampleFifo(); //!< transfer samples from baseband sources to sink if in running state + void workSamples(SampleVector& data, unsigned int iBegin, unsigned int iEnd); State gotoIdle(); //!< Go to the idle state State gotoInit(); //!< Go to the acquisition init state from idle @@ -121,13 +116,11 @@ private: State gotoError(const QString& errorMsg); //!< Go to an error state void handleSetSink(DeviceSampleSink* sink); //!< Manage sink setting - void checkNumberOfBasebandSources(); private slots: - void handleData(int nbSamples); //!< Handle data when samples have to be written to the sample FIFO + void handleData(); //!< Handle data when samples have to be written to the sample FIFO void handleInputMessages(); //!< Handle input message queue void handleSynchronousMessages(); //!< Handle synchronous messages with the thread - void handleForwardToSpectrumSink(int nbSamples); }; diff --git a/sdrbase/dsp/filterrc.cpp b/sdrbase/dsp/filterrc.cpp index d3abdf851..2096304a9 100644 --- a/sdrbase/dsp/filterrc.cpp +++ b/sdrbase/dsp/filterrc.cpp @@ -56,4 +56,31 @@ void LowPassFilterRC::process(const Real& sample_in, Real& sample_out) sample_out = m_y1; } +// Construct 1st order high-pass IIR filter. +HighPassFilterRC::HighPassFilterRC(Real timeconst) : + m_timeconst(timeconst), + m_y1(0) +{ + m_a1 = 1 - exp(-1/m_timeconst); + m_b0 = 1 + m_a1; +} +// Reconfigure +void HighPassFilterRC::configure(Real timeconst) +{ + m_timeconst = timeconst; + m_y1 = 0; + m_a1 = 1 - exp(-1/m_timeconst); + m_b0 = 1 + m_a1; + + qDebug() << "HighPassFilterRC::configure: t: " << m_timeconst + << " a1: " << m_a1 + << " b0: " << m_b0; +} + +// Process samples. +void HighPassFilterRC::process(const Real& sample_in, Real& sample_out) +{ + m_y1 = (sample_in * m_b0) - (m_y1 * m_a1); + sample_out = m_y1; +} diff --git a/sdrbase/dsp/filterrc.h b/sdrbase/dsp/filterrc.h index fd72edc0d..7b2a4b2d0 100644 --- a/sdrbase/dsp/filterrc.h +++ b/sdrbase/dsp/filterrc.h @@ -37,7 +37,34 @@ public: /** * Reconfigure filter with new time constant */ - void configure(Real timeout); + void configure(Real timeconst); + + /** Process samples. */ + void process(const Real& sample_in, Real& sample_out); + +private: + Real m_timeconst; + Real m_y1; + Real m_a1; + Real m_b0; +}; + +/** First order high-pass IIR filter for real-valued signals. */ +class SDRBASE_API HighPassFilterRC +{ +public: + + /** + * Construct 1st order high-pass IIR filter. + * + * timeconst :: RC time constant in seconds (1 / (2 * PI * cutoff_freq) + */ + HighPassFilterRC(Real timeconst); + + /** + * Reconfigure filter with new time constant + */ + void configure(Real timeconst); /** Process samples. */ void process(const Real& sample_in, Real& sample_out); diff --git a/sdrbase/dsp/lowpass.h b/sdrbase/dsp/lowpass.h index d10fe8444..b7b5e6ed4 100644 --- a/sdrbase/dsp/lowpass.h +++ b/sdrbase/dsp/lowpass.h @@ -5,9 +5,6 @@ #include #include "dsp/dsptypes.h" -#undef M_PI -#define M_PI 3.14159265358979323846 - template class Lowpass { public: Lowpass() : m_ptr(0) { } diff --git a/sdrbase/dsp/mimochannel.h b/sdrbase/dsp/mimochannel.h index d6d1082bf..48b8d062d 100644 --- a/sdrbase/dsp/mimochannel.h +++ b/sdrbase/dsp/mimochannel.h @@ -38,7 +38,7 @@ public: virtual void startSources() = 0; virtual void stopSources() = 0; virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, unsigned int sinkIndex) = 0; - virtual void pull(const SampleVector::iterator& begin, unsigned int nbSamples, unsigned int sourceIndex) = 0; + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples, unsigned int sourceIndex) = 0; virtual bool handleMessage(const Message& cmd) = 0; //!< Processing of a message. Returns true if message has actually been processed MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication diff --git a/sdrbase/dsp/samplemofifo.cpp b/sdrbase/dsp/samplemofifo.cpp index 482030b8d..a43bdbc7e 100644 --- a/sdrbase/dsp/samplemofifo.cpp +++ b/sdrbase/dsp/samplemofifo.cpp @@ -17,13 +17,15 @@ #include "samplemofifo.h" +const unsigned int SampleMOFifo::m_rwDivisor = 2; + void SampleMOFifo::init(unsigned int nbStreams, unsigned int size) { m_nbStreams = nbStreams; m_size = size; m_readCount = 0; m_readHead = 0; - m_writeHead = size/8; + m_writeHead = size/m_rwDivisor; m_data.resize(nbStreams); m_vReadCount.clear(); m_vReadHead.clear(); @@ -34,7 +36,7 @@ void SampleMOFifo::init(unsigned int nbStreams, unsigned int size) m_data[stream].resize(size); m_vReadCount.push_back(0); m_vReadHead.push_back(0); - m_vWriteHead.push_back(size/8); + m_vWriteHead.push_back(size/m_rwDivisor); } } @@ -43,14 +45,14 @@ void SampleMOFifo::resize(unsigned int size) m_size = size; m_readCount = 0; m_readHead = 0; - m_writeHead = size/8; + m_writeHead = size/m_rwDivisor; for (unsigned int stream = 0; stream < m_nbStreams; stream++) { m_data[stream].resize(size); m_vReadCount.push_back(0); m_vReadHead.push_back(0); - m_vWriteHead.push_back(size/8); + m_vWriteHead.push_back(size/m_rwDivisor); } } @@ -74,14 +76,14 @@ void SampleMOFifo::reset() { m_readCount = 0; m_readHead = 0; - m_writeHead = m_size/8; + m_writeHead = m_size/m_rwDivisor; for (unsigned int stream = 0; stream < m_nbStreams; stream++) { m_data[stream].resize(m_size); m_vReadCount[stream] = 0; m_vReadHead[stream] = 0; - m_vWriteHead[stream] = m_size/8; + m_vWriteHead[stream] = m_size/m_rwDivisor; } } @@ -170,40 +172,6 @@ void SampleMOFifo::writeSync( ipart2End = remaining; m_writeHead = remaining; } -} - -void SampleMOFifo::commitWriteSync(unsigned int amount) -{ - QMutexLocker mutexLocker(&m_mutex); - m_readCount = amount < m_readCount ? m_readCount - amount : 0; -} - -void SampleMOFifo::writeSync( - unsigned int& ipart1Begin, unsigned int& ipart1End, // first part offsets where to write - unsigned int& ipart2Begin, unsigned int& ipart2End // second part offsets -) -{ - QMutexLocker mutexLocker(&m_mutex); - unsigned int spaceLeft = m_size - m_writeHead; - unsigned int amount = m_readHead >= m_writeHead ? m_readHead - m_writeHead : m_size + m_readHead - m_writeHead; - - if (amount <= spaceLeft) - { - ipart1Begin = m_writeHead; - ipart1End = m_writeHead + amount; - ipart2Begin = m_size; - ipart2End = m_size; - m_writeHead += amount; - } - else - { - unsigned int remaining = amount - spaceLeft; - ipart1Begin = m_writeHead; - ipart1End = m_size; - ipart2Begin = 0; - ipart2End = remaining; - m_writeHead = remaining; - } m_readCount = amount < m_readCount ? m_readCount - amount : 0; } @@ -295,41 +263,11 @@ void SampleMOFifo::writeAsync( ipart2End = remaining; m_vWriteHead[stream] = remaining; } -} - -void SampleMOFifo::commitWriteAsync(unsigned int amount, unsigned int stream) -{ - QMutexLocker mutexLocker(&m_mutex); - m_vReadCount[stream] = amount < m_vReadCount[stream] ? m_vReadCount[stream] - amount : 0; -} - -void SampleMOFifo::writeAsync( - unsigned int& ipart1Begin, unsigned int& ipart1End, - unsigned int& ipart2Begin, unsigned int& ipart2End, - unsigned int stream -) -{ - QMutexLocker mutexLocker(&m_mutex); - unsigned int spaceLeft = m_size - m_vWriteHead[stream]; - unsigned int amount = m_vReadHead[stream] >= m_vWriteHead[stream] ? m_vReadHead[stream] - m_vWriteHead[stream] : m_size + m_vReadHead[stream] - m_vWriteHead[stream]; - - if (amount <= spaceLeft) - { - ipart1Begin = m_vWriteHead[stream]; - ipart1End = m_vWriteHead[stream] + amount; - ipart2Begin = m_size; - ipart2End = m_size; - m_vWriteHead[stream] += amount; - } - else - { - unsigned int remaining = amount - spaceLeft; - ipart1Begin = m_vWriteHead[stream]; - ipart1End = m_size; - ipart2Begin = 0; - ipart2End = remaining; - m_vWriteHead[stream] = remaining; - } m_vReadCount[stream] = amount < m_vReadCount[stream] ? m_vReadCount[stream] - amount : 0; } + +unsigned int SampleMOFifo::getSizePolicy(unsigned int sampleRate) +{ + return (sampleRate/100)*64; // .64s +} diff --git a/sdrbase/dsp/samplemofifo.h b/sdrbase/dsp/samplemofifo.h index 6eccfec3b..2d8d71b7d 100644 --- a/sdrbase/dsp/samplemofifo.h +++ b/sdrbase/dsp/samplemofifo.h @@ -40,16 +40,11 @@ public: unsigned int& ipart2Begin, unsigned int& ipart2End // second part offsets ); void writeSync(const std::vector& vbegin, unsigned int amount); //!< copy write - void writeSync( //!< in place write with auto amount - unsigned int& ipart1Begin, unsigned int& ipart1End, // first part offsets where to write - unsigned int& ipart2Begin, unsigned int& ipart2End // second part offsets - ); void writeSync( //!< in place write with given amount unsigned int amount, unsigned int& ipart1Begin, unsigned int& ipart1End, // first part offsets where to write unsigned int& ipart2Begin, unsigned int& ipart2End // second part offsets ); - void commitWriteSync(unsigned int amount); //!< For in place write tells how much samples were written once done void readAsync( unsigned int amount, @@ -58,18 +53,12 @@ public: unsigned int stream ); void writeAsync(const SampleVector::iterator& begin, unsigned int amount, unsigned int stream); //!< copy write - void writeAsync( //!< in place write with auto amount - unsigned int& ipart1Begin, unsigned int& ipart1End, - unsigned int& ipart2Begin, unsigned int& ipart2End, - unsigned int stream - ); void writeAsync( //!< in place write with given amount unsigned int amount, unsigned int& ipart1Begin, unsigned int& ipart1End, unsigned int& ipart2Begin, unsigned int& ipart2End, unsigned int stream ); - void commitWriteAsync(unsigned int amount, unsigned int stream); //!< For in place write tells how much samples were written once done std::vector& getData() { return m_data; } SampleVector& getData(unsigned int stream) { return m_data[stream]; } @@ -90,6 +79,9 @@ public: return m_vReadCount[stream]; } + static unsigned int getSizePolicy(unsigned int sampleRate); + static const unsigned int m_rwDivisor; + signals: void dataSyncRead(); void dataAsyncRead(int streamIndex); diff --git a/sdrbase/dsp/samplesourcefifo.cpp b/sdrbase/dsp/samplesourcefifo.cpp new file mode 100644 index 000000000..059c40f36 --- /dev/null +++ b/sdrbase/dsp/samplesourcefifo.cpp @@ -0,0 +1,134 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "samplesourcefifo.h" + +const unsigned int SampleSourceFifo::m_rwDivisor = 2; +const unsigned int SampleSourceFifo::m_guardDivisor = 10; + +SampleSourceFifo::SampleSourceFifo(QObject *parent) : + QObject(parent) +{} + +SampleSourceFifo::SampleSourceFifo(unsigned int size, QObject *parent) : + QObject(parent) +{ + resize(size); +} + +void SampleSourceFifo::resize(unsigned int size) +{ + QMutexLocker mutexLocker(&m_mutex); + m_size = size; + m_lowGuard = m_size / m_guardDivisor; + m_highGuard = m_size - (m_size/m_guardDivisor); + m_midPoint = m_size / m_rwDivisor; + m_readCount = 0; + m_readHead = 0; + m_writeHead = m_midPoint; + m_data.resize(size); +} + +void SampleSourceFifo::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_readCount = 0; + m_readHead = 0; + m_writeHead = m_midPoint; +} + +SampleSourceFifo::~SampleSourceFifo() +{} + +void SampleSourceFifo::read( + unsigned int amount, + unsigned int& ipart1Begin, unsigned int& ipart1End, // first part offsets where to read + unsigned int& ipart2Begin, unsigned int& ipart2End // second part offsets +) +{ + QMutexLocker mutexLocker(&m_mutex); + unsigned int spaceLeft = m_size - m_readHead; + m_readCount += amount; + + if (amount <= spaceLeft) + { + ipart1Begin = m_readHead; + ipart1End = m_readHead + amount; + ipart2Begin = m_size; + ipart2End = m_size; + m_readHead += amount; + } + else + { + unsigned int remaining = (amount < m_size ? amount : m_size) - spaceLeft; + ipart1Begin = m_readHead; + ipart1End = m_size; + ipart2Begin = 0; + ipart2End = remaining; + m_readHead = remaining; + } + + emit dataRead(); +} + +void SampleSourceFifo::write( + unsigned int amount, + unsigned int& ipart1Begin, unsigned int& ipart1End, // first part offsets where to write + unsigned int& ipart2Begin, unsigned int& ipart2End // second part offsets +) +{ + QMutexLocker mutexLocker(&m_mutex); + unsigned int rwDelta = m_writeHead >= m_readHead ? m_writeHead - m_readHead : m_size - (m_readHead - m_writeHead); + + if (rwDelta < m_lowGuard) + { + qWarning("SampleSourceFifo::write: underrun (write too slow) using %d old samples", m_midPoint - m_lowGuard); + m_writeHead = m_readHead + m_midPoint < m_size ? m_readHead + m_midPoint : m_readHead + m_midPoint - m_size; + } + else if (rwDelta > m_highGuard) + { + qWarning("SampleSourceFifo::write: overrrun (read too slow) dropping %d samples", m_highGuard - m_midPoint); + m_writeHead = m_readHead + m_midPoint < m_size ? m_readHead + m_midPoint : m_readHead + m_midPoint - m_size; + } + + unsigned int spaceLeft = m_size - m_writeHead; + + if (amount <= spaceLeft) + { + ipart1Begin = m_writeHead; + ipart1End = m_writeHead + amount; + ipart2Begin = m_size; + ipart2End = m_size; + m_writeHead += amount; + } + else + { + unsigned int remaining = (amount < m_size ? amount : m_size) - spaceLeft; + ipart1Begin = m_writeHead; + ipart1End = m_size; + ipart2Begin = 0; + ipart2End = remaining; + m_writeHead = remaining; + } + + m_readCount = amount < m_readCount ? m_readCount - amount : 0; +} + +unsigned int SampleSourceFifo::getSizePolicy(unsigned int sampleRate) +{ + return (sampleRate/100)*64; // .64s +} \ No newline at end of file diff --git a/sdrbase/dsp/samplesourcefifo.h b/sdrbase/dsp/samplesourcefifo.h new file mode 100644 index 000000000..205cdb48d --- /dev/null +++ b/sdrbase/dsp/samplesourcefifo.h @@ -0,0 +1,83 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef SDRBASE_DSP_SAMPLESOURCEFIFO_H_ +#define SDRBASE_DSP_SAMPLESOURCEFIFO_H_ + +#include +#include +#include "dsp/dsptypes.h" +#include "export.h" + +class SDRBASE_API SampleSourceFifo : public QObject { + Q_OBJECT +public: + SampleSourceFifo(QObject *parent = nullptr); + SampleSourceFifo(unsigned int size, QObject *parent = nullptr); + ~SampleSourceFifo(); + void resize(unsigned int size); + void reset(); + + SampleVector& getData() { return m_data; } + void read( + unsigned int amount, + unsigned int& ipart1Begin, unsigned int& ipart1End, // first part offsets where to read + unsigned int& ipart2Begin, unsigned int& ipart2End // second part offsets + ); + void write( //!< in place write + unsigned int amount, + unsigned int& ipart1Begin, unsigned int& ipart1End, // first part offsets where to write + unsigned int& ipart2Begin, unsigned int& ipart2End // second part offsets + ); + unsigned int remainder() + { + QMutexLocker mutexLocker(&m_mutex); + return m_readCount; + } + /** returns ratio of off center over buffer size with sign: negative read lags and positive read leads */ + float getRWBalance() const + { + int delta; + if (m_writeHead > m_readHead) { + delta = (m_size/m_rwDivisor) - (m_writeHead - m_readHead); + } else { + delta = (m_readHead - m_writeHead) - (m_size/m_rwDivisor); + } + return delta / (float) m_size; + } + unsigned int size() const { return m_size; } + + static unsigned int getSizePolicy(unsigned int sampleRate); + static const unsigned int m_rwDivisor; + static const unsigned int m_guardDivisor; + +signals: + void dataRead(); + +private: + SampleVector m_data; + unsigned int m_size; + unsigned int m_lowGuard; + unsigned int m_highGuard; + unsigned int m_midPoint; + unsigned int m_readHead; + unsigned int m_writeHead; + unsigned int m_readCount; + QMutex m_mutex; +}; + +#endif // SDRBASE_DSP_SAMPLESOURCEFIFO_H_ \ No newline at end of file diff --git a/sdrbase/dsp/upsamplechannelizer.cpp b/sdrbase/dsp/upsamplechannelizer.cpp index 065fa8d25..7e1887ea7 100644 --- a/sdrbase/dsp/upsamplechannelizer.cpp +++ b/sdrbase/dsp/upsamplechannelizer.cpp @@ -29,11 +29,13 @@ UpSampleChannelizer::UpSampleChannelizer(ChannelSampleSource* sampleSource) : m_filterChainSetMode(false), m_sampleSource(sampleSource), - m_outputSampleRate(0), + m_basebandSampleRate(0), m_requestedInputSampleRate(0), m_requestedCenterFrequency(0), - m_currentInputSampleRate(0), - m_currentCenterFrequency(0) + m_channelSampleRate(0), + m_channelFrequencyOffset(0), + m_log2Interp(0), + m_filterChainHash(0) { } @@ -109,64 +111,85 @@ void UpSampleChannelizer::pull(SampleVector::iterator begin, unsigned int nbSamp } } -void UpSampleChannelizer::applyConfiguration() +void UpSampleChannelizer::prefetch(unsigned int nbSamples) +{ + unsigned int log2Interp = m_filterStages.size(); + m_sampleSource->prefetch(nbSamples/(1< stageIndexes; - m_currentCenterFrequency = m_outputSampleRate * HBFilterChainConverter::convertToIndexes(log2Interp, filterChainHash, stageIndexes); - m_requestedCenterFrequency = m_currentCenterFrequency; + m_channelFrequencyOffset = m_basebandSampleRate * HBFilterChainConverter::convertToIndexes(m_log2Interp, m_filterChainHash, stageIndexes); + m_requestedCenterFrequency = m_channelFrequencyOffset; freeFilterChain(); - m_currentCenterFrequency = m_outputSampleRate * setFilterChain(stageIndexes); - m_currentInputSampleRate = m_outputSampleRate / (1 << m_filterStages.size()); - m_requestedInputSampleRate = m_currentInputSampleRate; - qDebug() << "UpSampleChannelizer::setInterpolation: done:" - << " in:" << m_outputSampleRate - << " out:" << m_currentInputSampleRate - << " fc:" << m_currentCenterFrequency; + m_channelFrequencyOffset = m_basebandSampleRate * setFilterChain(stageIndexes); + m_channelSampleRate = m_basebandSampleRate / (1 << m_filterStages.size()); + m_requestedInputSampleRate = m_channelSampleRate; + + qDebug() << "UpSampleChannelizer::applyInterpolation:" + << " m_log2Interp:" << m_log2Interp + << " m_filterChainHash:" << m_filterChainHash + << " out:" << m_basebandSampleRate + << " in:" << m_channelSampleRate + << " fc:" << m_channelFrequencyOffset; } #ifdef USE_SSE4_1 diff --git a/sdrbase/dsp/upsamplechannelizer.h b/sdrbase/dsp/upsamplechannelizer.h index 8f4727b62..7fd75b447 100644 --- a/sdrbase/dsp/upsamplechannelizer.h +++ b/sdrbase/dsp/upsamplechannelizer.h @@ -23,6 +23,8 @@ #include #include "export.h" +#include "util/message.h" + #include "channelsamplesource.h" #ifdef USE_SSE4_1 @@ -40,10 +42,13 @@ public: virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples); - void setInterpolation(unsigned int log2Interp, unsigned int filterChainHash); //!< Define channelizer with interpolation factor and filter chain definition - void applyConfiguration(int requestedSampleRate, qint64 requestedCenterFrequency); //!< Define channelizer with requested sample rate and center frequency (shift in the baseband) - void setOutputSampleRate(int outputSampleRate); + void setInterpolation(unsigned int log2Interp, unsigned int filterChainHash); //!< Define channelizer with interpolation factor and filter chain definition + void setChannelization(int requestedSampleRate, qint64 requestedCenterFrequency); //!< Define channelizer with requested sample rate and center frequency (shift in the baseband) + void setBasebandSampleRate(int basebandSampleRate, bool interp = false); //!< interp: true => use direct interpolation false => use channel configuration + int getChannelSampleRate() const { return m_channelSampleRate; }; + int getChannelFrequencyOffset() const { return m_channelFrequencyOffset; } protected: struct FilterStage { @@ -75,22 +80,22 @@ protected: bool m_filterChainSetMode; std::vector m_stageSamples; ChannelSampleSource* m_sampleSource; //!< Modulator - int m_outputSampleRate; + int m_basebandSampleRate; int m_requestedInputSampleRate; int m_requestedCenterFrequency; - int m_currentInputSampleRate; - int m_currentCenterFrequency; + int m_channelSampleRate; + int m_channelFrequencyOffset; + unsigned int m_log2Interp; + unsigned int m_filterChainHash; SampleVector m_sampleBuffer; Sample m_sampleIn; - void applyConfiguration(); + void applyChannelization(); + void applyInterpolation(); bool signalContainsChannel(Real sigStart, Real sigEnd, Real chanStart, Real chanEnd) const; Real createFilterChain(Real sigStart, Real sigEnd, Real chanStart, Real chanEnd); double setFilterChain(const std::vector& stageIndexes); //!< returns offset in ratio of sample rate void freeFilterChain(); - -signals: - void outputSampleRateChanged(); }; diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 5f1ccc461..085b80dea 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -2449,6 +2449,13 @@ bool WebAPIRequestMapper::getChannel( QJsonObject settingsJsonObject = channelSettingsJson[channelSettingsKey].toObject(); channelSettingsKeys = settingsJsonObject.keys(); + // get possible sub-keys + if (channelSettingsKeys.contains("cwKeyer")) + { + QJsonObject cwJson; // unused + appendSettingsSubKeys(settingsJsonObject, cwJson, "cwKeyer", channelSettingsKeys); + } + if (channelSettingsKey == "AMDemodSettings") { channelSettings->setAmDemodSettings(new SWGSDRangel::SWGAMDemodSettings()); diff --git a/sdrgui/mainwindow.cpp b/sdrgui/mainwindow.cpp index 20b5aad06..82cdba556 100644 --- a/sdrgui/mainwindow.cpp +++ b/sdrgui/mainwindow.cpp @@ -448,12 +448,12 @@ void MainWindow::addMIMODevice() QStringList rxChannelNamesList(rxChannelNames); channelSelector->addItems(rxChannelNamesList); m_deviceUIs.back()->setNumberOfAvailableRxChannels(rxChannelNamesList.size()); - // Add Tx channels TODO: restore when channel sinks fixed for MIMO - // QList txChannelNames; - // m_pluginManager->listTxChannels(txChannelNames); - // QStringList txChannelNamesList(txChannelNames); - // channelSelector->addItems(txChannelNamesList); - // m_deviceUIs.back()->setNumberOfAvailableTxChannels(txChannelNamesList.size()); + // Add Tx channels + QList txChannelNames; + m_pluginManager->listTxChannels(txChannelNames); + QStringList txChannelNamesList(txChannelNames); + channelSelector->addItems(txChannelNamesList); + m_deviceUIs.back()->setNumberOfAvailableTxChannels(txChannelNamesList.size()); connect(m_deviceUIs.back()->m_samplingDeviceControl->getAddChannelButton(), SIGNAL(clicked(bool)), this, SLOT(channelAddClicked(bool))); diff --git a/swagger/sdrangel/code/qt5/client/SWGHttpRequest.cpp b/swagger/sdrangel/code/qt5/client/SWGHttpRequest.cpp index 6f72adc6f..e89f0cae2 100644 --- a/swagger/sdrangel/code/qt5/client/SWGHttpRequest.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGHttpRequest.cpp @@ -1,6 +1,6 @@ /** * SDRangel - * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- * * OpenAPI spec version: 4.11.6 * Contact: f4exb06@gmail.com