Skip to content

Commit

Permalink
Introduce unit tests, clang-format (#84)
Browse files Browse the repository at this point in the history
* Add some basic, mostly text-related unit tests using Catch2
* Also, add -o option
* Add .clang-format
* Add some comments to functions
  • Loading branch information
windytan committed Jun 13, 2024
1 parent 9016e3e commit 93b8767
Show file tree
Hide file tree
Showing 15 changed files with 818 additions and 358 deletions.
12 changes: 12 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
BasedOnStyle: Google
IndentWidth: 2
---
Language: Cpp
Standard: c++14
ColumnLimit: 100
AllowShortIfStatementsOnASingleLine: false
AlignArrayOfStructures: Left
AlignConsecutiveAssignments: Consecutive
AllowShortFunctionsOnASingleLine: Empty
IncludeBlocks: Preserve
9 changes: 6 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,19 @@ jobs:
- name: compile
run: cd build && meson compile

macos-build:
macos-build-and-test:
runs-on: macos-latest

steps:
- uses: actions/checkout@v4
- name: Install dependencies (brew)
run: brew install meson libsndfile liquid-dsp
run: brew install meson libsndfile liquid-dsp catch2
- name: meson setup
run: meson setup build
- name: compile
run: cd build && meson compile
- name: test
run: cd build && meson test

windows-msys2-mingw-build:
runs-on: windows-latest
Expand Down Expand Up @@ -107,7 +109,8 @@ jobs:
./bootstrap.sh &&
./configure --prefix=/usr &&
make && make install
# Cygwin does not allow underscore variables that start with an uppercase when compiling with gcc
# Cygwin does not allow underscore variables that start with an uppercase when
# compiling with gcc
- name: Patch liquid-dsp
shell: C:\cygwin\bin\bash.exe -eo pipefail '{0}'
run: perl -i -p -e 's/(?<=\s)(_[A-Z])(?=[,\)])/\L\1__/g' /usr/include/liquid/liquid.h
Expand Down
22 changes: 13 additions & 9 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

## HEAD

* Migrate build system from autotools to meson (#90)
* Add GitHub Workflows CI builds for macOS and Windows (MSYS2/MinGW + Cygwin)
* Remove unmaintained build options for non-liquid, non-TMC builds
* Add support for enhanced RadioText (eRT)
* Maintainability changes:
* Migrate build system from autotools to meson (#90)
* Add GitHub Workflows CI builds for macOS and Windows via MSYS2/MinGW + Cygwin
* Add basic Catch2 unit tests (#84)
* Add .clang-format (not automated)
* Remove unmaintained build options for non-liquid, non-TMC builds
* Fix compiler warnings and other code cleanup
* UX changes:
* Breaking: Print a warning to stderr if the raw MPX input sample rate is
not specified
* Improve error reporting in general
* Add `--output hex` (same as `--output-hex`) to mirror `--input hex`
* Add support for Enhanced RadioText (eRT)
* Add support for Long PS in Group 15A (#104)
* Fix detection of invalid date/time (timestamps >2000 years ago)
* Fix compiler warnings and other code cleanup
* UX changes:
* * Print a warning if the raw MPX input sample rate is not specified
* * Improve error reporting in general
* * Add `--output hex` (same as `--output-hex`) to mirror `--input hex`
* Noise resistance improvements:
* Require three (instead of two) repeats of a new PI before accepting it
* Require three (instead of two) synchronization pulses before locking
Expand Down
111 changes: 64 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,56 +97,69 @@ Please refer to the [wiki][Wiki: Use cases] for more details and usage examples.
radio_command | redsea [OPTIONS]
redsea -f WAVEFILE
-c, --channels CHANS Number of channels in the raw input signal. Each
channel is demodulated independently.
-e, --feed-through Echo the input signal to stdout and print
decoded groups to stderr.
-E, --bler Display the average block error rate, or the
percentage of blocks that had errors before
error correction. Averaged over the last 12
groups. For hex input, this is the percentage
of missing blocks.
-f, --file FILENAME Use an audio file as MPX input. All formats
readable by libsndfile should work.
-i, --input FORMAT Decode stdin as FORMAT (see the wiki for more info):
bits Unsynchronized ASCII bit stream (011010110...).
All characters but '0' and '1' are ignored.
hex RDS Spy hex format.
mpx Mono S16LE PCM-encoded MPX waveform (default).
tef Serial data from the TEF6686 tuner.
-l, --loctable DIR Load TMC location table from a directory in TMC
Exchange format. This option can be specified
multiple times to load several location tables.
-p, --show-partial Show some multi-group data even before they've been
fully received (PS names, RadioText, alternative
frequencies). partial_ will be prepended to their
names. This is good for noisy conditions.
-r, --samplerate RATE Set sample frequency of the raw MPX input signal in Hz.
Will resample (slow) if this differs from 171000 Hz.
-R, --show-raw Show raw group data as hex in the JSON stream.
-t, --timestamp FORMAT Add time of decoding to JSON groups; see
man strftime for formatting options (or
try "%c"). Use "%f" to add hundredths of seconds.
-u, --rbds RBDS mode; use North American program type names
and "back-calculate" the station's call sign from
its PI code. Note that this calculation gives an
incorrect call sign for most stations that transmit
TMC.
-b, --input-bits Same as --input bits (for backwards compatibility).
-c, --channels CHANS Number of channels in the raw input signal. Channels are
interleaved streams of samples that are demodulated
independently.
-e, --feed-through Echo the input signal to stdout and print decoded groups
to stderr. This only works for raw PCM.
-E, --bler Display the average block error rate, or the percentage
of blocks that had errors before error correction.
Averaged over the last 12 groups. For hex input, this is
the percentage of missing blocks.
-f, --file FILENAME Read MPX input from a wave file with headers (.wav,
.flac, ...). If you have headered wave data via stdin,
use '-'. Or you can specify another format with --input.
-h, --input-hex Same as --input hex (for backwards compatibility).
-i, --input FORMAT Decode input as FORMAT (see the redsea wiki in github
for more info).
bits Unsynchronized ASCII bit stream (011010110...).
All characters but '0' and '1' are ignored.
hex RDS Spy hex format. (Timestamps will be ignored)
pcm MPX as raw mono S16LE PCM. Remember to also
specify --samplerate. If you're reading from a
sound file with headers (WAV, FLAC, ...) don't
specify this.
tef Serial data from the TEF6686 tuner.
-l, --loctable DIR Load TMC location table from a directory in TMC Exchange
format. This option can be specified multiple times to
load several location tables.
-o, --output FORMAT Print output as FORMAT:
hex RDS Spy hex format.
json Newline-delimited JSON (default).
-p, --show-partial Under noisy conditions, redsea may not be able to fully
receive all information. Multi-group data such as PS
names, RadioText, and alternative frequencies are
especially vulnerable. This option makes it display them
even if not fully received, as
partial_{ps,radiotext,alt_frequencies}.
-r, --samplerate RATE Set sample frequency of raw PCM input in Hz. Will
resample if this differs from 171000 Hz.
-R, --show-raw Include raw group data as hex in the JSON stream.
-t, --timestamp FORMAT Add time of decoding to JSON groups; see man strftime
for formatting options (or try "%c"). Use "%f" to add
hundredths of seconds.
-u, --rbds RBDS mode; use North American program type names and
"back-calculate" the station's call sign from its PI
code. Note that this calculation gives an incorrect call
sign for most stations that transmit TMC.
-v, --version Print version string and exit.
-x, --output-hex Output hex groups in the RDS Spy format,
suppressing JSON output.
-x, --output-hex Same as --output hex (for backwards compatibility).
```

### Formatting and filtering the JSON output
Expand Down Expand Up @@ -180,6 +193,10 @@ type:
* C++14 compiler
* meson + ninja

### Testing

* Catch2

## Troubleshooting

### Can't find liquid-dsp on macOS
Expand Down
14 changes: 11 additions & 3 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ else
add_project_arguments('-std=c++14', language : 'cpp')
endif

sources = [
sources_no_main = [
'ext/json/jsoncpp.cpp',
'src/block_sync.cc',
'src/channel.cc',
Expand All @@ -64,12 +64,20 @@ sources = [
'src/liquid_wrappers.cc',
'src/options.cc',
'src/rdsstring.cc',
'src/redsea.cc',
'src/subcarrier.cc',
'src/tables.cc',
'src/tmc/tmc.cc',
'src/tmc/locationdb.cc',
'src/util.cc'
]

executable('redsea', sources, dependencies: [iconv, sndfile, liquid])
executable('redsea', [sources_no_main, 'src/redsea.cc'], dependencies: [iconv, sndfile, liquid])

### Unit tests ###

catch2_dep = dependency('catch2-with-main', required: false)

if catch2_dep.found()
test_exec = executable('redsea-test', [sources_no_main, 'test/unit.cc'], dependencies: [ iconv, sndfile, liquid, catch2_dep ])
test('Smoke tests', test_exec)
endif
8 changes: 7 additions & 1 deletion src/block_sync.cc
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ bool SyncPulse::couldFollow(const SyncPulse& other) const {
getBlockNumberForOffset(offset);
}

// Push a detected sync pulse to the buffer for determination of validity.
// @param offset The calculated cyclic offset
// @param bitcount Bit position where the detection happened
void SyncPulseBuffer::push(Offset offset, int bitcount) {
for (size_t i = 0; i < pulses_.size() - 1; i++) {
pulses_[i] = pulses_[i + 1];
Expand Down Expand Up @@ -210,6 +213,7 @@ bool SyncPulseBuffer::isSequenceFound() const {

BlockStream::BlockStream(const Options& options) : options_(options) {}

// The data had errors that couldn't be corrected.
void BlockStream::handleUncorrectableError() {
// EN 50067:1998, section C.1.2:
// Sync is lost when >45 out of last 50 blocks are erroneous
Expand All @@ -219,13 +223,13 @@ void BlockStream::handleUncorrectableError() {
}
}

// Try to find a cyclic pattern in the offset words.
void BlockStream::acquireSync(Block block) {
if (is_in_sync_)
return;

num_bits_since_sync_lost_++;

// Try to find a repeating offset sequence
if (block.offset != Offset::invalid) {
sync_buffer_.push(block.offset, bitcount_);

Expand All @@ -250,6 +254,7 @@ void BlockStream::pushBit(bool bit) {
}
}

// Search the input register for block data + offset. If found, add it to the group.
void BlockStream::findBlockInInputRegister() {
Block block;
block.raw = input_register_ & kBlockBitmask;
Expand Down Expand Up @@ -289,6 +294,7 @@ void BlockStream::findBlockInInputRegister() {
}
}

// A whole group of four blocks was received.
void BlockStream::handleNewlyReceivedGroup() {
ready_group_ = current_group_;
has_group_ready_ = true;
Expand Down
39 changes: 27 additions & 12 deletions src/channel.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
*/
#include "src/channel.h"

#include <chrono>
#include <iostream>

#include "src/common.h"

namespace redsea {
Expand All @@ -29,10 +32,22 @@ namespace redsea {
* bits, or groups. Usage of these inputs shouldn't be intermixed.
*
*/
Channel::Channel(const Options& options, int which_channel) :
options_(options),
which_channel_(which_channel),
block_stream_(options), station_(0x0000, options, which_channel, false) {
Channel::Channel(const Options& options, int which_channel, std::ostream& output_stream = std::cout)
: options_(options),
which_channel_(which_channel),
output_stream_(output_stream),
block_stream_(options),
station_(options, which_channel) {}

// Used for testing (PI is already known)
Channel::Channel(const Options& options, std::ostream& output_stream, uint16_t pi)
: options_(options),
which_channel_(0),
output_stream_(output_stream),
block_stream_(options),
station_(options, 0, pi) {
cached_pi_.update(pi);
cached_pi_.update(pi);
}

void Channel::processBit(bool bit) {
Expand All @@ -51,7 +66,8 @@ void Channel::processBits(const BitBuffer& buffer) {

// Calculate this group's rx time based on the buffer timestamp and bit offset
auto group_time = buffer.time_received -
std::chrono::milliseconds(static_cast<int>((buffer.bits.size() - 1 - i_bit) / 1187.5 * 1e3));
std::chrono::milliseconds(
static_cast<int>((buffer.bits.size() - 1 - i_bit) / 1187.5 * 1e3));

// When the source is faster than real-time, backwards timestamp calculation
// produces meaningless results. We want to make sure that the time stays monotonic.
Expand All @@ -67,6 +83,7 @@ void Channel::processBits(const BitBuffer& buffer) {
}
}

// Handle this group as if it was just received.
void Channel::processGroup(Group group) {
if (options_.timestamp && !group.hasTime()) {
auto now = std::chrono::system_clock::now();
Expand All @@ -92,7 +109,7 @@ void Channel::processGroup(Group group) {
const auto pi_status = cached_pi_.update(group.getPI());
switch (pi_status) {
case CachedPI::Result::ChangeConfirmed:
station_ = Station(cached_pi_.get(), options_, which_channel_);
station_ = Station(options_, which_channel_, cached_pi_.get());
break;

case CachedPI::Result::SpuriousChange:
Expand All @@ -103,17 +120,15 @@ void Channel::processGroup(Group group) {
}
}

auto stream = options_.feed_thru ? &std::cerr : &std::cout;

if (options_.output_type == redsea::OutputType::Hex) {
if (!group.isEmpty()) {
group.printHex(stream);
group.printHex(output_stream_);
if (options_.timestamp)
*stream << ' ' << getTimePointString(group.getRxTime(), options_.time_format);
*stream << '\n' << std::flush;
output_stream_ << ' ' << getTimePointString(group.getRxTime(), options_.time_format);
output_stream_ << '\n' << std::flush;
}
} else {
station_.updateAndPrint(group, stream);
station_.updateAndPrint(group, output_stream_);
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/channel.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#ifndef CHANNEL_H_
#define CHANNEL_H_

#include <iostream>

#include "src/block_sync.h"
#include "src/common.h"
#include "src/options.h"
Expand Down Expand Up @@ -70,7 +72,8 @@ class CachedPI {

class Channel {
public:
Channel(const Options& options, int which_channel);
Channel(const Options& options, int which_channel, std::ostream& output_stream);
Channel(const Options& options, std::ostream& output_stream, uint16_t pi);
void processBit(bool bit);
void processBits(const BitBuffer& buffer);
void processGroup(Group group);
Expand All @@ -81,6 +84,7 @@ class Channel {
private:
Options options_;
int which_channel_;
std::ostream& output_stream_;
CachedPI cached_pi_{};
BlockStream block_stream_;
Station station_;
Expand Down
Loading

0 comments on commit 93b8767

Please sign in to comment.