aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--ChangeLog16
-rw-r--r--INSTALL.md158
-rw-r--r--Makefile.am43
-rw-r--r--README.md15
-rw-r--r--TODO66
-rw-r--r--configure.ac39
-rw-r--r--doc/easydabv3.ini37
-rw-r--r--doc/example.ini32
-rwxr-xr-xdoc/fir-filter/generate-filter.py2
-rwxr-xr-xdoc/receive_events.py59
-rwxr-xr-xdoc/zmq-ctrl/json_remote_server.py131
-rwxr-xr-xdoc/zmq-ctrl/zmq_remote.py22
-rw-r--r--fpm/LICENSE21
-rw-r--r--fpm/README.md48
-rw-r--r--fpm/fixed.hpp490
-rw-r--r--fpm/ios.hpp740
-rw-r--r--fpm/math.hpp684
-rw-r--r--kiss/CHANGELOG123
-rw-r--r--kiss/COPYING11
-rw-r--r--kiss/README.md245
-rw-r--r--kiss/_kiss_fft_guts.h167
-rw-r--r--kiss/kfc.c109
-rw-r--r--kiss/kfc.h54
-rw-r--r--kiss/kiss_fft.c420
-rw-r--r--kiss/kiss_fft.h160
-rw-r--r--kiss/kiss_fft_log.h36
-rw-r--r--kiss/kiss_fftnd.c188
-rw-r--r--kiss/kiss_fftnd.h26
-rw-r--r--kiss/kiss_fftndr.c120
-rw-r--r--kiss/kiss_fftndr.h55
-rw-r--r--kiss/kiss_fftr.c155
-rw-r--r--kiss/kiss_fftr.h54
-rw-r--r--lib/Globals.cpp2
-rw-r--r--lib/Json.cpp128
-rw-r--r--lib/Json.h65
-rw-r--r--lib/RemoteControl.cpp61
-rw-r--r--lib/RemoteControl.h14
-rw-r--r--lib/Socket.cpp233
-rw-r--r--lib/Socket.h46
-rw-r--r--lib/ThreadsafeQueue.h54
-rw-r--r--lib/edi/ETIDecoder.cpp14
-rw-r--r--lib/edi/ETIDecoder.hpp4
-rw-r--r--lib/edi/common.cpp33
-rw-r--r--lib/edi/common.hpp12
-rw-r--r--m4/ax_cxx_compile_stdcxx.m489
-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
124 files changed, 10220 insertions, 2220 deletions
diff --git a/.gitignore b/.gitignore
index 3ac59f6..2a6b136 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,9 @@ config.h
config.h.in
config.status
odr-dabmod
+*~
+
+*.iq
__pycache__/
*.py[cod]
diff --git a/ChangeLog b/ChangeLog
index a0d99dd..6266e8c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,22 @@
This file contains information about the changes done to
ODR-DabMod in this repository
+2025-02-26: Matthias P. Braendli <matthias@mpb.li>
+ (v3.0.0):
+ Add support for the PrecisionWave DEXTER Modulator.
+ Improve timestamp decoding and handling.
+ Fix TII carrier levels.
+ Add `showjson` command to RC to support structured output,
+ and add useful metrics to the RC for monitoring, e.g. some Ensemble information from FIC.
+ Remove EasyDABv3 support.
+ Remove ZeroMQ input.
+ Require C++17.
+ v2.7.0 retracted.
+
+2022-04-20: Matthias P. Braendli <matthias@mpb.li>
+ (v2.6.0):
+ Add timestamps to log output.
+
2022-02-02: Matthias P. Braendli <matthias@mpb.li>
(v2.5.0):
Add support for BladeRF devices.
diff --git a/INSTALL.md b/INSTALL.md
index 5cb5a04..b53e204 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -1,41 +1,76 @@
-Required dependencies:
-======================
-
-* A C++11 capable compiler
-* pkg-config
-* FFTW 3.x
-* autoconf
-* Optional UHD for USRP
-* Optional LimeSuite for LimeSDR support
-* Optional SoapySDR (see below)
-* Optional bladerf (see below)
-* Optional ZeroMQ http://www.zeromq.org
-
-Simple install procedure:
-=========================
-
- % tar xjf odr-dabmod-X.Y.Z.tar.bz2 # Unpack the source
- % cd odr-dabmod-X.Y.Z # Change to the source directory
- % ./configure
- # Run the configure script
- % make # Build ODR-DabMod
- [ as root ]
- % make install # Install ODR-DabMod
-
-Configure options
-=================
-
+You have 3 ways to install odr-dabmod on your host:
+
+# Using your linux distribution packaging system
+`odr-dabmod` is available on the official repositories of several debian-based distributions, such as Debian
+(from Debian 12), Ubuntu (from 24.10), Opensuse and Arch.
+
+If you are using Debian 12 (Bookworm), you will need to
+[add the backports repository](https://backports.debian.org/Instructions/)
+
+**Notice**: this package does not include the web-based GUI and Digital Predistortion Computation engine
+
+# Using installation scripts
+If your linux distribution is debian-based, you can install odr-dabmod
+as well as the other main components of the mmbTools set with the
+[Opendigitalradio dab-scripts](https://github.com/opendigitalradio/dab-scripts.git)
+
+# Compiling manually
+Unlike the 2 previous options, this one allows you to compile odr-dabmod with the features you really need.
+
+## Dependencies
+### Debian Bullseye-based OS:
+```
+# Required packages
+## C++11 compiler
+sudo apt-get install --yes build-essential automake libtool
+
+## FFTW 3.x
+sudo apt-get install --yes libfftw3-dev
+
+# optional packages
+## ZeroMQ http://www.zeromq.org
+sudo apt-get install --yes libzmq3-dev libzmq5
+
+## UHD for USRP
+sudo apt-get install --yes libuhd-dev
+
+## LimeSuite for LimeSDR support
+sudo apt-get install --yes liblimesuite-dev
+
+## SoapySDR (see below)
+sudo apt-get install --yes libsoapysdr-dev
+
+## bladerf (see below)
+sudo apt-get install --yes libbladerf-dev
+```
+
+## Compilation
+1. Clone this repository:
+ ```
+ # stable version:
+ git clone https://github.com/Opendigitalradio/ODR-DabMod.git
+
+ # or development version (at your own risk):
+ git clone https://github.com/Opendigitalradio/ODR-DabMod.git -b next
+ ```
+1. Configure the project
+ ```
+ cd ODR-DabMod
+ ./bootstrap
+ ./configure
+ ```
+1. Compile and install:
+ ```
+ make
+ sudo make install
+ ```
+
+### Configure options
The configure script can be launched with a variety of options:
-
-Disable ZeroMQ input (to be used with ODR-DabMux), output and remotecontrol: `--disable-zeromq`
-
-Disable the binding to the UHD driver for USRPs: `--disable-output-uhd`
-
-Compile using the `-ffast-math` option that gives a substantial speedup at the cost of floating point correctness: `--enable-fast-math`
-
-On some ARM systems you might need `--with-boost-libdir=/usr/lib/arm-linux-gnueabihf` so that boost gets found.
-
-Do not pass `-march=native` to the compiler: `--disable-native`
+- Disable ZeroMQ input (to be used with ODR-DabMod), output and remotecontrol: `--disable-zeromq`
+- Disable the binding to the UHD driver for USRPs: `--disable-output-uhd`
+- Compile using the `-ffast-math` option that gives a substantial speedup at the cost of floating point correctness: `--enable-fast-math`
+- Do not pass `-march=native` to the compiler by using the argument: `--disable-native`
**Remark:** Do not compile ODR-DabMod with `-march=native` compiler option. This is meant for distribution package maintainers who want to use their own march option, and for people running into compilation issues due to `-march=native`. (e.g. GCC bug 70132 on ARM systems)
@@ -44,11 +79,11 @@ Do not pass `-march=native` to the compiler: `--disable-native`
Create debugging files for each DSP block for data analysis: `--enable-trace`
For more information, call:
+```
+./configure --help
+```
- % ./configure --help
-
-Performance optimisation
-------------------------
+#### Performance optimisation
While the performance of modern systems is good enough in most cases to
run ODR-DabMod, it is sometimes necessary to increase the compilation
optimisation if all features are used or on slow systems.
@@ -61,45 +96,28 @@ Tricks for best performance:
* Disable assertions with `-DNDEBUG`
Applying all together:
+```
+./configure CFLAGS="-O3 -DNDEBUG" CXXFLAGS="-O3 -DNDEBUG" --enable-fast-math
+```
- % ./configure CFLAGS="-O3 -DNDEBUG" CXXFLAGS="-O3 -DNDEBUG" --enable-fast-math
-
-Checking for memory usage issues
---------------------------------
+#### Checking for memory usage issues
If your compiler supports it, you can enable the address sanitizer to check for memory
issues:
-
- % ./configure CFLAGS="-fsanitize=address -g -O2" CXXFLAGS="-fsanitize=address -g -O2"
+```
+./configure CFLAGS="-fsanitize=address -g -O2" CXXFLAGS="-fsanitize=address -g -O2"
+```
The resulting binary will be instrumented with additional memory checks, which have a
measurable overhead. Please report if you get warnings or errors when using the sanitizer.
-Nearly as simple install procedure using repository:
-====================================================
-
-* Download and install dependencies as above
-* Clone the git repository
-* Bootstrap autotools:
-
- % ./bootstrap.sh
-
- In case this fails, try:
-
- % aclocal && automake --gnu --add-missing && autoconf
-
-* Then use `./configure` as above
-
-SoapySDR support and required dependencies
-==========================================
-
+## SoapySDR support and required dependencies
SoapySDR is a vendor-neutral library to drive SDR devices. It can be used to
drive the HackRF and the LimeSDR among others.
Required dependencies that need to be installed are, in order:
-
1. SoapySDR itself from https://github.com/pothosware/SoapySDR
-2. The LimeSuite for the LimeSDR from https://github.com/myriadrf/LimeSuite
-3. HackRF support for SoapySDR from https://github.com/pothosware/SoapyHackRF
+1. The LimeSuite for the LimeSDR from https://github.com/myriadrf/LimeSuite
+1. HackRF support for SoapySDR from https://github.com/pothosware/SoapyHackRF
ODR-DabMod will automatically recognise if the SoapySDR library is installed on
your system, and will print at the end of `./configure` if support is enabled or
@@ -107,7 +125,5 @@ not.
A configuration example is available in `doc/example.ini`
-BladeRF support
-===============
-
+## BladeRF support
In order to use `--enable-bladerf`, you need to install the `libbladerf2` including the -dev package.
diff --git a/Makefile.am b/Makefile.am
index 39280fb..87d553a 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,7 +1,7 @@
# Copyright (C) 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
@@ -35,17 +35,20 @@ endif
bin_PROGRAMS = odr-dabmod
-odr_dabmod_CFLAGS = -Wall -Isrc -Ilib \
- $(GITVERSION_FLAGS)
-odr_dabmod_CXXFLAGS = -Wall -Isrc -Ilib -std=c++11 \
- $(GITVERSION_FLAGS) $(BOOST_CPPFLAGS)
-odr_dabmod_LDADD = $(BOOST_LDFLAGS) $(BOOST_THREAD_LIB)
+KISS_FLAGS=-DFIXED_POINT=16
+odr_dabmod_CFLAGS = -Wall -Isrc -Ilib -Ikiss \
+ $(GITVERSION_FLAGS) $(KISS_FLAGS)
+odr_dabmod_CXXFLAGS = -Wall -Isrc -Ilib -Ikiss \
+ $(GITVERSION_FLAGS) $(BOOST_CPPFLAGS) $(KISS_FLAGS)
+odr_dabmod_LDADD = $(BOOST_LDFLAGS) $(BOOST_THREAD_LIB) $(UHD_LIBS) $(LIMESDR_LIBS) $(ADDITIONAL_UHD_LIBS)
odr_dabmod_SOURCES = src/DabMod.cpp \
src/PcDebug.h \
src/DabModulator.cpp \
src/DabModulator.h \
src/Buffer.cpp \
src/Buffer.h \
+ src/CharsetTools.cpp \
+ src/CharsetTools.h \
src/ConfigParser.cpp \
src/ConfigParser.h \
src/ModPlugin.cpp \
@@ -54,6 +57,10 @@ odr_dabmod_SOURCES = src/DabMod.cpp \
src/EtiReader.h \
src/Eti.cpp \
src/Eti.h \
+ src/Events.cpp \
+ src/Events.h \
+ src/FigParser.cpp \
+ src/FigParser.h \
src/FicSource.cpp \
src/FicSource.h \
src/PuncturingRule.cpp \
@@ -75,7 +82,6 @@ odr_dabmod_SOURCES = src/DabMod.cpp \
src/InputMemory.h \
src/InputReader.h \
src/InputTcpReader.cpp \
- src/InputZeroMQReader.cpp \
src/OutputFile.cpp \
src/OutputFile.h \
src/FrameMultiplexer.cpp \
@@ -99,6 +105,8 @@ odr_dabmod_SOURCES = src/DabMod.cpp \
lib/RemoteControl.h \
lib/Log.cpp \
lib/Log.h \
+ lib/Json.h \
+ lib/Json.cpp \
lib/Globals.cpp \
lib/INIReader.h \
lib/crc.h \
@@ -123,10 +131,7 @@ odr_dabmod_SOURCES = src/DabMod.cpp \
lib/edi/ETIDecoder.hpp \
lib/edi/ETIDecoder.cpp \
lib/edi/PFT.hpp \
- lib/edi/PFT.cpp
-
-if !COMPILE_FOR_EASYDABV3
-odr_dabmod_SOURCES += \
+ lib/edi/PFT.cpp \
src/FIRFilter.cpp \
src/FIRFilter.h \
src/MemlessPoly.cpp \
@@ -138,6 +143,8 @@ odr_dabmod_SOURCES += \
src/output/SDR.cpp \
src/output/SDR.h \
src/output/SDRDevice.h \
+ src/output/Dexter.cpp \
+ src/output/Dexter.h \
src/output/Soapy.cpp \
src/output/Soapy.h \
src/output/UHD.cpp \
@@ -169,9 +176,17 @@ odr_dabmod_SOURCES += \
src/PAPRStats.cpp \
src/PAPRStats.h \
src/TII.cpp \
- src/TII.h
+ src/TII.h \
+ kiss/kfc.h \
+ kiss/kfc.c \
+ kiss/kiss_fft.c \
+ kiss/kiss_fft.h \
+ kiss/kiss_fftnd.c \
+ kiss/kiss_fftnd.h \
+ kiss/kiss_fftndr.c \
+ kiss/kiss_fftndr.h \
+ kiss/kiss_fftr.c \
+ kiss/kiss_fftr.h
-odr_dabmod_LDADD += $(UHD_LIBS) $(LIMESDR_LIBS) $(ADDITIONAL_UHD_LIBS)
-endif
man_MANS = man/odr-dabmod.1
diff --git a/README.md b/README.md
index a23de3d..a331c8b 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,6 @@ to ETSI EN 300 401. It is the continuation of the work started by
the Communications Research Center Canada, and is now pursued in the
[Opendigitalradio project](http://opendigitalradio.org).
-
ODR-DabMod is part of the ODR-mmbTools tool-set. More information about the
ODR-mmbTools is available in the *guide*, available on the
[Opendigitalradio mmbTools page](http://www.opendigitalradio.org/mmbtools).
@@ -17,6 +16,7 @@ Features
- Supports native DAB sample rate and can also resample to other rates
- Supports all four DAB transmission modes
- Configuration file support, see `doc/example.ini`
+- First-class support for the [PrecisionWave DEXTER](https://precisionwave.com/products/dexter) device
- First-class support for [USRP devices](https://www.ettus.com/product) using UHD driver
- Tested for B200, B100, USRP2, USRP1
- With WBX daughterboard (where appropriate)
@@ -31,15 +31,16 @@ Features
- TII insertion
- Logging: log to file, to syslog
- EDI sources: TCP and UDP, both with and without Protection and Fragmentation Layer.
-- ETI sources: ETI-over-TCP, file (Raw, Framed and Streamed) and ZeroMQ
+- ETI sources: ETI-over-TCP, file (Raw, Framed and Streamed)
- A Telnet and ZeroMQ remote-control that can be used to change
some parameters during runtime and retrieve statistics.
See `doc/README-RC.md` for more information
- ZeroMQ PUB and REP output, useful for sending IQ to GNURadio flowgraphs.
-- Development has stalled on the following topics:
- - Experimental prototype about digital predistortion for PA linearisation.
- - See `python/dpd/README.md`
- - A web GUI for control and supervision of modulator and predistortion engine. See `python/gui/README.md`
+
+Development has stalled on the following topics:
+- Experimental prototype about digital predistortion for PA linearisation.
+ - See `python/dpd/README.md`
+- A web GUI for control and supervision of modulator and predistortion engine. See `python/gui/README.md`
The `src/` directory contains the source code of ODR-DabMod.
@@ -64,8 +65,6 @@ CONTACT
=======
Matthias P. Braendli *matthias [at] mpb [dot] li*
-Pascal Charest *pascal [dot] charest [at] crc [dot] ca*
-
With thanks to other contributors listed in AUTHORS
http://opendigitalradio.org/
diff --git a/TODO b/TODO
index ddca4c1..4580945 100644
--- a/TODO
+++ b/TODO
@@ -1,59 +1,7 @@
-This TODO file lists ideas and features for future developments. They are
-more or less ordered according to their benefit, but that is subjective
-to some degree.
+This TODO file lists ideas and features for future developments.
Unless written, no activity has been started on the topics.
-TODOs for ongoing SDR output refactoring
-----------------------------------------
-Currently, all the frontend tuning and timestamping settings are UHD-specific.
-To make it possible to run with synchronous=1 using Soapy, refactoring the
-output to share the parts that are common.
-
-This would go towards SFN support with LimeSDR devices, under the condition
-that the LimeSDR gets support for a HW timestamp that can be set from a PPS
-signal. Discussion ongoing here
-https://discourse.myriadrf.org/t/synchronize-two-limesdr/1714
-
-DPD will be possible too.
-
-
-Test sleep_through_frame implementation.
-
-Clean up and separate GPS and refclk checks.
- * *done* handle UHD GPSDO and time
- * *done* handle SoapySDR time
- * Add refclk stuff and timestamps to Soapy.
- * Ensure muting is set properly at startup.
- * Ensure synchronous is taken in account.
- * Verify resync after underflow and muting
- * Add GPSDO status to RC and munin.
-
-*done* Add antenna selection to config.
-
-Test RC entries.
-
-*done* Portability: replace clock_gettime with std::chrono
-
-*done* Make an abstraction for the DPD feedback server, use it for Soapy and UHD.
-
-*optional* Move staticdelay into a new process block
-
-Double-check all #includes
-
-Move other non SDR outputs to the output folder.
-
-Tests, both with B200 and LimeSDR:
-- No timestamps
-- with timestamps
-- LO offset
-- muting through RC
-- proper muting in absence of timestamps
-- GPS lock loss behaviour
-- All RC commands
-- Underrun recovery
-- Proper teardown
-- DPD server
Smaller things
--------------
@@ -61,18 +9,6 @@ Smaller things
Remove GuardIntervalInserter implementation for window==0, as it was shown both are equivalent.
-Finalise EDI input
-------------------
-The EDI input, based on work started in http://git.mpb.li/git/odr-edilib/
-is not complete:
-
- * Add option to define max fill of input udp buffer.
- * Flag and present errors in some way (packets missing, RS faults, sequence errors, etc)
- * Detect and handle changes in mux composition
- * Fix misbehaviours when packets are intentionnally interleaved
- * Fix hangup where it tries to decode old PSEQs for too long
-
-
Resampler improvements
----------------------
* Assess quality of window currently used.
diff --git a/configure.ac b/configure.ac
index 7dc7bb5..46bbaf1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,7 +1,7 @@
# Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 Her Majesty the
# Queen in Right of Canada (Communications Research Center Canada)
-# Copyright (C) 2022 Matthias P. Braendli, http://opendigitalradio.org
+# Copyright (C) 2025 Matthias P. Braendli, http://opendigitalradio.org
# This file is part of ODR-DabMod.
#
@@ -19,7 +19,7 @@
# along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>.
AC_PREREQ([2.69])
-AC_INIT([ODR-DabMod],[2.5.0],[matthias.braendli@mpb.li])
+AC_INIT([ODR-DabMod],[3.0.0],[matthias.braendli@mpb.li])
AC_CONFIG_AUX_DIR([build-aux])
AC_CONFIG_MACRO_DIR([m4])
AC_CANONICAL_TARGET
@@ -34,7 +34,7 @@ AC_PROG_CC
AM_PROG_CC_C_O
AC_PROG_INSTALL
-AX_CXX_COMPILE_STDCXX(11,noext,mandatory)
+AX_CXX_COMPILE_STDCXX(17,noext,mandatory)
EXTRA=""
AC_ARG_ENABLE([prof],
@@ -47,14 +47,14 @@ AC_ARG_ENABLE([trace],
[AS_HELP_STRING([--enable-trace], [Enable trace output])],
[], [enable_trace=no])
AC_ARG_ENABLE([zeromq],
- [AS_HELP_STRING([--disable-zeromq], [Disable ZeroMQ input, output and remote control])],
+ [AS_HELP_STRING([--disable-zeromq], [Disable ZeroMQ output and remote control])],
[], [enable_zeromq=yes])
AC_ARG_ENABLE([native],
[AS_HELP_STRING([--disable-native], [Do not compile with -march=native])],
[], [enable_native=yes])
-AC_ARG_ENABLE([easydabv3],
- [AS_HELP_STRING([--enable-easydabv3], [Build for EasyDABv3 board])],
- [], [enable_easydabv3=no])
+AC_ARG_ENABLE([dexter],
+ [AS_HELP_STRING([--enable-dexter], [Build for PrecisionWave Dexter board])],
+ [], [enable_dexter=no])
AC_ARG_ENABLE([limesdr],
[AS_HELP_STRING([--enable-limesdr], [Build for LimeSDR board])],
[], [enable_limesdr=no])
@@ -77,9 +77,7 @@ AX_CHECK_COMPILE_FLAG([-Wdouble-promotion], [CXXFLAGS="$CXXFLAGS -Wdouble-promot
AX_CHECK_COMPILE_FLAG(["-Wformat=2"], [CXXFLAGS="$CXXFLAGS -Wformat=2"], [], ["-Werror"])
AC_LANG_POP([C++])
-
-AS_IF([test "x$enable_easydabv3" = "xno"],
- [PKG_CHECK_MODULES([FFTW], [fftw3f], [], [AC_MSG_ERROR([FFTW is required])])])
+PKG_CHECK_MODULES([FFTW], [fftw3f], [], [AC_MSG_ERROR([FFTW is required])])
echo "Checking zeromq"
@@ -101,28 +99,27 @@ AS_IF([test "x$enable_trace" != "xno"],
# Define conditionals for Makefile.am
AM_CONDITIONAL([IS_GIT_REPO], [test -d '.git'])
-AM_CONDITIONAL([COMPILE_FOR_EASYDABV3], [test "x$enable_easydabv3" = "xyes"])
# Defines for config.h
AX_PTHREAD([], AC_MSG_ERROR([requires pthread]))
-AS_IF([test "x$enable_easydabv3" = "xno"],
- [PKG_CHECK_MODULES([SOAPYSDR], [SoapySDR], enable_soapysdr=yes, enable_soapysdr=no)])
+PKG_CHECK_MODULES([SOAPYSDR], [SoapySDR], enable_soapysdr=yes, enable_soapysdr=no)
AS_IF([test "x$enable_limesdr" = "xyes"],
[AC_CHECK_LIB([LimeSuite], [LMS_Init], [LIMESDR_LIBS="-lLimeSuite"],
[AC_MSG_ERROR([LimeSDR LimeSuite is required])])])
+AS_IF([test "x$enable_dexter" = "xyes"],
+ [AC_CHECK_LIB([iio], [iio_create_scan_context], [IIO_LIBS="-liio"],
+ [AC_MSG_ERROR([libiio is required])])])
+
AS_IF([test "x$enable_bladerf" = "xyes"],
[AC_CHECK_LIB([bladeRF], [bladerf_open], [BLADERF_LIBS="-lbladeRF"],
[AC_MSG_ERROR([BladeRF library is required])])])
AC_SUBST([CFLAGS], ["$CFLAGS $EXTRA $FFTW_CFLAGS $SOAPYSDR_CFLAGS $PTHREAD_CFLAGS"])
AC_SUBST([CXXFLAGS], ["$CXXFLAGS $EXTRA $FFTW_CFLAGS $SOAPYSDR_CFLAGS $PTHREAD_CFLAGS"])
-AC_SUBST([LIBS], ["$FFTW_LIBS $SOAPYSDR_LIBS $PTHREAD_LIBS $ZMQ_LIBS $LIMESDR_LIBS $BLADERF_LIBS"])
-
-AS_IF([test "x$enable_easydabv3" = "xyes" && test "x$enable_output_uhd" == "xyes"],
- AC_MSG_ERROR([Cannot enable both EasyDABv3 and UHD output]))
+AC_SUBST([LIBS], ["$FFTW_LIBS $SOAPYSDR_LIBS $PTHREAD_LIBS $ZMQ_LIBS $LIMESDR_LIBS $IIO_LIBS $BLADERF_LIBS"])
# Checks for UHD.
AS_IF([test "x$enable_output_uhd" = "xyes"],
@@ -144,12 +141,12 @@ AS_IF([test "x$enable_soapysdr" = "xyes"],
AS_IF([test "x$enable_limesdr" = "xyes"],
[AC_DEFINE(HAVE_LIMESDR, [1], [Define if LimeSDR output is enabled]) ])
+AS_IF([test "x$enable_dexter" = "xyes"],
+ [AC_DEFINE(HAVE_DEXTER, [1], [Define if Dexter output is enabled])])
+
AS_IF([test "x$enable_bladerf" = "xyes"],
[AC_DEFINE(HAVE_BLADERF, [1], [Define if BladeRF output is enabled]) ])
-AS_IF([test "x$enable_easydabv3" = "xyes"],
- AC_DEFINE(BUILD_FOR_EASYDABV3, [1], [Define if we are building for EasyDABv3]))
-
# Checks for header files.
AC_CHECK_HEADERS([fcntl.h limits.h memory.h netinet/in.h stdint.h stdlib.h string.h sys/time.h sys/timeb.h unistd.h])
@@ -217,7 +214,7 @@ echo "***********************************************"
echo
enabled=""
disabled=""
-for feat in prof trace output_uhd zeromq soapysdr easydabv3 limesdr bladerf
+for feat in prof trace output_uhd zeromq soapysdr limesdr bladerf dexter
do
eval var=\$enable_$feat
AS_IF([test "x$var" = "xyes"],
diff --git a/doc/easydabv3.ini b/doc/easydabv3.ini
deleted file mode 100644
index 5f0103f..0000000
--- a/doc/easydabv3.ini
+++ /dev/null
@@ -1,37 +0,0 @@
-; This sample configuration is useful if ODR-DabMod is compiled
-; with --enable-easydabv3
-
-[remotecontrol]
-zmqctrl=1
-zmqctrlendpoint=tcp://127.0.0.1:9400
-; There is no telnet RC available in this build
-
-
-[log]
-syslog=0
-filelog=0
-filename=odr-dabmod.log
-
-[input]
-transport=zeromq
-source=tcp://localhost:9100
-max_frames_queued=400
-
-; There are no [modulator], [cfr], [firfilter], [poly] nor [tii] sections
-
-[output]
-output=file
-
-[fileoutput]
-; to be confirmed
-format=complexf
-
-filename=/dev/csdiof1
-
-show_metadata=0
-; TODO add option for writing out timestamps to csdiof1
-
-[delaymanagement]
-synchronous=0
-mutenotimestamps=0
-offset=1.002
diff --git a/doc/example.ini b/doc/example.ini
index aca7634..0d0f8e3 100644
--- a/doc/example.ini
+++ b/doc/example.ini
@@ -1,5 +1,12 @@
; Sample configuration file for ODR-DabMod
+[general]
+; Before starting up, run this command, and abort if it doesn't return 0.
+; This is useful to ensure that NTP has properly synchronised time.
+startupcheck=
+;startupcheck=chronyc waitsync 10 0.01
+;startupcheck=ntp-wait -fv
+
[remotecontrol]
; The RC feature is described in detail in doc/README-RC.md
@@ -69,13 +76,6 @@ loop=0
;transport=tcp
;source=localhost:9200
-; When recieving data using ZeroMQ, the source is the URI to be used
-;transport=zeromq
-;source=tcp://localhost:9100
-; The option max_frames_queued defines the maximum number of ETI frames
-; (frame duration: 24ms) that can be in the input queue
-;max_frames_queued=100
-
[modulator]
; Mode 'fix' uses a fixed factor and is really not recommended. It is more
; useful on an academic perspective for people trying to understand the DAB
@@ -103,6 +103,8 @@ gainmode=var
; If not defined, use Transmission Mode 1
;mode=1
+fixed_point=1
+
; The digital gain is a value that is multiplied to each sample. It is used
; to tune the chain to make sure that no non-linearities appear up to the
; USRP daughterboard programmable gain amplifier (PGA).
@@ -164,7 +166,7 @@ enabled=0
polycoeffile=polyCoefs
[output]
-; choose output: possible values: uhd, file, zmq, soapysdr, limesdr, bladerf
+; choose output: possible values: uhd, file, zmq, dexter, soapysdr, limesdr, bladerf
output=uhd
[fileoutput]
@@ -322,6 +324,20 @@ channel=13C
; Set to 0 to disable
;dpd_port=50055
+[dexteroutput]
+; More details about the PrecisionWave DEXTER:
+; https://github.com/PrecisionWave/DexterDABModulator
+txgain=65535
+
+; channel/frequency is applied to ad9957.center_frequency
+;frequency=234208000
+channel=13C
+
+; lo offset is applied to dexter_dsp_tx.frequency0
+lo_offset=0
+
+max_gps_holdover_time=3600
+
[limeoutput]
; Lime output directly runs against the LMS device driver. It does not support SFN nor predistortion.
device=
diff --git a/doc/fir-filter/generate-filter.py b/doc/fir-filter/generate-filter.py
index fd181ed..0836446 100755
--- a/doc/fir-filter/generate-filter.py
+++ b/doc/fir-filter/generate-filter.py
@@ -56,7 +56,7 @@ cutoff = 810e3
transition_width = 250e3
# Generate filter taps and print them out
-taps = digital.filter.firdes_low_pass(gain, sampling_freq, cutoff, transition_width) # hamming window
+taps = digital.filter.firdes.low_pass(gain, sampling_freq, cutoff, transition_width) # hamming window
print(len(taps))
for t in taps:
diff --git a/doc/receive_events.py b/doc/receive_events.py
new file mode 100755
index 0000000..bfd6f86
--- /dev/null
+++ b/doc/receive_events.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+#
+# This is an example program that shows
+# how to receive runtime events from ODR-DabMod
+#
+# LICENSE: see bottom of file
+
+import sys
+import zmq
+import json
+from pprint import pprint
+
+context = zmq.Context()
+sock = context.socket(zmq.SUB)
+
+ep = "tcp://127.0.0.1:5556"
+print(f"Receive from {ep}")
+sock.connect(ep)
+
+# subscribe to all events
+sock.setsockopt(zmq.SUBSCRIBE, bytes([]))
+
+while True:
+ parts = sock.recv_multipart()
+ if len(parts) == 2:
+ print("Received event '{}'".format(parts[0].decode()))
+ pprint(json.loads(parts[1].decode()))
+
+ else:
+ print("Received strange event:")
+ pprint(parts)
+
+ print()
+
+
+# This is free and unencumbered software released into the public domain.
+#
+# Anyone is free to copy, modify, publish, use, compile, sell, or
+# distribute this software, either in source code form or as a compiled
+# binary, for any purpose, commercial or non-commercial, and by any
+# means.
+#
+# In jurisdictions that recognize copyright laws, the author or authors
+# of this software dedicate any and all copyright interest in the
+# software to the public domain. We make this dedication for the benefit
+# of the public at large and to the detriment of our heirs and
+# successors. We intend this dedication to be an overt act of
+# relinquishment in perpetuity of all present and future rights to this
+# software under copyright law.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# For more information, please refer to <http://unlicense.org>
diff --git a/doc/zmq-ctrl/json_remote_server.py b/doc/zmq-ctrl/json_remote_server.py
new file mode 100755
index 0000000..728cf7c
--- /dev/null
+++ b/doc/zmq-ctrl/json_remote_server.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+#
+# This is an example program that illustrates
+# how to interact with the zeromq remote control
+# using JSON.
+#
+# LICENSE: see bottom of file
+
+import sys
+import zmq
+from pprint import pprint
+import json
+import re
+from http.server import BaseHTTPRequestHandler, HTTPServer
+import time
+
+re_url = re.compile(r"/([a-zA-Z0-9]+).json")
+
+ZMQ_REMOTE = "tcp://localhost:9400"
+HTTP_HOSTNAME = "localhost"
+HTTP_PORT = 8080
+
+class DabMuxServer(BaseHTTPRequestHandler):
+ def err500(self, message):
+ self.send_response(500)
+ self.send_header("Content-type", "application/json")
+ self.end_headers()
+ self.wfile.write(json.dumps({"error": message}).encode())
+
+ def do_GET(self):
+ m = re_url.match(self.path)
+ if m:
+ sock = context.socket(zmq.REQ)
+ poller = zmq.Poller()
+ poller.register(sock, zmq.POLLIN)
+ sock.connect(ZMQ_REMOTE)
+
+ sock.send(b"ping")
+ socks = dict(poller.poll(1000))
+ if socks:
+ if socks.get(sock) == zmq.POLLIN:
+ data = sock.recv()
+ if data != b"ok":
+ print(f"Received {data} to ping!", file=sys.stderr)
+ self.err500("ping failure")
+ return
+ else:
+ print("ZMQ error: ping timeout", file=sys.stderr)
+ self.err500("ping timeout")
+ return
+
+ sock.send(b"showjson", flags=zmq.SNDMORE)
+ sock.send(m.group(1).encode())
+
+ socks = dict(poller.poll(1000))
+ if socks:
+ if socks.get(sock) == zmq.POLLIN:
+ data = sock.recv_multipart()
+ print("Received: {}".format(len(data)), file=sys.stderr)
+ parts = []
+ for i, part_data in enumerate(data):
+ part = part_data.decode()
+ print(" RX {}: {}".format(i, part.replace('\n',' ')), file=sys.stderr)
+
+ if i == 0 and part != "fail":
+ self.send_response(200)
+ self.send_header("Content-type", "application/json")
+ self.end_headers()
+ self.wfile.write(part_data)
+ return
+ parts.append(part)
+ self.err500("data error " + " ".join(parts))
+ return
+
+ else:
+ print("ZMQ error: timeout", file=sys.stderr)
+ self.err500("timeout")
+ return
+ else:
+ self.send_response(200)
+ self.send_header("Content-type", "text/html")
+ self.end_headers()
+ self.wfile.write("""<html><head><title>ODR-DabMod RC HTTP server</title></head>\n""".encode())
+ self.wfile.write("""<body>\n""".encode())
+ for mod in ("sdr", "tist", "modulator", "tii", "ofdm", "gain", "guardinterval"):
+ self.wfile.write(f"""<p><a href="{mod}.json">{mod}.json</a></p>\n""".encode())
+ self.wfile.write("""</body></html>\n""".encode())
+
+
+if __name__ == "__main__":
+ context = zmq.Context()
+
+ webServer = HTTPServer((HTTP_HOSTNAME, HTTP_PORT), DabMuxServer)
+ print("Server started http://%s:%s" % (HTTP_HOSTNAME, HTTP_PORT))
+
+ try:
+ webServer.serve_forever()
+ except KeyboardInterrupt:
+ pass
+
+ webServer.server_close()
+
+ context.destroy(linger=5)
+
+
+# This is free and unencumbered software released into the public domain.
+#
+# Anyone is free to copy, modify, publish, use, compile, sell, or
+# distribute this software, either in source code form or as a compiled
+# binary, for any purpose, commercial or non-commercial, and by any
+# means.
+#
+# In jurisdictions that recognize copyright laws, the author or authors
+# of this software dedicate any and all copyright interest in the
+# software to the public domain. We make this dedication for the benefit
+# of the public at large and to the detriment of our heirs and
+# successors. We intend this dedication to be an overt act of
+# relinquishment in perpetuity of all present and future rights to this
+# software under copyright law.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# For more information, please refer to <http://unlicense.org>
+
+
diff --git a/doc/zmq-ctrl/zmq_remote.py b/doc/zmq-ctrl/zmq_remote.py
index 56465d3..7581575 100755
--- a/doc/zmq-ctrl/zmq_remote.py
+++ b/doc/zmq-ctrl/zmq_remote.py
@@ -16,7 +16,7 @@ poller = zmq.Poller()
poller.register(sock, zmq.POLLIN)
if len(sys.argv) < 2:
- print("Usage: program url cmd [args...]")
+ print("Usage: program url cmd [args...]", file=sys.stderr)
sys.exit(1)
sock.connect(sys.argv[1])
@@ -25,7 +25,7 @@ message_parts = sys.argv[2:]
# first do a ping test
-print("ping")
+print("ping", file=sys.stderr)
sock.send(b"ping")
socks = dict(poller.poll(1000))
@@ -33,9 +33,9 @@ if socks:
if socks.get(sock) == zmq.POLLIN:
data = sock.recv_multipart()
- print("Received: {}".format(len(data)))
+ print("Received: {}".format(len(data)), file=sys.stderr)
for i,part in enumerate(data):
- print(" {}".format(part))
+ print(" {}".format(part), file=sys.stderr)
for i, part in enumerate(message_parts):
if i == len(message_parts) - 1:
@@ -43,18 +43,22 @@ if socks:
else:
f = zmq.SNDMORE
- print("Send {}({}): '{}'".format(i, f, part))
+ print("Send {}({}): '{}'".format(i, f, part), file=sys.stderr)
sock.send(part.encode(), flags=f)
data = sock.recv_multipart()
- print("Received: {}".format(len(data)))
- for i,part in enumerate(data):
- print(" RX {}: {}".format(i, part.decode().replace('\n',' ')))
+ print("Received: {}".format(len(data)), file=sys.stderr)
+ for i, part in enumerate(data):
+ if message_parts[0] == 'showjson':
+ # This allows you to pipe the JSON into another tool
+ print(part.decode())
+ else:
+ print(" RX {}: {}".format(i, part.decode().replace('\n',' ')), file=sys.stderr)
else:
- print("ZMQ error: timeout")
+ print("ZMQ error: timeout", file=sys.stderr)
context.destroy(linger=5)
# This is free and unencumbered software released into the public domain.
diff --git a/fpm/LICENSE b/fpm/LICENSE
new file mode 100644
index 0000000..bb86b71
--- /dev/null
+++ b/fpm/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Mike Lankamp
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/fpm/README.md b/fpm/README.md
new file mode 100644
index 0000000..38ee444
--- /dev/null
+++ b/fpm/README.md
@@ -0,0 +1,48 @@
+# fpm
+A C++ header-only fixed-point math library. "fpm" stands for "fixed-point math".
+
+It is designed to serve as a drop-in replacement for floating-point types and aims to provide as much of the standard library's functionality as possible with exclusively integers. `fpm` requires C++11 or higher.
+
+[![Build Status](https://travis-ci.org/MikeLankamp/fpm.svg?branch=master)](https://travis-ci.org/MikeLankamp/fpm)
+[![Build status](https://ci.appveyor.com/api/projects/status/0velpwqk38spu412?svg=true)](https://ci.appveyor.com/project/MikeLankamp/fpm)
+
+`fpm` is designed to guard against accidental conversion to and from floats and supports many of the standard C++ maths functions, including trigonometry, power and logarithmic functions, with performance and accuracy generally comparable to alternative libraries.
+
+## Why use fixed-point math?
+There are several reasons why you can not or choose not to use floating-point math, but still want a similar type:
+* Your target platform lacks an FPU, does not support floating-point operations or its floating-point operations are
+ considerably slower than fixed-point integer operations.
+* You require deterministic calculations.
+
+If any of these reasons apply for you, and your problem domain has a clearly outlined range and required resolution,
+then fixed-point numbers might be a solution for you.
+
+## Quick Start
+To use `fpm`, include its header `<fpm/fixed.hpp>` and use the `fpm::fixed_16_16`, `fpm::fixed_24_8` or `fpm::fixed_8_24`
+types as if they were native floating-pointer types:
+```c++
+#include <fpm/fixed.hpp> // For fpm::fixed_16_16
+#include <fpm/math.hpp> // For fpm::cos
+#include <fpm/ios.hpp> // For fpm::operator<<
+#include <iostream> // For std::cin, std::cout
+
+int main() {
+ std::cout << "Please input a number: ";
+ fpm::fixed_16_16 x;
+ std::cin >> x;
+ std::cout << "The cosine of " << x << " radians is: " << cos(x) << std::endl;
+ return 0;
+}
+```
+
+To use the fixed-point equivalents of the `<math.h>` functions such as `sqrt`, `sin` and `log`, include the header `<fpm/math.hpp>`.
+To stream fixed-point values to or from streams, include the header `<fpm/ios.hpp>`.
+
+## Documentation
+Please refer to the [documentation](docs/index.md) for detailed information how to use `fpm`, or skip straight to the [performance](docs/performance.md) or [accuracy](docs/accuracy.md) results.
+
+## Contributions
+This library is a work-in-progress. We welcome any contributions that improve the functional coverage or the performance or accuracy of the mathematical functions.
+
+## License
+See the [LICENSE](LICENSE) file
diff --git a/fpm/fixed.hpp b/fpm/fixed.hpp
new file mode 100644
index 0000000..e2e71bf
--- /dev/null
+++ b/fpm/fixed.hpp
@@ -0,0 +1,490 @@
+#ifndef FPM_FIXED_HPP
+#define FPM_FIXED_HPP
+
+#include <cassert>
+#include <cmath>
+#include <cstdint>
+#include <functional>
+#include <limits>
+#include <type_traits>
+
+namespace fpm
+{
+
+//! Fixed-point number type
+//! \tparam BaseType the base integer type used to store the fixed-point number. This can be a signed or unsigned type.
+//! \tparam IntermediateType the integer type used to store intermediate results during calculations.
+//! \tparam FractionBits the number of bits of the BaseType used to store the fraction
+//! \tparam EnableRounding enable rounding of LSB for multiplication, division, and type conversion
+template <typename BaseType, typename IntermediateType, unsigned int FractionBits, bool EnableRounding = true>
+class fixed
+{
+ static_assert(std::is_integral<BaseType>::value, "BaseType must be an integral type");
+ static_assert(FractionBits > 0, "FractionBits must be greater than zero");
+ static_assert(FractionBits <= sizeof(BaseType) * 8 - 1, "BaseType must at least be able to contain entire fraction, with space for at least one integral bit");
+ static_assert(sizeof(IntermediateType) > sizeof(BaseType), "IntermediateType must be larger than BaseType");
+ static_assert(std::is_signed<IntermediateType>::value == std::is_signed<BaseType>::value, "IntermediateType must have same signedness as BaseType");
+
+ // Although this value fits in the BaseType in terms of bits, if there's only one integral bit, this value
+ // is incorrect (flips from positive to negative), so we must extend the size to IntermediateType.
+ static constexpr IntermediateType FRACTION_MULT = IntermediateType(1) << FractionBits;
+
+ struct raw_construct_tag {};
+ constexpr inline fixed(BaseType val, raw_construct_tag) noexcept : m_value(val) {}
+
+public:
+ inline fixed() noexcept = default;
+
+ // Converts an integral number to the fixed-point type.
+ // Like static_cast, this truncates bits that don't fit.
+ template <typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+ constexpr inline explicit fixed(T val) noexcept
+ : m_value(static_cast<BaseType>(val * FRACTION_MULT))
+ {}
+
+ // Converts an floating-point number to the fixed-point type.
+ // Like static_cast, this truncates bits that don't fit.
+ template <typename T, typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr>
+ constexpr inline explicit fixed(T val) noexcept
+ : m_value(static_cast<BaseType>((EnableRounding) ?
+ (val >= 0.0) ? (val * FRACTION_MULT + T{0.5}) : (val * FRACTION_MULT - T{0.5})
+ : (val * FRACTION_MULT)))
+ {}
+
+ // Constructs from another fixed-point type with possibly different underlying representation.
+ // Like static_cast, this truncates bits that don't fit.
+ template <typename B, typename I, unsigned int F, bool R>
+ constexpr inline explicit fixed(fixed<B,I,F,R> val) noexcept
+ : m_value(from_fixed_point<F>(val.raw_value()).raw_value())
+ {}
+
+ // Explicit conversion to a floating-point type
+ template <typename T, typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr>
+ constexpr inline explicit operator T() const noexcept
+ {
+ return static_cast<T>(m_value) / FRACTION_MULT;
+ }
+
+ // Explicit conversion to an integral type
+ template <typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+ constexpr inline explicit operator T() const noexcept
+ {
+ return static_cast<T>(m_value / FRACTION_MULT);
+ }
+
+ // Returns the raw underlying value of this type.
+ // Do not use this unless you know what you're doing.
+ constexpr inline BaseType raw_value() const noexcept
+ {
+ return m_value;
+ }
+
+ //! Constructs a fixed-point number from another fixed-point number.
+ //! \tparam NumFractionBits the number of bits used by the fraction in \a value.
+ //! \param value the integer fixed-point number
+ template <unsigned int NumFractionBits, typename T, typename std::enable_if<(NumFractionBits > FractionBits)>::type* = nullptr>
+ static constexpr inline fixed from_fixed_point(T value) noexcept
+ {
+ // To correctly round the last bit in the result, we need one more bit of information.
+ // We do this by multiplying by two before dividing and adding the LSB to the real result.
+ return (EnableRounding) ? fixed(static_cast<BaseType>(
+ value / (T(1) << (NumFractionBits - FractionBits)) +
+ (value / (T(1) << (NumFractionBits - FractionBits - 1)) % 2)),
+ raw_construct_tag{}) :
+ fixed(static_cast<BaseType>(value / (T(1) << (NumFractionBits - FractionBits))),
+ raw_construct_tag{});
+ }
+
+ template <unsigned int NumFractionBits, typename T, typename std::enable_if<(NumFractionBits <= FractionBits)>::type* = nullptr>
+ static constexpr inline fixed from_fixed_point(T value) noexcept
+ {
+ return fixed(static_cast<BaseType>(
+ value * (T(1) << (FractionBits - NumFractionBits))),
+ raw_construct_tag{});
+ }
+
+ // Constructs a fixed-point number from its raw underlying value.
+ // Do not use this unless you know what you're doing.
+ static constexpr inline fixed from_raw_value(BaseType value) noexcept
+ {
+ return fixed(value, raw_construct_tag{});
+ }
+
+ //
+ // Constants
+ //
+ static constexpr fixed e() { return from_fixed_point<61>(6267931151224907085ll); }
+ static constexpr fixed pi() { return from_fixed_point<61>(7244019458077122842ll); }
+ static constexpr fixed half_pi() { return from_fixed_point<62>(7244019458077122842ll); }
+ static constexpr fixed two_pi() { return from_fixed_point<60>(7244019458077122842ll); }
+
+ //
+ // Arithmetic member operators
+ //
+
+ constexpr inline fixed operator-() const noexcept
+ {
+ return fixed::from_raw_value(-m_value);
+ }
+
+ inline fixed& operator+=(const fixed& y) noexcept
+ {
+ m_value += y.m_value;
+ return *this;
+ }
+
+ template <typename I, typename std::enable_if<std::is_integral<I>::value>::type* = nullptr>
+ inline fixed& operator+=(I y) noexcept
+ {
+ m_value += y * FRACTION_MULT;
+ return *this;
+ }
+
+ inline fixed& operator-=(const fixed& y) noexcept
+ {
+ m_value -= y.m_value;
+ return *this;
+ }
+
+ template <typename I, typename std::enable_if<std::is_integral<I>::value>::type* = nullptr>
+ inline fixed& operator-=(I y) noexcept
+ {
+ m_value -= y * FRACTION_MULT;
+ return *this;
+ }
+
+ inline fixed& operator*=(const fixed& y) noexcept
+ {
+ if (EnableRounding){
+ // Normal fixed-point multiplication is: x * y / 2**FractionBits.
+ // To correctly round the last bit in the result, we need one more bit of information.
+ // We do this by multiplying by two before dividing and adding the LSB to the real result.
+ auto value = (static_cast<IntermediateType>(m_value) * y.m_value) / (FRACTION_MULT / 2);
+ m_value = static_cast<BaseType>((value / 2) + (value % 2));
+ } else {
+ auto value = (static_cast<IntermediateType>(m_value) * y.m_value) / FRACTION_MULT;
+ m_value = static_cast<BaseType>(value);
+ }
+ return *this;
+ }
+
+ template <typename I, typename std::enable_if<std::is_integral<I>::value>::type* = nullptr>
+ inline fixed& operator*=(I y) noexcept
+ {
+ m_value *= y;
+ return *this;
+ }
+
+ inline fixed& operator/=(const fixed& y) noexcept
+ {
+ assert(y.m_value != 0);
+ if (EnableRounding){
+ // Normal fixed-point division is: x * 2**FractionBits / y.
+ // To correctly round the last bit in the result, we need one more bit of information.
+ // We do this by multiplying by two before dividing and adding the LSB to the real result.
+ auto value = (static_cast<IntermediateType>(m_value) * FRACTION_MULT * 2) / y.m_value;
+ m_value = static_cast<BaseType>((value / 2) + (value % 2));
+ } else {
+ auto value = (static_cast<IntermediateType>(m_value) * FRACTION_MULT) / y.m_value;
+ m_value = static_cast<BaseType>(value);
+ }
+ return *this;
+ }
+
+ template <typename I, typename std::enable_if<std::is_integral<I>::value>::type* = nullptr>
+ inline fixed& operator/=(I y) noexcept
+ {
+ m_value /= y;
+ return *this;
+ }
+
+private:
+ BaseType m_value;
+};
+
+//
+// Convenience typedefs
+//
+
+using fixed_16_16 = fixed<std::int32_t, std::int64_t, 16>;
+using fixed_24_8 = fixed<std::int32_t, std::int64_t, 8>;
+using fixed_8_24 = fixed<std::int32_t, std::int64_t, 24>;
+
+//
+// Addition
+//
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline fixed<B, I, F, R> operator+(const fixed<B, I, F, R>& x, const fixed<B, I, F, R>& y) noexcept
+{
+ return fixed<B, I, F, R>(x) += y;
+}
+
+template <typename B, typename I, unsigned int F, bool R, typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+constexpr inline fixed<B, I, F, R> operator+(const fixed<B, I, F, R>& x, T y) noexcept
+{
+ return fixed<B, I, F, R>(x) += y;
+}
+
+template <typename B, typename I, unsigned int F, bool R, typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+constexpr inline fixed<B, I, F, R> operator+(T x, const fixed<B, I, F, R>& y) noexcept
+{
+ return fixed<B, I, F, R>(y) += x;
+}
+
+//
+// Subtraction
+//
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline fixed<B, I, F, R> operator-(const fixed<B, I, F, R>& x, const fixed<B, I, F, R>& y) noexcept
+{
+ return fixed<B, I, F, R>(x) -= y;
+}
+
+template <typename B, typename I, unsigned int F, bool R, typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+constexpr inline fixed<B, I, F, R> operator-(const fixed<B, I, F, R>& x, T y) noexcept
+{
+ return fixed<B, I, F, R>(x) -= y;
+}
+
+template <typename B, typename I, unsigned int F, bool R, typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+constexpr inline fixed<B, I, F, R> operator-(T x, const fixed<B, I, F, R>& y) noexcept
+{
+ return fixed<B, I, F, R>(x) -= y;
+}
+
+//
+// Multiplication
+//
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline fixed<B, I, F, R> operator*(const fixed<B, I, F, R>& x, const fixed<B, I, F, R>& y) noexcept
+{
+ return fixed<B, I, F, R>(x) *= y;
+}
+
+template <typename B, typename I, unsigned int F, bool R, typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+constexpr inline fixed<B, I, F, R> operator*(const fixed<B, I, F, R>& x, T y) noexcept
+{
+ return fixed<B, I, F, R>(x) *= y;
+}
+
+template <typename B, typename I, unsigned int F, bool R, typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+constexpr inline fixed<B, I, F, R> operator*(T x, const fixed<B, I, F, R>& y) noexcept
+{
+ return fixed<B, I, F, R>(y) *= x;
+}
+
+//
+// Division
+//
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline fixed<B, I, F, R> operator/(const fixed<B, I, F, R>& x, const fixed<B, I, F, R>& y) noexcept
+{
+ return fixed<B, I, F, R>(x) /= y;
+}
+
+template <typename B, typename I, unsigned int F, typename T, bool R, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+constexpr inline fixed<B, I, F, R> operator/(const fixed<B, I, F, R>& x, T y) noexcept
+{
+ return fixed<B, I, F, R>(x) /= y;
+}
+
+template <typename B, typename I, unsigned int F, typename T, bool R, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+constexpr inline fixed<B, I, F, R> operator/(T x, const fixed<B, I, F, R>& y) noexcept
+{
+ return fixed<B, I, F, R>(x) /= y;
+}
+
+//
+// Comparison operators
+//
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool operator==(const fixed<B, I, F, R>& x, const fixed<B, I, F, R>& y) noexcept
+{
+ return x.raw_value() == y.raw_value();
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool operator!=(const fixed<B, I, F, R>& x, const fixed<B, I, F, R>& y) noexcept
+{
+ return x.raw_value() != y.raw_value();
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool operator<(const fixed<B, I, F, R>& x, const fixed<B, I, F, R>& y) noexcept
+{
+ return x.raw_value() < y.raw_value();
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool operator>(const fixed<B, I, F, R>& x, const fixed<B, I, F, R>& y) noexcept
+{
+ return x.raw_value() > y.raw_value();
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool operator<=(const fixed<B, I, F, R>& x, const fixed<B, I, F, R>& y) noexcept
+{
+ return x.raw_value() <= y.raw_value();
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool operator>=(const fixed<B, I, F, R>& x, const fixed<B, I, F, R>& y) noexcept
+{
+ return x.raw_value() >= y.raw_value();
+}
+
+namespace detail
+{
+// Number of base-10 digits required to fully represent a number of bits
+static constexpr int max_digits10(int bits)
+{
+ // 8.24 fixed-point equivalent of (int)ceil(bits * std::log10(2));
+ using T = long long;
+ return static_cast<int>((T{bits} * 5050445 + (T{1} << 24) - 1) >> 24);
+}
+
+// Number of base-10 digits that can be fully represented by a number of bits
+static constexpr int digits10(int bits)
+{
+ // 8.24 fixed-point equivalent of (int)(bits * std::log10(2));
+ using T = long long;
+ return static_cast<int>((T{bits} * 5050445) >> 24);
+}
+
+} // namespace detail
+} // namespace fpm
+
+// Specializations for customization points
+namespace std
+{
+
+template <typename B, typename I, unsigned int F, bool R>
+struct hash<fpm::fixed<B,I,F,R>>
+{
+ using argument_type = fpm::fixed<B, I, F, R>;
+ using result_type = std::size_t;
+
+ result_type operator()(argument_type arg) const noexcept(noexcept(std::declval<std::hash<B>>()(arg.raw_value()))) {
+ return m_hash(arg.raw_value());
+ }
+
+private:
+ std::hash<B> m_hash;
+};
+
+template <typename B, typename I, unsigned int F, bool R>
+struct numeric_limits<fpm::fixed<B,I,F,R>>
+{
+ static constexpr bool is_specialized = true;
+ static constexpr bool is_signed = std::numeric_limits<B>::is_signed;
+ static constexpr bool is_integer = false;
+ static constexpr bool is_exact = true;
+ static constexpr bool has_infinity = false;
+ static constexpr bool has_quiet_NaN = false;
+ static constexpr bool has_signaling_NaN = false;
+ static constexpr std::float_denorm_style has_denorm = std::denorm_absent;
+ static constexpr bool has_denorm_loss = false;
+ static constexpr std::float_round_style round_style = std::round_to_nearest;
+ static constexpr bool is_iec_559 = false;
+ static constexpr bool is_bounded = true;
+ static constexpr bool is_modulo = std::numeric_limits<B>::is_modulo;
+ static constexpr int digits = std::numeric_limits<B>::digits;
+
+ // Any number with `digits10` significant base-10 digits (that fits in
+ // the range of the type) is guaranteed to be convertible from text and
+ // back without change. Worst case, this is 0.000...001, so we can only
+ // guarantee this case. Nothing more.
+ static constexpr int digits10 = 1;
+
+ // This is equal to max_digits10 for the integer and fractional part together.
+ static constexpr int max_digits10 =
+ fpm::detail::max_digits10(std::numeric_limits<B>::digits - F) + fpm::detail::max_digits10(F);
+
+ static constexpr int radix = 2;
+ static constexpr int min_exponent = 1 - F;
+ static constexpr int min_exponent10 = -fpm::detail::digits10(F);
+ static constexpr int max_exponent = std::numeric_limits<B>::digits - F;
+ static constexpr int max_exponent10 = fpm::detail::digits10(std::numeric_limits<B>::digits - F);
+ static constexpr bool traps = true;
+ static constexpr bool tinyness_before = false;
+
+ static constexpr fpm::fixed<B,I,F,R> lowest() noexcept {
+ return fpm::fixed<B,I,F,R>::from_raw_value(std::numeric_limits<B>::lowest());
+ };
+
+ static constexpr fpm::fixed<B,I,F,R> min() noexcept {
+ return lowest();
+ }
+
+ static constexpr fpm::fixed<B,I,F,R> max() noexcept {
+ return fpm::fixed<B,I,F,R>::from_raw_value(std::numeric_limits<B>::max());
+ };
+
+ static constexpr fpm::fixed<B,I,F,R> epsilon() noexcept {
+ return fpm::fixed<B,I,F,R>::from_raw_value(1);
+ };
+
+ static constexpr fpm::fixed<B,I,F,R> round_error() noexcept {
+ return fpm::fixed<B,I,F,R>(1) / 2;
+ };
+
+ static constexpr fpm::fixed<B,I,F,R> denorm_min() noexcept {
+ return min();
+ }
+};
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::is_specialized;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::is_signed;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::is_integer;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::is_exact;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::has_infinity;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::has_quiet_NaN;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::has_signaling_NaN;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr std::float_denorm_style numeric_limits<fpm::fixed<B,I,F,R>>::has_denorm;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::has_denorm_loss;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr std::float_round_style numeric_limits<fpm::fixed<B,I,F,R>>::round_style;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::is_iec_559;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::is_bounded;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::is_modulo;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr int numeric_limits<fpm::fixed<B,I,F,R>>::digits;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr int numeric_limits<fpm::fixed<B,I,F,R>>::digits10;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr int numeric_limits<fpm::fixed<B,I,F,R>>::max_digits10;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr int numeric_limits<fpm::fixed<B,I,F,R>>::radix;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr int numeric_limits<fpm::fixed<B,I,F,R>>::min_exponent;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr int numeric_limits<fpm::fixed<B,I,F,R>>::min_exponent10;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr int numeric_limits<fpm::fixed<B,I,F,R>>::max_exponent;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr int numeric_limits<fpm::fixed<B,I,F,R>>::max_exponent10;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::traps;
+template <typename B, typename I, unsigned int F, bool R>
+constexpr bool numeric_limits<fpm::fixed<B,I,F,R>>::tinyness_before;
+
+}
+
+#endif
diff --git a/fpm/ios.hpp b/fpm/ios.hpp
new file mode 100644
index 0000000..69581fb
--- /dev/null
+++ b/fpm/ios.hpp
@@ -0,0 +1,740 @@
+#ifndef FPM_IOS_HPP
+#define FPM_IOS_HPP
+
+#include "fixed.hpp"
+#include "math.hpp"
+#include <array>
+#include <algorithm>
+#include <cctype>
+#include <climits>
+#include <limits>
+#include <ios>
+#include <vector>
+
+namespace fpm
+{
+
+template <typename CharT, typename B, typename I, unsigned int F, bool R>
+std::basic_ostream<CharT>& operator<<(std::basic_ostream<CharT>& os, fixed<B, I, F, R> x) noexcept
+{
+ const auto uppercase = ((os.flags() & std::ios_base::uppercase) != 0);
+ const auto showpoint = ((os.flags() & std::ios_base::showpoint) != 0);
+ const auto adjustfield = (os.flags() & std::ios_base::adjustfield);
+ const auto width = os.width();
+ const auto& ctype = std::use_facet<std::ctype<CharT>>(os.getloc());
+ const auto& numpunct = std::use_facet<std::numpunct<CharT>>(os.getloc());
+
+ auto floatfield = (os.flags() & std::ios_base::floatfield);
+ auto precision = os.precision();
+ auto show_trailing_zeros = true;
+ auto use_significant_digits = false;
+
+ // Invalid precision? Reset to the default
+ if (precision < 0)
+ {
+ precision = 6;
+ }
+
+ // Output buffer. Needs to be big enough for the formatted number without padding.
+ // Optional prefixes (i.e. "+"/"-", decimal separator, exponent "e+/-" and/or "0x").
+ constexpr auto worst_case_constant_size = 6;
+ // Maximum number of digits from the base type (covers integral + fractional digits)
+ constexpr auto worst_case_digit_count = std::numeric_limits<B>::digits10 + 2;
+ // Exponent suffixes (i.e. maximum digits based on log of the base type size).
+ // Needs a log10, but that isn't constexpr, so we're over-allocating on the stack. Can't hurt.
+ constexpr auto worst_case_suffix_size = std::numeric_limits<B>::digits;
+ // Double the digit count: in the worst case the thousands grouping add a character per digit.
+ using buffer_t = std::array<CharT, worst_case_constant_size + worst_case_digit_count * 2 + worst_case_suffix_size>;
+ buffer_t buffer;
+
+ // Output cursor
+ auto end = buffer.begin();
+
+ // Keep track of the start of "internal" padding
+ typename buffer_t::iterator internal_pad = buffer.end();
+
+ // Representation of a number.
+ // The value of the number is: raw / divisor * (10|2) ^ exponent
+ // The base of the exponent is 2 in hexfloat mode, or 10 otherwise.
+ struct number_t {
+ I raw; // raw fixed-point value
+ I divisor; // the divisor indicating the place of the decimal point
+ int exponent; // the exponent applied
+ };
+
+ // Convert a value without exponent to scientific representation
+ // where the part before the decimal point is less than 10.
+ const auto as_scientific = [](number_t value) {
+ assert(value.exponent == 0);
+ if (value.raw > 0)
+ {
+ while (value.raw / 10 >= value.divisor) {
+ value.divisor *= 10;
+ ++value.exponent;
+ }
+ while (value.raw < value.divisor) {
+ value.raw *= 10;
+ --value.exponent;
+ }
+ }
+ return value;
+ };
+
+ number_t value = { x.raw_value(), I{1} << F, 0};
+
+ auto base = B{10};
+
+ // First write the sign
+ if (value.raw < 0)
+ {
+ *end++ = ctype.widen('-');
+ value.raw = -value.raw;
+ internal_pad = end;
+ }
+ else if (os.flags() & std::ios_base::showpos)
+ {
+ *end++ = ctype.widen('+');
+ internal_pad = end;
+ }
+ assert(value.raw >= 0);
+
+ switch (floatfield)
+ {
+ case std::ios_base::fixed | std::ios_base::scientific:
+ // Hexadecimal mode: figure out the hexadecimal exponent and write "0x"
+ if (value.raw > 0)
+ {
+ auto bit = detail::find_highest_bit(value.raw);
+ value.exponent = bit - F; // exponent is applied to base 2
+ value.divisor = I{1} << bit; // divisor is at the highest bit, ensuring it starts with "1."
+ precision = (bit + 3) / 4; // precision is number of nibbles, so we show all of them
+ }
+ base = 16;
+ show_trailing_zeros = false; // Always strip trailing zeros in hexfloat mode
+
+ *end++ = ctype.widen('0');
+ *end++ = ctype.widen(uppercase ? 'X' : 'x');
+ break;
+
+ case std::ios_base::scientific:
+ // Scientific mode, normalize value to scientific notation
+ value = as_scientific(value);
+ break;
+
+ case std::ios_base::fixed:
+ // Fixed mode. Nothing to do.
+ break;
+
+ default:
+ {
+ // "auto" mode: figure out the exponent
+ const number_t sci_value = as_scientific(value);
+
+ // Now `precision` indicates the number of *significant digits* (not fractional digits).
+ use_significant_digits = true;
+ precision = std::max<std::streamsize>(precision, 1);
+
+ if (sci_value.exponent >= precision || sci_value.exponent < -4) {
+ // Display as scientific format
+ floatfield = std::ios_base::scientific;
+ value = sci_value;
+ } else {
+ // Display as fixed format.
+ // "showpoint" indicates whether or not we show trailing zeros
+ floatfield = std::ios_base::fixed;
+ show_trailing_zeros = showpoint;
+ }
+ break;
+ }
+ };
+
+ // If we didn't write a sign, any internal padding starts here
+ // (after a potential "0x" for hexfloats).
+ if (internal_pad == buffer.end()) {
+ internal_pad = end;
+ }
+
+ // Separate out the integral part of the number
+ I integral = value.raw / value.divisor;
+ value.raw %= value.divisor;
+
+ // Here we start printing the number itself
+ const char* const digits = uppercase ? "0123456789ABCDEF" : "0123456789abcdef";
+ const auto digits_start = end;
+
+ // Are we already printing significant digits? (yes if we're not counting significant digits)
+ bool significant_digits = !use_significant_digits;
+
+ // Print the integral part
+ int last_digit = 0;
+ if (integral == 0) {
+ *end++ = ctype.widen('0');
+ if (value.raw == 0) {
+ // If the fraction is zero too, all zeros including the integral count
+ // as significant digits.
+ significant_digits = true;
+ }
+ } else {
+ while (integral > 0) {
+ last_digit = integral % base;
+ *end++ = ctype.widen(digits[last_digit]);
+ integral /= base;
+ }
+ std::reverse(digits_start, end);
+ significant_digits = true;
+ }
+
+ if (use_significant_digits && significant_digits)
+ {
+ // Apparently the integral part was significant; subtract its
+ // length from the remaining significant digits.
+ precision -= (end - digits_start);
+ }
+
+ // At this point, `value` contains only the fraction and
+ // `precision` holds the number of digits to print.
+ assert(value.raw < value.divisor);
+ assert(precision >= 0);
+
+ // Location of decimal point
+ typename buffer_t::iterator point = buffer.end();
+
+ // Start (and length) of the trailing zeros to insert while printing
+ // By tracking this to print them later instead of actually printing them now,
+ // we can support large precisions with a small printing buffer.
+ typename buffer_t::iterator trailing_zeros_start = buffer.end();
+ std::streamsize trailing_zeros_count = 0;
+
+ if (precision > 0)
+ {
+ // Print the fractional part
+ *(point = end++) = numpunct.decimal_point();
+
+ for (int i = 0; i < precision; ++i)
+ {
+ if (value.raw == 0)
+ {
+ // The rest of the digits are all zeros, mark them
+ // to be printed in this spot.
+ trailing_zeros_start = end;
+ trailing_zeros_count = precision - i;
+ break;
+ }
+
+ // Shift the divisor if we can to avoid overflow on the value
+ if (value.divisor % base == 0) {
+ value.divisor /= base;
+ } else {
+ value.raw *= base;
+ }
+ assert(value.divisor > 0);
+ assert(value.raw >= 0);
+ last_digit = (value.raw / value.divisor) % base;
+ value.raw %= value.divisor;
+ *end++ = ctype.widen(digits[last_digit]);
+
+ if (!significant_digits) {
+ // We're still finding the first significant digit
+ if (last_digit != 0) {
+ // Found it
+ significant_digits = true;
+ } else {
+ // Not yet; increment number of digits to print
+ ++precision;
+ }
+ }
+ }
+ }
+ else if (showpoint)
+ {
+ // No fractional part to print, but we still want the point
+ *(point = end++) = numpunct.decimal_point();
+ }
+
+ // Insert `ch` into the output at `position`, updating all references accordingly
+ const auto insert_character = [&](typename buffer_t::iterator position, CharT ch) {
+ assert(position >= buffer.begin() && position < end);
+ std::move_backward(position, end, end + 1);
+ if (point != buffer.end() && position < point) {
+ ++point;
+ }
+ if (trailing_zeros_start != buffer.end() && position < trailing_zeros_start) {
+ ++trailing_zeros_start;
+ }
+ ++end;
+ *position = ch;
+ };
+
+ // Round the number: round to nearest
+ bool increment = false;
+ if (value.raw > value.divisor / 2) {
+ // Round up
+ increment = true;
+ } else if (value.raw == value.divisor / 2) {
+ // It's a tie (i.e. "xyzw.5"): round to even
+ increment = ((last_digit % 2) == 1);
+ }
+
+ if (increment)
+ {
+ auto p = end - 1;
+ // Increment all digits backwards while we see "9"
+ while (p >= digits_start) {
+ if (p == point) {
+ // Skip over the decimal point
+ --p;
+ }
+ if ((*p)++ != ctype.widen('9')) {
+ break;
+ }
+ *p-- = ctype.widen('0');
+ }
+
+ if (p < digits_start) {
+ // We've incremented all the way to the start (all 9's), we need to insert the
+ // carried-over 1 from incrementing the last 9.
+ assert(p == digits_start - 1);
+ insert_character(++p, ctype.widen('1'));
+
+ if (floatfield == std::ios::scientific)
+ {
+ // We just made the integral part equal to 10, so we shift the decimal point
+ // back one place (if any) and tweak the exponent, so that we keep the integer part
+ // less than 10.
+ if (point != buffer.end()) {
+ assert(p + 2 == point);
+ std::swap(*(point - 1), *point);
+ --point;
+ }
+ ++value.exponent;
+
+ // We've introduced an extra digit so we need to strip the last digit
+ // to maintain the same precision
+ --end;
+ }
+ }
+
+ if (use_significant_digits && *p == ctype.widen('1') && point != buffer.end()) {
+ // We've converted a leading zero to a 1 so we need to strip the last digit
+ // (behind the decimal point) to maintain the same significant digit count.
+ --end;
+ }
+ }
+
+ if (point != buffer.end())
+ {
+ if (!show_trailing_zeros)
+ {
+ // Remove trailing zeros
+ while (*(end - 1) == ctype.widen('0')) {
+ --end;
+ }
+
+ // Also clear the "trailing zeros to append during printing" range
+ trailing_zeros_start = buffer.end();
+ trailing_zeros_count = 0;
+ }
+
+ if (end - 1 == point && trailing_zeros_count == 0 && !showpoint) {
+ // Remove the decimal point, too
+ --end;
+ }
+ }
+
+ // Apply thousands grouping
+ const auto& grouping = numpunct.grouping();
+ if (!grouping.empty())
+ {
+ // Step backwards from the end or decimal point, inserting the
+ // thousands separator at every group interval.
+ const CharT thousands_sep = ctype.widen(numpunct.thousands_sep());
+ std::size_t group = 0;
+ auto p = point != buffer.end() ? point : end;
+ auto size = static_cast<int>(grouping[group]);
+ while (size > 0 && size < CHAR_MAX && p - digits_start > size) {
+ p -= size;
+ insert_character(p, thousands_sep);
+ if (group < grouping.size() - 1) {
+ size = static_cast<int>(grouping[++group]);
+ }
+ }
+ }
+
+ // Print the exponent if required
+ assert(floatfield != 0);
+ if (floatfield & std::ios_base::scientific)
+ {
+ // Hexadecimal (%a/%A) or decimal (%e/%E) scientific notation
+ if (floatfield & std::ios_base::fixed) {
+ *end++ = ctype.widen(uppercase ? 'P' : 'p');
+ } else {
+ *end++ = ctype.widen(uppercase ? 'E' : 'e');
+ }
+
+ if (value.exponent < 0) {
+ *end++ = ctype.widen('-');
+ value.exponent = -value.exponent;
+ } else {
+ *end++ = ctype.widen('+');
+ }
+
+ if (floatfield == std::ios_base::scientific) {
+ // In decimal scientific notation (%e/%E), the exponent is at least two digits
+ if (value.exponent < 10) {
+ *end++ = ctype.widen('0');
+ }
+ }
+
+ const auto exponent_start = end;
+ if (value.exponent == 0) {
+ *end++ = ctype.widen('0');
+ } else while (value.exponent > 0) {
+ *end++ = ctype.widen(digits[value.exponent % 10]);
+ value.exponent /= 10;
+ }
+ std::reverse(exponent_start, end);
+ }
+
+ // Write character `ch` `count` times to the stream
+ const auto sputcn = [&](CharT ch, std::streamsize count){
+ // Fill a buffer to output larger chunks
+ constexpr std::streamsize chunk_size = 64;
+ std::array<CharT, chunk_size> fill_buffer;
+ std::fill_n(fill_buffer.begin(), std::min(count, chunk_size), ch);
+
+ for (std::streamsize size, left = count; left > 0; left -= size) {
+ size = std::min(chunk_size, left);
+ os.rdbuf()->sputn(&fill_buffer[0], size);
+ }
+ };
+
+ // Outputs a range of characters, making sure to output the trailing zeros range
+ // if it lies in the specified range
+ const auto put_range = [&](typename buffer_t::const_iterator begin, typename buffer_t::const_iterator end) {
+ assert(end >= begin);
+ if (trailing_zeros_start >= begin && trailing_zeros_start <= end) {
+ // Print range with trailing zeros range in the middle
+ assert(trailing_zeros_count > 0);
+ os.rdbuf()->sputn(&*begin, trailing_zeros_start - begin);
+ sputcn(ctype.widen('0'), trailing_zeros_count);
+ os.rdbuf()->sputn(&*trailing_zeros_start, end - trailing_zeros_start);
+ } else {
+ // Print range as-is
+ os.rdbuf()->sputn(&*begin, end - begin);
+ }
+ };
+
+ // Pad the buffer if necessary.
+ // Note that the length of trailing zeros is counted towards the length of the content.
+ const auto content_size = end - buffer.begin() + trailing_zeros_count;
+ if (content_size >= width)
+ {
+ // Buffer needs no padding, output as-is
+ put_range(buffer.begin(), end);
+ }
+ else
+ {
+ const auto pad_size = width - content_size;
+ switch (adjustfield)
+ {
+ case std::ios_base::left:
+ // Content is left-aligned, so output the buffer, followed by the padding
+ put_range(buffer.begin(), end);
+ sputcn(os.fill(), pad_size);
+ break;
+ case std::ios_base::internal:
+ // Content is internally aligned, so output the buffer up to the "internal pad"
+ // point, followed by the padding, followed by the remainder of the buffer.
+ put_range(buffer.begin(), internal_pad);
+ sputcn(os.fill(), pad_size);
+ put_range(internal_pad, end);
+ break;
+ default:
+ // Content is right-aligned, so output the padding, followed by the buffer
+ sputcn(os.fill(), pad_size);
+ put_range(buffer.begin(), end);
+ break;
+ }
+ }
+
+ // Width is reset after every write
+ os.width(0);
+
+ return os;
+}
+
+
+template <typename CharT, class Traits, typename B, typename I, unsigned int F, bool R>
+std::basic_istream<CharT, Traits>& operator>>(std::basic_istream<CharT, Traits>& is, fixed<B, I, F, R>& x)
+{
+ typename std::basic_istream<CharT, Traits>::sentry sentry(is);
+ if (!sentry)
+ {
+ return is;
+ }
+
+ const auto& ctype = std::use_facet<std::ctype<CharT>>(is.getloc());
+ const auto& numpunct = std::use_facet<std::numpunct<CharT>>(is.getloc());
+
+ bool thousands_separator_allowed = false;
+ const bool supports_thousands_separators = !numpunct.grouping().empty();
+
+ const auto& is_valid_character = [](char ch) {
+ // Note: allowing ['p', 'i', 'n', 't', 'y'] is technically in violation of the spec (we are emulating std::num_get),
+ // but otherwise we cannot parse hexfloats and "infinity". This is a known issue with the spec (LWG #2381).
+ return std::isxdigit(ch) ||
+ ch == 'x' || ch == 'X' || ch == 'p' || ch == 'P' ||
+ ch == 'i' || ch == 'I' || ch == 'n' || ch == 'N' ||
+ ch == 't' || ch == 'T' || ch == 'y' || ch == 'Y' ||
+ ch == '-' || ch == '+';
+ };
+
+ const auto& peek = [&]() {
+ for(;;) {
+ auto ch = is.rdbuf()->sgetc();
+ if (ch == Traits::eof()) {
+ is.setstate(std::ios::eofbit);
+ return '\0';
+ }
+ if (ch == numpunct.decimal_point()) {
+ return '.';
+ }
+ if (ch == numpunct.thousands_sep())
+ {
+ if (!supports_thousands_separators || !thousands_separator_allowed) {
+ return '\0';
+ }
+ // Ignore valid thousands separators
+ is.rdbuf()->sbumpc();
+ continue;
+ }
+ auto res = ctype.narrow(ch, 0);
+ if (!is_valid_character(res)) {
+ // Invalid character: end input
+ return '\0';
+ }
+ return res;
+ }
+ };
+
+ const auto& bump = [&]() {
+ is.rdbuf()->sbumpc();
+ };
+
+ const auto& next = [&]() {
+ bump();
+ return peek();
+ };
+
+ bool negate = false;
+ auto ch = peek();
+ if (ch == '-') {
+ negate = true;
+ ch = next();
+ } else if (ch == '+') {
+ ch = next();
+ }
+
+ const char infinity[] = "infinity";
+ // Must be "inf" or "infinity"
+ int i = 0;
+ while (i < 8 && ch == infinity[i]) {
+ ++i;
+ ch = next();
+ }
+
+ if (i > 0) {
+ if (i == 3 || i == 8) {
+ x = negate ? std::numeric_limits<fixed<B, I, F, R>>::min() : std::numeric_limits<fixed<B, I, F, R>>::max();
+ } else {
+ is.setstate(std::ios::failbit);
+ }
+ return is;
+ }
+
+ char exponent_char = 'e';
+ int base = 10;
+
+ constexpr auto NoFraction = std::numeric_limits<std::size_t>::max();
+ std::size_t fraction_start = NoFraction;
+ std::vector<unsigned char> significand;
+
+ if (ch == '0') {
+ ch = next();
+ if (ch == 'x' || ch == 'X') {
+ // Hexfloat
+ exponent_char = 'p';
+ base = 16;
+ ch = next();
+ } else {
+ significand.push_back(0);
+ }
+ }
+
+ // Parse the significand
+ thousands_separator_allowed = true;
+ for (;; ch = next()) {
+ if (ch == '.') {
+ if (fraction_start != NoFraction) {
+ // Double decimal point. Stop parsing.
+ break;
+ }
+ fraction_start = significand.size();
+ thousands_separator_allowed = false;
+ } else {
+ unsigned char val = base;
+ if (ch >= '0' && ch <= '9') {
+ val = ch - '0';
+ } else if (ch >= 'a' && ch <= 'f') {
+ val = ch - 'a' + 10;
+ } else if (ch >= 'A' && ch <= 'F') {
+ val = ch - 'A' + 10;
+ }
+ if (val < 0 || val >= base) {
+ break;
+ }
+ significand.push_back(val);
+ }
+ }
+ if (significand.empty()) {
+ // We need a significand
+ is.setstate(std::ios::failbit);
+ return is;
+ }
+ thousands_separator_allowed = false;
+
+ if (fraction_start == NoFraction) {
+ // If we haven't seen a fraction yet, place it at the end of the significand
+ fraction_start = significand.size();
+ }
+
+ // Parse the exponent
+ bool exponent_overflow = false;
+ std::size_t exponent = 0;
+ bool exponent_negate = false;
+ if (std::tolower(ch) == exponent_char)
+ {
+ ch = next();
+ if (ch == '-') {
+ exponent_negate = true;
+ ch = next();
+ } else if (ch == '+') {
+ ch = next();
+ }
+
+ bool parsed = false;
+ while (std::isdigit(ch)) {
+ if (exponent <= std::numeric_limits<int>::max() / 10) {
+ exponent = exponent * 10 + (ch - '0');
+ } else {
+ exponent_overflow = true;
+ }
+ parsed = true;
+ ch = next();
+ }
+ if (!parsed) {
+ // If the exponent character is given, the exponent value may not be empty
+ is.setstate(std::ios::failbit);
+ return is;
+ }
+ }
+
+ // We've parsed all we need. Construct the value.
+ if (exponent_overflow) {
+ // Absolute exponent is too large
+ if (std::all_of(significand.begin(), significand.end(), [](unsigned char x){ return x == 0; })) {
+ // Significand is zero. Exponent doesn't matter.
+ x = fixed<B, I, F, R>(0);
+ } else if (exponent_negate) {
+ // A huge negative exponent approaches 0.
+ x = fixed<B, I, F, R>::from_raw_value(0);
+ } else {
+ // A huge positive exponent approaches infinity.
+ x = std::numeric_limits<fixed<B, I, F, R>>::max();
+ }
+ return is;
+ }
+
+ // Shift the fraction offset according to exponent
+ {
+ const auto exponent_mult = (base == 10) ? 1: 4;
+ if (exponent_negate) {
+ const auto adjust = std::min(exponent / exponent_mult, fraction_start);
+ fraction_start -= adjust;
+ exponent -= adjust * exponent_mult;
+ } else {
+ const auto adjust = std::min(exponent / exponent_mult, significand.size() - fraction_start);
+ fraction_start += adjust;
+ exponent -= adjust * exponent_mult;
+ }
+ }
+
+ constexpr auto IsSigned = std::is_signed<B>::value;
+ constexpr auto IntBits = sizeof(B) * 8 - F - (IsSigned ? 1 : 0);
+ constexpr auto MaxInt = (I{1} << IntBits) - 1;
+ constexpr auto MaxFraction = (I{1} << F) - 1;
+ constexpr auto MaxValue = (I{1} << sizeof(B) * 8) - 1;
+
+ // Parse the integer part
+ I integer = 0;
+ for (std::size_t i = 0; i < fraction_start; ++i) {
+ if (integer > MaxInt / base) {
+ // Overflow
+ x = negate ? std::numeric_limits<fixed<B, I, F, R>>::min() : std::numeric_limits<fixed<B, I, F, R>>::max();
+ return is;
+ }
+ assert(significand[i] < base);
+ integer = integer * base + significand[i];
+ }
+
+ // Parse the fractional part
+ I fraction = 0;
+ I divisor = 1;
+ for (std::size_t i = fraction_start; i < significand.size(); ++i) {
+ assert(significand[i] < base);
+ if (divisor > MaxFraction / base) {
+ // We're done
+ break;
+ }
+ fraction = fraction * base + significand[i];
+ divisor *= base;
+ }
+
+ // Construct the value from the parsed parts
+ I raw_value = (integer << F) + (fraction << F) / divisor;
+
+ // Apply remaining exponent
+ if (exponent_char == 'p') {
+ // Base-2 exponent
+ if (exponent_negate) {
+ raw_value >>= exponent;
+ } else {
+ raw_value <<= exponent;
+ }
+ } else {
+ // Base-10 exponent
+ if (exponent_negate) {
+ I remainder = 0;
+ for (std::size_t e = 0; e < exponent; ++e) {
+ remainder = raw_value % 10;
+ raw_value /= 10;
+ }
+ raw_value += remainder / 5;
+ } else {
+ for (std::size_t e = 0; e < exponent; ++e) {
+ if (raw_value > MaxValue / 10) {
+ // Overflow
+ x = negate ? std::numeric_limits<fixed<B, I, F, R>>::min() : std::numeric_limits<fixed<B, I, F, R>>::max();
+ return is;
+ }
+ raw_value *= 10;
+ }
+ }
+ }
+ x = fixed<B, I, F, R>::from_raw_value(static_cast<B>(negate ? -raw_value : raw_value));
+ return is;
+}
+
+}
+
+#endif
diff --git a/fpm/math.hpp b/fpm/math.hpp
new file mode 100644
index 0000000..7a76349
--- /dev/null
+++ b/fpm/math.hpp
@@ -0,0 +1,684 @@
+#ifndef FPM_MATH_HPP
+#define FPM_MATH_HPP
+
+#include "fixed.hpp"
+#include <cmath>
+
+#ifdef _MSC_VER
+#include <intrin.h>
+#endif
+
+namespace fpm
+{
+
+//
+// Helper functions
+//
+namespace detail
+{
+
+// Returns the index of the most-signifcant set bit
+inline long find_highest_bit(unsigned long long value) noexcept
+{
+ assert(value != 0);
+#if defined(_MSC_VER)
+ unsigned long index;
+#if defined(_WIN64)
+ _BitScanReverse64(&index, value);
+#else
+ if (_BitScanReverse(&index, static_cast<unsigned long>(value >> 32)) != 0) {
+ index += 32;
+ } else {
+ _BitScanReverse(&index, static_cast<unsigned long>(value & 0xfffffffflu));
+ }
+#endif
+ return index;
+#elif defined(__GNUC__) || defined(__clang__)
+ return sizeof(value) * 8 - 1 - __builtin_clzll(value);
+#else
+# error "your platform does not support find_highest_bit()"
+#endif
+}
+
+}
+
+//
+// Classification methods
+//
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline int fpclassify(fixed<B, I, F, R> x) noexcept
+{
+ return (x.raw_value() == 0) ? FP_ZERO : FP_NORMAL;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool isfinite(fixed<B, I, F, R>) noexcept
+{
+ return true;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool isinf(fixed<B, I, F, R>) noexcept
+{
+ return false;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool isnan(fixed<B, I, F, R>) noexcept
+{
+ return false;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool isnormal(fixed<B, I, F, R> x) noexcept
+{
+ return x.raw_value() != 0;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool signbit(fixed<B, I, F, R> x) noexcept
+{
+ return x.raw_value() < 0;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool isgreater(fixed<B, I, F, R> x, fixed<B, I, F, R> y) noexcept
+{
+ return x > y;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool isgreaterequal(fixed<B, I, F, R> x, fixed<B, I, F, R> y) noexcept
+{
+ return x >= y;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool isless(fixed<B, I, F, R> x, fixed<B, I, F, R> y) noexcept
+{
+ return x < y;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool islessequal(fixed<B, I, F, R> x, fixed<B, I, F, R> y) noexcept
+{
+ return x <= y;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool islessgreater(fixed<B, I, F, R> x, fixed<B, I, F, R> y) noexcept
+{
+ return x != y;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline bool isunordered(fixed<B, I, F, R> x, fixed<B, I, F, R> y) noexcept
+{
+ return false;
+}
+
+//
+// Nearest integer operations
+//
+template <typename B, typename I, unsigned int F, bool R>
+inline fixed<B, I, F, R> ceil(fixed<B, I, F, R> x) noexcept
+{
+ constexpr auto FRAC = B(1) << F;
+ auto value = x.raw_value();
+ if (value > 0) value += FRAC - 1;
+ return fixed<B, I, F, R>::from_raw_value(value / FRAC * FRAC);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+inline fixed<B, I, F, R> floor(fixed<B, I, F, R> x) noexcept
+{
+ constexpr auto FRAC = B(1) << F;
+ auto value = x.raw_value();
+ if (value < 0) value -= FRAC - 1;
+ return fixed<B, I, F, R>::from_raw_value(value / FRAC * FRAC);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+inline fixed<B, I, F, R> trunc(fixed<B, I, F, R> x) noexcept
+{
+ constexpr auto FRAC = B(1) << F;
+ return fixed<B, I, F, R>::from_raw_value(x.raw_value() / FRAC * FRAC);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+inline fixed<B, I, F, R> round(fixed<B, I, F, R> x) noexcept
+{
+ constexpr auto FRAC = B(1) << F;
+ auto value = x.raw_value() / (FRAC / 2);
+ return fixed<B, I, F, R>::from_raw_value(((value / 2) + (value % 2)) * FRAC);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> nearbyint(fixed<B, I, F, R> x) noexcept
+{
+ // Rounding mode is assumed to be FE_TONEAREST
+ constexpr auto FRAC = B(1) << F;
+ auto value = x.raw_value();
+ const bool is_half = std::abs(value % FRAC) == FRAC / 2;
+ value /= FRAC / 2;
+ value = (value / 2) + (value % 2);
+ value -= (value % 2) * is_half;
+ return fixed<B, I, F, R>::from_raw_value(value * FRAC);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline fixed<B, I, F, R> rint(fixed<B, I, F, R> x) noexcept
+{
+ // Rounding mode is assumed to be FE_TONEAREST
+ return nearbyint(x);
+}
+
+//
+// Mathematical functions
+//
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline fixed<B, I, F, R> abs(fixed<B, I, F, R> x) noexcept
+{
+ return (x >= fixed<B, I, F, R>{0}) ? x : -x;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline fixed<B, I, F, R> fmod(fixed<B, I, F, R> x, fixed<B, I, F, R> y) noexcept
+{
+ return
+ assert(y.raw_value() != 0),
+ fixed<B, I, F, R>::from_raw_value(x.raw_value() % y.raw_value());
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline fixed<B, I, F, R> remainder(fixed<B, I, F, R> x, fixed<B, I, F, R> y) noexcept
+{
+ return
+ assert(y.raw_value() != 0),
+ x - nearbyint(x / y) * y;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+inline fixed<B, I, F, R> remquo(fixed<B, I, F, R> x, fixed<B, I, F, R> y, int* quo) noexcept
+{
+ assert(y.raw_value() != 0);
+ assert(quo != nullptr);
+ *quo = x.raw_value() / y.raw_value();
+ return fixed<B, I, F, R>::from_raw_value(x.raw_value() % y.raw_value());
+}
+
+//
+// Manipulation functions
+//
+
+template <typename B, typename I, unsigned int F, bool R, typename C, typename J, unsigned int G, bool S>
+constexpr inline fixed<B, I, F, R> copysign(fixed<B, I, F, R> x, fixed<C, J, G, S> y) noexcept
+{
+ return
+ x = abs(x),
+ (y >= fixed<C, J, G, S>{0}) ? x : -x;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline fixed<B, I, F, R> nextafter(fixed<B, I, F, R> from, fixed<B, I, F, R> to) noexcept
+{
+ return from == to ? to :
+ to > from ? fixed<B, I, F, R>::from_raw_value(from.raw_value() + 1)
+ : fixed<B, I, F, R>::from_raw_value(from.raw_value() - 1);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+constexpr inline fixed<B, I, F, R> nexttoward(fixed<B, I, F, R> from, fixed<B, I, F, R> to) noexcept
+{
+ return nextafter(from, to);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+inline fixed<B, I, F, R> modf(fixed<B, I, F, R> x, fixed<B, I, F, R>* iptr) noexcept
+{
+ const auto raw = x.raw_value();
+ constexpr auto FRAC = B{1} << F;
+ *iptr = fixed<B, I, F, R>::from_raw_value(raw / FRAC * FRAC);
+ return fixed<B, I, F, R>::from_raw_value(raw % FRAC);
+}
+
+
+//
+// Power functions
+//
+
+template <typename B, typename I, unsigned int F, bool R, typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+fixed<B, I, F, R> pow(fixed<B, I, F, R> base, T exp) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+
+ if (base == Fixed(0)) {
+ assert(exp > 0);
+ return Fixed(0);
+ }
+
+ Fixed result {1};
+ if (exp < 0)
+ {
+ for (Fixed intermediate = base; exp != 0; exp /= 2, intermediate *= intermediate)
+ {
+ if ((exp % 2) != 0)
+ {
+ result /= intermediate;
+ }
+ }
+ }
+ else
+ {
+ for (Fixed intermediate = base; exp != 0; exp /= 2, intermediate *= intermediate)
+ {
+ if ((exp % 2) != 0)
+ {
+ result *= intermediate;
+ }
+ }
+ }
+ return result;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> pow(fixed<B, I, F, R> base, fixed<B, I, F, R> exp) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+
+ if (base == Fixed(0)) {
+ assert(exp > Fixed(0));
+ return Fixed(0);
+ }
+
+ if (exp < Fixed(0))
+ {
+ return 1 / pow(base, -exp);
+ }
+
+ constexpr auto FRAC = B(1) << F;
+ if (exp.raw_value() % FRAC == 0)
+ {
+ // Non-fractional exponents are easier to calculate
+ return pow(base, exp.raw_value() / FRAC);
+ }
+
+ // For negative bases we do not support fractional exponents.
+ // Technically fractions with odd denominators could work,
+ // but that's too much work to figure out.
+ assert(base > Fixed(0));
+ return exp2(log2(base) * exp);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> exp(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ if (x < Fixed(0)) {
+ return 1 / exp(-x);
+ }
+ constexpr auto FRAC = B(1) << F;
+ const B x_int = x.raw_value() / FRAC;
+ x -= x_int;
+ assert(x >= Fixed(0) && x < Fixed(1));
+
+ constexpr auto fA = Fixed::template from_fixed_point<63>( 128239257017632854ll); // 1.3903728105644451e-2
+ constexpr auto fB = Fixed::template from_fixed_point<63>( 320978614890280666ll); // 3.4800571158543038e-2
+ constexpr auto fC = Fixed::template from_fixed_point<63>(1571680799599592947ll); // 1.7040197373796334e-1
+ constexpr auto fD = Fixed::template from_fixed_point<63>(4603349000587966862ll); // 4.9909609871464493e-1
+ constexpr auto fE = Fixed::template from_fixed_point<62>(4612052447974689712ll); // 1.0000794567422495
+ constexpr auto fF = Fixed::template from_fixed_point<63>(9223361618412247875ll); // 9.9999887043019773e-1
+ return pow(Fixed::e(), x_int) * (((((fA * x + fB) * x + fC) * x + fD) * x + fE) * x + fF);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> exp2(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ if (x < Fixed(0)) {
+ return 1 / exp2(-x);
+ }
+ constexpr auto FRAC = B(1) << F;
+ const B x_int = x.raw_value() / FRAC;
+ x -= x_int;
+ assert(x >= Fixed(0) && x < Fixed(1));
+
+ constexpr auto fA = Fixed::template from_fixed_point<63>( 17491766697771214ll); // 1.8964611454333148e-3
+ constexpr auto fB = Fixed::template from_fixed_point<63>( 82483038782406547ll); // 8.9428289841091295e-3
+ constexpr auto fC = Fixed::template from_fixed_point<63>( 515275173969157690ll); // 5.5866246304520701e-2
+ constexpr auto fD = Fixed::template from_fixed_point<63>(2214897896212987987ll); // 2.4013971109076949e-1
+ constexpr auto fE = Fixed::template from_fixed_point<63>(6393224161192452326ll); // 6.9315475247516736e-1
+ constexpr auto fF = Fixed::template from_fixed_point<63>(9223371050976163566ll); // 9.9999989311082668e-1
+ return Fixed(1 << x_int) * (((((fA * x + fB) * x + fC) * x + fD) * x + fE) * x + fF);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> expm1(fixed<B, I, F, R> x) noexcept
+{
+ return exp(x) - 1;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> log2(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ assert(x > Fixed(0));
+
+ // Normalize input to the [1:2] domain
+ B value = x.raw_value();
+ const long highest = detail::find_highest_bit(value);
+ if (highest >= F) {
+ value >>= (highest - F);
+ } else {
+ value <<= (F - highest);
+ }
+ x = Fixed::from_raw_value(value);
+ assert(x >= Fixed(1) && x < Fixed(2));
+
+ constexpr auto fA = Fixed::template from_fixed_point<63>( 413886001457275979ll); // 4.4873610194131727e-2
+ constexpr auto fB = Fixed::template from_fixed_point<63>(-3842121857793256941ll); // -4.1656368651734915e-1
+ constexpr auto fC = Fixed::template from_fixed_point<62>( 7522345947206307744ll); // 1.6311487636297217
+ constexpr auto fD = Fixed::template from_fixed_point<61>(-8187571043052183818ll); // -3.5507929249026341
+ constexpr auto fE = Fixed::template from_fixed_point<60>( 5870342889289496598ll); // 5.0917108110420042
+ constexpr auto fF = Fixed::template from_fixed_point<61>(-6457199832668582866ll); // -2.8003640347009253
+ return Fixed(highest - F) + (((((fA * x + fB) * x + fC) * x + fD) * x + fE) * x + fF);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> log(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ return log2(x) / log2(Fixed::e());
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> log10(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ return log2(x) / log2(Fixed(10));
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> log1p(fixed<B, I, F, R> x) noexcept
+{
+ return log(1 + x);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> cbrt(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+
+ if (x == Fixed(0))
+ {
+ return x;
+ }
+ if (x < Fixed(0))
+ {
+ return -cbrt(-x);
+ }
+ assert(x >= Fixed(0));
+
+ // Finding the cube root of an integer, taken from Hacker's Delight,
+ // based on the square root algorithm.
+
+ // We start at the greatest power of eight that's less than the argument.
+ int ofs = ((detail::find_highest_bit(x.raw_value()) + 2*F) / 3 * 3);
+ I num = I{x.raw_value()};
+ I res = 0;
+
+ const auto do_round = [&]
+ {
+ for (; ofs >= 0; ofs -= 3)
+ {
+ res += res;
+ const I val = (3*res*(res + 1) + 1) << ofs;
+ if (num >= val)
+ {
+ num -= val;
+ res++;
+ }
+ }
+ };
+
+ // We should shift by 2*F (since there are two multiplications), but that
+ // could overflow even the intermediate type, so we have to split the
+ // algorithm up in two rounds of F bits each. Each round will deplete
+ // 'num' digit by digit, so after a round we can shift it again.
+ num <<= F;
+ ofs -= F;
+ do_round();
+
+ num <<= F;
+ ofs += F;
+ do_round();
+
+ return Fixed::from_raw_value(static_cast<B>(res));
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> sqrt(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+
+ assert(x >= Fixed(0));
+ if (x == Fixed(0))
+ {
+ return x;
+ }
+
+ // Finding the square root of an integer in base-2, from:
+ // https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Binary_numeral_system_.28base_2.29
+
+ // Shift by F first because it's fixed-point.
+ I num = I{x.raw_value()} << F;
+ I res = 0;
+
+ // "bit" starts at the greatest power of four that's less than the argument.
+ for (I bit = I{1} << ((detail::find_highest_bit(x.raw_value()) + F) / 2 * 2); bit != 0; bit >>= 2)
+ {
+ const I val = res + bit;
+ res >>= 1;
+ if (num >= val)
+ {
+ num -= val;
+ res += bit;
+ }
+ }
+
+ // Round the last digit up if necessary
+ if (num > res)
+ {
+ res++;
+ }
+
+ return Fixed::from_raw_value(static_cast<B>(res));
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> hypot(fixed<B, I, F, R> x, fixed<B, I, F, R> y) noexcept
+{
+ assert(x != 0 || y != 0);
+ return sqrt(x*x + y*y);
+}
+
+//
+// Trigonometry functions
+//
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> sin(fixed<B, I, F, R> x) noexcept
+{
+ // This sine uses a fifth-order curve-fitting approximation originally
+ // described by Jasper Vijn on coranac.com which has a worst-case
+ // relative error of 0.07% (over [-pi:pi]).
+ using Fixed = fixed<B, I, F, R>;
+
+ // Turn x from [0..2*PI] domain into [0..4] domain
+ x = fmod(x, Fixed::two_pi());
+ x = x / Fixed::half_pi();
+
+ // Take x modulo one rotation, so [-4..+4].
+ if (x < Fixed(0)) {
+ x += Fixed(4);
+ }
+
+ int sign = +1;
+ if (x > Fixed(2)) {
+ // Reduce domain to [0..2].
+ sign = -1;
+ x -= Fixed(2);
+ }
+
+ if (x > Fixed(1)) {
+ // Reduce domain to [0..1].
+ x = Fixed(2) - x;
+ }
+
+ const Fixed x2 = x*x;
+ return sign * x * (Fixed::pi() - x2*(Fixed::two_pi() - 5 - x2*(Fixed::pi() - 3)))/2;
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+inline fixed<B, I, F, R> cos(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ if (x > Fixed(0)) { // Prevent an overflow due to the addition of π/2
+ return sin(x - (Fixed::two_pi() - Fixed::half_pi()));
+ } else {
+ return sin(Fixed::half_pi() + x);
+ }
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+inline fixed<B, I, F, R> tan(fixed<B, I, F, R> x) noexcept
+{
+ auto cx = cos(x);
+
+ // Tangent goes to infinity at 90 and -90 degrees.
+ // We can't represent that with fixed-point maths.
+ assert(abs(cx).raw_value() > 1);
+
+ return sin(x) / cx;
+}
+
+namespace detail {
+
+// Calculates atan(x) assuming that x is in the range [0,1]
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> atan_sanitized(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ assert(x >= Fixed(0) && x <= Fixed(1));
+
+ constexpr auto fA = Fixed::template from_fixed_point<63>( 716203666280654660ll); // 0.0776509570923569
+ constexpr auto fB = Fixed::template from_fixed_point<63>(-2651115102768076601ll); // -0.287434475393028
+ constexpr auto fC = Fixed::template from_fixed_point<63>( 9178930894564541004ll); // 0.995181681698119 (PI/4 - A - B)
+
+ const auto xx = x * x;
+ return ((fA*xx + fB)*xx + fC)*x;
+}
+
+// Calculate atan(y / x), assuming x != 0.
+//
+// If x is very, very small, y/x can easily overflow the fixed-point range.
+// If q = y/x and q > 1, atan(q) would calculate atan(1/q) as intermediate step
+// anyway. We can shortcut that here and avoid the loss of information, thus
+// improving the accuracy of atan(y/x) for very small x.
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> atan_div(fixed<B, I, F, R> y, fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ assert(x != Fixed(0));
+
+ // Make sure y and x are positive.
+ // If y / x is negative (when y or x, but not both, are negative), negate the result to
+ // keep the correct outcome.
+ if (y < Fixed(0)) {
+ if (x < Fixed(0)) {
+ return atan_div(-y, -x);
+ }
+ return -atan_div(-y, x);
+ }
+ if (x < Fixed(0)) {
+ return -atan_div(y, -x);
+ }
+ assert(y >= Fixed(0));
+ assert(x > Fixed(0));
+
+ if (y > x) {
+ return Fixed::half_pi() - detail::atan_sanitized(x / y);
+ }
+ return detail::atan_sanitized(y / x);
+}
+
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> atan(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ if (x < Fixed(0))
+ {
+ return -atan(-x);
+ }
+
+ if (x > Fixed(1))
+ {
+ return Fixed::half_pi() - detail::atan_sanitized(Fixed(1) / x);
+ }
+
+ return detail::atan_sanitized(x);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> asin(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ assert(x >= Fixed(-1) && x <= Fixed(+1));
+
+ const auto yy = Fixed(1) - x * x;
+ if (yy == Fixed(0))
+ {
+ return copysign(Fixed::half_pi(), x);
+ }
+ return detail::atan_div(x, sqrt(yy));
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> acos(fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ assert(x >= Fixed(-1) && x <= Fixed(+1));
+
+ if (x == Fixed(-1))
+ {
+ return Fixed::pi();
+ }
+ const auto yy = Fixed(1) - x * x;
+ return Fixed(2)*detail::atan_div(sqrt(yy), Fixed(1) + x);
+}
+
+template <typename B, typename I, unsigned int F, bool R>
+fixed<B, I, F, R> atan2(fixed<B, I, F, R> y, fixed<B, I, F, R> x) noexcept
+{
+ using Fixed = fixed<B, I, F, R>;
+ if (x == Fixed(0))
+ {
+ assert(y != Fixed(0));
+ return (y > Fixed(0)) ? Fixed::half_pi() : -Fixed::half_pi();
+ }
+
+ auto ret = detail::atan_div(y, x);
+
+ if (x < Fixed(0))
+ {
+ return (y >= Fixed(0)) ? ret + Fixed::pi() : ret - Fixed::pi();
+ }
+ return ret;
+}
+
+}
+
+#endif
diff --git a/kiss/CHANGELOG b/kiss/CHANGELOG
new file mode 100644
index 0000000..2dd3603
--- /dev/null
+++ b/kiss/CHANGELOG
@@ -0,0 +1,123 @@
+1.3.0 2012-07-18
+ removed non-standard malloc.h from kiss_fft.h
+
+ moved -lm to end of link line
+
+ checked various return values
+
+ converted python Numeric code to NumPy
+
+ fixed test of int32_t on 64 bit OS
+
+ added padding in a couple of places to allow SIMD alignment of structs
+
+1.2.9 2010-05-27
+ threadsafe ( including OpenMP )
+
+ first edition of kissfft.hh the C++ template fft engine
+
+1.2.8
+ Changed memory.h to string.h -- apparently more standard
+
+ Added openmp extensions. This can have fairly linear speedups for larger FFT sizes.
+
+1.2.7
+ Shrank the real-fft memory footprint. Thanks to Galen Seitz.
+
+1.2.6 (Nov 14, 2006) The "thanks to GenArts" release.
+ Added multi-dimensional real-optimized FFT, see tools/kiss_fftndr
+ Thanks go to GenArts, Inc. for sponsoring the development.
+
+1.2.5 (June 27, 2006) The "release for no good reason" release.
+ Changed some harmless code to make some compilers' warnings go away.
+ Added some more digits to pi -- why not.
+ Added kiss_fft_next_fast_size() function to help people decide how much to pad.
+ Changed multidimensional test from 8 dimensions to only 3 to avoid testing
+ problems with fixed point (sorry Buckaroo Banzai).
+
+1.2.4 (Oct 27, 2005) The "oops, inverse fixed point real fft was borked" release.
+ Fixed scaling bug for inverse fixed point real fft -- also fixed test code that should've been failing.
+ Thanks to Jean-Marc Valin for bug report.
+
+ Use sys/types.h for more portable types than short,int,long => int16_t,int32_t,int64_t
+ If your system does not have these, you may need to define them -- but at least it breaks in a
+ loud and easily fixable way -- unlike silently using the wrong size type.
+
+ Hopefully tools/psdpng.c is fixed -- thanks to Steve Kellog for pointing out the weirdness.
+
+1.2.3 (June 25, 2005) The "you want to use WHAT as a sample" release.
+ Added ability to use 32 bit fixed point samples -- requires a 64 bit intermediate result, a la 'long long'
+
+ Added ability to do 4 FFTs in parallel by using SSE SIMD instructions. This is accomplished by
+ using the __m128 (vector of 4 floats) as kiss_fft_scalar. Define USE_SIMD to use this.
+
+ I know, I know ... this is drifting a bit from the "kiss" principle, but the speed advantages
+ make it worth it for some. Also recent gcc makes it SOO easy to use vectors of 4 floats like a POD type.
+
+1.2.2 (May 6, 2005) The Matthew release
+ Replaced fixed point division with multiply&shift. Thanks to Jean-Marc Valin for
+ discussions regarding. Considerable speedup for fixed-point.
+
+ Corrected overflow protection in real fft routines when using fixed point.
+ Finder's Credit goes to Robert Oschler of robodance for pointing me at the bug.
+ This also led to the CHECK_OVERFLOW_OP macro.
+
+1.2.1 (April 4, 2004)
+ compiles cleanly with just about every -W warning flag under the sun
+
+ reorganized kiss_fft_state so it could be read-only/const. This may be useful for embedded systems
+ that are willing to predeclare twiddle factors, factorization.
+
+ Fixed C_MUL,S_MUL on 16-bit platforms.
+
+ tmpbuf will only be allocated if input & output buffers are same
+ scratchbuf will only be allocated for ffts that are not multiples of 2,3,5
+
+ NOTE: The tmpbuf,scratchbuf changes may require synchronization code for multi-threaded apps.
+
+
+1.2 (Feb 23, 2004)
+ interface change -- cfg object is forward declaration of struct instead of void*
+ This maintains type saftey and lets the compiler warn/error about stupid mistakes.
+ (prompted by suggestion from Erik de Castro Lopo)
+
+ small speed improvements
+
+ added psdpng.c -- sample utility that will create png spectrum "waterfalls" from an input file
+ ( not terribly useful yet)
+
+1.1.1 (Feb 1, 2004 )
+ minor bug fix -- only affects odd rank, in-place, multi-dimensional FFTs
+
+1.1 : (Jan 30,2004)
+ split sample_code/ into test/ and tools/
+
+ Removed 2-D fft and added N-D fft (arbitrary)
+
+ modified fftutil.c to allow multi-d FFTs
+
+ Modified core fft routine to allow an input stride via kiss_fft_stride()
+ (eased support of multi-D ffts)
+
+ Added fast convolution filtering (FIR filtering using overlap-scrap method, with tail scrap)
+
+ Add kfc.[ch]: the KISS FFT Cache. It takes care of allocs for you ( suggested by Oscar Lesta ).
+
+1.0.1 (Dec 15, 2003)
+ fixed bug that occurred when nfft==1. Thanks to Steven Johnson.
+
+1.0 : (Dec 14, 2003)
+ changed kiss_fft function from using a single buffer, to two buffers.
+ If the same buffer pointer is supplied for both in and out, kiss will
+ manage the buffer copies.
+
+ added kiss_fft2d and kiss_fftr as separate source files (declarations in kiss_fft.h )
+
+0.4 :(Nov 4,2003) optimized for radix 2,3,4,5
+
+0.3 :(Oct 28, 2003) woops, version 2 didn't actually factor out any radices other than 2.
+ Thanks to Steven Johnson for finding this one.
+
+0.2 :(Oct 27, 2003) added mixed radix, only radix 2,4 optimized versions
+
+0.1 :(May 19 2003) initial release, radix 2 only
diff --git a/kiss/COPYING b/kiss/COPYING
new file mode 100644
index 0000000..6b4b622
--- /dev/null
+++ b/kiss/COPYING
@@ -0,0 +1,11 @@
+Copyright (c) 2003-2010 Mark Borgerding . All rights reserved.
+
+KISS FFT is provided under:
+
+ SPDX-License-Identifier: BSD-3-Clause
+
+Being under the terms of the BSD 3-clause "New" or "Revised" License,
+according with:
+
+ LICENSES/BSD-3-Clause
+
diff --git a/kiss/README.md b/kiss/README.md
new file mode 100644
index 0000000..1138a0c
--- /dev/null
+++ b/kiss/README.md
@@ -0,0 +1,245 @@
+# KISS FFT [![Build Status](https://travis-ci.com/mborgerding/kissfft.svg?branch=master)](https://travis-ci.com/mborgerding/kissfft)
+
+KISS FFT - A mixed-radix Fast Fourier Transform based up on the principle,
+"Keep It Simple, Stupid."
+
+There are many great fft libraries already around. Kiss FFT is not trying
+to be better than any of them. It only attempts to be a reasonably efficient,
+moderately useful FFT that can use fixed or floating data types and can be
+incorporated into someone's C program in a few minutes with trivial licensing.
+
+## USAGE:
+
+The basic usage for 1-d complex FFT is:
+
+```c
+ #include "kiss_fft.h"
+ kiss_fft_cfg cfg = kiss_fft_alloc( nfft ,is_inverse_fft ,0,0 );
+ while ...
+
+ ... // put kth sample in cx_in[k].r and cx_in[k].i
+
+ kiss_fft( cfg , cx_in , cx_out );
+
+ ... // transformed. DC is in cx_out[0].r and cx_out[0].i
+
+ kiss_fft_free(cfg);
+```
+ - **Note**: frequency-domain data is stored from dc up to 2pi.
+ so cx_out[0] is the dc bin of the FFT
+ and cx_out[nfft/2] is the Nyquist bin (if exists)
+
+Declarations are in "kiss_fft.h", along with a brief description of the
+functions you'll need to use.
+
+Code definitions for 1d complex FFTs are in kiss_fft.c.
+
+You can do other cool stuff with the extras you'll find in tools/
+> - multi-dimensional FFTs
+> - real-optimized FFTs (returns the positive half-spectrum:
+ (nfft/2+1) complex frequency bins)
+> - fast convolution FIR filtering (not available for fixed point)
+> - spectrum image creation
+
+The core fft and most tools/ code can be compiled to use float, double,
+ Q15 short or Q31 samples. The default is float.
+
+## BUILDING:
+
+There are two functionally-equivalent build systems supported by kissfft:
+
+ - Make (traditional Makefiles for Unix / Linux systems)
+ - CMake (more modern and feature-rich build system developed by Kitware)
+
+To build kissfft, the following build environment can be used:
+
+ - GNU build environment with GCC, Clang and GNU Make or CMake (>= 3.6)
+ - Microsoft Visual C++ (MSVC) with CMake (>= 3.6)
+
+Additional libraries required to build and test kissfft include:
+
+ - libpng for psdpng tool,
+ - libfftw3 to validate kissfft results against it,
+ - python 2/3 with Numpy to validate kissfft results against it.
+ - OpenMP supported by GCC, Clang or MSVC for multi-core FFT transformations
+
+Environments like Cygwin and MinGW can be highly likely used to build kissfft
+targeting Windows platform, but no tests were performed to the date.
+
+Both Make and CMake builds are easily configurable:
+
+ - `KISSFFT_DATATYPE=<datatype>` (for Make) or `-DKISSFFT_DATATYPE=<datatype>`
+ (for CMake) denote the principal datatype used by kissfft. It can be one
+ of the following:
+
+ - float (default)
+ - double
+ - int16_t
+ - int32_t
+ - SIMD (requires SSE instruction set support on target CPU)
+
+ - `KISSFFT_OPENMP=1` (for Make) or `-DKISSFFT_OPENMP=ON` (for CMake) builds kissfft
+ with OpenMP support. Please note that a supported compiler is required and this
+ option is turned off by default.
+
+ - `KISSFFT_STATIC=1` (for Make) or `-DKISSFFT_STATIC=ON` (for CMake) instructs
+ the builder to create static library ('.lib' for Windows / '.a' for Unix or Linux).
+ By default, this option is turned off and the shared library is created
+ ('.dll' for Windows, '.so' for Linux or Unix, '.dylib' for Mac OSX)
+
+ - `-DKISSFFT_TEST=OFF` (for CMake) disables building tests for kissfft. On Make,
+ building tests is done separately by 'make testall' or 'make testsingle', so
+ no specific setting is required.
+
+ - `KISSFFT_TOOLS=0` (for Make) or `-DKISSFFT_TOOLS=OFF` (for CMake) builds kissfft
+ without command-line tools like 'fastconv'. By default the tools are built.
+
+ - `KISSFFT_USE_ALLOCA=1` (for Make) or `-DKISSFFT_USE_ALLOCA=ON` (for CMake)
+ build kissfft with 'alloca' usage instead of 'malloc' / 'free'.
+
+ - `PREFIX=/full/path/to/installation/prefix/directory` (for Make) or
+ `-DCMAKE_INSTALL_PREFIX=/full/path/to/installation/prefix/directory` (for CMake)
+ specifies the prefix directory to install kissfft into.
+
+For example, to build kissfft as a static library with 'int16_t' datatype and
+OpenMP support using Make, run the command from kissfft source tree:
+
+```
+make KISSFFT_DATATYPE=int16_t KISSFFT_STATIC=1 KISSFFT_OPENMP=1 all
+```
+
+The same configuration for CMake is:
+
+```
+mkdir build && cd build
+cmake -DKISSFFT_DATATYPE=int16_t -DKISSFFT_STATIC=ON -DKISSFFT_OPENMP=ON ..
+make all
+```
+
+To specify '/tmp/1234' as installation prefix directory, run:
+
+
+```
+make PREFIX=/tmp/1234 KISSFFT_DATATYPE=int16_t KISSFFT_STATIC=1 KISSFFT_OPENMP=1 install
+```
+
+or
+
+```
+mkdir build && cd build
+cmake -DCMAKE_INSTALL_PREFIX=/tmp/1234 -DKISSFFT_DATATYPE=int16_t -DKISSFFT_STATIC=ON -DKISSFFT_OPENMP=ON ..
+make all
+make install
+```
+
+## TESTING:
+
+To validate the build configured as an example above, run the following command from
+kissfft source tree:
+
+```
+make KISSFFT_DATATYPE=int16_t KISSFFT_STATIC=1 KISSFFT_OPENMP=1 testsingle
+```
+
+if using Make, or:
+
+```
+make test
+```
+
+if using CMake.
+
+To test all possible build configurations, please run an extended testsuite from
+kissfft source tree:
+
+```
+sh test/kissfft-testsuite.sh
+```
+
+Please note that the extended testsuite takes around 20-40 minutes depending on device
+it runs on. This testsuite is useful for reporting bugs or testing the pull requests.
+
+## BACKGROUND
+
+I started coding this because I couldn't find a fixed point FFT that didn't
+use assembly code. I started with floating point numbers so I could get the
+theory straight before working on fixed point issues. In the end, I had a
+little bit of code that could be recompiled easily to do ffts with short, float
+or double (other types should be easy too).
+
+Once I got my FFT working, I was curious about the speed compared to
+a well respected and highly optimized fft library. I don't want to criticize
+this great library, so let's call it FFT_BRANDX.
+During this process, I learned:
+
+> 1. FFT_BRANDX has more than 100K lines of code. The core of kiss_fft is about 500 lines (cpx 1-d).
+> 2. It took me an embarrassingly long time to get FFT_BRANDX working.
+> 3. A simple program using FFT_BRANDX is 522KB. A similar program using kiss_fft is 18KB (without optimizing for size).
+> 4. FFT_BRANDX is roughly twice as fast as KISS FFT in default mode.
+
+It is wonderful that free, highly optimized libraries like FFT_BRANDX exist.
+But such libraries carry a huge burden of complexity necessary to extract every
+last bit of performance.
+
+**Sometimes simpler is better, even if it's not better.**
+
+## FREQUENTLY ASKED QUESTIONS:
+> Q: Can I use kissfft in a project with a ___ license?</br>
+> A: Yes. See LICENSE below.
+
+> Q: Why don't I get the output I expect?</br>
+> A: The two most common causes of this are
+> 1) scaling : is there a constant multiplier between what you got and what you want?
+> 2) mixed build environment -- all code must be compiled with same preprocessor
+> definitions for FIXED_POINT and kiss_fft_scalar
+
+> Q: Will you write/debug my code for me?</br>
+> A: Probably not unless you pay me. I am happy to answer pointed and topical questions, but
+> I may refer you to a book, a forum, or some other resource.
+
+
+## PERFORMANCE
+ (on Athlon XP 2100+, with gcc 2.96, float data type)
+
+Kiss performed 10000 1024-pt cpx ffts in .63 s of cpu time.
+For comparison, it took md5sum twice as long to process the same amount of data.
+Transforming 5 minutes of CD quality audio takes less than a second (nfft=1024).
+
+**DO NOT:**
+- use Kiss if you need the Fastest Fourier Transform in the World
+- ask me to add features that will bloat the code
+
+## UNDER THE HOOD
+
+Kiss FFT uses a time decimation, mixed-radix, out-of-place FFT. If you give it an input buffer
+and output buffer that are the same, a temporary buffer will be created to hold the data.
+
+No static data is used. The core routines of kiss_fft are thread-safe (but not all of the tools directory).[
+
+No scaling is done for the floating point version (for speed).
+Scaling is done both ways for the fixed-point version (for overflow prevention).
+
+Optimized butterflies are used for factors 2,3,4, and 5.
+
+The real (i.e. not complex) optimization code only works for even length ffts. It does two half-length
+FFTs in parallel (packed into real&imag), and then combines them via twiddling. The result is
+nfft/2+1 complex frequency bins from DC to Nyquist. If you don't know what this means, search the web.
+
+The fast convolution filtering uses the overlap-scrap method, slightly
+modified to put the scrap at the tail.
+
+## LICENSE
+ Revised BSD License, see COPYING for verbiage.
+ Basically, "free to use&change, give credit where due, no guarantees"
+ Note this license is compatible with GPL at one end of the spectrum and closed, commercial software at
+ the other end. See http://www.fsf.org/licensing/licenses
+
+## TODO
+ - Add real optimization for odd length FFTs
+ - Document/revisit the input/output fft scaling
+ - Make doc describing the overlap (tail) scrap fast convolution filtering in kiss_fastfir.c
+ - Test all the ./tools/ code with fixed point (kiss_fastfir.c doesn't work, maybe others)
+
+## AUTHOR
+ Mark Borgerding
+ Mark@Borgerding.net
diff --git a/kiss/_kiss_fft_guts.h b/kiss/_kiss_fft_guts.h
new file mode 100644
index 0000000..4bd8d1c
--- /dev/null
+++ b/kiss/_kiss_fft_guts.h
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+/* kiss_fft.h
+ defines kiss_fft_scalar as either short or a float type
+ and defines
+ typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */
+
+#ifndef _kiss_fft_guts_h
+#define _kiss_fft_guts_h
+
+#include "kiss_fft.h"
+#include "kiss_fft_log.h"
+#include <limits.h>
+
+#define MAXFACTORS 32
+/* e.g. an fft of length 128 has 4 factors
+ as far as kissfft is concerned
+ 4*4*4*2
+ */
+
+struct kiss_fft_state{
+ int nfft;
+ int inverse;
+ int factors[2*MAXFACTORS];
+ kiss_fft_cpx twiddles[1];
+};
+
+/*
+ Explanation of macros dealing with complex math:
+
+ C_MUL(m,a,b) : m = a*b
+ C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise
+ C_SUB( res, a,b) : res = a - b
+ C_SUBFROM( res , a) : res -= a
+ C_ADDTO( res , a) : res += a
+ * */
+#ifdef FIXED_POINT
+#include <stdint.h>
+#if (FIXED_POINT==32)
+# define FRACBITS 31
+# define SAMPPROD int64_t
+#define SAMP_MAX INT32_MAX
+#define SAMP_MIN INT32_MIN
+#else
+# define FRACBITS 15
+# define SAMPPROD int32_t
+#define SAMP_MAX INT16_MAX
+#define SAMP_MIN INT16_MIN
+#endif
+
+#if defined(CHECK_OVERFLOW)
+# define CHECK_OVERFLOW_OP(a,op,b) \
+ if ( (SAMPPROD)(a) op (SAMPPROD)(b) > SAMP_MAX || (SAMPPROD)(a) op (SAMPPROD)(b) < SAMP_MIN ) { \
+ KISS_FFT_WARNING("overflow (%d " #op" %d) = %ld", (a),(b),(SAMPPROD)(a) op (SAMPPROD)(b)); }
+#endif
+
+
+# define smul(a,b) ( (SAMPPROD)(a)*(b) )
+# define sround( x ) (kiss_fft_scalar)( ( (x) + (1<<(FRACBITS-1)) ) >> FRACBITS )
+
+# define S_MUL(a,b) sround( smul(a,b) )
+
+# define C_MUL(m,a,b) \
+ do{ (m).r = sround( smul((a).r,(b).r) - smul((a).i,(b).i) ); \
+ (m).i = sround( smul((a).r,(b).i) + smul((a).i,(b).r) ); }while(0)
+
+# define DIVSCALAR(x,k) \
+ (x) = sround( smul( x, SAMP_MAX/k ) )
+
+# define C_FIXDIV(c,div) \
+ do { DIVSCALAR( (c).r , div); \
+ DIVSCALAR( (c).i , div); }while (0)
+
+# define C_MULBYSCALAR( c, s ) \
+ do{ (c).r = sround( smul( (c).r , s ) ) ;\
+ (c).i = sround( smul( (c).i , s ) ) ; }while(0)
+
+#else /* not FIXED_POINT*/
+
+# define S_MUL(a,b) ( (a)*(b) )
+#define C_MUL(m,a,b) \
+ do{ (m).r = (a).r*(b).r - (a).i*(b).i;\
+ (m).i = (a).r*(b).i + (a).i*(b).r; }while(0)
+# define C_FIXDIV(c,div) /* NOOP */
+# define C_MULBYSCALAR( c, s ) \
+ do{ (c).r *= (s);\
+ (c).i *= (s); }while(0)
+#endif
+
+#ifndef CHECK_OVERFLOW_OP
+# define CHECK_OVERFLOW_OP(a,op,b) /* noop */
+#endif
+
+#define C_ADD( res, a,b)\
+ do { \
+ CHECK_OVERFLOW_OP((a).r,+,(b).r)\
+ CHECK_OVERFLOW_OP((a).i,+,(b).i)\
+ (res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \
+ }while(0)
+#define C_SUB( res, a,b)\
+ do { \
+ CHECK_OVERFLOW_OP((a).r,-,(b).r)\
+ CHECK_OVERFLOW_OP((a).i,-,(b).i)\
+ (res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \
+ }while(0)
+#define C_ADDTO( res , a)\
+ do { \
+ CHECK_OVERFLOW_OP((res).r,+,(a).r)\
+ CHECK_OVERFLOW_OP((res).i,+,(a).i)\
+ (res).r += (a).r; (res).i += (a).i;\
+ }while(0)
+
+#define C_SUBFROM( res , a)\
+ do {\
+ CHECK_OVERFLOW_OP((res).r,-,(a).r)\
+ CHECK_OVERFLOW_OP((res).i,-,(a).i)\
+ (res).r -= (a).r; (res).i -= (a).i; \
+ }while(0)
+
+
+#ifdef FIXED_POINT
+# define KISS_FFT_COS(phase) floor(.5+SAMP_MAX * cos (phase))
+# define KISS_FFT_SIN(phase) floor(.5+SAMP_MAX * sin (phase))
+# define HALF_OF(x) ((x)>>1)
+#elif defined(USE_SIMD)
+# define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) )
+# define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) )
+# define HALF_OF(x) ((x)*_mm_set1_ps(.5))
+#else
+# define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase)
+# define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase)
+# define HALF_OF(x) ((x)*((kiss_fft_scalar).5))
+#endif
+
+#define kf_cexp(x,phase) \
+ do{ \
+ (x)->r = KISS_FFT_COS(phase);\
+ (x)->i = KISS_FFT_SIN(phase);\
+ }while(0)
+
+
+/* a debugging function */
+#define pcpx(c)\
+ KISS_FFT_DEBUG("%g + %gi\n",(double)((c)->r),(double)((c)->i))
+
+
+#ifdef KISS_FFT_USE_ALLOCA
+// define this to allow use of alloca instead of malloc for temporary buffers
+// Temporary buffers are used in two case:
+// 1. FFT sizes that have "bad" factors. i.e. not 2,3 and 5
+// 2. "in-place" FFTs. Notice the quotes, since kissfft does not really do an in-place transform.
+#include <alloca.h>
+#define KISS_FFT_TMP_ALLOC(nbytes) alloca(nbytes)
+#define KISS_FFT_TMP_FREE(ptr)
+#else
+#define KISS_FFT_TMP_ALLOC(nbytes) KISS_FFT_MALLOC(nbytes)
+#define KISS_FFT_TMP_FREE(ptr) KISS_FFT_FREE(ptr)
+#endif
+
+#endif /* _kiss_fft_guts_h */
+
diff --git a/kiss/kfc.c b/kiss/kfc.c
new file mode 100644
index 0000000..a405d9b
--- /dev/null
+++ b/kiss/kfc.c
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#include "kfc.h"
+
+typedef struct cached_fft *kfc_cfg;
+
+struct cached_fft
+{
+ int nfft;
+ int inverse;
+ kiss_fft_cfg cfg;
+ kfc_cfg next;
+};
+
+static kfc_cfg cache_root=NULL;
+static int ncached=0;
+
+static kiss_fft_cfg find_cached_fft(int nfft,int inverse)
+{
+ size_t len;
+ kfc_cfg cur=cache_root;
+ kfc_cfg prev=NULL;
+ while ( cur ) {
+ if ( cur->nfft == nfft && inverse == cur->inverse )
+ break;/*found the right node*/
+ prev = cur;
+ cur = prev->next;
+ }
+ if (cur== NULL) {
+ /* no cached node found, need to create a new one*/
+ kiss_fft_alloc(nfft,inverse,0,&len);
+#ifdef USE_SIMD
+ int padding = (16-sizeof(struct cached_fft)) & 15;
+ // make sure the cfg aligns on a 16 byte boundary
+ len += padding;
+#endif
+ cur = (kfc_cfg)KISS_FFT_MALLOC((sizeof(struct cached_fft) + len ));
+ if (cur == NULL)
+ return NULL;
+ cur->cfg = (kiss_fft_cfg)(cur+1);
+#ifdef USE_SIMD
+ cur->cfg = (kiss_fft_cfg) ((char*)(cur+1)+padding);
+#endif
+ kiss_fft_alloc(nfft,inverse,cur->cfg,&len);
+ cur->nfft=nfft;
+ cur->inverse=inverse;
+ cur->next = NULL;
+ if ( prev )
+ prev->next = cur;
+ else
+ cache_root = cur;
+ ++ncached;
+ }
+ return cur->cfg;
+}
+
+void kfc_cleanup(void)
+{
+ kfc_cfg cur=cache_root;
+ kfc_cfg next=NULL;
+ while (cur){
+ next = cur->next;
+ free(cur);
+ cur=next;
+ }
+ ncached=0;
+ cache_root = NULL;
+}
+void kfc_fft(int nfft, const kiss_fft_cpx * fin,kiss_fft_cpx * fout)
+{
+ kiss_fft( find_cached_fft(nfft,0),fin,fout );
+}
+
+void kfc_ifft(int nfft, const kiss_fft_cpx * fin,kiss_fft_cpx * fout)
+{
+ kiss_fft( find_cached_fft(nfft,1),fin,fout );
+}
+
+#ifdef KFC_TEST
+static void check(int nc)
+{
+ if (ncached != nc) {
+ fprintf(stderr,"ncached should be %d,but it is %d\n",nc,ncached);
+ exit(1);
+ }
+}
+
+int main(void)
+{
+ kiss_fft_cpx buf1[1024],buf2[1024];
+ memset(buf1,0,sizeof(buf1));
+ check(0);
+ kfc_fft(512,buf1,buf2);
+ check(1);
+ kfc_fft(512,buf1,buf2);
+ check(1);
+ kfc_ifft(512,buf1,buf2);
+ check(2);
+ kfc_cleanup();
+ check(0);
+ return 0;
+}
+#endif
diff --git a/kiss/kfc.h b/kiss/kfc.h
new file mode 100644
index 0000000..d7d8c1b
--- /dev/null
+++ b/kiss/kfc.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#ifndef KFC_H
+#define KFC_H
+#include "kiss_fft.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+KFC -- Kiss FFT Cache
+
+Not needing to deal with kiss_fft_alloc and a config
+object may be handy for a lot of programs.
+
+KFC uses the underlying KISS FFT functions, but caches the config object.
+The first time kfc_fft or kfc_ifft for a given FFT size, the cfg
+object is created for it. All subsequent calls use the cached
+configuration object.
+
+NOTE:
+You should probably not use this if your program will be using a lot
+of various sizes of FFTs. There is a linear search through the
+cached objects. If you are only using one or two FFT sizes, this
+will be negligible. Otherwise, you may want to use another method
+of managing the cfg objects.
+
+ There is no automated cleanup of the cached objects. This could lead
+to large memory usage in a program that uses a lot of *DIFFERENT*
+sized FFTs. If you want to force all cached cfg objects to be freed,
+call kfc_cleanup.
+
+ */
+
+/*forward complex FFT */
+void KISS_FFT_API kfc_fft(int nfft, const kiss_fft_cpx * fin,kiss_fft_cpx * fout);
+/*reverse complex FFT */
+void KISS_FFT_API kfc_ifft(int nfft, const kiss_fft_cpx * fin,kiss_fft_cpx * fout);
+
+/*free all cached objects*/
+void KISS_FFT_API kfc_cleanup(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/kiss/kiss_fft.c b/kiss/kiss_fft.c
new file mode 100644
index 0000000..58c24a0
--- /dev/null
+++ b/kiss/kiss_fft.c
@@ -0,0 +1,420 @@
+/*
+ * Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+
+#include "_kiss_fft_guts.h"
+/* The guts header contains all the multiplication and addition macros that are defined for
+ fixed or floating point complex numbers. It also delares the kf_ internal functions.
+ */
+
+static void kf_bfly2(
+ kiss_fft_cpx * Fout,
+ const size_t fstride,
+ const kiss_fft_cfg st,
+ int m
+ )
+{
+ kiss_fft_cpx * Fout2;
+ kiss_fft_cpx * tw1 = st->twiddles;
+ kiss_fft_cpx t;
+ Fout2 = Fout + m;
+ do{
+ C_FIXDIV(*Fout,2); C_FIXDIV(*Fout2,2);
+
+ C_MUL (t, *Fout2 , *tw1);
+ tw1 += fstride;
+ C_SUB( *Fout2 , *Fout , t );
+ C_ADDTO( *Fout , t );
+ ++Fout2;
+ ++Fout;
+ }while (--m);
+}
+
+static void kf_bfly4(
+ kiss_fft_cpx * Fout,
+ const size_t fstride,
+ const kiss_fft_cfg st,
+ const size_t m
+ )
+{
+ kiss_fft_cpx *tw1,*tw2,*tw3;
+ kiss_fft_cpx scratch[6];
+ size_t k=m;
+ const size_t m2=2*m;
+ const size_t m3=3*m;
+
+
+ tw3 = tw2 = tw1 = st->twiddles;
+
+ do {
+ C_FIXDIV(*Fout,4); C_FIXDIV(Fout[m],4); C_FIXDIV(Fout[m2],4); C_FIXDIV(Fout[m3],4);
+
+ C_MUL(scratch[0],Fout[m] , *tw1 );
+ C_MUL(scratch[1],Fout[m2] , *tw2 );
+ C_MUL(scratch[2],Fout[m3] , *tw3 );
+
+ C_SUB( scratch[5] , *Fout, scratch[1] );
+ C_ADDTO(*Fout, scratch[1]);
+ C_ADD( scratch[3] , scratch[0] , scratch[2] );
+ C_SUB( scratch[4] , scratch[0] , scratch[2] );
+ C_SUB( Fout[m2], *Fout, scratch[3] );
+ tw1 += fstride;
+ tw2 += fstride*2;
+ tw3 += fstride*3;
+ C_ADDTO( *Fout , scratch[3] );
+
+ if(st->inverse) {
+ Fout[m].r = scratch[5].r - scratch[4].i;
+ Fout[m].i = scratch[5].i + scratch[4].r;
+ Fout[m3].r = scratch[5].r + scratch[4].i;
+ Fout[m3].i = scratch[5].i - scratch[4].r;
+ }else{
+ Fout[m].r = scratch[5].r + scratch[4].i;
+ Fout[m].i = scratch[5].i - scratch[4].r;
+ Fout[m3].r = scratch[5].r - scratch[4].i;
+ Fout[m3].i = scratch[5].i + scratch[4].r;
+ }
+ ++Fout;
+ }while(--k);
+}
+
+static void kf_bfly3(
+ kiss_fft_cpx * Fout,
+ const size_t fstride,
+ const kiss_fft_cfg st,
+ size_t m
+ )
+{
+ size_t k=m;
+ const size_t m2 = 2*m;
+ kiss_fft_cpx *tw1,*tw2;
+ kiss_fft_cpx scratch[5];
+ kiss_fft_cpx epi3;
+ epi3 = st->twiddles[fstride*m];
+
+ tw1=tw2=st->twiddles;
+
+ do{
+ C_FIXDIV(*Fout,3); C_FIXDIV(Fout[m],3); C_FIXDIV(Fout[m2],3);
+
+ C_MUL(scratch[1],Fout[m] , *tw1);
+ C_MUL(scratch[2],Fout[m2] , *tw2);
+
+ C_ADD(scratch[3],scratch[1],scratch[2]);
+ C_SUB(scratch[0],scratch[1],scratch[2]);
+ tw1 += fstride;
+ tw2 += fstride*2;
+
+ Fout[m].r = Fout->r - HALF_OF(scratch[3].r);
+ Fout[m].i = Fout->i - HALF_OF(scratch[3].i);
+
+ C_MULBYSCALAR( scratch[0] , epi3.i );
+
+ C_ADDTO(*Fout,scratch[3]);
+
+ Fout[m2].r = Fout[m].r + scratch[0].i;
+ Fout[m2].i = Fout[m].i - scratch[0].r;
+
+ Fout[m].r -= scratch[0].i;
+ Fout[m].i += scratch[0].r;
+
+ ++Fout;
+ }while(--k);
+}
+
+static void kf_bfly5(
+ kiss_fft_cpx * Fout,
+ const size_t fstride,
+ const kiss_fft_cfg st,
+ int m
+ )
+{
+ kiss_fft_cpx *Fout0,*Fout1,*Fout2,*Fout3,*Fout4;
+ int u;
+ kiss_fft_cpx scratch[13];
+ kiss_fft_cpx * twiddles = st->twiddles;
+ kiss_fft_cpx *tw;
+ kiss_fft_cpx ya,yb;
+ ya = twiddles[fstride*m];
+ yb = twiddles[fstride*2*m];
+
+ Fout0=Fout;
+ Fout1=Fout0+m;
+ Fout2=Fout0+2*m;
+ Fout3=Fout0+3*m;
+ Fout4=Fout0+4*m;
+
+ tw=st->twiddles;
+ for ( u=0; u<m; ++u ) {
+ C_FIXDIV( *Fout0,5); C_FIXDIV( *Fout1,5); C_FIXDIV( *Fout2,5); C_FIXDIV( *Fout3,5); C_FIXDIV( *Fout4,5);
+ scratch[0] = *Fout0;
+
+ C_MUL(scratch[1] ,*Fout1, tw[u*fstride]);
+ C_MUL(scratch[2] ,*Fout2, tw[2*u*fstride]);
+ C_MUL(scratch[3] ,*Fout3, tw[3*u*fstride]);
+ C_MUL(scratch[4] ,*Fout4, tw[4*u*fstride]);
+
+ C_ADD( scratch[7],scratch[1],scratch[4]);
+ C_SUB( scratch[10],scratch[1],scratch[4]);
+ C_ADD( scratch[8],scratch[2],scratch[3]);
+ C_SUB( scratch[9],scratch[2],scratch[3]);
+
+ Fout0->r += scratch[7].r + scratch[8].r;
+ Fout0->i += scratch[7].i + scratch[8].i;
+
+ scratch[5].r = scratch[0].r + S_MUL(scratch[7].r,ya.r) + S_MUL(scratch[8].r,yb.r);
+ scratch[5].i = scratch[0].i + S_MUL(scratch[7].i,ya.r) + S_MUL(scratch[8].i,yb.r);
+
+ scratch[6].r = S_MUL(scratch[10].i,ya.i) + S_MUL(scratch[9].i,yb.i);
+ scratch[6].i = -S_MUL(scratch[10].r,ya.i) - S_MUL(scratch[9].r,yb.i);
+
+ C_SUB(*Fout1,scratch[5],scratch[6]);
+ C_ADD(*Fout4,scratch[5],scratch[6]);
+
+ scratch[11].r = scratch[0].r + S_MUL(scratch[7].r,yb.r) + S_MUL(scratch[8].r,ya.r);
+ scratch[11].i = scratch[0].i + S_MUL(scratch[7].i,yb.r) + S_MUL(scratch[8].i,ya.r);
+ scratch[12].r = - S_MUL(scratch[10].i,yb.i) + S_MUL(scratch[9].i,ya.i);
+ scratch[12].i = S_MUL(scratch[10].r,yb.i) - S_MUL(scratch[9].r,ya.i);
+
+ C_ADD(*Fout2,scratch[11],scratch[12]);
+ C_SUB(*Fout3,scratch[11],scratch[12]);
+
+ ++Fout0;++Fout1;++Fout2;++Fout3;++Fout4;
+ }
+}
+
+/* perform the butterfly for one stage of a mixed radix FFT */
+static void kf_bfly_generic(
+ kiss_fft_cpx * Fout,
+ const size_t fstride,
+ const kiss_fft_cfg st,
+ int m,
+ int p
+ )
+{
+ int u,k,q1,q;
+ kiss_fft_cpx * twiddles = st->twiddles;
+ kiss_fft_cpx t;
+ int Norig = st->nfft;
+
+ kiss_fft_cpx * scratch = (kiss_fft_cpx*)KISS_FFT_TMP_ALLOC(sizeof(kiss_fft_cpx)*p);
+ if (scratch == NULL){
+ KISS_FFT_ERROR("Memory allocation failed.");
+ return;
+ }
+
+ for ( u=0; u<m; ++u ) {
+ k=u;
+ for ( q1=0 ; q1<p ; ++q1 ) {
+ scratch[q1] = Fout[ k ];
+ C_FIXDIV(scratch[q1],p);
+ k += m;
+ }
+
+ k=u;
+ for ( q1=0 ; q1<p ; ++q1 ) {
+ int twidx=0;
+ Fout[ k ] = scratch[0];
+ for (q=1;q<p;++q ) {
+ twidx += fstride * k;
+ if (twidx>=Norig) twidx-=Norig;
+ C_MUL(t,scratch[q] , twiddles[twidx] );
+ C_ADDTO( Fout[ k ] ,t);
+ }
+ k += m;
+ }
+ }
+ KISS_FFT_TMP_FREE(scratch);
+}
+
+static
+void kf_work(
+ kiss_fft_cpx * Fout,
+ const kiss_fft_cpx * f,
+ const size_t fstride,
+ int in_stride,
+ int * factors,
+ const kiss_fft_cfg st
+ )
+{
+ kiss_fft_cpx * Fout_beg=Fout;
+ const int p=*factors++; /* the radix */
+ const int m=*factors++; /* stage's fft length/p */
+ const kiss_fft_cpx * Fout_end = Fout + p*m;
+
+#ifdef _OPENMP
+ // use openmp extensions at the
+ // top-level (not recursive)
+ if (fstride==1 && p<=5 && m!=1)
+ {
+ int k;
+
+ // execute the p different work units in different threads
+# pragma omp parallel for
+ for (k=0;k<p;++k)
+ kf_work( Fout +k*m, f+ fstride*in_stride*k,fstride*p,in_stride,factors,st);
+ // all threads have joined by this point
+
+ switch (p) {
+ case 2: kf_bfly2(Fout,fstride,st,m); break;
+ case 3: kf_bfly3(Fout,fstride,st,m); break;
+ case 4: kf_bfly4(Fout,fstride,st,m); break;
+ case 5: kf_bfly5(Fout,fstride,st,m); break;
+ default: kf_bfly_generic(Fout,fstride,st,m,p); break;
+ }
+ return;
+ }
+#endif
+
+ if (m==1) {
+ do{
+ *Fout = *f;
+ f += fstride*in_stride;
+ }while(++Fout != Fout_end );
+ }else{
+ do{
+ // recursive call:
+ // DFT of size m*p performed by doing
+ // p instances of smaller DFTs of size m,
+ // each one takes a decimated version of the input
+ kf_work( Fout , f, fstride*p, in_stride, factors,st);
+ f += fstride*in_stride;
+ }while( (Fout += m) != Fout_end );
+ }
+
+ Fout=Fout_beg;
+
+ // recombine the p smaller DFTs
+ switch (p) {
+ case 2: kf_bfly2(Fout,fstride,st,m); break;
+ case 3: kf_bfly3(Fout,fstride,st,m); break;
+ case 4: kf_bfly4(Fout,fstride,st,m); break;
+ case 5: kf_bfly5(Fout,fstride,st,m); break;
+ default: kf_bfly_generic(Fout,fstride,st,m,p); break;
+ }
+}
+
+/* facbuf is populated by p1,m1,p2,m2, ...
+ where
+ p[i] * m[i] = m[i-1]
+ m0 = n */
+static
+void kf_factor(int n,int * facbuf)
+{
+ int p=4;
+ double floor_sqrt;
+ floor_sqrt = floor( sqrt((double)n) );
+
+ /*factor out powers of 4, powers of 2, then any remaining primes */
+ do {
+ while (n % p) {
+ switch (p) {
+ case 4: p = 2; break;
+ case 2: p = 3; break;
+ default: p += 2; break;
+ }
+ if (p > floor_sqrt)
+ p = n; /* no more factors, skip to end */
+ }
+ n /= p;
+ *facbuf++ = p;
+ *facbuf++ = n;
+ } while (n > 1);
+}
+
+/*
+ *
+ * User-callable function to allocate all necessary storage space for the fft.
+ *
+ * The return value is a contiguous block of memory, allocated with malloc. As such,
+ * It can be freed with free(), rather than a kiss_fft-specific function.
+ * */
+kiss_fft_cfg kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem )
+{
+ KISS_FFT_ALIGN_CHECK(mem)
+
+ kiss_fft_cfg st=NULL;
+ size_t memneeded = KISS_FFT_ALIGN_SIZE_UP(sizeof(struct kiss_fft_state)
+ + sizeof(kiss_fft_cpx)*(nfft-1)); /* twiddle factors*/
+
+ if ( lenmem==NULL ) {
+ st = ( kiss_fft_cfg)KISS_FFT_MALLOC( memneeded );
+ }else{
+ if (mem != NULL && *lenmem >= memneeded)
+ st = (kiss_fft_cfg)mem;
+ *lenmem = memneeded;
+ }
+ if (st) {
+ int i;
+ st->nfft=nfft;
+ st->inverse = inverse_fft;
+
+ for (i=0;i<nfft;++i) {
+ const double pi=3.141592653589793238462643383279502884197169399375105820974944;
+ double phase = -2*pi*i / nfft;
+ if (st->inverse)
+ phase *= -1;
+ kf_cexp(st->twiddles+i, phase );
+ }
+
+ kf_factor(nfft,st->factors);
+ }
+ return st;
+}
+
+
+void kiss_fft_stride(kiss_fft_cfg st,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int in_stride)
+{
+ if (fin == fout) {
+ //NOTE: this is not really an in-place FFT algorithm.
+ //It just performs an out-of-place FFT into a temp buffer
+ if (fout == NULL){
+ KISS_FFT_ERROR("fout buffer NULL.");
+ return;
+ }
+
+ kiss_fft_cpx * tmpbuf = (kiss_fft_cpx*)KISS_FFT_TMP_ALLOC( sizeof(kiss_fft_cpx)*st->nfft);
+ if (tmpbuf == NULL){
+ KISS_FFT_ERROR("Memory allocation error.");
+ return;
+ }
+
+
+
+ kf_work(tmpbuf,fin,1,in_stride, st->factors,st);
+ memcpy(fout,tmpbuf,sizeof(kiss_fft_cpx)*st->nfft);
+ KISS_FFT_TMP_FREE(tmpbuf);
+ }else{
+ kf_work( fout, fin, 1,in_stride, st->factors,st );
+ }
+}
+
+void kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout)
+{
+ kiss_fft_stride(cfg,fin,fout,1);
+}
+
+
+void kiss_fft_cleanup(void)
+{
+ // nothing needed any more
+}
+
+int kiss_fft_next_fast_size(int n)
+{
+ while(1) {
+ int m=n;
+ while ( (m%2) == 0 ) m/=2;
+ while ( (m%3) == 0 ) m/=3;
+ while ( (m%5) == 0 ) m/=5;
+ if (m<=1)
+ break; /* n is completely factorable by twos, threes, and fives */
+ n++;
+ }
+ return n;
+}
diff --git a/kiss/kiss_fft.h b/kiss/kiss_fft.h
new file mode 100644
index 0000000..dce1034
--- /dev/null
+++ b/kiss/kiss_fft.h
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#ifndef KISS_FFT_H
+#define KISS_FFT_H
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+
+// Define KISS_FFT_SHARED macro to properly export symbols
+#ifdef KISS_FFT_SHARED
+# ifdef _WIN32
+# ifdef KISS_FFT_BUILD
+# define KISS_FFT_API __declspec(dllexport)
+# else
+# define KISS_FFT_API __declspec(dllimport)
+# endif
+# else
+# define KISS_FFT_API __attribute__ ((visibility ("default")))
+# endif
+#else
+# define KISS_FFT_API
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ ATTENTION!
+ If you would like a :
+ -- a utility that will handle the caching of fft objects
+ -- real-only (no imaginary time component ) FFT
+ -- a multi-dimensional FFT
+ -- a command-line utility to perform ffts
+ -- a command-line utility to perform fast-convolution filtering
+
+ Then see kfc.h kiss_fftr.h kiss_fftnd.h fftutil.c kiss_fastfir.c
+ in the tools/ directory.
+*/
+
+/* User may override KISS_FFT_MALLOC and/or KISS_FFT_FREE. */
+#ifdef USE_SIMD
+# include <xmmintrin.h>
+# define kiss_fft_scalar __m128
+# ifndef KISS_FFT_MALLOC
+# define KISS_FFT_MALLOC(nbytes) _mm_malloc(nbytes,16)
+# define KISS_FFT_ALIGN_CHECK(ptr)
+# define KISS_FFT_ALIGN_SIZE_UP(size) ((size + 15UL) & ~0xFUL)
+# endif
+# ifndef KISS_FFT_FREE
+# define KISS_FFT_FREE _mm_free
+# endif
+#else
+# define KISS_FFT_ALIGN_CHECK(ptr)
+# define KISS_FFT_ALIGN_SIZE_UP(size) (size)
+# ifndef KISS_FFT_MALLOC
+# define KISS_FFT_MALLOC malloc
+# endif
+# ifndef KISS_FFT_FREE
+# define KISS_FFT_FREE free
+# endif
+#endif
+
+
+#ifdef FIXED_POINT
+#include <stdint.h>
+# if (FIXED_POINT == 32)
+# define kiss_fft_scalar int32_t
+# else
+# define kiss_fft_scalar int16_t
+# endif
+#else
+# ifndef kiss_fft_scalar
+/* default is float */
+# define kiss_fft_scalar float
+# endif
+#endif
+
+typedef struct {
+ kiss_fft_scalar r;
+ kiss_fft_scalar i;
+}kiss_fft_cpx;
+
+typedef struct kiss_fft_state* kiss_fft_cfg;
+
+/*
+ * kiss_fft_alloc
+ *
+ * Initialize a FFT (or IFFT) algorithm's cfg/state buffer.
+ *
+ * typical usage: kiss_fft_cfg mycfg=kiss_fft_alloc(1024,0,NULL,NULL);
+ *
+ * The return value from fft_alloc is a cfg buffer used internally
+ * by the fft routine or NULL.
+ *
+ * If lenmem is NULL, then kiss_fft_alloc will allocate a cfg buffer using malloc.
+ * The returned value should be free()d when done to avoid memory leaks.
+ *
+ * The state can be placed in a user supplied buffer 'mem':
+ * If lenmem is not NULL and mem is not NULL and *lenmem is large enough,
+ * then the function places the cfg in mem and the size used in *lenmem
+ * and returns mem.
+ *
+ * If lenmem is not NULL and ( mem is NULL or *lenmem is not large enough),
+ * then the function returns NULL and places the minimum cfg
+ * buffer size in *lenmem.
+ * */
+
+kiss_fft_cfg KISS_FFT_API kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem);
+
+/*
+ * kiss_fft(cfg,in_out_buf)
+ *
+ * Perform an FFT on a complex input buffer.
+ * for a forward FFT,
+ * fin should be f[0] , f[1] , ... ,f[nfft-1]
+ * fout will be F[0] , F[1] , ... ,F[nfft-1]
+ * Note that each element is complex and can be accessed like
+ f[k].r and f[k].i
+ * */
+void KISS_FFT_API kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout);
+
+/*
+ A more generic version of the above function. It reads its input from every Nth sample.
+ * */
+void KISS_FFT_API kiss_fft_stride(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int fin_stride);
+
+/* If kiss_fft_alloc allocated a buffer, it is one contiguous
+ buffer and can be simply free()d when no longer needed*/
+#define kiss_fft_free KISS_FFT_FREE
+
+/*
+ Cleans up some memory that gets managed internally. Not necessary to call, but it might clean up
+ your compiler output to call this before you exit.
+*/
+void KISS_FFT_API kiss_fft_cleanup(void);
+
+
+/*
+ * Returns the smallest integer k, such that k>=n and k has only "fast" factors (2,3,5)
+ */
+int KISS_FFT_API kiss_fft_next_fast_size(int n);
+
+/* for real ffts, we need an even size */
+#define kiss_fftr_next_fast_size_real(n) \
+ (kiss_fft_next_fast_size( ((n)+1)>>1)<<1)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/kiss/kiss_fft_log.h b/kiss/kiss_fft_log.h
new file mode 100644
index 0000000..b5b631a
--- /dev/null
+++ b/kiss/kiss_fft_log.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#ifndef kiss_fft_log_h
+#define kiss_fft_log_h
+
+#define ERROR 1
+#define WARNING 2
+#define INFO 3
+#define DEBUG 4
+
+#define STRINGIFY(x) #x
+#define TOSTRING(x) STRINGIFY(x)
+
+#if defined(NDEBUG)
+# define KISS_FFT_LOG_MSG(severity, ...) ((void)0)
+#else
+# define KISS_FFT_LOG_MSG(severity, ...) \
+ fprintf(stderr, "[" #severity "] " __FILE__ ":" TOSTRING(__LINE__) " "); \
+ fprintf(stderr, __VA_ARGS__); \
+ fprintf(stderr, "\n")
+#endif
+
+#define KISS_FFT_ERROR(...) KISS_FFT_LOG_MSG(ERROR, __VA_ARGS__)
+#define KISS_FFT_WARNING(...) KISS_FFT_LOG_MSG(WARNING, __VA_ARGS__)
+#define KISS_FFT_INFO(...) KISS_FFT_LOG_MSG(INFO, __VA_ARGS__)
+#define KISS_FFT_DEBUG(...) KISS_FFT_LOG_MSG(DEBUG, __VA_ARGS__)
+
+
+
+#endif /* kiss_fft_log_h */ \ No newline at end of file
diff --git a/kiss/kiss_fftnd.c b/kiss/kiss_fftnd.c
new file mode 100644
index 0000000..5d5b089
--- /dev/null
+++ b/kiss/kiss_fftnd.c
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#include "kiss_fftnd.h"
+#include "_kiss_fft_guts.h"
+
+struct kiss_fftnd_state{
+ int dimprod; /* dimsum would be mighty tasty right now */
+ int ndims;
+ int *dims;
+ kiss_fft_cfg *states; /* cfg states for each dimension */
+ kiss_fft_cpx * tmpbuf; /*buffer capable of hold the entire input */
+};
+
+kiss_fftnd_cfg kiss_fftnd_alloc(const int *dims,int ndims,int inverse_fft,void*mem,size_t*lenmem)
+{
+ KISS_FFT_ALIGN_CHECK(mem)
+
+ kiss_fftnd_cfg st = NULL;
+ int i;
+ int dimprod=1;
+ size_t memneeded = KISS_FFT_ALIGN_SIZE_UP(sizeof(struct kiss_fftnd_state));
+ char * ptr = NULL;
+
+ for (i=0;i<ndims;++i) {
+ size_t sublen=0;
+ kiss_fft_alloc (dims[i], inverse_fft, NULL, &sublen);
+ memneeded += sublen; /* st->states[i] */
+ dimprod *= dims[i];
+ }
+ memneeded += KISS_FFT_ALIGN_SIZE_UP(sizeof(int) * ndims);/* st->dims */
+ memneeded += KISS_FFT_ALIGN_SIZE_UP(sizeof(void*) * ndims);/* st->states */
+ memneeded += KISS_FFT_ALIGN_SIZE_UP(sizeof(kiss_fft_cpx) * dimprod); /* st->tmpbuf */
+
+ if (lenmem == NULL) {/* allocate for the caller*/
+ ptr = (char *) malloc (memneeded);
+ } else { /* initialize supplied buffer if big enough */
+ if (*lenmem >= memneeded)
+ ptr = (char *) mem;
+ *lenmem = memneeded; /*tell caller how big struct is (or would be) */
+ }
+ if (!ptr)
+ return NULL; /*malloc failed or buffer too small */
+
+ st = (kiss_fftnd_cfg) ptr;
+ st->dimprod = dimprod;
+ st->ndims = ndims;
+ ptr += KISS_FFT_ALIGN_SIZE_UP(sizeof(struct kiss_fftnd_state));
+
+ st->states = (kiss_fft_cfg *)ptr;
+ ptr += KISS_FFT_ALIGN_SIZE_UP(sizeof(void*) * ndims);
+
+ st->dims = (int*)ptr;
+ ptr += KISS_FFT_ALIGN_SIZE_UP(sizeof(int) * ndims);
+
+ st->tmpbuf = (kiss_fft_cpx*)ptr;
+ ptr += KISS_FFT_ALIGN_SIZE_UP(sizeof(kiss_fft_cpx) * dimprod);
+
+ for (i=0;i<ndims;++i) {
+ size_t len;
+ st->dims[i] = dims[i];
+ kiss_fft_alloc (st->dims[i], inverse_fft, NULL, &len);
+ st->states[i] = kiss_fft_alloc (st->dims[i], inverse_fft, ptr,&len);
+ ptr += len;
+ }
+ /*
+Hi there!
+
+If you're looking at this particular code, it probably means you've got a brain-dead bounds checker
+that thinks the above code overwrites the end of the array.
+
+It doesn't.
+
+-- Mark
+
+P.S.
+The below code might give you some warm fuzzies and help convince you.
+ */
+ if ( ptr - (char*)st != (int)memneeded ) {
+ fprintf(stderr,
+ "################################################################################\n"
+ "Internal error! Memory allocation miscalculation\n"
+ "################################################################################\n"
+ );
+ }
+ return st;
+}
+
+/*
+ This works by tackling one dimension at a time.
+
+ In effect,
+ Each stage starts out by reshaping the matrix into a DixSi 2d matrix.
+ A Di-sized fft is taken of each column, transposing the matrix as it goes.
+
+Here's a 3-d example:
+Take a 2x3x4 matrix, laid out in memory as a contiguous buffer
+ [ [ [ a b c d ] [ e f g h ] [ i j k l ] ]
+ [ [ m n o p ] [ q r s t ] [ u v w x ] ] ]
+
+Stage 0 ( D=2): treat the buffer as a 2x12 matrix
+ [ [a b ... k l]
+ [m n ... w x] ]
+
+ FFT each column with size 2.
+ Transpose the matrix at the same time using kiss_fft_stride.
+
+ [ [ a+m a-m ]
+ [ b+n b-n]
+ ...
+ [ k+w k-w ]
+ [ l+x l-x ] ]
+
+ Note fft([x y]) == [x+y x-y]
+
+Stage 1 ( D=3) treats the buffer (the output of stage D=2) as an 3x8 matrix,
+ [ [ a+m a-m b+n b-n c+o c-o d+p d-p ]
+ [ e+q e-q f+r f-r g+s g-s h+t h-t ]
+ [ i+u i-u j+v j-v k+w k-w l+x l-x ] ]
+
+ And perform FFTs (size=3) on each of the columns as above, transposing
+ the matrix as it goes. The output of stage 1 is
+ (Legend: ap = [ a+m e+q i+u ]
+ am = [ a-m e-q i-u ] )
+
+ [ [ sum(ap) fft(ap)[0] fft(ap)[1] ]
+ [ sum(am) fft(am)[0] fft(am)[1] ]
+ [ sum(bp) fft(bp)[0] fft(bp)[1] ]
+ [ sum(bm) fft(bm)[0] fft(bm)[1] ]
+ [ sum(cp) fft(cp)[0] fft(cp)[1] ]
+ [ sum(cm) fft(cm)[0] fft(cm)[1] ]
+ [ sum(dp) fft(dp)[0] fft(dp)[1] ]
+ [ sum(dm) fft(dm)[0] fft(dm)[1] ] ]
+
+Stage 2 ( D=4) treats this buffer as a 4*6 matrix,
+ [ [ sum(ap) fft(ap)[0] fft(ap)[1] sum(am) fft(am)[0] fft(am)[1] ]
+ [ sum(bp) fft(bp)[0] fft(bp)[1] sum(bm) fft(bm)[0] fft(bm)[1] ]
+ [ sum(cp) fft(cp)[0] fft(cp)[1] sum(cm) fft(cm)[0] fft(cm)[1] ]
+ [ sum(dp) fft(dp)[0] fft(dp)[1] sum(dm) fft(dm)[0] fft(dm)[1] ] ]
+
+ Then FFTs each column, transposing as it goes.
+
+ The resulting matrix is the 3d FFT of the 2x3x4 input matrix.
+
+ Note as a sanity check that the first element of the final
+ stage's output (DC term) is
+ sum( [ sum(ap) sum(bp) sum(cp) sum(dp) ] )
+ , i.e. the summation of all 24 input elements.
+
+*/
+void kiss_fftnd(kiss_fftnd_cfg st,const kiss_fft_cpx *fin,kiss_fft_cpx *fout)
+{
+ int i,k;
+ const kiss_fft_cpx * bufin=fin;
+ kiss_fft_cpx * bufout;
+
+ /*arrange it so the last bufout == fout*/
+ if ( st->ndims & 1 ) {
+ bufout = fout;
+ if (fin==fout) {
+ memcpy( st->tmpbuf, fin, sizeof(kiss_fft_cpx) * st->dimprod );
+ bufin = st->tmpbuf;
+ }
+ }else
+ bufout = st->tmpbuf;
+
+ for ( k=0; k < st->ndims; ++k) {
+ int curdim = st->dims[k];
+ int stride = st->dimprod / curdim;
+
+ for ( i=0 ; i<stride ; ++i )
+ kiss_fft_stride( st->states[k], bufin+i , bufout+i*curdim, stride );
+
+ /*toggle back and forth between the two buffers*/
+ if (bufout == st->tmpbuf){
+ bufout = fout;
+ bufin = st->tmpbuf;
+ }else{
+ bufout = st->tmpbuf;
+ bufin = fout;
+ }
+ }
+}
diff --git a/kiss/kiss_fftnd.h b/kiss/kiss_fftnd.h
new file mode 100644
index 0000000..956ba94
--- /dev/null
+++ b/kiss/kiss_fftnd.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#ifndef KISS_FFTND_H
+#define KISS_FFTND_H
+
+#include "kiss_fft.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct kiss_fftnd_state * kiss_fftnd_cfg;
+
+kiss_fftnd_cfg KISS_FFT_API kiss_fftnd_alloc(const int *dims,int ndims,int inverse_fft,void*mem,size_t*lenmem);
+void KISS_FFT_API kiss_fftnd(kiss_fftnd_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/kiss/kiss_fftndr.c b/kiss/kiss_fftndr.c
new file mode 100644
index 0000000..e979d03
--- /dev/null
+++ b/kiss/kiss_fftndr.c
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#include "kiss_fftndr.h"
+#include "_kiss_fft_guts.h"
+#define MAX(x,y) ( ( (x)<(y) )?(y):(x) )
+
+struct kiss_fftndr_state
+{
+ int dimReal;
+ int dimOther;
+ kiss_fftr_cfg cfg_r;
+ kiss_fftnd_cfg cfg_nd;
+ void * tmpbuf;
+};
+
+static int prod(const int *dims, int ndims)
+{
+ int x=1;
+ while (ndims--)
+ x *= *dims++;
+ return x;
+}
+
+kiss_fftndr_cfg kiss_fftndr_alloc(const int *dims,int ndims,int inverse_fft,void*mem,size_t*lenmem)
+{
+ KISS_FFT_ALIGN_CHECK(mem)
+
+ kiss_fftndr_cfg st = NULL;
+ size_t nr=0 , nd=0,ntmp=0;
+ int dimReal = dims[ndims-1];
+ int dimOther = prod(dims,ndims-1);
+ size_t memneeded;
+ char * ptr = NULL;
+
+ (void)kiss_fftr_alloc(dimReal,inverse_fft,NULL,&nr);
+ (void)kiss_fftnd_alloc(dims,ndims-1,inverse_fft,NULL,&nd);
+ ntmp =
+ MAX( 2*dimOther , dimReal+2) * sizeof(kiss_fft_scalar) // freq buffer for one pass
+ + dimOther*(dimReal+2) * sizeof(kiss_fft_scalar); // large enough to hold entire input in case of in-place
+
+ memneeded = KISS_FFT_ALIGN_SIZE_UP(sizeof( struct kiss_fftndr_state )) + KISS_FFT_ALIGN_SIZE_UP(nr) + KISS_FFT_ALIGN_SIZE_UP(nd) + KISS_FFT_ALIGN_SIZE_UP(ntmp);
+
+ if (lenmem==NULL) {
+ ptr = (char*) malloc(memneeded);
+ }else{
+ if (*lenmem >= memneeded)
+ ptr = (char *)mem;
+ *lenmem = memneeded;
+ }
+ if (ptr==NULL)
+ return NULL;
+
+ st = (kiss_fftndr_cfg) ptr;
+ memset( st , 0 , memneeded);
+ ptr += KISS_FFT_ALIGN_SIZE_UP(sizeof(struct kiss_fftndr_state));
+
+ st->dimReal = dimReal;
+ st->dimOther = dimOther;
+ st->cfg_r = kiss_fftr_alloc( dimReal,inverse_fft,ptr,&nr);
+ ptr += KISS_FFT_ALIGN_SIZE_UP(nr);
+ st->cfg_nd = kiss_fftnd_alloc(dims,ndims-1,inverse_fft, ptr,&nd);
+ ptr += KISS_FFT_ALIGN_SIZE_UP(nd);
+ st->tmpbuf = ptr;
+
+ return st;
+}
+
+void kiss_fftndr(kiss_fftndr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata)
+{
+ int k1,k2;
+ int dimReal = st->dimReal;
+ int dimOther = st->dimOther;
+ int nrbins = dimReal/2+1;
+
+ kiss_fft_cpx * tmp1 = (kiss_fft_cpx*)st->tmpbuf;
+ kiss_fft_cpx * tmp2 = tmp1 + MAX(nrbins,dimOther);
+
+ // timedata is N0 x N1 x ... x Nk real
+
+ // take a real chunk of data, fft it and place the output at correct intervals
+ for (k1=0;k1<dimOther;++k1) {
+ kiss_fftr( st->cfg_r, timedata + k1*dimReal , tmp1 ); // tmp1 now holds nrbins complex points
+ for (k2=0;k2<nrbins;++k2)
+ tmp2[ k2*dimOther+k1 ] = tmp1[k2];
+ }
+
+ for (k2=0;k2<nrbins;++k2) {
+ kiss_fftnd(st->cfg_nd, tmp2+k2*dimOther, tmp1); // tmp1 now holds dimOther complex points
+ for (k1=0;k1<dimOther;++k1)
+ freqdata[ k1*(nrbins) + k2] = tmp1[k1];
+ }
+}
+
+void kiss_fftndri(kiss_fftndr_cfg st,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata)
+{
+ int k1,k2;
+ int dimReal = st->dimReal;
+ int dimOther = st->dimOther;
+ int nrbins = dimReal/2+1;
+ kiss_fft_cpx * tmp1 = (kiss_fft_cpx*)st->tmpbuf;
+ kiss_fft_cpx * tmp2 = tmp1 + MAX(nrbins,dimOther);
+
+ for (k2=0;k2<nrbins;++k2) {
+ for (k1=0;k1<dimOther;++k1)
+ tmp1[k1] = freqdata[ k1*(nrbins) + k2 ];
+ kiss_fftnd(st->cfg_nd, tmp1, tmp2+k2*dimOther);
+ }
+
+ for (k1=0;k1<dimOther;++k1) {
+ for (k2=0;k2<nrbins;++k2)
+ tmp1[k2] = tmp2[ k2*dimOther+k1 ];
+ kiss_fftri( st->cfg_r,tmp1,timedata + k1*dimReal);
+ }
+}
diff --git a/kiss/kiss_fftndr.h b/kiss/kiss_fftndr.h
new file mode 100644
index 0000000..0d56a1f
--- /dev/null
+++ b/kiss/kiss_fftndr.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#ifndef KISS_NDR_H
+#define KISS_NDR_H
+
+#include "kiss_fft.h"
+#include "kiss_fftr.h"
+#include "kiss_fftnd.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct kiss_fftndr_state *kiss_fftndr_cfg;
+
+
+kiss_fftndr_cfg KISS_FFT_API kiss_fftndr_alloc(const int *dims,int ndims,int inverse_fft,void*mem,size_t*lenmem);
+/*
+ dims[0] must be even
+
+ If you don't care to allocate space, use mem = lenmem = NULL
+*/
+
+
+void KISS_FFT_API kiss_fftndr(
+ kiss_fftndr_cfg cfg,
+ const kiss_fft_scalar *timedata,
+ kiss_fft_cpx *freqdata);
+/*
+ input timedata has dims[0] X dims[1] X ... X dims[ndims-1] scalar points
+ output freqdata has dims[0] X dims[1] X ... X dims[ndims-1]/2+1 complex points
+*/
+
+void KISS_FFT_API kiss_fftndri(
+ kiss_fftndr_cfg cfg,
+ const kiss_fft_cpx *freqdata,
+ kiss_fft_scalar *timedata);
+/*
+ input and output dimensions are the exact opposite of kiss_fftndr
+*/
+
+
+#define kiss_fftndr_free free
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/kiss/kiss_fftr.c b/kiss/kiss_fftr.c
new file mode 100644
index 0000000..778a9a6
--- /dev/null
+++ b/kiss/kiss_fftr.c
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#include "kiss_fftr.h"
+#include "_kiss_fft_guts.h"
+
+struct kiss_fftr_state{
+ kiss_fft_cfg substate;
+ kiss_fft_cpx * tmpbuf;
+ kiss_fft_cpx * super_twiddles;
+#ifdef USE_SIMD
+ void * pad;
+#endif
+};
+
+kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem)
+{
+ KISS_FFT_ALIGN_CHECK(mem)
+
+ int i;
+ kiss_fftr_cfg st = NULL;
+ size_t subsize = 0, memneeded;
+
+ if (nfft & 1) {
+ KISS_FFT_ERROR("Real FFT optimization must be even.");
+ return NULL;
+ }
+ nfft >>= 1;
+
+ kiss_fft_alloc (nfft, inverse_fft, NULL, &subsize);
+ memneeded = sizeof(struct kiss_fftr_state) + subsize + sizeof(kiss_fft_cpx) * ( nfft * 3 / 2);
+
+ if (lenmem == NULL) {
+ st = (kiss_fftr_cfg) KISS_FFT_MALLOC (memneeded);
+ } else {
+ if (*lenmem >= memneeded)
+ st = (kiss_fftr_cfg) mem;
+ *lenmem = memneeded;
+ }
+ if (!st)
+ return NULL;
+
+ st->substate = (kiss_fft_cfg) (st + 1); /*just beyond kiss_fftr_state struct */
+ st->tmpbuf = (kiss_fft_cpx *) (((char *) st->substate) + subsize);
+ st->super_twiddles = st->tmpbuf + nfft;
+ kiss_fft_alloc(nfft, inverse_fft, st->substate, &subsize);
+
+ for (i = 0; i < nfft/2; ++i) {
+ double phase =
+ -3.14159265358979323846264338327 * ((double) (i+1) / nfft + .5);
+ if (inverse_fft)
+ phase *= -1;
+ kf_cexp (st->super_twiddles+i,phase);
+ }
+ return st;
+}
+
+void kiss_fftr(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata)
+{
+ /* input buffer timedata is stored row-wise */
+ int k,ncfft;
+ kiss_fft_cpx fpnk,fpk,f1k,f2k,tw,tdc;
+
+ if ( st->substate->inverse) {
+ KISS_FFT_ERROR("kiss fft usage error: improper alloc");
+ return;/* The caller did not call the correct function */
+ }
+
+ ncfft = st->substate->nfft;
+
+ /*perform the parallel fft of two real signals packed in real,imag*/
+ kiss_fft( st->substate , (const kiss_fft_cpx*)timedata, st->tmpbuf );
+ /* The real part of the DC element of the frequency spectrum in st->tmpbuf
+ * contains the sum of the even-numbered elements of the input time sequence
+ * The imag part is the sum of the odd-numbered elements
+ *
+ * The sum of tdc.r and tdc.i is the sum of the input time sequence.
+ * yielding DC of input time sequence
+ * The difference of tdc.r - tdc.i is the sum of the input (dot product) [1,-1,1,-1...
+ * yielding Nyquist bin of input time sequence
+ */
+
+ tdc.r = st->tmpbuf[0].r;
+ tdc.i = st->tmpbuf[0].i;
+ C_FIXDIV(tdc,2);
+ CHECK_OVERFLOW_OP(tdc.r ,+, tdc.i);
+ CHECK_OVERFLOW_OP(tdc.r ,-, tdc.i);
+ freqdata[0].r = tdc.r + tdc.i;
+ freqdata[ncfft].r = tdc.r - tdc.i;
+#ifdef USE_SIMD
+ freqdata[ncfft].i = freqdata[0].i = _mm_set1_ps(0);
+#else
+ freqdata[ncfft].i = freqdata[0].i = 0;
+#endif
+
+ for ( k=1;k <= ncfft/2 ; ++k ) {
+ fpk = st->tmpbuf[k];
+ fpnk.r = st->tmpbuf[ncfft-k].r;
+ fpnk.i = - st->tmpbuf[ncfft-k].i;
+ C_FIXDIV(fpk,2);
+ C_FIXDIV(fpnk,2);
+
+ C_ADD( f1k, fpk , fpnk );
+ C_SUB( f2k, fpk , fpnk );
+ C_MUL( tw , f2k , st->super_twiddles[k-1]);
+
+ freqdata[k].r = HALF_OF(f1k.r + tw.r);
+ freqdata[k].i = HALF_OF(f1k.i + tw.i);
+ freqdata[ncfft-k].r = HALF_OF(f1k.r - tw.r);
+ freqdata[ncfft-k].i = HALF_OF(tw.i - f1k.i);
+ }
+}
+
+void kiss_fftri(kiss_fftr_cfg st,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata)
+{
+ /* input buffer timedata is stored row-wise */
+ int k, ncfft;
+
+ if (st->substate->inverse == 0) {
+ KISS_FFT_ERROR("kiss fft usage error: improper alloc");
+ return;/* The caller did not call the correct function */
+ }
+
+ ncfft = st->substate->nfft;
+
+ st->tmpbuf[0].r = freqdata[0].r + freqdata[ncfft].r;
+ st->tmpbuf[0].i = freqdata[0].r - freqdata[ncfft].r;
+ C_FIXDIV(st->tmpbuf[0],2);
+
+ for (k = 1; k <= ncfft / 2; ++k) {
+ kiss_fft_cpx fk, fnkc, fek, fok, tmp;
+ fk = freqdata[k];
+ fnkc.r = freqdata[ncfft - k].r;
+ fnkc.i = -freqdata[ncfft - k].i;
+ C_FIXDIV( fk , 2 );
+ C_FIXDIV( fnkc , 2 );
+
+ C_ADD (fek, fk, fnkc);
+ C_SUB (tmp, fk, fnkc);
+ C_MUL (fok, tmp, st->super_twiddles[k-1]);
+ C_ADD (st->tmpbuf[k], fek, fok);
+ C_SUB (st->tmpbuf[ncfft - k], fek, fok);
+#ifdef USE_SIMD
+ st->tmpbuf[ncfft - k].i *= _mm_set1_ps(-1.0);
+#else
+ st->tmpbuf[ncfft - k].i *= -1;
+#endif
+ }
+ kiss_fft (st->substate, st->tmpbuf, (kiss_fft_cpx *) timedata);
+}
diff --git a/kiss/kiss_fftr.h b/kiss/kiss_fftr.h
new file mode 100644
index 0000000..7fd73d2
--- /dev/null
+++ b/kiss/kiss_fftr.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#ifndef KISS_FTR_H
+#define KISS_FTR_H
+
+#include "kiss_fft.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+/*
+
+ Real optimized version can save about 45% cpu time vs. complex fft of a real seq.
+
+
+
+ */
+
+typedef struct kiss_fftr_state *kiss_fftr_cfg;
+
+
+kiss_fftr_cfg KISS_FFT_API kiss_fftr_alloc(int nfft,int inverse_fft,void * mem, size_t * lenmem);
+/*
+ nfft must be even
+
+ If you don't care to allocate space, use mem = lenmem = NULL
+*/
+
+
+void KISS_FFT_API kiss_fftr(kiss_fftr_cfg cfg,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata);
+/*
+ input timedata has nfft scalar points
+ output freqdata has nfft/2+1 complex points
+*/
+
+void KISS_FFT_API kiss_fftri(kiss_fftr_cfg cfg,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata);
+/*
+ input freqdata has nfft/2+1 complex points
+ output timedata has nfft scalar points
+*/
+
+#define kiss_fftr_free KISS_FFT_FREE
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/lib/Globals.cpp b/lib/Globals.cpp
index 6be26ec..6bd38fb 100644
--- a/lib/Globals.cpp
+++ b/lib/Globals.cpp
@@ -32,5 +32,7 @@
// the RC needs logging, and needs to be initialised later.
Logger etiLog;
+#if ENABLE_REMOTECONTROL
RemoteControllers rcs;
+#endif // ENABLE_REMOTECONTROL
diff --git a/lib/Json.cpp b/lib/Json.cpp
new file mode 100644
index 0000000..361a149
--- /dev/null
+++ b/lib/Json.cpp
@@ -0,0 +1,128 @@
+/*
+ Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012
+ Her Majesty the Queen in Right of Canada (Communications Research
+ Center Canada)
+
+ Copyright (C) 2023
+ 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 <iostream>
+#include <sstream>
+#include <iomanip>
+#include <string>
+#include <algorithm>
+
+#include "Json.h"
+
+namespace json {
+ static std::string escape_json(const std::string &s) {
+ std::ostringstream o;
+ for (auto c = s.cbegin(); c != s.cend(); c++) {
+ switch (*c) {
+ case '"': o << "\\\""; break;
+ case '\\': o << "\\\\"; break;
+ case '\b': o << "\\b"; break;
+ case '\f': o << "\\f"; break;
+ case '\n': o << "\\n"; break;
+ case '\r': o << "\\r"; break;
+ case '\t': o << "\\t"; break;
+ default:
+ if ('\x00' <= *c && *c <= '\x1f') {
+ o << "\\u"
+ << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(*c);
+ } else {
+ o << *c;
+ }
+ }
+ }
+ return o.str();
+ }
+
+ std::string map_to_json(const map_t& values) {
+ std::ostringstream ss;
+ ss << "{ ";
+ size_t ix = 0;
+ for (const auto& element : values) {
+ if (ix > 0) {
+ ss << ",";
+ }
+
+ ss << "\"" << escape_json(element.first) << "\": ";
+ ss << value_to_json(element.second);
+
+ ix++;
+ }
+ ss << " }";
+
+ return ss.str();
+ }
+
+ std::string value_to_json(const value_t& value)
+ {
+ std::ostringstream ss;
+
+ if (std::holds_alternative<std::string>(value.v)) {
+ ss << "\"" << escape_json(std::get<std::string>(value.v)) << "\"";
+ }
+ else if (std::holds_alternative<double>(value.v)) {
+ ss << std::fixed << std::get<double>(value.v);
+ }
+ else if (std::holds_alternative<uint64_t>(value.v)) {
+ ss << std::get<uint64_t>(value.v);
+ }
+ else if (std::holds_alternative<int64_t>(value.v)) {
+ ss << std::get<int64_t>(value.v);
+ }
+ else if (std::holds_alternative<uint32_t>(value.v)) {
+ ss << std::get<uint32_t>(value.v);
+ }
+ else if (std::holds_alternative<int32_t>(value.v)) {
+ ss << std::get<int32_t>(value.v);
+ }
+ else if (std::holds_alternative<bool>(value.v)) {
+ ss << (std::get<bool>(value.v) ? "true" : "false");
+ }
+ else if (std::holds_alternative<std::nullopt_t>(value.v)) {
+ ss << "null";
+ }
+ else if (std::holds_alternative<std::vector<json::value_t> >(value.v)) {
+ const auto& vec = std::get<std::vector<json::value_t> >(value.v);
+ ss << "[ ";
+ size_t list_ix = 0;
+ for (const auto& list_element : vec) {
+ if (list_ix > 0) {
+ ss << ",";
+ }
+ ss << value_to_json(list_element);
+ list_ix++;
+ }
+ ss << "]";
+ }
+ else if (std::holds_alternative<std::shared_ptr<json::map_t> >(value.v)) {
+ const map_t& v = *std::get<std::shared_ptr<json::map_t> >(value.v);
+ ss << map_to_json(v);
+ }
+ else {
+ throw std::logic_error("variant alternative not handled");
+ }
+
+ return ss.str();
+ }
+}
diff --git a/lib/Json.h b/lib/Json.h
new file mode 100644
index 0000000..b082f92
--- /dev/null
+++ b/lib/Json.h
@@ -0,0 +1,65 @@
+/*
+ Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012
+ Her Majesty the Queen in Right of Canada (Communications Research
+ Center Canada)
+
+ Copyright (C) 2023
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://www.opendigitalradio.org
+
+ This module adds remote-control capability to some of the dabmux/dabmod modules.
+ */
+/*
+ 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
+
+#include <vector>
+#include <memory>
+#include <optional>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+#include <variant>
+
+namespace json {
+
+ // STL containers are not required to support incomplete types,
+ // hence the shared_ptr
+
+ struct value_t {
+ std::variant<
+ std::shared_ptr<std::unordered_map<std::string, value_t>>,
+ std::vector<value_t>,
+ std::string,
+ double,
+ int64_t,
+ uint64_t,
+ int32_t,
+ uint32_t,
+ bool,
+ std::nullopt_t> v;
+ };
+
+ using map_t = std::unordered_map<std::string, value_t>;
+
+ std::string map_to_json(const map_t& values);
+ std::string value_to_json(const value_t& value);
+}
diff --git a/lib/RemoteControl.cpp b/lib/RemoteControl.cpp
index 9ca8d22..dca3373 100644
--- a/lib/RemoteControl.cpp
+++ b/lib/RemoteControl.cpp
@@ -25,11 +25,15 @@
#include <list>
#include <string>
#include <iostream>
+#include <sstream>
+#include <iomanip>
#include <string>
#include <algorithm>
#include "RemoteControl.h"
-#include "zmq.hpp"
+#if defined(HAVE_ZEROMQ)
+ #include "zmq.hpp"
+#endif
using namespace std;
@@ -100,6 +104,18 @@ std::list< std::vector<std::string> > RemoteControllers::get_param_list_values(c
return allparams;
}
+
+
+std::string RemoteControllers::get_showjson() {
+ json::map_t root;
+ for (auto &controllable : rcs.controllables) {
+ root[controllable->get_rc_name()].v =
+ std::make_shared<json::map_t>(controllable->get_all_values());
+ }
+
+ return json::map_to_json(root);
+}
+
std::string RemoteControllers::get_param(const std::string& name, const std::string& param) {
RemoteControllable* controllable = get_controllable_(name);
return controllable->get_parameter(param);
@@ -121,7 +137,7 @@ RemoteControllable* RemoteControllers::get_controllable_(const std::string& name
[&](RemoteControllable* r) { return r->get_rc_name() == name; });
if (rc == controllables.end()) {
- throw ParameterError("Module name unknown");
+ throw ParameterError(string{"Module name '"} + name + "' unknown");
}
else {
return *rc;
@@ -425,10 +441,15 @@ void RemoteControllerZmq::recv_all(zmq::socket_t& pSocket, std::vector<std::stri
bool more = true;
do {
zmq::message_t msg;
- pSocket.recv(msg);
- std::string incoming((char*)msg.data(), msg.size());
- message.push_back(incoming);
- more = msg.more();
+ const auto zresult = pSocket.recv(msg);
+ if (zresult) {
+ std::string incoming((char*)msg.data(), msg.size());
+ message.push_back(incoming);
+ more = msg.more();
+ }
+ else {
+ more = false;
+ }
} while (more);
}
@@ -455,6 +476,7 @@ void RemoteControllerZmq::send_fail_reply(zmq::socket_t &pSocket, const std::str
void RemoteControllerZmq::process()
{
m_fault = false;
+ m_active = true;
// create zmq reply socket for receiving ctrl parameters
try {
@@ -512,8 +534,21 @@ void RemoteControllerZmq::process()
repSocket.send(zmsg, (--cohort_size > 0) ? zmq::send_flags::sndmore : zmq::send_flags::none);
}
}
+ else if (msg.size() == 1 && command == "showjson") {
+ try {
+ std::string json = rcs.get_showjson();
+
+ zmq::message_t zmsg(json.size());
+ memcpy(zmsg.data(), json.data(), json.size());
+
+ repSocket.send(zmsg, zmq::send_flags::none);
+ }
+ catch (const ParameterError &err) {
+ send_fail_reply(repSocket, err.what());
+ }
+ }
else if (msg.size() == 2 && command == "show") {
- std::string module((char*) msg[1].data(), msg[1].size());
+ const std::string module((char*) msg[1].data(), msg[1].size());
try {
list< vector<string> > r = rcs.get_param_list_values(module);
size_t r_size = r.size();
@@ -531,8 +566,8 @@ void RemoteControllerZmq::process()
}
}
else if (msg.size() == 3 && command == "get") {
- std::string module((char*) msg[1].data(), msg[1].size());
- std::string parameter((char*) msg[2].data(), msg[2].size());
+ const std::string module((char*) msg[1].data(), msg[1].size());
+ const std::string parameter((char*) msg[2].data(), msg[2].size());
try {
std::string value = rcs.get_param(module, parameter);
@@ -545,9 +580,9 @@ void RemoteControllerZmq::process()
}
}
else if (msg.size() == 4 && command == "set") {
- std::string module((char*) msg[1].data(), msg[1].size());
- std::string parameter((char*) msg[2].data(), msg[2].size());
- std::string value((char*) msg[3].data(), msg[3].size());
+ const std::string module((char*) msg[1].data(), msg[1].size());
+ const std::string parameter((char*) msg[2].data(), msg[2].size());
+ const std::string value((char*) msg[3].data(), msg[3].size());
try {
rcs.set_param(module, parameter, value);
@@ -559,7 +594,7 @@ void RemoteControllerZmq::process()
}
else {
send_fail_reply(repSocket,
- "Unsupported command. commands: list, show, get, set");
+ "Unsupported command. commands: list, show, get, set, showjson");
}
}
}
diff --git a/lib/RemoteControl.h b/lib/RemoteControl.h
index 2358b3a..7dd763d 100644
--- a/lib/RemoteControl.h
+++ b/lib/RemoteControl.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://www.opendigitalradio.org
@@ -31,11 +31,15 @@
# include "config.h"
#endif
+#define ENABLE_REMOTECONTROL 1
+
#if defined(HAVE_ZEROMQ)
# include "zmq.hpp"
#endif
#include <list>
+#include <unordered_map>
+#include <variant>
#include <map>
#include <memory>
#include <string>
@@ -46,6 +50,7 @@
#include "Log.h"
#include "Socket.h"
+#include "Json.h"
#define RC_ADD_PARAMETER(p, desc) { \
std::vector<std::string> p; \
@@ -113,13 +118,13 @@ class RemoteControllable {
}
/* Base function to set parameters. */
- virtual void set_parameter(
- const std::string& parameter,
- const std::string& value) = 0;
+ virtual void set_parameter(const std::string& parameter, const std::string& value) = 0;
/* Getting a parameter always returns a string. */
virtual const std::string get_parameter(const std::string& parameter) const = 0;
+ virtual const json::map_t get_all_values() const = 0;
+
protected:
std::string m_rc_name;
std::list< std::vector<std::string> > m_parameters;
@@ -135,6 +140,7 @@ class RemoteControllers {
void check_faults();
std::list< std::vector<std::string> > get_param_list_values(const std::string& name);
std::string get_param(const std::string& name, const std::string& param);
+ std::string get_showjson();
void set_param(
const std::string& name,
diff --git a/lib/Socket.cpp b/lib/Socket.cpp
index 1ff6418..938b573 100644
--- a/lib/Socket.cpp
+++ b/lib/Socket.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009 Her Majesty the
Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2020
+ Copyright (C) 2022
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -24,12 +24,13 @@
#include "Socket.h"
-#include <iostream>
+#include <stdexcept>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <fcntl.h>
#include <poll.h>
+#include <netinet/tcp.h>
namespace Socket {
@@ -105,16 +106,20 @@ UDPSocket::UDPSocket(UDPSocket&& other)
{
m_sock = other.m_sock;
m_port = other.m_port;
+ m_multicast_source = other.m_multicast_source;
other.m_port = 0;
other.m_sock = INVALID_SOCKET;
+ other.m_multicast_source = "";
}
const UDPSocket& UDPSocket::operator=(UDPSocket&& other)
{
m_sock = other.m_sock;
m_port = other.m_port;
+ m_multicast_source = other.m_multicast_source;
other.m_port = 0;
other.m_sock = INVALID_SOCKET;
+ other.m_multicast_source = "";
return *this;
}
@@ -143,6 +148,7 @@ void UDPSocket::reinit(int port, const std::string& name)
// No need to bind to a given port, creating the
// socket is enough
m_sock = ::socket(AF_INET, SOCK_DGRAM, 0);
+ post_init();
return;
}
@@ -179,6 +185,7 @@ void UDPSocket::reinit(int port, const std::string& name)
if (::bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0) {
m_sock = sfd;
+ post_init();
break;
}
@@ -188,10 +195,47 @@ void UDPSocket::reinit(int port, const std::string& name)
freeaddrinfo(result);
if (rp == nullptr) {
- throw runtime_error("Could not bind");
+ throw runtime_error(string{"Could not bind to port "} + to_string(port));
+ }
+}
+
+void UDPSocket::post_init() {
+ int pktinfo = 1;
+ if (setsockopt(m_sock, IPPROTO_IP, IP_PKTINFO, &pktinfo, sizeof(pktinfo)) == SOCKET_ERROR) {
+ throw runtime_error(string("Can't request pktinfo: ") + strerror(errno));
+ }
+
+}
+
+void UDPSocket::init_receive_multicast(int port, const string& local_if_addr, const string& mcastaddr)
+{
+ if (m_sock != INVALID_SOCKET) {
+ ::close(m_sock);
+ }
+
+ m_port = port;
+ m_sock = ::socket(AF_INET, SOCK_DGRAM, 0);
+ post_init();
+
+ int reuse_setting = 1;
+ if (setsockopt(m_sock, SOL_SOCKET, SO_REUSEADDR, &reuse_setting, sizeof(reuse_setting)) == SOCKET_ERROR) {
+ throw runtime_error("Can't reuse address");
+ }
+
+ struct sockaddr_in la;
+ memset((char *) &la, 0, sizeof(la));
+ la.sin_family = AF_INET;
+ la.sin_port = htons(port);
+ la.sin_addr.s_addr = INADDR_ANY;
+ if (::bind(m_sock, (struct sockaddr*)&la, sizeof(la))) {
+ throw runtime_error(string("Could not bind: ") + strerror(errno));
}
+
+ m_multicast_source = mcastaddr;
+ join_group(mcastaddr.c_str(), local_if_addr.c_str());
}
+
void UDPSocket::close()
{
if (m_sock != INVALID_SOCKET) {
@@ -211,16 +255,26 @@ UDPSocket::~UDPSocket()
UDPPacket UDPSocket::receive(size_t max_size)
{
+ struct sockaddr_in addr;
+ struct msghdr msg;
+ struct iovec iov;
+ constexpr size_t BUFFER_SIZE = 1024;
+ char control_buffer[BUFFER_SIZE];
+ struct cmsghdr *cmsg;
+
UDPPacket packet(max_size);
- socklen_t addrSize;
- addrSize = sizeof(*packet.address.as_sockaddr());
- ssize_t ret = recvfrom(m_sock,
- packet.buffer.data(),
- packet.buffer.size(),
- 0,
- packet.address.as_sockaddr(),
- &addrSize);
+ memset(&msg, 0, sizeof(msg));
+ msg.msg_name = &addr;
+ msg.msg_namelen = sizeof(addr);
+ msg.msg_iov = &iov;
+ iov.iov_base = packet.buffer.data();
+ iov.iov_len = packet.buffer.size();
+ msg.msg_iovlen = 1;
+ msg.msg_control = control_buffer;
+ msg.msg_controllen = sizeof(control_buffer);
+
+ ssize_t ret = recvmsg(m_sock, &msg, 0);
if (ret == SOCKET_ERROR) {
packet.buffer.resize(0);
@@ -231,12 +285,42 @@ UDPPacket UDPSocket::receive(size_t max_size)
if (errno == EAGAIN or errno == EWOULDBLOCK)
#endif
{
- return 0;
+ return packet;
}
throw runtime_error(string("Can't receive data: ") + strerror(errno));
}
- packet.buffer.resize(ret);
+ struct in_pktinfo *pktinfo = nullptr;
+ for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+ if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) {
+ pktinfo = (struct in_pktinfo *)CMSG_DATA(cmsg);
+ break;
+ }
+ }
+
+ if (pktinfo) {
+ char src_addr[INET_ADDRSTRLEN];
+ char dst_addr[INET_ADDRSTRLEN];
+ inet_ntop(AF_INET, &(addr.sin_addr), src_addr, INET_ADDRSTRLEN);
+ inet_ntop(AF_INET, &(pktinfo->ipi_addr), dst_addr, INET_ADDRSTRLEN);
+ //fprintf(stderr, "Received packet from %s to %s: %zu\n", src_addr, dst_addr, ret);
+
+ memcpy(&packet.address.addr, &addr, sizeof(addr));
+
+ if (m_multicast_source.empty() or
+ strcmp(dst_addr, m_multicast_source.c_str()) == 0) {
+ packet.buffer.resize(ret);
+ }
+ else {
+ // Ignore packet for different multicast group
+ packet.buffer.resize(0);
+ }
+ }
+ else {
+ //fprintf(stderr, "No pktinfo: %zu\n", ret);
+ packet.buffer.resize(ret);
+ }
+
return packet;
}
@@ -268,14 +352,14 @@ void UDPSocket::send(const std::string& data, InetAddress destination)
}
}
-void UDPSocket::joinGroup(const char* groupname, const char* if_addr)
+void UDPSocket::join_group(const char* groupname, const char* if_addr)
{
ip_mreqn group;
if ((group.imr_multiaddr.s_addr = inet_addr(groupname)) == INADDR_NONE) {
throw runtime_error("Cannot convert multicast group name");
}
if (!IN_MULTICAST(ntohl(group.imr_multiaddr.s_addr))) {
- throw runtime_error("Group name is not a multicast address");
+ throw runtime_error(string("Group name '") + groupname + "' is not a multicast address");
}
if (if_addr) {
@@ -287,7 +371,7 @@ void UDPSocket::joinGroup(const char* groupname, const char* if_addr)
group.imr_ifindex = 0;
if (setsockopt(m_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group))
== SOCKET_ERROR) {
- throw runtime_error(string("Can't join multicast group") + strerror(errno));
+ throw runtime_error(string("Can't join multicast group: ") + strerror(errno));
}
}
@@ -295,12 +379,12 @@ void UDPSocket::setMulticastSource(const char* source_addr)
{
struct in_addr addr;
if (inet_aton(source_addr, &addr) == 0) {
- throw runtime_error(string("Can't parse source address") + strerror(errno));
+ throw runtime_error(string("Can't parse source address: ") + strerror(errno));
}
if (setsockopt(m_sock, IPPROTO_IP, IP_MULTICAST_IF, &addr, sizeof(addr))
== SOCKET_ERROR) {
- throw runtime_error(string("Can't set source address") + strerror(errno));
+ throw runtime_error(string("Can't set source address: ") + strerror(errno));
}
}
@@ -308,7 +392,7 @@ void UDPSocket::setMulticastTTL(int ttl)
{
if (setsockopt(m_sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl))
== SOCKET_ERROR) {
- throw runtime_error(string("Can't set multicast ttl") + strerror(errno));
+ throw runtime_error(string("Can't set multicast ttl: ") + strerror(errno));
}
}
@@ -326,15 +410,13 @@ void UDPReceiver::add_receive_port(int port, const string& bindto, const string&
UDPSocket sock;
if (IN_MULTICAST(ntohl(inet_addr(mcastaddr.c_str())))) {
- sock.reinit(port, mcastaddr);
- sock.setMulticastSource(bindto.c_str());
- sock.joinGroup(mcastaddr.c_str(), bindto.c_str());
+ sock.init_receive_multicast(port, bindto, mcastaddr);
}
else {
sock.reinit(port, bindto);
}
- m_sockets.push_back(move(sock));
+ m_sockets.push_back(std::move(sock));
}
vector<UDPReceiver::ReceivedPacket> UDPReceiver::receive(int timeout_ms)
@@ -365,11 +447,13 @@ vector<UDPReceiver::ReceivedPacket> UDPReceiver::receive(int timeout_ms)
for (size_t i = 0; i < m_sockets.size(); i++) {
if (fds[i].revents & POLLIN) {
auto p = m_sockets[i].receive(2048); // This is larger than the usual MTU
- ReceivedPacket rp;
- rp.packetdata = move(p.buffer);
- rp.received_from = move(p.address);
- rp.port_received_on = m_sockets[i].getPort();
- received.push_back(move(rp));
+ if (not p.buffer.empty()) {
+ ReceivedPacket rp;
+ rp.packetdata = std::move(p.buffer);
+ rp.received_from = std::move(p.address);
+ rp.port_received_on = m_sockets[i].getPort();
+ received.push_back(std::move(rp));
+ }
}
}
@@ -394,7 +478,7 @@ TCPSocket::~TCPSocket()
TCPSocket::TCPSocket(TCPSocket&& other) :
m_sock(other.m_sock),
- m_remote_address(move(other.m_remote_address))
+ m_remote_address(std::move(other.m_remote_address))
{
if (other.m_sock != -1) {
other.m_sock = -1;
@@ -610,6 +694,37 @@ void TCPSocket::connect(const std::string& hostname, int port, bool nonblock)
}
}
+void TCPSocket::enable_keepalive(int time, int intvl, int probes)
+{
+ if (m_sock == INVALID_SOCKET) {
+ throw std::logic_error("You may not call enable_keepalive on invalid socket");
+ }
+ int optval = 1;
+ auto optlen = sizeof(optval);
+ if (setsockopt(m_sock, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen) < 0) {
+ std::string errstr(strerror(errno));
+ throw std::runtime_error("TCP: Could not set SO_KEEPALIVE: " + errstr);
+ }
+
+ optval = time;
+ if (setsockopt(m_sock, SOL_TCP, TCP_KEEPIDLE, &optval, optlen) < 0) {
+ std::string errstr(strerror(errno));
+ throw std::runtime_error("TCP: Could not set TCP_KEEPIDLE: " + errstr);
+ }
+
+ optval = intvl;
+ if (setsockopt(m_sock, SOL_TCP, TCP_KEEPINTVL, &optval, optlen) < 0) {
+ std::string errstr(strerror(errno));
+ throw std::runtime_error("TCP: Could not set TCP_KEEPINTVL: " + errstr);
+ }
+
+ optval = probes;
+ if (setsockopt(m_sock, SOL_TCP, TCP_KEEPCNT, &optval, optlen) < 0) {
+ std::string errstr(strerror(errno));
+ throw std::runtime_error("TCP: Could not set TCP_KEEPCNT: " + errstr);
+ }
+}
+
void TCPSocket::listen(int port, const string& name)
{
if (m_sock != INVALID_SOCKET) {
@@ -852,22 +967,33 @@ ssize_t TCPClient::recv(void *buffer, size_t length, int flags, int timeout_ms)
reconnect();
}
+ m_last_received_packet_ts = chrono::steady_clock::now();
+
return ret;
}
catch (const TCPSocket::Interrupted&) {
return -1;
}
catch (const TCPSocket::Timeout&) {
+ const auto timeout = chrono::milliseconds(timeout_ms * 5);
+ if (m_last_received_packet_ts.has_value() and
+ chrono::steady_clock::now() - *m_last_received_packet_ts > timeout)
+ {
+ // This is to catch half-closed TCP connections
+ reconnect();
+ }
+
return 0;
}
- return 0;
+ throw std::logic_error("unreachable");
}
void TCPClient::reconnect()
{
TCPSocket newsock;
m_sock = std::move(newsock);
+ m_last_received_packet_ts = nullopt;
m_sock.connect(m_hostname, m_port, true);
}
@@ -875,7 +1001,7 @@ TCPConnection::TCPConnection(TCPSocket&& sock) :
queue(),
m_running(true),
m_sender_thread(),
- m_sock(move(sock))
+ m_sock(std::move(sock))
{
#if MISSING_OWN_ADDR
auto own_addr = m_sock.getOwnAddress();
@@ -938,8 +1064,9 @@ void TCPConnection::process()
}
-TCPDataDispatcher::TCPDataDispatcher(size_t max_queue_size) :
- m_max_queue_size(max_queue_size)
+TCPDataDispatcher::TCPDataDispatcher(size_t max_queue_size, size_t buffers_to_preroll) :
+ m_max_queue_size(max_queue_size),
+ m_buffers_to_preroll(buffers_to_preroll)
{
}
@@ -967,12 +1094,20 @@ void TCPDataDispatcher::write(const vector<uint8_t>& data)
throw runtime_error(m_exception_data);
}
+ auto lock = unique_lock<mutex>(m_mutex);
+
+ if (m_buffers_to_preroll > 0) {
+ m_preroll_queue.push_back(data);
+ if (m_preroll_queue.size() > m_buffers_to_preroll) {
+ m_preroll_queue.pop_front();
+ }
+ }
+
for (auto& connection : m_connections) {
connection.queue.push(data);
}
- m_connections.remove_if(
- [&](const TCPConnection& conn){ return conn.queue.size() > m_max_queue_size; });
+ m_connections.remove_if( [&](const TCPConnection& conn){ return conn.queue.size() > m_max_queue_size; });
}
void TCPDataDispatcher::process()
@@ -984,7 +1119,14 @@ void TCPDataDispatcher::process()
// Add a new TCPConnection to the list, constructing it from the client socket
auto sock = m_listener_socket.accept(timeout_ms);
if (sock.valid()) {
- m_connections.emplace(m_connections.begin(), move(sock));
+ auto lock = unique_lock<mutex>(m_mutex);
+ m_connections.emplace(m_connections.begin(), std::move(sock));
+
+ if (m_buffers_to_preroll > 0) {
+ for (const auto& buf : m_preroll_queue) {
+ m_connections.front().queue.push(buf);
+ }
+ }
}
}
}
@@ -1050,7 +1192,7 @@ void TCPReceiveServer::process()
}
else {
buf.resize(r);
- m_queue.push(make_shared<TCPReceiveMessageData>(move(buf)));
+ m_queue.push(make_shared<TCPReceiveMessageData>(std::move(buf)));
}
}
catch (const TCPSocket::Interrupted&) {
@@ -1091,7 +1233,7 @@ TCPSendClient::~TCPSendClient()
}
}
-void TCPSendClient::sendall(const std::vector<uint8_t>& buffer)
+TCPSendClient::ErrorStats TCPSendClient::sendall(const std::vector<uint8_t>& buffer)
{
if (not m_running) {
throw runtime_error(m_exception_data);
@@ -1103,6 +1245,17 @@ void TCPSendClient::sendall(const std::vector<uint8_t>& buffer)
vector<uint8_t> discard;
m_queue.try_pop(discard);
}
+
+ TCPSendClient::ErrorStats es;
+ es.num_reconnects = m_num_reconnects.load();
+
+ es.has_seen_new_errors = es.num_reconnects != m_num_reconnects_prev;
+ m_num_reconnects_prev = es.num_reconnects;
+
+ auto lock = unique_lock<mutex>(m_error_mutex);
+ es.last_error = m_last_error;
+
+ return es;
}
void TCPSendClient::process()
@@ -1124,12 +1277,16 @@ void TCPSendClient::process()
}
else {
try {
+ m_num_reconnects.fetch_add(1, std::memory_order_seq_cst);
m_sock.connect(m_hostname, m_port);
m_is_connected = true;
}
catch (const runtime_error& e) {
m_is_connected = false;
this_thread::sleep_for(chrono::seconds(1));
+
+ auto lock = unique_lock<mutex>(m_error_mutex);
+ m_last_error = e.what();
}
}
}
diff --git a/lib/Socket.h b/lib/Socket.h
index f5143a0..7709145 100644
--- a/lib/Socket.h
+++ b/lib/Socket.h
@@ -2,7 +2,7 @@
Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009 Her Majesty the
Queen in Right of Canada (Communications Research Center Canada)
- Copyright (C) 2020
+ Copyright (C) 2024
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -31,9 +31,11 @@
#include "ThreadsafeQueue.h"
#include <cstdlib>
#include <atomic>
-#include <iostream>
+#include <chrono>
#include <list>
#include <memory>
+#include <optional>
+#include <string>
#include <thread>
#include <vector>
@@ -111,13 +113,13 @@ class UDPSocket
/** Close the already open socket, and create a new one. Throws a runtime_error on error. */
void reinit(int port);
void reinit(int port, const std::string& name);
+ void init_receive_multicast(int port, const std::string& local_if_addr, const std::string& mcastaddr);
void close(void);
void send(UDPPacket& packet);
void send(const std::vector<uint8_t>& data, InetAddress destination);
void send(const std::string& data, InetAddress destination);
UDPPacket receive(size_t max_size);
- void joinGroup(const char* groupname, const char* if_addr = nullptr);
void setMulticastSource(const char* source_addr);
void setMulticastTTL(int ttl);
@@ -129,9 +131,14 @@ class UDPSocket
SOCKET getNativeSocket() const;
int getPort() const;
+ private:
+ void join_group(const char* groupname, const char* if_addr = nullptr);
+ void post_init();
+
protected:
SOCKET m_sock = INVALID_SOCKET;
int m_port = 0;
+ std::string m_multicast_source = "";
};
/* UDP packet receiver supporting receiving from several ports at once */
@@ -173,6 +180,11 @@ class TCPSocket {
void listen(int port, const std::string& name);
void close(void);
+ /* Enable TCP keepalive. See
+ * https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/usingkeepalive.html
+ */
+ void enable_keepalive(int time, int intvl, int probes);
+
/* throws a runtime_error on failure, an invalid socket on timeout */
TCPSocket accept(int timeout_ms);
@@ -226,6 +238,8 @@ class TCPClient {
TCPSocket m_sock;
std::string m_hostname;
int m_port;
+
+ std::optional<std::chrono::steady_clock::time_point> m_last_received_packet_ts;
};
/* Helper class for TCPDataDispatcher, contains a queue of pending data and
@@ -254,7 +268,7 @@ class TCPConnection
class TCPDataDispatcher
{
public:
- TCPDataDispatcher(size_t max_queue_size);
+ TCPDataDispatcher(size_t max_queue_size, size_t buffers_to_preroll);
~TCPDataDispatcher();
TCPDataDispatcher(const TCPDataDispatcher&) = delete;
TCPDataDispatcher& operator=(const TCPDataDispatcher&) = delete;
@@ -266,11 +280,16 @@ class TCPDataDispatcher
void process();
size_t m_max_queue_size;
+ size_t m_buffers_to_preroll;
+
std::atomic<bool> m_running = ATOMIC_VAR_INIT(false);
std::string m_exception_data;
std::thread m_listener_thread;
TCPSocket m_listener_socket;
+
+ std::mutex m_mutex;
+ std::deque<std::vector<uint8_t> > m_preroll_queue;
std::list<TCPConnection> m_connections;
};
@@ -314,10 +333,18 @@ class TCPSendClient {
public:
TCPSendClient(const std::string& hostname, int port);
~TCPSendClient();
+ TCPSendClient(const TCPSendClient&) = delete;
+ TCPSendClient& operator=(const TCPSendClient&) = delete;
- /* Throws a runtime_error on error
- */
- void sendall(const std::vector<uint8_t>& buffer);
+
+ struct ErrorStats {
+ std::string last_error = "";
+ size_t num_reconnects = 0;
+ bool has_seen_new_errors = false;
+ };
+
+ /* Throws a runtime_error when the process thread isn't running */
+ ErrorStats sendall(const std::vector<uint8_t>& buffer);
private:
void process();
@@ -334,6 +361,11 @@ class TCPSendClient {
std::string m_exception_data;
std::thread m_sender_thread;
TCPSocket m_listener_socket;
+
+ std::atomic<size_t> m_num_reconnects = ATOMIC_VAR_INIT(0);
+ size_t m_num_reconnects_prev = 0;
+ std::mutex m_error_mutex;
+ std::string m_last_error = "";
};
}
diff --git a/lib/ThreadsafeQueue.h b/lib/ThreadsafeQueue.h
index 815dfe0..8b385d6 100644
--- a/lib/ThreadsafeQueue.h
+++ b/lib/ThreadsafeQueue.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
An implementation for a threadsafe queue, depends on C++11
@@ -32,6 +32,7 @@
#include <condition_variable>
#include <queue>
#include <utility>
+#include <cassert>
/* This queue is meant to be used by two threads. One producer
* that pushes elements into the queue, and one consumer that
@@ -69,7 +70,6 @@ public:
}
size_t queue_size = the_queue.size();
lock.unlock();
-
the_rx_notification.notify_one();
return queue_size;
@@ -93,11 +93,57 @@ public:
return queue_size;
}
+ struct push_overflow_result { bool overflowed; size_t new_size; };
+
+ /* Push one element into the queue, and if queue is
+ * full remove one element from the other end.
+ *
+ * max_size == 0 is not allowed.
+ *
+ * returns the new queue size and a flag if overflow occurred.
+ */
+ push_overflow_result push_overflow(T const& val, size_t max_size)
+ {
+ assert(max_size > 0);
+ std::unique_lock<std::mutex> lock(the_mutex);
+
+ bool overflow = false;
+ while (the_queue.size() >= max_size) {
+ overflow = true;
+ the_queue.pop();
+ }
+ the_queue.push(val);
+ const size_t queue_size = the_queue.size();
+ lock.unlock();
+
+ the_rx_notification.notify_one();
+
+ return {overflow, queue_size};
+ }
+
+ push_overflow_result push_overflow(T&& val, size_t max_size)
+ {
+ assert(max_size > 0);
+ std::unique_lock<std::mutex> lock(the_mutex);
+
+ bool overflow = false;
+ while (the_queue.size() >= max_size) {
+ overflow = true;
+ the_queue.pop();
+ }
+ the_queue.emplace(std::move(val));
+ const size_t queue_size = the_queue.size();
+ lock.unlock();
+
+ the_rx_notification.notify_one();
+
+ return {overflow, queue_size};
+ }
+
+
/* Push one element into the queue, but wait until the
* queue size goes below the threshold.
*
- * Notify waiting thread.
- *
* returns the new queue size.
*/
size_t push_wait_if_full(T const& val, size_t threshold)
diff --git a/lib/edi/ETIDecoder.cpp b/lib/edi/ETIDecoder.cpp
index 0a4da54..1a726cf 100644
--- a/lib/edi/ETIDecoder.cpp
+++ b/lib/edi/ETIDecoder.cpp
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2020
+ Copyright (C) 2024
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -44,7 +44,7 @@ ETIDecoder::ETIDecoder(ETIDataCollector& data_collector) :
std::bind(&ETIDecoder::decode_estn, this, _1, _2));
m_dispatcher.register_tag("*dmy",
std::bind(&ETIDecoder::decode_stardmy, this, _1, _2));
- m_dispatcher.register_tagpacket_handler(std::bind(&ETIDecoder::decode_tagpacket, this, _1));
+ m_dispatcher.register_afpacket_handler(std::bind(&ETIDecoder::decode_afpacket, this, _1));
}
void ETIDecoder::set_verbose(bool verbose)
@@ -174,7 +174,7 @@ bool ETIDecoder::decode_deti(const std::vector<uint8_t>& value, const tag_name_t
fic.begin());
i += fic_length;
- m_data_collector.update_fic(move(fic));
+ m_data_collector.update_fic(std::move(fic));
}
if (rfudf) {
@@ -215,7 +215,7 @@ bool ETIDecoder::decode_estn(const std::vector<uint8_t>& value, const tag_name_t
value.end(),
back_inserter(stc.mst));
- m_data_collector.add_subchannel(move(stc));
+ m_data_collector.add_subchannel(std::move(stc));
return true;
}
@@ -225,9 +225,9 @@ bool ETIDecoder::decode_stardmy(const std::vector<uint8_t>&, const tag_name_t&)
return true;
}
-bool ETIDecoder::decode_tagpacket(const std::vector<uint8_t>& value)
+bool ETIDecoder::decode_afpacket(std::vector<uint8_t>&& value)
{
- m_received_tagpacket.tagpacket = value;
+ m_received_tagpacket.afpacket = std::move(value);
return true;
}
@@ -237,7 +237,7 @@ void ETIDecoder::packet_completed()
ReceivedTagPacket tp;
swap(tp, m_received_tagpacket);
- m_data_collector.assemble(move(tp));
+ m_data_collector.assemble(std::move(tp));
}
}
diff --git a/lib/edi/ETIDecoder.hpp b/lib/edi/ETIDecoder.hpp
index 3949a14..1ad6c64 100644
--- a/lib/edi/ETIDecoder.hpp
+++ b/lib/edi/ETIDecoder.hpp
@@ -58,7 +58,7 @@ struct eti_stc_data {
};
struct ReceivedTagPacket {
- std::vector<uint8_t> tagpacket;
+ std::vector<uint8_t> afpacket;
frame_timestamp_t timestamp;
seq_info_t seq;
};
@@ -133,7 +133,7 @@ class ETIDecoder {
bool decode_estn(const std::vector<uint8_t>& value, const tag_name_t& n);
bool decode_stardmy(const std::vector<uint8_t>& value, const tag_name_t& n);
- bool decode_tagpacket(const std::vector<uint8_t>& value);
+ bool decode_afpacket(std::vector<uint8_t>&& value);
void packet_completed();
diff --git a/lib/edi/common.cpp b/lib/edi/common.cpp
index c99997a..38eadf9 100644
--- a/lib/edi/common.cpp
+++ b/lib/edi/common.cpp
@@ -33,9 +33,9 @@ namespace EdiDecoder {
using namespace std;
-bool frame_timestamp_t::valid() const
+bool frame_timestamp_t::is_valid() const
{
- return tsta != 0xFFFFFF;
+ return tsta != 0xFFFFFF and seconds != 0;
}
string frame_timestamp_t::to_string() const
@@ -43,7 +43,7 @@ string frame_timestamp_t::to_string() const
const time_t seconds_in_unix_epoch = to_unix_epoch();
stringstream ss;
- if (valid()) {
+ if (is_valid()) {
ss << "Timestamp: ";
}
else {
@@ -129,10 +129,9 @@ std::string tag_name_to_human_readable(const tag_name_t& name)
return s;
}
-TagDispatcher::TagDispatcher(
- std::function<void()>&& af_packet_completed) :
- m_af_packet_completed(move(af_packet_completed)),
- m_tagpacket_handler([](const std::vector<uint8_t>& /*ignore*/){})
+TagDispatcher::TagDispatcher(std::function<void()>&& af_packet_completed) :
+ m_af_packet_completed(std::move(af_packet_completed)),
+ m_afpacket_handler([](std::vector<uint8_t>&& /*ignore*/){})
{
}
@@ -278,7 +277,6 @@ void TagDispatcher::setMaxDelay(int num_af_packets)
}
-#define AFPACKET_HEADER_LEN 10 // includes SYNC
TagDispatcher::decode_result_t TagDispatcher::decode_afpacket(
const std::vector<uint8_t> &input_data)
{
@@ -341,25 +339,30 @@ TagDispatcher::decode_result_t TagDispatcher::decode_afpacket(
return {decode_state_e::Error, AFPACKET_HEADER_LEN + taglength + crclen};
}
else {
+ vector<uint8_t> afpacket(AFPACKET_HEADER_LEN + taglength + crclen);
+ copy(input_data.begin(),
+ input_data.begin() + AFPACKET_HEADER_LEN + taglength + crclen,
+ afpacket.begin());
+ m_afpacket_handler(std::move(afpacket));
+
vector<uint8_t> payload(taglength);
copy(input_data.begin() + AFPACKET_HEADER_LEN,
input_data.begin() + AFPACKET_HEADER_LEN + taglength,
payload.begin());
- return {
- decode_tagpacket(payload) ? decode_state_e::Ok : decode_state_e::Error,
- AFPACKET_HEADER_LEN + taglength + crclen};
+ auto result = decode_tagpacket(payload) ? decode_state_e::Ok : decode_state_e::Error;
+ return {result, AFPACKET_HEADER_LEN + taglength + crclen};
}
}
void TagDispatcher::register_tag(const std::string& tag, tag_handler&& h)
{
- m_handlers[tag] = move(h);
+ m_handlers[tag] = std::move(h);
}
-void TagDispatcher::register_tagpacket_handler(tagpacket_handler&& h)
+void TagDispatcher::register_afpacket_handler(afpacket_handler&& h)
{
- m_tagpacket_handler = move(h);
+ m_afpacket_handler = std::move(h);
}
@@ -428,8 +431,6 @@ bool TagDispatcher::decode_tagpacket(const vector<uint8_t> &payload)
}
}
- m_tagpacket_handler(payload);
-
return success;
}
diff --git a/lib/edi/common.hpp b/lib/edi/common.hpp
index c3e6c40..fdd7424 100644
--- a/lib/edi/common.hpp
+++ b/lib/edi/common.hpp
@@ -32,12 +32,14 @@
namespace EdiDecoder {
+constexpr size_t AFPACKET_HEADER_LEN = 10; // includes SYNC
+
struct frame_timestamp_t {
uint32_t seconds = 0;
uint32_t utco = 0;
uint32_t tsta = 0xFFFFFF; // According to EN 300 797 Annex B
- bool valid() const;
+ bool is_valid() const;
std::string to_string() const;
std::time_t to_unix_epoch() const;
std::chrono::system_clock::time_point to_system_clock() const;
@@ -133,9 +135,9 @@ class TagDispatcher {
*/
void register_tag(const std::string& tag, tag_handler&& h);
- /* The complete tagpacket can also be retrieved */
- using tagpacket_handler = std::function<void(const std::vector<uint8_t>&)>;
- void register_tagpacket_handler(tagpacket_handler&& h);
+ /* The complete AF packet can also be retrieved */
+ using afpacket_handler = std::function<void(std::vector<uint8_t>&&)>;
+ void register_afpacket_handler(afpacket_handler&& h);
seq_info_t get_seq_info() const {
return m_last_sequences;
@@ -160,7 +162,7 @@ class TagDispatcher {
std::vector<uint8_t> m_input_data;
std::map<std::string, tag_handler> m_handlers;
std::function<void()> m_af_packet_completed;
- tagpacket_handler m_tagpacket_handler;
+ afpacket_handler m_afpacket_handler;
std::vector<std::string> m_ignored_tags;
};
diff --git a/m4/ax_cxx_compile_stdcxx.m4 b/m4/ax_cxx_compile_stdcxx.m4
index 43087b2..8edf515 100644
--- a/m4/ax_cxx_compile_stdcxx.m4
+++ b/m4/ax_cxx_compile_stdcxx.m4
@@ -10,13 +10,13 @@
#
# Check for baseline language coverage in the compiler for the specified
# version of the C++ standard. If necessary, add switches to CXX and
-# CXXCPP to enable support. VERSION may be '11' (for the C++11 standard)
-# or '14' (for the C++14 standard).
+# CXXCPP to enable support. VERSION may be '11', '14', '17', or '20' for
+# the respective C++ standard version.
#
# The second argument, if specified, indicates whether you insist on an
# extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g.
# -std=c++11). If neither is specified, you get whatever works, with
-# preference for an extended mode.
+# preference for no added switch, and then for an extended mode.
#
# The third argument, if specified 'mandatory' or if left unspecified,
# indicates that baseline support for the specified C++ standard is
@@ -35,13 +35,15 @@
# Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
# Copyright (c) 2016, 2018 Krzesimir Nowak <qdlacz@gmail.com>
# Copyright (c) 2019 Enji Cooper <yaneurabeya@gmail.com>
+# Copyright (c) 2020 Jason Merrill <jason@redhat.com>
+# Copyright (c) 2021 Jörn Heusipp <osmanx@problemloesungsmaschine.de>
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice
# and this notice are preserved. This file is offered as-is, without any
# warranty.
-#serial 11
+#serial 18
dnl This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro
dnl (serial version number 13).
@@ -50,6 +52,7 @@ AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl
m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"],
[$1], [14], [ax_cxx_compile_alternatives="14 1y"],
[$1], [17], [ax_cxx_compile_alternatives="17 1z"],
+ [$1], [20], [ax_cxx_compile_alternatives="20"],
[m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl
m4_if([$2], [], [],
[$2], [ext], [],
@@ -62,6 +65,16 @@ AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl
AC_LANG_PUSH([C++])dnl
ac_success=no
+ m4_if([$2], [], [dnl
+ AC_CACHE_CHECK(whether $CXX supports C++$1 features by default,
+ ax_cv_cxx_compile_cxx$1,
+ [AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+ [ax_cv_cxx_compile_cxx$1=yes],
+ [ax_cv_cxx_compile_cxx$1=no])])
+ if test x$ax_cv_cxx_compile_cxx$1 = xyes; then
+ ac_success=yes
+ fi])
+
m4_if([$2], [noext], [], [dnl
if test x$ac_success = xno; then
for alternative in ${ax_cxx_compile_alternatives}; do
@@ -91,9 +104,18 @@ AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl
dnl HP's aCC needs +std=c++11 according to:
dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf
dnl Cray's crayCC needs "-h std=c++11"
+ dnl MSVC needs -std:c++NN for C++17 and later (default is C++14)
for alternative in ${ax_cxx_compile_alternatives}; do
- for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do
- cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+ for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}" MSVC; do
+ if test x"$switch" = xMSVC; then
+ dnl AS_TR_SH maps both `:` and `=` to `_` so -std:c++17 would collide
+ dnl with -std=c++17. We suffix the cache variable name with _MSVC to
+ dnl avoid this.
+ switch=-std:c++${alternative}
+ cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_${switch}_MSVC])
+ else
+ cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+ fi
AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
$cachevar,
[ac_save_CXX="$CXX"
@@ -140,7 +162,6 @@ m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11],
_AX_CXX_COMPILE_STDCXX_testbody_new_in_11
)
-
dnl Test body for checking C++14 support
m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14],
@@ -148,12 +169,24 @@ m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14],
_AX_CXX_COMPILE_STDCXX_testbody_new_in_14
)
+dnl Test body for checking C++17 support
+
m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17],
_AX_CXX_COMPILE_STDCXX_testbody_new_in_11
_AX_CXX_COMPILE_STDCXX_testbody_new_in_14
_AX_CXX_COMPILE_STDCXX_testbody_new_in_17
)
+dnl Test body for checking C++20 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_20],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_17
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_20
+)
+
+
dnl Tests for new features in C++11
m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[
@@ -165,7 +198,11 @@ m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[
#error "This is not a C++ compiler"
-#elif __cplusplus < 201103L
+// MSVC always sets __cplusplus to 199711L in older versions; newer versions
+// only set it correctly if /Zc:__cplusplus is specified as well as a
+// /std:c++NN switch:
+// https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/
+#elif __cplusplus < 201103L && !defined _MSC_VER
#error "This is not a C++11 compiler"
@@ -456,7 +493,7 @@ m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[
#error "This is not a C++ compiler"
-#elif __cplusplus < 201402L
+#elif __cplusplus < 201402L && !defined _MSC_VER
#error "This is not a C++14 compiler"
@@ -580,7 +617,7 @@ m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[
#error "This is not a C++ compiler"
-#elif __cplusplus < 201703L
+#elif __cplusplus < 201703L && !defined _MSC_VER
#error "This is not a C++17 compiler"
@@ -946,6 +983,36 @@ namespace cxx17
} // namespace cxx17
-#endif // __cplusplus < 201703L
+#endif // __cplusplus < 201703L && !defined _MSC_VER
+
+]])
+
+
+dnl Tests for new features in C++20
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_20], [[
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 202002L && !defined _MSC_VER
+
+#error "This is not a C++20 compiler"
+
+#else
+
+#include <version>
+
+namespace cxx20
+{
+
+// As C++20 supports feature test macros in the standard, there is no
+// immediate need to actually test for feature availability on the
+// Autoconf side.
+
+} // namespace cxx20
+
+#endif // __cplusplus < 202002L && !defined _MSC_VER
]])
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();