From 13d6cf201f499d43c0a66930680ca3985e773e57 Mon Sep 17 00:00:00 2001 From: Roman Lebedev Date: Tue, 27 Jun 2023 05:05:01 +0300 Subject: [PATCH] Engine: pipe-in the EBU R 128 loudness normalization gain stuff The idea is that Integrated Loudness is an integral part of the song, much like knowing it's beginning / ending in the file, and we must handle it the exact same way, and pipe it through all the way. At the same time, `EngineBase` knows Target Level (from settings), and these two combined tell us the Gain needed to normalize the Loudness of the particular Song (`EngineBase::Load()` does that). So the actual backend only needs to handle the Volume. We don't currently support changing Target Level on the fly. We don't currently support changing Loudness-normalizing Gain on the fly. This does not handle the case when the song is loaded from URL and thus the EBU R 128 measures, that exist, are not nessesairly correct. --- src/core/player.cpp | 4 ++-- src/engine/enginebase.cpp | 24 +++++++++++++++++++++--- src/engine/enginebase.h | 9 +++++++-- src/engine/gstengine.cpp | 13 +++++++------ src/engine/gstengine.h | 4 ++-- src/engine/gstenginepipeline.cpp | 11 ++++++++++- src/engine/gstenginepipeline.h | 8 +++++++- src/engine/vlcengine.cpp | 5 ++++- src/engine/vlcengine.h | 2 +- src/playlist/playlistitem.h | 2 ++ 10 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/core/player.cpp b/src/core/player.cpp index 30ddcb9a8..5b9832cd2 100644 --- a/src/core/player.cpp +++ b/src/core/player.cpp @@ -341,7 +341,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) { if (is_current) { qLog(Debug) << "Playing song" << item->Metadata().title() << result.stream_url_ << "position" << play_offset_nanosec_; - engine_->Play(result.media_url_, result.stream_url_, stream_change_type_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec(), play_offset_nanosec_); + engine_->Play(result.media_url_, result.stream_url_, stream_change_type_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec(), play_offset_nanosec_, song.ebur128_integrated_loudness_lufs()); current_item_ = item; play_offset_nanosec_ = 0; } @@ -731,7 +731,7 @@ void Player::PlayAt(const int index, const quint64 offset_nanosec, EngineBase::T } else { qLog(Debug) << "Playing song" << current_item_->Metadata().title() << url << "position" << offset_nanosec; - engine_->Play(current_item_->Url(), url, change, current_item_->Metadata().has_cue(), current_item_->effective_beginning_nanosec(), current_item_->effective_end_nanosec(), offset_nanosec); + engine_->Play(current_item_->Url(), url, change, current_item_->Metadata().has_cue(), current_item_->effective_beginning_nanosec(), current_item_->effective_end_nanosec(), offset_nanosec, current_item_->effective_ebur128_integrated_loudness_lufs()); } } diff --git a/src/engine/enginebase.cpp b/src/engine/enginebase.cpp index 667e575ba..d40b8fed4 100644 --- a/src/engine/enginebase.cpp +++ b/src/engine/enginebase.cpp @@ -43,6 +43,7 @@ EngineBase::EngineBase(QObject *parent) volume_(100), beginning_nanosec_(0), end_nanosec_(0), + ebur128_loudness_normalizing_gain_db_(0.0), scope_(kScopeSize), buffering_(false), equalizer_enabled_(false), @@ -51,6 +52,8 @@ EngineBase::EngineBase(QObject *parent) rg_preamp_(0.0), rg_fallbackgain_(0.0), rg_compression_(true), + ebur128_loudness_normalization_(false), + ebur128_target_level_lufs_(-23.0), buffer_duration_nanosec_(BackendSettingsPage::kDefaultBufferDuration * kNsecPerMsec), buffer_low_watermark_(BackendSettingsPage::kDefaultBufferLowWatermark), buffer_high_watermark_(BackendSettingsPage::kDefaultBufferHighWatermark), @@ -104,7 +107,7 @@ QString EngineBase::Description(const Type type) { } -bool EngineBase::Load(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec) { +bool EngineBase::Load(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional ebur128_integrated_loudness_lufs) { Q_UNUSED(force_stop_at_end); @@ -113,15 +116,27 @@ bool EngineBase::Load(const QUrl &media_url, const QUrl &stream_url, const Track beginning_nanosec_ = beginning_nanosec; end_nanosec_ = end_nanosec; + ebur128_loudness_normalizing_gain_db_ = 0.0; + if (ebur128_loudness_normalization_ && ebur128_integrated_loudness_lufs) { + auto computeGain_dB = [](double source_dB, double target_dB) { + // Let's suppose the `source_dB` is -12 dB, while `target_dB` is -23 dB. + // In that case, we'd need to apply -11 dB of gain, which is computed as: + // -12 dB + x dB = -23 dB --> x dB = -23 dB - (-12 dB) + return target_dB - source_dB; + }; + + ebur128_loudness_normalizing_gain_db_ = computeGain_dB(*ebur128_integrated_loudness_lufs, ebur128_target_level_lufs_); + } + about_to_end_emitted_ = false; return true; } -bool EngineBase::Play(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const quint64 offset_nanosec) { +bool EngineBase::Play(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const quint64 offset_nanosec, const std::optional ebur128_integrated_loudness_lufs) { - if (!Load(media_url, stream_url, flags, force_stop_at_end, beginning_nanosec, end_nanosec)) { + if (!Load(media_url, stream_url, flags, force_stop_at_end, beginning_nanosec, end_nanosec, ebur128_integrated_loudness_lufs)) { return false; } @@ -167,6 +182,9 @@ void EngineBase::ReloadSettings() { rg_fallbackgain_ = s.value("rgfallbackgain", 0.0).toDouble(); rg_compression_ = s.value("rgcompression", true).toBool(); + ebur128_loudness_normalization_ = s.value("ebur128_loudness_normalization", false).toBool(); + ebur128_target_level_lufs_ = s.value("ebur128_target_level_lufs", -23.0).toDouble(); + fadeout_enabled_ = s.value("FadeoutEnabled", false).toBool(); crossfade_enabled_ = s.value("CrossfadeEnabled", false).toBool(); autocrossfade_enabled_ = s.value("AutoCrossfadeEnabled", false).toBool(); diff --git a/src/engine/enginebase.h b/src/engine/enginebase.h index 126108306..7aba94cc8 100644 --- a/src/engine/enginebase.h +++ b/src/engine/enginebase.h @@ -104,7 +104,7 @@ class EngineBase : public QObject { virtual bool Init() = 0; virtual State state() const = 0; virtual void StartPreloading(const QUrl&, const QUrl&, const bool, const qint64, const qint64) {} - virtual bool Load(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec); + virtual bool Load(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional ebur128_integrated_loudness_lufs); virtual bool Play(const quint64 offset_nanosec) = 0; virtual void Stop(const bool stop_after = false) = 0; virtual void Pause() = 0; @@ -132,7 +132,7 @@ class EngineBase : public QObject { // Plays a media stream represented with the URL 'u' from the given 'beginning' to the given 'end' (usually from 0 to a song's length). // Both markers should be passed in nanoseconds. 'end' can be negative, indicating that the real length of 'u' stream is unknown. - bool Play(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const quint64 offset_nanosec); + bool Play(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const quint64 offset_nanosec, const std::optional ebur128_integrated_loudness_lufs); void SetVolume(const uint volume); public slots: @@ -194,6 +194,7 @@ class EngineBase : public QObject { qint64 end_nanosec_; QUrl media_url_; QUrl stream_url_; + double ebur128_loudness_normalizing_gain_db_; Scope scope_; bool buffering_; bool equalizer_enabled_; @@ -209,6 +210,10 @@ class EngineBase : public QObject { double rg_fallbackgain_; bool rg_compression_; + // EBU R 128 Loudness Normalization + bool ebur128_loudness_normalization_; + double ebur128_target_level_lufs_; + // Buffering quint64 buffer_duration_nanosec_; double buffer_low_watermark_; diff --git a/src/engine/gstengine.cpp b/src/engine/gstengine.cpp index b216c79a9..42307a1e1 100644 --- a/src/engine/gstengine.cpp +++ b/src/engine/gstengine.cpp @@ -176,11 +176,11 @@ void GstEngine::StartPreloading(const QUrl &media_url, const QUrl &stream_url, c } -bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec) { +bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional ebur128_integrated_loudness_lufs) { EnsureInitialized(); - EngineBase::Load(stream_url, media_url, change, force_stop_at_end, beginning_nanosec, end_nanosec); + EngineBase::Load(stream_url, media_url, change, force_stop_at_end, beginning_nanosec, end_nanosec, ebur128_integrated_loudness_lufs); const QByteArray gst_url = FixupUrl(stream_url); @@ -195,7 +195,7 @@ bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const Engine return true; } - std::shared_ptr pipeline = CreatePipeline(media_url, stream_url, gst_url, force_stop_at_end ? end_nanosec : 0); + std::shared_ptr pipeline = CreatePipeline(media_url, stream_url, gst_url, force_stop_at_end ? end_nanosec : 0, ebur128_loudness_normalizing_gain_db_); if (!pipeline) return false; if (crossfade) StartFadeout(); @@ -648,7 +648,7 @@ void GstEngine::PlayDone(const GstStateChangeReturn ret, const quint64 offset_na const QByteArray redirect_url = current_pipeline_->redirect_url(); if (!redirect_url.isEmpty() && redirect_url != current_pipeline_->gst_url()) { qLog(Info) << "Redirecting to" << redirect_url; - current_pipeline_ = CreatePipeline(current_pipeline_->media_url(), current_pipeline_->stream_url(), redirect_url, end_nanosec_); + current_pipeline_ = CreatePipeline(current_pipeline_->media_url(), current_pipeline_->stream_url(), redirect_url, end_nanosec_, current_pipeline_->ebur128_loudness_normalizing_gain_db()); Play(offset_nanosec); return; } @@ -788,6 +788,7 @@ std::shared_ptr GstEngine::CreatePipeline() { ret->set_stereo_balancer_enabled(stereo_balancer_enabled_); ret->set_equalizer_enabled(equalizer_enabled_); ret->set_replaygain(rg_enabled_, rg_mode_, rg_preamp_, rg_fallbackgain_, rg_compression_); + ret->set_ebur128_loudness_normalization(ebur128_loudness_normalization_); ret->set_buffer_duration_nanosec(buffer_duration_nanosec_); ret->set_buffer_low_watermark(buffer_low_watermark_); ret->set_buffer_high_watermark(buffer_high_watermark_); @@ -815,11 +816,11 @@ std::shared_ptr GstEngine::CreatePipeline() { } -std::shared_ptr GstEngine::CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec) { +std::shared_ptr GstEngine::CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db) { std::shared_ptr ret = CreatePipeline(); QString error; - if (!ret->InitFromUrl(media_url, stream_url, gst_url, end_nanosec, error)) { + if (!ret->InitFromUrl(media_url, stream_url, gst_url, end_nanosec, ebur128_loudness_normalizing_gain_db, error)) { ret.reset(); emit Error(error); emit StateChanged(EngineBase::State::Error); diff --git a/src/engine/gstengine.h b/src/engine/gstengine.h index 3c639f754..1a39a392d 100644 --- a/src/engine/gstengine.h +++ b/src/engine/gstengine.h @@ -61,7 +61,7 @@ class GstEngine : public EngineBase, public GstBufferConsumer { bool Init() override; EngineBase::State state() const override; void StartPreloading(const QUrl &media_url, const QUrl &stream_url, const bool force_stop_at_end, const qint64 beginning_nanosec, const qint64 end_nanosec) override; - bool Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec) override; + bool Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional ebur128_integrated_loudness_lufs) override; bool Play(const quint64 offset_nanosec) override; void Stop(const bool stop_after = false) override; void Pause() override; @@ -132,7 +132,7 @@ class GstEngine : public EngineBase, public GstBufferConsumer { void StopTimers(); std::shared_ptr CreatePipeline(); - std::shared_ptr CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec); + std::shared_ptr CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db); void UpdateScope(int chunk_length); diff --git a/src/engine/gstenginepipeline.cpp b/src/engine/gstenginepipeline.cpp index 30a4706a8..9dc335531 100644 --- a/src/engine/gstenginepipeline.cpp +++ b/src/engine/gstenginepipeline.cpp @@ -78,6 +78,7 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent) rg_preamp_(0.0), rg_fallbackgain_(0.0), rg_compression_(true), + ebur128_loudness_normalization_(false), buffer_duration_nanosec_(BackendSettingsPage::kDefaultBufferDuration * kNsecPerMsec), buffer_low_watermark_(BackendSettingsPage::kDefaultBufferLowWatermark), buffer_high_watermark_(BackendSettingsPage::kDefaultBufferHighWatermark), @@ -99,6 +100,7 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent) pending_seek_nanosec_(-1), last_known_position_ns_(0), next_uri_set_(false), + ebur128_loudness_normalizing_gain_db_(0.0), volume_set_(false), volume_internal_(-1.0), volume_percent_(100), @@ -237,6 +239,12 @@ void GstEnginePipeline::set_replaygain(const bool enabled, const int mode, const } +void GstEnginePipeline::set_ebur128_loudness_normalization(const bool enabled) { + + ebur128_loudness_normalization_ = enabled; + +} + void GstEnginePipeline::set_buffer_duration_nanosec(const quint64 buffer_duration_nanosec) { buffer_duration_nanosec_ = buffer_duration_nanosec; } @@ -289,11 +297,12 @@ GstElement *GstEnginePipeline::CreateElement(const QString &factory_name, const } -bool GstEnginePipeline::InitFromUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, QString &error) { +bool GstEnginePipeline::InitFromUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db, QString &error) { media_url_ = media_url; stream_url_ = stream_url; gst_url_ = gst_url; + ebur128_loudness_normalizing_gain_db_ = ebur128_loudness_normalizing_gain_db; end_offset_nanosec_ = end_nanosec; guint version_major = 0, version_minor = 0, version_micro = 0, version_nano = 0; diff --git a/src/engine/gstenginepipeline.h b/src/engine/gstenginepipeline.h index ae0ebddfd..a46ebe862 100644 --- a/src/engine/gstenginepipeline.h +++ b/src/engine/gstenginepipeline.h @@ -66,6 +66,7 @@ class GstEnginePipeline : public QObject { void set_stereo_balancer_enabled(const bool enabled); void set_equalizer_enabled(const bool enabled); void set_replaygain(const bool enabled, const int mode, const double preamp, const double fallbackgain, const bool compression); + void set_ebur128_loudness_normalization(const bool enabled); void set_buffer_duration_nanosec(const quint64 duration_nanosec); void set_buffer_low_watermark(const double value); void set_buffer_high_watermark(const double value); @@ -76,7 +77,7 @@ class GstEnginePipeline : public QObject { void set_fading_enabled(const bool enabled); // Creates the pipeline, returns false on error - bool InitFromUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, QString &error); + bool InitFromUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db, QString &error); // GstBufferConsumers get fed audio data. Thread-safe. void AddBufferConsumer(GstBufferConsumer *consumer); @@ -102,6 +103,7 @@ class GstEnginePipeline : public QObject { // Get information about the music playback QUrl media_url() const { return media_url_; } QUrl stream_url() const { return stream_url_; } + double ebur128_loudness_normalizing_gain_db() const { return ebur128_loudness_normalizing_gain_db_; } QByteArray gst_url() const { return gst_url_; } QUrl next_media_url() const { return next_media_url_; } QUrl next_stream_url() const { return next_stream_url_; } @@ -215,6 +217,9 @@ class GstEnginePipeline : public QObject { double rg_fallbackgain_; bool rg_compression_; + // EBU R 128 Loudness Normalization + bool ebur128_loudness_normalization_; + // Buffering quint64 buffer_duration_nanosec_; double buffer_low_watermark_; @@ -282,6 +287,7 @@ class GstEnginePipeline : public QObject { // Complete the transition to the next song when it starts playing bool next_uri_set_; + double ebur128_loudness_normalizing_gain_db_; bool volume_set_; gdouble volume_internal_; uint volume_percent_; diff --git a/src/engine/vlcengine.cpp b/src/engine/vlcengine.cpp index 8652811da..3a129058d 100644 --- a/src/engine/vlcengine.cpp +++ b/src/engine/vlcengine.cpp @@ -98,9 +98,12 @@ bool VLCEngine::Init() { } -bool VLCEngine::Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec) { +bool VLCEngine::Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional ebur128_integrated_loudness_lufs) { + + // FIXME: why is this not calling `EngineBase::Load()`? Q_UNUSED(media_url); + Q_UNUSED(ebur128_integrated_loudness_lufs); Q_UNUSED(change); Q_UNUSED(force_stop_at_end); Q_UNUSED(beginning_nanosec); diff --git a/src/engine/vlcengine.h b/src/engine/vlcengine.h index 352d1ffda..41afbf7d6 100644 --- a/src/engine/vlcengine.h +++ b/src/engine/vlcengine.h @@ -47,7 +47,7 @@ class VLCEngine : public EngineBase { Type type() const override { return Type::VLC; } bool Init() override; EngineBase::State state() const override { return state_; } - bool Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec) override; + bool Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional ebur128_integrated_loudness_lufs) override; bool Play(const quint64 offset_nanosec) override; void Stop(const bool stop_after = false) override; void Pause() override; diff --git a/src/playlist/playlistitem.h b/src/playlist/playlistitem.h index 5f163ad72..e37e90668 100644 --- a/src/playlist/playlistitem.h +++ b/src/playlist/playlistitem.h @@ -87,6 +87,8 @@ class PlaylistItem : public std::enable_shared_from_this { Song StreamMetadata() { return HasTemporaryMetadata() ? temp_metadata_ : Metadata(); } QUrl StreamUrl() const { return HasTemporaryMetadata() && temp_metadata_.effective_stream_url().isValid() ? temp_metadata_.effective_stream_url() : Url(); } + std::optional effective_ebur128_integrated_loudness_lufs() const { return HasTemporaryMetadata() && temp_metadata_.is_valid() ? temp_metadata_.ebur128_integrated_loudness_lufs() : Metadata().ebur128_integrated_loudness_lufs(); } + qint64 effective_beginning_nanosec() const { return HasTemporaryMetadata() && temp_metadata_.is_valid() && temp_metadata_.beginning_nanosec() != -1 ? temp_metadata_.beginning_nanosec() : Metadata().beginning_nanosec(); } qint64 effective_end_nanosec() const { return HasTemporaryMetadata() && temp_metadata_.is_valid() && temp_metadata_.end_nanosec() != -1 ? temp_metadata_.end_nanosec() : Metadata().end_nanosec(); }