From c2467d222ec08ddc4c6f79ea01773496090f809f Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Tue, 31 Oct 2023 23:02:10 +0100 Subject: Add FIC decoder, present ensemble info for monitoring --- src/CharsetTools.cpp | 143 +++++++ src/CharsetTools.h | 58 +++ src/DabMod.cpp | 69 ++++ src/EtiReader.cpp | 17 +- src/EtiReader.h | 11 + src/FigParser.cpp | 1047 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/FigParser.h | 385 +++++++++++++++++++ 7 files changed, 1723 insertions(+), 7 deletions(-) create mode 100644 src/CharsetTools.cpp create mode 100644 src/CharsetTools.h create mode 100644 src/FigParser.cpp create mode 100644 src/FigParser.h (limited to 'src') 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 . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#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 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 . + */ + +#pragma once +#include +#include +#include +#include +#include +#include +#include + +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 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 services; + + for (const auto& s : ediInput->ediReader.getServiceInfo()) { + auto service_map = make_shared(); + (*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&& 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 getEnsembleInfo() const { + return m_fic_decoder.observer.ensemble; + } + + std::map 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 > 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 . + */ + +#include "FigParser.h" +#include "PcDebug.h" +#include "Log.h" +#include "crc.h" +#include "CharsetTools.h" + +#include +#include +#include +#include +#include + + +template +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 . + */ + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 audio_comps_t; +typedef std::map comp_defs_t; +typedef std::map comp_labels_t; +typedef std::vector ua_data_t; +typedef std::map comp_sls_uas_t; +typedef std::set 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 fic_services_t; +typedef std::map fic_subchannels_t; + +// --- FICDecoderObserver ----------------------------------------------------------------- +class FICDecoderObserver { + public: + virtual ~FICDecoderObserver() {} + + std::optional ensemble; + std::optional utc_dt; + std::map 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); +}; + -- cgit v1.2.3