From c1599cb19601443c48c45c5ad8d1b0d40a6238bc Mon Sep 17 00:00:00 2001 From: Stefan Pöschel Date: Sun, 24 Apr 2016 22:18:56 +0200 Subject: DLS: add DL Plus support - adds support for Dynamic Label Plus to mot-encoder through a new parameter block which prepends the DLS text within the regarding file - adds an option to add DL Plus data to the VLC input ICY text of dabplus-enc --- src/VLCInput.cpp | 26 ++++- src/VLCInput.h | 7 +- src/dabplus-enc.cpp | 68 +++++++------ src/mot-encoder.cpp | 278 ++++++++++++++++++++++++++++++++++++++++++++++++---- src/utils.c | 10 ++ src/utils.h | 3 + 6 files changed, 334 insertions(+), 58 deletions(-) diff --git a/src/VLCInput.cpp b/src/VLCInput.cpp index 468ef70..fc7e07f 100644 --- a/src/VLCInput.cpp +++ b/src/VLCInput.cpp @@ -337,20 +337,36 @@ ssize_t VLCInputDirect::read(uint8_t* buf, size_t length) return read; } -bool write_icy_to_file(const std::string& text, const std::string& filename) +bool write_icy_to_file(const std::string& text, const std::string& filename, bool dl_plus) { FILE* fd = fopen(filename.c_str(), "wb"); if (fd) { - int ret = fputs(text.c_str(), fd); + bool ret = true; + + // if desired, prepend DL Plus information + if (dl_plus) { + std::stringstream ss; + ss << "##### parameters { #####\n"; + ss << "DL_PLUS=1\n"; + + // if non-empty text, add PROGRAMME.NOW tag + if (!text.empty()) + ss << "DL_PLUS_TAG=33 0 " << (strlen_utf8(text.c_str()) - 1) << "\n"; // -1 ! + + ss << "##### parameters } #####\n"; + ret &= fputs(ss.str().c_str(), fd) >= 0; + } + + ret &= fputs(text.c_str(), fd) >= 0; fclose(fd); - return ret >= 0; + return ret; } return false; } -void VLCInput::write_icy_text(const std::string& filename) +void VLCInput::write_icy_text(const std::string& filename, bool dl_plus) { if (icy_text_written.valid()) { auto status = icy_text_written.wait_for(std::chrono::microseconds(1)); @@ -364,7 +380,7 @@ void VLCInput::write_icy_text(const std::string& filename) else { if (m_nowplaying_previous != m_nowplaying) { icy_text_written = std::async(std::launch::async, - std::bind(write_icy_to_file, m_nowplaying, filename)); + std::bind(write_icy_to_file, m_nowplaying, filename, dl_plus)); } diff --git a/src/VLCInput.h b/src/VLCInput.h index ffa9258..3467525 100644 --- a/src/VLCInput.h +++ b/src/VLCInput.h @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -36,6 +37,10 @@ #include "SampleQueue.h" #include "common.h" +extern "C" { +#include "utils.h" +} + /* Common functionality for the direct libvlc input and the * threaded libvlc input */ @@ -67,7 +72,7 @@ class VLCInput /* Write the last received ICY-Text to the * file. */ - void write_icy_text(const std::string& filename); + void write_icy_text(const std::string& filename, bool dl_plus); // Callbacks for VLC diff --git a/src/dabplus-enc.cpp b/src/dabplus-enc.cpp index 81eb24b..0385930 100644 --- a/src/dabplus-enc.cpp +++ b/src/dabplus-enc.cpp @@ -126,6 +126,7 @@ void usage(const char* name) { " -L OPTION Give an additional options to VLC (can be given\n" " multiple times)\n" " -w, --write-icy-text=filename Write the ICY Text into the file, so that mot-encoder can read it.\n" + " -W, --write-icy-text-dl-plus When writing the ICY Text into the file, add DL Plus information.\n" #else " The VLC input was disabled at compile-time\n" #endif @@ -306,6 +307,7 @@ int main(int argc, char *argv[]) // For the VLC input std::string vlc_uri = ""; std::string vlc_icytext_file = ""; + bool vlc_icytext_dlplus = false; std::string vlc_gain = ""; std::string vlc_cache = ""; std::vector vlc_additional_opts; @@ -355,35 +357,36 @@ int main(int argc, char *argv[]) char secretkey[CURVE_KEYLEN+1]; const struct option longopts[] = { - {"bitrate", required_argument, 0, 'b'}, - {"channels", required_argument, 0, 'c'}, - {"dabmode", required_argument, 0, 4 }, - {"dabpsy", required_argument, 0, 5 }, - {"device", required_argument, 0, 'd'}, - {"format", required_argument, 0, 'f'}, - {"input", required_argument, 0, 'i'}, - {"jack", required_argument, 0, 'j'}, - {"output", required_argument, 0, 'o'}, - {"pad", required_argument, 0, 'p'}, - {"pad-fifo", required_argument, 0, 'P'}, - {"rate", required_argument, 0, 'r'}, - {"secret-key", required_argument, 0, 'k'}, - {"silence", required_argument, 0, 's'}, - {"vlc-cache", required_argument, 0, 'C'}, - {"vlc-gain", required_argument, 0, 'g'}, - {"vlc-uri", required_argument, 0, 'v'}, - {"vlc-opt", required_argument, 0, 'L'}, - {"write-icy-text", required_argument, 0, 'w'}, - {"aaclc", no_argument, 0, 0 }, - {"dab", no_argument, 0, 'a'}, - {"drift-comp", no_argument, 0, 'D'}, - {"fifo-silence", no_argument, 0, 3 }, - {"help", no_argument, 0, 'h'}, - {"level", no_argument, 0, 'l'}, - {"no-afterburner", no_argument, 0, 'A'}, - {"ps", no_argument, 0, 2 }, - {"sbr", no_argument, 0, 1 }, - {"verbosity", no_argument, 0, 'V'}, + {"bitrate", required_argument, 0, 'b'}, + {"channels", required_argument, 0, 'c'}, + {"dabmode", required_argument, 0, 4 }, + {"dabpsy", required_argument, 0, 5 }, + {"device", required_argument, 0, 'd'}, + {"format", required_argument, 0, 'f'}, + {"input", required_argument, 0, 'i'}, + {"jack", required_argument, 0, 'j'}, + {"output", required_argument, 0, 'o'}, + {"pad", required_argument, 0, 'p'}, + {"pad-fifo", required_argument, 0, 'P'}, + {"rate", required_argument, 0, 'r'}, + {"secret-key", required_argument, 0, 'k'}, + {"silence", required_argument, 0, 's'}, + {"vlc-cache", required_argument, 0, 'C'}, + {"vlc-gain", required_argument, 0, 'g'}, + {"vlc-uri", required_argument, 0, 'v'}, + {"vlc-opt", required_argument, 0, 'L'}, + {"write-icy-text", required_argument, 0, 'w'}, + {"write-icy-text-dl-plus", no_argument, 0, 'W'}, + {"aaclc", no_argument, 0, 0 }, + {"dab", no_argument, 0, 'a'}, + {"drift-comp", no_argument, 0, 'D'}, + {"fifo-silence", no_argument, 0, 3 }, + {"help", no_argument, 0, 'h'}, + {"level", no_argument, 0, 'l'}, + {"no-afterburner", no_argument, 0, 'A'}, + {"ps", no_argument, 0, 2 }, + {"sbr", no_argument, 0, 1 }, + {"verbosity", no_argument, 0, 'V'}, {0, 0, 0, 0}, }; @@ -407,7 +410,7 @@ int main(int argc, char *argv[]) int index; while(ch != -1) { - ch = getopt_long(argc, argv, "aAhDlVb:c:f:i:j:k:L:o:r:d:p:P:s:v:w:g:C:", longopts, &index); + ch = getopt_long(argc, argv, "aAhDlVb:c:f:i:j:k:L:o:r:d:p:P:s:v:w:Wg:C:", longopts, &index); switch (ch) { case 0: // AAC-LC aot = AOT_DABPLUS_AAC_LC; @@ -496,6 +499,9 @@ int main(int argc, char *argv[]) case 'w': vlc_icytext_file = optarg; break; + case 'W': + vlc_icytext_dlplus = true; + break; case 'g': vlc_gain = optarg; break; @@ -929,7 +935,7 @@ int main(int argc, char *argv[]) } if (not vlc_icytext_file.empty()) { - vlc_in->write_icy_text(vlc_icytext_file); + vlc_in->write_icy_text(vlc_icytext_file, vlc_icytext_dlplus); } } #endif diff --git a/src/mot-encoder.cpp b/src/mot-encoder.cpp index 4268d58..28e6ce8 100644 --- a/src/mot-encoder.cpp +++ b/src/mot-encoder.cpp @@ -3,7 +3,7 @@ Copyright (C) 2014, 2015 Matthias P. Braendli (http://opendigitalradio.org) - Copyright (C) 2015 Stefan Pöschel (http://opendigitalradio.org) + Copyright (C) 2015, 2016 Stefan Pöschel (http://opendigitalradio.org) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -224,7 +224,8 @@ void createMscDG(MSCDG* msc, unsigned short int dgtype, int *cindex, unsigned sh struct DATA_GROUP; DATA_GROUP* packMscDG(MSCDG* msc); -void prepend_dls_dgs(const std::string& text, uint8_t charset); +struct DL_STATE; +void prepend_dl_dgs(const DL_STATE& dl_state, uint8_t charset); void writeDLS(int output_fd, const std::string& dls_file, uint8_t charset, bool raw_dls, bool remove_dls); // PAD related @@ -389,15 +390,84 @@ void MOTHeader::AddExtensionVarSize(int param_id, const uint8_t* data_field, siz #define FPAD_LEN 2 #define DLS_SEG_LEN_PREFIX 2 #define DLS_SEG_LEN_CHAR_MAX 16 -#define DLS_CMD_REMOVE_LABEL 0x01 +#define DLS_CMD_REMOVE_LABEL 0b0001 +#define DLS_CMD_DL_PLUS 0b0010 +#define DL_PLUS_CMD_TAGS 0b0000 + +#define DL_PARAMS_OPEN "##### parameters { #####" +#define DL_PARAMS_CLOSE "##### parameters } #####" CharsetConverter charset_converter; +struct DL_PLUS_TAG { + int content_type; + int start_marker; + int length_marker; + + DL_PLUS_TAG() : + content_type(0), // = DUMMY + start_marker(0), + length_marker(0) + {} + + DL_PLUS_TAG(int content_type, int start_marker, int length_marker) : + content_type(content_type), + start_marker(start_marker), + length_marker(length_marker) + {} + + bool operator==(const DL_PLUS_TAG& other) const { + return + content_type == other.content_type && + start_marker == other.start_marker && + length_marker == other.length_marker; + } + bool operator!=(const DL_PLUS_TAG& other) const { + return !(*this == other); + } +}; + +typedef std::vector dl_plus_tags_t; + +struct DL_STATE { + std::string dl_text; + + bool dl_plus_enabled; + bool dl_plus_item_toggle; + bool dl_plus_item_running; + dl_plus_tags_t dl_plus_tags; + + DL_STATE() : + dl_plus_enabled(false), + dl_plus_item_toggle(false), + dl_plus_item_running(false) + {} + + bool operator==(const DL_STATE& other) const { + if (dl_text != other.dl_text) + return false; + if (dl_plus_enabled != other.dl_plus_enabled) + return false; + if (dl_plus_enabled) { + if (dl_plus_item_toggle != other.dl_plus_item_toggle) + return false; + if (dl_plus_item_running != other.dl_plus_item_running) + return false; + if (dl_plus_tags != other.dl_plus_tags) + return false; + } + return true; + } + bool operator!=(const DL_STATE& other) const { + return !(*this == other); + } +}; + + typedef uint8_vector_t pad_t; static bool dls_toggle = false; -static std::string dlstext_prev = ""; -static bool dlstext_prev_set = false; - +static bool dl_plus_toggle = false; +static DL_STATE dl_state_prev; class PADPacketizer { @@ -1375,21 +1445,163 @@ DATA_GROUP* createDynamicLabelCommand(uint8_t command) { return dg; } +DATA_GROUP* createDynamicLabelPlus(const DL_STATE& dl_state) { + size_t tags_size = dl_state.dl_plus_tags.size(); + size_t len_dl_plus_cmd_field = 1 + 3 * tags_size; + DATA_GROUP* dg = new DATA_GROUP(2 + len_dl_plus_cmd_field, 2, 3); + uint8_vector_t &seg_data = dg->data; + + // prefix: toggle? + first seg + last seg + command flag + command + seg_data[0] = + (dl_plus_toggle ? (1 << 7) : 0) + + (1 << 6) + + (1 << 5) + + (1 << 4) + + DLS_CMD_DL_PLUS; + + // prefix: link bit + length + seg_data[1] = + (dls_toggle ? (1 << 7) : 0) + + (len_dl_plus_cmd_field - 1); // -1 ! + + // DL Plus tags command: CId + IT + IR + NT + seg_data[2] = + (DL_PLUS_CMD_TAGS << 4) + + (dl_state.dl_plus_item_toggle ? (1 << 3) : 0) + + (dl_state.dl_plus_item_running ? (1 << 2) : 0) + + (tags_size - 1); // -1 ! + + for (size_t i = 0; i < tags_size; i++) { + // DL Plus tags command: Content Type + Start Marker + Length Marker + seg_data[3 + 3 * i] = dl_state.dl_plus_tags[i].content_type & 0x7F; + seg_data[4 + 3 * i] = dl_state.dl_plus_tags[i].start_marker & 0x7F; + seg_data[5 + 3 * i] = dl_state.dl_plus_tags[i].length_marker & 0x7F; + } + + // CRC + dg->AppendCRC(); + + return dg; +} + + +std::vector split_string(const std::string &s, const char delimiter) { + std::vector result; + std::stringstream ss(s); + std::string part; + + while (std::getline(ss, part, delimiter)) + result.push_back(part); + return result; +} + +bool parse_dl_param_bool(const std::string &key, const std::string &value, bool &target) { + if (value == "0") { + target = 0; + return true; + } + if (value == "1") { + target = 1; + return true; + } + fprintf(stderr, "mot-encoder Warning: DL parameter '%s' has unsupported value '%s' - ignored\n", key.c_str(), value.c_str()); + return false; +} + +bool parse_dl_param_int_dl_plus_tag(const std::string &key, const std::string &value, int &target) { + int value_int = atoi(value.c_str()); + if (value_int >= 0x00 && value_int <= 0x7F) { + target = value_int; + return true; + } + fprintf(stderr, "mot-encoder Warning: DL Plus tag parameter '%s' %d out of range - ignored\n", key.c_str(), value_int); + return false; +} + +void parse_dl_params(std::ifstream &dls_fstream, DL_STATE &dl_state) { + std::string line; + while (std::getline(dls_fstream, line)) { + // return on params close + if (line == DL_PARAMS_CLOSE) + return; + + // ignore empty lines and comments + if (line.empty() || line[0] == '#') + continue; + + // parse key/value pair + size_t separator_pos = line.find('='); + if (separator_pos == std::string::npos) { + fprintf(stderr, "mot-encoder Warning: DL parameter line '%s' without separator - ignored\n", line.c_str()); + continue; + } + std::string key = line.substr(0, separator_pos); + std::string value = line.substr(separator_pos + 1); +#if DEBUG + fprintf(stderr, "parse_dl_params: key: '%s', value: '%s'\n", key.c_str(), value.c_str()); +#endif + + if (key == "DL_PLUS") { + parse_dl_param_bool(key, value, dl_state.dl_plus_enabled); + continue; + } + if (key == "DL_PLUS_ITEM_TOGGLE") { + parse_dl_param_bool(key, value, dl_state.dl_plus_item_toggle); + continue; + } + if (key == "DL_PLUS_ITEM_RUNNING") { + parse_dl_param_bool(key, value, dl_state.dl_plus_item_running); + continue; + } + if (key == "DL_PLUS_TAG") { + if (dl_state.dl_plus_tags.size() == 4) { + fprintf(stderr, "mot-encoder Warning: DL Plus tag ignored, as already four tags present\n"); + continue; + } + + // split value + std::vector params = split_string(value, ' '); + if (params.size() != 3) { + fprintf(stderr, "mot-encoder Warning: DL Plus tag value '%s' does not have three parts - ignored\n", value.c_str()); + continue; + } + + int content_type, start_marker, length_marker; + if (parse_dl_param_int_dl_plus_tag("content_type", params[0], content_type) & + parse_dl_param_int_dl_plus_tag("start_marker", params[1], start_marker) & + parse_dl_param_int_dl_plus_tag("length_marker", params[2], length_marker)) + dl_state.dl_plus_tags.push_back(DL_PLUS_TAG(content_type, start_marker, length_marker)); + continue; + } + + fprintf(stderr, "mot-encoder Warning: DL parameter '%s' unknown - ignored\n", key.c_str()); + } + + fprintf(stderr, "mot-encoder Warning: no param closing tag, so the DLS text will be empty\n"); +} + + void writeDLS(int output_fd, const std::string& dls_file, uint8_t charset, bool raw_dls, bool remove_dls) { - std::ifstream dls_fstream(dls_file.c_str()); + std::ifstream dls_fstream(dls_file); if (!dls_fstream.is_open()) { std::cerr << "Could not open " << dls_file << std::endl; return; } + DL_STATE dl_state; + std::vector dls_lines; std::string line; // Read and convert lines one by one because the converter doesn't understand // line endings while (std::getline(dls_fstream, line)) { - if (not line.empty()) { + if (line.empty()) + continue; + if (line == DL_PARAMS_OPEN) { + parse_dl_params(dls_fstream, dl_state); + } else { if (not raw_dls && charset == CHARSET_UTF8) { dls_lines.push_back(charset_converter.convert(line)); } @@ -1416,32 +1628,52 @@ void writeDLS(int output_fd, const std::string& dls_file, uint8_t charset, bool ss << dls_lines[i]; } - std::string dlstext = ss.str(); - if (dlstext.size() > MAXDLS) - dlstext.resize(MAXDLS); + + dl_state.dl_text = ss.str(); + if (dl_state.dl_text.size() > MAXDLS) + dl_state.dl_text.resize(MAXDLS); + + + // if DL Plus enabled, but no DL Plus tags were added, add the required DUMMY tag + if (dl_state.dl_plus_enabled && dl_state.dl_plus_tags.empty()) + dl_state.dl_plus_tags.push_back(DL_PLUS_TAG()); if (not raw_dls) charset = CHARSET_COMPLETE_EBU_LATIN; - // Toggle the toggle bit only on (first call or) new text - bool dlstext_is_new = !dlstext_prev_set || (dlstext != dlstext_prev); + // toggle the toggle bit only on new DL state + bool dl_state_is_new = dl_state != dl_state_prev; if (verbose) { - fprintf(stderr, "mot-encoder writing %s DLS text \"%s\"\n", dlstext_is_new ? "new" : "old", dlstext.c_str()); + fprintf(stderr, "mot-encoder writing %s DLS text \"%s\"\n", dl_state_is_new ? "new" : "old", dl_state.dl_text.c_str()); + if (dl_state.dl_plus_enabled) { + fprintf( + stderr, "mot-encoder writing %s DL Plus tags (IT/IR: %d/%d): ", + dl_state_is_new ? "new" : "old", + dl_state.dl_plus_item_toggle ? 1 : 0, + dl_state.dl_plus_item_running ? 1 : 0); + for (dl_plus_tags_t::const_iterator it = dl_state.dl_plus_tags.begin(); it != dl_state.dl_plus_tags.end(); it++) { + if (it != dl_state.dl_plus_tags.begin()) + fprintf(stderr, ", "); + fprintf(stderr, "%d (S/L: %d/%d)", it->content_type, it->start_marker, it->length_marker); + } + fprintf(stderr, "\n"); + } } DATA_GROUP *remove_label_dg = NULL; - if (dlstext_is_new) { + if (dl_state_is_new) { if (remove_dls) remove_label_dg = createDynamicLabelCommand(DLS_CMD_REMOVE_LABEL); dls_toggle = !dls_toggle; // indicate changed text + if (dl_state.dl_plus_enabled) + dl_plus_toggle = !dl_plus_toggle; - dlstext_prev = dlstext; - dlstext_prev_set = true; + dl_state_prev = dl_state; } - prepend_dls_dgs(dlstext, charset); + prepend_dl_dgs(dl_state, charset); if (remove_label_dg) pad_packetizer->queue.push_front(remove_label_dg); pad_packetizer->WriteAllPADs(output_fd); @@ -1491,17 +1723,21 @@ DATA_GROUP* dls_get(const std::string& text, uint8_t charset, unsigned int seg_i } -void prepend_dls_dgs(const std::string& text, uint8_t charset) { +void prepend_dl_dgs(const DL_STATE& dl_state, uint8_t charset) { // process all DL segments - int seg_count = dls_count(text); + int seg_count = dls_count(dl_state.dl_text); std::vector segs; for (int seg_index = 0; seg_index < seg_count; seg_index++) { #if DEBUG fprintf(stderr, "Segment number %d\n", seg_index + 1); #endif - segs.push_back(dls_get(text, charset, seg_index)); + segs.push_back(dls_get(dl_state.dl_text, charset, seg_index)); } + // if enabled, add DL Plus data group + if (dl_state.dl_plus_enabled) + segs.push_back(createDynamicLabelPlus(dl_state)); + // prepend to packetizer pad_packetizer->queue.insert(pad_packetizer->queue.begin(), segs.begin(), segs.end()); diff --git a/src/utils.c b/src/utils.c index 0168a23..24da427 100644 --- a/src/utils.c +++ b/src/utils.c @@ -28,3 +28,13 @@ const char* level(int channel, int peak) return text[index][channel]; } +size_t strlen_utf8(const char *s) { + size_t result = 0; + + // ignore continuation bytes - only count single/leading bytes + while (*s) + if ((*s++ & 0xC0) != 0x80) + result++; + + return result; +} diff --git a/src/utils.h b/src/utils.h index a0ab1ae..e411963 100644 --- a/src/utils.h +++ b/src/utils.h @@ -3,6 +3,7 @@ #include #include +#include #define MIN(a,b) (((a)<(b))?(a):(b)) #define MAX(a,b) (((a)>(b))?(a):(b)) @@ -45,5 +46,7 @@ struct zmq_frame_header_t #define ZMQ_FRAME_DATA(f) ( ((uint8_t*)f)+sizeof(struct zmq_frame_header_t) ) +size_t strlen_utf8(const char *s); + #endif -- cgit v1.2.3