diff options
124 files changed, 10220 insertions, 2220 deletions
@@ -15,6 +15,9 @@ config.h config.h.in config.status odr-dabmod +*~ + +*.iq __pycache__/ *.py[cod] @@ -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. @@ -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 @@ -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/ @@ -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. + +[](https://travis-ci.org/MikeLankamp/fpm) +[](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 [](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; +} @@ -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(); |