From 35c11afd54e158485483d445608d014ca2485b3a Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 3 Dec 2021 19:59:54 +0100 Subject: [PATCH] player/Thread: add option "mixramp_analyzer" --- NEWS | 2 + doc/user.rst | 7 ++- src/config/Option.hxx | 3 ++ src/config/PlayerConfig.cxx | 3 +- src/config/PlayerConfig.hxx | 2 + src/config/Templates.cxx | 1 + src/decoder/Control.hxx | 8 +++ src/pcm/MixRampGlue.cxx | 105 ++++++++++++++++++++++++++++++++++++ src/pcm/MixRampGlue.hxx | 34 ++++++++++++ src/pcm/meson.build | 1 + src/player/Thread.cxx | 85 +++++++++++++++++++++++++++++ 11 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 src/pcm/MixRampGlue.cxx create mode 100644 src/pcm/MixRampGlue.hxx diff --git a/NEWS b/NEWS index 393c46767..2770f21dc 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,6 @@ ver 0.24 (not yet released) +* player + - add option "mixramp_analyzer" to scan MixRamp tags on-the-fly ver 0.23.6 (not yet released) diff --git a/doc/user.rst b/doc/user.rst index a273def79..3b1052836 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -658,13 +658,16 @@ MPD enables MixRamp if: - :ref:`mixrampdb ` is set to a reasonable value, e.g.:: mpc mixrampdb -17 -- both songs have MixRamp tags +- both songs have MixRamp tags (or ``mixramp_analyzer`` is enabled) - both songs have the same audio format (or :ref:`audio_output_format` is configured) The `MixRamp `__ tool can be -used to add MixRamp tags to your song files. +used to add MixRamp tags to your song files. To analyze songs +on-the-fly, you can enable the ``mixramp_analyzer`` option in +:file:`mpd.conf`:: + mixramp_analyzer "yes" Client Connections diff --git a/src/config/Option.hxx b/src/config/Option.hxx index a62fc691e..54ec38d05 100644 --- a/src/config/Option.hxx +++ b/src/config/Option.hxx @@ -80,6 +80,9 @@ enum class ConfigOption { DESPOTIFY_USER, DESPOTIFY_PASSWORD, DESPOTIFY_HIGH_BITRATE, + + MIXRAMP_ANALYZER, + MAX }; diff --git a/src/config/PlayerConfig.cxx b/src/config/PlayerConfig.cxx index ffb9afa56..6bac2a014 100644 --- a/src/config/PlayerConfig.cxx +++ b/src/config/PlayerConfig.cxx @@ -67,6 +67,7 @@ PlayerConfig::PlayerConfig(const ConfigData &config) return ParseAudioFormat(s, true); })), - replay_gain(config) + replay_gain(config), + mixramp_analyzer(config.GetBool(ConfigOption::MIXRAMP_ANALYZER, false)) { } diff --git a/src/config/PlayerConfig.hxx b/src/config/PlayerConfig.hxx index b9112313d..ef5d7d22f 100644 --- a/src/config/PlayerConfig.hxx +++ b/src/config/PlayerConfig.hxx @@ -39,6 +39,8 @@ struct PlayerConfig { ReplayGainConfig replay_gain; + bool mixramp_analyzer = false; + PlayerConfig() = default; explicit PlayerConfig(const ConfigData &config); diff --git a/src/config/Templates.cxx b/src/config/Templates.cxx index f3789a0e2..c59932060 100644 --- a/src/config/Templates.cxx +++ b/src/config/Templates.cxx @@ -76,6 +76,7 @@ const ConfigTemplate config_param_templates[] = { { "despotify_user", false, true }, { "despotify_password", false, true }, { "despotify_high_bitrate", false, true }, + { "mixramp_analyzer" }, }; static constexpr unsigned n_config_param_templates = diff --git a/src/decoder/Control.hxx b/src/decoder/Control.hxx index 26ff047d2..085070ba5 100644 --- a/src/decoder/Control.hxx +++ b/src/decoder/Control.hxx @@ -419,6 +419,10 @@ public: return mix_ramp.GetStart(); } + void SetMixRampStart(std::string &&s) noexcept { + mix_ramp.SetStart(std::move(s)); + } + const char *GetMixRampEnd() const noexcept { return mix_ramp.GetEnd(); } @@ -427,6 +431,10 @@ public: return previous_mix_ramp.GetEnd(); } + void SetMixRampPreviousEnd(std::string &&s) noexcept { + previous_mix_ramp.SetEnd(std::move(s)); + } + void SetMixRamp(MixRampInfo &&new_value) noexcept { mix_ramp = std::move(new_value); } diff --git a/src/pcm/MixRampGlue.cxx b/src/pcm/MixRampGlue.cxx new file mode 100644 index 000000000..fd8e7dfdd --- /dev/null +++ b/src/pcm/MixRampGlue.cxx @@ -0,0 +1,105 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "MixRampGlue.hxx" +#include "MixRampAnalyzer.hxx" +#include "AudioFormat.hxx" +#include "MusicPipe.hxx" +#include "MusicChunk.hxx" +#include "util/Compiler.h" + +#include + +static std::string +StartToString(const MixRampArray &a) noexcept +{ + std::string s; + + MixRampItem last{}; + for (const auto &i : a) { + if (i.time < FloatDuration{} || i == last) + continue; + + char buffer[64]; + sprintf(buffer, "%.2f %.2f;", i.volume, i.time.count()); + last = i; + + s.append(buffer); + } + + return s; +} + +static std::string +EndToString(const MixRampArray &a, FloatDuration total_time) noexcept +{ + std::string s; + + MixRampItem last{}; + for (const auto &i : a) { + if (i.time < FloatDuration{} || i == last) + continue; + + char buffer[64]; + sprintf(buffer, "%.2f %.2f;", + i.volume, (total_time - i.time).count()); + last = i; + + s.append(buffer); + } + + return s; +} + +static std::string +ToString(const MixRampData &mr, FloatDuration total_time, + MixRampDirection direction) noexcept +{ + switch (direction) { + case MixRampDirection::START: + return StartToString(mr.start); + + case MixRampDirection::END: + return EndToString(mr.end, total_time); + } + + gcc_unreachable(); +} + +std::string +AnalyzeMixRamp(const MusicPipe &pipe, const AudioFormat &audio_format, + MixRampDirection direction) noexcept +{ + if (audio_format.sample_rate != ReplayGainAnalyzer::SAMPLE_RATE || + audio_format.channels != ReplayGainAnalyzer::CHANNELS || + audio_format.format != SampleFormat::FLOAT) + // TODO: auto-convert + return {}; + + const auto *chunk = pipe.Peek(); + if (chunk == nullptr) + return {}; + + MixRampAnalyzer a; + do { + a.Process(ConstBuffer::FromVoid({chunk->data, chunk->length})); + } while ((chunk = chunk->next.get()) != nullptr); + + return ToString(a.GetResult(), a.GetTime(), direction); +} diff --git a/src/pcm/MixRampGlue.hxx b/src/pcm/MixRampGlue.hxx new file mode 100644 index 000000000..ce5ad0323 --- /dev/null +++ b/src/pcm/MixRampGlue.hxx @@ -0,0 +1,34 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +struct AudioFormat; +class MusicPipe; + +enum class MixRampDirection { + START, END +}; + +[[gnu::pure]] +std::string +AnalyzeMixRamp(const MusicPipe &pipe, const AudioFormat &audio_format, + MixRampDirection direction) noexcept; diff --git a/src/pcm/meson.build b/src/pcm/meson.build index 04860df9f..a8dff1d7a 100644 --- a/src/pcm/meson.build +++ b/src/pcm/meson.build @@ -49,6 +49,7 @@ pcm_sources = [ 'AudioCompress/compress.c', 'ReplayGainAnalyzer.cxx', 'MixRampAnalyzer.cxx', + 'MixRampGlue.cxx', ] libsamplerate_dep = dependency('samplerate', version: '>= 0.1.3', required: get_option('libsamplerate')) diff --git a/src/player/Thread.cxx b/src/player/Thread.cxx index 18178bf63..991a6bb98 100644 --- a/src/player/Thread.cxx +++ b/src/player/Thread.cxx @@ -44,6 +44,7 @@ #include "MusicChunk.hxx" #include "song/DetachedSong.hxx" #include "CrossFade.hxx" +#include "pcm/MixRampGlue.hxx" #include "tag/Tag.hxx" #include "Idle.hxx" #include "util/Compiler.h" @@ -328,6 +329,17 @@ private: */ bool OpenOutput() noexcept; + std::string UnlockAnalyzeMixRamp(const MusicPipe &pipe, + const AudioFormat &audio_format, + MixRampDirection direction) noexcept; + + /** + * @return false if more chunks of the next song are needed to + * scan for MixRamp data + */ + [[nodiscard]] + bool MixRampScannerReady() noexcept; + void CheckCrossFade() noexcept; /** @@ -473,6 +485,75 @@ real_song_duration(const DetachedSong &song, return {SongTime(decoder_duration) - start_time}; } +std::string +Player::UnlockAnalyzeMixRamp(const MusicPipe &_pipe, + const AudioFormat &audio_format, + MixRampDirection direction) noexcept +{ + const ScopeUnlock unlock(pc.mutex); + return AnalyzeMixRamp(_pipe, audio_format, direction); +} + +inline bool +Player::MixRampScannerReady() noexcept +{ + assert(pipe); + assert(dc.pipe); + + if (!pc.cross_fade.IsMixRampEnabled()) + return true; + + if (!pc.config.mixramp_analyzer) + /* always ready if the scanner is disabled */ + return true; + + if (dc.GetMixRampPreviousEnd() == nullptr) { + // TODO: scan incrementally backwards until mixrampdb is reached + auto s = UnlockAnalyzeMixRamp(*pipe, play_audio_format, + MixRampDirection::END); + if (!s.empty()) { + FmtDebug(player_domain, "Analyzed MixRamp end: {}", s); + dc.SetMixRampPreviousEnd(std::move(s)); + } + + if (dc.GetMixRampStart() == nullptr) + /* scan the next song in the next call; first, + let the main loop submit a few more chunks + to the outputs for playback to avoid + xrun */ + return false; + } + + if (dc.GetMixRampStart() == nullptr) { + const std::size_t want_pipe_bytes = + dc.out_audio_format.TimeToSize(std::chrono::seconds{20}); + const std::size_t want_pipe_chunks = + std::min((want_pipe_bytes + sizeof(MusicChunk::data) - 1) + / sizeof(MusicChunk::data), + buffer.GetSize() / std::size_t{3}); + + if (dc.pipe->GetSize() < want_pipe_chunks) { + /* need more data */ + if (!buffer.IsFull()) { + decoder_woken = true; + dc.Signal(); + } + + return false; + } + + // TODO: scan incrementally until mixrampdb is reached + auto s = UnlockAnalyzeMixRamp(*dc.pipe, dc.out_audio_format, + MixRampDirection::START); + if (!s.empty()) { + FmtDebug(player_domain, "Analyzed MixRamp start: {}", s); + dc.SetMixRampStart(std::move(s)); + } + } + + return true; +} + bool Player::OpenOutput() noexcept { @@ -813,6 +894,10 @@ Player::CheckCrossFade() noexcept return; } + if (!MixRampScannerReady()) + /* need more chunks for the MixRamp scanner */ + return; + /* enable cross fading in this song? if yes, calculate how many chunks will be required for it */ cross_fade_chunks =