aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMatthias P. Braendli <matthias.braendli@mpb.li>2023-10-31 23:02:10 +0100
committerMatthias P. Braendli <matthias.braendli@mpb.li>2023-10-31 23:02:10 +0100
commitc2467d222ec08ddc4c6f79ea01773496090f809f (patch)
tree33132f324da5b9bfb7a3d711c85053f80ac4f427 /src
parent6728ddc82936d8d6223a885bf7fbcec9a00c18a6 (diff)
downloaddabmod-c2467d222ec08ddc4c6f79ea01773496090f809f.tar.gz
dabmod-c2467d222ec08ddc4c6f79ea01773496090f809f.tar.bz2
dabmod-c2467d222ec08ddc4c6f79ea01773496090f809f.zip
Add FIC decoder, present ensemble info for monitoring
Diffstat (limited to 'src')
-rw-r--r--src/CharsetTools.cpp143
-rw-r--r--src/CharsetTools.h58
-rw-r--r--src/DabMod.cpp69
-rw-r--r--src/EtiReader.cpp17
-rw-r--r--src/EtiReader.h11
-rw-r--r--src/FigParser.cpp1047
-rw-r--r--src/FigParser.h385
7 files changed, 1723 insertions, 7 deletions
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/DabMod.cpp b/src/DabMod.cpp
index d43ebd5..0ab112b 100644
--- a/src/DabMod.cpp
+++ b/src/DabMod.cpp
@@ -115,6 +115,9 @@ class ModulatorData : public RemoteControllable {
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(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(num_services, "(Read-only) Number of services in the ensemble");
}
virtual ~ModulatorData() {}
@@ -134,6 +137,42 @@ class ModulatorData : public RemoteControllable {
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 == "num_services") {
+ if (ediInput) {
+ ss << ediInput->ediReader.getSubchannels().size();
+ }
+ else {
+ throw ParameterError("Not available yet");
+ }
+ }
else {
ss << "Parameter '" << parameter <<
"' is not exported by controllable " << get_rc_name();
@@ -148,6 +187,36 @@ class ModulatorData : public RemoteControllable {
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["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;
+ json::value_t v;
+ v.v = service_map;
+ services.push_back(v);
+ }
+
+ map["ensemble_services"].v = services;
+
+ }
return map;
}
diff --git a/src/EtiReader.cpp b/src/EtiReader.cpp
index 580088b..244cc18 100644
--- a/src/EtiReader.cpp
+++ b/src/EtiReader.cpp
@@ -228,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;
@@ -295,9 +295,9 @@ uint32_t EtiReader::getPPSOffset()
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);
}
@@ -411,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(
@@ -463,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");
@@ -609,7 +612,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);
}
diff --git a/src/EtiReader.h b/src/EtiReader.h
index fb2c84c..29091bd 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"
@@ -174,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;
@@ -197,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
diff --git a/src/FigParser.cpp b/src/FigParser.cpp
new file mode 100644
index 0000000..bda2f83
--- /dev/null
+++ b/src/FigParser.cpp
@@ -0,0 +1,1047 @@
+/*
+ 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)
+{
+ 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);
+};
+