From 2ecb9ee3e97ce02a84e180245604a97d85a86a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oona=20R=C3=A4is=C3=A4nen?= Date: Fri, 12 Jul 2024 10:59:37 +0300 Subject: [PATCH] Maintainability & housekeeping * use meson for finding deps & building * remove unmaintained macro-based build options * liquid-dsp and sndfile are now actual dependencies * add .clang-format * add build check pipeline --- .clang-format | 18 ++ .github/workflows/build.yml | 86 ++++++++ CHANGES.md | 14 +- Makefile.am | 2 - README.md | 52 ++--- autogen.sh | 2 - configure.ac | 51 ----- meson.build | 65 ++++++ src/Makefile.am | 6 - src/deinvert.cc | 426 ++++++------------------------------ src/deinvert.h | 130 ++--------- src/io.h | 158 +++++++++++++ src/liquid_wrappers.cc | 17 +- src/liquid_wrappers.h | 20 +- src/wdsp.cc | 66 ------ src/wdsp.h | 50 ----- test/test.pl | 109 +++++++++ 17 files changed, 572 insertions(+), 700 deletions(-) create mode 100644 .clang-format create mode 100644 .github/workflows/build.yml delete mode 100644 Makefile.am delete mode 100755 autogen.sh delete mode 100644 configure.ac create mode 100644 meson.build delete mode 100644 src/Makefile.am create mode 100644 src/io.h delete mode 100644 src/wdsp.cc delete mode 100644 src/wdsp.h create mode 100644 test/test.pl diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..e9fbef8 --- /dev/null +++ b/.clang-format @@ -0,0 +1,18 @@ +--- +BasedOnStyle: Google +IndentWidth: 2 +--- +Language: Cpp +Standard: c++14 +ColumnLimit: 100 +AllowShortIfStatementsOnASingleLine: false +AlignArrayOfStructures: Left +AlignConsecutiveAssignments: Consecutive +AllowShortFunctionsOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: true +AlignConsecutiveShortCaseStatements: + Enabled: true + AcrossEmptyLines: true +AlignConsecutiveDeclarations: + Enabled: true +IncludeBlocks: Preserve diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d457e7a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,86 @@ +name: build + +on: + push: + branches: [ master, dev ] + tags: [ 'v*' ] + pull_request: + branches: [ master ] + +jobs: + build-ubuntu-22-04: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + - name: Install dependencies (apt) + run: sudo apt install python3-pip ninja-build libsndfile1-dev libliquid-dev + - name: Install meson (pip3) + run: pip3 install --user meson + - name: Compile via makefile + run: make + - name: meson setup + run: meson setup -Dwerror=true build + - name: meson compile + run: cd build && meson compile + + build-ubuntu-20-04: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v4 + - name: Install dependencies (apt) + run: sudo apt install python3-pip ninja-build libsndfile1-dev libliquid-dev + - name: Install meson (pip3) + run: pip3 install --user meson + - name: Compile via makefile + run: make + - name: meson setup + run: meson setup -Dwerror=true build + - name: meson compile + run: cd build && meson compile + + build-debian-oldoldstable: + runs-on: ubuntu-latest + container: debian:buster + + steps: + - uses: actions/checkout@v4 + - name: Install dependencies (apt-get) + run: apt-get update && apt-get -y install python3-pip ninja-build build-essential libsndfile1-dev libliquid-dev + - name: Install meson (pip3) + run: pip3 install --user meson + - name: Compile via makefile + run: make + - name: meson setup + run: export PATH=$PATH:$HOME/.local/bin && meson setup -Dwerror=true build + - name: meson compile + run: export PATH=$PATH:$HOME/.local/bin && cd build && meson compile + + build-macos: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + - name: Install dependencies (brew) + run: brew install meson libsndfile liquid-dsp + - name: Compile via makefile + run: make + - name: meson setup + run: meson setup -Dwerror=true build + - name: meson compile + run: cd build && meson compile + + test: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + - name: Install dependencies (apt) + run: sudo apt install meson libsndfile1-dev libliquid-dev perl sox + - name: meson setup + run: meson setup -Dwerror=true build + - name: meson compile + run: cd build && meson compile + - name: test + run: cd test && perl test.pl diff --git a/CHANGES.md b/CHANGES.md index b944c2c..9cd3f40 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,18 @@ # deinvert changelog +## 1.0 (2024) + +* Maintenance release; no new features +* Long-term maintainability: + * Add basic end-to-end tests, .clang-format, and build check pipelines + * Use meson (instead of autotools - which stopped working for me) to find deps & build + * Remove unmaintained macro-based build options + * liquid-dsp and sndfile are now actual dependencies +* Fixes: + * Default-initialize buffers +* Documentation: + * There is a [wiki](https://github.com/windytan/deinvert/wiki) and more (in-)security reminders + ## 0.3 (2018-05-31) * Reduce artifacts from DC removal filter at startup @@ -8,4 +21,3 @@ ## 0.2 (2018-01-03) * Add DC removal filter to fix 'beeping' issues - diff --git a/Makefile.am b/Makefile.am deleted file mode 100644 index c6231c2..0000000 --- a/Makefile.am +++ /dev/null @@ -1,2 +0,0 @@ -SUBDIRS = src -dist_doc_DATA = README.md diff --git a/README.md b/README.md index 6b753f2..3fb9a2d 100644 --- a/README.md +++ b/README.md @@ -11,28 +11,28 @@ instructions and examples. ## Prerequisites -By default, deinvert requires liquid-dsp, libsndfile, and GNU autotools. On -Ubuntu, these can be installed like so: +deinvert requires liquid-dsp, libsndfile, and meson. - sudo apt install libsndfile1-dev libliquid-dev automake +On Ubuntu, these can be installed like so: + + sudo apt install libsndfile1-dev libliquid-dev meson + +On older Debians: + + sudo apt-get install python3-pip ninja-build build-essential libsndfile1-dev libliquid-dev + pip3 install --user meson sudo ldconfig On macOS I recommend using [homebrew](https://brew.sh/): xcode-select --install - brew install libsndfile liquid-dsp automake - -But deinvert can be compiled without liquid-dsp using the configure -option `--without-liquid`; filtering will be disabled and the result will not -sound as good. It can also be compiled -without libsndfile using the configure option `--without-sndfile`; WAV -support will be disabled, only raw input/output will work. + brew install libsndfile liquid-dsp meson ## Compiling - ./autogen.sh - ./configure [--without-liquid] [--without-sndfile] - make + meson setup build + cd build + meson compile ## Usage @@ -47,19 +47,19 @@ and outputs in the same format via stdout. The inversion carrier defaults to (De)scrambling a WAV file with setting 4: - ./src/deinvert -i input.wav -o output.wav -p 4 + ./build/deinvert -i input.wav -o output.wav -p 4 ### Split-band inversion (De)scrambling split-band inversion with a bandwidth of 3500 Hz, split at 1200 Hz: - ./src/deinvert -i input.wav -o output.wav -f 3500 -s 1200 + ./build/deinvert -i input.wav -o output.wav -f 3500 -s 1200 ### Invert a live signal from RTL-SDR Descrambling a live FM channel at 27 Megahertz from an RTL-SDR, setting 4: - rtl_fm -M fm -f 27.0M -s 12k -g 50 -l 70 | ./src/deinvert -r 12000 -p 4 |\ + rtl_fm -M fm -f 27.0M -s 12k -g 50 -l 70 | ./build/deinvert -r 12000 -p 4 |\ play -r 12k -c 1 -t .s16 - ### Invert a live signal from Gqrx (requires netcat) @@ -70,12 +70,12 @@ Descrambling a live FM channel at 27 Megahertz from an RTL-SDR, setting 4: 4. In the Audio window, enable UDP. 5. Run this command in a terminal window: - nc -u -l localhost 12345 | ./src/deinvert -r 48000 | play -r 48k -c 1 -t .s16 - + nc -u -l localhost 12345 | ./build/deinvert -r 48000 | play -r 48k -c 1 -t .s16 - ### Full options - ./src/deinvert [OPTIONS] + ./build/deinvert [OPTIONS] -f, --frequency FREQ Frequency of the inversion carrier, in Hertz. @@ -86,6 +86,7 @@ Descrambling a live FM channel at 27 Megahertz from an RTL-SDR, setting 4: -o, --output-file FILE Write output to a WAV file instead of stdout. An existing file will be overwritten. + The input sample rate will be used. -p, --preset NUM Scrambler frequency preset (1-8), referring to the set of common carrier frequencies used by @@ -116,18 +117,11 @@ Descrambling a live FM channel at 27 Megahertz from an RTL-SDR, setting 4: ## Troubleshooting -### Can't find liquid-dsp on macOS - -If you've installed liquid-dsp yet `configure` can't find it, it's possible that -XCode command line tools aren't installed. Run this command to fix it: +### I can't understand the speech even after deinverting - xcode-select --install - -### Can't find liquid-dsp on Linux - -Try running this in the terminal: - - sudo ldconfig +In this case, the sample is probably not frequency inversion scrambled. +It's very rare to encounter frequency inversion scrambling nowadays. See the +[wiki](https://github.com/windytan/deinvert/wiki) for details. ### I hear a high-pitched beep in the result diff --git a/autogen.sh b/autogen.sh deleted file mode 100755 index 6976ccd..0000000 --- a/autogen.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -autoreconf --install || exit 1 diff --git a/configure.ac b/configure.ac deleted file mode 100644 index 043aa43..0000000 --- a/configure.ac +++ /dev/null @@ -1,51 +0,0 @@ -AC_PREREQ([2.69]) -AC_INIT([deinvert], [0.3], [oona@kapsi.fi], [deinvert], - [https://github.com/windytan/deinvert]) -AM_INIT_AUTOMAKE([1.10 -Wall -Werror foreign subdir-objects]) -AC_CONFIG_HEADERS([config.h]) -AC_CONFIG_FILES([ - Makefile - src/Makefile -]) - -AC_ARG_WITH([liquid], - [AS_HELP_STRING([--without-liquid], - [disable support for demodulation using liquid-dsp])]) - -AC_ARG_WITH([sndfile], - [AS_HELP_STRING([--without-sndfile], - [disable support for reading audio files via libsndfile])]) - -LIQUID= -AS_IF([test "x$with_liquid" != xno], - [AC_CHECK_LIB([liquid], [nco_crcf_mix_up], - [AC_SUBST([LIQUID], ["-lliquid"]) - AC_DEFINE([HAVE_LIQUID], [1], - [Define if you have liquid]) - ], - [AC_MSG_FAILURE( - [Could not find liquid-dsp (use --without-liquid to disable)])])]) - -MACPORTS_LD= -AC_CHECK_FILE(/opt/local/lib, - [AC_SUBST([MACPORTS_LD], ["-L/opt/local/lib"])]) - -MACPORTS_CF= -AC_CHECK_FILE(/opt/local/lib, - [AC_SUBST([MACPORTS_CF], ["-I/opt/local/include"])]) - -SNDFILE= -AS_IF([test "x$with_sndfile" != xno], - [AC_CHECK_LIB([sndfile], [main], - [AC_SUBST([SNDFILE], ["-lsndfile"]) - AC_DEFINE([HAVE_SNDFILE], [1], - [Define if you have libsndfile]) - ], - [AC_MSG_FAILURE( - [Could not find libsndfile (use --without-sndfile to disable)])])]) - -AC_PROG_CXX -AC_PROG_RANLIB -AC_LANG([C++]) - -AC_OUTPUT diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..b0138a8 --- /dev/null +++ b/meson.build @@ -0,0 +1,65 @@ +project( + 'deinvert', + 'cpp', + default_options: ['warning_level=3', 'buildtype=release', 'optimization=3'], + version: '1.0', +) + +# Store version number to be compiled in +conf = configuration_data() +conf.set_quoted('VERSION', meson.project_version()) +configure_file(output: 'config.h', configuration: conf) + +######################## +### Compiler options ### +######################## + +cc = meson.get_compiler('cpp') +add_project_arguments(cc.get_supported_arguments(['-Wno-unknown-pragmas']), language: 'cpp') + +# We want to use M_PI on Windows +if build_machine.system() == 'windows' + add_project_arguments('-D_USE_MATH_DEFINES=1', language: 'cpp') +endif + +# Explicit GNU extensions on Cygwin +if build_machine.system() == 'cygwin' + add_project_arguments('-std=gnu++14', language: 'cpp') +else + add_project_arguments('-std=c++14', language: 'cpp') +endif + +#################### +### Dependencies ### +#################### + +# Find libsndfile +sndfile = dependency('sndfile') + +# Find liquid-dsp +if build_machine.system() == 'darwin' + fs = import('fs') + # Homebrew system + if fs.is_dir('/opt/homebrew/lib') + liquid_lib = cc.find_library('liquid', dirs: ['/opt/homebrew/lib']) + liquid_inc = include_directories('/opt/homebrew/include') + # MacPorts system + else + liquid_lib = cc.find_library('liquid', dirs: ['/opt/local/lib']) + liquid_inc = include_directories('/opt/local/include') + endif + liquid = declare_dependency(dependencies: liquid_lib, include_directories: liquid_inc) +else + liquid = cc.find_library('liquid') +endif + +############################ +### Sources & Executable ### +############################ + +sources = [ + 'src/deinvert.cc', + 'src/liquid_wrappers.cc', +] + +executable('deinvert', sources, dependencies: [liquid, sndfile], install: true) diff --git a/src/Makefile.am b/src/Makefile.am deleted file mode 100644 index 3ffb8ec..0000000 --- a/src/Makefile.am +++ /dev/null @@ -1,6 +0,0 @@ -bin_PROGRAMS = deinvert -deinvert_CPPFLAGS = -std=c++11 -Wall -Wextra -Wstrict-overflow -Wshadow \ - $(MACPORTS_CF) \ - -Wuninitialized -pedantic $(RFLAGS) -deinvert_LDADD = $(MACPORTS_LD) -lc $(LIQUID) $(SNDFILE) -deinvert_SOURCES = deinvert.cc liquid_wrappers.cc wdsp.cc diff --git a/src/deinvert.cc b/src/deinvert.cc index c41b367..2153612 100644 --- a/src/deinvert.cc +++ b/src/deinvert.cc @@ -17,346 +17,77 @@ */ #include "src/deinvert.h" -#include +#include +#include #include #include #include +#include #include +#include -#include "config.h" +#include "src/io.h" #include "src/liquid_wrappers.h" -#include "src/wdsp.h" +#include "src/options.h" namespace deinvert { namespace { -#ifdef HAVE_LIQUID -const int kMaxFilterLength = 2047; +constexpr int kMaxFilterLength = 2047; int FilterLengthInSamples(float len_seconds, float samplerate) { - int filter_length = 2 * std::round(samplerate * - len_seconds) + 1; - filter_length = filter_length < deinvert::kMaxFilterLength ? - filter_length : deinvert::kMaxFilterLength; + const int filter_length = + std::min(2 * static_cast(std::round(samplerate * len_seconds)) + 1, kMaxFilterLength); return filter_length; } -#endif } // namespace -void PrintUsage() { - std::cout << - "deinvert [OPTIONS]\n" - "\n" - "-f, --frequency FREQ Frequency of the inversion carrier, in Hertz.\n" - "\n" - "-h, --help Display this usage help.\n" - "\n" - "-i, --input-file FILE Use an audio file as input. All formats\n" - " supported by libsndfile should work.\n" - "\n" - "-o, --output-file FILE Write output to a WAV file instead of stdout. An\n" - " existing file will be overwritten.\n" - "\n" - "-p, --preset NUM Scrambler frequency preset (1-8), referring to\n" - " the set of common carrier frequencies used by\n" - " e.g. the Selectone ST-20B scrambler.\n" - "\n" - "-q, --quality NUM Filter quality, from 0 (worst and fastest) to\n" - " 3 (best and slowest). The default is 2.\n" - "\n" - "-r, --samplerate RATE Sampling rate of raw input audio, in Hertz.\n" - "\n" - "-s, --split-frequency Split point for split-band inversion, in Hertz.\n" - "\n" - "-v, --version Display version string.\n"; -} - -void PrintVersion() { -#ifdef DEBUG - std::cout << PACKAGE_STRING << "-debug by OH2EIQ" << std::endl; -#else - std::cout << PACKAGE_STRING << " by OH2EIQ" << std::endl; -#endif -} - -Options GetOptions(int argc, char** argv) { - deinvert::Options options; - - static struct option long_options[] = { - { "frequency", no_argument, 0, 'f'}, - { "preset", 1, 0, 'p'}, - { "input-file", 1, 0, 'i'}, - { "help", no_argument, 0, 'h'}, - { "nofilter", no_argument, 0, 'n'}, - { "output-file", 1, 0, 'o'}, - { "quality", 1, 0, 'q'}, - { "samplerate", 1, 0, 'r'}, - { "split-frequency", 1, 0, 's'}, - { "version", no_argument, 0, 'v'}, - {0, 0, 0, 0}}; - - static const std::vector selectone_carriers({ - 2632.f, 2718.f, 2868.f, 3023.f, 3196.f, 3339.f, 3495.f, 3729.f - }); - - options.frequency_hi = selectone_carriers.at(0); - -#ifdef HAVE_LIQUID - options.quality = 2; -#else - options.quality = 0; -#endif - - int option_index = 0; - int option_char; - int selectone_num; - bool samplerate_set = false; - bool carrier_frequency_set = false; - bool carrier_preset_set = false; - - while ((option_char = getopt_long(argc, argv, "f:hi:no:p:q:r:s:v", - long_options, - &option_index)) >= 0) { - switch (option_char) { - case 'i': -#ifdef HAVE_SNDFILE - options.infilename = std::string(optarg); - options.input_type = deinvert::InputType::sndfile; -#else - throw std::runtime_error("deinvert was compiled without libsndfile"); -#endif - break; - case 'f': - options.frequency_hi = std::atoi(optarg); - carrier_frequency_set = true; - break; - case 'n': - options.quality = 0; - break; - case 'o': -#ifdef HAVE_SNDFILE - options.output_type = deinvert::OutputType::wavfile; - options.outfilename = std::string(optarg); -#else - throw std::runtime_error("deinvert was compiled without libsndfile"); -#endif - break; - case 'p': - selectone_num = std::atoi(optarg); - carrier_preset_set = true; - if (selectone_num >= 1 && - selectone_num <= 8) - options.frequency_hi = selectone_carriers.at(selectone_num - 1); - else - throw - std::runtime_error("preset should be a number from 1 to 8"); - break; - case 'q': -#ifdef HAVE_LIQUID - options.quality = std::atoi(optarg); - if (options.quality < 0 || options.quality > 3) - throw std::runtime_error("please specify filter quality from 0 to 3"); -#else - std::cerr << "warning: deinvert was built without liquid-dsp, " - << "filtering disabled" << std::endl; -#endif - break; - case 'r': - options.samplerate = std::atoi(optarg); - samplerate_set = true; - break; - case 's': - options.frequency_lo = std::atoi(optarg); - options.is_split_band = true; - break; - case 'v': - PrintVersion(); - options.just_exit = true; - break; - case 'h': - default: - PrintUsage(); - options.just_exit = true; - break; - } - if (options.just_exit) - break; - } - - if (!carrier_preset_set && !carrier_frequency_set) - std::cerr << "deinvert: warning: carrier frequency not set, trying " - << "2632 Hz\n"; - - if (options.input_type == deinvert::InputType::stdin && !samplerate_set) - std::cerr << "deinvert: warning: sample rate not set, trying 44100 Hz\n"; - - if (options.is_split_band && options.frequency_lo >= options.frequency_hi) - throw - std::runtime_error("split point must be below the inversion carrier"); - - if (options.samplerate < options.frequency_hi * 2.0f) - throw std::runtime_error( - "sample rate must be at least twice the inversion frequency"); - - return options; -} - -AudioReader::~AudioReader() { -} - -bool AudioReader::eof() const { - return is_eof_; -} - -StdinReader::StdinReader(const Options& options) : - samplerate_(options.samplerate) { - is_eof_ = false; -} - -StdinReader::~StdinReader() { -} - -std::vector StdinReader::ReadBlock() { - int num_read = - fread(buffer_.data(), sizeof(buffer_[0]), kIOBufferSize, stdin); - - if (num_read < kIOBufferSize) - is_eof_ = true; - - std::vector result(num_read); - for (int i = 0; i < num_read; i++) - result[i] = buffer_[i] * (1.f / 32768.f); - - return result; -} +DCRemover::DCRemover(size_t length) : buffer_(length) {} -float StdinReader::samplerate() const { - return samplerate_; -} +void DCRemover::push(float sample) { + if (buffer_.size() > 0) { + buffer_[index_] = sample; + index_ = (index_ + 1) % buffer_.size(); -#ifdef HAVE_SNDFILE -SndfileReader::SndfileReader(const Options& options) : - info_({0, 0, 0, 0, 0, 0}), - file_(sf_open(options.infilename.c_str(), SFM_READ, &info_)) { - is_eof_ = false; - if (file_ == nullptr) { - throw std::runtime_error(options.infilename + ": " + sf_strerror(nullptr)); - } else if (info_.samplerate < options.frequency_hi * 2.0f) { - throw std::runtime_error( - "sample rate must be at least twice the inversion frequency"); + if (index_ == 0) + is_filled_ = true; } } -SndfileReader::~SndfileReader() { - sf_close(file_); -} - -std::vector SndfileReader::ReadBlock() { - std::vector result; - if (is_eof_) - return result; - - int to_read = kIOBufferSize / info_.channels; - - sf_count_t num_read = sf_readf_float(file_, buffer_.data(), to_read); - if (num_read != to_read) - is_eof_ = true; - - if (info_.channels == 1) { - result = std::vector(buffer_.begin(), buffer_.begin() + num_read); +float DCRemover::execute(float sample) const { + if (buffer_.size() == 0) { + return sample; } else { - result = std::vector(num_read); - for (size_t i = 0; i < result.size(); i++) - result[i] = buffer_[i * info_.channels]; - } - return result; -} - -float SndfileReader::samplerate() const { - return info_.samplerate; -} -#endif + float sum = std::accumulate(buffer_.begin(), buffer_.end(), 0.0f); -AudioWriter::~AudioWriter() { -} - -RawPCMWriter::RawPCMWriter() : buffer_pos_(0) { -} + if (is_filled_) + sum /= buffer_.size(); + else + sum /= (index_ == 0 ? 1 : index_); -bool RawPCMWriter::push(float sample) { - int16_t outsample = sample * 32767.f; - buffer_[buffer_pos_] = outsample; - buffer_pos_++; - if (buffer_pos_ == kIOBufferSize) { - fwrite(buffer_.data(), sizeof(buffer_[0]), kIOBufferSize, stdout); - buffer_pos_ = 0; + return sample - sum; } - return true; } -#ifdef HAVE_SNDFILE -SndfileWriter::SndfileWriter(const std::string& fname, int rate) : - info_({0, rate, 1, SF_FORMAT_WAV | SF_FORMAT_PCM_16, 0, 0}), - file_(sf_open(fname.c_str(), SFM_WRITE, &info_)), - buffer_pos_(0) { - if (file_ == nullptr) - throw std::runtime_error(fname + ": " + sf_strerror(nullptr)); -} - -SndfileWriter::~SndfileWriter() { - write(); - sf_close(file_); -} - -bool SndfileWriter::push(float sample) { - bool success = true; - buffer_[buffer_pos_] = sample; - if (buffer_pos_ == kIOBufferSize - 1) { - success = write(); - } - - buffer_pos_ = (buffer_pos_ + 1) % kIOBufferSize; - return success; -} - -bool SndfileWriter::write() { - sf_count_t num_to_write = buffer_pos_ + 1; - return (file_ != nullptr && - sf_write_float(file_, buffer_.data(), num_to_write) == num_to_write); -} -#endif - -Inverter::Inverter(float freq_prefilter, float freq_shift, - float freq_postfilter, float samplerate, - int filter_quality) : -#ifdef HAVE_LIQUID - filter_lengths_({0.f, 0.0006f, 0.0024f, 0.0064f}), - filter_attenuation_({60.f, 60.f, 60.f, 80.f}), - prefilter_(FilterLengthInSamples(filter_lengths_.at(filter_quality), - samplerate), - freq_prefilter / samplerate, - filter_attenuation_.at(filter_quality)), - postfilter_(FilterLengthInSamples(filter_lengths_.at(filter_quality), - samplerate), - freq_postfilter / samplerate, - filter_attenuation_.at(filter_quality)), - oscillator_(LIQUID_VCO, freq_shift * 2.0f * M_PI / samplerate), - do_filter_(filter_quality > 0) -#else - oscillator_(freq_shift * 2.0f * M_PI / samplerate), - do_filter_(false) -#endif -{} +Inverter::Inverter(float freq_prefilter, float freq_shift, float freq_postfilter, float samplerate, + int filter_quality) + : filter_lengths_({0.f, 0.0006f, 0.0024f, 0.0064f}), + filter_attenuation_({60.f, 60.f, 60.f, 80.f}), + prefilter_(FilterLengthInSamples(filter_lengths_.at(filter_quality), samplerate), + freq_prefilter / samplerate, filter_attenuation_.at(filter_quality)), + postfilter_(FilterLengthInSamples(filter_lengths_.at(filter_quality), samplerate), + freq_postfilter / samplerate, filter_attenuation_.at(filter_quality)), + oscillator_(LIQUID_VCO, freq_shift * 2.0f * M_PI / samplerate), + do_filter_(filter_quality > 0) {} float Inverter::execute(float insample) { oscillator_.Step(); float result; -#ifdef HAVE_LIQUID if (do_filter_) { prefilter_.push(insample); postfilter_.push(oscillator_.MixUp(prefilter_.execute()).real()); @@ -364,37 +95,28 @@ float Inverter::execute(float insample) { } else { result = oscillator_.MixUp({insample, 0.0f}).real(); } -#else - result = oscillator_.MixUp({insample, 0.0f}).real(); -#endif return result; } } // namespace deinvert -void SimpleDescramble(deinvert::Options options, - std::unique_ptr& reader, - std::unique_ptr& writer) { - static const std::vector filter_gain_compensation({ - 1.0f, 1.4f, 1.8f, 1.8f - }); - float gain = filter_gain_compensation.at(options.quality); +void SimpleDescramble(deinvert::Options options, std::unique_ptr &reader, + std::unique_ptr &writer) { + static const std::vector filter_gain_compensation({1.0f, 1.4f, 1.8f, 1.8f}); + float gain = filter_gain_compensation.at(options.quality); - int dc_remover_length = - (options.quality * options.samplerate * 0.002f); + const int dc_remover_length = (options.quality * options.samplerate * 0.002f); - wdsp::DCRemover dcremover(dc_remover_length); + deinvert::DCRemover dcremover(dc_remover_length); - deinvert::Inverter inverter(options.frequency_hi, - options.frequency_hi, - options.frequency_hi, options.samplerate, - options.quality); + deinvert::Inverter inverter(options.frequency_hi, options.frequency_hi, options.frequency_hi, + options.samplerate, options.quality); while (!reader->eof()) { for (float insample : reader->ReadBlock()) { dcremover.push(insample); - bool can_still_write = + const bool can_still_write = writer->push(gain * inverter.execute(dcremover.execute(insample))); if (!can_still_write) continue; @@ -402,47 +124,39 @@ void SimpleDescramble(deinvert::Options options, } } -void SplitBandDescramble(deinvert::Options options, - std::unique_ptr& reader, - std::unique_ptr& writer) { - static const std::vector filter_gain_compensation({ - 0.5f, 1.4f, 1.8f, 1.8f - }); - float gain = filter_gain_compensation.at(options.quality); +void SplitBandDescramble(deinvert::Options options, std::unique_ptr &reader, + std::unique_ptr &writer) { + static const std::vector filter_gain_compensation({0.5f, 1.4f, 1.8f, 1.8f}); + const float gain = filter_gain_compensation.at(options.quality); - int dc_remover_length = - (options.quality * options.samplerate * 0.002f); + const int dc_remover_length = (options.quality * options.samplerate * 0.002f); - wdsp::DCRemover dcremover(dc_remover_length); + deinvert::DCRemover dcremover(dc_remover_length); - deinvert::Inverter inverter1(options.frequency_lo, options.frequency_lo, - options.frequency_lo, options.samplerate, - options.quality); - deinvert::Inverter inverter2(options.frequency_hi, - options.frequency_lo + options.frequency_hi, - options.frequency_hi, options.samplerate, - options.quality); + deinvert::Inverter inverter1(options.frequency_lo, options.frequency_lo, options.frequency_lo, + options.samplerate, options.quality); + deinvert::Inverter inverter2(options.frequency_hi, options.frequency_lo + options.frequency_hi, + options.frequency_hi, options.samplerate, options.quality); while (!reader->eof()) { - for (float insample : reader->ReadBlock()) { + for (const float insample : reader->ReadBlock()) { dcremover.push(insample); - float dcremoved = dcremover.execute(insample); + const float dcremoved = dcremover.execute(insample); - bool can_still_write = writer->push(gain * - (inverter1.execute(dcremoved) + - inverter2.execute(dcremoved))); + const bool can_still_write = + writer->push(gain * (inverter1.execute(dcremoved) + inverter2.execute(dcremoved))); if (!can_still_write) continue; } } } -int main(int argc, char** argv) { +int main(int argc, char **argv) { deinvert::Options options; try { options = deinvert::GetOptions(argc, argv); - } catch (std::exception& e) { + } catch (std::exception &e) { std::cerr << "error: " << e.what() << std::endl; return EXIT_FAILURE; } @@ -454,35 +168,27 @@ int main(int argc, char** argv) { std::unique_ptr writer; if (options.input_type == deinvert::InputType::sndfile) { -#ifdef HAVE_SNDFILE try { - reader = std::unique_ptr( - new deinvert::SndfileReader(options)); - } catch (std::exception& e) { + reader = std::unique_ptr(new deinvert::SndfileReader(options)); + } catch (std::exception &e) { std::cerr << e.what() << std::endl; return EXIT_FAILURE; } options.samplerate = reader->samplerate(); -#endif } else { - reader = std::unique_ptr( - new deinvert::StdinReader(options)); + reader = std::unique_ptr(new deinvert::StdinReader(options)); } if (options.output_type == deinvert::OutputType::wavfile) { -#ifdef HAVE_SNDFILE try { writer = std::unique_ptr( - new deinvert::SndfileWriter(options.outfilename, - options.samplerate)); - } catch (std::exception& e) { + new deinvert::SndfileWriter(options.outfilename, options.samplerate)); + } catch (std::exception &e) { std::cerr << e.what() << std::endl; return EXIT_FAILURE; } } else { -#endif - writer = std::unique_ptr( - new deinvert::RawPCMWriter()); + writer = std::unique_ptr(new deinvert::RawPCMWriter()); } if (options.is_split_band) { diff --git a/src/deinvert.h b/src/deinvert.h index b3f73b4..79f95f5 100644 --- a/src/deinvert.h +++ b/src/deinvert.h @@ -15,142 +15,44 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ -#ifndef DEINVERT_H_ -#define DEINVERT_H_ +#pragma once -#include #include +#include #include #include #include "config.h" #include "src/liquid_wrappers.h" -#include "src/wdsp.h" - -#ifdef HAVE_SNDFILE -#include -#endif +#include "src/io.h" namespace deinvert { -const int kIOBufferSize = 4096; - -enum class InputType { - stdin, sndfile -}; - -enum class OutputType { - raw_stdout, wavfile -}; - -struct Options { - Options() : just_exit(false), - is_split_band(false), - quality(2), - samplerate(44100), - input_type(InputType::stdin), - output_type(OutputType::raw_stdout) {} - bool just_exit; - bool is_split_band; - int quality; - float samplerate; - float frequency_lo; - float frequency_hi; - float split_frequency; - InputType input_type; - OutputType output_type; - std::string infilename; - std::string outfilename; -}; - -class AudioReader { - public: - virtual ~AudioReader(); - bool eof() const; - virtual std::vector ReadBlock() = 0; - virtual float samplerate() const = 0; - - protected: - bool is_eof_; -}; - -class StdinReader : public AudioReader { - public: - explicit StdinReader(const Options& options); - ~StdinReader() override; - std::vector ReadBlock() override; - float samplerate() const override; - - private: - float samplerate_; - std::array buffer_{}; -}; - -#ifdef HAVE_SNDFILE -class SndfileReader : public AudioReader { - public: - explicit SndfileReader(const Options& options); - ~SndfileReader(); - std::vector ReadBlock() override; - float samplerate() const override; - - private: - SF_INFO info_; - SNDFILE* file_; - std::array buffer_{}; -}; -#endif - -class AudioWriter { - public: - virtual ~AudioWriter(); - virtual bool push(float sample) = 0; -}; - -class RawPCMWriter : public AudioWriter { - public: - RawPCMWriter(); - bool push(float sample) override; - - private: - std::array buffer_{}; - size_t buffer_pos_; -}; - -#ifdef HAVE_SNDFILE -class SndfileWriter : public AudioWriter { +class DCRemover { public: - SndfileWriter(const std::string& fname, int rate); - ~SndfileWriter() override; - bool push(float sample) override; + explicit DCRemover(size_t length); + void push(float sample); + float execute(float sample) const; private: - bool write(); - SF_INFO info_; - SNDFILE* file_; - std::array buffer_{}; - size_t buffer_pos_; + std::vector buffer_; + size_t index_{}; + bool is_filled_{}; }; -#endif class Inverter { public: - Inverter(float freq_prefilter, float freq_shift, float freq_postfilter, - float samplerate, int filter_quality); + Inverter(float freq_prefilter, float freq_shift, float freq_postfilter, float samplerate, + int filter_quality); float execute(float insample); private: const std::vector filter_lengths_; const std::vector filter_attenuation_; -#ifdef HAVE_LIQUID - liquid::FIRFilter prefilter_; - liquid::FIRFilter postfilter_; - liquid::NCO oscillator_; -#else - wdsp::NCO oscillator_; -#endif - const bool do_filter_; + liquid::FIRFilter prefilter_; + liquid::FIRFilter postfilter_; + liquid::NCO oscillator_; + const bool do_filter_; }; } // namespace deinvert -#endif // DEINVERT_H_ diff --git a/src/io.h b/src/io.h new file mode 100644 index 0000000..a7d7b47 --- /dev/null +++ b/src/io.h @@ -0,0 +1,158 @@ +#pragma once + +#include +#include + +#include + +#include "src/options.h" + +namespace deinvert { + +constexpr int kIOBufferSize = 4096; + +class AudioReader { + public: + virtual ~AudioReader() = default; + bool eof() const { + return is_eof_; + }; + virtual std::vector ReadBlock() = 0; + virtual float samplerate() const = 0; + + protected: + bool is_eof_; +}; + +class StdinReader : public AudioReader { + public: + explicit StdinReader(const Options &options) : samplerate_(options.samplerate) { + is_eof_ = false; + } + ~StdinReader() override = default; + std::vector ReadBlock() override { + const int num_read = fread(buffer_.data(), sizeof(buffer_[0]), kIOBufferSize, stdin); + + if (num_read < kIOBufferSize) + is_eof_ = true; + + std::vector result(num_read); + for (int i = 0; i < num_read; i++) result[i] = buffer_[i] * (1.f / 32768.f); + + return result; + }; + float samplerate() const override { + return samplerate_; + }; + + private: + float samplerate_; + std::array buffer_{}; +}; + +class SndfileReader : public AudioReader { + public: + explicit SndfileReader(const Options &options) + : info_({0, 0, 0, 0, 0, 0}), file_(sf_open(options.infilename.c_str(), SFM_READ, &info_)) { + is_eof_ = false; + if (file_ == nullptr) { + throw std::runtime_error(options.infilename + ": " + sf_strerror(nullptr)); + } else if (info_.samplerate < options.frequency_hi * 2.0f) { + throw std::runtime_error("sample rate must be at least twice the inversion frequency"); + } + } + ~SndfileReader() override { + sf_close(file_); + }; + std::vector ReadBlock() override { + std::vector result; + if (is_eof_) + return result; + + const int to_read = kIOBufferSize / info_.channels; + + const sf_count_t num_read = sf_readf_float(file_, buffer_.data(), to_read); + if (num_read != to_read) + is_eof_ = true; + + if (info_.channels == 1) { + result = std::vector(buffer_.begin(), buffer_.begin() + num_read); + } else { + result = std::vector(num_read); + for (size_t i = 0; i < result.size(); i++) result[i] = buffer_[i * info_.channels]; + } + return result; + }; + float samplerate() const override { + return info_.samplerate; + }; + + private: + SF_INFO info_; + SNDFILE *file_; + std::array buffer_{}; +}; + +class AudioWriter { + public: + virtual ~AudioWriter() = default; + virtual bool push(float sample) = 0; +}; + +class RawPCMWriter : public AudioWriter { + public: + RawPCMWriter() = default; + bool push(float sample) override { + const int16_t outsample = static_cast(sample * 32767.f); + buffer_[buffer_pos_] = outsample; + buffer_pos_++; + if (buffer_pos_ == kIOBufferSize) { + fwrite(buffer_.data(), sizeof(buffer_[0]), kIOBufferSize, stdout); + buffer_pos_ = 0; + } + return true; + } + + private: + std::array buffer_{}; + size_t buffer_pos_{}; +}; + +class SndfileWriter : public AudioWriter { + public: + SndfileWriter(const std::string &fname, int rate) + : info_({0, rate, 1, SF_FORMAT_WAV | SF_FORMAT_PCM_16, 0, 0}), + file_(sf_open(fname.c_str(), SFM_WRITE, &info_)) { + if (file_ == nullptr) + throw std::runtime_error(fname + ": " + sf_strerror(nullptr)); + } + + ~SndfileWriter() override { + write(); + sf_close(file_); + }; + + bool push(float sample) override { + bool success = true; + buffer_[buffer_pos_] = sample; + if (buffer_pos_ == kIOBufferSize - 1) { + success = write(); + } + + buffer_pos_ = (buffer_pos_ + 1) % kIOBufferSize; + return success; + }; + + private: + bool write() { + const sf_count_t num_to_write = buffer_pos_ + 1; + return (file_ != nullptr && + sf_write_float(file_, buffer_.data(), num_to_write) == num_to_write); + } + SF_INFO info_; + SNDFILE *file_; + std::array buffer_{}; + size_t buffer_pos_{}; +}; + +} // namespace deinvert \ No newline at end of file diff --git a/src/liquid_wrappers.cc b/src/liquid_wrappers.cc index 5901737..9ccadad 100644 --- a/src/liquid_wrappers.cc +++ b/src/liquid_wrappers.cc @@ -17,12 +17,18 @@ #include "src/liquid_wrappers.h" #include "config.h" -#ifdef HAVE_LIQUID #include #include +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +// https://github.com/jgaeddert/liquid-dsp/issues/229 +#pragma clang diagnostic ignored "-Wreturn-type-c-linkage" +extern "C" { #include "liquid/liquid.h" +} +#pragma clang diagnostic pop namespace liquid { @@ -49,8 +55,7 @@ float FIRFilter::execute() { return result; } -NCO::NCO(liquid_ncotype type, float freq) : - object_(nco_crcf_create(type)) { +NCO::NCO(liquid_ncotype type, float freq) : object_(nco_crcf_create(type)) { nco_crcf_set_frequency(object_, freq); } @@ -68,10 +73,4 @@ void NCO::Step() { nco_crcf_step(object_); } -float NCO::frequency() { - return nco_crcf_get_frequency(object_); -} - } // namespace liquid - -#endif diff --git a/src/liquid_wrappers.h b/src/liquid_wrappers.h index 712741a..bb0644e 100644 --- a/src/liquid_wrappers.h +++ b/src/liquid_wrappers.h @@ -14,15 +14,20 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ -#ifndef LIQUID_WRAPPERS_H_ -#define LIQUID_WRAPPERS_H_ +#pragma once #include "config.h" -#ifdef HAVE_LIQUID #include +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +// https://github.com/jgaeddert/liquid-dsp/issues/229 +#pragma clang diagnostic ignored "-Wreturn-type-c-linkage" +extern "C" { #include "liquid/liquid.h" +} +#pragma clang diagnostic pop namespace liquid { @@ -30,7 +35,7 @@ class FIRFilter { public: FIRFilter(int len, float fc, float As = 80.0f, float mu = 0.0f); ~FIRFilter(); - void push(float s); + void push(float s); float execute(); private: @@ -42,15 +47,10 @@ class NCO { explicit NCO(liquid_ncotype type, float freq); ~NCO(); std::complex MixUp(std::complex s); - void Step(); - float frequency(); + void Step(); private: nco_crcf object_; }; } // namespace liquid - -#endif // HAVE_LIQUID - -#endif // LIQUID_WRAPPERS_H_ diff --git a/src/wdsp.cc b/src/wdsp.cc deleted file mode 100644 index f9f04e3..0000000 --- a/src/wdsp.cc +++ /dev/null @@ -1,66 +0,0 @@ -/* - * deinvert - a voice inversion descrambler - * Copyright (c) Oona Räisänen - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - */ -#include "wdsp.h" - -#include -#include - -namespace wdsp { - -NCO::NCO(double frequency) : frequency_(frequency), phase_(0.0) { -} - -void NCO::Step() { - phase_ += frequency_; -} - -std::complex NCO::MixUp(std::complex sample_in) const { - return {real(sample_in) * cosf(phase_) - imag(sample_in) * sinf(phase_), - imag(sample_in) * cosf(phase_) + real(sample_in) * sinf(phase_)}; -} - -DCRemover::DCRemover(size_t length) : buffer_(length), index_(0), - is_filled_(false) { -} - -void DCRemover::push(float sample) { - if (buffer_.size() > 0) { - buffer_[index_] = sample; - index_ = (index_ + 1) % buffer_.size(); - - if (index_ == 0) - is_filled_ = true; - } -} - -float DCRemover::execute(float sample) { - if (buffer_.size() == 0) { - return sample; - } else { - float sum = std::accumulate(buffer_.begin(), buffer_.end(), 0.0f); - - if (is_filled_) - sum /= buffer_.size(); - else - sum /= (index_ == 0 ? 1 : index_); - - return sample - sum; - } -} - -} // namespace wdsp diff --git a/src/wdsp.h b/src/wdsp.h deleted file mode 100644 index 63f69c9..0000000 --- a/src/wdsp.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * deinvert - a voice inversion descrambler - * Copyright (c) Oona Räisänen - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - */ -#ifndef WDSP_H_ -#define WDSP_H_ - -#include -#include - -namespace wdsp { - -class NCO { - public: - explicit NCO(double frequency); - void Step(); - std::complex MixUp(std::complex sample_in) const; - - private: - double frequency_; - double phase_; -}; - -class DCRemover { - public: - explicit DCRemover(size_t length); - void push(float sample); - float execute(float sample); - - private: - std::vector buffer_; - size_t index_; - bool is_filled_; -}; - -} // namespace wdsp -#endif // WDSP_H_ diff --git a/test/test.pl b/test/test.pl new file mode 100644 index 0000000..b7adadd --- /dev/null +++ b/test/test.pl @@ -0,0 +1,109 @@ +use strict; +use warnings; +use IPC::Cmd qw(can_run); +use Carp; + +# deinvert tests + +my $binary = "../build/deinvert"; +my $print_even_if_successful = 1; + +my $test_file = "test.wav"; +my $output_file = "output.wav"; + +my $has_failures = 0; + +main(); + +sub main { + system("uname -rms"); + + testSimpleInversion(); + + print $has_failures ? "Tests did not pass\n" : "All passed\n"; + + exit $has_failures; +} + +sub testSimpleInversion { + for my $test_frequency ( 500, 600, 700 ) { + checkThatFrequencyInvertsAsItShould($test_frequency); + } +} + +sub checkThatFrequencyInvertsAsItShould { + my ($test_frequency) = @_; + my $inversion_carrier = 2632; + generateTestSoundWithSimpleBeep($test_frequency); + deinvertTestFileWithOptions( "-f " . $inversion_carrier ); + my $measured_frequency = findFrequencyOfOutputFile(); + my $expected_frequency = + calculateExpectedInvertedFrequency( $test_frequency, $inversion_carrier ); + + my $result = abs( $expected_frequency - $measured_frequency ) < 2; + check( $result, + "Carrier " + . $inversion_carrier . " Hz: " + . $test_frequency + . " Hz becomes " + . $measured_frequency + . ", should be ~" + . $expected_frequency ); +} + +sub generateTestSoundWithSimpleBeep { + my ($test_frequency) = @_; + unlink($test_file); + system( +"sox -n -c 1 -e signed -b 16 -r 48k $test_file synth sin $test_frequency trim 0 5 vol 0.5 fade 0.2" + ); +} + +sub calculateExpectedInvertedFrequency { + my ( $original_frequency, $inversion_carrier ) = @_; + return $inversion_carrier - $original_frequency; +} + +sub deinvertTestFileWithOptions { + my ($options) = @_; + system( $binary. " -i $test_file -o $output_file " . $options ); +} + +sub findFrequencyOfOutputFile { + my $detected_frequency = 0; + + my @dft; + my $maxbin = 0; + for (qx!sox $output_file -n stat -freq 2>&1!) { + if (/^([\d\.]+)\s+([\d\.]+)/) { + push @dft, [ $1, $2 ]; + $maxbin = $#dft if ( $2 > $dft[$maxbin]->[1] ); + last if ( @dft == 4096 ); + } + } + + # Parabolic FFT peak interpolation + my $delta = 0; + if ( $maxbin > 0 && $maxbin < $#dft ) { + $delta = + 0.5 * ( $dft[ $maxbin - 1 ]->[1] - $dft[ $maxbin + 1 ]->[1] ) / + ( $dft[ $maxbin - 1 ]->[1] - + 2 * $dft[$maxbin]->[1] + + $dft[ $maxbin + 1 ]->[1] ); + } + + return $dft[$maxbin]->[0] + + ( $dft[ $maxbin + 1 ]->[0] - $dft[$maxbin]->[0] ) * $delta; +} + +# bool is expected to be true, otherwise fail with message +sub check { + my ( $bool, $message ) = @_; + if ( !$bool || $print_even_if_successful ) { + print( ( $bool ? "[ OK ] " : "[FAIL] " ) . $message . "\n" ); + + $has_failures = 1 if ( !$bool ); + } + + return; +}