diff --git a/src/pcm/MixRampAnalyzer.cxx b/src/pcm/MixRampAnalyzer.cxx new file mode 100644 index 000000000..939d0dbe6 --- /dev/null +++ b/src/pcm/MixRampAnalyzer.cxx @@ -0,0 +1,64 @@ +/* + * 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 "MixRampAnalyzer.hxx" +#include "util/ConstBuffer.hxx" + +inline void +MixRampData::Add(MixRampItem item) noexcept +{ + for (std::size_t i = 0; i < mixramp_volumes.size(); ++i) { + if (start[i].time < FloatDuration{} && + item.volume >= mixramp_volumes[i]) + start[i] = item; + + if (item.volume >= mixramp_volumes[i]) + end[i] = item; + } + +} + +void +MixRampAnalyzer::Process(ConstBuffer src) noexcept +{ + while (!src.empty()) { + std::size_t chunk_remaining = chunk_frames - chunk_fill; + assert(chunk_remaining > 0); + + if (chunk_remaining > src.size) { + gain_analyzer.Process(src); + chunk_fill += src.size; + return; + } + + gain_analyzer.Process({src.data, chunk_remaining}); + src.skip_front(chunk_remaining); + + gain_analyzer.Flush(); + + const double gain = (double)gain_analyzer.GetGain(); + const double volume = -gain; + + result.Add({GetTime(), volume}); + + ++chunk_number; + chunk_fill = 0; + gain_analyzer = {}; + } +} diff --git a/src/pcm/MixRampAnalyzer.hxx b/src/pcm/MixRampAnalyzer.hxx new file mode 100644 index 000000000..779640d27 --- /dev/null +++ b/src/pcm/MixRampAnalyzer.hxx @@ -0,0 +1,86 @@ +/* + * 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 "ReplayGainAnalyzer.hxx" +#include "Chrono.hxx" + +constexpr auto mixramp_volumes = std::array{ + -90., -60., -40., -30., -24., -21., -18., + -15., -12., -9., -6., -3., 0., 3., 6., +}; + +struct MixRampItem { + FloatDuration time; + double volume; + + constexpr bool operator==(const MixRampItem &other) const noexcept { + return time == other.time && volume == other.volume; + } + + constexpr bool operator!=(const MixRampItem &other) const noexcept { + return !(*this == other); + } +}; + +using MixRampArray = std::array; + +struct MixRampData { + MixRampArray start, end; + + constexpr MixRampData() noexcept:start{}, end{} { + for (std::size_t i = 0; i < mixramp_volumes.size(); ++i) { + start[i].time = end[i].time = FloatDuration{-1}; + start[i].volume = end[i].volume = mixramp_volumes[i]; + } + } + + void Add(MixRampItem item) noexcept; +}; + +/** + * Analyze a 44.1 kHz / stereo / float32 audio stream and calculate + * MixRamp tags. + */ +class MixRampAnalyzer { + static constexpr std::size_t chunk_duration_fraction = 10; + static constexpr std::size_t chunk_frames = + ReplayGainAnalyzer::SAMPLE_RATE / chunk_duration_fraction; + static constexpr FloatDuration chunk_duration{1.0 / chunk_duration_fraction}; + + WindowReplayGainAnalyzer gain_analyzer; + + MixRampData result; + + std::size_t chunk_number = 0; + + std::size_t chunk_fill = 0; + +public: + void Process(ConstBuffer src) noexcept; + + FloatDuration GetTime() const noexcept { + return chunk_number * chunk_duration; + } + + const auto &GetResult() const noexcept { + return result; + } +}; diff --git a/src/pcm/meson.build b/src/pcm/meson.build index 6fa0d6bc0..04860df9f 100644 --- a/src/pcm/meson.build +++ b/src/pcm/meson.build @@ -48,6 +48,7 @@ pcm_sources = [ 'ConfiguredResampler.cxx', 'AudioCompress/compress.c', 'ReplayGainAnalyzer.cxx', + 'MixRampAnalyzer.cxx', ] libsamplerate_dep = dependency('samplerate', version: '>= 0.1.3', required: get_option('libsamplerate')) diff --git a/test/RunMixRampAnalyzer.cxx b/test/RunMixRampAnalyzer.cxx new file mode 100644 index 000000000..652ba98f2 --- /dev/null +++ b/test/RunMixRampAnalyzer.cxx @@ -0,0 +1,85 @@ +/* + * 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 "ReadFrames.hxx" +#include "pcm/MixRampAnalyzer.hxx" +#include "io/FileDescriptor.hxx" +#include "util/ConstBuffer.hxx" +#include "util/PrintException.hxx" + +#include +#include + +#include +#include + +int +main(int, char **) noexcept +try { + constexpr std::size_t frame_size = sizeof(ReplayGainAnalyzer::Frame); + + const FileDescriptor input_fd(STDIN_FILENO); + + MixRampAnalyzer a; + + while (true) { + std::array buffer; + + size_t nbytes = ReadFrames(input_fd, + buffer.data(), sizeof(buffer), + frame_size); + if (nbytes == 0) + break; + + const std::size_t n_frames = nbytes / frame_size; + a.Process({buffer.data(), n_frames}); + } + + const auto data = a.GetResult(); + + const auto total_time = a.GetTime(); + + printf("MIXRAMP_START="); + + MixRampItem last{}; + + for (const auto &i : data.start) { + if (i.time >= FloatDuration{} && i != last) { + printf("%.2f %.2f;", i.volume, i.time.count()); + last = i; + } + } + + printf("\n"); + + printf("MIXRAMP_END="); + last = {}; + for (const auto &i : data.end) { + if (i.time >= FloatDuration{} && i != last) { + printf("%.2f %.2f;", i.volume, (total_time - i.time).count()); + last = i; + } + } + printf("\n"); + + return EXIT_SUCCESS; +} catch (...) { + PrintException(std::current_exception()); + return EXIT_FAILURE; +} diff --git a/test/meson.build b/test/meson.build index 01bcfd0ba..1701396df 100644 --- a/test/meson.build +++ b/test/meson.build @@ -567,6 +567,17 @@ executable( ], ) +executable( + 'RunMixRampAnalyzer', + 'RunMixRampAnalyzer.cxx', + 'ReadFrames.cxx', + include_directories: inc, + dependencies: [ + pcm_dep, + io_dep, + ], +) + # # Encoder #