aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias P. Braendli <matthias.braendli@mpb.li>2020-03-31 17:07:38 +0200
committerMatthias P. Braendli <matthias.braendli@mpb.li>2020-03-31 17:07:38 +0200
commitec75b9e317baf249d67295300bc5308b7c33f4ac (patch)
tree6f43693530b463fc913f7c7153a3f54a43ebd04b
parenta1eb6cf861d3c1cbd4e6c016be3cbd2a1e3d797d (diff)
downloadODR-AudioEnc-ec75b9e317baf249d67295300bc5308b7c33f4ac.tar.gz
ODR-AudioEnc-ec75b9e317baf249d67295300bc5308b7c33f4ac.tar.bz2
ODR-AudioEnc-ec75b9e317baf249d67295300bc5308b7c33f4ac.zip
Fix GStreamer input, rework ICY-Text write
-rw-r--r--Makefile.am5
-rw-r--r--configure.ac2
-rw-r--r--src/GSTInput.cpp112
-rw-r--r--src/GSTInput.h19
-rw-r--r--src/VLCInput.cpp86
-rw-r--r--src/VLCInput.h42
-rw-r--r--src/odr-audioenc.cpp55
-rw-r--r--src/utils.c43
-rw-r--r--src/utils.cpp96
-rw-r--r--src/utils.h45
10 files changed, 302 insertions, 203 deletions
diff --git a/Makefile.am b/Makefile.am
index d05aa7f..29ac878 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -82,6 +82,9 @@ odr_audioenc_LDADD = libtoolame-dab.la \
$(odr_audioenc_LDADD_ALSA) \
$(LIBVLC_LIBS) $(GST_LIBS)
odr_audioenc_CXXFLAGS = $(GST_CFLAGS) $(GITVERSION_FLAGS) \
+ -Ifdk-aac/libSYS/include/ \
+ -Ifdk-aac/libAACenc/include/ \
+ -Ifdk-aac/libAACdec/include/ \
-Wall -ggdb -O2 -Isrc -Icontrib
odr_audioenc_SOURCES = src/odr-audioenc.cpp \
@@ -105,7 +108,7 @@ odr_audioenc_SOURCES = src/odr-audioenc.cpp \
src/encryption.c \
src/encryption.h \
src/zmq.hpp \
- src/utils.c \
+ src/utils.cpp \
src/utils.h \
src/wavfile.cpp \
src/common.h \
diff --git a/configure.ac b/configure.ac
index 5501032..17963a3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -59,6 +59,7 @@ AS_IF([test "x$enable_gst" = "xyes"],
[PKG_CHECK_MODULES(GST, [
gstreamer-1.0 >= $GST_REQUIRED
gstreamer-base-1.0 >= $GST_REQUIRED
+ gstreamer-app-1.0 >= $GST_REQUIRED
gstreamer-controller-1.0 >= $GST_REQUIRED
gstreamer-audio-1.0 >= $GST_REQUIRED
], [
@@ -70,6 +71,7 @@ AS_IF([test "x$enable_gst" = "xyes"],
gstreamer-1.0 >= $GST_REQUIRED
gstreamer-base-1.0 >= $GST_REQUIRED
+ gstreamer-app-1.0 >= $GST_REQUIRED
gstreamer-controller-1.0 >= $GST_REQUIRED
gstreamer-audio-1.0 >= $GST_REQUIRED
diff --git a/src/GSTInput.cpp b/src/GSTInput.cpp
index 41fbfc0..bc7d44b 100644
--- a/src/GSTInput.cpp
+++ b/src/GSTInput.cpp
@@ -26,6 +26,7 @@
#include <cstring>
#include <gst/audio/audio.h>
+#include <gst/app/gstappsink.h>
#include "GSTInput.h"
@@ -46,8 +47,7 @@ GSTInput::GSTInput(const std::string& uri,
m_uri(uri),
m_channels(channels),
m_rate(rate),
- m_gst_data(queue),
- m_samplequeue(queue)
+ m_gst_data(queue)
{ }
static void error_cb(GstBus *bus, GstMessage *msg, GSTData *data)
@@ -61,8 +61,6 @@ static void error_cb(GstBus *bus, GstMessage *msg, GSTData *data)
g_printerr("Debugging information: %s\n", debug_info ? debug_info : "none");
g_clear_error(&err);
g_free(debug_info);
-
- g_main_loop_quit(data->main_loop);
}
static void cb_newpad(GstElement *decodebin, GstPad *pad, GSTData *data)
@@ -89,10 +87,9 @@ static void cb_newpad(GstElement *decodebin, GstPad *pad, GSTData *data)
g_object_unref(audiopad);
}
-static GstFlowReturn new_sample (GstElement *sink, GSTData *data) {
- GstSample *sample;
+static GstFlowReturn new_sample(GstElement *sink, GSTData *data) {
/* Retrieve the buffer */
- g_signal_emit_by_name(sink, "pull-sample", &sample);
+ GstSample* sample = gst_app_sink_pull_sample(GST_APP_SINK(sink));
if (sample) {
GstBuffer* buffer = gst_sample_get_buffer(sample);
@@ -121,6 +118,14 @@ void GSTInput::prepare()
m_gst_data.audio_convert = gst_element_factory_make("audioconvert", "audio_convert");
assert(m_gst_data.audio_convert != nullptr);
+ m_gst_data.audio_resample = gst_element_factory_make("audioresample", "audio_resample");
+ assert(m_gst_data.audio_resample != nullptr);
+ g_object_set(m_gst_data.audio_resample,
+ "sinc-filter-mode", GST_AUDIO_RESAMPLER_FILTER_MODE_FULL,
+ "quality", 6, // between 0 and 10, 10 being best
+ /* default audio-resampler-method: GST_AUDIO_RESAMPLER_METHOD_KAISER */
+ NULL);
+
m_gst_data.caps_filter = gst_element_factory_make("capsfilter", "caps_filter");
assert(m_gst_data.caps_filter != nullptr);
@@ -135,6 +140,7 @@ void GSTInput::prepare()
m_gst_data.pipeline = gst_pipeline_new("pipeline");
assert(m_gst_data.pipeline != nullptr);
+ // TODO also set max-buffers
g_object_set(m_gst_data.app_sink, "emit-signals", TRUE, "caps", audio_caps, NULL);
g_signal_connect(m_gst_data.app_sink, "new-sample", G_CALLBACK(new_sample), &m_gst_data);
gst_caps_unref(audio_caps);
@@ -142,11 +148,13 @@ void GSTInput::prepare()
gst_bin_add_many(GST_BIN(m_gst_data.pipeline),
m_gst_data.uridecodebin,
m_gst_data.audio_convert,
+ m_gst_data.audio_resample,
m_gst_data.caps_filter,
m_gst_data.app_sink, NULL);
if (gst_element_link_many(
m_gst_data.audio_convert,
+ m_gst_data.audio_resample,
m_gst_data.caps_filter,
m_gst_data.app_sink, NULL) != true) {
throw runtime_error("Could not link GST elements");
@@ -157,23 +165,101 @@ void GSTInput::prepare()
g_signal_connect(G_OBJECT(m_gst_data.bus), "message::error", (GCallback)error_cb, &m_gst_data);
gst_element_set_state(m_gst_data.pipeline, GST_STATE_PLAYING);
+
+ m_running = true;
+ m_thread = std::thread(&GSTInput::process, this);
}
bool GSTInput::read_source(size_t num_bytes)
{
- // Reading done in glib main loop
- GstMessage *msg = gst_bus_pop_filtered(m_gst_data.bus, GST_MESSAGE_EOS);
+ return m_running;
+}
- if (msg) {
+ICY_TEXT_t GSTInput::get_icy_text() const
+{
+ ICY_TEXT_t now_playing;
+ {
+ std::lock_guard<std::mutex> lock(m_nowplaying_mutex);
+ now_playing = m_nowplaying;
+ }
+
+ return now_playing;
+}
+
+void GSTInput::process()
+{
+ while (m_running) {
+ GstMessage *msg = gst_bus_timed_pop(m_gst_data.bus, 100000);
+
+ if (not msg) {
+ continue;
+ }
+
+ switch (GST_MESSAGE_TYPE(msg)) {
+ case GST_MESSAGE_BUFFERING:
+ {
+ gint percent = 0;
+ gst_message_parse_buffering(msg, &percent);
+ //fprintf(stderr, "GST buffering %d\n", percent);
+ break;
+ }
+ case GST_MESSAGE_TAG:
+ {
+ GstTagList *tags = nullptr;
+ gst_message_parse_tag(msg, &tags);
+ //fprintf(stderr, "Got tags from element %s\n", GST_OBJECT_NAME(msg->src));
+
+ string new_title;
+
+ auto extract_title = [](const GstTagList *list, const gchar *tag, void *user_data) {
+ GValue val = { 0, };
+
+ auto new_title = (string*)user_data;
+
+ gst_tag_list_copy_value(&val, list, tag);
+
+ if (strcmp(tag, "title") == 0 and G_VALUE_HOLDS_STRING(&val)) {
+ *new_title = g_value_dup_string(&val);
+ }
+
+ g_value_unset(&val);
+ };
+
+ gst_tag_list_foreach(tags, extract_title, &new_title);
+
+ gst_tag_list_unref(tags);
+ {
+ std::lock_guard<std::mutex> lock(m_nowplaying_mutex);
+ m_nowplaying.useNowPlaying(new_title);
+ }
+ break;
+ }
+ case GST_MESSAGE_ERROR:
+ {
+ GError *err = nullptr;
+ gst_message_parse_error(msg, &err, nullptr);
+ fprintf(stderr, "GST error: %s\n", err->message);
+ g_error_free(err);
+ m_fault = true;
+ break;
+ }
+ case GST_MESSAGE_EOS:
+ m_fault = true;
+ break;
+ default:
+ //fprintf(stderr, "GST message %s\n", gst_message_type_get_name(GST_MESSAGE_TYPE(msg)));
+ break;
+ }
gst_message_unref(msg);
- return false;
}
- return true;
}
GSTInput::~GSTInput()
{
- fprintf(stderr, "<<<<<<<<<<<<<<<<<<<< DTOR\n");
+ m_running = false;
+ if (m_thread.joinable()) {
+ m_thread.join();
+ }
if (m_gst_data.bus) {
gst_object_unref(m_gst_data.bus);
diff --git a/src/GSTInput.h b/src/GSTInput.h
index 07cf62e..4bfae34 100644
--- a/src/GSTInput.h
+++ b/src/GSTInput.h
@@ -27,6 +27,8 @@
#if HAVE_GST
#include <string>
+#include <atomic>
+#include <thread>
#include <vector>
#include <cstddef>
#include <cstdint>
@@ -36,10 +38,7 @@
#include "SampleQueue.h"
#include "common.h"
#include "InputInterface.h"
-
-extern "C" {
#include "utils.h"
-}
struct GSTData {
GSTData(SampleQueue<uint8_t>& samplequeue);
@@ -47,11 +46,11 @@ struct GSTData {
GstElement *pipeline = nullptr;
GstElement *uridecodebin = nullptr;
GstElement *audio_convert = nullptr;
+ GstElement *audio_resample = nullptr;
GstElement *caps_filter = nullptr;
GstElement *app_sink = nullptr;
GstBus *bus = nullptr;
- GMainLoop *main_loop = nullptr;
SampleQueue<uint8_t>& samplequeue;
};
@@ -72,9 +71,11 @@ class GSTInput : public InputInterface
virtual bool read_source(size_t num_bytes) override;
+ ICY_TEXT_t get_icy_text() const;
+
int getRate() { return m_rate; }
- virtual bool fault_detected(void) const override { return false; };
+ virtual bool fault_detected(void) const override { return m_fault; };
private:
std::string m_uri;
unsigned m_channels;
@@ -82,7 +83,13 @@ class GSTInput : public InputInterface
GSTData m_gst_data;
- SampleQueue<uint8_t>& m_samplequeue;
+ mutable std::mutex m_nowplaying_mutex;
+ ICY_TEXT_t m_nowplaying;
+
+ void process();
+ std::atomic<bool> m_fault = ATOMIC_VAR_INIT(false);
+ std::atomic<bool> m_running;
+ std::thread m_thread;
};
#endif // HAVE_GST
diff --git a/src/VLCInput.cpp b/src/VLCInput.cpp
index d2ae4f0..7b10d81 100644
--- a/src/VLCInput.cpp
+++ b/src/VLCInput.cpp
@@ -431,91 +431,17 @@ ssize_t VLCInput::m_read(uint8_t* buf, size_t length)
return err;
}
-const std::string VLCInput::ICY_TEXT_SEPARATOR = " - ";
-/*! Write the corresponding text to a file readable by ODR-PadEnc, with optional
- * DL+ information. The text is passed as a copy because we actually use the
- * m_nowplaying variable which is also accessed in another thread, so better
- * make a copy.
- *
- * \return false on failure
- */
-bool write_icy_to_file(const ICY_TEXT_T text, const std::string& filename, bool dl_plus)
+ICY_TEXT_t VLCInput::get_icy_text() const
{
- FILE* fd = fopen(filename.c_str(), "wb");
- if (fd) {
- bool ret = true;
- bool artist_title_used = !text.artist.empty() and !text.title.empty();
-
- // 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 tag
- if (artist_title_used) {
- size_t artist_len = strlen_utf8(text.artist.c_str());
- size_t title_start = artist_len + strlen_utf8(VLCInput::ICY_TEXT_SEPARATOR.c_str());
-
- // ITEM.ARTIST
- ss << "DL_PLUS_TAG=4 0 " << (artist_len - 1) << "\n"; // -1 !
-
- // ITEM.TITLE
- ss << "DL_PLUS_TAG=1 " << title_start << " " << (strlen_utf8(text.title.c_str()) - 1) << "\n"; // -1 !
- } else if (!text.now_playing.empty()) {
- // PROGRAMME.NOW
- ss << "DL_PLUS_TAG=33 0 " << (strlen_utf8(text.now_playing.c_str()) - 1) << "\n"; // -1 !
- }
-
- ss << "##### parameters } #####\n";
- ret &= fputs(ss.str().c_str(), fd) >= 0;
- }
-
- if (artist_title_used) {
- ret &= fputs(text.artist.c_str(), fd) >= 0;
- ret &= fputs(VLCInput::ICY_TEXT_SEPARATOR.c_str(), fd) >= 0;
- ret &= fputs(text.title.c_str(), fd) >= 0;
- }
- else {
- ret &= fputs(text.now_playing.c_str(), fd) >= 0;
- }
- fclose(fd);
-
- return ret;
- }
-
- return false;
-}
-
-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));
- if (status == std::future_status::ready) {
- if (not icy_text_written.get()) {
- fprintf(stderr, "Failed to write ICY Text to file!\n");
- }
- }
- }
-
- else {
+ ICY_TEXT_t now_playing;
+ {
std::lock_guard<std::mutex> lock(m_nowplaying_mutex);
-
- if (m_nowplaying_previous != m_nowplaying) {
- /*! We write the ICY text in a separate task because
- * we do not want to have a delay due to IO
- */
- icy_text_written = std::async(std::launch::async,
- std::bind(write_icy_to_file, m_nowplaying, filename, dl_plus));
-
- }
-
- m_nowplaying_previous = m_nowplaying;
+ now_playing = m_nowplaying;
}
-}
-
+ return now_playing;
+}
/*! How many samples we insert into the queue each call
* 10 samples @ 32kHz = 3.125ms
diff --git a/src/VLCInput.h b/src/VLCInput.h
index 0eb3e37..47a9cdd 100644
--- a/src/VLCInput.h
+++ b/src/VLCInput.h
@@ -41,42 +41,12 @@
#include "SampleQueue.h"
#include "common.h"
#include "InputInterface.h"
-
-extern "C" {
#include "utils.h"
-}
/*! Common functionality for the direct libvlc input and the
* threaded libvlc input
*/
-struct ICY_TEXT_T {
- std::string artist;
- std::string title;
- std::string now_playing;
-
- bool operator==(const ICY_TEXT_T& other) const {
- return
- artist == other.artist and
- title == other.title and
- now_playing == other.now_playing;
- }
- bool operator!=(const ICY_TEXT_T& other) const {
- return !(*this == other);
- }
- void useArtistTitle(const std::string& artist, const std::string& title) {
- this->artist = artist;
- this->title = title;
- now_playing = "";
- }
- void useNowPlaying(const std::string& now_playing) {
- artist = "";
- title = "";
- this->now_playing = now_playing;
- }
-};
-
-
class VLCInput : public InputInterface
{
public:
@@ -114,7 +84,7 @@ class VLCInput : public InputInterface
/*! Write the last received ICY-Text to the
* file.
*/
- void write_icy_text(const std::string& filename, bool dl_plus);
+ ICY_TEXT_t get_icy_text() const;
//! Callbacks for VLC
@@ -135,10 +105,6 @@ class VLCInput : public InputInterface
int getRate() { return m_rate; }
virtual bool fault_detected(void) const override { return m_fault; };
-
- /*! Separator string used when artist/title are written
- */
- static const std::string ICY_TEXT_SEPARATOR;
private:
/*! Stop the player and release resources
*/
@@ -178,10 +144,8 @@ class VLCInput : public InputInterface
/*! VLC can give us the ICY-Text from an Icecast stream,
* which we optionally write into a text file for ODR-PadEnc
*/
- std::future<bool> icy_text_written;
- std::mutex m_nowplaying_mutex;
- ICY_TEXT_T m_nowplaying;
- ICY_TEXT_T m_nowplaying_previous;
+ mutable std::mutex m_nowplaying_mutex;
+ ICY_TEXT_t m_nowplaying;
// VLC pointers
libvlc_instance_t *m_vlc;
diff --git a/src/odr-audioenc.cpp b/src/odr-audioenc.cpp
index aab76b2..e077981 100644
--- a/src/odr-audioenc.cpp
+++ b/src/odr-audioenc.cpp
@@ -61,10 +61,10 @@
#include "Outputs.h"
#include "common.h"
#include "wavfile.h"
+#include "utils.h"
extern "C" {
#include "encryption.h"
-#include "utils.h"
}
#include <algorithm>
@@ -98,9 +98,7 @@ using vec_u8 = std::vector<uint8_t>;
using namespace std;
-
-
-void usage(const char* name)
+static void usage(const char* name)
{
fprintf(stderr,
"ODR-AudioEnc %s is an audio encoder for both DAB and DAB+.\n"
@@ -160,8 +158,6 @@ void usage(const char* name)
" multiple times)\n"
" -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 ODR-PadEnc 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
@@ -171,6 +167,8 @@ void usage(const char* name)
#else
" The GStreamer input was disabled at compile-time\n"
#endif
+ " -w, --write-icy-text=filename Write the ICY Text into the file, so that ODR-PadEnc can read it.\n"
+ " -W, --write-icy-text-dl-plus When writing the ICY Text into the file, add DL Plus information.\n"
" Drift compensation\n"
" -D, --drift-comp Enable ALSA/VLC sound card drift compensation.\n"
" Encoder parameters:\n"
@@ -407,6 +405,9 @@ public:
int sample_rate=48000;
int channels=2;
+ string icytext_file;
+ bool icytext_dlplus = false;
+
// For the ALSA input
string alsa_device;
@@ -417,13 +418,12 @@ public:
// For the VLC input
string vlc_uri;
- string vlc_icytext_file;
- bool vlc_icytext_dlplus = false;
string vlc_gain;
string vlc_cache;
vector<string> vlc_additional_opts;
unsigned verbosity = 0;
+ // For the GST input
string gst_uri;
string jack_name;
@@ -985,14 +985,33 @@ int AudioEnc::run()
* The VLC input is the only input that can also give us metadata, which
* we can hand over to ODR-PadEnc.
*/
+ if (not icytext_file.empty()) {
+ ICY_TEXT_t text;
+
+ if (false) {}
#if HAVE_VLC
- if (not vlc_uri.empty() and not vlc_icytext_file.empty()) {
// Using std::dynamic_pointer_cast would be safer, but is C++17
- VLCInput *vlc_input = (VLCInput*)(input.get());
- vlc_input->write_icy_text(vlc_icytext_file, vlc_icytext_dlplus);
- }
+ else if (not vlc_uri.empty()) {
+ VLCInput *vlc_input = (VLCInput*)(input.get());
+ text = vlc_input->get_icy_text();
+ }
+#endif
+#if HAVE_GST
+ else if (not gst_uri.empty()) {
+ GSTInput *gst_input = (GSTInput*)(input.get());
+ text = gst_input->get_icy_text();
+ }
#endif
+ if (text) {
+ bool success = write_icy_to_file(text, icytext_file, icytext_dlplus);
+
+ if (not success) {
+ fprintf(stderr, "Failed to write ICY Text\n");
+ }
+ }
+ }
+
/*! \section AudioLevel
* Audio level measurement is always done assuming we have two
* channels, and is formally wrong in mono, but still gives
@@ -1536,15 +1555,15 @@ int main(int argc, char *argv[])
case 'S':
audio_enc.send_stats_to = optarg;
break;
-#ifdef HAVE_VLC
- case 'v':
- audio_enc.vlc_uri = optarg;
- break;
case 'w':
- audio_enc.vlc_icytext_file = optarg;
+ audio_enc.icytext_file = optarg;
break;
case 'W':
- audio_enc.vlc_icytext_dlplus = true;
+ audio_enc.icytext_dlplus = true;
+ break;
+#ifdef HAVE_VLC
+ case 'v':
+ audio_enc.vlc_uri = optarg;
break;
case 'g':
audio_enc.vlc_gain = optarg;
diff --git a/src/utils.c b/src/utils.c
deleted file mode 100644
index 928304e..0000000
--- a/src/utils.c
+++ /dev/null
@@ -1,43 +0,0 @@
-#include "utils.h"
-#include <unistd.h>
-#include <stdint.h>
-#include <math.h>
-
-#define MIN(a,b) (((a)<(b))?(a):(b))
-#define MAX(a,b) (((a)>(b))?(a):(b))
-
-/* Taken from sox */
-const char* level(int channel, int peak)
-{
- static char const * const text[][2] = {
- /* White: 2dB steps */
- {"", ""}, {"-", "-"}, {"=", "="}, {"-=", "=-"},
- {"==", "=="}, {"-==", "==-"}, {"===", "==="}, {"-===", "===-"},
- {"====", "===="}, {"-====", "====-"}, {"=====", "====="},
- {"-=====", "=====-"}, {"======", "======"},
- /* Red: 1dB steps */
- {"!=====", "=====!"},
- };
- int const red = 1, white = NUMOF(text) - red;
-
- double linear = ((double)peak) / INT16_MAX;
-
- int vu_dB = linear ? floor(2 * white + red + linear_to_dB(linear)) : 0;
-
- int index = vu_dB < 2 * white ?
- MAX(vu_dB / 2, 0) :
- MIN(vu_dB - white, red + white - 1);
-
- 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.cpp b/src/utils.cpp
new file mode 100644
index 0000000..525f05e
--- /dev/null
+++ b/src/utils.cpp
@@ -0,0 +1,96 @@
+#include <cmath>
+#include <cstdint>
+#include <cstddef>
+#include <sstream>
+
+#include "utils.h"
+#include <unistd.h>
+
+#define MIN(a,b) (((a)<(b))?(a):(b))
+#define MAX(a,b) (((a)>(b))?(a):(b))
+
+/* Taken from sox */
+const char* level(int channel, int peak)
+{
+ static char const * const text[][2] = {
+ /* White: 2dB steps */
+ {"", ""}, {"-", "-"}, {"=", "="}, {"-=", "=-"},
+ {"==", "=="}, {"-==", "==-"}, {"===", "==="}, {"-===", "===-"},
+ {"====", "===="}, {"-====", "====-"}, {"=====", "====="},
+ {"-=====", "=====-"}, {"======", "======"},
+ /* Red: 1dB steps */
+ {"!=====", "=====!"},
+ };
+ int const red = 1, white = NUMOF(text) - red;
+
+ double linear = ((double)peak) / INT16_MAX;
+
+ int vu_dB = linear ? floor(2 * white + red + linear_to_dB(linear)) : 0;
+
+ int index = vu_dB < 2 * white ?
+ MAX(vu_dB / 2, 0) :
+ MIN(vu_dB - white, red + white - 1);
+
+ 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;
+}
+
+static const std::string ICY_TEXT_SEPARATOR = " - ";
+
+bool write_icy_to_file(const ICY_TEXT_t text, const std::string& filename, bool dl_plus)
+{
+ FILE* fd = fopen(filename.c_str(), "wb");
+ if (fd) {
+ bool ret = true;
+ bool artist_title_used = !text.artist.empty() and !text.title.empty();
+
+ // 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 tag
+ if (artist_title_used) {
+ size_t artist_len = strlen_utf8(text.artist.c_str());
+ size_t title_start = artist_len + strlen_utf8(ICY_TEXT_SEPARATOR.c_str());
+
+ // ITEM.ARTIST
+ ss << "DL_PLUS_TAG=4 0 " << (artist_len - 1) << "\n"; // -1 !
+
+ // ITEM.TITLE
+ ss << "DL_PLUS_TAG=1 " << title_start << " " << (strlen_utf8(text.title.c_str()) - 1) << "\n"; // -1 !
+ } else if (!text.now_playing.empty()) {
+ // PROGRAMME.NOW
+ ss << "DL_PLUS_TAG=33 0 " << (strlen_utf8(text.now_playing.c_str()) - 1) << "\n"; // -1 !
+ }
+
+ ss << "##### parameters } #####\n";
+ ret &= fputs(ss.str().c_str(), fd) >= 0;
+ }
+
+ if (artist_title_used) {
+ ret &= fputs(text.artist.c_str(), fd) >= 0;
+ ret &= fputs(ICY_TEXT_SEPARATOR.c_str(), fd) >= 0;
+ ret &= fputs(text.title.c_str(), fd) >= 0;
+ }
+ else {
+ ret &= fputs(text.now_playing.c_str(), fd) >= 0;
+ }
+ fclose(fd);
+
+ return ret;
+ }
+
+ return false;
+}
diff --git a/src/utils.h b/src/utils.h
index ca77a53..2f6b639 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -1,8 +1,9 @@
#pragma once
-#include <math.h>
-#include <stdint.h>
-#include <stddef.h>
+#include <string>
+#include <cmath>
+#include <cstdint>
+#include <cstddef>
#define NUMOF(l) (sizeof(l) / sizeof(*l))
@@ -15,3 +16,41 @@ const char* level(int channel, int peak);
size_t strlen_utf8(const char *s);
+struct ICY_TEXT_t {
+ std::string artist;
+ std::string title;
+ std::string now_playing;
+
+ operator bool() const {
+ return not (artist.empty() and title.empty() and now_playing.empty());
+ }
+
+ bool operator==(const ICY_TEXT_t& other) const {
+ return
+ artist == other.artist and
+ title == other.title and
+ now_playing == other.now_playing;
+ }
+ bool operator!=(const ICY_TEXT_t& other) const {
+ return !(*this == other);
+ }
+ void useArtistTitle(const std::string& artist, const std::string& title) {
+ this->artist = artist;
+ this->title = title;
+ now_playing = "";
+ }
+ void useNowPlaying(const std::string& now_playing) {
+ artist = "";
+ title = "";
+ this->now_playing = now_playing;
+ }
+};
+
+/*! Write the corresponding text to a file readable by ODR-PadEnc, with optional
+ * DL+ information. The text is passed as a copy because we actually use the
+ * m_nowplaying variable which is also accessed in another thread, so better
+ * make a copy.
+ *
+ * \return false on failure
+ */
+bool write_icy_to_file(const ICY_TEXT_t text, const std::string& filename, bool dl_plus);