player/Thread: add option "mixramp_analyzer"
This commit is contained in:
parent
c884e2f285
commit
35c11afd54
2
NEWS
2
NEWS
|
@ -1,4 +1,6 @@
|
||||||
ver 0.24 (not yet released)
|
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)
|
ver 0.23.6 (not yet released)
|
||||||
|
|
||||||
|
|
|
@ -658,13 +658,16 @@ MPD enables MixRamp if:
|
||||||
- :ref:`mixrampdb <command_mixrampdb>` is set to a reasonable value,
|
- :ref:`mixrampdb <command_mixrampdb>` is set to a reasonable value,
|
||||||
e.g.::
|
e.g.::
|
||||||
mpc mixrampdb -17
|
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`
|
- both songs have the same audio format (or :ref:`audio_output_format`
|
||||||
is configured)
|
is configured)
|
||||||
|
|
||||||
The `MixRamp <http://sourceforge.net/projects/mixramp>`__ tool can be
|
The `MixRamp <http://sourceforge.net/projects/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
|
Client Connections
|
||||||
|
|
|
@ -80,6 +80,9 @@ enum class ConfigOption {
|
||||||
DESPOTIFY_USER,
|
DESPOTIFY_USER,
|
||||||
DESPOTIFY_PASSWORD,
|
DESPOTIFY_PASSWORD,
|
||||||
DESPOTIFY_HIGH_BITRATE,
|
DESPOTIFY_HIGH_BITRATE,
|
||||||
|
|
||||||
|
MIXRAMP_ANALYZER,
|
||||||
|
|
||||||
MAX
|
MAX
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ PlayerConfig::PlayerConfig(const ConfigData &config)
|
||||||
|
|
||||||
return ParseAudioFormat(s, true);
|
return ParseAudioFormat(s, true);
|
||||||
})),
|
})),
|
||||||
replay_gain(config)
|
replay_gain(config),
|
||||||
|
mixramp_analyzer(config.GetBool(ConfigOption::MIXRAMP_ANALYZER, false))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,8 @@ struct PlayerConfig {
|
||||||
|
|
||||||
ReplayGainConfig replay_gain;
|
ReplayGainConfig replay_gain;
|
||||||
|
|
||||||
|
bool mixramp_analyzer = false;
|
||||||
|
|
||||||
PlayerConfig() = default;
|
PlayerConfig() = default;
|
||||||
|
|
||||||
explicit PlayerConfig(const ConfigData &config);
|
explicit PlayerConfig(const ConfigData &config);
|
||||||
|
|
|
@ -76,6 +76,7 @@ const ConfigTemplate config_param_templates[] = {
|
||||||
{ "despotify_user", false, true },
|
{ "despotify_user", false, true },
|
||||||
{ "despotify_password", false, true },
|
{ "despotify_password", false, true },
|
||||||
{ "despotify_high_bitrate", false, true },
|
{ "despotify_high_bitrate", false, true },
|
||||||
|
{ "mixramp_analyzer" },
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr unsigned n_config_param_templates =
|
static constexpr unsigned n_config_param_templates =
|
||||||
|
|
|
@ -419,6 +419,10 @@ public:
|
||||||
return mix_ramp.GetStart();
|
return mix_ramp.GetStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetMixRampStart(std::string &&s) noexcept {
|
||||||
|
mix_ramp.SetStart(std::move(s));
|
||||||
|
}
|
||||||
|
|
||||||
const char *GetMixRampEnd() const noexcept {
|
const char *GetMixRampEnd() const noexcept {
|
||||||
return mix_ramp.GetEnd();
|
return mix_ramp.GetEnd();
|
||||||
}
|
}
|
||||||
|
@ -427,6 +431,10 @@ public:
|
||||||
return previous_mix_ramp.GetEnd();
|
return previous_mix_ramp.GetEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetMixRampPreviousEnd(std::string &&s) noexcept {
|
||||||
|
previous_mix_ramp.SetEnd(std::move(s));
|
||||||
|
}
|
||||||
|
|
||||||
void SetMixRamp(MixRampInfo &&new_value) noexcept {
|
void SetMixRamp(MixRampInfo &&new_value) noexcept {
|
||||||
mix_ramp = std::move(new_value);
|
mix_ramp = std::move(new_value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <stdio.h>
|
||||||
|
|
||||||
|
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<ReplayGainAnalyzer::Frame>::FromVoid({chunk->data, chunk->length}));
|
||||||
|
} while ((chunk = chunk->next.get()) != nullptr);
|
||||||
|
|
||||||
|
return ToString(a.GetResult(), a.GetTime(), direction);
|
||||||
|
}
|
|
@ -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 <string>
|
||||||
|
|
||||||
|
struct AudioFormat;
|
||||||
|
class MusicPipe;
|
||||||
|
|
||||||
|
enum class MixRampDirection {
|
||||||
|
START, END
|
||||||
|
};
|
||||||
|
|
||||||
|
[[gnu::pure]]
|
||||||
|
std::string
|
||||||
|
AnalyzeMixRamp(const MusicPipe &pipe, const AudioFormat &audio_format,
|
||||||
|
MixRampDirection direction) noexcept;
|
|
@ -49,6 +49,7 @@ pcm_sources = [
|
||||||
'AudioCompress/compress.c',
|
'AudioCompress/compress.c',
|
||||||
'ReplayGainAnalyzer.cxx',
|
'ReplayGainAnalyzer.cxx',
|
||||||
'MixRampAnalyzer.cxx',
|
'MixRampAnalyzer.cxx',
|
||||||
|
'MixRampGlue.cxx',
|
||||||
]
|
]
|
||||||
|
|
||||||
libsamplerate_dep = dependency('samplerate', version: '>= 0.1.3', required: get_option('libsamplerate'))
|
libsamplerate_dep = dependency('samplerate', version: '>= 0.1.3', required: get_option('libsamplerate'))
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
#include "MusicChunk.hxx"
|
#include "MusicChunk.hxx"
|
||||||
#include "song/DetachedSong.hxx"
|
#include "song/DetachedSong.hxx"
|
||||||
#include "CrossFade.hxx"
|
#include "CrossFade.hxx"
|
||||||
|
#include "pcm/MixRampGlue.hxx"
|
||||||
#include "tag/Tag.hxx"
|
#include "tag/Tag.hxx"
|
||||||
#include "Idle.hxx"
|
#include "Idle.hxx"
|
||||||
#include "util/Compiler.h"
|
#include "util/Compiler.h"
|
||||||
|
@ -328,6 +329,17 @@ private:
|
||||||
*/
|
*/
|
||||||
bool OpenOutput() noexcept;
|
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;
|
void CheckCrossFade() noexcept;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -473,6 +485,75 @@ real_song_duration(const DetachedSong &song,
|
||||||
return {SongTime(decoder_duration) - start_time};
|
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
|
bool
|
||||||
Player::OpenOutput() noexcept
|
Player::OpenOutput() noexcept
|
||||||
{
|
{
|
||||||
|
@ -813,6 +894,10 @@ Player::CheckCrossFade() noexcept
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!MixRampScannerReady())
|
||||||
|
/* need more chunks for the MixRamp scanner */
|
||||||
|
return;
|
||||||
|
|
||||||
/* enable cross fading in this song? if yes, calculate how
|
/* enable cross fading in this song? if yes, calculate how
|
||||||
many chunks will be required for it */
|
many chunks will be required for it */
|
||||||
cross_fade_chunks =
|
cross_fade_chunks =
|
||||||
|
|
Loading…
Reference in New Issue