aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Buffer.cpp1
-rw-r--r--src/Buffer.h11
-rw-r--r--src/CharsetTools.cpp143
-rw-r--r--src/CharsetTools.h58
-rw-r--r--src/CicEqualizer.h8
-rw-r--r--src/ConfigParser.cpp94
-rw-r--r--src/ConfigParser.h23
-rw-r--r--src/DabMod.cpp504
-rw-r--r--src/DabModulator.cpp304
-rw-r--r--src/DabModulator.h42
-rw-r--r--src/DifferentialModulator.cpp77
-rw-r--r--src/DifferentialModulator.h5
-rw-r--r--src/EtiReader.cpp88
-rw-r--r--src/EtiReader.h30
-rw-r--r--src/Events.cpp95
-rw-r--r--src/Events.h67
-rw-r--r--src/FIRFilter.cpp9
-rw-r--r--src/FIRFilter.h17
-rw-r--r--src/FicSource.cpp69
-rw-r--r--src/FicSource.h21
-rw-r--r--src/FigParser.cpp1052
-rw-r--r--src/FigParser.h385
-rw-r--r--src/Flowgraph.cpp10
-rw-r--r--src/FormatConverter.cpp177
-rw-r--r--src/FormatConverter.h20
-rw-r--r--src/FrameMultiplexer.cpp12
-rw-r--r--src/FrequencyInterleaver.cpp90
-rw-r--r--src/FrequencyInterleaver.h9
-rw-r--r--src/GainControl.cpp67
-rw-r--r--src/GainControl.h17
-rw-r--r--src/GuardIntervalInserter.cpp271
-rw-r--r--src/GuardIntervalInserter.h49
-rw-r--r--src/InputFileReader.cpp3
-rw-r--r--src/InputReader.h61
-rw-r--r--src/InputTcpReader.cpp3
-rw-r--r--src/InputZeroMQReader.cpp323
-rw-r--r--src/MemlessPoly.cpp14
-rw-r--r--src/MemlessPoly.h19
-rw-r--r--src/ModPlugin.h6
-rw-r--r--src/NullSymbol.cpp18
-rw-r--r--src/NullSymbol.h6
-rw-r--r--src/OfdmGenerator.cpp392
-rw-r--r--src/OfdmGenerator.h106
-rw-r--r--src/OutputFile.cpp31
-rw-r--r--src/OutputMemory.cpp26
-rw-r--r--src/OutputMemory.h6
-rw-r--r--src/OutputZeroMQ.cpp3
-rw-r--r--src/PAPRStats.cpp1
-rw-r--r--src/PAPRStats.h5
-rw-r--r--src/PhaseReference.cpp135
-rw-r--r--src/PhaseReference.h22
-rw-r--r--src/QpskSymbolMapper.cpp267
-rw-r--r--src/QpskSymbolMapper.h5
-rw-r--r--src/Resampler.h3
-rw-r--r--src/SignalMultiplexer.cpp13
-rw-r--r--src/SignalMultiplexer.h5
-rw-r--r--src/TII.cpp115
-rw-r--r--src/TII.h21
-rw-r--r--src/TimestampDecoder.cpp65
-rw-r--r--src/TimestampDecoder.h25
-rw-r--r--src/Utils.cpp184
-rw-r--r--src/Utils.h23
-rw-r--r--[-rwxr-xr-x]src/output/BladeRF.cpp28
-rw-r--r--[-rwxr-xr-x]src/output/BladeRF.h17
-rw-r--r--src/output/Dexter.cpp703
-rw-r--r--src/output/Dexter.h138
-rw-r--r--src/output/Feedback.cpp2
-rw-r--r--src/output/Feedback.h2
-rw-r--r--src/output/Lime.cpp46
-rw-r--r--src/output/Lime.h14
-rw-r--r--src/output/SDR.cpp291
-rw-r--r--src/output/SDR.h17
-rw-r--r--src/output/SDRDevice.h35
-rw-r--r--src/output/Soapy.cpp29
-rw-r--r--src/output/Soapy.h12
-rw-r--r--src/output/UHD.cpp61
-rw-r--r--src/output/UHD.h21
-rw-r--r--src/output/USRPTime.cpp9
78 files changed, 5277 insertions, 1879 deletions
diff --git a/src/Buffer.cpp b/src/Buffer.cpp
index 20f71f9..2c4b171 100644
--- a/src/Buffer.cpp
+++ b/src/Buffer.cpp
@@ -28,6 +28,7 @@
#include "Buffer.h"
#include "PcDebug.h"
+#include <unistd.h>
#include <string>
#include <stdexcept>
#include <cstdlib>
diff --git a/src/Buffer.h b/src/Buffer.h
index d181a46..2c2a65e 100644
--- a/src/Buffer.h
+++ b/src/Buffer.h
@@ -31,12 +31,19 @@
# include <config.h>
#endif
-#include <unistd.h>
#include <vector>
#include <memory>
+#include <complex>
+#include "fpm/fixed.hpp"
+
+typedef std::complex<float> complexf;
+
+using fixed_16 = fpm::fixed<std::int16_t, std::int32_t, 14>;
+typedef std::complex<fixed_16> complexfix;
+typedef std::complex<fpm::fixed_16_16> complexfix_wide;
/* Buffer is a container for a byte array, which is memory-aligned
- * to 32 bytes for SSE performance.
+ * to 32 bytes for SIMD performance.
*
* The allocation/freeing of the data is handled internally.
*/
diff --git a/src/CharsetTools.cpp b/src/CharsetTools.cpp
new file mode 100644
index 0000000..d35c121
--- /dev/null
+++ b/src/CharsetTools.cpp
@@ -0,0 +1,143 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
+ the Queen in Right of Canada (Communications Research Center Canada)
+
+ Most parts of this file are taken from dablin,
+ Copyright (C) 2015-2022 Stefan Pöschel
+
+ Copyright (C) 2023
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+ */
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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, either version 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <vector>
+#include <algorithm>
+#include <stdexcept>
+#include <string>
+#include <ctime>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include "CharsetTools.h"
+
+// --- CharsetTools -----------------------------------------------------------------
+const char* CharsetTools::no_char = "";
+const char* CharsetTools::ebu_values_0x00_to_0x1F[] = {
+ no_char , "\u0118", "\u012E", "\u0172", "\u0102", "\u0116", "\u010E", "\u0218", "\u021A", "\u010A", no_char , no_char , "\u0120", "\u0139" , "\u017B", "\u0143",
+ "\u0105", "\u0119", "\u012F", "\u0173", "\u0103", "\u0117", "\u010F", "\u0219", "\u021B", "\u010B", "\u0147", "\u011A", "\u0121", "\u013A", "\u017C", no_char
+};
+const char* CharsetTools::ebu_values_0x7B_to_0xFF[] = {
+ /* starting some chars earlier than 0x80 -----> */ "\u00AB", "\u016F", "\u00BB", "\u013D", "\u0126",
+ "\u00E1", "\u00E0", "\u00E9", "\u00E8", "\u00ED", "\u00EC", "\u00F3", "\u00F2", "\u00FA", "\u00F9", "\u00D1", "\u00C7", "\u015E", "\u00DF", "\u00A1", "\u0178",
+ "\u00E2", "\u00E4", "\u00EA", "\u00EB", "\u00EE", "\u00EF", "\u00F4", "\u00F6", "\u00FB", "\u00FC", "\u00F1", "\u00E7", "\u015F", "\u011F", "\u0131", "\u00FF",
+ "\u0136", "\u0145", "\u00A9", "\u0122", "\u011E", "\u011B", "\u0148", "\u0151", "\u0150", "\u20AC", "\u00A3", "\u0024", "\u0100", "\u0112", "\u012A", "\u016A",
+ "\u0137", "\u0146", "\u013B", "\u0123", "\u013C", "\u0130", "\u0144", "\u0171", "\u0170", "\u00BF", "\u013E", "\u00B0", "\u0101", "\u0113", "\u012B", "\u016B",
+ "\u00C1", "\u00C0", "\u00C9", "\u00C8", "\u00CD", "\u00CC", "\u00D3", "\u00D2", "\u00DA", "\u00D9", "\u0158", "\u010C", "\u0160", "\u017D", "\u00D0", "\u013F",
+ "\u00C2", "\u00C4", "\u00CA", "\u00CB", "\u00CE", "\u00CF", "\u00D4", "\u00D6", "\u00DB", "\u00DC", "\u0159", "\u010D", "\u0161", "\u017E", "\u0111", "\u0140",
+ "\u00C3", "\u00C5", "\u00C6", "\u0152", "\u0177", "\u00DD", "\u00D5", "\u00D8", "\u00DE", "\u014A", "\u0154", "\u0106", "\u015A", "\u0179", "\u0164", "\u00F0",
+ "\u00E3", "\u00E5", "\u00E6", "\u0153", "\u0175", "\u00FD", "\u00F5", "\u00F8", "\u00FE", "\u014B", "\u0155", "\u0107", "\u015B", "\u017A", "\u0165", "\u0127"
+};
+
+std::string CharsetTools::ConvertCharEBUToUTF8(const uint8_t value) {
+ // convert via LUT
+ if(value <= 0x1F)
+ return ebu_values_0x00_to_0x1F[value];
+ if(value >= 0x7B)
+ return ebu_values_0x7B_to_0xFF[value - 0x7B];
+
+ // convert by hand (avoiding a LUT with mostly 1:1 mapping)
+ switch(value) {
+ case 0x24:
+ return "\u0142";
+ case 0x5C:
+ return "\u016E";
+ case 0x5E:
+ return "\u0141";
+ case 0x60:
+ return "\u0104";
+ }
+
+ // leave untouched
+ return std::string((char*) &value, 1);
+}
+
+
+std::string CharsetTools::ConvertTextToUTF8(const uint8_t *data, size_t len, int charset, std::string* charset_name) {
+ // remove undesired chars
+ std::vector<uint8_t> cleaned_data;
+ for(size_t i = 0; i < len; i++) {
+ switch(data[i]) {
+ case 0x00: // NULL
+ case 0x0A: // PLB
+ case 0x0B: // EoH
+ case 0x1F: // PWB
+ continue;
+ default:
+ cleaned_data.push_back(data[i]);
+ }
+ }
+
+ // convert characters
+ if(charset == 0b0000) { // EBU Latin based
+ if(charset_name)
+ *charset_name = "EBU Latin based";
+
+ std::string result;
+ for(const uint8_t& c : cleaned_data)
+ result += ConvertCharEBUToUTF8(c);
+ return result;
+ }
+
+ if(charset == 0b1111) { // UTF-8
+ if(charset_name)
+ *charset_name = "UTF-8";
+
+ return std::string((char*) &cleaned_data[0], cleaned_data.size());
+ }
+
+ // ignore unsupported charset
+ return "";
+}
+
+
+size_t StringTools::UTF8CharsLen(const std::string &s, size_t chars) {
+ size_t result;
+ for(result = 0; result < s.size(); result++) {
+ // if not a continuation byte, handle counter
+ if((s[result] & 0xC0) != 0x80) {
+ if(chars == 0)
+ break;
+ chars--;
+ }
+ }
+ return result;
+}
+
+size_t StringTools::UTF8Len(const std::string &s) {
+ // ignore continuation bytes
+ return std::count_if(s.cbegin(), s.cend(), [](const char c){return (c & 0xC0) != 0x80;});
+}
+
+std::string StringTools::UTF8Substr(const std::string &s, size_t pos, size_t count) {
+ std::string result = s;
+ result.erase(0, UTF8CharsLen(result, pos));
+ result.erase(UTF8CharsLen(result, count));
+ return result;
+}
diff --git a/src/CharsetTools.h b/src/CharsetTools.h
new file mode 100644
index 0000000..f86692f
--- /dev/null
+++ b/src/CharsetTools.h
@@ -0,0 +1,58 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
+ the Queen in Right of Canada (Communications Research Center Canada)
+
+ Most parts of this file are taken from dablin,
+ Copyright (C) 2015-2022 Stefan Pöschel
+
+ Copyright (C) 2023
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+ */
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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, either version 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <vector>
+#include <stdexcept>
+#include <string>
+#include <ctime>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+
+class CharsetTools {
+ private:
+ static const char* no_char;
+ static const char* ebu_values_0x00_to_0x1F[];
+ static const char* ebu_values_0x7B_to_0xFF[];
+ static std::string ConvertCharEBUToUTF8(const uint8_t value);
+ public:
+ static std::string ConvertTextToUTF8(const uint8_t *data, size_t len, int charset, std::string* charset_name);
+};
+
+typedef std::vector<std::string> string_vector_t;
+
+// --- StringTools -----------------------------------------------------------------
+class StringTools {
+private:
+ static size_t UTF8CharsLen(const std::string &s, size_t chars);
+public:
+ static size_t UTF8Len(const std::string &s);
+ static std::string UTF8Substr(const std::string &s, size_t pos, size_t count);
+};
diff --git a/src/CicEqualizer.h b/src/CicEqualizer.h
index 792da02..4510d0c 100644
--- a/src/CicEqualizer.h
+++ b/src/CicEqualizer.h
@@ -25,18 +25,10 @@
# include <config.h>
#endif
-
#include "ModPlugin.h"
#include <vector>
#include <sys/types.h>
-#include <complex>
-#ifdef __SSE__
-# include <xmmintrin.h>
-#endif
-
-
-typedef std::complex<float> complexf;
class CicEqualizer : public ModCodec
{
diff --git a/src/ConfigParser.cpp b/src/ConfigParser.cpp
index ee7acc3..c92a520 100644
--- a/src/ConfigParser.cpp
+++ b/src/ConfigParser.cpp
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -37,8 +37,7 @@
#include "ConfigParser.h"
#include "Utils.h"
#include "Log.h"
-#include "DabModulator.h"
-#include "output/SDR.h"
+#include "Events.h"
using namespace std;
@@ -64,6 +63,27 @@ static GainMode parse_gainmode(const std::string &gainMode_setting)
throw std::runtime_error("Configuration error");
}
+static FFTEngine parse_fft_engine(const std::string &fft_engine_setting)
+{
+ string fft_engine_minuscule(fft_engine_setting);
+ std::transform(fft_engine_minuscule.begin(), fft_engine_minuscule.end(),
+ fft_engine_minuscule.begin(), ::tolower);
+
+ if (fft_engine_minuscule == "fftw") {
+ return FFTEngine::FFTW;
+ }
+ else if (fft_engine_minuscule == "kiss") {
+ return FFTEngine::KISS;
+ }
+ else if (fft_engine_minuscule == "dexter") {
+ return FFTEngine::DEXTER;
+ }
+
+ cerr << "Modulator fft_engine setting '" << fft_engine_setting <<
+ "' not recognised." << endl;
+ throw std::runtime_error("Configuration error");
+}
+
static void parse_configfile(
const std::string& configuration_file,
mod_settings_t& mod_settings)
@@ -79,6 +99,8 @@ static void parse_configfile(
throw std::runtime_error("Cannot read configuration file");
}
+ mod_settings.startupCheck = pt.Get("general.startupcheck", "");
+
// remote controller interfaces:
if (pt.GetInteger("remotecontrol.telnet", 0) == 1) {
try {
@@ -113,14 +135,21 @@ static void parse_configfile(
}
mod_settings.inputTransport = pt.Get("input.transport", "file");
- mod_settings.inputMaxFramesQueued = pt.GetInteger("input.max_frames_queued",
- ZMQ_INPUT_MAX_FRAME_QUEUE);
- mod_settings.edi_max_delay_ms = pt.GetReal("input.edi_max_delay", 0.0f);
+ mod_settings.edi_max_delay_ms = pt.GetReal("input.edi_max_delay", 0.0);
mod_settings.inputName = pt.Get("input.source", "/dev/stdin");
// log parameters:
+ const string events_endpoint = pt.Get("log.events_endpoint", "");
+ if (not events_endpoint.empty()) {
+#if defined(HAVE_ZEROMQ)
+ events.bind(events_endpoint);
+#else
+ throw std::runtime_error("Cannot configure events sender when compiled without zeromq");
+#endif
+ }
+
if (pt.GetInteger("log.syslog", 0) == 1) {
etiLog.register_backend(make_shared<LogToSyslog>());
}
@@ -148,6 +177,9 @@ static void parse_configfile(
mod_settings.showProcessTime);
// modulator parameters:
+ const string fft_engine_setting = pt.Get("modulator.fft_engine", "fftw");
+ mod_settings.fftEngine = parse_fft_engine(fft_engine_setting);
+
const string gainMode_setting = pt.Get("modulator.gainmode", "var");
mod_settings.gainMode = parse_gainmode(gainMode_setting);
mod_settings.gainmodeVariance = pt.GetReal("modulator.normalise_variance",
@@ -247,7 +279,7 @@ static void parse_configfile(
throw std::runtime_error("Configuration error");
}
else if (sdr_device_config.frequency == 0) {
- sdr_device_config.frequency = parseChannel(chan);
+ sdr_device_config.frequency = parse_channel(chan);
}
else if (sdr_device_config.frequency != 0 && chan != "") {
std::cerr << " UHD output: cannot define both frequency and channel.\n";
@@ -280,7 +312,8 @@ static void parse_configfile(
mod_settings.sdr_device_config = sdr_device_config;
mod_settings.useUHDOutput = true;
}
-#endif
+#endif // defined(HAVE_OUTPUT_UHD)
+
#if defined(HAVE_SOAPYSDR)
else if (output_selected == "soapysdr") {
auto& outputsoapy_conf = mod_settings.sdr_device_config;
@@ -300,7 +333,7 @@ static void parse_configfile(
throw std::runtime_error("Configuration error");
}
else if (outputsoapy_conf.frequency == 0) {
- outputsoapy_conf.frequency = parseChannel(chan);
+ outputsoapy_conf.frequency = parse_channel(chan);
}
else if (outputsoapy_conf.frequency != 0 && chan != "") {
std::cerr << " soapy output: cannot define both frequency and channel.\n";
@@ -311,7 +344,34 @@ static void parse_configfile(
mod_settings.useSoapyOutput = true;
}
-#endif
+#endif // defined(HAVE_SOAPYSDR)
+
+#if defined(HAVE_DEXTER)
+ else if (output_selected == "dexter") {
+ auto& outputdexter_conf = mod_settings.sdr_device_config;
+ outputdexter_conf.txgain = pt.GetReal("dexteroutput.txgain", 0.0);
+ outputdexter_conf.lo_offset = pt.GetReal("dexteroutput.lo_offset", 0.0);
+ outputdexter_conf.frequency = pt.GetReal("dexteroutput.frequency", 0);
+ std::string chan = pt.Get("dexteroutput.channel", "");
+ outputdexter_conf.dabMode = mod_settings.dabMode;
+ outputdexter_conf.maxGPSHoldoverTime = pt.GetInteger("dexteroutput.max_gps_holdover_time", 0);
+
+ if (outputdexter_conf.frequency == 0 && chan == "") {
+ std::cerr << " dexter output enabled, but neither frequency nor channel defined.\n";
+ throw std::runtime_error("Configuration error");
+ }
+ else if (outputdexter_conf.frequency == 0) {
+ outputdexter_conf.frequency = parse_channel(chan);
+ }
+ else if (outputdexter_conf.frequency != 0 && chan != "") {
+ std::cerr << " dexter output: cannot define both frequency and channel.\n";
+ throw std::runtime_error("Configuration error");
+ }
+
+ mod_settings.useDexterOutput = true;
+ }
+#endif // defined(HAVE_DEXTER)
+
#if defined(HAVE_LIMESDR)
else if (output_selected == "limesdr") {
auto& outputlime_conf = mod_settings.sdr_device_config;
@@ -330,7 +390,7 @@ static void parse_configfile(
throw std::runtime_error("Configuration error");
}
else if (outputlime_conf.frequency == 0) {
- outputlime_conf.frequency = parseChannel(chan);
+ outputlime_conf.frequency = parse_channel(chan);
}
else if (outputlime_conf.frequency != 0 && chan != "") {
std::cerr << " Lime output: cannot define both frequency and channel.\n";
@@ -341,7 +401,7 @@ static void parse_configfile(
mod_settings.useLimeOutput = true;
}
-#endif
+#endif // defined(HAVE_LIMESDR)
#if defined(HAVE_BLADERF)
else if (output_selected == "bladerf") {
@@ -359,7 +419,7 @@ static void parse_configfile(
throw std::runtime_error("Configuration error");
}
else if (outputbladerf_conf.frequency == 0) {
- outputbladerf_conf.frequency = parseChannel(chan);
+ outputbladerf_conf.frequency = parse_channel(chan);
}
else if (outputbladerf_conf.frequency != 0 && chan != "") {
std::cerr << " BladeRF output: cannot define both frequency and channel.\n";
@@ -370,7 +430,7 @@ static void parse_configfile(
mod_settings.useBladeRFOutput = true;
}
-#endif
+#endif // defined(HAVE_BLADERF)
#if defined(HAVE_ZEROMQ)
else if (output_selected == "zmq") {
@@ -385,7 +445,7 @@ static void parse_configfile(
}
-#if defined(HAVE_OUTPUT_UHD)
+#if defined(HAVE_OUTPUT_UHD) || defined(HAVE_DEXTER)
mod_settings.sdr_device_config.enableSync = (pt.GetInteger("delaymanagement.synchronous", 0) == 1);
mod_settings.sdr_device_config.muteNoTimestamps = (pt.GetInteger("delaymanagement.mutenotimestamps", 0) == 1);
if (mod_settings.sdr_device_config.enableSync) {
@@ -406,7 +466,6 @@ static void parse_configfile(
throw std::runtime_error("Configuration error");
}
}
-
#endif
@@ -551,8 +610,7 @@ void parse_args(int argc, char **argv, mod_settings_t& mod_settings)
if (mod_settings.inputName.substr(0, 4) == "zmq+" &&
mod_settings.inputName.find("://") != std::string::npos) {
- // if the name starts with zmq+XYZ://somewhere:port
- mod_settings.inputTransport = "zeromq";
+ throw std::runtime_error("Support for ZeroMQ input transport has been removed.");
}
else if (mod_settings.inputName.substr(0, 6) == "tcp://") {
mod_settings.inputTransport = "tcp";
diff --git a/src/ConfigParser.h b/src/ConfigParser.h
index 574caa2..3bacfdd 100644
--- a/src/ConfigParser.h
+++ b/src/ConfigParser.h
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -34,15 +34,17 @@
#include <string>
#include "GainControl.h"
#include "TII.h"
-#include "output/SDR.h"
-#include "output/UHD.h"
-#include "output/Soapy.h"
-#include "output/Lime.h"
-#include "output/BladeRF.h"
+#include "output/SDRDevice.h"
-#define ZMQ_INPUT_MAX_FRAME_QUEUE 500
+enum class FFTEngine {
+ FFTW, // floating point in software
+ KISS, // fixed-point in software
+ DEXTER // fixed-point in FPGA
+};
struct mod_settings_t {
+ std::string startupCheck;
+
std::string outputName;
bool useZeroMQOutput = false;
std::string zmqOutputSocketType = "";
@@ -51,9 +53,11 @@ struct mod_settings_t {
bool fileOutputShowMetadata = false;
bool useUHDOutput = false;
bool useSoapyOutput = false;
+ bool useDexterOutput = false;
bool useLimeOutput = false;
bool useBladeRFOutput = false;
- const std::string BladeRFOutputFormat = "s16"; // to transmit SC16 IQ
+
+ FFTEngine fftEngine = FFTEngine::FFTW;
size_t outputRate = 2048000;
size_t clockRate = 0;
@@ -69,7 +73,6 @@ struct mod_settings_t {
bool loop = false;
std::string inputName = "";
std::string inputTransport = "file";
- unsigned inputMaxFramesQueued = ZMQ_INPUT_MAX_FRAME_QUEUE;
float edi_max_delay_ms = 0.0f;
tii_config_t tiiConfig;
@@ -87,9 +90,7 @@ struct mod_settings_t {
// Settings for the OFDM windowing
size_t ofdmWindowOverlap = 0;
-#if defined(HAVE_OUTPUT_UHD) || defined(HAVE_SOAPYSDR) || defined(HAVE_LIMESDR) || defined(HAVE_BLADERF)
Output::SDRDeviceConfig sdr_device_config;
-#endif
bool showProcessTime = true;
};
diff --git a/src/DabMod.cpp b/src/DabMod.cpp
index f97c05d..7866818 100644
--- a/src/DabMod.cpp
+++ b/src/DabMod.cpp
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2019
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -25,15 +25,14 @@
along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
*/
+#include <fftw3.h>
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <memory>
-#include <complex>
#include <string>
#include <iostream>
-#include <iomanip>
#include <cstdlib>
#include <stdexcept>
#include <cstdio>
@@ -46,16 +45,17 @@
# include <netinet/in.h>
#endif
+#include "Events.h"
#include "Utils.h"
#include "Log.h"
#include "DabModulator.h"
-#include "InputMemory.h"
#include "OutputFile.h"
#include "FormatConverter.h"
#include "FrameMultiplexer.h"
#include "output/SDR.h"
#include "output/UHD.h"
#include "output/Soapy.h"
+#include "output/Dexter.h"
#include "output/Lime.h"
#include "output/BladeRF.h"
#include "OutputZeroMQ.h"
@@ -72,16 +72,16 @@
* samples can have peaks up to about 48000. The value of 50000
* should guarantee that with a digital gain of 1.0, UHD never clips
* our samples.
+ *
+ * This only applies when fixed_point == false.
*/
static const float normalise_factor = 50000.0f;
-//Empirical normalisation factors used to normalise the samples to amplitude 1.
+// Empirical normalisation factors used to normalise the samples to amplitude 1.
static const float normalise_factor_file_fix = 81000.0f;
static const float normalise_factor_file_var = 46000.0f;
static const float normalise_factor_file_max = 46000.0f;
-typedef std::complex<float> complexf;
-
using namespace std;
volatile sig_atomic_t running = 1;
@@ -93,18 +93,148 @@ void signalHandler(int signalNb)
running = 0;
}
-struct modulator_data
-{
- // For ETI
- std::shared_ptr<InputReader> inputReader;
- std::shared_ptr<EtiReader> etiReader;
+class ModulatorData : public RemoteControllable {
+ public:
+ // For ETI
+ std::shared_ptr<InputReader> inputReader;
+ std::shared_ptr<EtiReader> etiReader;
+
+ // For EDI
+ std::shared_ptr<EdiInput> ediInput;
+
+ // Common to both EDI and EDI
+ uint64_t framecount = 0;
+ Flowgraph *flowgraph = nullptr;
+
+
+ // RC-related
+ ModulatorData() : RemoteControllable("mainloop") {
+ RC_ADD_PARAMETER(num_modulator_restarts, "(Read-only) Number of mod restarts");
+ RC_ADD_PARAMETER(most_recent_edi_decoded, "(Read-only) UNIX Timestamp of most recently decoded EDI frame");
+ RC_ADD_PARAMETER(edi_source, "(Read-only) URL of the EDI/TCP source");
+ RC_ADD_PARAMETER(running_since, "(Read-only) UNIX Timestamp of most recent modulator restart");
+ RC_ADD_PARAMETER(ensemble_label, "(Read-only) Label of the ensemble");
+ RC_ADD_PARAMETER(ensemble_eid, "(Read-only) Ensemble ID");
+ RC_ADD_PARAMETER(ensemble_services, "(Read-only, only JSON) Ensemble service information");
+ RC_ADD_PARAMETER(num_services, "(Read-only) Number of services in the ensemble");
+ }
+
+ virtual ~ModulatorData() {}
+
+ virtual void set_parameter(const std::string& parameter, const std::string& value) {
+ throw ParameterError("Parameter " + parameter + " is read-only");
+ }
+
+ virtual const std::string get_parameter(const std::string& parameter) const {
+ stringstream ss;
+ if (parameter == "num_modulator_restarts") {
+ ss << num_modulator_restarts;
+ }
+ else if (parameter == "running_since") {
+ ss << running_since;
+ }
+ else if (parameter == "most_recent_edi_decoded") {
+ ss << most_recent_edi_decoded;
+ }
+ else if (parameter == "ensemble_label") {
+ if (ediInput) {
+ const auto ens = ediInput->ediReader.getEnsembleInfo();
+ if (ens) {
+ ss << FICDecoder::ConvertLabelToUTF8(ens->label, nullptr);
+ }
+ else {
+ throw ParameterError("Not available yet");
+ }
+ }
+ else {
+ throw ParameterError("Not available yet");
+ }
+ }
+ else if (parameter == "ensemble_eid") {
+ if (ediInput) {
+ const auto ens = ediInput->ediReader.getEnsembleInfo();
+ if (ens) {
+ ss << ens->eid;
+ }
+ else {
+ throw ParameterError("Not available yet");
+ }
+ }
+ else {
+ throw ParameterError("Not available yet");
+ }
+ }
+ else if (parameter == "edi_source") {
+ if (ediInput) {
+ ss << ediInput->ediTransport.getTcpUri();
+ }
+ else {
+ throw ParameterError("Not available yet");
+ }
+ }
+ else if (parameter == "num_services") {
+ if (ediInput) {
+ ss << ediInput->ediReader.getSubchannels().size();
+ }
+ else {
+ throw ParameterError("Not available yet");
+ }
+ }
+ else if (parameter == "ensemble_services") {
+ throw ParameterError("ensemble_services is only available through 'showjson'");
+ }
+ else {
+ ss << "Parameter '" << parameter <<
+ "' is not exported by controllable " << get_rc_name();
+ throw ParameterError(ss.str());
+ }
+ return ss.str();
+ }
- // For EDI
- std::shared_ptr<EdiInput> ediInput;
+ virtual const json::map_t get_all_values() const
+ {
+ json::map_t map;
+ map["num_modulator_restarts"].v = num_modulator_restarts;
+ map["running_since"].v = running_since;
+ map["most_recent_edi_decoded"].v = most_recent_edi_decoded;
+
+ if (ediInput) {
+ map["edi_source"].v = ediInput->ediTransport.getTcpUri();
+ map["num_services"].v = ediInput->ediReader.getSubchannels().size();
+
+ const auto ens = ediInput->ediReader.getEnsembleInfo();
+ if (ens) {
+ map["ensemble_label"].v = FICDecoder::ConvertLabelToUTF8(ens->label, nullptr);
+ map["ensemble_eid"].v = ens->eid;
+ }
+ else {
+ map["ensemble_label"].v = nullopt;
+ map["ensemble_eid"].v = nullopt;
+ }
+
+ std::vector<json::value_t> services;
+
+ for (const auto& s : ediInput->ediReader.getServiceInfo()) {
+ auto service_map = make_shared<json::map_t>();
+ (*service_map)["sad"].v = s.second.subchannel.start;
+ (*service_map)["sid"].v = s.second.sid;
+ (*service_map)["label"].v = FICDecoder::ConvertLabelToUTF8(s.second.label, nullptr);
+ (*service_map)["bitrate"].v = s.second.subchannel.bitrate;
+ (*service_map)["protection_level"].v = s.second.subchannel.pl;
+ json::value_t v;
+ v.v = service_map;
+ services.push_back(v);
+ }
+
+ map["ensemble_services"].v = services;
+
+ }
+ return map;
+ }
- // Common to both EDI and EDI
- uint64_t framecount = 0;
- Flowgraph *flowgraph = nullptr;
+ size_t num_modulator_restarts = 0;
+ time_t most_recent_edi_decoded = 0;
+ time_t running_since = 0;
};
enum class run_modulator_state_t {
@@ -114,91 +244,19 @@ enum class run_modulator_state_t {
reconfigure // Some sort of change of configuration we cannot handle happened
};
-static run_modulator_state_t run_modulator(modulator_data& m);
-
-static void printModSettings(const mod_settings_t& mod_settings)
-{
- stringstream ss;
- // Print settings
- ss << "Input\n";
- ss << " Type: " << mod_settings.inputTransport << "\n";
- ss << " Source: " << mod_settings.inputName << "\n";
-
- ss << "Output\n";
-
- if (mod_settings.useFileOutput) {
- ss << " Name: " << mod_settings.outputName << "\n";
- }
-#if defined(HAVE_OUTPUT_UHD)
- else if (mod_settings.useUHDOutput) {
- ss << " UHD\n" <<
- " Device: " << mod_settings.sdr_device_config.device << "\n" <<
- " Subdevice: " <<
- mod_settings.sdr_device_config.subDevice << "\n" <<
- " master_clock_rate: " <<
- mod_settings.sdr_device_config.masterClockRate << "\n" <<
- " refclk: " <<
- mod_settings.sdr_device_config.refclk_src << "\n" <<
- " pps source: " <<
- mod_settings.sdr_device_config.pps_src << "\n";
- }
-#endif
-#if defined(HAVE_SOAPYSDR)
- else if (mod_settings.useSoapyOutput) {
- ss << " SoapySDR\n"
- " Device: " << mod_settings.sdr_device_config.device << "\n" <<
- " master_clock_rate: " <<
- mod_settings.sdr_device_config.masterClockRate << "\n";
- }
-#endif
-#if defined(HAVE_LIMESDR)
- else if (mod_settings.useLimeOutput) {
- ss << " LimeSDR\n"
- " Device: " << mod_settings.sdr_device_config.device << "\n" <<
- " master_clock_rate: " <<
- mod_settings.sdr_device_config.masterClockRate << "\n";
- }
-#endif
-#if defined(HAVE_BLADERF)
- else if (mod_settings.useBladeRFOutput) {
- ss << " BladeRF\n"
- " Device: " << mod_settings.sdr_device_config.device << "\n" <<
- " refclk: " << mod_settings.sdr_device_config.refclk_src << "\n";
- }
-#endif
- else if (mod_settings.useZeroMQOutput) {
- ss << " ZeroMQ\n" <<
- " Listening on: " << mod_settings.outputName << "\n" <<
- " Socket type : " << mod_settings.zmqOutputSocketType << "\n";
- }
+static run_modulator_state_t run_modulator(const mod_settings_t& mod_settings, ModulatorData& m);
- ss << " Sampling rate: ";
- if (mod_settings.outputRate > 1000) {
- if (mod_settings.outputRate > 1000000) {
- ss << std::fixed << std::setprecision(4) <<
- mod_settings.outputRate / 1000000.0 <<
- " MHz\n";
- }
- else {
- ss << std::fixed << std::setprecision(4) <<
- mod_settings.outputRate / 1000.0 <<
- " kHz\n";
- }
- }
- else {
- ss << std::fixed << std::setprecision(4) <<
- mod_settings.outputRate << " Hz\n";
- }
- fprintf(stderr, "%s", ss.str().c_str());
-}
-static shared_ptr<ModOutput> prepare_output(
- mod_settings_t& s)
+static shared_ptr<ModOutput> prepare_output(mod_settings_t& s)
{
shared_ptr<ModOutput> output;
if (s.useFileOutput) {
- if (s.fileOutputFormat == "complexf") {
+ if (s.fftEngine != FFTEngine::FFTW) {
+ // Intentionally ignore fileOutputFormat, it is always sc16
+ output = make_shared<OutputFile>(s.outputName, s.fileOutputShowMetadata);
+ }
+ else if (s.fileOutputFormat == "complexf") {
output = make_shared<OutputFile>(s.outputName, s.fileOutputShowMetadata);
}
else if (s.fileOutputFormat == "complexf_normalised") {
@@ -234,6 +292,7 @@ static shared_ptr<ModOutput> prepare_output(
else if (s.useUHDOutput) {
s.normalise = 1.0f / normalise_factor;
s.sdr_device_config.sampleRate = s.outputRate;
+ s.sdr_device_config.fixedPoint = (s.fftEngine != FFTEngine::FFTW);
auto uhddevice = make_shared<Output::UHD>(s.sdr_device_config);
output = make_shared<Output::SDR>(s.sdr_device_config, uhddevice);
rcs.enrol((Output::SDR*)output.get());
@@ -244,15 +303,27 @@ static shared_ptr<ModOutput> prepare_output(
/* We normalise the same way as for the UHD output */
s.normalise = 1.0f / normalise_factor;
s.sdr_device_config.sampleRate = s.outputRate;
+ if (s.fftEngine != FFTEngine::FFTW) throw runtime_error("soapy fixed_point unsupported");
auto soapydevice = make_shared<Output::Soapy>(s.sdr_device_config);
output = make_shared<Output::SDR>(s.sdr_device_config, soapydevice);
rcs.enrol((Output::SDR*)output.get());
}
#endif
+#if defined(HAVE_DEXTER)
+ else if (s.useDexterOutput) {
+ /* We normalise specifically range [-32768; 32767] */
+ s.normalise = 32767.0f / normalise_factor;
+ s.sdr_device_config.sampleRate = s.outputRate;
+ auto dexterdevice = make_shared<Output::Dexter>(s.sdr_device_config);
+ output = make_shared<Output::SDR>(s.sdr_device_config, dexterdevice);
+ rcs.enrol((Output::SDR*)output.get());
+ }
+#endif
#if defined(HAVE_LIMESDR)
else if (s.useLimeOutput) {
/* We normalise the same way as for the UHD output */
s.normalise = 1.0f / normalise_factor;
+ if (s.fftEngine != FFTEngine::FFTW) throw runtime_error("limesdr fixed_point unsupported");
s.sdr_device_config.sampleRate = s.outputRate;
auto limedevice = make_shared<Output::Lime>(s.sdr_device_config);
output = make_shared<Output::SDR>(s.sdr_device_config, limedevice);
@@ -263,6 +334,7 @@ static shared_ptr<ModOutput> prepare_output(
else if (s.useBladeRFOutput) {
/* We normalise specifically for the BladeRF output : range [-2048; 2047] */
s.normalise = 2047.0f / normalise_factor;
+ if (s.fftEngine != FFTEngine::FFTW) throw runtime_error("bladerf fixed_point unsupported");
s.sdr_device_config.sampleRate = s.outputRate;
auto bladerfdevice = make_shared<Output::BladeRF>(s.sdr_device_config);
output = make_shared<Output::SDR>(s.sdr_device_config, bladerfdevice);
@@ -308,6 +380,10 @@ int launch_modulator(int argc, char* argv[])
mod_settings_t mod_settings;
parse_args(argc, argv, mod_settings);
+#if defined(HAVE_ZEROMQ)
+ etiLog.register_backend(make_shared<LogToEventSender>());
+#endif // defined(HAVE_ZEROMQ)
+
etiLog.level(info) << "Configuration parsed. Starting up version " <<
#if defined(GITVERSION)
GITVERSION;
@@ -319,26 +395,84 @@ int launch_modulator(int argc, char* argv[])
mod_settings.useUHDOutput or
mod_settings.useZeroMQOutput or
mod_settings.useSoapyOutput or
+ mod_settings.useDexterOutput or
mod_settings.useLimeOutput or
mod_settings.useBladeRFOutput)) {
throw std::runtime_error("Configuration error: Output not specified");
}
+ if (not mod_settings.startupCheck.empty()) {
+ etiLog.level(info) << "Running startup check '" << mod_settings.startupCheck << "'";
+ int wstatus = system(mod_settings.startupCheck.c_str());
+
+ if (WIFEXITED(wstatus)) {
+ if (WEXITSTATUS(wstatus) == 0) {
+ etiLog.level(info) << "Startup check ok";
+ }
+ else {
+ etiLog.level(error) << "Startup check failed, returned " << WEXITSTATUS(wstatus);
+ return 1;
+ }
+ }
+ else {
+ etiLog.level(error) << "Startup check failed, child didn't terminate normally";
+ return 1;
+ }
+ }
+
printModSettings(mod_settings);
- shared_ptr<FormatConverter> format_converter;
- if (mod_settings.useFileOutput and
+ ModulatorData m;
+ rcs.enrol(&m);
+
+ // Neither KISS FFT used for fixedpoint nor the FFT Accelerator used for DEXTER need planning.
+ if (mod_settings.fftEngine == FFTEngine::FFTW) {
+ // This is mostly useful on ARM systems where FFTW planning takes some time. If we do it here
+ // it will be done before the modulator starts up
+ etiLog.level(debug) << "Running FFTW planning...";
+ constexpr size_t fft_size = 2048; // Transmission Mode I. If different, it'll recalculate on OfdmGenerator
+ // initialisation
+ auto *fft_in = (fftwf_complex*)fftwf_malloc(sizeof(fftwf_complex) * fft_size);
+ auto *fft_out = (fftwf_complex*)fftwf_malloc(sizeof(fftwf_complex) * fft_size);
+ if (fft_in == nullptr or fft_out == nullptr) {
+ throw std::runtime_error("FFTW malloc failed");
+ }
+ fftwf_set_timelimit(2);
+ fftwf_plan plan = fftwf_plan_dft_1d(fft_size, fft_in, fft_out, FFTW_FORWARD, FFTW_MEASURE);
+ fftwf_destroy_plan(plan);
+ plan = fftwf_plan_dft_1d(fft_size, fft_in, fft_out, FFTW_BACKWARD, FFTW_MEASURE);
+ fftwf_destroy_plan(plan);
+ fftwf_free(fft_in);
+ fftwf_free(fft_out);
+ etiLog.level(debug) << "FFTW planning done.";
+ }
+
+ std::string output_format;
+ if (mod_settings.fftEngine == FFTEngine::KISS) {
+ output_format = ""; //fixed point is native sc16, no converter needed
+ }
+ else if (mod_settings.fftEngine == FFTEngine::DEXTER) {
+ output_format = "s16"; // FPGA FFT Engine outputs s32
+ }
+ // else FFTW, i.e. floating point
+ else if (mod_settings.useFileOutput and
(mod_settings.fileOutputFormat == "s8" or
mod_settings.fileOutputFormat == "u8" or
mod_settings.fileOutputFormat == "s16")) {
- format_converter = make_shared<FormatConverter>(mod_settings.fileOutputFormat);
+ output_format = mod_settings.fileOutputFormat;
+ }
+ else if (mod_settings.useBladeRFOutput or mod_settings.useDexterOutput) {
+ output_format = "s16";
}
- else if (mod_settings.useBladeRFOutput) {
- format_converter = make_shared<FormatConverter>(mod_settings.BladeRFOutputFormat);
- }
auto output = prepare_output(mod_settings);
+ if (not output_format.empty()) {
+ if (auto o = dynamic_pointer_cast<Output::SDR>(output)) {
+ o->set_sample_size(FormatConverter::get_format_size(output_format));
+ }
+ }
+
// Set thread priority to realtime
if (int r = set_realtime_prio(1)) {
etiLog.level(error) << "Could not set priority for modulator:" << r;
@@ -365,17 +499,6 @@ int launch_modulator(int argc, char* argv[])
inputReader = inputFileReader;
}
- else if (mod_settings.inputTransport == "zeromq") {
-#if !defined(HAVE_ZEROMQ)
- throw std::runtime_error("Unable to open input: "
- "ZeroMQ input transport selected, but not compiled in!");
-#else
- auto inputZeroMQReader = make_shared<InputZeroMQReader>();
- inputZeroMQReader->Open(mod_settings.inputName, mod_settings.inputMaxFramesQueued);
- rcs.enrol(inputZeroMQReader.get());
- inputReader = inputZeroMQReader;
-#endif
- }
else if (mod_settings.inputTransport == "tcp") {
auto inputTcpReader = make_shared<InputTcpReader>();
inputTcpReader->Open(mod_settings.inputName);
@@ -386,40 +509,37 @@ int launch_modulator(int argc, char* argv[])
"invalid input transport " + mod_settings.inputTransport + " selected!");
}
+ m.ediInput = ediInput;
+ m.inputReader = inputReader;
+
bool run_again = true;
while (run_again) {
+ m.running_since = get_clock_realtime_seconds();
+
Flowgraph flowgraph(mod_settings.showProcessTime);
- modulator_data m;
- m.ediInput = ediInput;
- m.inputReader = inputReader;
+ m.framecount = 0;
m.flowgraph = &flowgraph;
shared_ptr<DabModulator> modulator;
if (inputReader) {
m.etiReader = make_shared<EtiReader>(mod_settings.tist_offset_s);
- modulator = make_shared<DabModulator>(*m.etiReader, mod_settings);
+ modulator = make_shared<DabModulator>(*m.etiReader, mod_settings, output_format);
}
else if (ediInput) {
- modulator = make_shared<DabModulator>(ediInput->ediReader, mod_settings);
+ modulator = make_shared<DabModulator>(ediInput->ediReader, mod_settings, output_format);
}
rcs.enrol(modulator.get());
- if (format_converter) {
- flowgraph.connect(modulator, format_converter);
- flowgraph.connect(format_converter, output);
- }
- else {
- flowgraph.connect(modulator, output);
- }
+ flowgraph.connect(modulator, output);
if (inputReader) {
etiLog.level(info) << inputReader->GetPrintableInfo();
}
- run_modulator_state_t st = run_modulator(m);
+ run_modulator_state_t st = run_modulator(mod_settings, m);
etiLog.log(trace, "DABMOD,run_modulator() = %d", st);
switch (st) {
@@ -440,17 +560,6 @@ int launch_modulator(int argc, char* argv[])
run_again = true;
}
}
-#if defined(HAVE_ZEROMQ)
- else if (auto in_zmq = dynamic_pointer_cast<InputZeroMQReader>(inputReader)) {
- run_again = true;
- // Create a new input reader
- rcs.remove_controllable(in_zmq.get());
- auto inputZeroMQReader = make_shared<InputZeroMQReader>();
- inputZeroMQReader->Open(mod_settings.inputName, mod_settings.inputMaxFramesQueued);
- rcs.enrol(inputZeroMQReader.get());
- inputReader = inputZeroMQReader;
- }
-#endif
else if (dynamic_pointer_cast<InputTcpReader>(inputReader)) {
// Keep the same inputReader, as there is no input buffer overflow
run_again = true;
@@ -473,28 +582,21 @@ int launch_modulator(int argc, char* argv[])
break;
}
- etiLog.level(info) << m.framecount << " DAB frames encoded";
- etiLog.level(info) << ((float)m.framecount * 0.024f) << " seconds encoded";
+ etiLog.level(info) << m.framecount << " DAB frames, " << ((float)m.framecount * 0.024f) << " seconds encoded";
+ m.num_modulator_restarts++;
}
etiLog.level(info) << "Terminating";
return ret;
}
-struct zmq_input_timeout : public std::exception
-{
- const char* what() const throw()
- {
- return "InputZMQ timeout";
- }
-};
-
-static run_modulator_state_t run_modulator(modulator_data& m)
+static run_modulator_state_t run_modulator(const mod_settings_t& mod_settings, ModulatorData& m)
{
auto ret = run_modulator_state_t::failure;
try {
int last_eti_fct = -1;
auto last_frame_received = chrono::steady_clock::now();
+ frame_timestamp ts;
Buffer data;
if (m.inputReader) {
data.setLength(6144);
@@ -515,36 +617,9 @@ static run_modulator_state_t run_modulator(modulator_data& m)
ret = run_modulator_state_t::normal_end;
break;
}
-#if defined(HAVE_ZEROMQ)
- else if (dynamic_pointer_cast<InputZeroMQReader>(m.inputReader)) {
- /* An empty frame marks a timeout. We ignore it, but we are
- * now able to handle SIGINT properly.
- *
- * Also, we reconnect zmq every 10 seconds to avoid some
- * issues, discussed in
- * https://stackoverflow.com/questions/26112992/zeromq-pub-sub-on-unreliable-connection
- *
- * > It is possible that the PUB socket sees the error
- * > while the SUB socket does not.
- * >
- * > The ZMTP RFC has a proposal for heartbeating that would
- * > solve this problem. The current best solution is for
- * > PUB sockets to send heartbeats (e.g. 1 per second) when
- * > traffic is low, and for SUB sockets to disconnect /
- * > reconnect if they stop getting these.
- *
- * We don't need a heartbeat, because our application is constant frame rate,
- * the frames themselves can act as heartbeats.
- */
-
- const auto now = chrono::steady_clock::now();
- if (last_frame_received + chrono::seconds(10) < now) {
- throw zmq_input_timeout();
- }
- }
-#endif // defined(HAVE_ZEROMQ)
else if (dynamic_pointer_cast<InputTcpReader>(m.inputReader)) {
- /* Same as for ZeroMQ */
+ /* An empty frame marks a timeout. We ignore it, but we are
+ * now able to handle SIGINT properly. */
}
else {
throw logic_error("Unhandled framesize==0!");
@@ -568,6 +643,7 @@ static run_modulator_state_t run_modulator(modulator_data& m)
fct = m.etiReader->getFct();
fp = m.etiReader->getFp();
+ ts = m.etiReader->getTimestamp();
}
else if (m.ediInput) {
while (running and not m.ediInput->ediReader.isFrameReady()) {
@@ -582,51 +658,59 @@ static run_modulator_state_t run_modulator(modulator_data& m)
running = 0;
break;
}
-
- if (last_frame_received + chrono::seconds(10) < chrono::steady_clock::now()) {
- etiLog.level(error) << "No EDI data received in 10 seconds.";
- running = 0;
- break;
- }
}
if (!running) {
break;
}
+ m.most_recent_edi_decoded = get_clock_realtime_seconds();
fct = m.ediInput->ediReader.getFct();
fp = m.ediInput->ediReader.getFp();
+ ts = m.ediInput->ediReader.getTimestamp();
}
- const unsigned expected_fct = (last_eti_fct + 1) % 250;
- if (last_eti_fct == -1) {
- if (fp != 0) {
- // Do not start the flowgraph before we get to FP 0
- // to ensure all blocks are properly aligned.
- if (m.ediInput) {
- m.ediInput->ediReader.clearFrame();
+ // timestamp is good if we run unsynchronised, or if margin is sufficient
+ bool ts_good = not mod_settings.sdr_device_config.enableSync or
+ (ts.timestamp_valid and ts.offset_to_system_time() > 0.2);
+
+ if (!ts_good) {
+ etiLog.level(warn) << "Modulator skipping frame " << fct <<
+ " TS " << (ts.timestamp_valid ? "valid" : "invalid") <<
+ " offset " << (ts.timestamp_valid ? ts.offset_to_system_time() : 0);
+ }
+ else {
+ bool modulate = true;
+ if (last_eti_fct == -1) {
+ if (fp != 0) {
+ // Do not start the flowgraph before we get to FP 0
+ // to ensure all blocks are properly aligned.
+ modulate = false;
+ }
+ else {
+ last_eti_fct = fct;
}
- continue;
}
else {
- last_eti_fct = fct;
+ const unsigned expected_fct = (last_eti_fct + 1) % 250;
+ if (fct == expected_fct) {
+ last_eti_fct = fct;
+ }
+ else {
+ etiLog.level(warn) << "ETI FCT discontinuity, expected " <<
+ expected_fct << " received " << fct;
+ if (m.ediInput) {
+ m.ediInput->ediReader.clearFrame();
+ }
+ return run_modulator_state_t::again;
+ }
+ }
+
+ if (modulate) {
m.framecount++;
m.flowgraph->run();
}
}
- else if (fct == expected_fct) {
- last_eti_fct = fct;
- m.framecount++;
- m.flowgraph->run();
- }
- else {
- etiLog.level(info) << "ETI FCT discontinuity, expected " <<
- expected_fct << " received " << fct;
- if (m.ediInput) {
- m.ediInput->ediReader.clearFrame();
- }
- return run_modulator_state_t::again;
- }
if (m.ediInput) {
m.ediInput->ediReader.clearFrame();
@@ -639,16 +723,6 @@ static run_modulator_state_t run_modulator(modulator_data& m)
}
}
}
- catch (const zmq_input_timeout&) {
- // The ZeroMQ input timeout
- etiLog.level(warn) << "Timeout";
- ret = run_modulator_state_t::again;
- }
- catch (const zmq_input_overflow& e) {
- // The ZeroMQ input has overflowed its buffer
- etiLog.level(warn) << e.what();
- ret = run_modulator_state_t::again;
- }
catch (const FrameMultiplexerError& e) {
// The FrameMultiplexer saw an error or a change in the size of a
// subchannel. This can be due to a multiplex reconfiguration.
diff --git a/src/DabModulator.cpp b/src/DabModulator.cpp
index aa4f2a8..5f7aaf6 100644
--- a/src/DabModulator.cpp
+++ b/src/DabModulator.cpp
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2019
+ Copyright (C) 2024
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -27,53 +27,53 @@
#include <string>
#include <memory>
+#include <vector>
#include "DabModulator.h"
#include "PcDebug.h"
-#if !defined(BUILD_FOR_EASYDABV3)
-# include "QpskSymbolMapper.h"
-# include "FrequencyInterleaver.h"
-# include "PhaseReference.h"
-# include "DifferentialModulator.h"
-# include "NullSymbol.h"
-# include "CicEqualizer.h"
-# include "OfdmGenerator.h"
-# include "GainControl.h"
-# include "GuardIntervalInserter.h"
-# include "Resampler.h"
-# include "FIRFilter.h"
-# include "MemlessPoly.h"
-# include "TII.h"
-#endif
-
-#include "FrameMultiplexer.h"
-#include "PrbsGenerator.h"
#include "BlockPartitioner.h"
-#include "SignalMultiplexer.h"
+#include "CicEqualizer.h"
#include "ConvEncoder.h"
+#include "DifferentialModulator.h"
+#include "FIRFilter.h"
+#include "FrameMultiplexer.h"
+#include "FrequencyInterleaver.h"
+#include "GainControl.h"
+#include "GuardIntervalInserter.h"
+#include "Log.h"
+#include "MemlessPoly.h"
+#include "NullSymbol.h"
+#include "OfdmGenerator.h"
+#include "PhaseReference.h"
+#include "PrbsGenerator.h"
#include "PuncturingEncoder.h"
-#include "TimeInterleaver.h"
-#include "TimestampDecoder.h"
+#include "QpskSymbolMapper.h"
#include "RemoteControl.h"
-#include "Log.h"
+#include "Resampler.h"
+#include "SignalMultiplexer.h"
+#include "TII.h"
+#include "TimeInterleaver.h"
using namespace std;
DabModulator::DabModulator(EtiSource& etiSource,
- mod_settings_t& settings) :
+ mod_settings_t& settings,
+ const std::string& format) :
ModInput(),
RemoteControllable("modulator"),
m_settings(settings),
- myEtiSource(etiSource),
- myFlowgraph()
+ m_format(format),
+ m_etiSource(etiSource),
+ m_flowgraph()
{
PDEBUG("DabModulator::DabModulator() @ %p\n", this);
RC_ADD_PARAMETER(rate, "(Read-only) IQ output samplerate");
+ RC_ADD_PARAMETER(num_clipped_samples, "(Read-only) Number of samples clipped in last frame during format conversion");
if (m_settings.dabMode == 0) {
- setMode(2);
+ setMode(1);
}
else {
setMode(m_settings.dabMode);
@@ -85,36 +85,36 @@ void DabModulator::setMode(unsigned mode)
{
switch (mode) {
case 1:
- myNbSymbols = 76;
- myNbCarriers = 1536;
- mySpacing = 2048;
- myNullSize = 2656;
- mySymSize = 2552;
- myFicSizeOut = 288;
+ m_nbSymbols = 76;
+ m_nbCarriers = 1536;
+ m_spacing = 2048;
+ m_nullSize = 2656;
+ m_symSize = 2552;
+ m_ficSizeOut = 288;
break;
case 2:
- myNbSymbols = 76;
- myNbCarriers = 384;
- mySpacing = 512;
- myNullSize = 664;
- mySymSize = 638;
- myFicSizeOut = 288;
+ m_nbSymbols = 76;
+ m_nbCarriers = 384;
+ m_spacing = 512;
+ m_nullSize = 664;
+ m_symSize = 638;
+ m_ficSizeOut = 288;
break;
case 3:
- myNbSymbols = 153;
- myNbCarriers = 192;
- mySpacing = 256;
- myNullSize = 345;
- mySymSize = 319;
- myFicSizeOut = 384;
+ m_nbSymbols = 153;
+ m_nbCarriers = 192;
+ m_spacing = 256;
+ m_nullSize = 345;
+ m_symSize = 319;
+ m_ficSizeOut = 384;
break;
case 4:
- myNbSymbols = 76;
- myNbCarriers = 768;
- mySpacing = 1024;
- myNullSize = 1328;
- mySymSize = 1276;
- myFicSizeOut = 288;
+ m_nbSymbols = 76;
+ m_nbCarriers = 768;
+ m_spacing = 1024;
+ m_nullSize = 1328;
+ m_symSize = 1276;
+ m_ficSizeOut = 288;
break;
default:
throw std::runtime_error("DabModulator::setMode invalid mode size");
@@ -128,27 +128,28 @@ int DabModulator::process(Buffer* dataOut)
PDEBUG("DabModulator::process(dataOut: %p)\n", dataOut);
- if (not myFlowgraph) {
+ if (not m_flowgraph) {
+ etiLog.level(debug) << "Setting up DabModulator...";
const unsigned mode = m_settings.dabMode;
setMode(mode);
- myFlowgraph = make_shared<Flowgraph>(m_settings.showProcessTime);
+ m_flowgraph = make_shared<Flowgraph>(m_settings.showProcessTime);
////////////////////////////////////////////////////////////////
// CIF data initialisation
////////////////////////////////////////////////////////////////
auto cifPrbs = make_shared<PrbsGenerator>(864 * 8, 0x110);
- auto cifMux = make_shared<FrameMultiplexer>(myEtiSource);
+ auto cifMux = make_shared<FrameMultiplexer>(m_etiSource);
auto cifPart = make_shared<BlockPartitioner>(mode);
-#if !defined(BUILD_FOR_EASYDABV3)
- auto cifMap = make_shared<QpskSymbolMapper>(myNbCarriers);
- auto cifRef = make_shared<PhaseReference>(mode);
- auto cifFreq = make_shared<FrequencyInterleaver>(mode);
- auto cifDiff = make_shared<DifferentialModulator>(myNbCarriers);
+ const bool fixedPoint = m_settings.fftEngine != FFTEngine::FFTW;
+ auto cifMap = make_shared<QpskSymbolMapper>(m_nbCarriers, fixedPoint);
+ auto cifRef = make_shared<PhaseReference>(mode, fixedPoint);
+ auto cifFreq = make_shared<FrequencyInterleaver>(mode, fixedPoint);
+ auto cifDiff = make_shared<DifferentialModulator>(m_nbCarriers, fixedPoint);
- auto cifNull = make_shared<NullSymbol>(myNbCarriers);
- auto cifSig = make_shared<SignalMultiplexer>(
- (1 + myNbSymbols) * myNbCarriers * sizeof(complexf));
+ auto cifNull = make_shared<NullSymbol>(m_nbCarriers,
+ fixedPoint ? sizeof(complexfix) : sizeof(complexf));
+ auto cifSig = make_shared<SignalMultiplexer>();
// TODO this needs a review
bool useCicEq = false;
@@ -169,8 +170,8 @@ int DabModulator::process(Buffer* dataOut)
shared_ptr<CicEqualizer> cifCicEq;
if (useCicEq) {
cifCicEq = make_shared<CicEqualizer>(
- myNbCarriers,
- (float)mySpacing * (float)m_settings.outputRate / 2048000.0f,
+ m_nbCarriers,
+ (float)m_spacing * (float)m_settings.outputRate / 2048000.0f,
cic_ratio);
}
@@ -179,46 +180,79 @@ int DabModulator::process(Buffer* dataOut)
try {
tii = make_shared<TII>(
m_settings.dabMode,
- m_settings.tiiConfig);
+ m_settings.tiiConfig,
+ fixedPoint);
rcs.enrol(tii.get());
- tiiRef = make_shared<PhaseReference>(mode);
+ tiiRef = make_shared<PhaseReference>(mode, fixedPoint);
}
catch (const TIIError& e) {
etiLog.level(error) << "Could not initialise TII: " << e.what();
}
- auto cifOfdm = make_shared<OfdmGenerator>(
- (1 + myNbSymbols),
- myNbCarriers,
- mySpacing,
- m_settings.enableCfr,
- m_settings.cfrClip,
- m_settings.cfrErrorClip);
+ shared_ptr<ModPlugin> cifOfdm;
+
+ switch (m_settings.fftEngine) {
+ case FFTEngine::FFTW:
+ {
+ auto ofdm = make_shared<OfdmGeneratorCF32>(
+ (1 + m_nbSymbols),
+ m_nbCarriers,
+ m_spacing,
+ m_settings.enableCfr,
+ m_settings.cfrClip,
+ m_settings.cfrErrorClip);
+ rcs.enrol(ofdm.get());
+ cifOfdm = ofdm;
+ }
+ break;
+ case FFTEngine::KISS:
+ cifOfdm = make_shared<OfdmGeneratorFixed>(
+ (1 + m_nbSymbols),
+ m_nbCarriers,
+ m_spacing);
+ break;
+ case FFTEngine::DEXTER:
+#if defined(HAVE_DEXTER)
+ cifOfdm = make_shared<OfdmGeneratorDEXTER>(
+ (1 + m_nbSymbols),
+ m_nbCarriers,
+ m_spacing);
+#else
+ throw std::runtime_error("Cannot use DEXTER fft engine without --enable-dexter");
+#endif
+ break;
+ }
- rcs.enrol(cifOfdm.get());
+ shared_ptr<GainControl> cifGain;
- auto cifGain = make_shared<GainControl>(
- mySpacing,
- m_settings.gainMode,
- m_settings.digitalgain,
- m_settings.normalise,
- m_settings.gainmodeVariance);
+ if (not fixedPoint) {
+ cifGain = make_shared<GainControl>(
+ m_spacing,
+ m_settings.gainMode,
+ m_settings.digitalgain,
+ m_settings.normalise,
+ m_settings.gainmodeVariance);
- rcs.enrol(cifGain.get());
+ rcs.enrol(cifGain.get());
+ }
auto cifGuard = make_shared<GuardIntervalInserter>(
- myNbSymbols, mySpacing, myNullSize, mySymSize,
- m_settings.ofdmWindowOverlap);
+ m_nbSymbols, m_spacing, m_nullSize, m_symSize,
+ m_settings.ofdmWindowOverlap, m_settings.fftEngine);
rcs.enrol(cifGuard.get());
shared_ptr<FIRFilter> cifFilter;
if (not m_settings.filterTapsFilename.empty()) {
+ if (fixedPoint) throw std::runtime_error("fixed point doesn't support fir filter");
+
cifFilter = make_shared<FIRFilter>(m_settings.filterTapsFilename);
rcs.enrol(cifFilter.get());
}
shared_ptr<MemlessPoly> cifPoly;
if (not m_settings.polyCoefFilename.empty()) {
+ if (fixedPoint) throw std::runtime_error("fixed point doesn't support predistortion");
+
cifPoly = make_shared<MemlessPoly>(m_settings.polyCoefFilename,
m_settings.polyNumThreads);
rcs.enrol(cifPoly.get());
@@ -226,21 +260,30 @@ int DabModulator::process(Buffer* dataOut)
shared_ptr<Resampler> cifRes;
if (m_settings.outputRate != 2048000) {
+ if (fixedPoint) throw std::runtime_error("fixed point doesn't support resampler");
+
cifRes = make_shared<Resampler>(
2048000,
m_settings.outputRate,
- mySpacing);
+ m_spacing);
}
-#endif
- myOutput = make_shared<OutputMemory>(dataOut);
+ if (m_settings.fftEngine == FFTEngine::FFTW and not m_format.empty()) {
+ m_formatConverter = make_shared<FormatConverter>(false, m_format);
+ }
+ else if (m_settings.fftEngine == FFTEngine::DEXTER) {
+ m_formatConverter = make_shared<FormatConverter>(true, m_format);
+ }
+ // KISS is already in s16
- myFlowgraph->connect(cifPrbs, cifMux);
+ m_output = make_shared<OutputMemory>(dataOut);
+
+ m_flowgraph->connect(cifPrbs, cifMux);
////////////////////////////////////////////////////////////////
// Processing FIC
////////////////////////////////////////////////////////////////
- shared_ptr<FicSource> fic(myEtiSource.getFic());
+ shared_ptr<FicSource> fic(m_etiSource.getFic());
////////////////////////////////////////////////////////////////
// Data initialisation
////////////////////////////////////////////////////////////////
@@ -272,15 +315,15 @@ int DabModulator::process(Buffer* dataOut)
PDEBUG(" Adding tail\n");
ficPunc->append_tail_rule(PuncturingRule(3, 0xcccccc));
- myFlowgraph->connect(fic, ficPrbs);
- myFlowgraph->connect(ficPrbs, ficConv);
- myFlowgraph->connect(ficConv, ficPunc);
- myFlowgraph->connect(ficPunc, cifPart);
+ m_flowgraph->connect(fic, ficPrbs);
+ m_flowgraph->connect(ficPrbs, ficConv);
+ m_flowgraph->connect(ficConv, ficPunc);
+ m_flowgraph->connect(ficPunc, cifPart);
////////////////////////////////////////////////////////////////
// Configuring subchannels
////////////////////////////////////////////////////////////////
- for (const auto& subchannel : myEtiSource.getSubchannels()) {
+ for (const auto& subchannel : m_etiSource.getSubchannels()) {
////////////////////////////////////////////////////////////
// Data initialisation
@@ -332,59 +375,59 @@ int DabModulator::process(Buffer* dataOut)
// Configuring time interleaver
auto subchInterleaver = make_shared<TimeInterleaver>(subchSizeOut);
- myFlowgraph->connect(subchannel, subchPrbs);
- myFlowgraph->connect(subchPrbs, subchConv);
- myFlowgraph->connect(subchConv, subchPunc);
- myFlowgraph->connect(subchPunc, subchInterleaver);
- myFlowgraph->connect(subchInterleaver, cifMux);
+ m_flowgraph->connect(subchannel, subchPrbs);
+ m_flowgraph->connect(subchPrbs, subchConv);
+ m_flowgraph->connect(subchConv, subchPunc);
+ m_flowgraph->connect(subchPunc, subchInterleaver);
+ m_flowgraph->connect(subchInterleaver, cifMux);
}
- myFlowgraph->connect(cifMux, cifPart);
-#if defined(BUILD_FOR_EASYDABV3)
- myFlowgraph->connect(cifPart, myOutput);
-#else
- myFlowgraph->connect(cifPart, cifMap);
- myFlowgraph->connect(cifMap, cifFreq);
- myFlowgraph->connect(cifRef, cifDiff);
- myFlowgraph->connect(cifFreq, cifDiff);
- myFlowgraph->connect(cifNull, cifSig);
- myFlowgraph->connect(cifDiff, cifSig);
+ m_flowgraph->connect(cifMux, cifPart);
+ m_flowgraph->connect(cifPart, cifMap);
+ m_flowgraph->connect(cifMap, cifFreq);
+ m_flowgraph->connect(cifRef, cifDiff);
+ m_flowgraph->connect(cifFreq, cifDiff);
+ m_flowgraph->connect(cifNull, cifSig);
+ m_flowgraph->connect(cifDiff, cifSig);
if (tii) {
- myFlowgraph->connect(tiiRef, tii);
- myFlowgraph->connect(tii, cifSig);
+ m_flowgraph->connect(tiiRef, tii);
+ m_flowgraph->connect(tii, cifSig);
}
shared_ptr<ModPlugin> prev_plugin = static_pointer_cast<ModPlugin>(cifSig);
- const std::list<shared_ptr<ModPlugin> > plugins({
+ const std::vector<shared_ptr<ModPlugin> > plugins({
static_pointer_cast<ModPlugin>(cifCicEq),
static_pointer_cast<ModPlugin>(cifOfdm),
static_pointer_cast<ModPlugin>(cifGain),
static_pointer_cast<ModPlugin>(cifGuard),
- static_pointer_cast<ModPlugin>(cifFilter), // optional block
- static_pointer_cast<ModPlugin>(cifRes), // optional block
- static_pointer_cast<ModPlugin>(cifPoly), // optional block
- static_pointer_cast<ModPlugin>(myOutput),
+ // optional blocks
+ static_pointer_cast<ModPlugin>(cifFilter),
+ static_pointer_cast<ModPlugin>(cifRes),
+ static_pointer_cast<ModPlugin>(cifPoly),
+ static_pointer_cast<ModPlugin>(m_formatConverter),
+ // mandatory block
+ static_pointer_cast<ModPlugin>(m_output),
});
for (auto& p : plugins) {
if (p) {
- myFlowgraph->connect(prev_plugin, p);
+ m_flowgraph->connect(prev_plugin, p);
prev_plugin = p;
}
}
-#endif
+ etiLog.level(debug) << "DabModulator set up.";
}
////////////////////////////////////////////////////////////////////
// Processing data
////////////////////////////////////////////////////////////////////
- return myFlowgraph->run();
+ return m_flowgraph->run();
}
meta_vec_t DabModulator::process_metadata(const meta_vec_t& metadataIn)
{
- if (myOutput) {
- return myOutput->get_latest_metadata();
+ if (m_output) {
+ return m_output->get_latest_metadata();
}
return {};
@@ -396,6 +439,9 @@ void DabModulator::set_parameter(const string& parameter, const string& value)
if (parameter == "rate") {
throw ParameterError("Parameter 'rate' is read-only");
}
+ else if (parameter == "num_clipped_samples") {
+ throw ParameterError("Parameter 'num_clipped_samples' is read-only");
+ }
else {
stringstream ss;
ss << "Parameter '" << parameter <<
@@ -410,6 +456,16 @@ const string DabModulator::get_parameter(const string& parameter) const
if (parameter == "rate") {
ss << m_settings.outputRate;
}
+ else if (parameter == "num_clipped_samples") {
+ if (m_formatConverter) {
+ ss << m_formatConverter->get_num_clipped_samples();
+ }
+ else {
+ ss << "Parameter '" << parameter <<
+ "' is not available when no format conversion is done.";
+ throw ParameterError(ss.str());
+ }
+ }
else {
ss << "Parameter '" << parameter <<
"' is not exported by controllable " << get_rc_name();
@@ -417,3 +473,11 @@ const string DabModulator::get_parameter(const string& parameter) const
}
return ss.str();
}
+
+const json::map_t DabModulator::get_all_values() const
+{
+ json::map_t map;
+ map["rate"].v = m_settings.outputRate;
+ map["num_clipped_samples"].v = m_formatConverter ? m_formatConverter->get_num_clipped_samples() : 0;
+ return map;
+}
diff --git a/src/DabModulator.h b/src/DabModulator.h
index 00d71f5..82782cd 100644
--- a/src/DabModulator.h
+++ b/src/DabModulator.h
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2019
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -39,17 +39,17 @@
#include "ConfigParser.h"
#include "EtiReader.h"
#include "Flowgraph.h"
-#include "GainControl.h"
+#include "FormatConverter.h"
#include "OutputMemory.h"
#include "RemoteControl.h"
-#include "Log.h"
-#include "TII.h"
-
class DabModulator : public ModInput, public ModMetadata, public RemoteControllable
{
public:
- DabModulator(EtiSource& etiSource, mod_settings_t& settings);
+ DabModulator(EtiSource& etiSource, mod_settings_t& settings, const std::string& format);
+ // Allowed formats: s8, u8 and s16. Empty string means no conversion
+
+ virtual ~DabModulator() {}
int process(Buffer* dataOut) override;
const char* name() override { return "DabModulator"; }
@@ -57,30 +57,30 @@ public:
virtual meta_vec_t process_metadata(const meta_vec_t& metadataIn) override;
/* Required to get the timestamp */
- EtiSource* getEtiSource() { return &myEtiSource; }
+ EtiSource* getEtiSource() { return &m_etiSource; }
/******* REMOTE CONTROL ********/
- virtual void set_parameter(const std::string& parameter,
- const std::string& value) override;
-
- virtual const std::string get_parameter(
- const std::string& parameter) const override;
+ virtual void set_parameter(const std::string& parameter, const std::string& value) override;
+ virtual const std::string get_parameter(const std::string& parameter) const override;
+ virtual const json::map_t get_all_values() const override;
protected:
void setMode(unsigned mode);
mod_settings_t& m_settings;
+ std::string m_format;
- EtiSource& myEtiSource;
- std::shared_ptr<Flowgraph> myFlowgraph;
+ EtiSource& m_etiSource;
+ std::shared_ptr<Flowgraph> m_flowgraph;
- size_t myNbSymbols;
- size_t myNbCarriers;
- size_t mySpacing;
- size_t myNullSize;
- size_t mySymSize;
- size_t myFicSizeOut;
+ size_t m_nbSymbols;
+ size_t m_nbCarriers;
+ size_t m_spacing;
+ size_t m_nullSize;
+ size_t m_symSize;
+ size_t m_ficSizeOut;
- std::shared_ptr<OutputMemory> myOutput;
+ std::shared_ptr<FormatConverter> m_formatConverter;
+ std::shared_ptr<OutputMemory> m_output;
};
diff --git a/src/DifferentialModulator.cpp b/src/DifferentialModulator.cpp
index 97a7998..21b4c3e 100644
--- a/src/DifferentialModulator.cpp
+++ b/src/DifferentialModulator.cpp
@@ -22,17 +22,14 @@
#include "DifferentialModulator.h"
#include "PcDebug.h"
-#include <stdio.h>
+#include <cstdio>
#include <stdexcept>
-#include <complex>
-#include <string.h>
+#include <cstring>
-typedef std::complex<float> complexf;
-
-
-DifferentialModulator::DifferentialModulator(size_t carriers) :
+DifferentialModulator::DifferentialModulator(size_t carriers, bool fixedPoint) :
ModMux(),
- d_carriers(carriers)
+ m_carriers(carriers),
+ m_fixedPoint(fixedPoint)
{
PDEBUG("DifferentialModulator::DifferentialModulator(%zu)\n", carriers);
@@ -42,10 +39,42 @@ DifferentialModulator::DifferentialModulator(size_t carriers) :
DifferentialModulator::~DifferentialModulator()
{
PDEBUG("DifferentialModulator::~DifferentialModulator()\n");
-
}
+template<typename T>
+void do_process(size_t carriers, const std::vector<Buffer*>& dataIn, Buffer* dataOut)
+{
+ size_t phaseSize = dataIn[0]->getLength() / sizeof(T);
+ size_t dataSize = dataIn[1]->getLength() / sizeof(T);
+ dataOut->setLength((phaseSize + dataSize) * sizeof(T));
+
+ const T* phase = reinterpret_cast<const T*>(dataIn[0]->getData());
+ const T* in = reinterpret_cast<const T*>(dataIn[1]->getData());
+ T* out = reinterpret_cast<T*>(dataOut->getData());
+
+ if (phaseSize != carriers) {
+ throw std::runtime_error(
+ "DifferentialModulator::process input phase size not valid!");
+ }
+ if (dataSize % carriers != 0) {
+ throw std::runtime_error(
+ "DifferentialModulator::process input data size not valid!");
+ }
+
+ memcpy(dataOut->getData(), phase, phaseSize * sizeof(T));
+ for (size_t i = 0; i < dataSize; i += carriers) {
+ for (size_t j = 0; j < carriers; j += 4) {
+ out[carriers + j] = out[j] * in[j];
+ out[carriers + j + 1] = out[j + 1] * in[j + 1];
+ out[carriers + j + 2] = out[j + 2] * in[j + 2];
+ out[carriers + j + 3] = out[j + 3] * in[j + 3];
+ }
+ in += carriers;
+ out += carriers;
+ }
+}
+
// dataIn[0] -> phase reference
// dataIn[1] -> data symbols
int DifferentialModulator::process(std::vector<Buffer*> dataIn, Buffer* dataOut)
@@ -67,33 +96,11 @@ int DifferentialModulator::process(std::vector<Buffer*> dataIn, Buffer* dataOut)
"DifferentialModulator::process nb of input streams not 2!");
}
- size_t phaseSize = dataIn[0]->getLength() / sizeof(complexf);
- size_t dataSize = dataIn[1]->getLength() / sizeof(complexf);
- dataOut->setLength((phaseSize + dataSize) * sizeof(complexf));
-
- const complexf* phase = reinterpret_cast<const complexf*>(dataIn[0]->getData());
- const complexf* in = reinterpret_cast<const complexf*>(dataIn[1]->getData());
- complexf* out = reinterpret_cast<complexf*>(dataOut->getData());
-
- if (phaseSize != d_carriers) {
- throw std::runtime_error(
- "DifferentialModulator::process input phase size not valid!");
- }
- if (dataSize % d_carriers != 0) {
- throw std::runtime_error(
- "DifferentialModulator::process input data size not valid!");
+ if (m_fixedPoint) {
+ do_process<complexfix>(m_carriers, dataIn, dataOut);
}
-
- memcpy(dataOut->getData(), phase, phaseSize * sizeof(complexf));
- for (size_t i = 0; i < dataSize; i += d_carriers) {
- for (size_t j = 0; j < d_carriers; j += 4) {
- out[d_carriers + j] = out[j] * in[j];
- out[d_carriers + j + 1] = out[j + 1] * in[j + 1];
- out[d_carriers + j + 2] = out[j + 2] * in[j + 2];
- out[d_carriers + j + 3] = out[j + 3] * in[j + 3];
- }
- in += d_carriers;
- out += d_carriers;
+ else {
+ do_process<complexf>(m_carriers, dataIn, dataOut);
}
return dataOut->getLength();
diff --git a/src/DifferentialModulator.h b/src/DifferentialModulator.h
index b26ea8b..9cc5081 100644
--- a/src/DifferentialModulator.h
+++ b/src/DifferentialModulator.h
@@ -35,7 +35,7 @@
class DifferentialModulator : public ModMux
{
public:
- DifferentialModulator(size_t carriers);
+ DifferentialModulator(size_t carriers, bool fixedPoint);
virtual ~DifferentialModulator();
DifferentialModulator(const DifferentialModulator&);
DifferentialModulator& operator=(const DifferentialModulator&);
@@ -45,6 +45,7 @@ public:
const char* name() { return "DifferentialModulator"; }
protected:
- size_t d_carriers;
+ size_t m_carriers;
+ size_t m_fixedPoint;
};
diff --git a/src/EtiReader.cpp b/src/EtiReader.cpp
index d1c7622..5726713 100644
--- a/src/EtiReader.cpp
+++ b/src/EtiReader.cpp
@@ -78,6 +78,11 @@ unsigned EtiReader::getFct()
return eti_fc.FCT;
}
+frame_timestamp EtiReader::getTimestamp()
+{
+ return myTimestampDecoder.getTimestamp();
+}
+
const std::vector<std::shared_ptr<SubchannelSource> > EtiReader::getSubchannels() const
{
@@ -223,7 +228,7 @@ int EtiReader::loadEtiData(const Buffer& dataIn)
unsigned size = mySources[i]->framesize();
PDEBUG("Writting %i bytes of subchannel data\n", size);
Buffer subch(size, in);
- mySources[i]->loadSubchannelData(move(subch));
+ mySources[i]->loadSubchannelData(std::move(subch));
input_size -= size;
framesize -= size;
in += size;
@@ -278,28 +283,21 @@ int EtiReader::loadEtiData(const Buffer& dataIn)
return dataIn.getLength() - input_size;
}
-bool EtiReader::sourceContainsTimestamp()
-{
- return (ntohl(eti_tist.TIST) & 0xFFFFFF) != 0xFFFFFF;
- /* See ETS 300 799, Annex C.2.2 */
-}
-
uint32_t EtiReader::getPPSOffset()
{
- if (!sourceContainsTimestamp()) {
- //fprintf(stderr, "****** SOURCE NO TS\n");
+ const uint32_t timestamp = ntohl(eti_tist.TIST) & 0xFFFFFF;
+
+ /* See ETS 300 799, Annex C.2.2 */
+ if (timestamp == 0xFFFFFF) {
return 0.0;
}
- uint32_t timestamp = ntohl(eti_tist.TIST) & 0xFFFFFF;
- //fprintf(stderr, "****** TIST 0x%x\n", timestamp);
-
return timestamp;
}
-EdiReader::EdiReader(
- double& tist_offset_s) :
- m_timestamp_decoder(tist_offset_s)
+EdiReader::EdiReader(double& tist_offset_s) :
+ m_timestamp_decoder(tist_offset_s),
+ m_fic_decoder(/*verbose*/ false)
{
rcs.enrol(&m_timestamp_decoder);
}
@@ -329,6 +327,11 @@ unsigned EdiReader::getFct()
return m_fc.fct();
}
+frame_timestamp EdiReader::getTimestamp()
+{
+ return m_timestamp_decoder.getTimestamp();
+}
+
const std::vector<std::shared_ptr<SubchannelSource> > EdiReader::getSubchannels() const
{
std::vector<std::shared_ptr<SubchannelSource> > sources;
@@ -346,15 +349,6 @@ const std::vector<std::shared_ptr<SubchannelSource> > EdiReader::getSubchannels(
return sources;
}
-bool EdiReader::sourceContainsTimestamp()
-{
- if (not (m_frameReady and m_fc_valid)) {
- throw std::runtime_error("Trying to get timestamp before it is ready");
- }
-
- return m_fc.tsta != 0xFFFFFF;
-}
-
bool EdiReader::isFrameReady()
{
return m_frameReady;
@@ -417,7 +411,10 @@ void EdiReader::update_fic(std::vector<uint8_t>&& fic)
if (not m_proto_valid) {
throw std::logic_error("Cannot update FIC before protocol");
}
- m_fic = move(fic);
+
+ m_fic_decoder.Process(fic.data(), fic.size());
+
+ m_fic = std::move(fic);
}
void EdiReader::update_edi_time(
@@ -469,7 +466,7 @@ void EdiReader::add_subchannel(EdiDecoder::eti_stc_data&& stc)
throw std::invalid_argument(
"EDI: MST data length inconsistent with FIC");
}
- source->loadSubchannelData(move(stc.mst));
+ source->loadSubchannelData(std::move(stc.mst));
if (m_sources.size() > 64) {
throw std::invalid_argument("Too many subchannels");
@@ -543,8 +540,8 @@ void EdiTransport::Open(const std::string& uri)
{
etiLog.level(info) << "Opening EDI :" << uri;
- const string proto = uri.substr(0, 3);
- if (proto == "udp") {
+ const string proto = uri.substr(0, 6);
+ if (proto == "udp://") {
if (m_proto == Proto::TCP) {
throw std::invalid_argument("Cannot specify both TCP and UDP urls");
}
@@ -564,7 +561,7 @@ void EdiTransport::Open(const std::string& uri)
m_mcastaddr = host_full.substr(found_mcast+1);
}
else if (found_port != 6) {
- m_bindto=host_full;
+ m_bindto = host_full;
}
etiLog.level(info) << "EDI UDP input: host:" << m_bindto <<
@@ -574,7 +571,7 @@ void EdiTransport::Open(const std::string& uri)
m_proto = Proto::UDP;
m_enabled = true;
}
- else if (proto == "tcp") {
+ else if (proto == "tcp://") {
if (m_proto != Proto::Unspecified) {
throw std::invalid_argument("Cannot call Open several times with TCP");
}
@@ -585,10 +582,11 @@ void EdiTransport::Open(const std::string& uri)
}
m_port = std::stoi(uri.substr(found_port+1));
- const std::string hostname = uri.substr(6, found_port-6);// skip tcp://
+ const std::string hostname = uri.substr(6, found_port-6);
etiLog.level(info) << "EDI TCP connect to " << hostname << ":" << m_port;
+ m_tcp_uri = uri;
m_tcpclient.connect(hostname, m_port);
m_proto = Proto::TCP;
m_enabled = true;
@@ -615,7 +613,7 @@ bool EdiTransport::rxPacket()
received_from = rp.received_from;
EdiDecoder::Packet p;
- p.buf = move(rp.packetdata);
+ p.buf = std::move(rp.packetdata);
p.received_on_port = rp.port_received_on;
m_decoder.push_packet(p);
}
@@ -650,23 +648,19 @@ bool EdiTransport::rxPacket()
// discontinuity.
m_tcpbuffer.resize(512);
const int timeout_ms = 1000;
- try {
- ssize_t ret = m_tcpclient.recv(m_tcpbuffer.data(), m_tcpbuffer.size(), 0, timeout_ms);
- if (ret == 0 or ret == -1) {
- return false;
- }
- else if (ret > (ssize_t)m_tcpbuffer.size()) {
- throw logic_error("EDI TCP: invalid recv() return value");
- }
- else {
- m_tcpbuffer.resize(ret);
- m_decoder.push_bytes(m_tcpbuffer);
- return true;
- }
- }
- catch (const Socket::TCPSocket::Timeout&) {
+
+ ssize_t ret = m_tcpclient.recv(m_tcpbuffer.data(), m_tcpbuffer.size(), 0, timeout_ms);
+ if (ret <= 0) {
return false;
}
+ else if (ret > (ssize_t)m_tcpbuffer.size()) {
+ throw logic_error("EDI TCP: invalid recv() return value");
+ }
+ else {
+ m_tcpbuffer.resize(ret);
+ m_decoder.push_bytes(m_tcpbuffer);
+ return true;
+ }
}
}
throw logic_error("Incomplete rxPacket implementation!");
diff --git a/src/EtiReader.h b/src/EtiReader.h
index d97acf6..703e42a 100644
--- a/src/EtiReader.h
+++ b/src/EtiReader.h
@@ -34,6 +34,7 @@
#include "Eti.h"
#include "Log.h"
#include "FicSource.h"
+#include "FigParser.h"
#include "Socket.h"
#include "SubchannelSource.h"
#include "TimestampDecoder.h"
@@ -59,8 +60,8 @@ public:
/* Get the current Frame Count */
virtual unsigned getFct() = 0;
- /* Returns true if we have valid time stamps in the ETI*/
- virtual bool sourceContainsTimestamp() = 0;
+ /* Returns current Timestamp */
+ virtual frame_timestamp getTimestamp() = 0;
/* Return the FIC source to be used for modulation */
virtual std::shared_ptr<FicSource>& getFic(void);
@@ -97,18 +98,17 @@ class EtiReader : public EtiSource
public:
EtiReader(double& tist_offset_s);
- virtual unsigned getMode();
- virtual unsigned getFp();
- virtual unsigned getFct();
+ virtual unsigned getMode() override;
+ virtual unsigned getFp() override;
+ virtual unsigned getFct() override;
+ virtual frame_timestamp getTimestamp() override;
/* Read ETI data from dataIn. Returns the number of bytes
* read from the buffer.
*/
int loadEtiData(const Buffer& dataIn);
- virtual bool sourceContainsTimestamp();
-
- virtual const std::vector<std::shared_ptr<SubchannelSource> > getSubchannels() const;
+ virtual const std::vector<std::shared_ptr<SubchannelSource> > getSubchannels() const override;
private:
/* Transform the ETI TIST to a PPS offset in units of 1/16384000 s */
@@ -141,7 +141,7 @@ public:
virtual unsigned getMode() override;
virtual unsigned getFp() override;
virtual unsigned getFct() override;
- virtual bool sourceContainsTimestamp() override;
+ virtual frame_timestamp getTimestamp() override;
virtual const std::vector<std::shared_ptr<SubchannelSource> > getSubchannels() const override;
virtual bool isFrameReady(void);
@@ -175,6 +175,15 @@ public:
// Gets called by the EDI library to tell us that all data for a frame was given to us
virtual void assemble(EdiDecoder::ReceivedTagPacket&& tagpacket) override;
+
+ std::optional<FIC_ENSEMBLE> getEnsembleInfo() const {
+ return m_fic_decoder.observer.ensemble;
+ }
+
+ std::map<int /*SId*/, LISTED_SERVICE> getServiceInfo() const {
+ return m_fic_decoder.observer.services;
+ }
+
private:
bool m_proto_valid = false;
bool m_frameReady = false;
@@ -198,6 +207,7 @@ private:
std::map<uint8_t, std::shared_ptr<SubchannelSource> > m_sources;
TimestampDecoder m_timestamp_decoder;
+ FICDecoder m_fic_decoder;
};
/* The EDI input does not use the inputs defined in InputReader.h, as they were
@@ -211,6 +221,7 @@ class EdiTransport {
void Open(const std::string& uri);
bool isEnabled(void) const { return m_enabled; }
+ std::string getTcpUri(void) const { return m_tcp_uri; }
/* Receive a packet and give it to the decoder. Returns
* true if a packet was received, false in case of socket
@@ -219,6 +230,7 @@ class EdiTransport {
bool rxPacket(void);
private:
+ std::string m_tcp_uri;
bool m_enabled;
int m_port;
std::string m_bindto;
diff --git a/src/Events.cpp b/src/Events.cpp
new file mode 100644
index 0000000..f8cc2b8
--- /dev/null
+++ b/src/Events.cpp
@@ -0,0 +1,95 @@
+/*
+ Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012
+ Her Majesty the Queen in Right of Canada (Communications Research
+ Center Canada)
+
+ Copyright (C) 2024
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://www.opendigitalradio.org
+ */
+/*
+ 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, either 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 for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include <string>
+#include <string>
+
+#include "Events.h"
+
+#if defined(HAVE_ZEROMQ)
+
+EventSender events;
+
+EventSender::EventSender() :
+ m_zmq_context(1),
+ m_socket(m_zmq_context, zmq::socket_type::pub)
+{
+ int linger = 2000;
+ m_socket.setsockopt(ZMQ_LINGER, &linger, sizeof(linger));
+}
+
+EventSender::~EventSender()
+{ }
+
+void EventSender::bind(const std::string& bind_endpoint)
+{
+ try {
+ m_socket.bind(bind_endpoint);
+ m_socket_valid = true;
+ }
+ catch (const zmq::error_t& err) {
+ fprintf(stderr, "Cannot bind event socket: %s", err.what());
+ }
+}
+
+void EventSender::send(const std::string& event_name, const json::map_t& detail)
+{
+ if (not m_socket_valid) {
+ return;
+ }
+
+ zmq::message_t zmsg1(event_name.data(), event_name.size());
+ const auto detail_json = json::map_to_json(detail);
+ zmq::message_t zmsg2(detail_json.data(), detail_json.size());
+
+ try {
+ m_socket.send(zmsg1, zmq::send_flags::sndmore);
+ m_socket.send(zmsg2, zmq::send_flags::none);
+ }
+ catch (const zmq::error_t& err) {
+ fprintf(stderr, "Cannot send event %s: %s", event_name.c_str(), err.what());
+ }
+}
+
+
+void LogToEventSender::log(log_level_t level, const std::string& message)
+{
+ std::string event_name;
+ if (level == log_level_t::warn) { event_name = "warn"; }
+ else if (level == log_level_t::error) { event_name = "error"; }
+ else if (level == log_level_t::alert) { event_name = "alert"; }
+ else if (level == log_level_t::emerg) { event_name = "emerg"; }
+
+ if (not event_name.empty()) {
+ json::map_t detail;
+ detail["message"].v = message;
+ events.send(event_name, detail);
+ }
+}
+
+std::string LogToEventSender::get_name() const
+{
+ return "EventSender";
+}
+#endif // defined(HAVE_ZEROMQ)
diff --git a/src/Events.h b/src/Events.h
new file mode 100644
index 0000000..ea1ace2
--- /dev/null
+++ b/src/Events.h
@@ -0,0 +1,67 @@
+/*
+ Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012
+ Her Majesty the Queen in Right of Canada (Communications Research
+ Center Canada)
+
+ Copyright (C) 2024
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://www.opendigitalradio.org
+ */
+/*
+ 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, either 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 for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#if defined(HAVE_ZEROMQ)
+# include "zmq.hpp"
+# include <string>
+# include "Log.h"
+# include "Json.h"
+
+class EventSender {
+ public:
+ EventSender();
+ EventSender(const EventSender& other) = delete;
+ const EventSender& operator=(const EventSender& other) = delete;
+ EventSender(EventSender&& other) = delete;
+ EventSender& operator=(EventSender&& other) = delete;
+ ~EventSender();
+
+ void bind(const std::string& bind_endpoint);
+
+ void send(const std::string& event_name, const json::map_t& detail);
+ private:
+ zmq::context_t m_zmq_context;
+ zmq::socket_t m_socket;
+ bool m_socket_valid = false;
+};
+
+class LogToEventSender: public LogBackend {
+ public:
+ virtual ~LogToEventSender() {};
+ virtual void log(log_level_t level, const std::string& message);
+ virtual std::string get_name() const;
+};
+
+/* events is a singleton used in all parts of the program to output log messages.
+ * It is constructed in Events.cpp */
+extern EventSender events;
+
+#endif // defined(HAVE_ZEROMQ)
diff --git a/src/FIRFilter.cpp b/src/FIRFilter.cpp
index 89cf0da..57e7127 100644
--- a/src/FIRFilter.cpp
+++ b/src/FIRFilter.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2007, 2008, 2009, 2010, 2011 Her Majesty the Queen in
Right of Canada (Communications Research Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -347,3 +347,10 @@ const string FIRFilter::get_parameter(const string& parameter) const
return ss.str();
}
+const json::map_t FIRFilter::get_all_values() const
+{
+ json::map_t map;
+ map["ntaps"].v = m_taps.size();
+ map["tapsfile"].v = m_taps_file;
+ return map;
+}
diff --git a/src/FIRFilter.h b/src/FIRFilter.h
index 8d2e707..2d8fba9 100644
--- a/src/FIRFilter.h
+++ b/src/FIRFilter.h
@@ -2,7 +2,7 @@
Copyright (C) 2007, 2008, 2009, 2010, 2011 Her Majesty the Queen in
Right of Canada (Communications Research Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -33,21 +33,14 @@
#include "RemoteControl.h"
#include "ModPlugin.h"
-#include "PcDebug.h"
#include <sys/types.h>
-#include <complex>
-#include <thread>
#include <vector>
-#include <time.h>
#include <cstdio>
#include <string>
-#include <memory>
#define FIRFILTER_PIPELINE_DELAY 1
-typedef std::complex<float> complexf;
-
class FIRFilter : public PipelinedModCodec, public RemoteControllable
{
public:
@@ -59,11 +52,9 @@ public:
const char* name() override { return "FIRFilter"; }
/******* REMOTE CONTROL ********/
- virtual void set_parameter(const std::string& parameter,
- const std::string& value) override;
-
- virtual const std::string get_parameter(
- const std::string& parameter) const override;
+ virtual void set_parameter(const std::string& parameter, const std::string& value) override;
+ virtual const std::string get_parameter(const std::string& parameter) const override;
+ virtual const json::map_t get_all_values() const override;
protected:
virtual int internal_process(Buffer* const dataIn, Buffer* dataOut) override;
diff --git a/src/FicSource.cpp b/src/FicSource.cpp
index 2b95085..d824058 100644
--- a/src/FicSource.cpp
+++ b/src/FicSource.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2022
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -27,7 +27,6 @@
#include "FicSource.h"
#include "PcDebug.h"
#include "Log.h"
-#include "TimestampDecoder.h"
#include <stdexcept>
#include <string>
@@ -36,46 +35,45 @@
#include <string.h>
-const std::vector<PuncturingRule>& FicSource::get_rules()
-{
- return d_puncturing_rules;
-}
-
-
FicSource::FicSource(unsigned ficf, unsigned mid) :
ModInput()
{
// PDEBUG("FicSource::FicSource(...)\n");
// PDEBUG(" Start address: %i\n", d_start_address);
-// PDEBUG(" Framesize: %i\n", d_framesize);
+// PDEBUG(" Framesize: %i\n", m_framesize);
// PDEBUG(" Protection: %i\n", d_protection);
if (ficf == 0) {
- d_framesize = 0;
- d_buffer.setLength(0);
+ m_buffer.setLength(0);
return;
}
if (mid == 3) {
- d_framesize = 32 * 4;
- d_puncturing_rules.emplace_back(29 * 16, 0xeeeeeeee);
- d_puncturing_rules.emplace_back(3 * 16, 0xeeeeeeec);
+ m_framesize = 32 * 4;
+ m_puncturing_rules.emplace_back(29 * 16, 0xeeeeeeee);
+ m_puncturing_rules.emplace_back(3 * 16, 0xeeeeeeec);
} else {
- d_framesize = 24 * 4;
- d_puncturing_rules.emplace_back(21 * 16, 0xeeeeeeee);
- d_puncturing_rules.emplace_back(3 * 16, 0xeeeeeeec);
+ m_framesize = 24 * 4;
+ m_puncturing_rules.emplace_back(21 * 16, 0xeeeeeeee);
+ m_puncturing_rules.emplace_back(3 * 16, 0xeeeeeeec);
}
- d_buffer.setLength(d_framesize);
+ m_buffer.setLength(m_framesize);
+}
+
+size_t FicSource::getFramesize() const
+{
+ return m_framesize;
}
-size_t FicSource::getFramesize()
+const std::vector<PuncturingRule>& FicSource::get_rules() const
{
- return d_framesize;
+ return m_puncturing_rules;
}
+
void FicSource::loadFicData(const Buffer& fic)
{
- d_buffer = fic;
+ m_buffer = fic;
}
int FicSource::process(Buffer* outputData)
@@ -83,34 +81,31 @@ int FicSource::process(Buffer* outputData)
PDEBUG("FicSource::process (outputData: %p, outputSize: %zu)\n",
outputData, outputData->getLength());
- if (d_buffer.getLength() != d_framesize) {
+ if (m_buffer.getLength() != m_framesize) {
throw std::runtime_error(
- "ERROR: FicSource::process.outputSize != d_framesize: " +
- std::to_string(d_buffer.getLength()) + " != " +
- std::to_string(d_framesize));
+ "ERROR: FicSource::process.outputSize != m_framesize: " +
+ std::to_string(m_buffer.getLength()) + " != " +
+ std::to_string(m_framesize));
}
- *outputData = d_buffer;
+ *outputData = m_buffer;
return outputData->getLength();
}
-void FicSource::loadTimestamp(const std::shared_ptr<struct frame_timestamp>& ts)
+void FicSource::loadTimestamp(const frame_timestamp& ts)
{
- d_ts = ts;
+ m_ts_valid = true;
+ m_ts = ts;
}
-
meta_vec_t FicSource::process_metadata(const meta_vec_t& metadataIn)
{
- if (not d_ts) {
- return {};
- }
-
- using namespace std;
meta_vec_t md_vec;
- flowgraph_metadata meta;
- meta.ts = d_ts;
- md_vec.push_back(meta);
+ if (m_ts_valid) {
+ flowgraph_metadata meta;
+ meta.ts = m_ts;
+ md_vec.push_back(meta);
+ }
return md_vec;
}
diff --git a/src/FicSource.h b/src/FicSource.h
index 93c1a7f..01dba2d 100644
--- a/src/FicSource.h
+++ b/src/FicSource.h
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2016
+ Copyright (C) 2022
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -33,6 +33,7 @@
#include "PuncturingRule.h"
#include "Eti.h"
#include "ModPlugin.h"
+#include "TimestampDecoder.h"
#include <vector>
#include <sys/types.h>
@@ -41,21 +42,21 @@ class FicSource : public ModInput, public ModMetadata
public:
FicSource(unsigned ficf, unsigned mid);
- size_t getFramesize();
- const std::vector<PuncturingRule>& get_rules();
+ size_t getFramesize() const;
+ const std::vector<PuncturingRule>& get_rules() const;
void loadFicData(const Buffer& fic);
int process(Buffer* outputData) override;
const char* name() override { return "FicSource"; }
- void loadTimestamp(const std::shared_ptr<struct frame_timestamp>& ts);
- virtual meta_vec_t process_metadata(
- const meta_vec_t& metadataIn) override;
+ void loadTimestamp(const frame_timestamp& ts);
+ virtual meta_vec_t process_metadata(const meta_vec_t& metadataIn) override;
private:
- size_t d_framesize;
- Buffer d_buffer;
- std::shared_ptr<struct frame_timestamp> d_ts;
- std::vector<PuncturingRule> d_puncturing_rules;
+ size_t m_framesize = 0;
+ Buffer m_buffer;
+ frame_timestamp m_ts;
+ bool m_ts_valid = false;
+ std::vector<PuncturingRule> m_puncturing_rules;
};
diff --git a/src/FigParser.cpp b/src/FigParser.cpp
new file mode 100644
index 0000000..4df4cb3
--- /dev/null
+++ b/src/FigParser.cpp
@@ -0,0 +1,1052 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
+ the Queen in Right of Canada (Communications Research Center Canada)
+
+ Most parts of this file are taken from dablin,
+ Copyright (C) 2015-2022 Stefan Pöschel
+
+ Copyright (C) 2023
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+ */
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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, either version 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "FigParser.h"
+#include "PcDebug.h"
+#include "Log.h"
+#include "crc.h"
+#include "CharsetTools.h"
+
+#include <stdexcept>
+#include <string>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+
+
+template<class T>
+static uint16_t read_16b(T buf)
+{
+ uint16_t value = 0;
+ value = (uint16_t)(buf[0]) << 8;
+ value |= (uint16_t)(buf[1]);
+ return value;
+}
+
+static bool checkCRC(const uint8_t *buf, size_t size)
+{
+ const uint16_t crc_from_packet = read_16b(buf + size - 2);
+ uint16_t crc_calc = 0xffff;
+ crc_calc = crc16(crc_calc, buf, size - 2);
+ crc_calc ^= 0xffff;
+ return crc_from_packet == crc_calc;
+}
+
+void FICDecoderObserver::FICChangeEnsemble(const FIC_ENSEMBLE& e)
+{
+ if (ensemble.has_value() and e.eid == ensemble->eid and e.ecc == ensemble->ecc) {
+ return;
+ }
+
+ services.clear();
+ ensemble = e;
+}
+
+void FICDecoderObserver::FICChangeService(const LISTED_SERVICE& ls)
+{
+ services[ls.sid] = ls;
+}
+void FICDecoderObserver::FICChangeUTCDateTime(const FIC_DAB_DT& dt)
+{
+ utc_dt = dt;
+}
+
+// --- FICDecoder -----------------------------------------------------------------
+FICDecoder::FICDecoder(bool verbose) :
+ verbose(verbose),
+ utc_dt_long(false)
+{ }
+
+
+void FICDecoder::Reset() {
+ ensemble = FIC_ENSEMBLE();
+ services.clear();
+ subchannels.clear();
+ utc_dt = FIC_DAB_DT();
+}
+
+void FICDecoder::Process(const uint8_t *data, size_t len) {
+ // check for integer FIB count
+ if(len % 32) {
+ etiLog.log(warn, "FICDecoder: Ignoring non-integer FIB count FIC data with %zu bytes\n", len);
+ return;
+ }
+
+ for(size_t i = 0; i < len; i += 32)
+ ProcessFIB(data + i);
+}
+
+void FICDecoder::ProcessFIB(const uint8_t *data) {
+ if (not checkCRC(data, 32)) {
+ observer.FICDiscardedFIB();
+ return;
+ }
+
+ // iterate over all FIGs
+ for(size_t offset = 0; offset < 30 && data[offset] != 0xFF;) {
+ int type = data[offset] >> 5;
+ size_t len = data[offset] & 0x1F;
+ offset++;
+
+ switch(type) {
+ case 0:
+ ProcessFIG0(data + offset, len);
+ break;
+ case 1:
+ ProcessFIG1(data + offset, len);
+ break;
+ // default:
+ // etiLog.log(warn, "FICDecoder: received unsupported FIG %d with %zu bytes\n", type, len);
+ }
+ offset += len;
+ }
+}
+
+
+void FICDecoder::ProcessFIG0(const uint8_t *data, size_t len) {
+ if(len < 1) {
+ etiLog.log(warn, "FICDecoder: received empty FIG 0\n");
+ return;
+ }
+
+ // read/skip FIG 0 header
+ FIG0_HEADER header(data[0]);
+ data++;
+ len--;
+
+ // ignore next config/other ensembles/data services
+ if(header.cn || header.oe || header.pd)
+ return;
+
+
+ // handle extension
+ switch(header.extension) {
+ case 0:
+ ProcessFIG0_0(data, len);
+ break;
+ case 1:
+ ProcessFIG0_1(data, len);
+ break;
+ case 2:
+ ProcessFIG0_2(data, len);
+ break;
+ case 5:
+ ProcessFIG0_5(data, len);
+ break;
+ case 8:
+ ProcessFIG0_8(data, len);
+ break;
+ case 9:
+ ProcessFIG0_9(data, len);
+ break;
+ case 10:
+ ProcessFIG0_10(data, len);
+ break;
+ case 13:
+ ProcessFIG0_13(data, len);
+ break;
+ case 17:
+ ProcessFIG0_17(data, len);
+ break;
+ case 18:
+ ProcessFIG0_18(data, len);
+ break;
+ case 19:
+ ProcessFIG0_19(data, len);
+ break;
+ // default:
+ // etiLog.log(warn, "FICDecoder: received unsupported FIG 0/%d with %zu field bytes\n", header.extension, len);
+ }
+}
+
+void FICDecoder::ProcessFIG0_0(const uint8_t *data, size_t len) {
+ // FIG 0/0 - Ensemble information
+ // EId and alarm flag only
+
+ if(len < 4)
+ return;
+
+ FIC_ENSEMBLE new_ensemble = ensemble;
+ new_ensemble.eid = data[0] << 8 | data[1];
+ new_ensemble.al_flag = data[2] & 0x20;
+
+ if(ensemble != new_ensemble) {
+ ensemble = new_ensemble;
+
+ if (verbose)
+ etiLog.log(debug, "FICDecoder: EId 0x%04X: alarm flag: %s\n",
+ ensemble.eid, ensemble.al_flag ? "true" : "false");
+
+ UpdateEnsemble();
+ }
+}
+
+void FICDecoder::ProcessFIG0_1(const uint8_t *data, size_t len) {
+ // FIG 0/1 - Basic sub-channel organization
+
+ // iterate through all sub-channels
+ for(size_t offset = 0; offset < len;) {
+ int subchid = data[offset] >> 2;
+ size_t start_address = (data[offset] & 0x03) << 8 | data[offset + 1];
+ offset += 2;
+
+ FIC_SUBCHANNEL sc;
+ sc.start = start_address;
+
+ bool short_long_form = data[offset] & 0x80;
+ if(short_long_form) {
+ // long form
+ int option = (data[offset] & 0x70) >> 4;
+ int pl = (data[offset] & 0x0C) >> 2;
+ size_t subch_size = (data[offset] & 0x03) << 8 | data[offset + 1];
+
+ switch(option) {
+ case 0b000:
+ sc.size = subch_size;
+ sc.pl = "EEP " + std::to_string(pl + 1) + "-A";
+ sc.bitrate = subch_size / eep_a_size_factors[pl] * 8;
+ break;
+ case 0b001:
+ sc.size = subch_size;
+ sc.pl = "EEP " + std::to_string(pl + 1) + "-B";
+ sc.bitrate = subch_size / eep_b_size_factors[pl] * 32;
+ break;
+ }
+ offset += 2;
+ } else {
+ // short form
+
+ bool table_switch = data[offset] & 0x40;
+ if(!table_switch) {
+ int table_index = data[offset] & 0x3F;
+ sc.size = uep_sizes[table_index];
+ sc.pl = "UEP " + std::to_string(uep_pls[table_index]);
+ sc.bitrate = uep_bitrates[table_index];
+ }
+ offset++;
+ }
+
+ if(!sc.IsNone()) {
+ FIC_SUBCHANNEL& current_sc = GetSubchannel(subchid);
+ sc.language = current_sc.language; // ignored for comparison
+ if(current_sc != sc) {
+ current_sc = sc;
+
+ if (verbose)
+ etiLog.log(debug, "FICDecoder: SubChId %2d: start %3zu CUs, size %3zu CUs, PL %-7s = %3d kBit/s\n", subchid, sc.start, sc.size, sc.pl.c_str(), sc.bitrate);
+
+ UpdateSubchannel(subchid);
+ }
+ }
+ }
+}
+
+void FICDecoder::ProcessFIG0_2(const uint8_t *data, size_t len) {
+ // FIG 0/2 - Basic service and service component definition
+ // programme services only
+
+ // iterate through all services
+ for(size_t offset = 0; offset < len;) {
+ uint16_t sid = data[offset] << 8 | data[offset + 1];
+ offset += 2;
+
+ size_t num_service_comps = data[offset++] & 0x0F;
+
+ // iterate through all service components
+ for(size_t comp = 0; comp < num_service_comps; comp++) {
+ int tmid = data[offset] >> 6;
+
+ switch(tmid) {
+ case 0b00: // MSC stream audio
+ int ascty = data[offset] & 0x3F;
+ int subchid = data[offset + 1] >> 2;
+ bool ps = data[offset + 1] & 0x02;
+ bool ca = data[offset + 1] & 0x01;
+
+ if(!ca) {
+ switch(ascty) {
+ case 0: // DAB
+ case 63: // DAB+
+ bool dab_plus = ascty == 63;
+
+ AUDIO_SERVICE audio_service(subchid, dab_plus);
+
+ FIC_SERVICE& service = GetService(sid);
+ AUDIO_SERVICE& current_audio_service = service.audio_comps[subchid];
+ if(current_audio_service != audio_service || ps != (service.pri_comp_subchid == subchid)) {
+ current_audio_service = audio_service;
+ if(ps)
+ service.pri_comp_subchid = subchid;
+
+ if (verbose)
+ etiLog.log(debug, "FICDecoder: SId 0x%04X: audio service (SubChId %2d, %-4s, %s)\n", sid, subchid, dab_plus ? "DAB+" : "DAB", ps ? "primary" : "secondary");
+
+ UpdateService(service);
+ }
+
+ break;
+ }
+ }
+ }
+
+ offset += 2;
+ }
+ }
+}
+
+void FICDecoder::ProcessFIG0_5(const uint8_t *data, size_t len) {
+ // FIG 0/5 - Service component language
+ // programme services only
+
+ // iterate through all components
+ for(size_t offset = 0; offset < len;) {
+ bool ls_flag = data[offset] & 0x80;
+ if(ls_flag) {
+ // long form - skipped, as not relevant
+ offset += 3;
+ } else {
+ // short form
+ bool msc_fic_flag = data[offset] & 0x40;
+
+ // handle only MSC components
+ if(!msc_fic_flag) {
+ int subchid = data[offset] & 0x3F;
+ int language = data[offset + 1];
+
+ FIC_SUBCHANNEL& current_sc = GetSubchannel(subchid);
+ if(current_sc.language != language) {
+ current_sc.language = language;
+
+ if (verbose)
+ etiLog.log(debug, "FICDecoder: SubChId %2d: language '%s'\n", subchid, ConvertLanguageToString(language).c_str());
+
+ UpdateSubchannel(subchid);
+ }
+ }
+
+ offset += 2;
+ }
+ }
+}
+
+void FICDecoder::ProcessFIG0_8(const uint8_t *data, size_t len) {
+ // FIG 0/8 - Service component global definition
+ // programme services only
+
+ // iterate through all service components
+ for(size_t offset = 0; offset < len;) {
+ uint16_t sid = data[offset] << 8 | data[offset + 1];
+ offset += 2;
+
+ bool ext_flag = data[offset] & 0x80;
+ int scids = data[offset] & 0x0F;
+ offset++;
+
+ bool ls_flag = data[offset] & 0x80;
+ if(ls_flag) {
+ // long form - skipped, as not relevant
+ offset += 2;
+ } else {
+ // short form
+ bool msc_fic_flag = data[offset] & 0x40;
+
+ // handle only MSC components
+ if(!msc_fic_flag) {
+ int subchid = data[offset] & 0x3F;
+
+ FIC_SERVICE& service = GetService(sid);
+ bool new_comp = service.comp_defs.find(scids) == service.comp_defs.end();
+ int& current_subchid = service.comp_defs[scids];
+ if(new_comp || current_subchid != subchid) {
+ current_subchid = subchid;
+
+ if (verbose)
+ etiLog.log(debug, "FICDecoder: SId 0x%04X, SCIdS %2d: MSC service component (SubChId %2d)\n", sid, scids, subchid);
+
+ UpdateService(service);
+ }
+ }
+
+ offset++;
+ }
+
+ // skip Rfa field, if needed
+ if(ext_flag)
+ offset++;
+ }
+}
+
+void FICDecoder::ProcessFIG0_9(const uint8_t *data, size_t len) {
+ // FIG 0/9 - Time and country identifier - Country, LTO and International table
+ // ensemble ECC/LTO and international table ID only
+
+ if(len < 3)
+ return;
+
+ FIC_ENSEMBLE new_ensemble = ensemble;
+ new_ensemble.lto = (data[0] & 0x20 ? -1 : 1) * (data[0] & 0x1F);
+ new_ensemble.ecc = data[1];
+ new_ensemble.inter_table_id = data[2];
+
+ if(ensemble != new_ensemble) {
+ ensemble = new_ensemble;
+
+ if (verbose)
+ etiLog.log(debug, "FICDecoder: ECC: 0x%02X, LTO: %s, international table ID: 0x%02X (%s)\n",
+ ensemble.ecc, ConvertLTOToString(ensemble.lto).c_str(), ensemble.inter_table_id, ConvertInterTableIDToString(ensemble.inter_table_id).c_str());
+
+ UpdateEnsemble();
+
+ // update services that changes may affect
+ for(const fic_services_t::value_type& service : services) {
+ const FIC_SERVICE& s = service.second;
+ if(s.pty_static != FIC_SERVICE::pty_none || s.pty_dynamic != FIC_SERVICE::pty_none)
+ UpdateService(s);
+ }
+ }
+}
+
+void FICDecoder::ProcessFIG0_10(const uint8_t *data, size_t len) {
+ // FIG 0/10 - Date and time (d&t)
+
+ if(len < 4)
+ return;
+
+ FIC_DAB_DT new_utc_dt;
+
+ // ignore short form, once long form available
+ bool utc_flag = data[2] & 0x08;
+ if(!utc_flag && utc_dt_long)
+ return;
+
+ // retrieve date
+ int mjd = (data[0] & 0x7F) << 10 | data[1] << 2 | data[2] >> 6;
+
+ int y0 = floor((mjd - 15078.2) / 365.25);
+ int m0 = floor((mjd - 14956.1 - floor(y0 * 365.25)) / 30.6001);
+ int d = mjd - 14956 - floor(y0 * 365.25) - floor(m0 * 30.6001);
+ int k = (m0 == 14 || m0 == 15) ? 1 : 0;
+ int y = y0 + k;
+ int m = m0 - 1 - k * 12;
+
+ new_utc_dt.dt.tm_year = y; // from 1900
+ new_utc_dt.dt.tm_mon = m - 1; // 0-based
+ new_utc_dt.dt.tm_mday = d;
+
+ // retrieve time
+ new_utc_dt.dt.tm_hour = (data[2] & 0x07) << 2 | data[3] >> 6;
+ new_utc_dt.dt.tm_min = data[3] & 0x3F;
+ new_utc_dt.dt.tm_isdst = -1; // ignore DST
+ if(utc_flag) {
+ // long form
+ if(len < 6)
+ return;
+ new_utc_dt.dt.tm_sec = data[4] >> 2;
+ new_utc_dt.ms = (data[4] & 0x03) << 8 | data[5];
+ utc_dt_long = true;
+ } else {
+ // short form
+ new_utc_dt.dt.tm_sec = 0;
+ new_utc_dt.ms = FIC_DAB_DT::ms_none;
+ }
+
+ if(utc_dt != new_utc_dt) {
+ // print only once (or once again on precision change)
+ if(utc_dt.IsNone() || utc_dt.IsMsNone() != new_utc_dt.IsMsNone())
+ if (verbose)
+ etiLog.log(debug, "FICDecoder: UTC date/time: %s\n", ConvertDateTimeToString(new_utc_dt, 0, true).c_str());
+
+ utc_dt = new_utc_dt;
+
+ observer.FICChangeUTCDateTime(utc_dt);
+ }
+}
+
+void FICDecoder::ProcessFIG0_13(const uint8_t *data, size_t len) {
+ // FIG 0/13 - User application information
+ // programme services only
+
+ // iterate through all service components
+ for(size_t offset = 0; offset < len;) {
+ uint16_t sid = data[offset] << 8 | data[offset + 1];
+ offset += 2;
+
+ int scids = data[offset] >> 4;
+ size_t num_scids_uas = data[offset] & 0x0F;
+ offset++;
+
+ // iterate through all user applications
+ for(size_t scids_ua = 0; scids_ua < num_scids_uas; scids_ua++) {
+ int ua_type = data[offset] << 3 | data[offset + 1] >> 5;
+ size_t ua_data_length = data[offset + 1] & 0x1F;
+ offset += 2;
+
+ // handle only Slideshow
+ if(ua_type == 0x002) {
+ FIC_SERVICE& service = GetService(sid);
+ if(service.comp_sls_uas.find(scids) == service.comp_sls_uas.end()) {
+ ua_data_t& sls_ua_data = service.comp_sls_uas[scids];
+
+ sls_ua_data.resize(ua_data_length);
+ if(ua_data_length)
+ memcpy(&sls_ua_data[0], data + offset, ua_data_length);
+
+ if (verbose)
+ etiLog.log(debug, "FICDecoder: SId 0x%04X, SCIdS %2d: Slideshow (%zu bytes UA data)\n", sid, scids, ua_data_length);
+
+ UpdateService(service);
+ }
+ }
+
+ offset += ua_data_length;
+ }
+ }
+}
+
+void FICDecoder::ProcessFIG0_17(const uint8_t *data, size_t len) {
+ // FIG 0/17 - Programme Type
+ // programme type only
+
+ // iterate through all services
+ for(size_t offset = 0; offset < len;) {
+ uint16_t sid = data[offset] << 8 | data[offset + 1];
+ bool sd = data[offset + 2] & 0x80;
+ bool l_flag = data[offset + 2] & 0x20;
+ bool cc_flag = data[offset + 2] & 0x10;
+ offset += 3;
+
+ // skip language, if present
+ if(l_flag)
+ offset++;
+
+ // programme type (international code)
+ int pty = data[offset] & 0x1F;
+ offset++;
+
+ // skip CC part, if present
+ if(cc_flag)
+ offset++;
+
+ FIC_SERVICE& service = GetService(sid);
+ int& current_pty = sd ? service.pty_dynamic : service.pty_static;
+ if(current_pty != pty) {
+ // suppress message, if dynamic FIC messages disabled and dynamic PTY not initally be set
+ bool show_msg = !(sd && current_pty != FIC_SERVICE::pty_none);
+
+ current_pty = pty;
+
+ if(verbose && show_msg) {
+ // assuming international table ID 0x01 here!
+ etiLog.log(debug, "FICDecoder: SId 0x%04X: programme type (%s): '%s'\n",
+ sid, sd ? "dynamic" : "static", ConvertPTYToString(pty, 0x01).c_str());
+ }
+
+ UpdateService(service);
+ }
+ }
+}
+
+void FICDecoder::ProcessFIG0_18(const uint8_t *data, size_t len) {
+ // FIG 0/18 - Announcement support
+
+ // iterate through all services
+ for(size_t offset = 0; offset < len;) {
+ uint16_t sid = data[offset] << 8 | data[offset + 1];
+ uint16_t asu_flags = data[offset + 2] << 8 | data[offset + 3];
+ size_t number_of_clusters = data[offset + 4] & 0x1F;
+ offset += 5;
+
+ cids_t cids;
+ for(size_t i = 0; i < number_of_clusters; i++)
+ cids.emplace(data[offset++]);
+
+ FIC_SERVICE& service = GetService(sid);
+ uint16_t& current_asu_flags = service.asu_flags;
+ cids_t& current_cids = service.cids;
+ if(current_asu_flags != asu_flags || current_cids != cids) {
+ current_asu_flags = asu_flags;
+ current_cids = cids;
+
+ std::string cids_str;
+ char cid_string[5];
+ for(const cids_t::value_type& cid : cids) {
+ if(!cids_str.empty())
+ cids_str += "/";
+ snprintf(cid_string, sizeof(cid_string), "0x%02X", cid);
+ cids_str += std::string(cid_string);
+ }
+
+ if (verbose)
+ etiLog.log(debug, "FICDecoder: SId 0x%04X: ASu flags 0x%04X, cluster(s) %s\n",
+ sid, asu_flags, cids_str.c_str());
+
+ UpdateService(service);
+ }
+ }
+}
+
+void FICDecoder::ProcessFIG0_19(const uint8_t *data, size_t len) {
+ // FIG 0/19 - Announcement switching
+
+ // iterate through all announcement clusters
+ for(size_t offset = 0; offset < len;) {
+ uint8_t cid = data[offset];
+ uint16_t asw_flags = data[offset + 1] << 8 | data[offset + 2];
+ bool region_flag = data[offset + 3] & 0x40;
+ int subchid = data[offset + 3] & 0x3F;
+ offset += region_flag ? 5 : 4;
+
+ FIC_ASW_CLUSTER ac;
+ ac.asw_flags = asw_flags;
+ ac.subchid = subchid;
+
+ FIC_ASW_CLUSTER& current_ac = ensemble.asw_clusters[cid];
+ if(current_ac != ac) {
+ current_ac = ac;
+
+ if (verbose) {
+ etiLog.log(debug, "FICDecoder: ASw cluster 0x%02X: flags 0x%04X, SubChId %2d\n",
+ cid, asw_flags, subchid);
+ }
+
+ UpdateEnsemble();
+
+ // update services that changes may affect
+ for(const fic_services_t::value_type& service : services) {
+ const FIC_SERVICE& s = service.second;
+ if(s.cids.find(cid) != s.cids.cend())
+ UpdateService(s);
+ }
+ }
+ }
+}
+
+void FICDecoder::ProcessFIG1(const uint8_t *data, size_t len) {
+ if(len < 1) {
+ etiLog.log(warn, "FICDecoder: received empty FIG 1\n");
+ return;
+ }
+
+ // read/skip FIG 1 header
+ FIG1_HEADER header(data[0]);
+ data++;
+ len--;
+
+ // ignore other ensembles
+ if(header.oe)
+ return;
+
+ // check for (un)supported extension + set ID field len
+ size_t len_id = -1;
+ switch(header.extension) {
+ case 0: // ensemble
+ case 1: // programme service
+ len_id = 2;
+ break;
+ case 4: // service component
+ // programme services only (P/D = 0)
+ if(data[0] & 0x80)
+ return;
+ len_id = 3;
+ break;
+ default:
+ // etiLog.log(debug, "FICDecoder: received unsupported FIG 1/%d with %zu field bytes\n", header.extension, len);
+ return;
+ }
+
+ // check length
+ size_t len_calced = len_id + 16 + 2;
+ if(len != len_calced) {
+ etiLog.log(warn, "FICDecoder: received FIG 1/%d having %zu field bytes (expected: %zu)\n", header.extension, len, len_calced);
+ return;
+ }
+
+ // parse actual label data
+ FIC_LABEL label;
+ label.charset = header.charset;
+ memcpy(label.label, data + len_id, 16);
+ label.short_label_mask = data[len_id + 16] << 8 | data[len_id + 17];
+
+
+ // handle extension
+ switch(header.extension) {
+ case 0: { // ensemble
+ uint16_t eid = data[0] << 8 | data[1];
+ ProcessFIG1_0(eid, label);
+ break; }
+ case 1: { // programme service
+ uint16_t sid = data[0] << 8 | data[1];
+ ProcessFIG1_1(sid, label);
+ break; }
+ case 4: { // service component
+ int scids = data[0] & 0x0F;
+ uint16_t sid = data[1] << 8 | data[2];
+ ProcessFIG1_4(sid, scids, label);
+ break; }
+ }
+}
+
+void FICDecoder::ProcessFIG1_0(uint16_t eid, const FIC_LABEL& label) {
+ if(ensemble.label != label) {
+ ensemble.label = label;
+
+ std::string label_str = ConvertLabelToUTF8(label, nullptr);
+ std::string short_label_str = DeriveShortLabelUTF8(label_str, label.short_label_mask);
+ if (verbose)
+ etiLog.log(debug, "FICDecoder: EId 0x%04X: ensemble label '" "\x1B[32m" "%s" "\x1B[0m" "' ('" "\x1B[32m" "%s" "\x1B[0m" "')\n",
+ eid, label_str.c_str(), short_label_str.c_str());
+
+ UpdateEnsemble();
+ }
+}
+
+void FICDecoder::ProcessFIG1_1(uint16_t sid, const FIC_LABEL& label) {
+ FIC_SERVICE& service = GetService(sid);
+ if(service.label != label) {
+ service.label = label;
+
+ if (verbose) {
+ std::string label_str = ConvertLabelToUTF8(label, nullptr);
+ std::string short_label_str = DeriveShortLabelUTF8(label_str, label.short_label_mask);
+ etiLog.log(debug, "FICDecoder: SId 0x%04X: programme service label '" "\x1B[32m" "%s" "\x1B[0m" "' ('" "\x1B[32m" "%s" "\x1B[0m" "')\n",
+ sid, label_str.c_str(), short_label_str.c_str());
+ }
+
+ UpdateService(service);
+ }
+}
+
+void FICDecoder::ProcessFIG1_4(uint16_t sid, int scids, const FIC_LABEL& label) {
+ // programme services only
+
+ FIC_SERVICE& service = GetService(sid);
+ FIC_LABEL& comp_label = service.comp_labels[scids];
+ if(comp_label != label) {
+ comp_label = label;
+
+ if (verbose) {
+ std::string label_str = ConvertLabelToUTF8(label, nullptr);
+ std::string short_label_str = DeriveShortLabelUTF8(label_str, label.short_label_mask);
+ etiLog.log(debug, "FICDecoder: SId 0x%04X, SCIdS %2d: service component label '" "\x1B[32m" "%s" "\x1B[0m" "' ('" "\x1B[32m" "%s" "\x1B[0m" "')\n",
+ sid, scids, label_str.c_str(), short_label_str.c_str());
+ }
+
+ UpdateService(service);
+ }
+}
+
+FIC_SUBCHANNEL& FICDecoder::GetSubchannel(int subchid) {
+ // created automatically, if not yet existing
+ return subchannels[subchid];
+}
+
+void FICDecoder::UpdateSubchannel(int subchid) {
+ // update services that consist of this sub-channel
+ for(const fic_services_t::value_type& service : services) {
+ const FIC_SERVICE& s = service.second;
+ if(s.audio_comps.find(subchid) != s.audio_comps.end())
+ UpdateService(s);
+ }
+}
+
+FIC_SERVICE& FICDecoder::GetService(uint16_t sid) {
+ FIC_SERVICE& result = services[sid]; // created, if not yet existing
+
+ // if new service, set SID
+ if(result.IsNone())
+ result.sid = sid;
+ return result;
+}
+
+void FICDecoder::UpdateService(const FIC_SERVICE& service) {
+ // abort update, if primary component or label not yet present
+ if(service.HasNoPriCompSubchid() || service.label.IsNone())
+ return;
+
+ // secondary components (if both component and definition are present)
+ bool multi_comps = false;
+ for(const comp_defs_t::value_type& comp_def : service.comp_defs) {
+ if(comp_def.second == service.pri_comp_subchid || service.audio_comps.find(comp_def.second) == service.audio_comps.end())
+ continue;
+ UpdateListedService(service, comp_def.first, true);
+ multi_comps = true;
+ }
+
+ // primary component
+ UpdateListedService(service, LISTED_SERVICE::scids_none, multi_comps);
+}
+
+void FICDecoder::UpdateListedService(const FIC_SERVICE& service, int scids, bool multi_comps) {
+ // assemble listed service
+ LISTED_SERVICE ls;
+ ls.sid = service.sid;
+ ls.scids = scids;
+ ls.label = service.label;
+ ls.pty_static = service.pty_static;
+ ls.pty_dynamic = service.pty_dynamic;
+ ls.asu_flags = service.asu_flags;
+ ls.cids = service.cids;
+ ls.pri_comp_subchid = service.pri_comp_subchid;
+ ls.multi_comps = multi_comps;
+
+ if(scids == LISTED_SERVICE::scids_none) { // primary component
+ ls.audio_service = service.audio_comps.at(service.pri_comp_subchid);
+ } else { // secondary component
+ ls.audio_service = service.audio_comps.at(service.comp_defs.at(scids));
+
+ // use component label, if available
+ comp_labels_t::const_iterator cl_it = service.comp_labels.find(scids);
+ if(cl_it != service.comp_labels.end())
+ ls.label = cl_it->second;
+ }
+
+ // use sub-channel information, if available
+ fic_subchannels_t::const_iterator sc_it = subchannels.find(ls.audio_service.subchid);
+ if(sc_it != subchannels.end())
+ ls.subchannel = sc_it->second;
+
+ /* check (for) Slideshow; currently only supported in X-PAD
+ * - derive the required SCIdS (if not yet known)
+ * - derive app type from UA data (if present)
+ */
+ int sls_scids = scids;
+ if(sls_scids == LISTED_SERVICE::scids_none) {
+ for(const comp_defs_t::value_type& comp_def : service.comp_defs) {
+ if(comp_def.second == ls.audio_service.subchid) {
+ sls_scids = comp_def.first;
+ break;
+ }
+ }
+ }
+ if(sls_scids != LISTED_SERVICE::scids_none && service.comp_sls_uas.find(sls_scids) != service.comp_sls_uas.end())
+ ls.sls_app_type = GetSLSAppType(service.comp_sls_uas.at(sls_scids));
+
+ // forward to observer
+ observer.FICChangeService(ls);
+}
+
+int FICDecoder::GetSLSAppType(const ua_data_t& ua_data) {
+ // default values, if no UA data present
+ bool ca_flag = false;
+ int xpad_app_type = 12;
+ bool dg_flag = false;
+ int dscty = 60; // MOT
+
+ // if UA data present, parse X-PAD data
+ if(ua_data.size() >= 2) {
+ ca_flag = ua_data[0] & 0x80;
+ xpad_app_type = ua_data[0] & 0x1F;
+ dg_flag = ua_data[1] & 0x80;
+ dscty = ua_data[1] & 0x3F;
+ }
+
+ // if no CA is used, but DGs and MOT, enable Slideshow
+ if(!ca_flag && !dg_flag && dscty == 60)
+ return xpad_app_type;
+ else
+ return LISTED_SERVICE::sls_app_type_none;
+}
+
+void FICDecoder::UpdateEnsemble() {
+ // abort update, if EId or label not yet present
+ if(ensemble.IsNone() || ensemble.label.IsNone())
+ return;
+
+ // forward to observer
+ observer.FICChangeEnsemble(ensemble);
+}
+
+std::string FICDecoder::ConvertLabelToUTF8(const FIC_LABEL& label, std::string* charset_name) {
+ std::string result = CharsetTools::ConvertTextToUTF8(label.label, sizeof(label.label), label.charset, charset_name);
+
+ // discard trailing spaces
+ size_t last_pos = result.find_last_not_of(' ');
+ if(last_pos != std::string::npos)
+ result.resize(last_pos + 1);
+
+ return result;
+}
+
+const size_t FICDecoder::uep_sizes[] = {
+ 16, 21, 24, 29, 35, 24, 29, 35, 42, 52, 29, 35, 42, 52, 32, 42,
+ 48, 58, 70, 40, 52, 58, 70, 84, 48, 58, 70, 84, 104, 58, 70, 84,
+ 104, 64, 84, 96, 116, 140, 80, 104, 116, 140, 168, 96, 116, 140, 168, 208,
+ 116, 140, 168, 208, 232, 128, 168, 192, 232, 280, 160, 208, 280, 192, 280, 416
+};
+const int FICDecoder::uep_pls[] = {
+ 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 5, 4,
+ 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3,
+ 2, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1,
+ 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 2, 5, 3, 1
+};
+const int FICDecoder::uep_bitrates[] = {
+ 32, 32, 32, 32, 32, 48, 48, 48, 48, 48, 56, 56, 56, 56, 64, 64,
+ 64, 64, 64, 80, 80, 80, 80, 80, 96, 96, 96, 96, 96, 112, 112, 112,
+ 112, 128, 128, 128, 128, 128, 160, 160, 160, 160, 160, 192, 192, 192, 192, 192,
+ 224, 224, 224, 224, 224, 256, 256, 256, 256, 256, 320, 320, 320, 384, 384, 384
+};
+const int FICDecoder::eep_a_size_factors[] = {12, 8, 6, 4};
+const int FICDecoder::eep_b_size_factors[] = {27, 21, 18, 15};
+
+const char* FICDecoder::languages_0x00_to_0x2B[] = {
+ "unknown/not applicable", "Albanian", "Breton", "Catalan", "Croatian", "Welsh", "Czech", "Danish",
+ "German", "English", "Spanish", "Esperanto", "Estonian", "Basque", "Faroese", "French",
+ "Frisian", "Irish", "Gaelic", "Galician", "Icelandic", "Italian", "Sami", "Latin",
+ "Latvian", "Luxembourgian", "Lithuanian", "Hungarian", "Maltese", "Dutch", "Norwegian", "Occitan",
+ "Polish", "Portuguese", "Romanian", "Romansh", "Serbian", "Slovak", "Slovene", "Finnish",
+ "Swedish", "Turkish", "Flemish", "Walloon"
+};
+const char* FICDecoder::languages_0x7F_downto_0x45[] = {
+ "Amharic", "Arabic", "Armenian", "Assamese", "Azerbaijani", "Bambora", "Belorussian", "Bengali",
+ "Bulgarian", "Burmese", "Chinese", "Chuvash", "Dari", "Fulani", "Georgian", "Greek",
+ "Gujurati", "Gurani", "Hausa", "Hebrew", "Hindi", "Indonesian", "Japanese", "Kannada",
+ "Kazakh", "Khmer", "Korean", "Laotian", "Macedonian", "Malagasay", "Malaysian", "Moldavian",
+ "Marathi", "Ndebele", "Nepali", "Oriya", "Papiamento", "Persian", "Punjabi", "Pushtu",
+ "Quechua", "Russian", "Rusyn", "Serbo-Croat", "Shona", "Sinhalese", "Somali", "Sranan Tongo",
+ "Swahili", "Tadzhik", "Tamil", "Tatar", "Telugu", "Thai", "Ukranian", "Urdu",
+ "Uzbek", "Vietnamese", "Zulu"
+};
+
+const char* FICDecoder::ptys_rds_0x00_to_0x1D[] = {
+ "No programme type", "News", "Current Affairs", "Information",
+ "Sport", "Education", "Drama", "Culture",
+ "Science", "Varied", "Pop Music", "Rock Music",
+ "Easy Listening Music", "Light Classical", "Serious Classical", "Other Music",
+ "Weather/meteorology", "Finance/Business", "Children's programmes", "Social Affairs",
+ "Religion", "Phone In", "Travel", "Leisure",
+ "Jazz Music", "Country Music", "National Music", "Oldies Music",
+ "Folk Music", "Documentary"
+};
+const char* FICDecoder::ptys_rbds_0x00_to_0x1D[] = {
+ "No program type", "News", "Information", "Sports",
+ "Talk", "Rock", "Classic Rock", "Adult Hits",
+ "Soft Rock", "Top 40", "Country", "Oldies",
+ "Soft", "Nostalgia", "Jazz", "Classical",
+ "Rhythm and Blues", "Soft Rhythm and Blues", "Foreign Language", "Religious Music",
+ "Religious Talk", "Personality", "Public", "College",
+ "(rfu)", "(rfu)", "(rfu)", "(rfu)",
+ "(rfu)", "Weather"
+};
+
+const char* FICDecoder::asu_types_0_to_10[] = {
+ "Alarm", "Road Traffic flash", "Transport flash", "Warning/Service",
+ "News flash", "Area weather flash", "Event announcement", "Special event",
+ "Programme Information", "Sport report", "Financial report"
+};
+
+std::string FICDecoder::ConvertLanguageToString(const int value) {
+ if(value >= 0x00 && value <= 0x2B)
+ return languages_0x00_to_0x2B[value];
+ if(value == 0x40)
+ return "background sound/clean feed";
+ if(value >= 0x45 && value <= 0x7F)
+ return languages_0x7F_downto_0x45[0x7F - value];
+ return "unknown (" + std::to_string(value) + ")";
+}
+
+std::string FICDecoder::ConvertLTOToString(const int value) {
+ // just to silence recent GCC's truncation warnings
+ int lto_value = value % 0x3F;
+
+ char lto_string[7];
+ snprintf(lto_string, sizeof(lto_string), "%+03d:%02d", lto_value / 2, (lto_value % 2) ? 30 : 0);
+ return lto_string;
+}
+
+std::string FICDecoder::ConvertInterTableIDToString(const int value) {
+ switch(value) {
+ case 0x01:
+ return "RDS PTY";
+ case 0x02:
+ return "RBDS PTY";
+ default:
+ return "unknown";
+ }
+}
+
+std::string FICDecoder::ConvertDateTimeToString(FIC_DAB_DT utc_dt, const int lto, bool output_ms) {
+ const char* weekdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
+
+ // if desired, apply LTO
+ if(lto)
+ utc_dt.dt.tm_min += lto * 30;
+
+ // normalize time (apply LTO, set day of week)
+ if(mktime(&utc_dt.dt) == (time_t) -1)
+ throw std::runtime_error("FICDecoder: error while normalizing date/time");
+
+ std::string result;
+ char s[11];
+
+ strftime(s, sizeof(s), "%F", &utc_dt.dt);
+ result += std::string(s) + ", " + weekdays[utc_dt.dt.tm_wday] + " - ";
+
+ if(!utc_dt.IsMsNone()) {
+ // long form
+ strftime(s, sizeof(s), "%T", &utc_dt.dt);
+ result += s;
+ if(output_ms) {
+ snprintf(s, sizeof(s), ".%03d", utc_dt.ms);
+ result += s;
+ }
+ } else {
+ // short form
+ strftime(s, sizeof(s), "%R", &utc_dt.dt);
+ result += s;
+ }
+
+ return result;
+}
+
+std::string FICDecoder::ConvertPTYToString(const int value, const int inter_table_id) {
+ switch(inter_table_id) {
+ case 0x01:
+ return value <= 0x1D ? ptys_rds_0x00_to_0x1D[value] : "(not used)";
+ case 0x02:
+ return value <= 0x1D ? ptys_rbds_0x00_to_0x1D[value] : "(not used)";
+ default:
+ return "(unknown)";
+ }
+}
+
+std::string FICDecoder::ConvertASuTypeToString(const int value) {
+ if(value >= 0 && value <= 10)
+ return asu_types_0_to_10[value];
+ return "unknown (" + std::to_string(value) + ")";
+}
+
+std::string FICDecoder::DeriveShortLabelUTF8(const std::string& long_label, uint16_t short_label_mask) {
+ std::string short_label;
+
+ for(size_t i = 0; i < long_label.length(); i++) // consider discarded trailing spaces
+ if(short_label_mask & (0x8000 >> i))
+ short_label += StringTools::UTF8Substr(long_label, i, 1);
+
+ return short_label;
+}
diff --git a/src/FigParser.h b/src/FigParser.h
new file mode 100644
index 0000000..b241123
--- /dev/null
+++ b/src/FigParser.h
@@ -0,0 +1,385 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
+ the Queen in Right of Canada (Communications Research Center Canada)
+
+ Most parts of this file are taken from dablin,
+ Copyright (C) 2015-2022 Stefan Pöschel
+
+ Copyright (C) 2023
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+ */
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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, either version 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <map>
+#include <set>
+#include <vector>
+#include <optional>
+#include <stdexcept>
+#include <string>
+#include <cmath>
+#include <ctime>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+
+struct FIG0_HEADER {
+ bool cn;
+ bool oe;
+ bool pd;
+ int extension;
+
+ FIG0_HEADER(uint8_t data) : cn(data & 0x80), oe(data & 0x40), pd(data & 0x20), extension(data & 0x1F) {}
+};
+
+struct FIG1_HEADER {
+ int charset;
+ bool oe;
+ int extension;
+
+ FIG1_HEADER(uint8_t data) : charset(data >> 4), oe(data & 0x08), extension(data & 0x07) {}
+};
+
+struct FIC_LABEL {
+ int charset;
+ uint8_t label[16];
+ uint16_t short_label_mask;
+
+ static const int charset_none = -1;
+ bool IsNone() const {return charset == charset_none;}
+
+ FIC_LABEL() : charset(charset_none), short_label_mask(0x0000) {
+ memset(label, 0x00, sizeof(label));
+ }
+
+ bool operator==(const FIC_LABEL & fic_label) const {
+ return charset == fic_label.charset && !memcmp(label, fic_label.label, sizeof(label)) && short_label_mask == fic_label.short_label_mask;
+ }
+ bool operator!=(const FIC_LABEL & fic_label) const {
+ return !(*this == fic_label);
+ }
+};
+
+struct FIC_SUBCHANNEL {
+ size_t start;
+ size_t size;
+ std::string pl;
+ int bitrate;
+ int language;
+
+ static const int language_none = -1;
+ bool IsNone() const {return pl.empty() && language == language_none;}
+
+ FIC_SUBCHANNEL() : start(0), size(0), bitrate(-1), language(language_none) {}
+
+ bool operator==(const FIC_SUBCHANNEL & fic_subchannel) const {
+ return
+ start == fic_subchannel.start &&
+ size == fic_subchannel.size &&
+ pl == fic_subchannel.pl &&
+ bitrate == fic_subchannel.bitrate &&
+ language == fic_subchannel.language;
+ }
+ bool operator!=(const FIC_SUBCHANNEL & fic_subchannel) const {
+ return !(*this == fic_subchannel);
+ }
+};
+
+struct FIC_ASW_CLUSTER {
+ uint16_t asw_flags;
+ int subchid;
+
+ static const int asw_flags_none = 0x0000;
+
+ static const int subchid_none = -1;
+ bool IsNone() const {return subchid == subchid_none;}
+
+ FIC_ASW_CLUSTER() : asw_flags(asw_flags_none), subchid(subchid_none) {}
+
+ bool operator==(const FIC_ASW_CLUSTER & fic_asw_cluster) const {
+ return asw_flags == fic_asw_cluster.asw_flags && subchid == fic_asw_cluster.subchid;
+ }
+ bool operator!=(const FIC_ASW_CLUSTER & fic_asw_cluster) const {
+ return !(*this == fic_asw_cluster);
+ }
+};
+
+typedef std::map<uint8_t,FIC_ASW_CLUSTER> asw_clusters_t;
+
+struct FIC_DAB_DT {
+ struct tm dt;
+ int ms;
+
+ static const int none = -1;
+ bool IsNone() const {return dt.tm_year == none;}
+
+ static const int ms_none = -1;
+ bool IsMsNone() const {return ms == ms_none;}
+
+ FIC_DAB_DT() : ms(ms_none) {
+ dt.tm_year = none;
+ }
+
+ bool operator==(const FIC_DAB_DT & fic_dab_dt) const {
+ return
+ ms == fic_dab_dt.ms &&
+ dt.tm_sec == fic_dab_dt.dt.tm_sec &&
+ dt.tm_min == fic_dab_dt.dt.tm_min &&
+ dt.tm_hour == fic_dab_dt.dt.tm_hour &&
+ dt.tm_mday == fic_dab_dt.dt.tm_mday &&
+ dt.tm_mon == fic_dab_dt.dt.tm_mon &&
+ dt.tm_year == fic_dab_dt.dt.tm_year;
+ }
+ bool operator!=(const FIC_DAB_DT & fic_dab_dt) const {
+ return !(*this == fic_dab_dt);
+ }
+};
+
+struct FIC_ENSEMBLE {
+ int eid;
+ bool al_flag;
+ FIC_LABEL label;
+ int ecc;
+ int lto;
+ int inter_table_id;
+ asw_clusters_t asw_clusters;
+
+ static const int eid_none = -1;
+ bool IsNone() const {return eid == eid_none;}
+
+ static const int ecc_none = -1;
+ static const int lto_none = -100;
+ static const int inter_table_id_none = -1;
+
+ FIC_ENSEMBLE() :
+ eid(eid_none),
+ al_flag(false),
+ ecc(ecc_none),
+ lto(lto_none),
+ inter_table_id(inter_table_id_none)
+ {}
+
+ bool operator==(const FIC_ENSEMBLE & ensemble) const {
+ return
+ eid == ensemble.eid &&
+ al_flag == ensemble.al_flag &&
+ label == ensemble.label &&
+ ecc == ensemble.ecc &&
+ lto == ensemble.lto &&
+ inter_table_id == ensemble.inter_table_id &&
+ asw_clusters == ensemble.asw_clusters;
+ }
+ bool operator!=(const FIC_ENSEMBLE & ensemble) const {
+ return !(*this == ensemble);
+ }
+};
+
+struct AUDIO_SERVICE {
+ int subchid;
+ bool dab_plus;
+
+ static const int subchid_none = -1;
+ bool IsNone() const {return subchid == subchid_none;}
+
+ AUDIO_SERVICE() : AUDIO_SERVICE(subchid_none, false) {}
+ AUDIO_SERVICE(int subchid, bool dab_plus) : subchid(subchid), dab_plus(dab_plus) {}
+
+ bool operator==(const AUDIO_SERVICE & audio_service) const {
+ return subchid == audio_service.subchid && dab_plus == audio_service.dab_plus;
+ }
+ bool operator!=(const AUDIO_SERVICE & audio_service) const {
+ return !(*this == audio_service);
+ }
+};
+
+typedef std::map<int,AUDIO_SERVICE> audio_comps_t;
+typedef std::map<int,int> comp_defs_t;
+typedef std::map<int,FIC_LABEL> comp_labels_t;
+typedef std::vector<uint8_t> ua_data_t;
+typedef std::map<int,ua_data_t> comp_sls_uas_t;
+typedef std::set<uint8_t> cids_t;
+
+struct FIC_SERVICE {
+ int sid;
+ int pri_comp_subchid;
+ FIC_LABEL label;
+ int pty_static;
+ int pty_dynamic;
+ uint16_t asu_flags;
+ cids_t cids;
+
+ // components
+ audio_comps_t audio_comps; // from FIG 0/2 : SubChId -> AUDIO_SERVICE
+ comp_defs_t comp_defs; // from FIG 0/8 : SCIdS -> SubChId
+ comp_labels_t comp_labels; // from FIG 1/4 : SCIdS -> FIC_LABEL
+ comp_sls_uas_t comp_sls_uas; // from FIG 0/13: SCIdS -> UA data
+
+ static const int sid_none = -1;
+ bool IsNone() const {return sid == sid_none;}
+
+ static const int pri_comp_subchid_none = -1;
+ bool HasNoPriCompSubchid() const {return pri_comp_subchid == pri_comp_subchid_none;}
+
+ static const int pty_none = -1;
+
+ static const int asu_flags_none = 0x0000;
+
+ FIC_SERVICE() : sid(sid_none), pri_comp_subchid(pri_comp_subchid_none), pty_static(pty_none), pty_dynamic(pty_none), asu_flags(asu_flags_none) {}
+};
+
+struct LISTED_SERVICE {
+ int sid;
+ int scids;
+ FIC_SUBCHANNEL subchannel;
+ AUDIO_SERVICE audio_service;
+ FIC_LABEL label;
+ int pty_static;
+ int pty_dynamic;
+ int sls_app_type;
+ uint16_t asu_flags;
+ cids_t cids;
+
+ int pri_comp_subchid; // only used for sorting
+ bool multi_comps;
+
+ static const int sid_none = -1;
+ bool IsNone() const {return sid == sid_none;}
+
+ static const int scids_none = -1;
+ bool IsPrimary() const {return scids == scids_none;}
+
+ static const int pty_none = -1;
+
+ static const int asu_flags_none = 0x0000;
+
+ static const int sls_app_type_none = -1;
+ bool HasSLS() const {return sls_app_type != sls_app_type_none;}
+
+ LISTED_SERVICE() :
+ sid(sid_none),
+ scids(scids_none),
+ pty_static(pty_none),
+ pty_dynamic(pty_none),
+ sls_app_type(sls_app_type_none),
+ asu_flags(asu_flags_none),
+ pri_comp_subchid(AUDIO_SERVICE::subchid_none),
+ multi_comps(false)
+ {}
+
+ bool operator<(const LISTED_SERVICE & service) const {
+ if(pri_comp_subchid != service.pri_comp_subchid)
+ return pri_comp_subchid < service.pri_comp_subchid;
+ if(sid != service.sid)
+ return sid < service.sid;
+ return scids < service.scids;
+ }
+};
+
+typedef std::map<uint16_t, FIC_SERVICE> fic_services_t;
+typedef std::map<int, FIC_SUBCHANNEL> fic_subchannels_t;
+
+// --- FICDecoderObserver -----------------------------------------------------------------
+class FICDecoderObserver {
+ public:
+ virtual ~FICDecoderObserver() {}
+
+ std::optional<FIC_ENSEMBLE> ensemble;
+ std::optional<FIC_DAB_DT> utc_dt;
+ std::map<int /*SId*/, LISTED_SERVICE> services;
+
+ virtual void FICChangeEnsemble(const FIC_ENSEMBLE& ensemble);
+ virtual void FICChangeService(const LISTED_SERVICE& service);
+ virtual void FICChangeUTCDateTime(const FIC_DAB_DT& utc_dt);
+
+ virtual void FICDiscardedFIB() {}
+};
+
+
+// --- FICDecoder -----------------------------------------------------------------
+class FICDecoder {
+ private:
+ bool verbose;
+
+ void ProcessFIB(const uint8_t *data);
+
+ void ProcessFIG0(const uint8_t *data, size_t len);
+ void ProcessFIG0_0(const uint8_t *data, size_t len);
+ void ProcessFIG0_1(const uint8_t *data, size_t len);
+ void ProcessFIG0_2(const uint8_t *data, size_t len);
+ void ProcessFIG0_5(const uint8_t *data, size_t len);
+ void ProcessFIG0_8(const uint8_t *data, size_t len);
+ void ProcessFIG0_9(const uint8_t *data, size_t len);
+ void ProcessFIG0_10(const uint8_t *data, size_t len);
+ void ProcessFIG0_13(const uint8_t *data, size_t len);
+ void ProcessFIG0_17(const uint8_t *data, size_t len);
+ void ProcessFIG0_18(const uint8_t *data, size_t len);
+ void ProcessFIG0_19(const uint8_t *data, size_t len);
+
+ void ProcessFIG1(const uint8_t *data, size_t len);
+ void ProcessFIG1_0(uint16_t eid, const FIC_LABEL& label);
+ void ProcessFIG1_1(uint16_t sid, const FIC_LABEL& label);
+ void ProcessFIG1_4(uint16_t sid, int scids, const FIC_LABEL& label);
+
+ FIC_SUBCHANNEL& GetSubchannel(int subchid);
+ void UpdateSubchannel(int subchid);
+ FIC_SERVICE& GetService(uint16_t sid);
+ void UpdateService(const FIC_SERVICE& service);
+ void UpdateListedService(const FIC_SERVICE& service, int scids, bool multi_comps);
+ int GetSLSAppType(const ua_data_t& ua_data);
+
+ FIC_ENSEMBLE ensemble;
+ void UpdateEnsemble();
+
+ fic_services_t services;
+ fic_subchannels_t subchannels; // from FIG 0/1: SubChId -> FIC_SUBCHANNEL
+
+ FIC_DAB_DT utc_dt;
+ bool utc_dt_long;
+
+ static const size_t uep_sizes[];
+ static const int uep_pls[];
+ static const int uep_bitrates[];
+ static const int eep_a_size_factors[];
+ static const int eep_b_size_factors[];
+
+ static const char* languages_0x00_to_0x2B[];
+ static const char* languages_0x7F_downto_0x45[];
+
+ static const char* ptys_rds_0x00_to_0x1D[];
+ static const char* ptys_rbds_0x00_to_0x1D[];
+
+ static const char* asu_types_0_to_10[];
+ public:
+ FICDecoder(bool verbose);
+ void Process(const uint8_t *data, size_t len);
+ void Reset();
+
+ FICDecoderObserver observer;
+
+ static std::string ConvertLabelToUTF8(const FIC_LABEL& label, std::string* charset_name);
+ static std::string ConvertLanguageToString(const int value);
+ static std::string ConvertLTOToString(const int value);
+ static std::string ConvertInterTableIDToString(const int value);
+ static std::string ConvertDateTimeToString(FIC_DAB_DT utc_dt, const int lto, bool output_ms);
+ static std::string ConvertPTYToString(const int value, const int inter_table_id);
+ static std::string ConvertASuTypeToString(const int value);
+ static std::string DeriveShortLabelUTF8(const std::string& long_label, uint16_t short_label_mask);
+};
+
diff --git a/src/Flowgraph.cpp b/src/Flowgraph.cpp
index 3d4cdcc..339e326 100644
--- a/src/Flowgraph.cpp
+++ b/src/Flowgraph.cpp
@@ -27,12 +27,10 @@
#include "Flowgraph.h"
#include "PcDebug.h"
#include "Log.h"
-#include <string>
#include <memory>
#include <algorithm>
#include <sstream>
#include <sys/types.h>
-#include <stdexcept>
#include <assert.h>
#include <sys/time.h>
@@ -254,15 +252,15 @@ Flowgraph::~Flowgraph()
char node_time_sz[1024] = {};
for (const auto &node : nodes) {
- snprintf(node_time_sz, 1023, " %30s: %10lu us (%2.2f %%)\n",
+ snprintf(node_time_sz, 1023, " %30s: %10lld us (%2.2f %%)\n",
node->plugin()->name(),
- node->processTime(),
+ (long long)node->processTime(),
node->processTime() * 100.0 / myProcessTime);
ss << node_time_sz;
}
- snprintf(node_time_sz, 1023, " %30s: %10lu us (100.00 %%)\n", "total",
- myProcessTime);
+ snprintf(node_time_sz, 1023, " %30s: %10lld us (100.00 %%)\n", "total",
+ (long long)myProcessTime);
ss << node_time_sz;
etiLog.level(debug) << ss.str();
diff --git a/src/FormatConverter.cpp b/src/FormatConverter.cpp
index 0f86d42..517f26e 100644
--- a/src/FormatConverter.cpp
+++ b/src/FormatConverter.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -28,55 +28,152 @@
#include "FormatConverter.h"
#include "PcDebug.h"
+#include "Log.h"
-#include <sys/types.h>
-#include <string.h>
#include <stdexcept>
+#include <cstring>
#include <assert.h>
-
-#ifdef __SSE__
-# include <xmmintrin.h>
+#include <sys/types.h>
+#if defined(__ARM_NEON)
+#include <arm_neon.h>
#endif
-FormatConverter::FormatConverter(const std::string& format) :
+FormatConverter::FormatConverter(bool input_is_complexfix_wide, const std::string& format_out) :
ModCodec(),
- m_format(format)
+ m_input_complexfix_wide(input_is_complexfix_wide),
+ m_format_out(format_out)
{ }
+FormatConverter::~FormatConverter()
+{
+ if (
+#if defined(__ARM_NEON)
+ not m_input_complexfix_wide
+#else
+ true
+#endif
+ ) {
+ etiLog.level(debug) << "FormatConverter: " <<
+ m_num_clipped_samples.load() << " clipped";
+ }
+}
+
+
/* Expect the input samples to be in the correct range for the required format */
int FormatConverter::process(Buffer* const dataIn, Buffer* dataOut)
{
PDEBUG("FormatConverter::process(dataIn: %p, dataOut: %p)\n",
dataIn, dataOut);
- size_t sizeIn = dataIn->getLength() / sizeof(float);
- float* in = reinterpret_cast<float*>(dataIn->getData());
-
- if (m_format == "s16") {
- dataOut->setLength(sizeIn * sizeof(int16_t));
- int16_t* out = reinterpret_cast<int16_t*>(dataOut->getData());
-
- for (size_t i = 0; i < sizeIn; i++) {
- out[i] = in[i];
+ size_t num_clipped_samples = 0;
+
+ if (m_input_complexfix_wide) {
+ size_t sizeIn = dataIn->getLength() / sizeof(int32_t);
+ if (m_format_out == "s16") {
+ dataOut->setLength(sizeIn * sizeof(int16_t));
+ const int32_t *in = reinterpret_cast<int32_t*>(dataIn->getData());
+ int16_t* out = reinterpret_cast<int16_t*>(dataOut->getData());
+
+ constexpr int shift = 6;
+
+#if defined(__ARM_NEON)
+ if (sizeIn % 4 != 0) {
+ throw std::logic_error("Unexpected length not multiple of 4");
+ }
+
+ for (size_t i = 0; i < sizeIn; i += 4) {
+ int32x4_t input_vec = vld1q_s32(&in[i]);
+ // Apply shift right, saturate on conversion to int16_t
+ int16x4_t output_vec = vqshrn_n_s32(input_vec, shift);
+ vst1_s16(&out[i], output_vec);
+ }
+#else
+ for (size_t i = 0; i < sizeIn; i++) {
+ const int32_t val = in[i] >> shift;
+ if (val < INT16_MIN) {
+ out[i] = INT16_MIN;
+ num_clipped_samples++;
+ }
+ else if (val > INT16_MAX) {
+ out[i] = INT16_MAX;
+ num_clipped_samples++;
+ }
+ else {
+ out[i] = val;
+ }
+ }
+#endif
}
- }
- else if (m_format == "u8") {
- dataOut->setLength(sizeIn * sizeof(int8_t));
- uint8_t* out = reinterpret_cast<uint8_t*>(dataOut->getData());
-
- for (size_t i = 0; i < sizeIn; i++) {
- out[i] = in[i] + 128;
+ else {
+ throw std::runtime_error("FormatConverter: Invalid fix format " + m_format_out);
}
}
else {
- dataOut->setLength(sizeIn * sizeof(int8_t));
- int8_t* out = reinterpret_cast<int8_t*>(dataOut->getData());
-
- for (size_t i = 0; i < sizeIn; i++) {
- out[i] = in[i];
+ size_t sizeIn = dataIn->getLength() / sizeof(float);
+ const float* in = reinterpret_cast<float*>(dataIn->getData());
+
+ if (m_format_out == "s16") {
+ dataOut->setLength(sizeIn * sizeof(int16_t));
+ int16_t* out = reinterpret_cast<int16_t*>(dataOut->getData());
+
+ for (size_t i = 0; i < sizeIn; i++) {
+ if (in[i] < INT16_MIN) {
+ out[i] = INT16_MIN;
+ num_clipped_samples++;
+ }
+ else if (in[i] > INT16_MAX) {
+ out[i] = INT16_MAX;
+ num_clipped_samples++;
+ }
+ else {
+ out[i] = in[i];
+ }
+ }
+ }
+ else if (m_format_out == "u8") {
+ dataOut->setLength(sizeIn * sizeof(int8_t));
+ uint8_t* out = reinterpret_cast<uint8_t*>(dataOut->getData());
+
+ for (size_t i = 0; i < sizeIn; i++) {
+ const auto samp = in[i] + 128.0f;
+ if (samp < 0) {
+ out[i] = 0;
+ num_clipped_samples++;
+ }
+ else if (samp > UINT8_MAX) {
+ out[i] = UINT8_MAX;
+ num_clipped_samples++;
+ }
+ else {
+ out[i] = samp;
+ }
+
+ }
+ }
+ else if (m_format_out == "s8") {
+ dataOut->setLength(sizeIn * sizeof(int8_t));
+ int8_t* out = reinterpret_cast<int8_t*>(dataOut->getData());
+
+ for (size_t i = 0; i < sizeIn; i++) {
+ if (in[i] < INT8_MIN) {
+ out[i] = INT8_MIN;
+ num_clipped_samples++;
+ }
+ else if (in[i] > INT8_MAX) {
+ out[i] = INT8_MAX;
+ num_clipped_samples++;
+ }
+ else {
+ out[i] = in[i];
+ }
+ }
+ }
+ else {
+ throw std::runtime_error("FormatConverter: Invalid format " + m_format_out);
}
}
+ m_num_clipped_samples.store(num_clipped_samples);
return dataOut->getLength();
}
@@ -85,3 +182,25 @@ const char* FormatConverter::name()
return "FormatConverter";
}
+size_t FormatConverter::get_num_clipped_samples() const
+{
+ return m_num_clipped_samples.load();
+}
+
+
+size_t FormatConverter::get_format_size(const std::string& format)
+{
+ // Returns 2*sizeof(SAMPLE_TYPE) because we have I + Q
+ if (format == "s16") {
+ return 4;
+ }
+ else if (format == "u8") {
+ return 2;
+ }
+ else if (format == "s8") {
+ return 2;
+ }
+ else {
+ throw std::runtime_error("FormatConverter: Invalid format " + format);
+ }
+}
diff --git a/src/FormatConverter.h b/src/FormatConverter.h
index cc8a606..1ed2283 100644
--- a/src/FormatConverter.h
+++ b/src/FormatConverter.h
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2022
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -33,20 +33,30 @@
#endif
#include "ModPlugin.h"
-#include <complex>
+#include <atomic>
#include <string>
-#include <cstdint>
class FormatConverter : public ModCodec
{
public:
- FormatConverter(const std::string& format);
+ static size_t get_format_size(const std::string& format);
+
+ // floating-point input allows output formats: s8, u8 and s16
+ // complexfix_wide input allows output formats: s16
+ // complexfix input is already in s16, and needs no converter
+ FormatConverter(bool input_is_complexfix_wide, const std::string& format_out);
+ virtual ~FormatConverter();
int process(Buffer* const dataIn, Buffer* dataOut);
const char* name();
+ size_t get_num_clipped_samples() const;
+
private:
- std::string m_format;
+ bool m_input_complexfix_wide;
+ std::string m_format_out;
+
+ std::atomic<size_t> m_num_clipped_samples = 0;
};
diff --git a/src/FrameMultiplexer.cpp b/src/FrameMultiplexer.cpp
index e893120..ebd8b76 100644
--- a/src/FrameMultiplexer.cpp
+++ b/src/FrameMultiplexer.cpp
@@ -25,17 +25,11 @@
*/
#include "FrameMultiplexer.h"
-#include "PcDebug.h"
-#include <stdio.h>
#include <string>
-#include <stdexcept>
-#include <complex>
-#include <memory>
-#include <assert.h>
-#include <string.h>
-
-typedef std::complex<float> complexf;
+#include <cstdio>
+#include <cassert>
+#include <cstring>
FrameMultiplexer::FrameMultiplexer(
const EtiSource& etiSource) :
diff --git a/src/FrequencyInterleaver.cpp b/src/FrequencyInterleaver.cpp
index e76d525..6f36dcb 100644
--- a/src/FrequencyInterleaver.cpp
+++ b/src/FrequencyInterleaver.cpp
@@ -22,17 +22,15 @@
#include "FrequencyInterleaver.h"
#include "PcDebug.h"
-#include <stdio.h>
#include <stdexcept>
#include <string>
-#include <stdlib.h>
-#include <complex>
+#include <cstdio>
+#include <cstdlib>
-typedef std::complex<float> complexf;
-
-FrequencyInterleaver::FrequencyInterleaver(size_t mode) :
- ModCodec()
+FrequencyInterleaver::FrequencyInterleaver(size_t mode, bool fixedPoint) :
+ ModCodec(),
+ m_fixedPoint(fixedPoint)
{
PDEBUG("FrequencyInterleaver::FrequencyInterleaver(%zu) @ %p\n",
mode, this);
@@ -42,54 +40,53 @@ FrequencyInterleaver::FrequencyInterleaver(size_t mode) :
size_t beta;
switch (mode) {
case 1:
- d_carriers = 1536;
+ m_carriers = 1536;
num = 2048;
beta = 511;
break;
case 2:
- d_carriers = 384;
+ m_carriers = 384;
num = 512;
beta = 127;
break;
case 3:
- d_carriers = 192;
+ m_carriers = 192;
num = 256;
beta = 63;
break;
case 0:
case 4:
- d_carriers = 768;
+ m_carriers = 768;
num = 1024;
beta = 255;
break;
default:
- PDEBUG("Carriers: %zu\n", (d_carriers >> 1) << 1);
- throw std::runtime_error("FrequencyInterleaver::FrequencyInterleaver "
- "nb of carriers invalid!");
- break;
+ PDEBUG("Carriers: %zu\n", (m_carriers >> 1) << 1);
+ throw std::runtime_error("FrequencyInterleaver: invalid dab mode");
}
- const int ret = posix_memalign((void**)(&d_indexes), 16, d_carriers * sizeof(size_t));
+ const int ret = posix_memalign((void**)(&m_indices), 16, m_carriers * sizeof(size_t));
if (ret != 0) {
throw std::runtime_error("memory allocation failed: " + std::to_string(ret));
}
- size_t* index = d_indexes;
+ size_t *index = m_indices;
size_t perm = 0;
PDEBUG("i: %4u, R: %4u\n", 0, 0);
for (size_t j = 1; j < num; ++j) {
perm = (alpha * perm + beta) & (num - 1);
- if (perm >= ((num - d_carriers) / 2)
- && perm <= (num - (num - d_carriers) / 2)
+ if (perm >= ((num - m_carriers) / 2)
+ && perm <= (num - (num - m_carriers) / 2)
&& perm != (num / 2)) {
PDEBUG("i: %4zu, R: %4zu, d: %4zu, n: %4zu, k: %5zi, index: %zu\n",
- j, perm, perm, index - d_indexes, perm - num / 2,
+ j, perm, perm, index - m_indices, perm - num / 2,
perm > num / 2
? perm - (1 + (num / 2))
- : perm + (d_carriers - (num / 2)));
+ : perm + (m_carriers - (num / 2)));
*(index++) = perm > num / 2 ?
- perm - (1 + (num / 2)) : perm + (d_carriers - (num / 2));
- } else {
+ perm - (1 + (num / 2)) : perm + (m_carriers - (num / 2));
+ }
+ else {
PDEBUG("i: %4zu, R: %4zu\n", j, perm);
}
}
@@ -100,9 +97,33 @@ FrequencyInterleaver::~FrequencyInterleaver()
{
PDEBUG("FrequencyInterleaver::~FrequencyInterleaver() @ %p\n", this);
- free(d_indexes);
+ free(m_indices);
}
+template<typename T>
+void do_process(Buffer* const dataIn, Buffer* dataOut,
+ size_t carriers, const size_t * const indices)
+{
+ const T* in = reinterpret_cast<const T*>(dataIn->getData());
+ T* out = reinterpret_cast<T*>(dataOut->getData());
+ size_t sizeIn = dataIn->getLength() / sizeof(T);
+
+ if (sizeIn % carriers != 0) {
+ throw std::runtime_error(
+ "FrequencyInterleaver::process input size not valid!");
+ }
+
+ for (size_t i = 0; i < sizeIn;) {
+// memset(out, 0, m_carriers * sizeof(T));
+ for (size_t j = 0; j < carriers; i += 4, j += 4) {
+ out[indices[j]] = in[i];
+ out[indices[j + 1]] = in[i + 1];
+ out[indices[j + 2]] = in[i + 2];
+ out[indices[j + 3]] = in[i + 3];
+ }
+ out += carriers;
+ }
+}
int FrequencyInterleaver::process(Buffer* const dataIn, Buffer* dataOut)
{
@@ -112,24 +133,11 @@ int FrequencyInterleaver::process(Buffer* const dataIn, Buffer* dataOut)
dataOut->setLength(dataIn->getLength());
- const complexf* in = reinterpret_cast<const complexf*>(dataIn->getData());
- complexf* out = reinterpret_cast<complexf*>(dataOut->getData());
- size_t sizeIn = dataIn->getLength() / sizeof(complexf);
-
- if (sizeIn % d_carriers != 0) {
- throw std::runtime_error(
- "FrequencyInterleaver::process input size not valid!");
+ if (m_fixedPoint) {
+ do_process<complexfix>(dataIn, dataOut, m_carriers, m_indices);
}
-
- for (size_t i = 0; i < sizeIn;) {
-// memset(out, 0, d_carriers * sizeof(complexf));
- for (size_t j = 0; j < d_carriers; i += 4, j += 4) {
- out[d_indexes[j]] = in[i];
- out[d_indexes[j + 1]] = in[i + 1];
- out[d_indexes[j + 2]] = in[i + 2];
- out[d_indexes[j + 3]] = in[i + 3];
- }
- out += d_carriers;
+ else {
+ do_process<complexf>(dataIn, dataOut, m_carriers, m_indices);
}
return 1;
diff --git a/src/FrequencyInterleaver.h b/src/FrequencyInterleaver.h
index 43ca21a..b31b968 100644
--- a/src/FrequencyInterleaver.h
+++ b/src/FrequencyInterleaver.h
@@ -25,16 +25,14 @@
# include <config.h>
#endif
-
#include "ModPlugin.h"
#include <sys/types.h>
-
class FrequencyInterleaver : public ModCodec
{
public:
- FrequencyInterleaver(size_t mode);
+ FrequencyInterleaver(size_t mode, bool fixedPoint);
virtual ~FrequencyInterleaver();
FrequencyInterleaver(const FrequencyInterleaver&) = delete;
FrequencyInterleaver& operator=(const FrequencyInterleaver&) = delete;
@@ -43,7 +41,8 @@ public:
const char* name() override { return "FrequencyInterleaver"; }
protected:
- size_t d_carriers;
- size_t* d_indexes;
+ bool m_fixedPoint;
+ size_t m_carriers;
+ size_t *m_indices;
};
diff --git a/src/GainControl.cpp b/src/GainControl.cpp
index b781640..e3e280f 100644
--- a/src/GainControl.cpp
+++ b/src/GainControl.cpp
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -35,7 +35,7 @@
#ifdef __SSE__
# include <xmmintrin.h>
-union __u128 {
+union u128_union_t {
__m128 m;
float f[4];
};
@@ -122,26 +122,30 @@ int GainControl::internal_process(Buffer* const dataIn, Buffer* dataOut)
__m128* out = reinterpret_cast<__m128*>(dataOut->getData());
size_t sizeIn = dataIn->getLength() / sizeof(__m128);
size_t sizeOut = dataOut->getLength() / sizeof(__m128);
- __u128 gain128;
+ u128_union_t gain128;
if ((sizeIn % m_frameSize) != 0) {
PDEBUG("%zu != %zu\n", sizeIn, m_frameSize);
- throw std::runtime_error(
- "GainControl::process input size not valid!");
+ throw std::runtime_error("GainControl::process input size not valid!");
}
const auto constantGain4 = _mm_set1_ps(constantGain);
for (size_t i = 0; i < sizeIn; i += m_frameSize) {
- gain128.m = computeGain(in, m_frameSize);
+ // Do not apply gain computation to the NULL symbol, which either
+ // is blank or contains TII. Apply the gain calculation from the next
+ // symbol on the NULL symbol to get consistent TII power.
+ if (i > 0) {
+ gain128.m = computeGain(in, m_frameSize);
+ }
+ else {
+ gain128.m = computeGain(in + m_frameSize, m_frameSize);
+ }
gain128.m = _mm_mul_ps(gain128.m, constantGain4);
PDEBUG("********** Gain: %10f **********\n", gain128.f[0]);
- ////////////////////////////////////////////////////////////////////////
- // Applying gain to output data
- ////////////////////////////////////////////////////////////////////////
for (size_t sample = 0; sample < m_frameSize; ++sample) {
out[sample] = _mm_mul_ps(in[sample], gain128.m);
}
@@ -163,7 +167,12 @@ int GainControl::internal_process(Buffer* const dataIn, Buffer* dataOut)
}
for (size_t i = 0; i < sizeIn; i += m_frameSize) {
- gain = constantGain * computeGain(in, m_frameSize);
+ // Do not apply gain computation to the NULL symbol, which either
+ // is blank or contains TII. Apply the gain calculation from the next
+ // symbol on the NULL symbol to get consistent TII power.
+ gain = constantGain * (i > 0 ?
+ computeGain(in, m_frameSize) :
+ computeGain(in + m_frameSize, m_frameSize));
PDEBUG("********** Gain: %10f **********\n", gain);
@@ -191,10 +200,10 @@ __m128 GainControl::computeGainFix(const __m128* in, size_t sizeIn)
__m128 GainControl::computeGainMax(const __m128* in, size_t sizeIn)
{
- __u128 gain128;
- __u128 min128;
- __u128 max128;
- __u128 tmp128;
+ u128_union_t gain128;
+ u128_union_t min128;
+ u128_union_t max128;
+ u128_union_t tmp128;
static const __m128 factor128 = _mm_set1_ps(0x7fff);
////////////////////////////////////////////////////////////////////////
@@ -241,10 +250,10 @@ __m128 GainControl::computeGainMax(const __m128* in, size_t sizeIn)
__m128 GainControl::computeGainVar(const __m128* in, size_t sizeIn)
{
- __u128 gain128;
- __u128 mean128;
- __u128 var128;
- __u128 tmp128;
+ u128_union_t gain128;
+ u128_union_t mean128;
+ u128_union_t var128;
+ u128_union_t tmp128;
static const __m128 factor128 = _mm_set1_ps(0x7fff);
mean128.m = _mm_setzero_ps();
@@ -288,8 +297,8 @@ __m128 GainControl::computeGainVar(const __m128* in, size_t sizeIn)
var128.m = _mm_add_ps(var128.m, q128);
/*
- __u128 udiff128; udiff128.m = diff128;
- __u128 udelta128; udelta128.m = delta128;
+ u128_union_t udiff128; udiff128.m = diff128;
+ u128_union_t udelta128; udelta128.m = delta128;
for (int off=0; off<4; off+=2) {
printf("S %zu, %.2f+%.2fj\t",
sample,
@@ -574,3 +583,21 @@ const string GainControl::get_parameter(const string& parameter) const
return ss.str();
}
+const json::map_t GainControl::get_all_values() const
+{
+ json::map_t map;
+ map["digital"].v = m_digGain;
+ switch (m_gainmode) {
+ case GainMode::GAIN_FIX:
+ map["mode"].v = "fix";
+ break;
+ case GainMode::GAIN_MAX:
+ map["mode"].v = "max";
+ break;
+ case GainMode::GAIN_VAR:
+ map["mode"].v = "var";
+ break;
+ }
+ map["var"].v = m_var_variance_rc;
+ return map;
+}
diff --git a/src/GainControl.h b/src/GainControl.h
index 4c9a2bc..d40a7d7 100644
--- a/src/GainControl.h
+++ b/src/GainControl.h
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -35,16 +35,13 @@
#include "RemoteControl.h"
#include <sys/types.h>
-#include <complex>
#include <string>
#include <mutex>
+
#ifdef __SSE__
# include <xmmintrin.h>
#endif
-
-typedef std::complex<float> complexf;
-
enum class GainMode { GAIN_FIX = 0, GAIN_MAX = 1, GAIN_VAR = 2 };
class GainControl : public PipelinedModCodec, public RemoteControllable
@@ -63,13 +60,9 @@ class GainControl : public PipelinedModCodec, public RemoteControllable
const char* name() override { return "GainControl"; }
/* Functions for the remote control */
- /* Base function to set parameters. */
- virtual void set_parameter(const std::string& parameter,
- const std::string& value) override;
-
- /* Getting a parameter always returns a string. */
- virtual const std::string get_parameter(
- const std::string& parameter) const override;
+ virtual void set_parameter(const std::string& parameter, const std::string& value) override;
+ virtual const std::string get_parameter(const std::string& parameter) const override;
+ virtual const json::map_t get_all_values() const override;
protected:
virtual int internal_process(
diff --git a/src/GuardIntervalInserter.cpp b/src/GuardIntervalInserter.cpp
index 0cd5bd5..3ff8fd5 100644
--- a/src/GuardIntervalInserter.cpp
+++ b/src/GuardIntervalInserter.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2206, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2024
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -29,39 +29,47 @@
#include <cstring>
#include <cassert>
#include <stdexcept>
-#include <complex>
#include <mutex>
-typedef std::complex<float> complexf;
+GuardIntervalInserter::Params::Params(
+ size_t nbSymbols,
+ size_t spacing,
+ size_t nullSize,
+ size_t symSize,
+ size_t& windowOverlap) :
+ nbSymbols(nbSymbols),
+ spacing(spacing),
+ nullSize(nullSize),
+ symSize(symSize),
+ windowOverlap(windowOverlap) {}
GuardIntervalInserter::GuardIntervalInserter(
size_t nbSymbols,
size_t spacing,
size_t nullSize,
size_t symSize,
- size_t& windowOverlap) :
+ size_t& windowOverlap,
+ FFTEngine fftEngine) :
ModCodec(),
RemoteControllable("guardinterval"),
- d_nbSymbols(nbSymbols),
- d_spacing(spacing),
- d_nullSize(nullSize),
- d_symSize(symSize),
- d_windowOverlap(windowOverlap)
+ m_fftEngine(fftEngine),
+ m_params(nbSymbols, spacing, nullSize, symSize, windowOverlap)
{
- if (d_nullSize == 0) {
+ if (nullSize == 0) {
throw std::logic_error("NULL symbol must be present");
}
+
RC_ADD_PARAMETER(windowlen, "Window length for OFDM windowng [0 to disable]");
/* We use a raised-cosine window for the OFDM windowing.
- * Each symbol is extended on both sides by d_windowOverlap samples.
+ * Each symbol is extended on both sides by windowOverlap samples.
*
*
* Sym n |####################|
* Sym n+1 |####################|
*
- * We now extend the symbols by d_windowOverlap (one dash)
+ * We now extend the symbols by windowOverlap (one dash)
*
* Sym n extended -|####################|-
* Sym n+1 extended -|####################|-
@@ -75,7 +83,7 @@ GuardIntervalInserter::GuardIntervalInserter(
* / \
* ... ________________/ \__ ...
*
- * The window length is 2*d_windowOverlap.
+ * The window length is 2*windowOverlap.
*/
update_window(windowOverlap);
@@ -87,44 +95,49 @@ GuardIntervalInserter::GuardIntervalInserter(
void GuardIntervalInserter::update_window(size_t new_window_overlap)
{
- std::lock_guard<std::mutex> lock(d_windowMutex);
+ std::lock_guard<std::mutex> lock(m_params.windowMutex);
- d_windowOverlap = new_window_overlap;
+ m_params.windowOverlap = new_window_overlap;
- // d_window only contains the rising window edge.
- d_window.resize(2*d_windowOverlap);
- for (size_t i = 0; i < 2*d_windowOverlap; i++) {
- d_window[i] = (float)(0.5 * (1.0 - cos(M_PI * i / (2*d_windowOverlap - 1))));
+ // m_params.window only contains the rising window edge.
+ m_params.windowFloat.resize(2*m_params.windowOverlap);
+ m_params.windowFix.resize(2*m_params.windowOverlap);
+ m_params.windowFixWide.resize(2*m_params.windowOverlap);
+ for (size_t i = 0; i < 2*m_params.windowOverlap; i++) {
+ const float value = (float)(0.5 * (1.0 - cos(M_PI * i / (2*m_params.windowOverlap - 1))));
+
+ m_params.windowFloat[i] = value;
+ m_params.windowFix[i] = complexfix::value_type((double)value);
+ m_params.windowFixWide[i] = complexfix_wide::value_type((double)value);
}
}
-int GuardIntervalInserter::process(Buffer* const dataIn, Buffer* dataOut)
+template<typename T>
+int do_process(const GuardIntervalInserter::Params& p, Buffer* const dataIn, Buffer* dataOut)
{
- PDEBUG("GuardIntervalInserter::process(dataIn: %p, dataOut: %p)\n",
+ PDEBUG("GuardIntervalInserter do_process(dataIn: %p, dataOut: %p)\n",
dataIn, dataOut);
- std::lock_guard<std::mutex> lock(d_windowMutex);
-
- // Every symbol overlaps over a length of d_windowOverlap with
+ // Every symbol overlaps over a length of windowOverlap with
// the previous symbol, and with the next symbol. First symbol
// receives no prefix window, because we don't remember the
// last symbol from the previous TF (yet). Last symbol also
// receives no suffix window, for the same reason.
// Overall output buffer length must stay independent of the windowing.
- dataOut->setLength((d_nullSize + (d_nbSymbols * d_symSize)) * sizeof(complexf));
+ dataOut->setLength((p.nullSize + (p.nbSymbols * p.symSize)) * sizeof(T));
- const complexf* in = reinterpret_cast<const complexf*>(dataIn->getData());
- complexf* out = reinterpret_cast<complexf*>(dataOut->getData());
- size_t sizeIn = dataIn->getLength() / sizeof(complexf);
+ const T* in = reinterpret_cast<const T*>(dataIn->getData());
+ T* out = reinterpret_cast<T*>(dataOut->getData());
+ size_t sizeIn = dataIn->getLength() / sizeof(T);
- const size_t num_symbols = d_nbSymbols + 1;
- if (sizeIn != num_symbols * d_spacing)
+ const size_t num_symbols = p.nbSymbols + 1;
+ if (sizeIn != num_symbols * p.spacing)
{
- PDEBUG("Nb symbols: %zu\n", d_nbSymbols);
- PDEBUG("Spacing: %zu\n", d_spacing);
- PDEBUG("Null size: %zu\n", d_nullSize);
- PDEBUG("Sym size: %zu\n", d_symSize);
- PDEBUG("\n%zu != %zu\n", sizeIn, (d_nbSymbols + 1) * d_spacing);
+ PDEBUG("Nb symbols: %zu\n", p.nbSymbols);
+ PDEBUG("Spacing: %zu\n", p.spacing);
+ PDEBUG("Null size: %zu\n", p.nullSize);
+ PDEBUG("Sym size: %zu\n", p.symSize);
+ PDEBUG("\n%zu != %zu\n", sizeIn, (p.nbSymbols + 1) * p.spacing);
throw std::runtime_error(
"GuardIntervalInserter::process input size not valid!");
}
@@ -132,65 +145,90 @@ int GuardIntervalInserter::process(Buffer* const dataIn, Buffer* dataOut)
// TODO remember the end of the last TF so that we can do some
// windowing too.
- if (d_windowOverlap) {
+ std::lock_guard<std::mutex> lock(p.windowMutex);
+ if (p.windowOverlap) {
{
// Handle Null symbol separately because it is longer
- const size_t prefixlength = d_nullSize - d_spacing;
+ const size_t prefixlength = p.nullSize - p.spacing;
// end = spacing
- memcpy(out, &in[d_spacing - prefixlength],
- prefixlength * sizeof(complexf));
+ memcpy(out, &in[p.spacing - prefixlength],
+ prefixlength * sizeof(T));
- memcpy(&out[prefixlength], in, (d_spacing - d_windowOverlap) * sizeof(complexf));
+ memcpy(&out[prefixlength], in, (p.spacing - p.windowOverlap) * sizeof(T));
// The remaining part of the symbol must have half of the window applied,
// sloping down from 1 to 0.5
- for (size_t i = 0; i < d_windowOverlap; i++) {
- const size_t out_ix = prefixlength + d_spacing - d_windowOverlap + i;
- const size_t in_ix = d_spacing - d_windowOverlap + i;
- out[out_ix] = in[in_ix] * d_window[2*d_windowOverlap - (i+1)];
+ for (size_t i = 0; i < p.windowOverlap; i++) {
+ const size_t out_ix = prefixlength + p.spacing - p.windowOverlap + i;
+ const size_t in_ix = p.spacing - p.windowOverlap + i;
+ if constexpr (std::is_same_v<complexf, T>) {
+ out[out_ix] = in[in_ix] * p.windowFloat[2*p.windowOverlap - (i+1)];
+ }
+ if constexpr (std::is_same_v<complexfix, T>) {
+ out[out_ix] = in[in_ix] * p.windowFix[2*p.windowOverlap - (i+1)];
+ }
+ if constexpr (std::is_same_v<complexfix_wide, T>) {
+ out[out_ix] = in[in_ix] * p.windowFixWide[2*p.windowOverlap - (i+1)];
+ }
}
// Suffix is taken from the beginning of the symbol, and sees the other
// half of the window applied.
- for (size_t i = 0; i < d_windowOverlap; i++) {
- const size_t out_ix = prefixlength + d_spacing + i;
- out[out_ix] = in[i] * d_window[d_windowOverlap - (i+1)];
+ for (size_t i = 0; i < p.windowOverlap; i++) {
+ const size_t out_ix = prefixlength + p.spacing + i;
+ if constexpr (std::is_same_v<complexf, T>) {
+ out[out_ix] = in[i] * p.windowFloat[p.windowOverlap - (i+1)];
+ }
+ if constexpr (std::is_same_v<complexfix, T>) {
+ out[out_ix] = in[i] * p.windowFix[p.windowOverlap - (i+1)];
+ }
+ if constexpr (std::is_same_v<complexfix_wide, T>) {
+ out[out_ix] = in[i] * p.windowFixWide[p.windowOverlap - (i+1)];
+ }
}
- in += d_spacing;
- out += d_nullSize;
+ in += p.spacing;
+ out += p.nullSize;
// out is now pointing to the proper end of symbol. There are
- // d_windowOverlap samples ahead that were already written.
+ // windowOverlap samples ahead that were already written.
}
// Data symbols
- for (size_t sym_ix = 0; sym_ix < d_nbSymbols; sym_ix++) {
+ for (size_t sym_ix = 0; sym_ix < p.nbSymbols; sym_ix++) {
/* _ix variables are indices into in[], _ox variables are
* indices for out[] */
- const ssize_t start_rise_ox = -d_windowOverlap;
- const size_t start_rise_ix = 2 * d_spacing - d_symSize - d_windowOverlap;
+ const ssize_t start_rise_ox = -p.windowOverlap;
+ const size_t start_rise_ix = 2 * p.spacing - p.symSize - p.windowOverlap;
/*
- const size_t start_real_symbol_ox = 0;
- const size_t start_real_symbol_ix = 2 * d_spacing - d_symSize;
- */
- const ssize_t end_rise_ox = d_windowOverlap;
- const size_t end_rise_ix = 2 * d_spacing - d_symSize + d_windowOverlap;
- const ssize_t end_cyclic_prefix_ox = d_symSize - d_spacing;
+ const size_t start_real_symbol_ox = 0;
+ const size_t start_real_symbol_ix = 2 * p.spacing - p.symSize;
+ */
+ const ssize_t end_rise_ox = p.windowOverlap;
+ const size_t end_rise_ix = 2 * p.spacing - p.symSize + p.windowOverlap;
+ const ssize_t end_cyclic_prefix_ox = p.symSize - p.spacing;
/* end_cyclic_prefix_ix = end of symbol
- const size_t begin_fall_ox = d_symSize - d_windowOverlap;
- const size_t begin_fall_ix = d_spacing - d_windowOverlap;
- const size_t end_real_symbol_ox = d_symSize;
- end_real_symbol_ix = end of symbol
- const size_t end_fall_ox = d_symSize + d_windowOverlap;
- const size_t end_fall_ix = d_spacing + d_windowOverlap;
- */
+ const size_t begin_fall_ox = p.symSize - p.windowOverlap;
+ const size_t begin_fall_ix = p.spacing - p.windowOverlap;
+ const size_t end_real_symbol_ox = p.symSize;
+ end_real_symbol_ix = end of symbol
+ const size_t end_fall_ox = p.symSize + p.windowOverlap;
+ const size_t end_fall_ix = p.spacing + p.windowOverlap;
+ */
ssize_t ox = start_rise_ox;
size_t ix = start_rise_ix;
for (size_t i = 0; ix < end_rise_ix; i++) {
- out[ox] += in[ix] * d_window.at(i);
+ if constexpr (std::is_same_v<complexf, T>) {
+ out[ox] += in[ix] * p.windowFloat.at(i);
+ }
+ if constexpr (std::is_same_v<complexfix, T>) {
+ out[ox] += in[ix] * p.windowFix.at(i);
+ }
+ if constexpr (std::is_same_v<complexfix_wide, T>) {
+ out[ox] += in[ix] * p.windowFixWide.at(i);
+ }
ix++;
ox++;
}
@@ -198,73 +236,103 @@ int GuardIntervalInserter::process(Buffer* const dataIn, Buffer* dataOut)
const size_t remaining_prefix_length = end_cyclic_prefix_ox - end_rise_ox;
memcpy( &out[ox], &in[ix],
- remaining_prefix_length * sizeof(complexf));
+ remaining_prefix_length * sizeof(T));
ox += remaining_prefix_length;
assert(ox == end_cyclic_prefix_ox);
ix = 0;
- const bool last_symbol = (sym_ix + 1 >= d_nbSymbols);
+ const bool last_symbol = (sym_ix + 1 >= p.nbSymbols);
if (last_symbol) {
// No windowing at all at end
- memcpy(&out[ox], &in[ix], d_spacing * sizeof(complexf));
- ox += d_spacing;
+ memcpy(&out[ox], &in[ix], p.spacing * sizeof(T));
+ ox += p.spacing;
}
else {
- // Copy the middle part of the symbol, d_windowOverlap samples
+ // Copy the middle part of the symbol, p.windowOverlap samples
// short of the end.
memcpy( &out[ox],
&in[ix],
- (d_spacing - d_windowOverlap) * sizeof(complexf));
- ox += d_spacing - d_windowOverlap;
- ix += d_spacing - d_windowOverlap;
- assert(ox == (ssize_t)(d_symSize - d_windowOverlap));
+ (p.spacing - p.windowOverlap) * sizeof(T));
+ ox += p.spacing - p.windowOverlap;
+ ix += p.spacing - p.windowOverlap;
+ assert(ox == (ssize_t)(p.symSize - p.windowOverlap));
// Apply window from 1 to 0.5 for the end of the symbol
- for (size_t i = 0; ox < (ssize_t)d_symSize; i++) {
- out[ox] = in[ix] * d_window[2*d_windowOverlap - (i+1)];
+ for (size_t i = 0; ox < (ssize_t)p.symSize; i++) {
+ if constexpr (std::is_same_v<complexf, T>) {
+ out[ox] = in[ix] * p.windowFloat[2*p.windowOverlap - (i+1)];
+ }
+ if constexpr (std::is_same_v<complexfix, T>) {
+ out[ox] = in[ix] * p.windowFix[2*p.windowOverlap - (i+1)];
+ }
+ if constexpr (std::is_same_v<complexfix_wide, T>) {
+ out[ox] = in[ix] * p.windowFixWide[2*p.windowOverlap - (i+1)];
+ }
ox++;
ix++;
}
- assert(ix == d_spacing);
+ assert(ix == p.spacing);
ix = 0;
// Cyclic suffix, with window from 0.5 to 0
- for (size_t i = 0; ox < (ssize_t)(d_symSize + d_windowOverlap); i++) {
- out[ox] = in[ix] * d_window[d_windowOverlap - (i+1)];
+ for (size_t i = 0; ox < (ssize_t)(p.symSize + p.windowOverlap); i++) {
+ if constexpr (std::is_same_v<complexf, T>) {
+ out[ox] = in[ix] * p.windowFloat[p.windowOverlap - (i+1)];
+ }
+ if constexpr (std::is_same_v<complexfix, T>) {
+ out[ox] = in[ix] * p.windowFix[p.windowOverlap - (i+1)];
+ }
+ if constexpr (std::is_same_v<complexfix_wide, T>) {
+ out[ox] = in[ix] * p.windowFixWide[p.windowOverlap - (i+1)];
+ }
ox++;
ix++;
}
- assert(ix == d_windowOverlap);
+ assert(ix == p.windowOverlap);
}
- out += d_symSize;
- in += d_spacing;
+ out += p.symSize;
+ in += p.spacing;
// out is now pointing to the proper end of symbol. There are
- // d_windowOverlap samples ahead that were already written.
+ // windowOverlap samples ahead that were already written.
}
}
else {
// Handle Null symbol separately because it is longer
// end - (nullSize - spacing) = 2 * spacing - nullSize
- memcpy(out, &in[2 * d_spacing - d_nullSize],
- (d_nullSize - d_spacing) * sizeof(complexf));
- memcpy(&out[d_nullSize - d_spacing], in, d_spacing * sizeof(complexf));
- in += d_spacing;
- out += d_nullSize;
+ memcpy(out, &in[2 * p.spacing - p.nullSize],
+ (p.nullSize - p.spacing) * sizeof(T));
+ memcpy(&out[p.nullSize - p.spacing], in, p.spacing * sizeof(T));
+ in += p.spacing;
+ out += p.nullSize;
// Data symbols
- for (size_t i = 0; i < d_nbSymbols; ++i) {
+ for (size_t i = 0; i < p.nbSymbols; ++i) {
// end - (symSize - spacing) = 2 * spacing - symSize
- memcpy(out, &in[2 * d_spacing - d_symSize],
- (d_symSize - d_spacing) * sizeof(complexf));
- memcpy(&out[d_symSize - d_spacing], in, d_spacing * sizeof(complexf));
- in += d_spacing;
- out += d_symSize;
+ memcpy(out, &in[2 * p.spacing - p.symSize],
+ (p.symSize - p.spacing) * sizeof(T));
+ memcpy(&out[p.symSize - p.spacing], in, p.spacing * sizeof(T));
+ in += p.spacing;
+ out += p.symSize;
}
}
- return sizeIn;
+ const auto sizeOut = dataOut->getLength();
+ return sizeOut;
+}
+
+int GuardIntervalInserter::process(Buffer* const dataIn, Buffer* dataOut)
+{
+ switch (m_fftEngine) {
+ case FFTEngine::FFTW:
+ return do_process<complexf>(m_params, dataIn, dataOut);
+ case FFTEngine::KISS:
+ return do_process<complexfix>(m_params, dataIn, dataOut);
+ case FFTEngine::DEXTER:
+ return do_process<complexfix_wide>(m_params, dataIn, dataOut);
+ }
+ throw std::logic_error("Unhandled fftEngine variant");
}
void GuardIntervalInserter::set_parameter(
@@ -293,7 +361,7 @@ const std::string GuardIntervalInserter::get_parameter(const std::string& parame
using namespace std;
stringstream ss;
if (parameter == "windowlen") {
- ss << d_windowOverlap;
+ ss << m_params.windowOverlap;
}
else {
ss << "Parameter '" << parameter <<
@@ -302,3 +370,10 @@ const std::string GuardIntervalInserter::get_parameter(const std::string& parame
}
return ss.str();
}
+
+const json::map_t GuardIntervalInserter::get_all_values() const
+{
+ json::map_t map;
+ map["windowlen"].v = m_params.windowOverlap;
+ return map;
+}
diff --git a/src/GuardIntervalInserter.h b/src/GuardIntervalInserter.h
index 02ba72c..738d5b3 100644
--- a/src/GuardIntervalInserter.h
+++ b/src/GuardIntervalInserter.h
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2024
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -30,9 +30,9 @@
# include <config.h>
#endif
+#include "ConfigParser.h"
#include "ModPlugin.h"
#include "RemoteControl.h"
-#include <stdint.h>
#include <vector>
/* The GuardIntervalInserter prepends the cyclic prefix to all
@@ -50,28 +50,45 @@ class GuardIntervalInserter : public ModCodec, public RemoteControllable
size_t spacing,
size_t nullSize,
size_t symSize,
- size_t& windowOverlap);
+ size_t& windowOverlap,
+ FFTEngine fftEngine);
+
+ virtual ~GuardIntervalInserter() {}
- int process(Buffer* const dataIn, Buffer* dataOut);
- const char* name() { return "GuardIntervalInserter"; }
+ int process(Buffer* const dataIn, Buffer* dataOut) override;
+ const char* name() override { return "GuardIntervalInserter"; }
/******* REMOTE CONTROL ********/
- virtual void set_parameter(const std::string& parameter,
- const std::string& value);
+ virtual void set_parameter(const std::string& parameter, const std::string& value) override;
+ virtual const std::string get_parameter(const std::string& parameter) const override;
+ virtual const json::map_t get_all_values() const override;
+
+ struct Params {
+ Params(
+ size_t nbSymbols,
+ size_t spacing,
+ size_t nullSize,
+ size_t symSize,
+ size_t& windowOverlap);
- virtual const std::string get_parameter(
- const std::string& parameter) const;
+ size_t nbSymbols;
+ size_t spacing;
+ size_t nullSize;
+ size_t symSize;
+ size_t& windowOverlap;
+
+ mutable std::mutex windowMutex;
+ std::vector<float> windowFloat;
+ std::vector<complexfix::value_type> windowFix;
+ std::vector<complexfix_wide::value_type> windowFixWide;
+ };
protected:
void update_window(size_t new_window_overlap);
- size_t d_nbSymbols;
- size_t d_spacing;
- size_t d_nullSize;
- size_t d_symSize;
+ FFTEngine m_fftEngine;
+
+ Params m_params;
- mutable std::mutex d_windowMutex;
- size_t& d_windowOverlap;
- std::vector<float> d_window;
};
diff --git a/src/InputFileReader.cpp b/src/InputFileReader.cpp
index 5a9780b..a6b482e 100644
--- a/src/InputFileReader.cpp
+++ b/src/InputFileReader.cpp
@@ -6,8 +6,7 @@
Copyrigth (C) 2018
Matthias P. Braendli, matthias.braendli@mpb.li
-
- Input module for reading the ETI data from file or pipe, or ZeroMQ.
+ Input module for reading the ETI data from file or pipe.
Supported file formats: RAW, FRAMED, STREAMED
Supports re-sync to RAW ETI file
diff --git a/src/InputReader.h b/src/InputReader.h
index ab45d4f..2484948 100644
--- a/src/InputReader.h
+++ b/src/InputReader.h
@@ -38,11 +38,6 @@
#include <memory>
#include <thread>
#include <unistd.h>
-#if defined(HAVE_ZEROMQ)
-# include "zmq.hpp"
-# include "ThreadsafeQueue.h"
-# include "RemoteControl.h"
-#endif
#include "Log.h"
#include "Socket.h"
#define INVALID_SOCKET -1
@@ -148,60 +143,4 @@ class InputTcpReader : public InputReader
std::string m_uri;
};
-struct zmq_input_overflow : public std::exception
-{
- const char* what () const throw ()
- {
- return "InputZMQ buffer overflow";
- }
-};
-
-#if defined(HAVE_ZEROMQ)
-/* A ZeroMQ input. See www.zeromq.org for more info */
-
-class InputZeroMQReader : public InputReader, public RemoteControllable
-{
- public:
- InputZeroMQReader();
- InputZeroMQReader(const InputZeroMQReader& other) = delete;
- InputZeroMQReader& operator=(const InputZeroMQReader& other) = delete;
- ~InputZeroMQReader();
-
- int Open(const std::string& uri, size_t max_queued_frames);
- virtual int GetNextFrame(void* buffer) override;
- virtual std::string GetPrintableInfo() const override;
-
- /* Base function to set parameters. */
- virtual void set_parameter(
- const std::string& parameter,
- const std::string& value) override;
-
- /* Getting a parameter always returns a string. */
- virtual const std::string get_parameter(
- const std::string& parameter) const override;
-
- private:
- std::atomic<bool> m_running = ATOMIC_VAR_INIT(false);
- std::string m_uri;
- size_t m_max_queued_frames = 0;
-
- // Either must contain a full ETI frame, or one flag must be set
- struct message_t {
- std::vector<uint8_t> eti_frame;
- bool overflow = false;
- bool timeout = false;
- bool fault = false;
- };
- ThreadsafeQueue<message_t> m_in_messages;
-
- mutable std::mutex m_last_in_messages_size_mutex;
- size_t m_last_in_messages_size = 0;
-
- void RecvProcess(void);
-
- zmq::context_t m_zmqcontext; // is thread-safe
- std::thread m_recv_thread;
-};
-
-#endif
diff --git a/src/InputTcpReader.cpp b/src/InputTcpReader.cpp
index 21f8496..8ba4d74 100644
--- a/src/InputTcpReader.cpp
+++ b/src/InputTcpReader.cpp
@@ -79,6 +79,9 @@ int InputTcpReader::GetNextFrame(void* buffer)
etiLog.level(debug) << "TCP input auto reconnect";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
+ else if (ret == -2) {
+ etiLog.level(debug) << "TCP input timeout";
+ }
return ret;
}
diff --git a/src/InputZeroMQReader.cpp b/src/InputZeroMQReader.cpp
deleted file mode 100644
index 40a07d4..0000000
--- a/src/InputZeroMQReader.cpp
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012
- Her Majesty the Queen in Right of Canada (Communications Research
- Center Canada)
-
- Copyright (C) 2018
- Matthias P. Braendli, matthias.braendli@mpb.li
-
- http://opendigitalradio.org
- */
-/*
- This file is part of ODR-DabMod.
-
- ODR-DabMod 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, either version 3 of the
- License, or (at your option) any later version.
-
- ODR-DabMod 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 for more details.
-
- You should have received a copy of the GNU General Public License
- along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
- */
-
-#ifdef HAVE_CONFIG_H
-# include "config.h"
-#endif
-
-#if defined(HAVE_ZEROMQ)
-
-#include <string>
-#include <cstring>
-#include <cstdio>
-#include <stdint.h>
-#include "zmq.hpp"
-#include "InputReader.h"
-#include "PcDebug.h"
-#include "Utils.h"
-
-using namespace std;
-
-constexpr int ZMQ_TIMEOUT_MS = 100;
-
-#define NUM_FRAMES_PER_ZMQ_MESSAGE 4
-/* A concatenation of four ETI frames,
- * whose maximal size is 6144.
- *
- * Four frames in one zmq message are sent, so that
- * we do not risk breaking ETI vs. transmission frame
- * phase.
- *
- * The header is followed by the four ETI frames.
- */
-struct zmq_msg_header_t
-{
- uint32_t version;
- uint16_t buflen[NUM_FRAMES_PER_ZMQ_MESSAGE];
-};
-
-#define ZMQ_DAB_MESSAGE_T_HEADERSIZE \
- (sizeof(uint32_t) + NUM_FRAMES_PER_ZMQ_MESSAGE*sizeof(uint16_t))
-
-InputZeroMQReader::InputZeroMQReader() :
- InputReader(),
- RemoteControllable("inputzmq")
-{
- RC_ADD_PARAMETER(buffer, "Size of input buffer [us] (read-only)");
-}
-
-InputZeroMQReader::~InputZeroMQReader()
-{
- m_running = false;
- // This avoids the ugly "context was terminated" error because it lets
- // poll do its thing first
- this_thread::sleep_for(chrono::milliseconds(2 * ZMQ_TIMEOUT_MS));
- m_zmqcontext.close();
- if (m_recv_thread.joinable()) {
- m_recv_thread.join();
- }
-}
-
-int InputZeroMQReader::Open(const string& uri, size_t max_queued_frames)
-{
- // The URL might start with zmq+tcp://
- if (uri.substr(0, 4) == "zmq+") {
- m_uri = uri.substr(4);
- }
- else {
- m_uri = uri;
- }
-
- m_max_queued_frames = max_queued_frames;
-
- m_running = true;
- m_recv_thread = std::thread(&InputZeroMQReader::RecvProcess, this);
-
- return 0;
-}
-
-int InputZeroMQReader::GetNextFrame(void* buffer)
-{
- if (not m_running) {
- throw runtime_error("ZMQ input is not ready yet");
- }
-
- message_t incoming;
-
- /* Do some prebuffering because reads will happen in bursts
- * (4 ETI frames in TM1) and we should make sure that
- * we can serve the data required for a full transmission frame.
- */
- if (m_in_messages.size() < 4) {
- const size_t prebuffering = 10;
- etiLog.log(trace, "ZMQ,wait1");
- m_in_messages.wait_and_pop(incoming, prebuffering);
- }
- else {
- etiLog.log(trace, "ZMQ,wait2");
- m_in_messages.wait_and_pop(incoming);
- }
- etiLog.log(trace, "ZMQ,pop");
-
- constexpr size_t framesize = 6144;
-
- if (incoming.timeout) {
- return 0;
- }
- else if (incoming.fault) {
- throw runtime_error("ZMQ input has terminated");
- }
- else if (incoming.overflow) {
- throw zmq_input_overflow();
- }
- else if (incoming.eti_frame.size() == framesize) {
- unique_lock<mutex> lock(m_last_in_messages_size_mutex);
- m_last_in_messages_size--;
- lock.unlock();
-
- memcpy(buffer, &incoming.eti_frame.front(), framesize);
-
- return framesize;
- }
- else {
- throw logic_error("ZMQ ETI not 6144");
- }
-}
-
-std::string InputZeroMQReader::GetPrintableInfo() const
-{
- return "Input ZeroMQ: Receiving from " + m_uri;
-}
-
-void InputZeroMQReader::RecvProcess()
-{
- set_thread_name("zmqinput");
-
- size_t queue_size = 0;
-
- zmq::socket_t subscriber(m_zmqcontext, ZMQ_SUB);
- // zmq sockets are not thread safe. That's why
- // we create it here, and not at object creation.
-
- bool success = true;
-
- try {
- subscriber.connect(m_uri.c_str());
- }
- catch (const zmq::error_t& err) {
- etiLog.level(error) << "Failed to connect ZeroMQ socket to '" <<
- m_uri << "': '" << err.what() << "'";
- success = false;
- }
-
- if (success) try {
- // subscribe to all messages
- subscriber.setsockopt(ZMQ_SUBSCRIBE, NULL, 0);
- }
- catch (const zmq::error_t& err) {
- etiLog.level(error) << "Failed to subscribe ZeroMQ socket to messages: '" <<
- err.what() << "'";
- success = false;
- }
-
- if (success) try {
- while (m_running) {
- zmq::message_t incoming;
- zmq::pollitem_t items[1];
- items[0].socket = subscriber;
- items[0].events = ZMQ_POLLIN;
- const int num_events = zmq::poll(items, 1, ZMQ_TIMEOUT_MS);
- if (num_events == 0) {
- message_t msg;
- msg.timeout = true;
- m_in_messages.push(move(msg));
- continue;
- }
-
- subscriber.recv(incoming);
-
- if (queue_size < m_max_queued_frames) {
- if (incoming.size() < ZMQ_DAB_MESSAGE_T_HEADERSIZE) {
- throw runtime_error("ZeroMQ packet too small for header");
- }
- else {
- zmq_msg_header_t dab_msg;
- memcpy(&dab_msg, incoming.data(), sizeof(zmq_msg_header_t));
-
- if (dab_msg.version != 1) {
- etiLog.level(error) <<
- "ZeroMQ wrong packet version " <<
- dab_msg.version;
- }
-
- int offset = sizeof(dab_msg.version) +
- NUM_FRAMES_PER_ZMQ_MESSAGE * sizeof(*dab_msg.buflen);
-
- for (int i = 0; i < NUM_FRAMES_PER_ZMQ_MESSAGE; i++) {
- if (dab_msg.buflen[i] > 6144) {
- stringstream ss;
- ss << "ZeroMQ buffer " << i <<
- " has invalid buflen " << dab_msg.buflen[i];
- throw runtime_error(ss.str());
- }
- else {
- vector<uint8_t> buf(6144, 0x55);
-
- const int framesize = dab_msg.buflen[i];
-
- if ((ssize_t)incoming.size() < offset + framesize) {
- throw runtime_error("ZeroMQ packet too small");
- }
-
- memcpy(&buf.front(),
- ((uint8_t*)incoming.data()) + offset,
- framesize);
-
- offset += framesize;
-
- message_t msg;
- msg.eti_frame = move(buf);
- queue_size = m_in_messages.push(move(msg));
- etiLog.log(trace, "ZMQ,push %zu", queue_size);
-
- unique_lock<mutex> lock(m_last_in_messages_size_mutex);
- m_last_in_messages_size++;
- }
- }
- }
- }
- else {
- message_t msg;
- msg.overflow = true;
- queue_size = m_in_messages.push(move(msg));
- etiLog.level(warn) << "ZeroMQ buffer overfull !";
- throw runtime_error("ZMQ input full");
- }
-
- if (queue_size < 5) {
- etiLog.level(warn) << "ZeroMQ buffer low: " << queue_size << " elements !";
- }
- }
- }
- catch (const zmq::error_t& err) {
- etiLog.level(error) << "ZeroMQ error during receive: '" << err.what() << "'";
- }
- catch (const std::exception& err) {
- etiLog.level(error) << "Exception during receive: '" << err.what() << "'";
- }
-
- m_running = false;
-
- etiLog.level(info) << "ZeroMQ input worker terminated";
-
- subscriber.close();
-
- message_t msg;
- msg.fault = true;
- queue_size = m_in_messages.push(move(msg));
-}
-
-// =======================================
-// Remote Control
-// =======================================
-void InputZeroMQReader::set_parameter(const string& parameter, const string& value)
-{
- stringstream ss(value);
- ss.exceptions ( stringstream::failbit | stringstream::badbit );
-
- if (parameter == "buffer") {
- throw ParameterError("Parameter " + parameter + " is read-only.");
- }
- else {
- stringstream ss_err;
- ss_err << "Parameter '" << parameter
- << "' is not exported by controllable " << get_rc_name();
- throw ParameterError(ss_err.str());
- }
-}
-
-const string InputZeroMQReader::get_parameter(const string& parameter) const
-{
- stringstream ss;
- ss << std::fixed;
- if (parameter == "buffer") {
- // Do not use size of the queue, as it will contain empty
- // frames to signal timeouts
- unique_lock<mutex> lock(m_last_in_messages_size_mutex);
- const long time_in_buffer_us = 24000 * m_last_in_messages_size;
- ss << time_in_buffer_us;
- }
- else {
- ss << "Parameter '" << parameter <<
- "' is not exported by controllable " << get_rc_name();
- throw ParameterError(ss.str());
- }
- return ss.str();
-}
-
-#endif
-
diff --git a/src/MemlessPoly.cpp b/src/MemlessPoly.cpp
index 905ca67..184b5bd 100644
--- a/src/MemlessPoly.cpp
+++ b/src/MemlessPoly.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2007, 2008, 2009, 2010, 2011 Her Majesty the Queen in
Right of Canada (Communications Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
Andreas Steger, andreas.steger@digris.ch
@@ -314,7 +314,7 @@ void MemlessPoly::worker_thread(MemlessPoly::worker_t *workerdata)
set_thread_name("MemlessPoly");
while (true) {
- worker_t::input_data_t in_data;
+ worker_t::input_data_t in_data = {};
try {
workerdata->in_queue.wait_and_pop(in_data);
}
@@ -386,7 +386,7 @@ int MemlessPoly::internal_process(Buffer* const dataIn, Buffer* dataOut)
// Wait for completion of the tasks
for (auto& worker : m_workers) {
- int ret;
+ int ret = 0;
worker.out_queue.wait_and_pop(ret);
}
}
@@ -467,3 +467,11 @@ const string MemlessPoly::get_parameter(const string& parameter) const
return ss.str();
}
+const json::map_t MemlessPoly::get_all_values() const
+{
+ json::map_t map;
+ map["ncoefs"].v = m_coefs_am.size();
+ map["coefs"].v = serialise_coefficients();
+ map["coeffile"].v = m_coefs_file;
+ return map;
+}
diff --git a/src/MemlessPoly.h b/src/MemlessPoly.h
index 4642596..72de62c 100644
--- a/src/MemlessPoly.h
+++ b/src/MemlessPoly.h
@@ -2,7 +2,7 @@
Copyright (C) 2007, 2008, 2009, 2010, 2011 Her Majesty the Queen in
Right of Canada (Communications Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -32,13 +32,10 @@
#include "RemoteControl.h"
#include "ModPlugin.h"
-#include "PcDebug.h"
#include "ThreadsafeQueue.h"
#include <sys/types.h>
#include <array>
-#include <complex>
-#include <memory>
#include <string>
#include <thread>
#include <vector>
@@ -47,8 +44,6 @@
#define MEMLESSPOLY_PIPELINE_DELAY 1
-typedef std::complex<float> complexf;
-
enum class dpd_type_t {
odd_only_poly,
lookup_table
@@ -63,17 +58,15 @@ public:
MemlessPoly& operator=(const MemlessPoly& other) = delete;
virtual ~MemlessPoly();
- virtual const char* name() { return "MemlessPoly"; }
+ virtual const char* name() override { return "MemlessPoly"; }
/******* REMOTE CONTROL ********/
- virtual void set_parameter(const std::string& parameter,
- const std::string& value);
-
- virtual const std::string get_parameter(
- const std::string& parameter) const;
+ virtual void set_parameter(const std::string& parameter, const std::string& value) override;
+ virtual const std::string get_parameter(const std::string& parameter) const override;
+ virtual const json::map_t get_all_values() const override;
private:
- int internal_process(Buffer* const dataIn, Buffer* dataOut);
+ int internal_process(Buffer* const dataIn, Buffer* dataOut) override;
void load_coefficients(std::istream& coefData);
std::string serialise_coefficients() const;
diff --git a/src/ModPlugin.h b/src/ModPlugin.h
index 7f03618..bb3ee2c 100644
--- a/src/ModPlugin.h
+++ b/src/ModPlugin.h
@@ -32,18 +32,16 @@
#include "Buffer.h"
#include "ThreadsafeQueue.h"
-#include <cstddef>
+#include "TimestampDecoder.h"
#include <vector>
-#include <memory>
#include <thread>
#include <atomic>
// All flowgraph elements derive from ModPlugin, or a variant of it.
// Some ModPlugins also support handling metadata.
-struct frame_timestamp;
struct flowgraph_metadata {
- std::shared_ptr<struct frame_timestamp> ts;
+ frame_timestamp ts;
};
using meta_vec_t = std::vector<flowgraph_metadata>;
diff --git a/src/NullSymbol.cpp b/src/NullSymbol.cpp
index 4684dfe..526e662 100644
--- a/src/NullSymbol.cpp
+++ b/src/NullSymbol.cpp
@@ -27,18 +27,16 @@
#include "NullSymbol.h"
#include "PcDebug.h"
-#include <stdio.h>
-#include <stdlib.h>
-#include <complex>
-#include <string.h>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
-typedef std::complex<float> complexf;
-
-NullSymbol::NullSymbol(size_t nbCarriers) :
+NullSymbol::NullSymbol(size_t numCarriers, size_t typeSize) :
ModInput(),
- myNbCarriers(nbCarriers)
+ m_numCarriers(numCarriers),
+ m_typeSize(typeSize)
{
- PDEBUG("NullSymbol::NullSymbol(%zu) @ %p\n", nbCarriers, this);
+ PDEBUG("NullSymbol::NullSymbol(%zu) @ %p\n", numCarriers, this);
}
@@ -52,7 +50,7 @@ int NullSymbol::process(Buffer* dataOut)
{
PDEBUG("NullSymbol::process(dataOut: %p)\n", dataOut);
- dataOut->setLength(myNbCarriers * 2 * sizeof(float));
+ dataOut->setLength(m_numCarriers * m_typeSize);
memset(dataOut->getData(), 0, dataOut->getLength());
return dataOut->getLength();
diff --git a/src/NullSymbol.h b/src/NullSymbol.h
index 814e434..6ba9e63 100644
--- a/src/NullSymbol.h
+++ b/src/NullSymbol.h
@@ -39,14 +39,14 @@
class NullSymbol : public ModInput
{
public:
- NullSymbol(size_t nbCarriers);
+ NullSymbol(size_t nunCarriers, size_t typeSize);
virtual ~NullSymbol();
int process(Buffer* dataOut);
const char* name() { return "NullSymbol"; }
private:
- size_t myNbCarriers;
-
+ size_t m_numCarriers;
+ size_t m_typeSize;
};
diff --git a/src/OfdmGenerator.cpp b/src/OfdmGenerator.cpp
index 2e68df0..38648c9 100644
--- a/src/OfdmGenerator.cpp
+++ b/src/OfdmGenerator.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2024
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -27,17 +27,19 @@
#include "OfdmGenerator.h"
#include "PcDebug.h"
-#define FFT_TYPE fftwf_complex
-
-#include <string.h>
#include <stdexcept>
#include <assert.h>
#include <string>
#include <numeric>
+#include <vector>
+#include <cstring>
+#include <complex>
static const size_t MAX_CLIP_STATS = 10;
-OfdmGenerator::OfdmGenerator(size_t nbSymbols,
+using FFTW_TYPE = fftwf_complex;
+
+OfdmGeneratorCF32::OfdmGeneratorCF32(size_t nbSymbols,
size_t nbCarriers,
size_t spacing,
bool& enableCfr,
@@ -62,8 +64,7 @@ OfdmGenerator::OfdmGenerator(size_t nbSymbols,
nbSymbols, nbCarriers, spacing, inverse ? "true" : "false", this);
if (nbCarriers > spacing) {
- throw std::runtime_error(
- "OfdmGenerator::OfdmGenerator nbCarriers > spacing!");
+ throw std::runtime_error("OfdmGenerator nbCarriers > spacing!");
}
/* register the parameters that can be remote controlled */
@@ -102,29 +103,29 @@ OfdmGenerator::OfdmGenerator(size_t nbSymbols,
PDEBUG(" myZeroSize: %u\n", myZeroSize);
const int N = mySpacing; // The size of the FFT
- myFftIn = (FFT_TYPE*)fftwf_malloc(sizeof(FFT_TYPE) * N);
- myFftOut = (FFT_TYPE*)fftwf_malloc(sizeof(FFT_TYPE) * N);
+ myFftIn = (FFTW_TYPE*)fftwf_malloc(sizeof(FFTW_TYPE) * N);
+ myFftOut = (FFTW_TYPE*)fftwf_malloc(sizeof(FFTW_TYPE) * N);
fftwf_set_timelimit(2);
myFftPlan = fftwf_plan_dft_1d(N,
myFftIn, myFftOut,
FFTW_BACKWARD, FFTW_MEASURE);
- myCfrPostClip = (FFT_TYPE*)fftwf_malloc(sizeof(FFT_TYPE) * N);
- myCfrPostFft = (FFT_TYPE*)fftwf_malloc(sizeof(FFT_TYPE) * N);
+ myCfrPostClip = (FFTW_TYPE*)fftwf_malloc(sizeof(FFTW_TYPE) * N);
+ myCfrPostFft = (FFTW_TYPE*)fftwf_malloc(sizeof(FFTW_TYPE) * N);
myCfrFft = fftwf_plan_dft_1d(N,
myCfrPostClip, myCfrPostFft,
FFTW_FORWARD, FFTW_MEASURE);
- if (sizeof(complexf) != sizeof(FFT_TYPE)) {
+ if (sizeof(complexf) != sizeof(FFTW_TYPE)) {
printf("sizeof(complexf) %zu\n", sizeof(complexf));
- printf("sizeof(FFT_TYPE) %zu\n", sizeof(FFT_TYPE));
+ printf("sizeof(FFT_TYPE) %zu\n", sizeof(FFTW_TYPE));
throw std::runtime_error(
"OfdmGenerator::process complexf size is not FFT_TYPE size!");
}
}
-OfdmGenerator::~OfdmGenerator()
+OfdmGeneratorCF32::~OfdmGeneratorCF32()
{
PDEBUG("OfdmGenerator::~OfdmGenerator() @ %p\n", this);
@@ -153,15 +154,15 @@ OfdmGenerator::~OfdmGenerator()
}
}
-int OfdmGenerator::process(Buffer* const dataIn, Buffer* dataOut)
+int OfdmGeneratorCF32::process(Buffer* const dataIn, Buffer* dataOut)
{
PDEBUG("OfdmGenerator::process(dataIn: %p, dataOut: %p)\n",
dataIn, dataOut);
dataOut->setLength(myNbSymbols * mySpacing * sizeof(complexf));
- FFT_TYPE* in = reinterpret_cast<FFT_TYPE*>(dataIn->getData());
- FFT_TYPE* out = reinterpret_cast<FFT_TYPE*>(dataOut->getData());
+ FFTW_TYPE *in = reinterpret_cast<FFTW_TYPE*>(dataIn->getData());
+ FFTW_TYPE *out = reinterpret_cast<FFTW_TYPE*>(dataOut->getData());
size_t sizeIn = dataIn->getLength() / sizeof(complexf);
size_t sizeOut = dataOut->getLength() / sizeof(complexf);
@@ -203,7 +204,7 @@ int OfdmGenerator::process(Buffer* const dataIn, Buffer* dataOut)
myPaprAfterCFR.clear();
}
- for (size_t i = 0; i < myNbSymbols; ++i) {
+ for (size_t i = 0; i < myNbSymbols; i++) {
myFftIn[0][0] = 0;
myFftIn[0][1] = 0;
@@ -212,22 +213,20 @@ int OfdmGenerator::process(Buffer* const dataIn, Buffer* dataOut)
* PosSrc=0 PosDst=1 PosSize=768
* NegSrc=768 NegDst=1280 NegSize=768
*/
- memset(&myFftIn[myZeroDst], 0, myZeroSize * sizeof(FFT_TYPE));
+ memset(&myFftIn[myZeroDst], 0, myZeroSize * sizeof(FFTW_TYPE));
memcpy(&myFftIn[myPosDst], &in[myPosSrc],
- myPosSize * sizeof(FFT_TYPE));
+ myPosSize * sizeof(FFTW_TYPE));
memcpy(&myFftIn[myNegDst], &in[myNegSrc],
- myNegSize * sizeof(FFT_TYPE));
-
+ myNegSize * sizeof(FFTW_TYPE));
if (myCfr) {
reference.resize(mySpacing);
memcpy(reinterpret_cast<fftwf_complex*>(reference.data()),
- myFftIn, mySpacing * sizeof(FFT_TYPE));
+ myFftIn, mySpacing * sizeof(FFTW_TYPE));
}
fftwf_execute(myFftPlan); // IFFT from myFftIn to myFftOut
-
if (myCfr) {
complexf *symbol = reinterpret_cast<complexf*>(myFftOut);
myPaprBeforeCFR.process_block(symbol, mySpacing);
@@ -235,7 +234,7 @@ int OfdmGenerator::process(Buffer* const dataIn, Buffer* dataOut)
if (myMERCalcIndex == i) {
before_cfr.resize(mySpacing);
memcpy(reinterpret_cast<fftwf_complex*>(before_cfr.data()),
- myFftOut, mySpacing * sizeof(FFT_TYPE));
+ myFftOut, mySpacing * sizeof(FFTW_TYPE));
}
/* cfr_one_iteration runs the myFftPlan again at the end, and
@@ -277,7 +276,7 @@ int OfdmGenerator::process(Buffer* const dataIn, Buffer* dataOut)
num_error_clip += stat.errclip_count;
}
- memcpy(out, myFftOut, mySpacing * sizeof(FFT_TYPE));
+ memcpy(out, myFftOut, mySpacing * sizeof(FFTW_TYPE));
in += myNbCarriers;
out += mySpacing;
@@ -308,14 +307,14 @@ int OfdmGenerator::process(Buffer* const dataIn, Buffer* dataOut)
return sizeOut;
}
-OfdmGenerator::cfr_iter_stat_t OfdmGenerator::cfr_one_iteration(
+OfdmGeneratorCF32::cfr_iter_stat_t OfdmGeneratorCF32::cfr_one_iteration(
complexf *symbol, const complexf *reference)
{
// use std::norm instead of std::abs to avoid calculating the
// square roots
const float clip_squared = myCfrClip * myCfrClip;
- OfdmGenerator::cfr_iter_stat_t ret;
+ OfdmGeneratorCF32::cfr_iter_stat_t ret;
// Clip
for (size_t i = 0; i < mySpacing; i++) {
@@ -331,7 +330,7 @@ OfdmGenerator::cfr_iter_stat_t OfdmGenerator::cfr_one_iteration(
}
// Take FFT of our clipped signal
- memcpy(myCfrPostClip, symbol, mySpacing * sizeof(FFT_TYPE));
+ memcpy(myCfrPostClip, symbol, mySpacing * sizeof(FFTW_TYPE));
fftwf_execute(myCfrFft); // FFT from myCfrPostClip to myCfrPostFft
// Calculate the error in frequency domain by subtracting our reference
@@ -374,7 +373,7 @@ OfdmGenerator::cfr_iter_stat_t OfdmGenerator::cfr_one_iteration(
}
-void OfdmGenerator::set_parameter(const std::string& parameter,
+void OfdmGeneratorCF32::set_parameter(const std::string& parameter,
const std::string& value)
{
using namespace std;
@@ -404,7 +403,7 @@ void OfdmGenerator::set_parameter(const std::string& parameter,
}
}
-const std::string OfdmGenerator::get_parameter(const std::string& parameter) const
+const std::string OfdmGeneratorCF32::get_parameter(const std::string& parameter) const
{
using namespace std;
stringstream ss;
@@ -457,3 +456,334 @@ const std::string OfdmGenerator::get_parameter(const std::string& parameter) con
}
return ss.str();
}
+
+const json::map_t OfdmGeneratorCF32::get_all_values() const
+{
+ json::map_t map;
+ // TODO needs rework of the values
+ return map;
+}
+
+OfdmGeneratorFixed::OfdmGeneratorFixed(size_t nbSymbols,
+ size_t nbCarriers,
+ size_t spacing,
+ bool inverse) :
+ ModCodec(),
+ myNbSymbols(nbSymbols),
+ myNbCarriers(nbCarriers),
+ mySpacing(spacing)
+{
+ PDEBUG("OfdmGenerator::OfdmGenerator(%zu, %zu, %zu, %s) @ %p\n",
+ nbSymbols, nbCarriers, spacing, inverse ? "true" : "false", this);
+
+ etiLog.level(info) << "Using KISS FFT by Mark Borgerding for fixed-point transform";
+
+ if (nbCarriers > spacing) {
+ throw std::runtime_error("OfdmGenerator nbCarriers > spacing!");
+ }
+
+ if (inverse) {
+ myPosDst = (nbCarriers & 1 ? 0 : 1);
+ myPosSrc = 0;
+ myPosSize = (nbCarriers + 1) / 2;
+ myNegDst = spacing - (nbCarriers / 2);
+ myNegSrc = (nbCarriers + 1) / 2;
+ myNegSize = nbCarriers / 2;
+ }
+ else {
+ myPosDst = (nbCarriers & 1 ? 0 : 1);
+ myPosSrc = nbCarriers / 2;
+ myPosSize = (nbCarriers + 1) / 2;
+ myNegDst = spacing - (nbCarriers / 2);
+ myNegSrc = 0;
+ myNegSize = nbCarriers / 2;
+ }
+ myZeroDst = myPosDst + myPosSize;
+ myZeroSize = myNegDst - myZeroDst;
+
+ PDEBUG(" myPosDst: %u\n", myPosDst);
+ PDEBUG(" myPosSrc: %u\n", myPosSrc);
+ PDEBUG(" myPosSize: %u\n", myPosSize);
+ PDEBUG(" myNegDst: %u\n", myNegDst);
+ PDEBUG(" myNegSrc: %u\n", myNegSrc);
+ PDEBUG(" myNegSize: %u\n", myNegSize);
+ PDEBUG(" myZeroDst: %u\n", myZeroDst);
+ PDEBUG(" myZeroSize: %u\n", myZeroSize);
+
+ const int N = mySpacing; // The size of the FFT
+
+ const size_t nbytes = N * sizeof(kiss_fft_cpx);
+ myFftIn = (kiss_fft_cpx*)KISS_FFT_MALLOC(nbytes);
+ myFftOut = (kiss_fft_cpx*)KISS_FFT_MALLOC(nbytes);
+ memset(myFftIn, 0, nbytes);
+
+ myKissCfg = kiss_fft_alloc(N, inverse, nullptr, nullptr);
+}
+
+OfdmGeneratorFixed::~OfdmGeneratorFixed()
+{
+ if (myKissCfg) KISS_FFT_FREE(myKissCfg);
+ if (myFftIn) KISS_FFT_FREE(myFftIn);
+ if (myFftOut) KISS_FFT_FREE(myFftOut);
+}
+
+int OfdmGeneratorFixed::process(Buffer* const dataIn, Buffer* dataOut)
+{
+ dataOut->setLength(myNbSymbols * mySpacing * sizeof(kiss_fft_cpx));
+
+ kiss_fft_cpx *in = reinterpret_cast<kiss_fft_cpx*>(dataIn->getData());
+ kiss_fft_cpx *out = reinterpret_cast<kiss_fft_cpx*>(dataOut->getData());
+
+ size_t sizeIn = dataIn->getLength() / sizeof(kiss_fft_cpx);
+ size_t sizeOut = dataOut->getLength() / sizeof(kiss_fft_cpx);
+
+ if (sizeIn != myNbSymbols * myNbCarriers) {
+ PDEBUG("Nb symbols: %zu\n", myNbSymbols);
+ PDEBUG("Nb carriers: %zu\n", myNbCarriers);
+ PDEBUG("Spacing: %zu\n", mySpacing);
+ PDEBUG("\n%zu != %zu\n", sizeIn, myNbSymbols * myNbCarriers);
+ throw std::runtime_error(
+ "OfdmGenerator::process input size not valid!");
+ }
+ if (sizeOut != myNbSymbols * mySpacing) {
+ PDEBUG("Nb symbols: %zu\n", myNbSymbols);
+ PDEBUG("Nb carriers: %zu\n", myNbCarriers);
+ PDEBUG("Spacing: %zu\n", mySpacing);
+ PDEBUG("\n%zu != %zu\n", sizeIn, myNbSymbols * mySpacing);
+ throw std::runtime_error(
+ "OfdmGenerator::process output size not valid!");
+ }
+
+ for (size_t i = 0; i < myNbSymbols; i++) {
+ myFftIn[0].r = 0;
+ myFftIn[0].i = 0;
+
+ /* For TM I this is:
+ * ZeroDst=769 ZeroSize=511
+ * PosSrc=0 PosDst=1 PosSize=768
+ * NegSrc=768 NegDst=1280 NegSize=768
+ */
+ memset(&myFftIn[myZeroDst], 0, myZeroSize * sizeof(kiss_fft_cpx));
+ memcpy(&myFftIn[myPosDst], &in[myPosSrc], myPosSize * sizeof(kiss_fft_cpx));
+ memcpy(&myFftIn[myNegDst], &in[myNegSrc], myNegSize * sizeof(kiss_fft_cpx));
+
+ kiss_fft(myKissCfg, myFftIn, myFftOut);
+
+ memcpy(out, myFftOut, mySpacing * sizeof(kiss_fft_cpx));
+
+ in += myNbCarriers;
+ out += mySpacing;
+ }
+
+ return sizeOut;
+}
+
+#ifdef HAVE_DEXTER
+OfdmGeneratorDEXTER::OfdmGeneratorDEXTER(size_t nbSymbols,
+ size_t nbCarriers,
+ size_t spacing) :
+ ModCodec(),
+ myNbSymbols(nbSymbols),
+ myNbCarriers(nbCarriers),
+ mySpacing(spacing)
+{
+ PDEBUG("OfdmGeneratorDEXTER::OfdmGeneratorDEXTER(%zu, %zu, %zu) @ %p\n",
+ nbSymbols, nbCarriers, spacing, this);
+
+ etiLog.level(info) << "Using DEXTER FFT Accelerator for fixed-point transform";
+
+ if (nbCarriers > spacing) {
+ throw std::runtime_error("OfdmGenerator nbCarriers > spacing!");
+ }
+
+ myPosDst = (nbCarriers & 1 ? 0 : 1);
+ myPosSrc = 0;
+ myPosSize = (nbCarriers + 1) / 2;
+ myNegDst = spacing - (nbCarriers / 2);
+ myNegSrc = (nbCarriers + 1) / 2;
+ myNegSize = nbCarriers / 2;
+
+ myZeroDst = myPosDst + myPosSize;
+ myZeroSize = myNegDst - myZeroDst;
+
+ PDEBUG(" myPosDst: %u\n", myPosDst);
+ PDEBUG(" myPosSrc: %u\n", myPosSrc);
+ PDEBUG(" myPosSize: %u\n", myPosSize);
+ PDEBUG(" myNegDst: %u\n", myNegDst);
+ PDEBUG(" myNegSrc: %u\n", myNegSrc);
+ PDEBUG(" myNegSize: %u\n", myNegSize);
+ PDEBUG(" myZeroDst: %u\n", myZeroDst);
+ PDEBUG(" myZeroSize: %u\n", myZeroSize);
+
+ const size_t nbytes_in = mySpacing * sizeof(complexfix);
+ const size_t nbytes_out = mySpacing * sizeof(complexfix_wide);
+
+#define IIO_ENSURE(expr, err) { \
+ if (!(expr)) { \
+ etiLog.log(error, "%s (%s:%d)\n", err, __FILE__, __LINE__); \
+ throw std::runtime_error("Failed to set FFT for OfdmGeneratorDEXTER"); \
+ } \
+}
+ IIO_ENSURE((m_ctx = iio_create_default_context()), "No context");
+ IIO_ENSURE(m_dev_in = iio_context_find_device(m_ctx, "fft-accelerator-in"), "no dev");
+ IIO_ENSURE(m_dev_out = iio_context_find_device(m_ctx, "fft-accelerator-out"), "no dev");
+ IIO_ENSURE(m_channel_in = iio_device_find_channel(m_dev_in, "voltage0", true), "no channel");
+ IIO_ENSURE(m_channel_out = iio_device_find_channel(m_dev_out, "voltage0", false), "no channel");
+
+ iio_channel_enable(m_channel_in);
+ iio_channel_enable(m_channel_out);
+
+ m_buf_in = iio_device_create_buffer(m_dev_in, nbytes_in, false);
+ if (!m_buf_in) {
+ throw std::runtime_error("OfdmGeneratorDEXTER could not create in buffer");
+ }
+
+ m_buf_out = iio_device_create_buffer(m_dev_out, nbytes_out, false);
+ if (!m_buf_out) {
+ throw std::runtime_error("OfdmGeneratorDEXTER could not create out buffer");
+ }
+}
+
+OfdmGeneratorDEXTER::~OfdmGeneratorDEXTER()
+{
+ if (m_buf_in) {
+ iio_buffer_destroy(m_buf_in);
+ m_buf_in = nullptr;
+ }
+
+ if (m_buf_out) {
+ iio_buffer_destroy(m_buf_out);
+ m_buf_out = nullptr;
+ }
+
+ if (m_channel_in) {
+ iio_channel_disable(m_channel_in);
+ m_channel_in = nullptr;
+ }
+
+ if (m_channel_out) {
+ iio_channel_disable(m_channel_out);
+ m_channel_out = nullptr;
+ }
+
+ if (m_ctx) {
+ iio_context_destroy(m_ctx);
+ m_ctx = nullptr;
+ }
+}
+
+int OfdmGeneratorDEXTER::process(Buffer* const dataIn, Buffer* dataOut)
+{
+ dataOut->setLength(myNbSymbols * mySpacing * sizeof(complexfix_wide));
+
+ complexfix *in = reinterpret_cast<complexfix*>(dataIn->getData());
+ complexfix_wide *out = reinterpret_cast<complexfix_wide*>(dataOut->getData());
+
+ size_t sizeIn = dataIn->getLength() / sizeof(complexfix);
+ size_t sizeOut = dataOut->getLength() / sizeof(complexfix_wide);
+
+ if (sizeIn != myNbSymbols * myNbCarriers) {
+ PDEBUG("Nb symbols: %zu\n", myNbSymbols);
+ PDEBUG("Nb carriers: %zu\n", myNbCarriers);
+ PDEBUG("Spacing: %zu\n", mySpacing);
+ PDEBUG("\n%zu != %zu\n", sizeIn, myNbSymbols * myNbCarriers);
+ throw std::runtime_error(
+ "OfdmGenerator::process input size not valid!");
+ }
+ if (sizeOut != myNbSymbols * mySpacing) {
+ PDEBUG("Nb symbols: %zu\n", myNbSymbols);
+ PDEBUG("Nb carriers: %zu\n", myNbCarriers);
+ PDEBUG("Spacing: %zu\n", mySpacing);
+ PDEBUG("\n%zu != %zu\n", sizeIn, myNbSymbols * mySpacing);
+ throw std::runtime_error("OfdmGenerator::process output size not valid!");
+ }
+
+ ptrdiff_t iio_buf_size = (uint8_t*)iio_buffer_end(m_buf_in) - (uint8_t*)iio_buffer_start(m_buf_in);
+ if (iio_buf_size != (ssize_t)(mySpacing * sizeof(complexfix))) {
+ throw std::runtime_error("OfdmGenerator::process incorrect iio buffer size!");
+ }
+
+ for (size_t i = 0; i < myNbSymbols; i++) {
+ complexfix *fft_in = reinterpret_cast<complexfix*>(iio_buffer_start(m_buf_in));
+
+ /* For TM I this is:
+ * ZeroDst=769 ZeroSize=511
+ * PosSrc=0 PosDst=1 PosSize=768
+ * NegSrc=768 NegDst=1280 NegSize=768
+ */
+
+ fft_in[0] = static_cast<complexfix::value_type>(0);
+ for (size_t i = 0; i < myZeroSize; i++) {
+ fft_in[myZeroDst + i] = static_cast<complexfix::value_type>(0);
+ }
+
+ memcpy(&fft_in[myPosDst], &in[myPosSrc], myPosSize * sizeof(complexfix));
+ memcpy(&fft_in[myNegDst], &in[myNegSrc], myNegSize * sizeof(complexfix));
+
+ ssize_t nbytes_tx = iio_buffer_push(m_buf_in);
+ if (nbytes_tx < 0) {
+ throw std::runtime_error("OfdmGenerator::process error pushing IIO buffer!");
+ }
+
+ in += myNbCarriers;
+
+ // Keep one buffer in flight while we're doing shuffling data around here,
+ // this improves performance.
+ // I believe that, by default, IIO allocates four buffers in total.
+ if (i > 0) {
+ ssize_t nbytes_rx = iio_buffer_refill(m_buf_out);
+ if (nbytes_rx < 0) {
+ throw std::runtime_error("OfdmGenerator::process error refilling IIO buffer!");
+ }
+
+ ptrdiff_t p_inc = iio_buffer_step(m_buf_out);
+ if (p_inc != 1) {
+ throw std::runtime_error("OfdmGenerator::process Wrong p_inc");
+ }
+
+ // The FFT Accelerator takes 16-bit I + 16-bit Q, and outputs 32-bit I and 32-bit Q.
+ // The formatconvert will take care of this
+ const uint8_t *fft_out = (const uint8_t*)iio_buffer_first(m_buf_out, m_channel_out);
+ const uint8_t *fft_out_end = (const uint8_t*)iio_buffer_end(m_buf_out);
+ constexpr size_t sizeof_out_iq = sizeof(complexfix_wide);
+ if ((fft_out_end - fft_out) != (ssize_t)(mySpacing * sizeof_out_iq)) {
+ fprintf(stderr, "FFT_OUT: %p %p %zu %zu\n",
+ fft_out, fft_out_end, (fft_out_end - fft_out),
+ mySpacing * sizeof_out_iq);
+ throw std::runtime_error("OfdmGenerator::process fft_out length invalid!");
+ }
+
+ memcpy(out, fft_out, mySpacing * sizeof_out_iq);
+
+ out += mySpacing;
+ }
+ }
+
+ ssize_t nbytes_rx = iio_buffer_refill(m_buf_out);
+ if (nbytes_rx < 0) {
+ throw std::runtime_error("OfdmGenerator::process error refilling IIO buffer!");
+ }
+
+ ptrdiff_t p_inc = iio_buffer_step(m_buf_out);
+ if (p_inc != 1) {
+ throw std::runtime_error("OfdmGenerator::process Wrong p_inc");
+ }
+
+ // The FFT Accelerator takes 16-bit I + 16-bit Q, and outputs 32-bit I and 32-bit Q.
+ // The formatconvert will take care of this
+ const uint8_t *fft_out = (const uint8_t*)iio_buffer_first(m_buf_out, m_channel_out);
+ const uint8_t *fft_out_end = (const uint8_t*)iio_buffer_end(m_buf_out);
+ constexpr size_t sizeof_out_iq = sizeof(complexfix_wide);
+ if ((fft_out_end - fft_out) != (ssize_t)(mySpacing * sizeof_out_iq)) {
+ fprintf(stderr, "FFT_OUT: %p %p %zu %zu\n",
+ fft_out, fft_out_end, (fft_out_end - fft_out),
+ mySpacing * sizeof_out_iq);
+ throw std::runtime_error("OfdmGenerator::process fft_out length invalid!");
+ }
+
+ memcpy(out, fft_out, mySpacing * sizeof_out_iq);
+
+ return sizeOut;
+}
+
+#endif // HAVE_DEXTER
diff --git a/src/OfdmGenerator.h b/src/OfdmGenerator.h
index 30fdff4..475b2a4 100644
--- a/src/OfdmGenerator.h
+++ b/src/OfdmGenerator.h
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2017
+ Copyright (C) 2024
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -33,40 +33,38 @@
#include "ModPlugin.h"
#include "RemoteControl.h"
#include "PAPRStats.h"
-#include "fftw3.h"
+#include "kiss_fft.h"
+
#include <cstddef>
-#include <vector>
-#include <complex>
#include <atomic>
+#include <fftw3.h>
-typedef std::complex<float> complexf;
+#ifdef HAVE_DEXTER
+# include <iio.h>
+#endif
-class OfdmGenerator : public ModCodec, public RemoteControllable
+// Complex Float uses FFTW
+class OfdmGeneratorCF32 : public ModCodec, public RemoteControllable
{
public:
- OfdmGenerator(size_t nbSymbols,
+ OfdmGeneratorCF32(size_t nbSymbols,
size_t nbCarriers,
size_t spacing,
bool& enableCfr,
float& cfrClip,
float& cfrErrorClip,
bool inverse = true);
- virtual ~OfdmGenerator();
- OfdmGenerator(const OfdmGenerator&) = delete;
- OfdmGenerator& operator=(const OfdmGenerator&) = delete;
+ virtual ~OfdmGeneratorCF32();
+ OfdmGeneratorCF32(const OfdmGeneratorCF32&) = delete;
+ OfdmGeneratorCF32& operator=(const OfdmGeneratorCF32&) = delete;
int process(Buffer* const dataIn, Buffer* dataOut) override;
const char* name() override { return "OfdmGenerator"; }
/* Functions for the remote control */
- /* Base function to set parameters. */
- virtual void set_parameter(
- const std::string& parameter,
- const std::string& value) override;
-
- /* Getting a parameter always returns a string. */
- virtual const std::string get_parameter(
- const std::string& parameter) const override;
+ virtual void set_parameter(const std::string& parameter, const std::string& value) override;
+ virtual const std::string get_parameter(const std::string& parameter) const override;
+ virtual const json::map_t get_all_values() const override;
protected:
struct cfr_iter_stat_t {
@@ -112,4 +110,76 @@ class OfdmGenerator : public ModCodec, public RemoteControllable
std::deque<double> myMERs;
};
+// Fixed point implementation uses KISS FFT with -DFIXED_POINT=32
+class OfdmGeneratorFixed : public ModCodec
+{
+ public:
+ OfdmGeneratorFixed(size_t nbSymbols,
+ size_t nbCarriers,
+ size_t spacing,
+ bool inverse = true);
+ virtual ~OfdmGeneratorFixed();
+ OfdmGeneratorFixed(const OfdmGeneratorFixed&) = delete;
+ OfdmGeneratorFixed& operator=(const OfdmGeneratorFixed&) = delete;
+
+ int process(Buffer* const dataIn, Buffer* dataOut) override;
+ const char* name() override { return "OfdmGenerator"; }
+
+ private:
+ kiss_fft_cfg myKissCfg = nullptr;
+ kiss_fft_cpx *myFftIn, *myFftOut;
+ const size_t myNbSymbols;
+ const size_t myNbCarriers;
+ const size_t mySpacing;
+ unsigned myPosSrc;
+ unsigned myPosDst;
+ unsigned myPosSize;
+ unsigned myNegSrc;
+ unsigned myNegDst;
+ unsigned myNegSize;
+ unsigned myZeroDst;
+ unsigned myZeroSize;
+};
+
+#ifdef HAVE_DEXTER
+// The PrecisionWave DEXTER device contains an FFT accelerator in FPGA
+// It only does inverse FFTs
+class OfdmGeneratorDEXTER : public ModCodec
+{
+ public:
+ OfdmGeneratorDEXTER(size_t nbSymbols,
+ size_t nbCarriers,
+ size_t spacing);
+ virtual ~OfdmGeneratorDEXTER();
+ OfdmGeneratorDEXTER(const OfdmGeneratorDEXTER&) = delete;
+ OfdmGeneratorDEXTER& operator=(const OfdmGeneratorDEXTER&) = delete;
+
+ int process(Buffer* const dataIn, Buffer* dataOut) override;
+ const char* name() override { return "OfdmGenerator"; }
+
+ private:
+ struct iio_context *m_ctx = nullptr;
+
+ // "in" and "out" are from the point of view of the FFT Accelerator block
+ struct iio_device *m_dev_in = nullptr;
+ struct iio_channel *m_channel_in = nullptr;
+ struct iio_buffer *m_buf_in = nullptr;
+
+ struct iio_device *m_dev_out = nullptr;
+ struct iio_channel *m_channel_out = nullptr;
+ struct iio_buffer *m_buf_out = nullptr;
+
+ const size_t myNbSymbols;
+ const size_t myNbCarriers;
+ const size_t mySpacing;
+ unsigned myPosSrc;
+ unsigned myPosDst;
+ unsigned myPosSize;
+ unsigned myNegSrc;
+ unsigned myNegDst;
+ unsigned myNegSize;
+ unsigned myZeroDst;
+ unsigned myZeroSize;
+};
+#endif // HAVE_DEXTER
diff --git a/src/OutputFile.cpp b/src/OutputFile.cpp
index acaebad..2ee838c 100644
--- a/src/OutputFile.cpp
+++ b/src/OutputFile.cpp
@@ -74,28 +74,23 @@ meta_vec_t OutputFile::process_metadata(const meta_vec_t& metadataIn)
frame_timestamp first_ts;
for (const auto& md : metadataIn) {
- if (md.ts) {
- // The following code assumes TM I, where we get called every 96ms.
- // Support for other transmission modes skipped because this is mostly
- // debugging code.
+ // The following code assumes TM I, where we get called every 96ms.
+ // Support for other transmission modes skipped because this is mostly
+ // debugging code.
- if (md.ts->fp == 0 or md.ts->fp == 4) {
- first_ts = *md.ts;
- }
+ if (md.ts.fp == 0 or md.ts.fp == 4) {
+ first_ts = md.ts;
+ }
- ss << " FCT=" << md.ts->fct <<
- " FP=" << (int)md.ts->fp;
- if (md.ts->timestamp_valid) {
- ss << " TS=" << md.ts->timestamp_sec << " + " <<
- std::fixed
- << (double)md.ts->timestamp_pps / 163840000.0 << ";";
- }
- else {
- ss << " TS invalid;";
- }
+ ss << " FCT=" << md.ts.fct <<
+ " FP=" << (int)md.ts.fp;
+ if (md.ts.timestamp_valid) {
+ ss << " TS=" << md.ts.timestamp_sec << " + " <<
+ std::fixed
+ << (double)md.ts.timestamp_pps / 163840000.0 << ";";
}
else {
- ss << " void, ";
+ ss << " TS invalid;";
}
}
diff --git a/src/OutputMemory.cpp b/src/OutputMemory.cpp
index d6ef917..f673555 100644
--- a/src/OutputMemory.cpp
+++ b/src/OutputMemory.cpp
@@ -26,20 +26,14 @@
#include "OutputMemory.h"
#include "PcDebug.h"
-#include "Log.h"
-#include "TimestampDecoder.h"
-
-#include <stdexcept>
-#include <string.h>
-#include <math.h>
-
+#include <cmath>
OutputMemory::OutputMemory(Buffer* dataOut)
: ModOutput()
{
PDEBUG("OutputMemory::OutputMemory(%p) @ %p\n", dataOut, this);
- setOutput(dataOut);
+ m_dataOut = dataOut;
#if OUTPUT_MEM_HISTOGRAM
myMax = 0.0f;
@@ -49,7 +43,6 @@ OutputMemory::OutputMemory(Buffer* dataOut)
#endif
}
-
OutputMemory::~OutputMemory()
{
#if OUTPUT_MEM_HISTOGRAM
@@ -66,19 +59,12 @@ OutputMemory::~OutputMemory()
PDEBUG("OutputMemory::~OutputMemory() @ %p\n", this);
}
-
-void OutputMemory::setOutput(Buffer* dataOut)
-{
- myDataOut = dataOut;
-}
-
-
int OutputMemory::process(Buffer* dataIn)
{
PDEBUG("OutputMemory::process(dataIn: %p)\n",
dataIn);
- *myDataOut = *dataIn;
+ *m_dataOut = *dataIn;
#if OUTPUT_MEM_HISTOGRAM
const float* in = (const float*)dataIn->getData();
@@ -93,17 +79,17 @@ int OutputMemory::process(Buffer* dataIn)
}
#endif
- return myDataOut->getLength();
+ return m_dataOut->getLength();
}
meta_vec_t OutputMemory::process_metadata(const meta_vec_t& metadataIn)
{
- myMetadata = metadataIn;
+ m_metadata = metadataIn;
return {};
}
meta_vec_t OutputMemory::get_latest_metadata()
{
- return myMetadata;
+ return m_metadata;
}
diff --git a/src/OutputMemory.h b/src/OutputMemory.h
index f0a5fbb..299d31d 100644
--- a/src/OutputMemory.h
+++ b/src/OutputMemory.h
@@ -61,11 +61,9 @@ public:
meta_vec_t get_latest_metadata(void);
- void setOutput(Buffer* dataOut);
-
protected:
- Buffer* myDataOut;
- meta_vec_t myMetadata;
+ Buffer* m_dataOut;
+ meta_vec_t m_metadata;
#if OUTPUT_MEM_HISTOGRAM
// keep track of max value
diff --git a/src/OutputZeroMQ.cpp b/src/OutputZeroMQ.cpp
index 373081b..c45c22e 100644
--- a/src/OutputZeroMQ.cpp
+++ b/src/OutputZeroMQ.cpp
@@ -68,7 +68,8 @@ int OutputZeroMQ::process(Buffer* dataIn)
if (m_type == ZMQ_REP) {
// A ZMQ_REP socket requires a request first
zmq::message_t msg;
- m_zmq_sock.recv(msg, zmq::recv_flags::none);
+ const auto rr = m_zmq_sock.recv(msg, zmq::recv_flags::none);
+ (void)rr;
}
m_zmq_sock.send(zmq::const_buffer{dataIn->getData(), dataIn->getLength()});
diff --git a/src/PAPRStats.cpp b/src/PAPRStats.cpp
index 0c9764a..103f02f 100644
--- a/src/PAPRStats.cpp
+++ b/src/PAPRStats.cpp
@@ -33,7 +33,6 @@
# include <iostream>
#endif
-
PAPRStats::PAPRStats(size_t num_blocks_to_accumulate) :
m_num_blocks_to_accumulate(num_blocks_to_accumulate)
{
diff --git a/src/PAPRStats.h b/src/PAPRStats.h
index 86ad8b0..a4ded86 100644
--- a/src/PAPRStats.h
+++ b/src/PAPRStats.h
@@ -31,12 +31,9 @@
#endif
#include <cstddef>
-#include <vector>
#include <deque>
#include <complex>
-typedef std::complex<float> complexf;
-
/* Helper class to calculate Peak-to-average-power ratio.
* Definition of PAPR:
*
@@ -53,6 +50,8 @@ typedef std::complex<float> complexf;
*/
class PAPRStats
{
+ typedef std::complex<float> complexf;
+
public:
PAPRStats(size_t num_blocks_to_accumulate);
diff --git a/src/PhaseReference.cpp b/src/PhaseReference.cpp
index 568e15e..71dec87 100644
--- a/src/PhaseReference.cpp
+++ b/src/PhaseReference.cpp
@@ -29,12 +29,10 @@
#include <stdexcept>
-using complexf = std::complex<float>;
-
/* ETSI EN 300 401 Table 43 (Clause 14.3.2)
* Contains h_{i,k} values
*/
-const uint8_t PhaseReference::d_h[4][32] = {
+static const uint8_t d_h[4][32] = {
/* h0 */ { 0, 2, 0, 0, 0, 0, 1, 1, 2, 0, 0, 0, 2, 2, 1, 1,
0, 2, 0, 0, 0, 0, 1, 1, 2, 0, 0, 0, 2, 2, 1, 1 },
/* h1 */ { 0, 3, 2, 3, 0, 1, 3, 0, 2, 1, 2, 3, 2, 3, 3, 0,
@@ -54,41 +52,80 @@ const uint8_t PhaseReference::d_h[4][32] = {
* Tables 44 to 47 describe the frequency interleaving done in
* FrequencyInterleaver.
*/
-PhaseReference::PhaseReference(unsigned int dabmode) :
+PhaseReference::PhaseReference(unsigned int dabmode, bool fixedPoint) :
ModInput(),
- d_dabmode(dabmode)
+ d_dabmode(dabmode),
+ d_fixedPoint(fixedPoint)
{
PDEBUG("PhaseReference::PhaseReference(%u) @ %p\n", dabmode, this);
switch (d_dabmode) {
case 1:
d_carriers = 1536;
- d_num = 2048;
break;
case 2:
d_carriers = 384;
- d_num = 512;
break;
case 3:
d_carriers = 192;
- d_num = 256;
break;
case 4:
d_dabmode = 0;
case 0:
d_carriers = 768;
- d_num = 1024;
break;
default:
throw std::runtime_error(
"PhaseReference::PhaseReference DAB mode not valid!");
}
- d_dataIn.resize(d_carriers);
- fillData();
+
+ if (d_fixedPoint) {
+ d_phaseRefFixed.fillData(d_dabmode, d_carriers);
+ }
+ else {
+ d_phaseRefCF32.fillData(d_dabmode, d_carriers);
+ }
}
-complexf convert(uint8_t data) {
+static const int table[][48][2] = {
+ { // Mode 0/4
+ // Positive part
+ { 0, 0 }, { 3, 1 }, { 2, 0 }, { 1, 2 }, { 0, 0 }, { 3, 1 },
+ { 2, 2 }, { 1, 2 }, { 0, 2 }, { 3, 1 }, { 2, 3 }, { 1, 0 },
+ // Negative part
+ { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 0, 2 }, { 1, 2 },
+ { 2, 0 }, { 3, 3 }, { 0, 3 }, { 1, 1 }, { 2, 3 }, { 3, 2 },
+ },
+ { // Mode 1
+ // Positive part
+ { 0, 3 }, { 3, 1 }, { 2, 1 }, { 1, 1 }, { 0, 2 }, { 3, 2 },
+ { 2, 1 }, { 1, 0 }, { 0, 2 }, { 3, 2 }, { 2, 3 }, { 1, 3 },
+ { 0, 0 }, { 3, 2 }, { 2, 1 }, { 1, 3 }, { 0, 3 }, { 3, 3 },
+ { 2, 3 }, { 1, 0 }, { 0, 3 }, { 3, 0 }, { 2, 1 }, { 1, 1 },
+ // Negative part
+ { 0, 1 }, { 1, 2 }, { 2, 0 }, { 3, 1 }, { 0, 3 }, { 1, 2 },
+ { 2, 2 }, { 3, 3 }, { 0, 2 }, { 1, 1 }, { 2, 2 }, { 3, 3 },
+ { 0, 1 }, { 1, 2 }, { 2, 3 }, { 3, 3 }, { 0, 2 }, { 1, 2 },
+ { 2, 2 }, { 3, 1 }, { 0, 1 }, { 1, 3 }, { 2, 1 }, { 3, 2 },
+ },
+ { // Mode 2
+ // Positive part
+ { 2, 0 }, { 1, 2 }, { 0, 2 }, { 3, 1 }, { 2, 0 }, { 1, 3 },
+ // Negative part
+ { 0, 2 }, { 1, 3 }, { 2, 2 }, { 3, 2 }, { 0, 1 }, { 1, 2 },
+ },
+ { // Mode 3
+ // Positive part
+ { 3, 2 }, { 2, 2 }, { 1, 2 },
+ // Negative part
+ { 0, 2 }, { 1, 3 }, { 2, 0 },
+ },
+};
+
+
+template <>
+complexf PhaseRefGen<complexf>::convert(uint8_t data) {
const complexf value[] = {
complexf(1, 0),
complexf(0, 1),
@@ -98,62 +135,37 @@ complexf convert(uint8_t data) {
return value[data % 4];
}
+template <>
+complexfix PhaseRefGen<complexfix>::convert(uint8_t data) {
+ constexpr auto one = fixed_16{1};
+ constexpr auto zero = fixed_16{0};
-void PhaseReference::fillData()
-{
- const int table[][48][2] = {
- { // Mode 0/4
- // Positive part
- { 0, 0 }, { 3, 1 }, { 2, 0 }, { 1, 2 }, { 0, 0 }, { 3, 1 },
- { 2, 2 }, { 1, 2 }, { 0, 2 }, { 3, 1 }, { 2, 3 }, { 1, 0 },
- // Negative part
- { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 0, 2 }, { 1, 2 },
- { 2, 0 }, { 3, 3 }, { 0, 3 }, { 1, 1 }, { 2, 3 }, { 3, 2 },
- },
- { // Mode 1
- // Positive part
- { 0, 3 }, { 3, 1 }, { 2, 1 }, { 1, 1 }, { 0, 2 }, { 3, 2 },
- { 2, 1 }, { 1, 0 }, { 0, 2 }, { 3, 2 }, { 2, 3 }, { 1, 3 },
- { 0, 0 }, { 3, 2 }, { 2, 1 }, { 1, 3 }, { 0, 3 }, { 3, 3 },
- { 2, 3 }, { 1, 0 }, { 0, 3 }, { 3, 0 }, { 2, 1 }, { 1, 1 },
- // Negative part
- { 0, 1 }, { 1, 2 }, { 2, 0 }, { 3, 1 }, { 0, 3 }, { 1, 2 },
- { 2, 2 }, { 3, 3 }, { 0, 2 }, { 1, 1 }, { 2, 2 }, { 3, 3 },
- { 0, 1 }, { 1, 2 }, { 2, 3 }, { 3, 3 }, { 0, 2 }, { 1, 2 },
- { 2, 2 }, { 3, 1 }, { 0, 1 }, { 1, 3 }, { 2, 1 }, { 3, 2 },
- },
- { // Mode 2
- // Positive part
- { 2, 0 }, { 1, 2 }, { 0, 2 }, { 3, 1 }, { 2, 0 }, { 1, 3 },
- // Negative part
- { 0, 2 }, { 1, 3 }, { 2, 2 }, { 3, 2 }, { 0, 1 }, { 1, 2 },
- },
- { // Mode 3
- // Positive part
- { 3, 2 }, { 2, 2 }, { 1, 2 },
- // Negative part
- { 0, 2 }, { 1, 3 }, { 2, 0 },
- },
+ const complexfix value[] = {
+ complexfix(one, zero),
+ complexfix(zero, one),
+ complexfix(-one, zero),
+ complexfix(zero, -one),
};
+ return value[data % 4];
+}
- if (d_dabmode > 3) {
- throw std::runtime_error(
- "PhaseReference::fillData invalid DAB mode!");
- }
-
- if (d_dataIn.size() != d_carriers) {
+template <typename T>
+void PhaseRefGen<T>::fillData(unsigned int dabmode, size_t carriers)
+{
+ dataIn.resize(carriers);
+ if (dataIn.size() != carriers) {
throw std::runtime_error(
- "PhaseReference::fillData d_dataIn has incorrect size!");
+ "PhaseReference::fillData dataIn has incorrect size!");
}
for (size_t index = 0,
offset = 0;
- index < d_dataIn.size();
+ index < dataIn.size();
++offset) {
for (size_t k = 0; k < 32; ++k) {
- d_dataIn[index++] = convert(
- d_h[ table[d_dabmode][offset][0] ][k] +
- table[d_dabmode][offset][1] );
+ dataIn[index++] = convert(
+ d_h[ table[dabmode][offset][0] ][k] +
+ table[dabmode][offset][1] );
}
}
}
@@ -163,7 +175,12 @@ int PhaseReference::process(Buffer* dataOut)
{
PDEBUG("PhaseReference::process(dataOut: %p)\n", dataOut);
- dataOut->setData(&d_dataIn[0], d_carriers * sizeof(complexf));
+ if (d_fixedPoint) {
+ dataOut->setData(d_phaseRefFixed.dataIn.data(), d_carriers * sizeof(complexfix));
+ }
+ else {
+ dataOut->setData(d_phaseRefCF32.dataIn.data(), d_carriers * sizeof(complexf));
+ }
return 1;
}
diff --git a/src/PhaseReference.h b/src/PhaseReference.h
index 6ecdc4e..735009c 100644
--- a/src/PhaseReference.h
+++ b/src/PhaseReference.h
@@ -32,25 +32,33 @@
#include "ModPlugin.h"
-#include <cstddef>
-#include <complex>
#include <vector>
+#include <cstddef>
+
+template <typename T>
+struct PhaseRefGen {
+ std::vector<T> dataIn;
+ void fillData(unsigned int dabmode, size_t carriers);
+
+ private:
+ T convert(uint8_t data);
+};
+
class PhaseReference : public ModInput
{
public:
- PhaseReference(unsigned int dabmode);
+ PhaseReference(unsigned int dabmode, bool fixedPoint);
int process(Buffer* dataOut) override;
const char* name() override { return "PhaseReference"; }
protected:
unsigned int d_dabmode;
+ bool d_fixedPoint;
size_t d_carriers;
- size_t d_num;
- const static uint8_t d_h[4][32];
- std::vector<std::complex<float> > d_dataIn;
- void fillData();
+ PhaseRefGen<complexf> d_phaseRefCF32;
+ PhaseRefGen<complexfix> d_phaseRefFixed;
};
diff --git a/src/QpskSymbolMapper.cpp b/src/QpskSymbolMapper.cpp
index e26853a..c12ad80 100644
--- a/src/QpskSymbolMapper.cpp
+++ b/src/QpskSymbolMapper.cpp
@@ -23,7 +23,6 @@
#include <cstdio>
#include <cstring>
#include <stdexcept>
-#include <complex>
#include <cmath>
#ifdef __SSE__
# include <xmmintrin.h>
@@ -32,12 +31,10 @@
#include "QpskSymbolMapper.h"
#include "PcDebug.h"
-
-typedef std::complex<float> complexf;
-
-QpskSymbolMapper::QpskSymbolMapper(size_t carriers) :
+QpskSymbolMapper::QpskSymbolMapper(size_t carriers, bool fixedPoint) :
ModCodec(),
- d_carriers(carriers) { }
+ m_fixedPoint(fixedPoint),
+ m_carriers(carriers) { }
int QpskSymbolMapper::process(Buffer* const dataIn, Buffer* dataOut)
{
@@ -45,112 +42,172 @@ int QpskSymbolMapper::process(Buffer* const dataIn, Buffer* dataOut)
"(dataIn: %p, dataOut: %p)\n",
dataIn, dataOut);
- dataOut->setLength(dataIn->getLength() * 4 * 2 * sizeof(float)); // 4 output complex symbols per input byte
-#ifdef __SSE__
- const uint8_t* in = reinterpret_cast<const uint8_t*>(dataIn->getData());
- __m128* out = reinterpret_cast<__m128*>(dataOut->getData());
-
- if (dataIn->getLength() % (d_carriers / 4) != 0) {
- throw std::runtime_error(
- "QpskSymbolMapper::process input size not valid: " +
- std::to_string(dataIn->getLength()) +
- "(input size) % (" + std::to_string(d_carriers) +
- " (carriers) / 4) != 0");
- }
+ // 4 output complex symbols per input byte
+
+ if (m_fixedPoint) {
+ dataOut->setLength(dataIn->getLength() * 4 * sizeof(complexfix));
+
+ using fixed_t = complexfix::value_type;
- const static __m128 symbols[16] = {
- _mm_setr_ps( M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, M_SQRT1_2),
- _mm_setr_ps( M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2),
- _mm_setr_ps( M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, M_SQRT1_2),
- _mm_setr_ps( M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2),
- _mm_setr_ps( M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2),
- _mm_setr_ps( M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2),
- _mm_setr_ps( M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2),
- _mm_setr_ps( M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2),
- _mm_setr_ps(-M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, M_SQRT1_2),
- _mm_setr_ps(-M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2),
- _mm_setr_ps(-M_SQRT1_2,- M_SQRT1_2, M_SQRT1_2, M_SQRT1_2),
- _mm_setr_ps(-M_SQRT1_2,- M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2),
- _mm_setr_ps(-M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2),
- _mm_setr_ps(-M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2),
- _mm_setr_ps(-M_SQRT1_2,- M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2),
- _mm_setr_ps(-M_SQRT1_2,- M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2)
- };
- size_t inOffset = 0;
- size_t outOffset = 0;
- uint8_t tmp = 0;
- for (size_t i = 0; i < dataIn->getLength(); i += d_carriers / 4) {
- for (size_t j = 0; j < d_carriers / 8; ++j) {
- tmp = (in[inOffset] & 0xc0) >> 4;
- tmp |= (in[inOffset + (d_carriers / 8)] & 0xc0) >> 6;
- out[outOffset] = symbols[tmp];
- tmp = (in[inOffset] & 0x30) >> 2;
- tmp |= (in[inOffset + (d_carriers / 8)] & 0x30) >> 4;
- out[outOffset + 1] = symbols[tmp];
- tmp = (in[inOffset] & 0x0c);
- tmp |= (in[inOffset + (d_carriers / 8)] & 0x0c) >> 2;
- out[outOffset + 2] = symbols[tmp];
- tmp = (in[inOffset] & 0x03) << 2;
- tmp |= (in[inOffset + (d_carriers / 8)] & 0x03);
- out[outOffset + 3] = symbols[tmp];
- ++inOffset;
- outOffset += 4;
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(dataIn->getData());
+ fixed_t* out = reinterpret_cast<fixed_t*>(dataOut->getData());
+
+ if (dataIn->getLength() % (m_carriers / 4) != 0) {
+ throw std::runtime_error(
+ "QpskSymbolMapper::process input size not valid!");
+ }
+
+ constexpr fixed_t v = static_cast<fixed_t>(M_SQRT1_2);
+
+ const static fixed_t symbols[16][4] = {
+ { v, v, v, v},
+ { v, v, v, -v},
+ { v, -v, v, v},
+ { v, -v, v, -v},
+ { v, v, -v, v},
+ { v, v, -v, -v},
+ { v, -v, -v, v},
+ { v, -v, -v, -v},
+ {-v, v, v, v},
+ {-v, v, v, -v},
+ {-v, -v, v, v},
+ {-v, -v, v, -v},
+ {-v, v, -v, v},
+ {-v, v, -v, -v},
+ {-v, -v, -v, v},
+ {-v, -v, -v, -v}
+ };
+ size_t inOffset = 0;
+ size_t outOffset = 0;
+ uint8_t tmp;
+ for (size_t i = 0; i < dataIn->getLength(); i += m_carriers / 4) {
+ for (size_t j = 0; j < m_carriers / 8; ++j) {
+ tmp = (in[inOffset] & 0xc0) >> 4;
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0xc0) >> 6;
+ memcpy(&out[outOffset], symbols[tmp], sizeof(fixed_t) * 4);
+ tmp = (in[inOffset] & 0x30) >> 2;
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0x30) >> 4;
+ memcpy(&out[outOffset + 4], symbols[tmp], sizeof(fixed_t) * 4);
+ tmp = (in[inOffset] & 0x0c);
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0x0c) >> 2;
+ memcpy(&out[outOffset + 8], symbols[tmp], sizeof(fixed_t) * 4);
+ tmp = (in[inOffset] & 0x03) << 2;
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0x03);
+ memcpy(&out[outOffset + 12], symbols[tmp], sizeof(fixed_t) * 4);
+ ++inOffset;
+ outOffset += 4*4;
+ }
+ inOffset += m_carriers / 8;
}
- inOffset += d_carriers / 8;
}
+ else {
+ dataOut->setLength(dataIn->getLength() * 4 * sizeof(complexf));
+#ifdef __SSE__
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(dataIn->getData());
+ __m128* out = reinterpret_cast<__m128*>(dataOut->getData());
+
+ if (dataIn->getLength() % (m_carriers / 4) != 0) {
+ throw std::runtime_error(
+ "QpskSymbolMapper::process input size not valid: " +
+ std::to_string(dataIn->getLength()) +
+ "(input size) % (" + std::to_string(m_carriers) +
+ " (carriers) / 4) != 0");
+ }
+
+ const static __m128 symbols[16] = {
+ _mm_setr_ps( M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, M_SQRT1_2),
+ _mm_setr_ps( M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2),
+ _mm_setr_ps( M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, M_SQRT1_2),
+ _mm_setr_ps( M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2),
+ _mm_setr_ps( M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2),
+ _mm_setr_ps( M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2),
+ _mm_setr_ps( M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2),
+ _mm_setr_ps( M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2),
+ _mm_setr_ps(-M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, M_SQRT1_2),
+ _mm_setr_ps(-M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2),
+ _mm_setr_ps(-M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, M_SQRT1_2),
+ _mm_setr_ps(-M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2),
+ _mm_setr_ps(-M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2),
+ _mm_setr_ps(-M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2),
+ _mm_setr_ps(-M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2),
+ _mm_setr_ps(-M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2)
+ };
+ size_t inOffset = 0;
+ size_t outOffset = 0;
+ uint8_t tmp = 0;
+ for (size_t i = 0; i < dataIn->getLength(); i += m_carriers / 4) {
+ for (size_t j = 0; j < m_carriers / 8; ++j) {
+ tmp = (in[inOffset] & 0xc0) >> 4;
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0xc0) >> 6;
+ out[outOffset] = symbols[tmp];
+ tmp = (in[inOffset] & 0x30) >> 2;
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0x30) >> 4;
+ out[outOffset + 1] = symbols[tmp];
+ tmp = (in[inOffset] & 0x0c);
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0x0c) >> 2;
+ out[outOffset + 2] = symbols[tmp];
+ tmp = (in[inOffset] & 0x03) << 2;
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0x03);
+ out[outOffset + 3] = symbols[tmp];
+ ++inOffset;
+ outOffset += 4;
+ }
+ inOffset += m_carriers / 8;
+ }
#else // !__SSE__
- const uint8_t* in = reinterpret_cast<const uint8_t*>(dataIn->getData());
- float* out = reinterpret_cast<float*>(dataOut->getData());
- if (dataIn->getLength() % (d_carriers / 4) != 0) {
- throw std::runtime_error(
- "QpskSymbolMapper::process input size not valid!");
- }
- if (dataOut->getLength() / sizeof(float) != dataIn->getLength() * 4 * 2) { // 4 output complex symbols per input byte
- throw std::runtime_error(
- "QpskSymbolMapper::process output size not valid!");
- }
+ const uint8_t* in = reinterpret_cast<const uint8_t*>(dataIn->getData());
+ float* out = reinterpret_cast<float*>(dataOut->getData());
+ if (dataIn->getLength() % (m_carriers / 4) != 0) {
+ throw std::runtime_error(
+ "QpskSymbolMapper::process input size not valid!");
+ }
+ if (dataOut->getLength() / sizeof(float) != dataIn->getLength() * 4 * 2) { // 4 output complex symbols per input byte
+ throw std::runtime_error(
+ "QpskSymbolMapper::process output size not valid!");
+ }
- const static float symbols[16][4] = {
- { M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, M_SQRT1_2},
- { M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2},
- { M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, M_SQRT1_2},
- { M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2},
- { M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2},
- { M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2},
- { M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2},
- { M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2},
- {-M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, M_SQRT1_2},
- {-M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2},
- {-M_SQRT1_2,- M_SQRT1_2, M_SQRT1_2, M_SQRT1_2},
- {-M_SQRT1_2,- M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2},
- {-M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2},
- {-M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2},
- {-M_SQRT1_2,- M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2},
- {-M_SQRT1_2,- M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2}
- };
- size_t inOffset = 0;
- size_t outOffset = 0;
- uint8_t tmp;
- for (size_t i = 0; i < dataIn->getLength(); i += d_carriers / 4) {
- for (size_t j = 0; j < d_carriers / 8; ++j) {
- tmp = (in[inOffset] & 0xc0) >> 4;
- tmp |= (in[inOffset + (d_carriers / 8)] & 0xc0) >> 6;
- memcpy(&out[outOffset], symbols[tmp], sizeof(float) * 4);
- tmp = (in[inOffset] & 0x30) >> 2;
- tmp |= (in[inOffset + (d_carriers / 8)] & 0x30) >> 4;
- memcpy(&out[outOffset + 4], symbols[tmp], sizeof(float) * 4);
- tmp = (in[inOffset] & 0x0c);
- tmp |= (in[inOffset + (d_carriers / 8)] & 0x0c) >> 2;
- memcpy(&out[outOffset + 8], symbols[tmp], sizeof(float) * 4);
- tmp = (in[inOffset] & 0x03) << 2;
- tmp |= (in[inOffset + (d_carriers / 8)] & 0x03);
- memcpy(&out[outOffset + 12], symbols[tmp], sizeof(float) * 4);
- ++inOffset;
- outOffset += 4*4;
+ const static float symbols[16][4] = {
+ { M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, M_SQRT1_2},
+ { M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2},
+ { M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, M_SQRT1_2},
+ { M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2},
+ { M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2},
+ { M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2},
+ { M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2},
+ { M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2},
+ {-M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, M_SQRT1_2},
+ {-M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2},
+ {-M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, M_SQRT1_2},
+ {-M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2},
+ {-M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2},
+ {-M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2},
+ {-M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, M_SQRT1_2},
+ {-M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2, -M_SQRT1_2}
+ };
+ size_t inOffset = 0;
+ size_t outOffset = 0;
+ uint8_t tmp;
+ for (size_t i = 0; i < dataIn->getLength(); i += m_carriers / 4) {
+ for (size_t j = 0; j < m_carriers / 8; ++j) {
+ tmp = (in[inOffset] & 0xc0) >> 4;
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0xc0) >> 6;
+ memcpy(&out[outOffset], symbols[tmp], sizeof(float) * 4);
+ tmp = (in[inOffset] & 0x30) >> 2;
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0x30) >> 4;
+ memcpy(&out[outOffset + 4], symbols[tmp], sizeof(float) * 4);
+ tmp = (in[inOffset] & 0x0c);
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0x0c) >> 2;
+ memcpy(&out[outOffset + 8], symbols[tmp], sizeof(float) * 4);
+ tmp = (in[inOffset] & 0x03) << 2;
+ tmp |= (in[inOffset + (m_carriers / 8)] & 0x03);
+ memcpy(&out[outOffset + 12], symbols[tmp], sizeof(float) * 4);
+ ++inOffset;
+ outOffset += 4*4;
+ }
+ inOffset += m_carriers / 8;
}
- inOffset += d_carriers / 8;
- }
#endif // __SSE__
+ }
return 1;
}
diff --git a/src/QpskSymbolMapper.h b/src/QpskSymbolMapper.h
index dbcf4dd..6cf7a2e 100644
--- a/src/QpskSymbolMapper.h
+++ b/src/QpskSymbolMapper.h
@@ -31,12 +31,13 @@
class QpskSymbolMapper : public ModCodec
{
public:
- QpskSymbolMapper(size_t carriers);
+ QpskSymbolMapper(size_t carriers, bool fixedPoint);
int process(Buffer* const dataIn, Buffer* dataOut);
const char* name() { return "QpskSymbolMapper"; }
protected:
- size_t d_carriers;
+ bool m_fixedPoint;
+ size_t m_carriers;
};
diff --git a/src/Resampler.h b/src/Resampler.h
index d1a9f7a..2c810f6 100644
--- a/src/Resampler.h
+++ b/src/Resampler.h
@@ -37,9 +37,6 @@
#define FFT_TYPE fftwf_complex
#define FFT_PLAN fftwf_plan
-#include <complex>
-typedef std::complex<float> complexf;
-
class Resampler : public ModCodec
{
diff --git a/src/SignalMultiplexer.cpp b/src/SignalMultiplexer.cpp
index 1d95bdd..d4955d0 100644
--- a/src/SignalMultiplexer.cpp
+++ b/src/SignalMultiplexer.cpp
@@ -22,25 +22,20 @@
#include "SignalMultiplexer.h"
#include "PcDebug.h"
-#include <stdio.h>
-#include <stdexcept>
+#include <cstdio>
#include <assert.h>
-#include <string.h>
-SignalMultiplexer::SignalMultiplexer(size_t framesize) :
- ModMux(),
- d_frameSize(framesize)
+SignalMultiplexer::SignalMultiplexer() :
+ ModMux()
{
- PDEBUG("SignalMultiplexer::SignalMultiplexer(%zu) @ %p\n", framesize, this);
-
+ PDEBUG("SignalMultiplexer::SignalMultiplexer() @ %p\n", this);
}
SignalMultiplexer::~SignalMultiplexer()
{
PDEBUG("SignalMultiplexer::~SignalMultiplexer() @ %p\n", this);
-
}
diff --git a/src/SignalMultiplexer.h b/src/SignalMultiplexer.h
index 5186a8d..1f6bc12 100644
--- a/src/SignalMultiplexer.h
+++ b/src/SignalMultiplexer.h
@@ -36,7 +36,7 @@
class SignalMultiplexer : public ModMux
{
public:
- SignalMultiplexer(size_t frameSize);
+ SignalMultiplexer();
virtual ~SignalMultiplexer();
SignalMultiplexer(const SignalMultiplexer&);
SignalMultiplexer& operator=(const SignalMultiplexer&);
@@ -44,8 +44,5 @@ public:
int process(std::vector<Buffer*> dataIn, Buffer* dataOut);
const char* name() { return "SignalMultiplexer"; }
-
-protected:
- size_t d_frameSize;
};
diff --git a/src/TII.cpp b/src/TII.cpp
index 904f3ff..bce15aa 100644
--- a/src/TII.cpp
+++ b/src/TII.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2024
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -27,11 +27,8 @@
#include "TII.h"
#include "PcDebug.h"
-#include <stdio.h>
-#include <stdexcept>
-#include <string.h>
-
-typedef std::complex<float> complexf;
+#include <cstdio>
+#include <cstring>
/* TII pattern for TM I, II, IV */
const int pattern_tm1_2_4[][8] = { // {{{
@@ -106,11 +103,12 @@ const int pattern_tm1_2_4[][8] = { // {{{
{1,1,1,0,1,0,0,0},
{1,1,1,1,0,0,0,0} }; // }}}
-TII::TII(unsigned int dabmode, tii_config_t& tii_config) :
+TII::TII(unsigned int dabmode, tii_config_t& tii_config, bool fixedPoint) :
ModCodec(),
RemoteControllable("tii"),
m_dabmode(dabmode),
- m_conf(tii_config)
+ m_conf(tii_config),
+ m_fixedPoint(fixedPoint)
{
PDEBUG("TII::TII(%u) @ %p\n", dabmode, this);
@@ -171,56 +169,72 @@ const char* TII::name()
return m_name.c_str();
}
+template<typename T>
+void do_process(size_t carriers, bool old_variant, const std::vector<bool>& Acp, Buffer* dataIn, Buffer* dataOut)
+{
+ const T* in = reinterpret_cast<const T*>(dataIn->getData());
+ T* out = reinterpret_cast<T*>(dataOut->getData());
+
+ /* Normalise the TII carrier power according to ETSI TR 101 496-3
+ * Clause 5.4.2.2 Paragraph 7:
+ *
+ * > The ratio of carriers in a TII symbol to a normal DAB symbol
+ * > is 1:48 for all Modes, so that the signal power in a TII symbol is
+ * > 16 dB below the signal power of the other symbols.
+ *
+ * This is because we only enable 32 out of 1536 carriers, not because
+ * every carrier is lower power.
+ */
+ for (size_t i = 0; i < Acp.size(); i++) {
+ /* See header file for an explanation of the old variant.
+ *
+ * A_{c,p}(k) and A_{c,p}(k-1) are never both simultaneously true,
+ * so instead of doing the sum inside z_{m,0,k}, we could do
+ *
+ * if (m_Acp[i]) out[i] = in[i];
+ * if (m_Acp[i-1]) out[i] = in[i-1]
+ *
+ * (Considering only the new variant)
+ *
+ * To avoid messing with indices, we substitute j = i-1
+ *
+ * if (m_Acp[i]) out[i] = in[i];
+ * if (m_Acp[j]) out[j+1] = in[j]
+ *
+ * and fuse the two conditionals together:
+ */
+ if (Acp[i]) {
+ out[i] = in[i];
+ out[i+1] = (old_variant ? in[i+1] : in[i]);
+ }
+ }
+}
int TII::process(Buffer* dataIn, Buffer* dataOut)
{
+ const size_t sizeof_samples = m_fixedPoint ? sizeof(complexfix) : sizeof(complexf);
+
PDEBUG("TII::process(dataOut: %p)\n",
dataOut);
if ( (dataIn == NULL) or
- (dataIn->getLength() != m_carriers * sizeof(complexf))) {
+ (dataIn->getLength() != m_carriers * sizeof_samples)) {
throw TIIError("TII::process input size not valid!");
}
- dataOut->setLength(m_carriers * sizeof(complexf));
- memset(dataOut->getData(), 0, dataOut->getLength());
+ dataOut->setLength(m_carriers * sizeof_samples);
+ memset(dataOut->getData(), 0, dataOut->getLength());
if (m_conf.enable and m_insert) {
std::lock_guard<std::mutex> lock(m_enabled_carriers_mutex);
- complexf* in = reinterpret_cast<complexf*>(dataIn->getData());
- complexf* out = reinterpret_cast<complexf*>(dataOut->getData());
-
- /* Normalise the TII carrier power according to ETSI TR 101 496-3
- * Clause 5.4.2.2 Paragraph 7:
- *
- * > The ratio of carriers in a TII symbol to a normal DAB symbol
- * > is 1:48 for all Modes, so that the signal power in a TII symbol is
- * > 16 dB below the signal power of the other symbols.
- *
- * This is because we only enable 32 out of 1536 carriers, not because
- * every carrier is lower power.
- */
- for (size_t i = 0; i < m_Acp.size(); i++) {
- /* See header file for an explanation of the old variant.
- *
- * A_{c,p}(k) and A_{c,p}(k-1) are never both simultaneously true,
- * so instead of doing the sum inside z_{m,0,k}, we could do
- *
- * if (m_Acp[i]) out[i] = in[i];
- * if (m_Acp[i-1]) out[i] = in[i-1]
- *
- * (Considering only the new variant)
- *
- * To avoid messing with indices, we substitute j = i-1
- *
- * if (m_Acp[i]) out[i] = in[i];
- * if (m_Acp[j]) out[j+1] = in[j]
- *
- * and fuse the two conditionals together:
- */
- if (m_Acp[i]) {
- out[i] = in[i];
- out[i+1] = (m_conf.old_variant ? in[i+1] : in[i]);
- }
+ if (m_fixedPoint) {
+ do_process<complexfix>(
+ m_carriers, m_conf.old_variant, m_Acp,
+ dataIn, dataOut);
+ }
+ else {
+ do_process<complexf>(
+ m_carriers, m_conf.old_variant, m_Acp,
+ dataIn, dataOut);
}
}
@@ -385,3 +399,12 @@ const std::string TII::get_parameter(const std::string& parameter) const
return ss.str();
}
+const json::map_t TII::get_all_values() const
+{
+ json::map_t map;
+ map["enable"].v = m_conf.enable;
+ map["pattern"].v = m_conf.pattern;
+ map["comb"].v = m_conf.comb;
+ map["old_variant"].v = m_conf.old_variant;
+ return map;
+}
diff --git a/src/TII.h b/src/TII.h
index d8c785d..6fe4d4f 100644
--- a/src/TII.h
+++ b/src/TII.h
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty
the Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2024
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -36,8 +36,6 @@
#include "RemoteControl.h"
#include <cstddef>
-#include <thread>
-#include <complex>
#include <vector>
#include <string>
@@ -81,17 +79,16 @@ class TIIError : public std::runtime_error {
class TII : public ModCodec, public RemoteControllable
{
public:
- TII(unsigned int dabmode, tii_config_t& tii_config);
+ TII(unsigned int dabmode, tii_config_t& tii_config, bool fixedPoint);
+ virtual ~TII() {}
- int process(Buffer* dataIn, Buffer* dataOut);
- const char* name();
+ int process(Buffer* dataIn, Buffer* dataOut) override;
+ const char* name() override;
/******* REMOTE CONTROL ********/
- virtual void set_parameter(const std::string& parameter,
- const std::string& value);
-
- virtual const std::string get_parameter(
- const std::string& parameter) const;
+ virtual void set_parameter(const std::string& parameter, const std::string& value) override;
+ virtual const std::string get_parameter(const std::string& parameter) const override;
+ virtual const json::map_t get_all_values() const override;
protected:
// Fill m_Acp with the correct carriers for the pattern/comb
@@ -107,6 +104,8 @@ class TII : public ModCodec, public RemoteControllable
// Remote-controllable settings
tii_config_t& m_conf;
+ bool m_fixedPoint = false;
+
// Internal flag when to insert TII
bool m_insert = true;
diff --git a/src/TimestampDecoder.cpp b/src/TimestampDecoder.cpp
index 3cfa0cc..a7972c9 100644
--- a/src/TimestampDecoder.cpp
+++ b/src/TimestampDecoder.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -36,6 +36,31 @@
//#define MDEBUG(fmt, args...) fprintf (LOG, "*****" fmt , ## args)
#define MDEBUG(fmt, args...) PDEBUG(fmt, ## args)
+double frame_timestamp::offset_to_system_time() const
+{
+ if (not timestamp_valid) {
+ throw new std::runtime_error("Cannot calculate offset for invalid timestamp");
+ }
+
+ struct timespec t;
+ if (clock_gettime(CLOCK_REALTIME, &t) != 0) {
+ throw std::runtime_error(std::string("Failed to retrieve CLOCK_REALTIME") + strerror(errno));
+ }
+
+ return get_real_secs() - (double)t.tv_sec - (t.tv_nsec / 1000000000.0);
+}
+
+std::string frame_timestamp::to_string() const
+{
+ time_t s = timestamp_sec;
+ std::stringstream ss;
+ char timestr[100];
+ if (std::strftime(timestr, sizeof(timestr), "%Y-%m-%dZ%H:%M:%S", std::gmtime(&s))) {
+ ss << timestr << " + " << ((double)timestamp_pps / 16384000.0);
+ }
+ return ss.str();
+}
+
frame_timestamp& frame_timestamp::operator+=(const double& diff)
{
double offset_pps, offset_secs;
@@ -75,20 +100,21 @@ TimestampDecoder::TimestampDecoder(double& offset_s) :
timestamp_offset << " offset";
}
-std::shared_ptr<frame_timestamp> TimestampDecoder::getTimestamp()
+frame_timestamp TimestampDecoder::getTimestamp()
{
- auto ts = std::make_shared<frame_timestamp>();
+ frame_timestamp ts;
- ts->timestamp_valid = full_timestamp_received;
- ts->timestamp_sec = time_secs;
- ts->timestamp_pps = time_pps;
- ts->fct = latestFCT;
- ts->fp = latestFP;
+ ts.timestamp_valid = full_timestamp_received;
+ ts.timestamp_sec = time_secs;
+ ts.timestamp_pps = time_pps;
+ ts.fct = latestFCT;
+ ts.fp = latestFP;
- ts->timestamp_refresh = offset_changed;
+ ts.timestamp_offset = timestamp_offset;
+ ts.offset_changed = offset_changed;
offset_changed = false;
- *ts += timestamp_offset;
+ ts += timestamp_offset;
return ts;
}
@@ -275,3 +301,22 @@ const std::string TimestampDecoder::get_parameter(
return ss.str();
}
+const json::map_t TimestampDecoder::get_all_values() const
+{
+ json::map_t map;
+ map["offset"].v = timestamp_offset;
+ if (full_timestamp_received) {
+ map["timestamp"].v = time_secs + ((double)time_pps / 16384000.0);
+ }
+ else {
+ map["timestamp"].v = std::nullopt;
+ }
+
+ if (full_timestamp_received) {
+ map["timestamp0"].v = time_secs_of_frame0 + ((double)time_pps_of_frame0 / 16384000.0);
+ }
+ else {
+ map["timestamp0"].v = std::nullopt;
+ }
+ return map;
+}
diff --git a/src/TimestampDecoder.h b/src/TimestampDecoder.h
index d083061..25796ca 100644
--- a/src/TimestampDecoder.h
+++ b/src/TimestampDecoder.h
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -39,10 +39,12 @@ struct frame_timestamp
int32_t fct;
uint8_t fp; // Frame Phase
- uint32_t timestamp_sec;
+ uint32_t timestamp_sec; // seconds in unix epoch
uint32_t timestamp_pps; // In units of 1/16384000 s
bool timestamp_valid = false;
- bool timestamp_refresh;
+
+ double timestamp_offset = 0.0; // copy of the configured modulator offset
+ bool offset_changed = false;
frame_timestamp& operator+=(const double& diff);
@@ -56,6 +58,8 @@ struct frame_timestamp
return timestamp_pps / 16384000.0;
}
+ double offset_to_system_time() const;
+
double get_real_secs() const {
double t = timestamp_sec;
t += pps_offset();
@@ -74,6 +78,8 @@ struct frame_timestamp
timestamp_pps = lrint(subsecond * 16384000.0);
}
+ std::string to_string() const;
+
void print(const char* t) const {
etiLog.log(debug,
"%s <frame_timestamp(%s, %d, %.9f, %d)>\n",
@@ -92,8 +98,9 @@ class TimestampDecoder : public RemoteControllable
* frame transmission
*/
TimestampDecoder(double& offset_s);
+ virtual ~TimestampDecoder() {}
- std::shared_ptr<frame_timestamp> getTimestamp(void);
+ frame_timestamp getTimestamp(void);
/* Update timestamp data from ETI */
void updateTimestampEti(
@@ -112,16 +119,12 @@ class TimestampDecoder : public RemoteControllable
/*********** REMOTE CONTROL ***************/
/* Base function to set parameters. */
- virtual void set_parameter(const std::string& parameter,
- const std::string& value);
-
- /* Getting a parameter always returns a string. */
- virtual const std::string get_parameter(
- const std::string& parameter) const;
+ virtual void set_parameter(const std::string& parameter, const std::string& value) override;
+ virtual const std::string get_parameter(const std::string& parameter) const override;
+ virtual const json::map_t get_all_values() const override;
const char* name() { return "TS"; }
-
protected:
/* Push a new MNSC field into the decoder */
void pushMNSCData(uint8_t framephase, uint16_t mnsc);
diff --git a/src/Utils.cpp b/src/Utils.cpp
index f39c4c9..23e0aa5 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -26,11 +26,16 @@
*/
#include "Utils.h"
-#include "GainControl.h"
+
+#include <ctime>
+#include <cstring>
+#include <sstream>
+#include <iomanip>
+#include <pthread.h>
#if defined(HAVE_PRCTL)
# include <sys/prctl.h>
#endif
-#include <pthread.h>
+
static void printHeader()
{
@@ -61,11 +66,10 @@ static void printHeader()
#if defined(__SSE__)
"SSE " <<
#endif
- "\n";
-
-#if defined(BUILD_FOR_EASYDABV3)
- std::cerr << " This is a build for the EasyDABv3 board" << std::endl;
+#if defined(__ARM_NEON)
+ "NEON " <<
#endif
+ "\n";
}
void printUsage(const char* progName)
@@ -77,15 +81,15 @@ void printUsage(const char* progName)
fprintf(out, "Usage with command line options:\n");
fprintf(out, "\t%s"
" input"
-#if defined(BUILD_FOR_EASYDABV3)
- " -f filename -F format"
-#else
- " (-f filename -F format | -u uhddevice -F frequency)"
-#endif
- " [-o offset]"
-#if !defined(BUILD_FOR_EASYDABV3)
+ " (-f filename -F format"
+#if defined(HAVE_OUTPUT_UHD)
+ " | -u uhddevice -F frequency"
+#endif // defined(HAVE_OUTPUT_UHD)
+ ") [-o offset]"
"\n\t"
+#if defined(HAVE_OUTPUT_UHD)
" [-G txgain]"
+#endif // defined(HAVE_OUTPUT_UHD)
" [-T filter_taps_file]"
" [-a gain]"
" [-c clockrate]"
@@ -93,14 +97,12 @@ void printUsage(const char* progName)
" [-g gainMode]"
" [-m dabMode]"
" [-r samplingRate]"
-#endif
" [-l]"
" [-h]"
"\n", progName);
fprintf(out, "Where:\n");
fprintf(out, "input: ETI input filename (default: stdin), or\n");
fprintf(out, " tcp://source:port for ETI-over-TCP input, or\n");
- fprintf(out, " zmq+tcp://source:port for ZMQ input.\n");
fprintf(out, " udp://:port for EDI input.\n");
fprintf(out, "-f name: Use file output with given filename. (use /dev/stdout for standard output)\n");
fprintf(out, "-F format: Set the output format (see doc/example.ini for formats) for the file output.\n");
@@ -108,10 +110,11 @@ void printUsage(const char* progName)
fprintf(out, " Specifying this option has two implications: It enables synchronous transmission,\n"
" requiring an external REFCLK and PPS signal and frames that do not contain a valid timestamp\n"
" get muted.\n\n");
-#if !defined(BUILD_FOR_EASYDABV3)
+#if defined(HAVE_OUTPUT_UHD)
fprintf(out, "-u device: Use UHD output with given device string. (use "" for default device)\n");
fprintf(out, "-F frequency: Set the transmit frequency when using UHD output. (mandatory option when using UHD)\n");
fprintf(out, "-G txgain: Set the transmit gain for the UHD driver (default: 0)\n");
+#endif // defined(HAVE_OUTPUT_UHD)
fprintf(out, "-T taps_file: Enable filtering before the output, using the specified file containing the filter taps.\n");
fprintf(out, " Use 'default' as taps_file to use the internal taps.\n");
fprintf(out, "-a gain: Apply digital amplitude gain.\n");
@@ -119,7 +122,6 @@ void printUsage(const char* progName)
fprintf(out, "-g gainmode: Set computation gain mode: fix, max or var\n");
fprintf(out, "-m mode: Set DAB mode: (0: auto, 1-4: force).\n");
fprintf(out, "-r rate: Set output sampling rate (default: 2048000).\n\n");
-#endif
fprintf(out, "-l: Loop file when reach end of file.\n");
fprintf(out, "-h: Print this help.\n");
}
@@ -132,7 +134,7 @@ void printVersion(void)
" ODR-DabMod is copyright (C) Her Majesty the Queen in Right of Canada,\n"
" 2005 -- 2012 Communications Research Centre (CRC),\n"
" and\n"
- " Copyright (C) 2018 Matthias P. Braendli, matthias.braendli@mpb.li\n"
+ " Copyright (C) 2023 Matthias P. Braendli, matthias.braendli@mpb.li\n"
"\n"
" http://opendigitalradio.org\n"
"\n"
@@ -157,6 +159,87 @@ void printStartupInfo()
printHeader();
}
+void printModSettings(const mod_settings_t& mod_settings)
+{
+ std::stringstream ss;
+ // Print settings
+ ss << "Input\n";
+ ss << " Type: " << mod_settings.inputTransport << "\n";
+ ss << " Source: " << mod_settings.inputName << "\n";
+
+ ss << "Output\n";
+
+ if (mod_settings.useFileOutput) {
+ ss << " Name: " << mod_settings.outputName << "\n";
+ }
+#if defined(HAVE_OUTPUT_UHD)
+ else if (mod_settings.useUHDOutput) {
+ ss << " UHD\n" <<
+ " Device: " << mod_settings.sdr_device_config.device << "\n" <<
+ " Subdevice: " <<
+ mod_settings.sdr_device_config.subDevice << "\n" <<
+ " master_clock_rate: " <<
+ mod_settings.sdr_device_config.masterClockRate << "\n" <<
+ " refclk: " <<
+ mod_settings.sdr_device_config.refclk_src << "\n" <<
+ " pps source: " <<
+ mod_settings.sdr_device_config.pps_src << "\n";
+ }
+#endif
+#if defined(HAVE_SOAPYSDR)
+ else if (mod_settings.useSoapyOutput) {
+ ss << " SoapySDR\n"
+ " Device: " << mod_settings.sdr_device_config.device << "\n" <<
+ " master_clock_rate: " <<
+ mod_settings.sdr_device_config.masterClockRate << "\n";
+ }
+#endif
+#if defined(HAVE_DEXTER)
+ else if (mod_settings.useDexterOutput) {
+ ss << " PrecisionWave DEXTER\n";
+ }
+#endif
+#if defined(HAVE_LIMESDR)
+ else if (mod_settings.useLimeOutput) {
+ ss << " LimeSDR\n"
+ " Device: " << mod_settings.sdr_device_config.device << "\n" <<
+ " master_clock_rate: " <<
+ mod_settings.sdr_device_config.masterClockRate << "\n";
+ }
+#endif
+#if defined(HAVE_BLADERF)
+ else if (mod_settings.useBladeRFOutput) {
+ ss << " BladeRF\n"
+ " Device: " << mod_settings.sdr_device_config.device << "\n" <<
+ " refclk: " << mod_settings.sdr_device_config.refclk_src << "\n";
+ }
+#endif
+ else if (mod_settings.useZeroMQOutput) {
+ ss << " ZeroMQ\n" <<
+ " Listening on: " << mod_settings.outputName << "\n" <<
+ " Socket type : " << mod_settings.zmqOutputSocketType << "\n";
+ }
+
+ ss << " Sampling rate: ";
+ if (mod_settings.outputRate > 1000) {
+ if (mod_settings.outputRate > 1000000) {
+ ss << std::fixed << std::setprecision(4) <<
+ mod_settings.outputRate / 1000000.0 <<
+ " MHz\n";
+ }
+ else {
+ ss << std::fixed << std::setprecision(4) <<
+ mod_settings.outputRate / 1000.0 <<
+ " kHz\n";
+ }
+ }
+ else {
+ ss << std::fixed << std::setprecision(4) <<
+ mod_settings.outputRate << " Hz\n";
+ }
+ fprintf(stderr, "%s", ss.str().c_str());
+}
+
int set_realtime_prio(int prio)
{
// Set thread priority to realtime
@@ -174,7 +257,7 @@ void set_thread_name(const char *name)
#endif
}
-double parseChannel(const std::string& chan)
+double parse_channel(const std::string& chan)
{
double freq;
if (chan == "5A") freq = 174928000;
@@ -216,12 +299,59 @@ double parseChannel(const std::string& chan)
else if (chan == "13E") freq = 237488000;
else if (chan == "13F") freq = 239200000;
else {
- std::cerr << " soapy output: channel " << chan << " does not exist in table\n";
- throw std::out_of_range("soapy channel selection error");
+ std::cerr << "Channel " << chan << " does not exist in table\n";
+ throw std::out_of_range("channel out of range");
}
return freq;
}
+std::optional<std::string> convert_frequency_to_channel(double frequency)
+{
+ const int freq = round(frequency);
+ std::string chan;
+ if (freq == 174928000) chan = "5A";
+ else if (freq == 176640000) chan = "5B";
+ else if (freq == 178352000) chan = "5C";
+ else if (freq == 180064000) chan = "5D";
+ else if (freq == 181936000) chan = "6A";
+ else if (freq == 183648000) chan = "6B";
+ else if (freq == 185360000) chan = "6C";
+ else if (freq == 187072000) chan = "6D";
+ else if (freq == 188928000) chan = "7A";
+ else if (freq == 190640000) chan = "7B";
+ else if (freq == 192352000) chan = "7C";
+ else if (freq == 194064000) chan = "7D";
+ else if (freq == 195936000) chan = "8A";
+ else if (freq == 197648000) chan = "8B";
+ else if (freq == 199360000) chan = "8C";
+ else if (freq == 201072000) chan = "8D";
+ else if (freq == 202928000) chan = "9A";
+ else if (freq == 204640000) chan = "9B";
+ else if (freq == 206352000) chan = "9C";
+ else if (freq == 208064000) chan = "9D";
+ else if (freq == 209936000) chan = "10A";
+ else if (freq == 211648000) chan = "10B";
+ else if (freq == 213360000) chan = "10C";
+ else if (freq == 215072000) chan = "10D";
+ else if (freq == 216928000) chan = "11A";
+ else if (freq == 218640000) chan = "11B";
+ else if (freq == 220352000) chan = "11C";
+ else if (freq == 222064000) chan = "11D";
+ else if (freq == 223936000) chan = "12A";
+ else if (freq == 225648000) chan = "12B";
+ else if (freq == 227360000) chan = "12C";
+ else if (freq == 229072000) chan = "12D";
+ else if (freq == 230784000) chan = "13A";
+ else if (freq == 232496000) chan = "13B";
+ else if (freq == 234208000) chan = "13C";
+ else if (freq == 235776000) chan = "13D";
+ else if (freq == 237488000) chan = "13E";
+ else if (freq == 239200000) chan = "13F";
+ else { return std::nullopt; }
+
+ return chan;
+}
+
std::chrono::milliseconds transmission_frame_duration(unsigned int dabmode)
{
using namespace std::chrono;
@@ -235,3 +365,13 @@ std::chrono::milliseconds transmission_frame_duration(unsigned int dabmode)
}
}
+
+time_t get_clock_realtime_seconds()
+{
+ struct timespec t;
+ if (clock_gettime(CLOCK_REALTIME, &t) != 0) {
+ throw std::runtime_error(std::string("Failed to retrieve CLOCK_REALTIME") + strerror(errno));
+ }
+
+ return t.tv_sec;
+}
diff --git a/src/Utils.h b/src/Utils.h
index 9e88488..b71c3b2 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -31,12 +31,14 @@
# include "config.h"
#endif
-#include <stdlib.h>
-#include <unistd.h>
-#include <stdio.h>
-#include <time.h>
+#include <optional>
#include <string>
#include <chrono>
+#include <cstdio>
+#include <ctime>
+#include <cstdlib>
+#include <unistd.h>
+#include "ConfigParser.h"
void printUsage(const char* progName);
@@ -44,15 +46,22 @@ void printVersion(void);
void printStartupInfo(void);
+void printModSettings(const mod_settings_t& mod_settings);
+
// Set SCHED_RR with priority prio (0=lowest)
int set_realtime_prio(int prio);
// Set the name of the thread
void set_thread_name(const char *name);
-// Convert a channel like 10A to a frequency
-double parseChannel(const std::string& chan);
+// Convert a channel like 10A to a frequency in Hz
+double parse_channel(const std::string& chan);
+
+// Convert a frequency in Hz to a channel.
+std::optional<std::string> convert_frequency_to_channel(double frequency);
// dabMode is either 1, 2, 3, 4, corresponding to TM I, TM II, TM III and TM IV.
// throws a runtime_error if dabMode is not one of these values.
std::chrono::milliseconds transmission_frame_duration(unsigned int dabmode);
+
+time_t get_clock_realtime_seconds();
diff --git a/src/output/BladeRF.cpp b/src/output/BladeRF.cpp
index a6ad0cc..fed2b09 100755..100644
--- a/src/output/BladeRF.cpp
+++ b/src/output/BladeRF.cpp
@@ -239,13 +239,10 @@ double BladeRF::get_bandwidth(void) const
return (double)bw;
}
-SDRDevice::RunStatistics BladeRF::get_run_statistics(void) const
+SDRDevice::run_statistics_t BladeRF::get_run_statistics(void) const
{
- RunStatistics rs;
- rs.num_underruns = underflows;
- rs.num_overruns = overflows;
- rs.num_late_packets = late_packets;
- rs.num_frames_modulated = num_frames_modulated;
+ run_statistics_t rs;
+ rs["frames"].v = num_frames_modulated;
return rs;
}
@@ -269,14 +266,14 @@ double BladeRF::get_rxgain(void) const
size_t BladeRF::receive_frame(
complexf *buf,
size_t num_samples,
- struct frame_timestamp &ts,
+ frame_timestamp &ts,
double timeout_secs)
{
// TODO
return 0;
}
-bool BladeRF::is_clk_source_ok() const
+bool BladeRF::is_clk_source_ok()
{
// TODO
return true;
@@ -287,24 +284,23 @@ const char *BladeRF::device_name(void) const
return "BladeRF";
}
-double BladeRF::get_temperature(void) const
+std::optional<double> BladeRF::get_temperature(void) const
{
if (not m_device)
throw runtime_error("BladeRF device not set up");
float temp = 0.0;
-
int status = bladerf_get_rfic_temperature(m_device, &temp);
- if (status < 0)
- {
+ if (status >= 0) {
+ return (double)temp;
+ }
+ else {
etiLog.level(error) << "Error getting BladeRF temperature: %s " << bladerf_strerror(status);
+ return std::nullopt;
}
-
- return (double)temp;
}
-
-void BladeRF::transmit_frame(const struct FrameData &frame) // SC16 frames
+void BladeRF::transmit_frame(struct FrameData&& frame) // SC16 frames
{
const size_t num_samples = frame.buf.size() / (2*sizeof(int16_t));
diff --git a/src/output/BladeRF.h b/src/output/BladeRF.h
index bc6db38..fa3419e 100755..100644
--- a/src/output/BladeRF.h
+++ b/src/output/BladeRF.h
@@ -74,8 +74,8 @@ class BladeRF : public Output::SDRDevice
virtual double get_txgain(void) const override;
virtual void set_bandwidth(double bandwidth) override;
virtual double get_bandwidth(void) const override;
- virtual void transmit_frame(const struct FrameData& frame) override;
- virtual RunStatistics get_run_statistics(void) const override;
+ virtual void transmit_frame(struct FrameData&& frame) override;
+ virtual run_statistics_t get_run_statistics(void) const override;
virtual double get_real_secs(void) const override;
virtual void set_rxgain(double rxgain) override;
@@ -83,14 +83,14 @@ class BladeRF : public Output::SDRDevice
virtual size_t receive_frame(
complexf *buf,
size_t num_samples,
- struct frame_timestamp& ts,
+ frame_timestamp& ts,
double timeout_secs) override;
// Return true if GPS and reference clock inputs are ok
- virtual bool is_clk_source_ok(void) const override;
+ virtual bool is_clk_source_ok(void) override;
virtual const char* device_name(void) const override;
- virtual double get_temperature(void) const override;
+ virtual std::optional<double> get_temperature(void) const override;
private:
@@ -99,14 +99,9 @@ class BladeRF : public Output::SDRDevice
bladerf_channel m_channel = BLADERF_CHANNEL_TX(0); // channel TX0
//struct bladerf_stream* m_stream; /* used for asynchronous api */
- size_t underflows = 0;
- size_t overflows = 0;
- size_t late_packets = 0;
size_t num_frames_modulated = 0;
- //size_t num_underflows_previous = 0;
- //size_t num_late_packets_previous = 0;
};
} // namespace Output
-#endif // HAVE_BLADERF \ No newline at end of file
+#endif // HAVE_BLADERF
diff --git a/src/output/Dexter.cpp b/src/output/Dexter.cpp
new file mode 100644
index 0000000..6e57220
--- /dev/null
+++ b/src/output/Dexter.cpp
@@ -0,0 +1,703 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2024
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+
+DESCRIPTION:
+ It is an output driver using libiio targeting the PrecisionWave DEXTER board.
+*/
+
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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, either version 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "output/Dexter.h"
+
+#ifdef HAVE_DEXTER
+
+#include <chrono>
+#include <limits>
+#include <cstdio>
+#include <iomanip>
+
+#include "Log.h"
+#include "Utils.h"
+
+using namespace std;
+
+namespace Output {
+
+static constexpr uint64_t DSP_CLOCK = 2048000uLL * 80;
+
+static constexpr uint64_t IIO_TIMEOUT_MS = 1000;
+
+static constexpr size_t TRANSMISSION_FRAME_LEN_SAMPS = (2656 + 76 * 2552) * /* I+Q */ 2;
+static constexpr size_t IIO_BUFFERS = 2;
+static constexpr size_t IIO_BUFFER_LEN_SAMPS = TRANSMISSION_FRAME_LEN_SAMPS / IIO_BUFFERS;
+
+static string get_iio_error(int err)
+{
+ char dst[256];
+ iio_strerror(-err, dst, sizeof(dst));
+ return string(dst);
+}
+
+static void fill_time(struct timespec *t)
+{
+ if (clock_gettime(CLOCK_REALTIME, t) != 0) {
+ throw std::runtime_error(string("Failed to retrieve CLOCK_REALTIME") + strerror(errno));
+ }
+}
+
+Dexter::Dexter(SDRDeviceConfig& config) :
+ SDRDevice(),
+ m_conf(config)
+{
+ etiLog.level(info) << "Dexter:Creating the device";
+
+ m_ctx = iio_create_local_context();
+ if (not m_ctx) {
+ throw std::runtime_error("Dexter: Unable to create iio context");
+ }
+
+ int r;
+ if ((r = iio_context_set_timeout(m_ctx, IIO_TIMEOUT_MS)) != 0) {
+ etiLog.level(error) << "Failed to set IIO timeout " << get_iio_error(r);
+ }
+
+ m_dexter_dsp_tx = iio_context_find_device(m_ctx, "dexter_dsp_tx");
+ if (not m_dexter_dsp_tx) {
+ throw std::runtime_error("Dexter: Unable to find dexter_dsp_tx iio device");
+ }
+
+ m_ad9957 = iio_context_find_device(m_ctx, "ad9957");
+ if (not m_ad9957) {
+ throw std::runtime_error("Dexter: Unable to find ad9957 iio device");
+ }
+
+ m_ad9957_tx0 = iio_context_find_device(m_ctx, "ad9957_tx0");
+ if (not m_ad9957_tx0) {
+ throw std::runtime_error("Dexter: Unable to find ad9957_tx0 iio device");
+ }
+
+ // TODO make DC offset configurable and add to RC
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "dc0", 0)) != 0) {
+ throw std::runtime_error("Failed to set dexter_dsp_tx.dc0 = false: " + get_iio_error(r));
+ }
+
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "dc1", 0)) != 0) {
+ throw std::runtime_error("Failed to set dexter_dsp_tx.dc1 = false: " + get_iio_error(r));
+ }
+
+ if (m_conf.sampleRate != 2048000) {
+ throw std::runtime_error("Dexter: Only 2048000 samplerate supported");
+ }
+
+ tune(m_conf.lo_offset, m_conf.frequency);
+ const double actual_freq = get_tx_freq();
+ etiLog.level(info) << "Dexter:Actual frequency: " <<
+ std::fixed << std::setprecision(3) << actual_freq / 1000.0 << " kHz.";
+
+ const auto actual_freq_long = llrint(round(actual_freq));
+ const auto configured_freq_long = llrint(round(m_conf.frequency - m_conf.lo_offset));
+
+ if (actual_freq_long != configured_freq_long) {
+ etiLog.level(error) << "Frequency tune: should " <<
+ std::fixed << std::setprecision(3) <<
+ (m_conf.frequency - m_conf.lo_offset) << " (" << configured_freq_long << ") " <<
+ " read back " << actual_freq << " (" << actual_freq_long << ")";
+ throw std::runtime_error("Could not set frequency!");
+ }
+
+ // skip: Set bandwidth
+ // skip: antenna
+
+ // The FIFO should not contain data, but setting gain=0 before setting start_clks to zero is an additional security
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", 0)) != 0) {
+ throw std::runtime_error("Failed to set dexter_dsp_tx.gain0 = 0 : " + get_iio_error(r));
+ }
+
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_flush_fifo_trigger", 1)) != 0) {
+ throw std::runtime_error("Failed to set dexter_dsp_tx.stream0_flush_fifo_trigger = 1 : " + get_iio_error(r));
+ }
+
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_start_clks", 0)) != 0) {
+ throw std::runtime_error("Failed to set dexter_dsp_tx.stream0_start_clks = 0 : " + get_iio_error(r));
+ }
+
+ constexpr int CHANNEL_INDEX = 0;
+ m_tx_channel = iio_device_get_channel(m_ad9957_tx0, CHANNEL_INDEX);
+ if (m_tx_channel == nullptr) {
+ throw std::runtime_error("Dexter: Cannot create IIO channel.");
+ }
+
+ iio_channel_enable(m_tx_channel);
+
+ m_buffer = iio_device_create_buffer(m_ad9957_tx0, IIO_BUFFER_LEN_SAMPS, 0);
+ if (not m_buffer) {
+ throw std::runtime_error("Dexter: Cannot create IIO buffer.");
+ }
+
+ // Flush the FPGA FIFO
+ {
+ constexpr size_t buflen_samps = TRANSMISSION_FRAME_LEN_SAMPS / IIO_BUFFERS;
+ constexpr size_t buflen = buflen_samps * sizeof(int16_t);
+
+ memset(iio_buffer_start(m_buffer), 0, buflen);
+ ssize_t pushed = iio_buffer_push(m_buffer);
+ if (pushed < 0) {
+ etiLog.level(error) << "Dexter: init push buffer " << get_iio_error(pushed);
+ }
+ this_thread::sleep_for(chrono::milliseconds(200));
+ }
+
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", m_conf.txgain)) != 0) {
+ etiLog.level(error) << "Failed to set dexter_dsp_tx.gain0 = " << m_conf.txgain <<
+ " : " << get_iio_error(r);
+ }
+
+ m_running = true;
+ m_underflow_read_thread = std::thread(&Dexter::underflow_read_process, this);
+}
+
+void Dexter::channel_up()
+{
+ int r;
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", m_conf.txgain)) != 0) {
+ etiLog.level(error) << "Failed to set dexter_dsp_tx.gain0 = " << m_conf.txgain <<
+ " : " << get_iio_error(r);
+ }
+
+ m_channel_is_up = true;
+ etiLog.level(debug) << "DEXTER CHANNEL_UP";
+}
+
+void Dexter::channel_down()
+{
+ int r;
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", 0)) != 0) {
+ etiLog.level(error) << "Failed to set dexter_dsp_tx.gain0 = 0: " << get_iio_error(r);
+ }
+
+ // Setting stream0_start_clocks to 0 will flush out the FIFO, but we need to wait a bit before
+ // we "up" the channel again
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_start_clks", 0)) != 0) {
+ etiLog.level(warn) << "Failed to set dexter_dsp_tx.stream0_start_clks = 0 : " << get_iio_error(r);
+ }
+
+ long long underflows_old = 0;
+
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "buffer_underflows0", &underflows_old)) != 0) {
+ etiLog.level(warn) << "Failed to read dexter_dsp_tx.buffer_underflows0 : " << get_iio_error(r);
+ }
+
+ long long underflows = underflows_old;
+
+ // Limiting to 10*96ms is just a safety to avoid running into an infinite loop
+ for (size_t i = 0; underflows == underflows_old && i < 10; i++) {
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "buffer_underflows0", &underflows)) != 0) {
+ etiLog.level(warn) << "Failed to read dexter_dsp_tx.buffer_underflows0 : " << get_iio_error(r);
+ }
+
+ this_thread::sleep_for(chrono::milliseconds(96));
+ }
+
+ if (underflows == underflows_old) {
+ etiLog.level(warn) << "DEXTER CHANNEL_DOWN, no underflow detected! " << underflows;
+ }
+
+ m_channel_is_up = false;
+ etiLog.level(debug) << "DEXTER CHANNEL_DOWN";
+}
+
+void Dexter::handle_hw_time()
+{
+ /*
+ * On startup, wait until `gpsdo_locked==1` and `pps_loss_of_signal==0`,
+ * then do the clocks alignment and go to normal state.
+ *
+ * In normal state, if `pps_loss_of_signal==1`, go to holdover state.
+ *
+ * If we've been in holdover state for longer than the configured time, or
+ * if `pps_loss_of_signal==0` stop the mod and restart.
+ */
+ int r;
+
+ switch (m_clock_state) {
+ case DexterClockState::Startup:
+ {
+ long long gpsdo_locked = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "gpsdo_locked", &gpsdo_locked)) != 0) {
+ etiLog.level(error) << "Failed to get dexter_dsp_tx.gpsdo_locked: " << get_iio_error(r);
+ throw std::runtime_error("Dexter: Cannot read IIO attribute");
+ }
+
+ long long pps_loss_of_signal = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_loss_of_signal", &pps_loss_of_signal)) != 0) {
+ etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_loss_of_signal: " << get_iio_error(r);
+ throw std::runtime_error("Dexter: Cannot read IIO attribute");
+ }
+
+ if (gpsdo_locked == 1 and pps_loss_of_signal == 0) {
+ /* Procedure:
+ * Wait 200ms after second change, fetch pps_clks attribute
+ * idem at the next second, and check that pps_clks incremented by DSP_CLOCK
+ * If ok, store the correspondence between current second change (measured in UTC clock time)
+ * and the counter value at pps rising edge. */
+
+ etiLog.level(info) << "Dexter: Waiting for second change...";
+
+ struct timespec time_at_startup;
+ fill_time(&time_at_startup);
+ time_at_startup.tv_nsec = 0;
+
+ struct timespec time_now;
+ do {
+ fill_time(&time_now);
+ this_thread::sleep_for(chrono::milliseconds(1));
+ } while (time_at_startup.tv_sec == time_now.tv_sec);
+ this_thread::sleep_for(chrono::milliseconds(200));
+
+ long long pps_clks = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_clks", &pps_clks)) != 0) {
+ etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_clks: " << get_iio_error(r);
+ throw std::runtime_error("Dexter: Cannot read IIO attribute");
+ }
+
+ time_t tnow = time_now.tv_sec;
+ etiLog.level(info) << "Dexter: pps_clks " << pps_clks << " at UTC " <<
+ put_time(std::gmtime(&tnow), "%Y-%m-%d %H:%M:%S");
+
+ time_at_startup.tv_sec = time_now.tv_sec;
+ do {
+ fill_time(&time_now);
+ this_thread::sleep_for(chrono::milliseconds(1));
+ } while (time_at_startup.tv_sec == time_now.tv_sec);
+ this_thread::sleep_for(chrono::milliseconds(200));
+
+ long long pps_clks2 = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_clks", &pps_clks2)) != 0) {
+ etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_clks: " << get_iio_error(r);
+ throw std::runtime_error("Dexter: Cannot read IIO attribute");
+ }
+ tnow = time_now.tv_sec;
+ etiLog.level(info) << "Dexter: pps_clks increased by " << pps_clks2 - pps_clks << " at UTC " <<
+ put_time(std::gmtime(&tnow), "%Y-%m-%d %H:%M:%S");
+
+ if ((uint64_t)pps_clks + DSP_CLOCK != (uint64_t)pps_clks2) {
+ throw std::runtime_error("Dexter: Wrong increase of pps_clks, expected " + to_string(DSP_CLOCK));
+ }
+
+ m_utc_seconds_at_startup = time_now.tv_sec;
+ m_clock_count_at_startup = pps_clks2;
+ m_holdover_since = chrono::steady_clock::time_point::min();
+ m_holdover_since_t = 0;
+ m_clock_state = DexterClockState::Normal;
+ etiLog.level(debug) << "Dexter: switch clock state Startup -> Normal";
+ }
+ }
+ break;
+ case DexterClockState::Normal:
+ {
+ long long pps_loss_of_signal = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_loss_of_signal", &pps_loss_of_signal)) != 0) {
+ etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_loss_of_signal: " << get_iio_error(r);
+ throw std::runtime_error("Dexter: Cannot read IIO attribute");
+ }
+
+ if (pps_loss_of_signal == 1) {
+ m_holdover_since = chrono::steady_clock::now();
+ m_holdover_since_t = chrono::system_clock::to_time_t(chrono::system_clock::now());
+ m_clock_state = DexterClockState::Holdover;
+ etiLog.level(debug) << "Dexter: switch clock state Normal -> Holdover";
+ }
+ }
+ break;
+ case DexterClockState::Holdover:
+ {
+ using namespace chrono;
+
+ long long pps_loss_of_signal = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_loss_of_signal", &pps_loss_of_signal)) != 0) {
+ etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_loss_of_signal: " << get_iio_error(r);
+ throw std::runtime_error("Dexter: Cannot read IIO attribute");
+ }
+
+ const duration<double> d = steady_clock::now() - m_holdover_since;
+ const auto max_holdover_duration = seconds(m_conf.maxGPSHoldoverTime);
+ if (d > max_holdover_duration or pps_loss_of_signal == 0) {
+ m_clock_state = DexterClockState::Startup;
+ m_utc_seconds_at_startup = 0;
+ m_clock_count_at_startup = 0;
+ m_holdover_since = chrono::steady_clock::time_point::min();
+ m_holdover_since_t = 0;
+ etiLog.level(debug) << "Dexter: switch clock state Holdover -> Startup";
+ }
+ }
+ break;
+ }
+}
+
+void Dexter::tune(double lo_offset, double frequency)
+{
+ // lo_offset is applied to the DSP, and frequency is given to the ad9957, this gives lower spurs
+
+ int r = 0;
+
+ const long long freq = frequency - lo_offset;
+ if ((r = iio_device_attr_write_longlong(m_ad9957, "center_frequency", freq)) != 0) {
+ etiLog.level(warn) << "Failed to set ad9957.center_frequency = " << freq << " : " << get_iio_error(r);
+ }
+
+ long long lo_offs = std::round(lo_offset);
+
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "frequency0", lo_offs)) != 0) {
+ etiLog.level(warn) << "Failed to set dexter_dsp_tx.frequency0 = " << lo_offs << " : " << get_iio_error(r);
+ }
+
+ m_conf.frequency = get_tx_freq();
+}
+
+double Dexter::get_tx_freq(void) const
+{
+ int r = 0;
+
+ long long lo_offset = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "frequency0", &lo_offset)) != 0) {
+ etiLog.level(warn) << "Failed to read dexter_dsp_tx.frequency0: " << get_iio_error(r);
+ return 0;
+ }
+
+ double frequency = 0;
+ if ((r = iio_device_attr_read_double(m_ad9957, "center_frequency", &frequency)) != 0) {
+ etiLog.level(warn) << "Failed to read ad9957.center_frequency: " << get_iio_error(r);
+ return 0;
+ }
+
+ return frequency + lo_offset;
+}
+
+void Dexter::set_txgain(double txgain)
+{
+ int r = 0;
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", txgain)) != 0) {
+ etiLog.level(warn) << "Failed to set dexter_dsp_tx.gain0 = " << txgain << ": " << get_iio_error(r);
+ }
+
+ long long txgain_readback = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "gain0", &txgain_readback)) != 0) {
+ etiLog.level(warn) << "Failed to read dexter_dsp_tx.gain0: " << get_iio_error(r);
+ }
+ else {
+ m_conf.txgain = txgain_readback;
+ }
+}
+
+double Dexter::get_txgain(void) const
+{
+ long long txgain_readback = 0;
+ int r = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "gain0", &txgain_readback)) != 0) {
+ etiLog.level(warn) << "Failed to read dexter_dsp_tx.gain0: " << get_iio_error(r);
+ }
+ return txgain_readback;
+}
+
+void Dexter::set_bandwidth(double bandwidth)
+{
+ return;
+}
+
+double Dexter::get_bandwidth(void) const
+{
+ return 0;
+}
+
+SDRDevice::run_statistics_t Dexter::get_run_statistics(void) const
+{
+ run_statistics_t rs;
+ {
+ std::unique_lock<std::mutex> lock(m_attr_thread_mutex);
+ rs["underruns"].v = underflows;
+ }
+ rs["latepackets"].v = num_late;
+ rs["frames"].v = num_frames_modulated;
+
+ rs["in_holdover_since"].v = 0;
+ rs["remaining_holdover_s"].v = m_conf.maxGPSHoldoverTime;
+ switch (m_clock_state) {
+ case DexterClockState::Startup:
+ rs["clock_state"].v = "startup"; break;
+ case DexterClockState::Normal:
+ rs["clock_state"].v = "normal"; break;
+ case DexterClockState::Holdover:
+ rs["clock_state"].v = "holdover";
+ rs["in_holdover_since"].v = m_holdover_since_t;
+ {
+ using namespace std::chrono;
+ const auto max_holdover_duration = seconds(m_conf.maxGPSHoldoverTime);
+ const duration<double> remaining = max_holdover_duration - (steady_clock::now() - m_holdover_since);
+ rs["remaining_holdover_s"].v = (ssize_t)duration_cast<seconds>(remaining).count();
+ }
+ break;
+ }
+
+ return rs;
+}
+
+double Dexter::get_real_secs(void) const
+{
+ long long clks = 0;
+ int r = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "clks", &clks)) != 0) {
+ etiLog.level(error) << "Failed to get dexter_dsp_tx.clks: " << get_iio_error(r);
+ throw std::runtime_error("Dexter: Cannot read IIO attribute");
+ }
+
+ switch (m_clock_state) {
+ case DexterClockState::Startup:
+ return 0;
+ case DexterClockState::Normal:
+ case DexterClockState::Holdover:
+ return (double)m_utc_seconds_at_startup + (double)(clks - m_clock_count_at_startup) / (double)DSP_CLOCK;
+ }
+ throw std::logic_error("Unhandled switch");
+}
+
+void Dexter::set_rxgain(double rxgain)
+{
+ // TODO
+}
+
+double Dexter::get_rxgain(void) const
+{
+ // TODO
+ return 0;
+}
+
+size_t Dexter::receive_frame(
+ complexf *buf,
+ size_t num_samples,
+ frame_timestamp& ts,
+ double timeout_secs)
+{
+ // TODO
+ return 0;
+}
+
+
+bool Dexter::is_clk_source_ok()
+{
+ if (m_conf.enableSync) {
+ handle_hw_time();
+ return m_clock_state != DexterClockState::Startup;
+ }
+ else {
+ return true;
+ }
+}
+
+const char* Dexter::device_name(void) const
+{
+ return "Dexter";
+}
+
+std::optional<double> Dexter::get_temperature(void) const
+{
+ std::ifstream in("/sys/bus/i2c/devices/1-002f/hwmon/hwmon0/temp1_input", std::ios::in | std::ios::binary);
+ if (in) {
+ double tbaseboard;
+ in >> tbaseboard;
+ return tbaseboard / 1000.0;
+ }
+ else {
+ return {};
+ }
+}
+
+void Dexter::transmit_frame(struct FrameData&& frame)
+{
+ constexpr size_t frame_len_bytes = TRANSMISSION_FRAME_LEN_SAMPS * sizeof(int16_t);
+ if (frame.buf.size() != frame_len_bytes) {
+ etiLog.level(debug) << "Dexter::transmit_frame Expected " <<
+ frame_len_bytes << " got " << frame.buf.size();
+ throw std::runtime_error("Dexter: invalid buffer size");
+ }
+
+ const bool require_timestamped_tx = (m_conf.enableSync and frame.ts.timestamp_valid);
+
+ if (not m_channel_is_up) {
+ if (require_timestamped_tx) {
+ if (m_clock_state == DexterClockState::Startup) {
+ return; // not ready
+ }
+ else {
+ constexpr uint64_t TIMESTAMP_PPS_PER_DSP_CLOCKS = DSP_CLOCK / 16384000;
+ // TIMESTAMP_PPS_PER_DSP_CLOCKS=10 because timestamp_pps is represented in 16.384 MHz clocks
+ uint64_t frame_start_clocks =
+ // at second level
+ ((int64_t)frame.ts.timestamp_sec - (int64_t)m_utc_seconds_at_startup) * DSP_CLOCK + m_clock_count_at_startup +
+ // at subsecond level
+ (uint64_t)frame.ts.timestamp_pps * TIMESTAMP_PPS_PER_DSP_CLOCKS;
+
+ const double margin_s = frame.ts.offset_to_system_time();
+
+ long long clks = 0;
+ int r = 0;
+ if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "clks", &clks)) != 0) {
+ etiLog.level(error) << "Failed to get dexter_dsp_tx.clks: " << get_iio_error(r);
+ throw std::runtime_error("Dexter: Cannot read IIO attribute");
+ }
+
+ const double margin_device_s = (double)(frame_start_clocks - clks) / DSP_CLOCK;
+
+ etiLog.level(debug) << "DEXTER FCT " << frame.ts.fct << " TS CLK " <<
+ ((int64_t)frame.ts.timestamp_sec - (int64_t)m_utc_seconds_at_startup) * DSP_CLOCK << " + " <<
+ m_clock_count_at_startup << " + " <<
+ (uint64_t)frame.ts.timestamp_pps * TIMESTAMP_PPS_PER_DSP_CLOCKS << " = " <<
+ frame_start_clocks << " DELTA " << margin_s << " " << margin_device_s;
+
+ // Ensure we hand the frame over to HW with a bit of margin
+ if (margin_s < 0.2) {
+ etiLog.level(warn) << "Skip frame short margin " << margin_s;
+ num_late++;
+ return;
+ }
+
+ if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_start_clks", frame_start_clocks)) != 0) {
+ etiLog.level(warn) << "Skip frame, failed to set dexter_dsp_tx.stream0_start_clks = " << frame_start_clocks << " : " << get_iio_error(r);
+ num_late++;
+ return;
+ }
+ m_require_timestamp_refresh = false;
+ }
+ }
+
+ channel_up();
+ }
+
+ if (m_require_timestamp_refresh) {
+ etiLog.level(debug) << "DEXTER REQUIRE REFRESH";
+ channel_down();
+ m_require_timestamp_refresh = false;
+ }
+
+ // DabMod::launch_modulator ensures we get int16_t IQ here
+ //const size_t num_samples = frame.buf.size() / (2*sizeof(int16_t));
+ //const int16_t *buf = reinterpret_cast<const int16_t*>(frame.buf.data());
+
+ if (m_channel_is_up) {
+ for (size_t i = 0; i < IIO_BUFFERS; i++) {
+ constexpr size_t buflen_samps = TRANSMISSION_FRAME_LEN_SAMPS / IIO_BUFFERS;
+ constexpr size_t buflen = buflen_samps * sizeof(int16_t);
+
+ memcpy(iio_buffer_start(m_buffer), frame.buf.data() + (i * buflen), buflen);
+ ssize_t pushed = iio_buffer_push(m_buffer);
+ if (pushed < 0) {
+ etiLog.level(error) << "Dexter: failed to push buffer " << get_iio_error(pushed) <<
+ " after " << num_buffers_pushed << " bufs";
+ num_buffers_pushed = 0;
+ channel_down();
+ break;
+ }
+ num_buffers_pushed++;
+ }
+ num_frames_modulated++;
+ }
+
+ {
+ std::unique_lock<std::mutex> lock(m_attr_thread_mutex);
+ size_t u = underflows;
+ lock.unlock();
+
+ if (u != 0 and u != prev_underflows) {
+ etiLog.level(warn) << "Dexter: underflow! " << prev_underflows << " -> " << u;
+ }
+
+ prev_underflows = u;
+ }
+}
+
+void Dexter::underflow_read_process()
+{
+ m_underflow_ctx = iio_create_local_context();
+ if (not m_underflow_ctx) {
+ throw std::runtime_error("Dexter: Unable to create iio context for underflow");
+ }
+
+ auto dexter_dsp_tx = iio_context_find_device(m_ctx, "dexter_dsp_tx");
+ if (not dexter_dsp_tx) {
+ throw std::runtime_error("Dexter: Unable to find dexter_dsp_tx iio device");
+ }
+
+ set_thread_name("dexter_underflow");
+
+ while (m_running) {
+ this_thread::sleep_for(chrono::seconds(1));
+ long long underflows_attr = 0;
+
+ int r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "buffer_underflows0", &underflows_attr);
+
+ if (r == 0) {
+ size_t underflows_new = underflows_attr;
+
+ std::unique_lock<std::mutex> lock(m_attr_thread_mutex);
+ if (underflows_new != underflows and underflows_attr != 0) {
+ underflows = underflows_new;
+ }
+ }
+ }
+ m_running = false;
+}
+
+Dexter::~Dexter()
+{
+ m_running = false;
+ if (m_underflow_read_thread.joinable()) {
+ m_underflow_read_thread.join();
+ }
+
+ if (m_ctx) {
+ if (m_dexter_dsp_tx) {
+ iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", 0);
+ }
+
+ if (m_buffer) {
+ iio_buffer_destroy(m_buffer);
+ m_buffer = nullptr;
+ }
+
+ if (m_tx_channel) {
+ iio_channel_disable(m_tx_channel);
+ }
+
+ iio_context_destroy(m_ctx);
+ m_ctx = nullptr;
+ }
+
+ if (m_underflow_ctx) {
+ iio_context_destroy(m_underflow_ctx);
+ m_underflow_ctx = nullptr;
+ }
+}
+
+} // namespace Output
+
+#endif // HAVE_DEXTER
diff --git a/src/output/Dexter.h b/src/output/Dexter.h
new file mode 100644
index 0000000..f8a17ba
--- /dev/null
+++ b/src/output/Dexter.h
@@ -0,0 +1,138 @@
+/*
+ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+ Queen in Right of Canada (Communications Research Center Canada)
+
+ Copyright (C) 2023
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://opendigitalradio.org
+
+DESCRIPTION:
+ It is an output driver using libiio targeting the PrecisionWave DEXTER board.
+*/
+
+/*
+ This file is part of ODR-DabMod.
+
+ ODR-DabMod 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, either version 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMod 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 for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#ifdef HAVE_DEXTER
+#include "iio.h"
+
+#include <string>
+#include <memory>
+#include <ctime>
+#include <mutex>
+#include <thread>
+#include <variant>
+
+#include "output/SDR.h"
+#include "ModPlugin.h"
+#include "EtiReader.h"
+#include "RemoteControl.h"
+
+namespace Output {
+
+enum class DexterClockState {
+ Startup,
+ Normal,
+ Holdover
+};
+
+class Dexter : public Output::SDRDevice
+{
+ public:
+ Dexter(SDRDeviceConfig& config);
+ Dexter(const Dexter& other) = delete;
+ Dexter& operator=(const Dexter& other) = delete;
+ virtual ~Dexter();
+
+ virtual void tune(double lo_offset, double frequency) override;
+ virtual double get_tx_freq(void) const override;
+ virtual void set_txgain(double txgain) override;
+ virtual double get_txgain() const override;
+ virtual void set_bandwidth(double bandwidth) override;
+ virtual double get_bandwidth() const override;
+ virtual void transmit_frame(struct FrameData&& frame) override;
+ virtual run_statistics_t get_run_statistics() const override;
+ virtual double get_real_secs() const override;
+
+ virtual void set_rxgain(double rxgain) override;
+ virtual double get_rxgain() const override;
+ virtual size_t receive_frame(
+ complexf *buf,
+ size_t num_samples,
+ frame_timestamp& ts,
+ double timeout_secs) override;
+
+ // Return true if GPS and reference clock inputs are ok
+ virtual bool is_clk_source_ok() override;
+ virtual const char* device_name() const override;
+
+ virtual std::optional<double> get_temperature() const override;
+
+ private:
+ void channel_up();
+ void channel_down();
+ void handle_hw_time();
+
+ bool m_channel_is_up = false;
+
+ SDRDeviceConfig& m_conf;
+
+ struct iio_context *m_ctx = nullptr;
+ struct iio_device *m_dexter_dsp_tx = nullptr;
+
+ struct iio_device *m_ad9957 = nullptr;
+ struct iio_device *m_ad9957_tx0 = nullptr;
+ struct iio_channel *m_tx_channel = nullptr;
+ struct iio_buffer *m_buffer = nullptr;
+
+ /* Underflows are counted in a separate thread */
+ struct iio_context *m_underflow_ctx = nullptr;
+ std::atomic<bool> m_running = ATOMIC_VAR_INIT(false);
+ std::thread m_underflow_read_thread;
+ void underflow_read_process();
+ mutable std::mutex m_attr_thread_mutex;
+ size_t underflows = 0;
+
+ size_t prev_underflows = 0;
+ size_t num_late = 0;
+ size_t num_frames_modulated = 0;
+
+ size_t num_buffers_pushed = 0;
+
+ DexterClockState m_clock_state = DexterClockState::Startup;
+
+ // Only valid when m_clock_state is not Startup
+ uint64_t m_utc_seconds_at_startup = 0;
+ uint64_t m_clock_count_at_startup = 0;
+
+ // Only valid when m_clock_state Holdover
+ std::chrono::steady_clock::time_point m_holdover_since =
+ std::chrono::steady_clock::time_point::min();
+ std::time_t m_holdover_since_t = 0;
+};
+
+} // namespace Output
+
+#endif //HAVE_DEXTER
+
diff --git a/src/output/Feedback.cpp b/src/output/Feedback.cpp
index 88d8319..d112b5a 100644
--- a/src/output/Feedback.cpp
+++ b/src/output/Feedback.cpp
@@ -84,7 +84,7 @@ DPDFeedbackServer::~DPDFeedbackServer()
void DPDFeedbackServer::set_tx_frame(
const std::vector<uint8_t> &buf,
- const struct frame_timestamp &buf_ts)
+ const frame_timestamp &buf_ts)
{
if (not m_running) {
throw runtime_error("DPDFeedbackServer not running");
diff --git a/src/output/Feedback.h b/src/output/Feedback.h
index aef86b0..b31347f 100644
--- a/src/output/Feedback.h
+++ b/src/output/Feedback.h
@@ -94,7 +94,7 @@ class DPDFeedbackServer {
~DPDFeedbackServer();
void set_tx_frame(const std::vector<uint8_t> &buf,
- const struct frame_timestamp& ts);
+ const frame_timestamp& ts);
private:
// Thread that reacts to burstRequests and receives from the SDR device
diff --git a/src/output/Lime.cpp b/src/output/Lime.cpp
index 6f7eed5..3ef981e 100644
--- a/src/output/Lime.cpp
+++ b/src/output/Lime.cpp
@@ -226,14 +226,9 @@ Lime::Lime(SDRDeviceConfig &config) : SDRDevice(), m_conf(config)
throw runtime_error("Unsupported interpolate: " + to_string(m_interpolate));
}
- if (m_conf.sampleRate != 2048000)
- {
- throw runtime_error("Lime output only supports native samplerate = 2048000");
- /* The buffer_size calculation below does not take into account resampling */
- }
-
// Frame duration is 96ms
- size_t buffer_size = FRAME_LENGTH * m_interpolate * 10; // We take 10 Frame buffer size Fifo
+ const size_t samplerate_ratio = m_conf.sampleRate / 2048000;
+ const size_t buffer_size = FRAME_LENGTH * m_interpolate * samplerate_ratio * 10; // We take 10 Frame buffer size Fifo
// Fifo seems to be round to multiple of SampleRate
m_tx_stream.channel = m_channel;
m_tx_stream.fifoSize = buffer_size;
@@ -323,13 +318,14 @@ double Lime::get_bandwidth(void) const
return bw;
}
-SDRDevice::RunStatistics Lime::get_run_statistics(void) const
+SDRDevice::run_statistics_t Lime::get_run_statistics(void) const
{
- RunStatistics rs;
- rs.num_underruns = underflows;
- rs.num_overruns = overflows;
- rs.num_late_packets = late_packets;
- rs.num_frames_modulated = num_frames_modulated;
+ run_statistics_t rs;
+ rs["underruns"].v = underflows;
+ rs["overruns"].v = overflows;
+ rs["dropped_packets"].v = dropped_packets;
+ rs["frames"].v = num_frames_modulated;
+ rs["fifo_fill"].v = m_last_fifo_fill_percent * 100;
return rs;
}
@@ -353,14 +349,14 @@ double Lime::get_rxgain(void) const
size_t Lime::receive_frame(
complexf *buf,
size_t num_samples,
- struct frame_timestamp &ts,
+ frame_timestamp &ts,
double timeout_secs)
{
// TODO
return 0;
}
-bool Lime::is_clk_source_ok() const
+bool Lime::is_clk_source_ok()
{
// TODO
return true;
@@ -371,25 +367,23 @@ const char *Lime::device_name(void) const
return "Lime";
}
-double Lime::get_temperature(void) const
+std::optional<double> Lime::get_temperature(void) const
{
if (not m_device)
throw runtime_error("Lime device not set up");
- float_type temp = numeric_limits<float_type>::quiet_NaN();
- if (LMS_GetChipTemperature(m_device, 0, &temp) < 0)
- {
+ float_type temp = 0;
+ if (LMS_GetChipTemperature(m_device, 0, &temp) >= 0) {
+ return temp;
+ }
+ else {
etiLog.level(error) << "Error getting LimeSDR temperature: %s " << LMS_GetLastErrorMessage();
+ return std::nullopt;
}
- return temp;
}
-float Lime::get_fifo_fill_percent(void) const
-{
- return m_last_fifo_fill_percent * 100;
-}
-void Lime::transmit_frame(const struct FrameData &frame)
+void Lime::transmit_frame(struct FrameData&& frame)
{
if (not m_device)
throw runtime_error("Lime device not set up");
@@ -411,7 +405,7 @@ void Lime::transmit_frame(const struct FrameData &frame)
LMS_GetStreamStatus(&m_tx_stream, &LimeStatus);
overflows += LimeStatus.overrun;
underflows += LimeStatus.underrun;
- late_packets += LimeStatus.droppedPackets;
+ dropped_packets += LimeStatus.droppedPackets;
#ifdef LIMEDEBUG
etiLog.level(info) << LimeStatus.fifoFilledCount << "/" << LimeStatus.fifoSize << ":" << numSamples << "Rate" << LimeStatus.linkRate / (2 * 2.0);
diff --git a/src/output/Lime.h b/src/output/Lime.h
index 72a018e..4510bf2 100644
--- a/src/output/Lime.h
+++ b/src/output/Lime.h
@@ -66,8 +66,8 @@ class Lime : public Output::SDRDevice
virtual double get_txgain(void) const override;
virtual void set_bandwidth(double bandwidth) override;
virtual double get_bandwidth(void) const override;
- virtual void transmit_frame(const struct FrameData &frame) override;
- virtual RunStatistics get_run_statistics(void) const override;
+ virtual void transmit_frame(struct FrameData&& frame) override;
+ virtual run_statistics_t get_run_statistics(void) const override;
virtual double get_real_secs(void) const override;
virtual void set_rxgain(double rxgain) override;
@@ -75,15 +75,14 @@ class Lime : public Output::SDRDevice
virtual size_t receive_frame(
complexf *buf,
size_t num_samples,
- struct frame_timestamp &ts,
+ frame_timestamp &ts,
double timeout_secs) override;
// Return true if GPS and reference clock inputs are ok
- virtual bool is_clk_source_ok(void) const override;
+ virtual bool is_clk_source_ok(void) override;
virtual const char *device_name(void) const override;
- virtual double get_temperature(void) const override;
- virtual float get_fifo_fill_percent(void) const;
+ virtual std::optional<double> get_temperature(void) const override;
private:
SDRDeviceConfig &m_conf;
@@ -95,11 +94,10 @@ class Lime : public Output::SDRDevice
std::vector<complexf> interpolatebuf;
std::vector<short> m_i16samples;
std::atomic<float> m_last_fifo_fill_percent = ATOMIC_VAR_INIT(0);
-
size_t underflows = 0;
size_t overflows = 0;
- size_t late_packets = 0;
+ size_t dropped_packets = 0;
size_t num_frames_modulated = 0;
};
diff --git a/src/output/SDR.cpp b/src/output/SDR.cpp
index 6078fc7..22398c7 100644
--- a/src/output/SDR.cpp
+++ b/src/output/SDR.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2024
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -25,13 +25,16 @@
*/
#include "output/SDR.h"
+#include "output/UHD.h"
#include "output/Lime.h"
+#include "output/Dexter.h"
#include "PcDebug.h"
#include "Log.h"
#include "RemoteControl.h"
#include "Utils.h"
+#include <chrono>
#include <cmath>
#include <iostream>
#include <assert.h>
@@ -46,17 +49,16 @@ using namespace std;
namespace Output {
-// Maximum number of frames that can wait in frames
-static constexpr size_t FRAMES_MAX_SIZE = 8;
+// Maximum number of frames that can wait in frames.
+// Keep it low when not using synchronised transmission, in order to reduce delay.
+// When using synchronised transmission, use a 6s buffer to give us enough margin.
+static constexpr size_t FRAMES_MAX_SIZE_UNSYNC = 8;
+static constexpr size_t FRAMES_MAX_SIZE_SYNC = 250;
// If the timestamp is further in the future than
// 100 seconds, abort
static constexpr double TIMESTAMP_ABORT_FUTURE = 100;
-// Add a delay to increase buffers when
-// frames are too far in the future
-static constexpr double TIMESTAMP_MARGIN_FUTURE = 0.5;
-
SDR::SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device) :
ModOutput(), ModMetadata(), RemoteControllable("sdr"),
m_config(config),
@@ -65,6 +67,7 @@ SDR::SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device) :
// muting is remote-controllable
m_config.muting = false;
+ m_running.store(true);
m_device_thread = std::thread(&SDR::process_thread_entry, this);
if (m_config.dpdFeedbackServerPort > 0) {
@@ -77,20 +80,40 @@ SDR::SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device) :
RC_ADD_PARAMETER(txgain, "TX gain");
RC_ADD_PARAMETER(rxgain, "RX gain for DPD feedback");
RC_ADD_PARAMETER(bandwidth, "Analog front-end bandwidth");
- RC_ADD_PARAMETER(freq, "Transmission frequency");
+ RC_ADD_PARAMETER(freq, "Transmission frequency in Hz");
+ RC_ADD_PARAMETER(channel, "Transmission frequency as channel");
RC_ADD_PARAMETER(muting, "Mute the output by stopping the transmitter");
RC_ADD_PARAMETER(temp, "Temperature in degrees C of the device");
RC_ADD_PARAMETER(underruns, "Counter of number of underruns");
RC_ADD_PARAMETER(latepackets, "Counter of number of late packets");
RC_ADD_PARAMETER(frames, "Counter of number of frames modulated");
- RC_ADD_PARAMETER(gpsdo_num_sv, "Number of Satellite Vehicles tracked by GPSDO");
- RC_ADD_PARAMETER(gpsdo_holdover, "1 if the GPSDO is in holdover, 0 if it is using gnss");
+ RC_ADD_PARAMETER(synchronous, "1 if configured for synchronous transmission");
+ RC_ADD_PARAMETER(max_gps_holdover_time, "Max holdover duration in seconds");
+
+#ifdef HAVE_OUTPUT_UHD
+ if (std::dynamic_pointer_cast<UHD>(device)) {
+ RC_ADD_PARAMETER(gpsdo_num_sv, "Number of Satellite Vehicles tracked by GPSDO");
+ RC_ADD_PARAMETER(gpsdo_holdover, "1 if the GPSDO is in holdover, 0 if it is using gnss");
+ }
+#endif // HAVE_OUTPUT_UHD
+
+ RC_ADD_PARAMETER(queued_frames_ms, "Number of frames queued, represented in milliseconds");
#ifdef HAVE_LIMESDR
if (std::dynamic_pointer_cast<Lime>(device)) {
RC_ADD_PARAMETER(fifo_fill, "A value representing the Lime FIFO fullness [percent]");
}
#endif // HAVE_LIMESDR
+
+#ifdef HAVE_DEXTER
+ if (std::dynamic_pointer_cast<Dexter>(device)) {
+ RC_ADD_PARAMETER(in_holdover_since, "DEXTER timestamp when holdover began");
+ RC_ADD_PARAMETER(remaining_holdover_s, "DEXTER remaining number of seconds in holdover");
+ RC_ADD_PARAMETER(clock_state, "DEXTER clock state: startup/normal/holdover");
+ }
+#endif // HAVE_DEXTER
+
+
}
SDR::~SDR()
@@ -104,6 +127,11 @@ SDR::~SDR()
}
}
+void SDR::set_sample_size(size_t size)
+{
+ m_size = size;
+}
+
int SDR::process(Buffer *dataIn)
{
if (not m_running) {
@@ -125,6 +153,7 @@ meta_vec_t SDR::process_metadata(const meta_vec_t& metadataIn)
if (m_device and m_running) {
FrameData frame;
frame.buf = std::move(m_frame);
+ frame.sampleSize = m_size;
if (metadataIn.empty()) {
etiLog.level(info) <<
@@ -138,7 +167,7 @@ meta_vec_t SDR::process_metadata(const meta_vec_t& metadataIn)
* This behaviour is different to earlier versions of ODR-DabMod,
* which took the timestamp from the latest ETI frame.
*/
- frame.ts = *(metadataIn[0].ts);
+ frame.ts = metadataIn[0].ts;
// TODO check device running
@@ -157,9 +186,12 @@ meta_vec_t SDR::process_metadata(const meta_vec_t& metadataIn)
m_config.sampleRate);
}
- size_t num_frames = m_queue.push_wait_if_full(frame,
- FRAMES_MAX_SIZE);
- etiLog.log(trace, "SDR,push %zu", num_frames);
+
+ const auto max_size = m_config.enableSync ? FRAMES_MAX_SIZE_SYNC : FRAMES_MAX_SIZE_UNSYNC;
+ auto r = m_queue.push_overflow(std::move(frame), max_size);
+ etiLog.log(trace, "SDR,push %d %zu", r.overflowed, r.new_size);
+
+ num_queue_overflows += r.overflowed ? 1 : 0;
}
}
else {
@@ -180,16 +212,11 @@ void SDR::process_thread_entry()
last_tx_time_initialised = false;
- size_t last_num_underflows = 0;
- size_t pop_prebuffering = FRAMES_MAX_SIZE;
-
- m_running.store(true);
-
try {
while (m_running.load()) {
struct FrameData frame;
etiLog.log(trace, "SDR,wait");
- m_queue.wait_and_pop(frame, pop_prebuffering);
+ m_queue.wait_and_pop(frame);
etiLog.log(trace, "SDR,pop");
if (m_running.load() == false) {
@@ -197,20 +224,7 @@ void SDR::process_thread_entry()
}
if (m_device) {
- handle_frame(frame);
-
- const auto rs = m_device->get_run_statistics();
-
- /* Ensure we fill frames after every underrun and
- * at startup to reduce underrun likelihood. */
- if (last_num_underflows < rs.num_underruns) {
- pop_prebuffering = FRAMES_MAX_SIZE;
- }
- else {
- pop_prebuffering = 1;
- }
-
- last_num_underflows = rs.num_underruns;
+ handle_frame(std::move(frame));
}
}
}
@@ -236,46 +250,19 @@ const char* SDR::name()
return m_name.c_str();
}
-void SDR::sleep_through_frame()
-{
- using namespace std::chrono;
-
- const auto now = steady_clock::now();
-
- if (not t_last_frame_initialised) {
- t_last_frame = now;
- t_last_frame_initialised = true;
- }
-
- const auto delta = now - t_last_frame;
- const auto wait_time = transmission_frame_duration(m_config.dabMode);
- if (wait_time > delta) {
- this_thread::sleep_for(wait_time - delta);
- }
-
- t_last_frame += wait_time;
-}
-
-void SDR::handle_frame(struct FrameData& frame)
+void SDR::handle_frame(struct FrameData&& frame)
{
// Assumes m_device is valid
- constexpr double tx_timeout = 20.0;
-
if (not m_device->is_clk_source_ok()) {
- sleep_through_frame();
return;
}
const auto& time_spec = frame.ts;
- if (m_config.enableSync and m_config.muteNoTimestamps and
- not time_spec.timestamp_valid) {
- sleep_through_frame();
- etiLog.log(info,
- "OutputSDR: Muting sample %d : no timestamp\n",
- frame.ts.fct);
+ if (m_config.enableSync and m_config.muteNoTimestamps and not time_spec.timestamp_valid) {
+ etiLog.log(info, "OutputSDR: Muting sample %d : no timestamp\n", frame.ts.fct);
return;
}
@@ -297,8 +284,13 @@ void SDR::handle_frame(struct FrameData& frame)
return;
}
+ if (frame.ts.offset_changed) {
+ etiLog.level(debug) << "TS offset changed";
+ m_device->require_timestamp_refresh();
+ }
+
if (last_tx_time_initialised) {
- const size_t sizeIn = frame.buf.size() / sizeof(complexf);
+ const size_t sizeIn = frame.buf.size() / frame.sampleSize;
// Checking units for the increment calculation:
// samps * ticks/s / (samps/s)
@@ -325,7 +317,7 @@ void SDR::handle_frame(struct FrameData& frame)
tx_second << "+" << (double)tx_pps/16384000.0 <<
"(" << tx_pps << ")";
- frame.ts.timestamp_refresh = true;
+ m_device->require_timestamp_refresh();
}
}
@@ -337,7 +329,7 @@ void SDR::handle_frame(struct FrameData& frame)
etiLog.log(trace, "SDR,tist %f", time_spec.get_real_secs());
- if (time_spec.get_real_secs() + tx_timeout < device_time) {
+ if (time_spec.get_real_secs() < device_time) {
etiLog.level(warn) <<
"OutputSDR: Timestamp in the past at FCT=" << frame.ts.fct << " offset: " <<
std::fixed <<
@@ -346,6 +338,7 @@ void SDR::handle_frame(struct FrameData& frame)
" frame " << frame.ts.fct <<
", tx_second " << tx_second <<
", pps " << pps_offset;
+ m_device->require_timestamp_refresh();
return;
}
@@ -359,13 +352,12 @@ void SDR::handle_frame(struct FrameData& frame)
}
if (m_config.muting) {
- etiLog.log(info,
- "OutputSDR: Muting FCT=%d requested",
- frame.ts.fct);
+ etiLog.log(info, "OutputSDR: Muting FCT=%d requested", frame.ts.fct);
+ m_device->require_timestamp_refresh();
return;
}
- m_device->transmit_frame(frame);
+ m_device->transmit_frame(std::move(frame));
}
// =======================================
@@ -391,23 +383,33 @@ void SDR::set_parameter(const string& parameter, const string& value)
else if (parameter == "freq") {
ss >> m_config.frequency;
m_device->tune(m_config.lo_offset, m_config.frequency);
- m_config.frequency = m_device->get_tx_freq();
+ }
+ else if (parameter == "channel") {
+ try {
+ const double frequency = parse_channel(value);
+
+ m_config.frequency = frequency;
+ m_device->tune(m_config.lo_offset, m_config.frequency);
+ }
+ catch (const std::out_of_range& e) {
+ throw ParameterError("Cannot parse channel");
+ }
}
else if (parameter == "muting") {
ss >> m_config.muting;
}
- else if (parameter == "underruns" or
- parameter == "latepackets" or
- parameter == "frames" or
- parameter == "gpsdo_num_sv" or
- parameter == "gpsdo_holdover" or
- parameter == "fifo_fill") {
- throw ParameterError("Parameter " + parameter + " is read-only.");
+ else if (parameter == "synchronous") {
+ uint32_t enableSync = 0;
+ ss >> enableSync;
+ m_config.enableSync = enableSync > 0;
+ }
+ else if (parameter == "max_gps_holdover_time") {
+ ss >> m_config.maxGPSHoldoverTime;
}
else {
stringstream ss_err;
ss_err << "Parameter '" << parameter
- << "' is not exported by controllable " << get_rc_name();
+ << "' is read-only or not exported by controllable " << get_rc_name();
throw ParameterError(ss_err.str());
}
}
@@ -428,6 +430,16 @@ const string SDR::get_parameter(const string& parameter) const
else if (parameter == "freq") {
ss << m_config.frequency;
}
+ else if (parameter == "channel") {
+ const auto maybe_freq = convert_frequency_to_channel(m_config.frequency);
+
+ if (maybe_freq.has_value()) {
+ ss << *maybe_freq;
+ }
+ else {
+ throw ParameterError("Frequency is outside list of channels");
+ }
+ }
else if (parameter == "muting") {
ss << m_config.muting;
}
@@ -435,55 +447,57 @@ const string SDR::get_parameter(const string& parameter) const
if (not m_device) {
throw ParameterError("OutputSDR has no device");
}
- const double temp = m_device->get_temperature();
- if (std::isnan(temp)) {
- throw ParameterError("Temperature not available");
+ const std::optional<double> temp = m_device->get_temperature();
+ if (temp) {
+ ss << *temp;
}
else {
- ss << temp;
- }
- }
- else if (parameter == "underruns" or
- parameter == "latepackets" or
- parameter == "frames" ) {
- if (not m_device) {
- throw ParameterError("OutputSDR has no device");
- }
- const auto stat = m_device->get_run_statistics();
-
- if (parameter == "underruns") {
- ss << stat.num_underruns;
- }
- else if (parameter == "latepackets") {
- ss << stat.num_late_packets;
- }
- else if (parameter == "frames") {
- ss << stat.num_frames_modulated;
+ throw ParameterError("Temperature not available");
}
}
- else if (parameter == "gpsdo_num_sv") {
- const auto stat = m_device->get_run_statistics();
- ss << stat.gpsdo_num_sv;
+ else if (parameter == "queued_frames_ms") {
+ ss << m_queue.size() *
+ chrono::duration_cast<chrono::milliseconds>(transmission_frame_duration(m_config.dabMode))
+ .count();
}
- else if (parameter == "gpsdo_holdover") {
- const auto stat = m_device->get_run_statistics();
- ss << (stat.gpsdo_holdover ? 1 : 0);
+ else if (parameter == "synchronous") {
+ ss << m_config.enableSync;
}
-#ifdef HAVE_LIMESDR
- else if (parameter == "fifo_fill") {
- const auto dev = std::dynamic_pointer_cast<Lime>(m_device);
-
- if (dev) {
- ss << dev->get_fifo_fill_percent();
- }
- else {
- ss << "Parameter '" << parameter <<
- "' is not exported by controllable " << get_rc_name();
- throw ParameterError(ss.str());
- }
+ else if (parameter == "max_gps_holdover_time") {
+ ss << m_config.maxGPSHoldoverTime;
}
-#endif // HAVE_LIMESDR
else {
+ if (m_device) {
+ const auto stat = m_device->get_run_statistics();
+ try {
+ const auto& value = stat.at(parameter).v;
+ if (std::holds_alternative<string>(value)) {
+ ss << std::get<string>(value);
+ }
+ else if (std::holds_alternative<double>(value)) {
+ ss << std::get<double>(value);
+ }
+ else if (std::holds_alternative<ssize_t>(value)) {
+ ss << std::get<ssize_t>(value);
+ }
+ else if (std::holds_alternative<size_t>(value)) {
+ ss << std::get<size_t>(value);
+ }
+ else if (std::holds_alternative<bool>(value)) {
+ ss << (std::get<bool>(value) ? 1 : 0);
+ }
+ else if (std::holds_alternative<std::nullopt_t>(value)) {
+ ss << "";
+ }
+ else {
+ throw std::logic_error("variant alternative not handled");
+ }
+ return ss.str();
+ }
+ catch (const std::out_of_range&) {
+ }
+ }
+
ss << "Parameter '" << parameter <<
"' is not exported by controllable " << get_rc_name();
throw ParameterError(ss.str());
@@ -491,4 +505,39 @@ const string SDR::get_parameter(const string& parameter) const
return ss.str();
}
+const json::map_t SDR::get_all_values() const
+{
+ json::map_t stat = m_device->get_run_statistics();
+
+ stat["txgain"].v = m_config.txgain;
+ stat["rxgain"].v = m_config.rxgain;
+ stat["freq"].v = m_config.frequency;
+ stat["muting"].v = m_config.muting;
+ stat["temp"].v = std::nullopt;
+
+ const auto maybe_freq = convert_frequency_to_channel(m_config.frequency);
+
+ if (maybe_freq.has_value()) {
+ stat["channel"].v = *maybe_freq;
+ }
+ else {
+ stat["channel"].v = std::nullopt;
+ }
+
+ if (m_device) {
+ const std::optional<double> temp = m_device->get_temperature();
+ if (temp) {
+ stat["temp"].v = *temp;
+ }
+ }
+ stat["queued_frames_ms"].v = m_queue.size() *
+ (size_t)chrono::duration_cast<chrono::milliseconds>(transmission_frame_duration(m_config.dabMode))
+ .count();
+
+ stat["synchronous"].v = m_config.enableSync;
+ stat["max_gps_holdover_time"].v = (size_t)m_config.maxGPSHoldoverTime;
+
+ return stat;
+}
+
} // namespace Output
diff --git a/src/output/SDR.h b/src/output/SDR.h
index ee89243..86bf295 100644
--- a/src/output/SDR.h
+++ b/src/output/SDR.h
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -34,16 +34,12 @@ DESCRIPTION:
# include <config.h>
#endif
-#include <chrono>
#include "ModPlugin.h"
-#include "EtiReader.h"
#include "output/SDRDevice.h"
#include "output/Feedback.h"
namespace Output {
-using complexf = std::complex<float>;
-
class SDR : public ModOutput, public ModMetadata, public RemoteControllable {
public:
SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device);
@@ -51,6 +47,7 @@ class SDR : public ModOutput, public ModMetadata, public RemoteControllable {
SDR operator=(const SDR& other) = delete;
virtual ~SDR();
+ virtual void set_sample_size(size_t size);
virtual int process(Buffer *dataIn) override;
virtual meta_vec_t process_metadata(const meta_vec_t& metadataIn) override;
@@ -66,15 +63,17 @@ class SDR : public ModOutput, public ModMetadata, public RemoteControllable {
virtual const std::string get_parameter(
const std::string& parameter) const override;
+ virtual const json::map_t get_all_values() const override;
+
private:
void process_thread_entry(void);
- void handle_frame(struct FrameData &frame);
- void sleep_through_frame(void);
+ void handle_frame(struct FrameData&& frame);
SDRDeviceConfig& m_config;
std::atomic<bool> m_running = ATOMIC_VAR_INIT(false);
std::thread m_device_thread;
+ size_t m_size = sizeof(complexf);
std::vector<uint8_t> m_frame;
ThreadsafeQueue<FrameData> m_queue;
@@ -86,9 +85,7 @@ class SDR : public ModOutput, public ModMetadata, public RemoteControllable {
bool last_tx_time_initialised = false;
uint32_t last_tx_second = 0;
uint32_t last_tx_pps = 0;
-
- bool t_last_frame_initialised = false;
- std::chrono::steady_clock::time_point t_last_frame;
+ size_t num_queue_overflows = 0;
};
}
diff --git a/src/output/SDRDevice.h b/src/output/SDRDevice.h
index bb63f60..ec9373d 100644
--- a/src/output/SDRDevice.h
+++ b/src/output/SDRDevice.h
@@ -2,7 +2,7 @@
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2019
+ Copyright (C) 2023
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -38,6 +38,7 @@ DESCRIPTION:
#include <string>
#include <vector>
#include <complex>
+#include <optional>
#include "TimestampDecoder.h"
@@ -56,6 +57,8 @@ struct SDRDeviceConfig {
std::string tx_antenna;
std::string rx_antenna;
+ bool fixedPoint = false;
+
long masterClockRate = 32768000;
unsigned sampleRate = 2048000;
double frequency = 0.0;
@@ -98,32 +101,25 @@ struct SDRDeviceConfig {
struct FrameData {
// Buffer holding frame data
std::vector<uint8_t> buf;
+ size_t sampleSize = sizeof(complexf);
// A full timestamp contains a TIST according to standard
// and time information within MNSC with tx_second.
- struct frame_timestamp ts;
+ frame_timestamp ts;
};
// All SDR Devices must implement the SDRDevice interface
class SDRDevice {
public:
- struct RunStatistics {
- size_t num_underruns = 0;
- size_t num_late_packets = 0;
- size_t num_overruns = 0;
- size_t num_frames_modulated = 0;
-
- int gpsdo_num_sv = 0;
- bool gpsdo_holdover = false;
- };
+ using run_statistics_t = json::map_t;
virtual void tune(double lo_offset, double frequency) = 0;
virtual double get_tx_freq(void) const = 0;
virtual void set_txgain(double txgain) = 0;
virtual double get_txgain(void) const = 0;
- virtual void transmit_frame(const struct FrameData& frame) = 0;
- virtual RunStatistics get_run_statistics(void) const = 0;
+ virtual void transmit_frame(struct FrameData&& frame) = 0;
+ virtual run_statistics_t get_run_statistics(void) const = 0;
virtual double get_real_secs(void) const = 0;
virtual void set_rxgain(double rxgain) = 0;
virtual double get_rxgain(void) const = 0;
@@ -132,16 +128,21 @@ class SDRDevice {
virtual size_t receive_frame(
complexf *buf,
size_t num_samples,
- struct frame_timestamp& ts,
+ frame_timestamp& ts,
double timeout_secs) = 0;
- // Returns device temperature in degrees C or NaN if not available
- virtual double get_temperature(void) const = 0;
+ // Returns device temperature in degrees C
+ virtual std::optional<double> get_temperature(void) const = 0;
// Return true if GPS and reference clock inputs are ok
- virtual bool is_clk_source_ok(void) const = 0;
+ virtual bool is_clk_source_ok(void) = 0;
virtual const char* device_name(void) const = 0;
+
+ virtual void require_timestamp_refresh() { m_require_timestamp_refresh = true; }
+
+ protected:
+ bool m_require_timestamp_refresh = false;
};
} // namespace Output
diff --git a/src/output/Soapy.cpp b/src/output/Soapy.cpp
index f138e9a..6a198b5 100644
--- a/src/output/Soapy.cpp
+++ b/src/output/Soapy.cpp
@@ -86,7 +86,6 @@ Soapy::Soapy(SDRDeviceConfig& config) :
" ksps.";
tune(m_conf.lo_offset, m_conf.frequency);
- m_conf.frequency = m_device->getFrequency(SOAPY_SDR_TX, 0);
etiLog.level(info) << "SoapySDR:Actual frequency: " <<
std::fixed << std::setprecision(3) <<
m_conf.frequency / 1000.0 << " kHz.";
@@ -143,6 +142,8 @@ void Soapy::tune(double lo_offset, double frequency)
SoapySDR::Kwargs offset_arg;
offset_arg["OFFSET"] = to_string(lo_offset);
m_device->setFrequency(SOAPY_SDR_TX, 0, m_conf.frequency, offset_arg);
+
+ m_conf.frequency = m_device->getFrequency(SOAPY_SDR_TX, 0);
}
double Soapy::get_tx_freq(void) const
@@ -180,13 +181,13 @@ double Soapy::get_bandwidth(void) const
return m_device->getBandwidth(SOAPY_SDR_TX, 0);
}
-SDRDevice::RunStatistics Soapy::get_run_statistics(void) const
+SDRDevice::run_statistics_t Soapy::get_run_statistics(void) const
{
- RunStatistics rs;
- rs.num_underruns = underflows;
- rs.num_overruns = overflows;
- rs.num_late_packets = late_packets;
- rs.num_frames_modulated = num_frames_modulated;
+ run_statistics_t rs;
+ rs["underruns"].v = underflows;
+ rs["overruns"].v = overflows;
+ rs["timeouts"].v = timeouts;
+ rs["frames"].v = num_frames_modulated;
return rs;
}
@@ -216,7 +217,7 @@ double Soapy::get_rxgain(void) const
size_t Soapy::receive_frame(
complexf *buf,
size_t num_samples,
- struct frame_timestamp& ts,
+ frame_timestamp& ts,
double timeout_secs)
{
int flags = 0;
@@ -254,7 +255,7 @@ size_t Soapy::receive_frame(
}
-bool Soapy::is_clk_source_ok() const
+bool Soapy::is_clk_source_ok()
{
// TODO
return true;
@@ -265,14 +266,14 @@ const char* Soapy::device_name(void) const
return "Soapy";
}
-double Soapy::get_temperature(void) const
+std::optional<double> Soapy::get_temperature(void) const
{
// TODO Unimplemented
// LimeSDR exports 'lms7_temp'
- return std::numeric_limits<double>::quiet_NaN();
+ return std::nullopt;
}
-void Soapy::transmit_frame(const struct FrameData& frame)
+void Soapy::transmit_frame(struct FrameData&& frame)
{
if (not m_device) throw runtime_error("Soapy device not set up");
@@ -311,7 +312,7 @@ void Soapy::transmit_frame(const struct FrameData& frame)
const bool eob_because_muting = m_conf.muting;
const bool end_of_burst = eob_because_muting or (
frame.ts.timestamp_valid and
- frame.ts.timestamp_refresh and
+ m_require_timestamp_refresh and
samps_to_send <= mtu );
int flags = 0;
@@ -320,6 +321,7 @@ void Soapy::transmit_frame(const struct FrameData& frame)
m_tx_stream, buffs, samps_to_send, flags, timeNs);
if (num_sent == SOAPY_SDR_TIMEOUT) {
+ timeouts++;
continue;
}
else if (num_sent == SOAPY_SDR_OVERFLOW) {
@@ -349,6 +351,7 @@ void Soapy::transmit_frame(const struct FrameData& frame)
SoapySDR::errToStr(ret_deact));
}
m_tx_stream_active = false;
+ m_require_timestamp_refresh = false;
}
if (eob_because_muting) {
diff --git a/src/output/Soapy.h b/src/output/Soapy.h
index 4ee53ca..4fce11a 100644
--- a/src/output/Soapy.h
+++ b/src/output/Soapy.h
@@ -65,8 +65,8 @@ class Soapy : public Output::SDRDevice
virtual double get_txgain(void) const override;
virtual void set_bandwidth(double bandwidth) override;
virtual double get_bandwidth(void) const override;
- virtual void transmit_frame(const struct FrameData& frame) override;
- virtual RunStatistics get_run_statistics(void) const override;
+ virtual void transmit_frame(struct FrameData&& frame) override;
+ virtual run_statistics_t get_run_statistics(void) const override;
virtual double get_real_secs(void) const override;
virtual void set_rxgain(double rxgain) override;
@@ -74,14 +74,14 @@ class Soapy : public Output::SDRDevice
virtual size_t receive_frame(
complexf *buf,
size_t num_samples,
- struct frame_timestamp& ts,
+ frame_timestamp& ts,
double timeout_secs) override;
// Return true if GPS and reference clock inputs are ok
- virtual bool is_clk_source_ok(void) const override;
+ virtual bool is_clk_source_ok(void) override;
virtual const char* device_name(void) const override;
- virtual double get_temperature(void) const override;
+ virtual std::optional<double> get_temperature(void) const override;
private:
SDRDeviceConfig& m_conf;
@@ -91,9 +91,9 @@ class Soapy : public Output::SDRDevice
SoapySDR::Stream *m_rx_stream = nullptr;
bool m_rx_stream_active = false;
+ size_t timeouts = 0;
size_t underflows = 0;
size_t overflows = 0;
- size_t late_packets = 0;
size_t num_frames_modulated = 0;
};
diff --git a/src/output/UHD.cpp b/src/output/UHD.cpp
index 3cf5aef..b30f9e1 100644
--- a/src/output/UHD.cpp
+++ b/src/output/UHD.cpp
@@ -31,10 +31,7 @@
//#define MDEBUG(fmt, args...) fprintf(LOG, fmt , ## args)
#define MDEBUG(fmt, args...)
-#include "PcDebug.h"
#include "Log.h"
-#include "RemoteControl.h"
-#include "Utils.h"
#include <thread>
#include <iomanip>
@@ -52,14 +49,12 @@
# include <uhd/utils/thread_priority.hpp>
#endif
-
-#include <cmath>
#include <iostream>
-#include <assert.h>
+#include <cmath>
+#include <cassert>
#include <stdexcept>
-#include <stdio.h>
+#include <cstdio>
#include <time.h>
-#include <errno.h>
#include <unistd.h>
#include <pthread.h>
@@ -204,7 +199,6 @@ UHD::UHD(SDRDeviceConfig& config) :
tune(m_conf.lo_offset, m_conf.frequency);
- m_conf.frequency = m_usrp->get_tx_freq();
etiLog.level(debug) << std::fixed << std::setprecision(3) <<
"OutputUHD:Actual TX frequency: " << m_conf.frequency;
@@ -236,7 +230,8 @@ UHD::UHD(SDRDeviceConfig& config) :
m_usrp->set_rx_gain(m_conf.rxgain);
etiLog.log(debug, "OutputUHD:Actual RX Gain: %f", m_usrp->get_rx_gain());
- const uhd::stream_args_t stream_args("fc32"); //complex floats
+ const uhd::stream_args_t stream_args(
+ m_conf.fixedPoint ? "sc16" : "fc32");
m_rx_stream = m_usrp->get_rx_stream(stream_args);
m_tx_stream = m_usrp->get_tx_stream(stream_args);
@@ -285,6 +280,8 @@ void UHD::tune(double lo_offset, double frequency)
m_usrp->set_rx_freq(frequency);
}
+
+ m_conf.frequency = m_usrp->get_tx_freq();
}
double UHD::get_tx_freq(void) const
@@ -315,11 +312,12 @@ double UHD::get_bandwidth(void) const
return m_usrp->get_tx_bandwidth();
}
-void UHD::transmit_frame(const struct FrameData& frame)
+void UHD::transmit_frame(struct FrameData&& frame)
{
const double tx_timeout = 20.0;
- const size_t sizeIn = frame.buf.size() / sizeof(complexf);
- const complexf* in_data = reinterpret_cast<const complexf*>(&frame.buf[0]);
+
+ const size_t sample_size = m_conf.fixedPoint ? (2 * sizeof(int16_t)) : sizeof(complexf);
+ const size_t sizeIn = frame.buf.size() / sample_size;
uhd::tx_metadata_t md_tx;
@@ -348,18 +346,19 @@ void UHD::transmit_frame(const struct FrameData& frame)
// EOB and quit the loop afterwards, to avoid an underrun.
md_tx.end_of_burst = eob_because_muting or (
frame.ts.timestamp_valid and
- frame.ts.timestamp_refresh and
+ m_require_timestamp_refresh and
samps_to_send <= usrp_max_num_samps );
+ m_require_timestamp_refresh = false;
- //send a single packet
+ // send a single packet
size_t num_tx_samps = m_tx_stream->send(
- &in_data[num_acc_samps],
+ frame.buf.data() + sample_size * num_acc_samps,
samps_to_send, md_tx, tx_timeout);
etiLog.log(trace, "UHD,sent %zu of %zu", num_tx_samps, samps_to_send);
num_acc_samps += num_tx_samps;
- md_tx.time_spec += uhd::time_spec_t(0, num_tx_samps/m_conf.sampleRate);
+ md_tx.time_spec += uhd::time_spec_t::from_ticks(num_tx_samps, (double)m_conf.sampleRate);
if (num_tx_samps == 0) {
etiLog.log(warn,
@@ -376,18 +375,22 @@ void UHD::transmit_frame(const struct FrameData& frame)
}
-SDRDevice::RunStatistics UHD::get_run_statistics(void) const
+SDRDevice::run_statistics_t UHD::get_run_statistics(void) const
{
- RunStatistics rs;
- rs.num_underruns = num_underflows;
- rs.num_overruns = num_overflows;
- rs.num_late_packets = num_late_packets;
- rs.num_frames_modulated = num_frames_modulated;
+ run_statistics_t rs;
+ rs["underruns"].v = num_underflows;
+ rs["overruns"].v = num_overflows;
+ rs["late_packets"].v = num_late_packets;
+ rs["frames"].v = num_frames_modulated;
if (m_device_time) {
const auto gpsdo_stat = m_device_time->get_gnss_stats();
- rs.gpsdo_holdover = gpsdo_stat.holdover;
- rs.gpsdo_num_sv = gpsdo_stat.num_sv;
+ rs["gpsdo_holdover"].v = gpsdo_stat.holdover;
+ rs["gpsdo_num_sv"].v = gpsdo_stat.num_sv;
+ }
+ else {
+ rs["gpsdo_holdover"].v = true;
+ rs["gpsdo_num_sv"].v = 0;
}
return rs;
}
@@ -411,7 +414,7 @@ double UHD::get_rxgain() const
size_t UHD::receive_frame(
complexf *buf,
size_t num_samples,
- struct frame_timestamp& ts,
+ frame_timestamp& ts,
double timeout_secs)
{
uhd::stream_cmd_t cmd(
@@ -434,7 +437,7 @@ size_t UHD::receive_frame(
}
// Return true if GPS and reference clock inputs are ok
-bool UHD::is_clk_source_ok(void) const
+bool UHD::is_clk_source_ok(void)
{
bool ok = true;
@@ -471,13 +474,13 @@ const char* UHD::device_name(void) const
return "UHD";
}
-double UHD::get_temperature(void) const
+std::optional<double> UHD::get_temperature(void) const
{
try {
return std::round(m_usrp->get_tx_sensor("temp", 0).to_real());
}
catch (const uhd::lookup_error &e) {
- return std::numeric_limits<double>::quiet_NaN();
+ return std::nullopt;
}
}
diff --git a/src/output/UHD.h b/src/output/UHD.h
index 29867fb..c4f1a45 100644
--- a/src/output/UHD.h
+++ b/src/output/UHD.h
@@ -45,24 +45,13 @@ DESCRIPTION:
#include <atomic>
#include <thread>
-#include "Log.h"
#include "output/SDR.h"
#include "output/USRPTime.h"
#include "TimestampDecoder.h"
-#include "RemoteControl.h"
-#include "ThreadsafeQueue.h"
#include <stdio.h>
#include <sys/types.h>
-// If the timestamp is further in the future than
-// 100 seconds, abort
-#define TIMESTAMP_ABORT_FUTURE 100
-
-// Add a delay to increase buffers when
-// frames are too far in the future
-#define TIMESTAMP_MARGIN_FUTURE 0.5
-
namespace Output {
class UHD : public Output::SDRDevice
@@ -79,8 +68,8 @@ class UHD : public Output::SDRDevice
virtual double get_txgain(void) const override;
virtual void set_bandwidth(double bandwidth) override;
virtual double get_bandwidth(void) const override;
- virtual void transmit_frame(const struct FrameData& frame) override;
- virtual RunStatistics get_run_statistics(void) const override;
+ virtual void transmit_frame(struct FrameData&& frame) override;
+ virtual run_statistics_t get_run_statistics(void) const override;
virtual double get_real_secs(void) const override;
virtual void set_rxgain(double rxgain) override;
@@ -88,14 +77,14 @@ class UHD : public Output::SDRDevice
virtual size_t receive_frame(
complexf *buf,
size_t num_samples,
- struct frame_timestamp& ts,
+ frame_timestamp& ts,
double timeout_secs) override;
// Return true if GPS and reference clock inputs are ok
- virtual bool is_clk_source_ok(void) const override;
+ virtual bool is_clk_source_ok(void) override;
virtual const char* device_name(void) const override;
- virtual double get_temperature(void) const override;
+ virtual std::optional<double> get_temperature(void) const override;
private:
SDRDeviceConfig& m_conf;
diff --git a/src/output/USRPTime.cpp b/src/output/USRPTime.cpp
index d1197ec..5a11851 100644
--- a/src/output/USRPTime.cpp
+++ b/src/output/USRPTime.cpp
@@ -46,11 +46,14 @@ USRPTime::USRPTime(
m_conf(conf),
time_last_check(timepoint_t::clock::now())
{
- if (m_conf.pps_src == "none") {
+ if (m_conf.refclk_src == "internal" and m_conf.pps_src != "none") {
+ etiLog.level(warn) << "OutputUHD: Unusal refclk and pps source settings. Setting time once, no monitoring.";
+ set_usrp_time_from_pps();
+ }
+ else if (m_conf.pps_src == "none") {
if (m_conf.enableSync) {
etiLog.level(warn) <<
- "OutputUHD: WARNING:"
- " you are using synchronous transmission without PPS input!";
+ "OutputUHD: you are using synchronous transmission without PPS input!";
}
set_usrp_time_from_localtime();