Skip to content

Commit

Permalink
(wip) Add support for AF Method B (#88)
Browse files Browse the repository at this point in the history
Redsea now decodes Alternative Frequencies transmitted with Method B.
This is used to transmit any number of different AF lists associated
with different tuned frequencies. Some of them may be transmitting
programme that varies region-by-region at different times of the day.

The JSON field alt_kilohertz was renamed to alt_frequencies_a. For
Method B there is a new field called alt_frequencies_b.
  • Loading branch information
windytan committed Jun 14, 2023
1 parent b34fd7c commit 743da78
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 36 deletions.
6 changes: 5 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

* Decode LTCC and LTECC in TMC (#80)
* Decode RDS output from the TEF6686 tuner (#89)
* Decode Alternative Frequencies sent with Method B (#88)
* Change the name of the field `alt_kilohertz` to either `alt_frequencies_a`
or `alt_frequencies_b`. The type of data sent by these methods differs.
When `--show-partial` is set, the AF list will be in `partial_alt_frequencies`
regardless of method.
* Add option `--input` / `-i` to specify the stdin input format (bits, hex,
mpx, tef). The old options will still work.
* Don't try to decode a Type 0 group if Block 2 wasn't received
* Fix automake script on Windows (#81)
* Fix compatibility with current liquid-dsp (#78)
* Fix output for UTF-8 encoded TMC location tables (#82)
Expand Down
37 changes: 34 additions & 3 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,33 @@
},
"additionalProperties": false
},
"alt_freq_b": {
"type": "object",
"properties": {
"tuned_frequency": {
"type": "integer",
"minimum": 153,
"maximum": 107900
},
"same_programme": {
"type": "array",
"items": {
"type": "integer",
"minimum": 153,
"maximum": 107900
}
},
"regional_variants": {
"type": "array",
"items": {
"type": "integer",
"minimum": 153,
"maximum": 107900
}
}
},
"additionalProperties": false
},
"tmc": {
"type": "object",
"oneOf": [{
Expand Down Expand Up @@ -300,13 +327,17 @@
"description": "Traffic Message Channel",
"$ref": "#/definitions/tmc"
},
"alt_kilohertz": {
"description": "Alternative Frequencies",
"alt_kilohertz_a": {
"description": "Alternative Frequencies (Method A)",
"type": "array",
"items": {
"type": "integer"
}
},
"alt_kilohertz_b": {
"description": "Alternative Frequencies (Method B)",
"$ref": "#/definitions/alt_freq_b"
},
"prog_item_number": {
"description": "A numeric identifier for the currently running program",
"type": "integer",
Expand Down Expand Up @@ -349,7 +380,7 @@
"description": "Incompletely received Program Service name",
"$ref": "#/definitions/ps"
},
"partial_alt_kilohertz": {
"partial_alt_frequencies": {
"description": "Incomplete list of Alternative Frequencies",
"type": "array",
"items": {
Expand Down
78 changes: 67 additions & 11 deletions src/groups.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <iostream>
#include <map>
#include <numeric>
#include <set>
#include <string>
#include <sstream>
#include <vector>
Expand Down Expand Up @@ -374,37 +375,92 @@ void Station::decodeBasics(const Group& group) {

// Group 0: Basic tuning and switching information
void Station::decodeType0(const Group& group) {
if (!group.has(BLOCK2))
return;

// Block 2: Flags
uint16_t segment_address = getBits<2>(group.getBlock2(), 0);
bool is_di = getBits<1>(group.getBlock2(), 2);
json_["di"][getDICodeString(segment_address)] = is_di;

json_["ta"] = static_cast<bool>(getBits<1>(group.getBlock2(), 4));
json_["is_music"] = static_cast<bool>(getBits<1>(group.getBlock2(), 3));

if (!group.has(BLOCK3))
if (!group.has(BLOCK3)) {
// Reset a Method B list to prevent mixing up different lists
if (alt_freq_list_.isMethodB())
alt_freq_list_.clear();
return;
}

// Block 3: Alternative frequencies
if (group.getType().version == GroupType::Version::A) {
alt_freq_list_.insert(getBits<8>(group.getBlock3(), 8));
alt_freq_list_.insert(getBits<8>(group.getBlock3(), 0));

if (alt_freq_list_.isComplete()) {
for (CarrierFrequency f : alt_freq_list_.get())
json_["alt_kilohertz"].append(f.kHz());
auto raw_frequencies = alt_freq_list_.getRawList();

// AF Method B sends longer lists with possible regional variants
if (alt_freq_list_.isMethodB()) {
int tuned_frequency = raw_frequencies[0];

// We use std::sets for filtering out duplicates
std::set<int> unique_alternative_frequencies;
std::set<int> unique_regional_variants;
std::vector<int> alternative_frequencies;
std::vector<int> regional_variants;

// Frequency pairs
for (size_t i = 1; i < raw_frequencies.size(); i += 2) {
int freq1 = raw_frequencies[i];
int freq2 = raw_frequencies[i + 1];

int non_tuned_frequency = (freq1 == tuned_frequency ? freq2 : freq1);

// "General case"
if (freq1 < freq2) {
alternative_frequencies.push_back(non_tuned_frequency);
unique_alternative_frequencies.insert(non_tuned_frequency);

// "Special case": Non-tuned frequency is a regional variant
} else {
regional_variants.push_back(non_tuned_frequency);
unique_regional_variants.insert(non_tuned_frequency);
}
}

// In noisy conditions we may miss a lot of 0A groups. This check catches
// the case where there's multiple copies of some frequencies.
const size_t expected_number_of_afs = raw_frequencies.size() / 2;
const size_t number_of_unique_afs = unique_alternative_frequencies.size() +
unique_regional_variants.size();
if (number_of_unique_afs == expected_number_of_afs) {
json_["alt_frequencies_b"]["*SORT01*tuned_frequency"] = tuned_frequency;

for (int frequency : alternative_frequencies)
json_["alt_frequencies_b"]["*SORT02*same_programme"].append(frequency);

for (int frequency : regional_variants)
json_["alt_frequencies_b"]["*SORT03*regional_variants"].append(frequency);
}

// AF Method A is a simple list
} else {
for (int frequency : raw_frequencies)
json_["alt_frequencies_a"].append(frequency);
}

alt_freq_list_.clear();

// If partial list is requested we'll print the raw list and not attempt to
// deduce whether it's Method A or B
} else if (options_.show_partial) {
for (CarrierFrequency f : alt_freq_list_.get())
json_["partial_alt_kilohertz"].append(f.kHz());
for (int f : alt_freq_list_.getRawList())
json_["partial_alt_frequencies"].append(f);
}
}

if (!group.has(BLOCK4))
return;

// Block 4: Program Service Name
ps_.update(segment_address * 2,
RDSChar(getBits<8>(group.getBlock4(), 8)),
RDSChar(getBits<8>(group.getBlock4(), 0)));
Expand Down Expand Up @@ -813,8 +869,8 @@ void Station::decodeType14(const Group& group) {
eon_alt_freqs_[on_pi].insert(getBits<8>(group.getBlock3(), 0));

if (eon_alt_freqs_[on_pi].isComplete()) {
for (CarrierFrequency freq : eon_alt_freqs_[on_pi].get())
json_["other_network"]["alt_kilohertz"].append(freq.kHz());
for (int freq : eon_alt_freqs_[on_pi].getRawList())
json_["other_network"]["alt_frequencies"].append(freq);
eon_alt_freqs_[on_pi].clear();
}
break;
Expand Down
2 changes: 1 addition & 1 deletion src/redsea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ void printUsage() {
" such as PS names, RadioText, and alternative\n"
" frequencies are especially vulnerable. This option\n"
" makes it display them even if not fully received,\n"
" as partial_{ps,radiotext,alt_kilohertz}.\n"
" as partial_{ps,radiotext,alt_frequencies}.\n"
"\n"
"-r, --samplerate RATE Set stdin sample frequency in Hz. Will resample\n"
" (slow) if this differs from 171000 Hz.\n"
Expand Down
4 changes: 2 additions & 2 deletions src/tmc/tmc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -570,8 +570,8 @@ void TMCService::receiveUserGroup(uint16_t x, uint16_t y, uint16_t z, Json::Valu
DKULTUR, for example, does not transmit information about the total
length of the list */
(*jsonroot)["tmc"]["other_network"]["pi"] = getPrefixedHexString(on_pi, 4);
for (CarrierFrequency f : other_network_freqs_.at(on_pi).get())
(*jsonroot)["tmc"]["other_network"]["frequencies"].append(f.str());
for (int f : other_network_freqs_.at(on_pi).getRawList())
(*jsonroot)["tmc"]["other_network"]["frequencies_khz"].append(f);
other_network_freqs_.clear();
break;
}
Expand Down
61 changes: 49 additions & 12 deletions src/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ std::string CarrierFrequency::str() const {

bool operator== (const CarrierFrequency &f1,
const CarrierFrequency &f2) {
return (f1.kHz() == f2.kHz());
return (f1.code_ == f2.code_);
}

bool operator< (const CarrierFrequency &f1,
Expand All @@ -134,31 +134,68 @@ void AltFreqList::insert(uint16_t af_code) {
CarrierFrequency::Band::FM);
lf_mf_follows_ = false;

if (frequency.isValid()) {
alt_freqs_.insert(frequency);
// AF code encodes a frequency
if (frequency.isValid() && num_expected_ > 0) {
if (num_received_ < num_expected_) {
int kHz = frequency.kHz();
alt_freqs_[num_received_] = kHz;
num_received_++;

// Error; no space left in the list.
} else {
clear();
}

// Filler
} else if (af_code == 205) {
// filler

// No AF exists
} else if (af_code == 224) {
// no AF exists

// Number of AFs
} else if (af_code >= 225 && af_code <= 249) {
num_alt_freqs_ = af_code - 224;
num_expected_ = af_code - 224;
num_received_ = 0;

// AM/LF freq follows
} else if (af_code == 250) {
// AM/LF freq follows
lf_mf_follows_ = true;

// Error; invalid AF code.
} else {
clear();
}
}

bool AltFreqList::isMethodB() const {
// Method B has an odd number of elements, at least 3
if (num_expected_ % 2 != 1 || num_received_ < 3)
return false;

// Method B is composed of pairs where one is always the tuned frequency
int tuned_frequency = alt_freqs_[0];
for (size_t i = 1; i < num_received_; i += 2) {
int freq1 = alt_freqs_[i];
int freq2 = alt_freqs_[i + 1];
if (freq1 != tuned_frequency && freq2 != tuned_frequency)
return false;
}

return true;
}

bool AltFreqList::isComplete() const {
return (alt_freqs_.size() == num_alt_freqs_ &&
num_alt_freqs_ > 0);
return num_expected_ == num_received_ &&
num_received_ > 0;
}

std::set<CarrierFrequency> AltFreqList::get() const {
return alt_freqs_;
// Return the sequence of frequencies as they were received (excluding special AF codes)
std::vector<int> AltFreqList::getRawList() const {
return std::vector<int>(alt_freqs_.begin(), alt_freqs_.begin() + num_received_);
}

void AltFreqList::clear() {
alt_freqs_.clear();
num_expected_ = num_received_ = 0;
}

std::vector<std::string> splitLine(const std::string& line, char delimiter) {
Expand Down
14 changes: 8 additions & 6 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
#include <cstdint>

#include <algorithm>
#include <array>
#include <map>
#include <numeric>
#include <set>
#include <string>
#include <vector>

Expand Down Expand Up @@ -83,7 +83,7 @@ class CarrierFrequency {
const CarrierFrequency &f2);

private:
uint16_t code_;
uint16_t code_ {};
Band band_ { Band::FM };
};

Expand All @@ -92,13 +92,15 @@ class AltFreqList {
AltFreqList() = default;
void insert(uint16_t af_code);
bool isComplete() const;
std::set<CarrierFrequency> get() const;
bool isMethodB() const;
std::vector<int> getRawList() const;
void clear();

private:
std::set<CarrierFrequency> alt_freqs_;
unsigned num_alt_freqs_ { 0 };
bool lf_mf_follows_ { false };
std::array<int, 25> alt_freqs_;
size_t num_expected_ { 0 };
size_t num_received_ { 0 };
bool lf_mf_follows_ { false };
};

template<typename T, size_t N>
Expand Down

0 comments on commit 743da78

Please sign in to comment.