From b5d57447d58868cd4442309909af23053bb00ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oona=20R=C3=A4is=C3=A4nen?= Date: Sun, 19 Oct 2025 09:48:31 +0300 Subject: [PATCH 1/2] Update tests, readme, CI pipeline --- .github/workflows/build.yml | 19 ++-- README.md | 90 ++++++++++----- test/test.pl | 215 +++++++++++++++++++++++++----------- 3 files changed, 224 insertions(+), 100 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b81df57..e31ead2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,22 +8,22 @@ on: branches: [ master ] jobs: - build-ubuntu-22-04: - runs-on: ubuntu-22.04 + build-ubuntu-24-04: + name: "Build on Ubuntu 24.04" + runs-on: ubuntu-24.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 + run: sudo apt install meson libsndfile1-dev libliquid-dev - 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 + build-ubuntu-22-04: + name: "Build on Ubuntu 22.04" + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -37,8 +37,9 @@ jobs: run: cd build && meson compile build-debian-oldoldstable: + name: "Build on Debian 11 (Bullseye)" runs-on: ubuntu-latest - container: debian:buster + container: debian:bullseye steps: - uses: actions/checkout@v4 @@ -52,6 +53,7 @@ jobs: run: export PATH=$PATH:$HOME/.local/bin && cd build && meson compile build-macos: + name: "Build on macOS" runs-on: macos-latest steps: @@ -64,6 +66,7 @@ jobs: run: cd build && meson compile test: + name: "Build and test on Ubuntu 22.04" runs-on: ubuntu-22.04 steps: diff --git a/README.md b/README.md index 950678c..702d5f3 100644 --- a/README.md +++ b/README.md @@ -15,71 +15,100 @@ deinvert requires liquid-dsp, libsndfile, and meson. On Ubuntu, these can be installed like so: - sudo apt install libsndfile1-dev libliquid-dev meson +```bash +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 +```bash +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/): +On macOS we recommend using [homebrew](https://brew.sh/): - xcode-select --install - brew install libsndfile liquid-dsp meson +```bash +xcode-select --install +brew install libsndfile liquid-dsp meson +``` ## Compiling - meson setup build - cd build - meson compile +```bash +meson setup build +cd build +meson compile +``` -If you wish to install it system-wide (/usr/local by default): +If you wish to install it system-wide (`/usr/local` by default): - meson install +```bash +meson install +``` -## Usage +If you don't install it, the binary will be available in the `build` +directory. -Note that since scrambling and descrambling are the same operation this -tool also works as a scrambler. +## Usage -If no input and output file is given, deinvert reads raw 16-bit PCM via stdin -and outputs in the same format via stdout. The inversion carrier defaults to -2632 Hz. +* If no filename is given as input or output, deinvert will use the standard + streams for raw PCM (16-bit signed-integer mono sound). +* The inversion carrier defaults to 2632 Hz. +* The input signal should be single-channel. Multi-channel input works from + a file, but the output will be mono. +* Note that since scrambling and descrambling are the same operation this + tool also works as a scrambler! ### Simple inversion, WAV input (De)scrambling a WAV file with setting 4: - ./build/deinvert -i input.wav -o output.wav -p 4 +```bash +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: - ./build/deinvert -i input.wav -o output.wav -f 3500 -s 1200 +```bash +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: +Descrambling a live FM channel at 27 Megahertz from an RTL-SDR, setting 4. Here +we also use `rtl_fm` from the RTL-SDR distribution and the `play` command from `sox` +as examples. - 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 - +```bash +rtl_fm -M fm -f 27.0M -s 12k -g 50 -l 70 |\ + deinvert -r 12000 -p 4 |\ + play -r 12k -c 1 -t .s16 - +``` ### Invert a live signal from Gqrx (requires netcat) +You can listen to descrambled audio while receiving a radio channel in Gqrx +at the same time: + 1. Set Gqrx to demodulate the audio (for example, narrow FM). 2. Go to the Audio window and click on the three dots button "...". 3. Go to Network and set host to localhost and port to e.g. 12345. 4. In the Audio window, enable UDP. -5. Run this command in a terminal window: +5. Run this command in a terminal window: (replace `play` with the audio + player of your choice): - nc -u -l localhost 12345 | ./build/deinvert -r 48000 | play -r 48k -c 1 -t .s16 - +```bash +nc -u -l localhost 12345 | deinvert -r 48000 | play -r 48k -c 1 -t .s16 - +``` ### Full options - ./build/deinvert [OPTIONS] + deinvert [OPTIONS] -f, --frequency FREQ Frequency of the inversion carrier, in Hertz. @@ -108,6 +137,9 @@ Descrambling a live FM channel at 27 Megahertz from an RTL-SDR, setting 4: ## Inversion carrier presets +We provide preset frequencies used by Selectone ST-20B as a convenience +(use the `-p` option): + | No | Frequency | | -- | --------- | | 1 | 2632 Hz | @@ -119,6 +151,8 @@ Descrambling a live FM channel at 27 Megahertz from an RTL-SDR, setting 4: | 7 | 3495 Hz | | 8 | 3729 Hz | +You can also provide any other frequency using the `-f` option. + ## Troubleshooting ### I can't understand the speech even after deinverting @@ -135,7 +169,9 @@ frequency should be around 250 Hz less than the inversion carrier frequency. For example, if the inversion carrier is 3023 Hz, the `play` command could be changed to: - play -r 12000 -c 1 -t .s16 - sinc -2800 +```bash +play -r 12000 -c 1 -t .s16 - sinc -2800 +``` ## Licensing diff --git a/test/test.pl b/test/test.pl index 39cc444..1b9c239 100644 --- a/test/test.pl +++ b/test/test.pl @@ -1,10 +1,14 @@ +#!/usr/bin/perl + +# deinvert tests +# - Requires sox +# - Will create some WAV files in the current directory + use strict; use warnings; use IPC::Cmd qw(can_run); use Carp; -# deinvert tests - my $binary = "../build/deinvert"; my $print_even_if_successful = 1; @@ -18,96 +22,177 @@ exit main(); sub main { - system("uname -rms"); + system( "uname", "-rms" ); + + testExeRunnable(); + + # Can't continue if the exe is not runnable + if ($has_failures) { + return $has_failures; + } + + testSimpleInversion(); + testSplitBandInversion(); - testSimpleInversion(); + print $has_failures ? "\nTests did not pass\n" : "\nAll tests passed\n"; - print $has_failures ? "Tests did not pass\n" : "All passed\n"; + return $has_failures; +} - return $has_failures; +sub testExeRunnable { + check( -x $binary, "$binary should exist" ); } +# Run tests for the simple inversion case +# testSimpleInversion() -> void sub testSimpleInversion { - for my $inversion_carrier (2632, 3729) { - for my $test_frequency ( 500, 600, 700 ) { - checkThatFrequencyInvertsAsItShould($test_frequency, $inversion_carrier); + + printf "\nSIMPLE %3s %10s %10s %10s %10s %10s\n", "Q", "Carrier", + "Original", + "Measured", "Expected", + "Margin_hz"; + + for my $quality ( 1, 3 ) { + for my $inversion_carrier ( 2632, 3729 ) { + for my $test_frequency ( 500, 600, 700 ) { + checkSimpleInversionFor( $test_frequency, $inversion_carrier, + $quality ); + } + } + } + return; +} + +sub testSplitBandInversion { + printf "\nSPLIT %3s %10s %10s %10s %10s %10s %10s\n", "Q", "Carrier", + "Split", + "Original", + "Measured", "Expected", + "Margin_hz"; + + my $split_point = 900; + + for my $quality ( 1, 3 ) { + for my $inversion_carrier ( 2632, 3729 ) { + for my $test_frequency ( 500, 600, 1100, 1200 ) { + checkSplitBandInversionFor( $test_frequency, $inversion_carrier, + $split_point, $quality ); + } + } } - } - return; + return; } -sub checkThatFrequencyInvertsAsItShould { - my ($test_frequency, $inversion_carrier) = @_; - 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 ); - - return; +# Generate test sound, run deinvert (simple inversion), measure output frequency +# checkSimpleInversionFor(test_frequency, inversion_carrier) -> void +sub checkSimpleInversionFor { + my ( $test_frequency, $inversion_carrier, $quality ) = @_; + generateTestSoundWithSimpleBeep($test_frequency); + invertTestFileWithOptions( "-f", $inversion_carrier, "-q", $quality ); + my $measured_frequency = findFrequencyOfOutputFile(); + my $expected_frequency = + calculateExpectedInvertedFrequency( $test_frequency, $inversion_carrier ); + + my $frequency_tolerance_hz = 2; + + my $result = abs( $expected_frequency - $measured_frequency ) < + $frequency_tolerance_hz; + + my $test_info = sprintf( "%3d %10d %10.2f %10.2f %10.2f %10.2f", + $quality, $inversion_carrier, $test_frequency, $measured_frequency, + $expected_frequency, $frequency_tolerance_hz ); + + check( $result, $test_info ); + + return; +} + +# checkSplitBandInversionFor(test_frequency, inversion_carrier, split_point) -> void +sub checkSplitBandInversionFor { + my ( $test_frequency, $inversion_carrier, $split_point, $quality ) = @_; + generateTestSoundWithSimpleBeep($test_frequency); + invertTestFileWithOptions( "-f", $inversion_carrier, "-s", $split_point, + "-q", $quality ); + my $measured_frequency = findFrequencyOfOutputFile(); + my $expected_frequency = calculateExpectedInvertedFrequency( + $test_frequency, $test_frequency < $split_point + ? $split_point + : $split_point + $inversion_carrier + ); + + my $frequency_tolerance_hz = 2; + + my $result = abs( $expected_frequency - $measured_frequency ) < + $frequency_tolerance_hz; + + my $test_info = sprintf( "%3d %10.2f %10.2f %10.2f %10.2f %10.2f %10.2f", + $quality, $inversion_carrier, $split_point, $test_frequency, + $measured_frequency, $expected_frequency, $frequency_tolerance_hz ); + + check( $result, $test_info ); + + return; } +# generateTestSoundWithSimpleBeep(test_frequency) -> void 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" ); - return; + 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" ); + return; } +# calculateExpectedInvertedFrequency(original_frequency, inversion_carrier) -> expected_frequency sub calculateExpectedInvertedFrequency { - my ( $original_frequency, $inversion_carrier ) = @_; - return $inversion_carrier - $original_frequency; + my ( $original_frequency, $inversion_carrier ) = @_; + return $inversion_carrier - $original_frequency; } -sub deinvertTestFileWithOptions { - my ($options) = @_; - system( $binary. " -i $test_file -o $output_file " . $options ); - return; +# Run deinvert with given options (results are in $output_file) +# invertTestFileWithOptions(options) -> void +sub invertTestFileWithOptions { + my @options = @_; + system( $binary, "-i", $test_file, "-o", $output_file, @options ); + return; } +# Use sox to find the dominant frequency in the output file +# findFrequencyOfOutputFile() -> frequency_hz 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 ); + 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] ); - } + # 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; + return $dft[$maxbin]->[0] + + ( $dft[ $maxbin + 1 ]->[0] - $dft[$maxbin]->[0] ) * $delta; } # bool is expected to be true, otherwise fail with message +# check(bool, message) -> void sub check { - my ( $bool, $message ) = @_; - if ( !$bool || $print_even_if_successful ) { - print( ( $bool ? "[ OK ] " : "[FAIL] " ) . $message . "\n" ); + my ( $bool, $message ) = @_; + if ( !$bool || $print_even_if_successful ) { + print( ( $bool ? "[ OK ] " : "[FAIL] " ) . $message . "\n" ); - $has_failures = 1 if ( !$bool ); - } + $has_failures = 1 if ( !$bool ); + } - return; + return; } From 90be7dead2f14c1e679ce72fd9f7fee83699e7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oona=20R=C3=A4is=C3=A4nen?= Date: Sun, 19 Oct 2025 09:48:49 +0300 Subject: [PATCH 2/2] Clean up includes, error messages --- src/deinvert.cc | 1 - src/deinvert.h | 5 ----- src/liquid_wrappers.cc | 2 -- src/liquid_wrappers.h | 2 -- src/options.h | 7 +++---- 5 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/deinvert.cc b/src/deinvert.cc index ed5079d..08c3cb2 100644 --- a/src/deinvert.cc +++ b/src/deinvert.cc @@ -23,7 +23,6 @@ #include #include #include -#include #include #include "src/io.h" diff --git a/src/deinvert.h b/src/deinvert.h index 29d445c..a9748bf 100644 --- a/src/deinvert.h +++ b/src/deinvert.h @@ -17,13 +17,8 @@ */ #pragma once -#include -#include -#include #include -#include "config.h" -#include "src/io.h" #include "src/liquid_wrappers.h" namespace deinvert { diff --git a/src/liquid_wrappers.cc b/src/liquid_wrappers.cc index 10dbaa1..e73760f 100644 --- a/src/liquid_wrappers.cc +++ b/src/liquid_wrappers.cc @@ -16,8 +16,6 @@ */ #include "src/liquid_wrappers.h" -#include "config.h" - #include #include diff --git a/src/liquid_wrappers.h b/src/liquid_wrappers.h index bb0644e..ec0463d 100644 --- a/src/liquid_wrappers.h +++ b/src/liquid_wrappers.h @@ -16,8 +16,6 @@ */ #pragma once -#include "config.h" - #include #pragma clang diagnostic push diff --git a/src/options.h b/src/options.h index 93b826f..7738910 100644 --- a/src/options.h +++ b/src/options.h @@ -157,14 +157,13 @@ inline Options GetOptions(int argc, char **argv) { if (options.input_type == InputType::sndfile && samplerate_set) throw std::runtime_error( - "don't specify sample rate (-r) with -i; I want to read it from the sound file"); + "conflicting options; don't specify a sample rate when using an input audio file"); if (options.is_split_band && options.frequency_lo >= options.frequency_hi) - throw std::runtime_error("split point must be below the inversion carrier"); + throw std::runtime_error("split point frequency 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 (see Nyquist-Shannon theorem)"); + throw std::runtime_error("sample rate must be at least twice the inversion frequency"); return options; }