From 69d2134fa2c39901eb354eac546c09e1be5c794b Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Sat, 11 Nov 2023 00:20:45 +0100 Subject: [PATCH] Rewrite of audio data processing/beat detection. Consolidated audio processing code into the PCM class, removing the BeatDetect class in the process. Beat detection now uses the same algorithm as Milkdrop, passing the proper relative bass/mid/treb values to presets. Makes many presets look less jumpy/flickering, as the values are now (smoothly) alternating around 1.0. Updating frame audio is now done in a function that must specifically be called. Any subsequent calls to GetFrameAudioData() will then return the exact same copy of the audio data. As of now with the exception that new waveform data may be passed in via a separate thread, which will then be returned and might not match the spectrum data. Will fix that in a later commit. --- src/libprojectM/Audio/BeatDetect.cpp | 151 ---------- src/libprojectM/Audio/BeatDetect.hpp | 100 ------- src/libprojectM/Audio/CMakeLists.txt | 4 +- src/libprojectM/Audio/Loudness.cpp | 62 ++++ src/libprojectM/Audio/Loudness.hpp | 97 ++++++ src/libprojectM/Audio/MilkdropFFT.cpp | 7 +- src/libprojectM/Audio/MilkdropFFT.hpp | 2 +- src/libprojectM/Audio/PCM.cpp | 281 ++++++------------ src/libprojectM/Audio/PCM.hpp | 181 ++++------- .../MilkdropPreset/CustomShape.cpp | 1 - .../MilkdropPreset/CustomWaveform.cpp | 5 +- src/libprojectM/MilkdropPreset/Waveform.cpp | 16 +- src/libprojectM/ProjectM.cpp | 37 +-- src/libprojectM/ProjectM.hpp | 6 +- src/libprojectM/ProjectMCWrapper.cpp | 14 +- src/libprojectM/TimeKeeper.cpp | 4 +- src/libprojectM/TimeKeeper.hpp | 35 ++- tests/libprojectM/CMakeLists.txt | 1 - tests/libprojectM/PCMTest.cpp | 58 ++-- 19 files changed, 404 insertions(+), 658 deletions(-) delete mode 100755 src/libprojectM/Audio/BeatDetect.cpp delete mode 100755 src/libprojectM/Audio/BeatDetect.hpp create mode 100644 src/libprojectM/Audio/Loudness.cpp create mode 100644 src/libprojectM/Audio/Loudness.hpp diff --git a/src/libprojectM/Audio/BeatDetect.cpp b/src/libprojectM/Audio/BeatDetect.cpp deleted file mode 100755 index 96eca68dc..000000000 --- a/src/libprojectM/Audio/BeatDetect.cpp +++ /dev/null @@ -1,151 +0,0 @@ -/** - * projectM -- Milkdrop-esque visualisation SDK - * Copyright (C)2003-2004 projectM Team - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * See 'LICENSE.txt' included within this release - * - */ -/** - * Takes sound data from wherever and returns beat detection values - * Uses statistical Energy-Based methods. Very simple - * - * Some stuff was taken from Frederic Patin's beat-detection article, - * you'll find it online - */ - -#include "BeatDetect.hpp" -#include "PCM.hpp" - -#include -#include - -namespace libprojectM { -namespace Audio { - -BeatDetect::BeatDetect(PCM& _pcm) - : pcm(_pcm) -{ -} - - -auto BeatDetect::Reset() noexcept -> void -{ - this->treb = 0; - this->mid = 0; - this->bass = 0; - this->trebAtt = 0; - this->midAtt = 0; - this->bassAtt = 0; - this->volAtt = 0; - this->volOld = 0; -} - - -auto BeatDetect::GetPCMScale() const noexcept -> float -{ - return beatSensitivity; -} - - -auto BeatDetect::GetFrameAudioData() const -> FrameAudioData -{ - FrameAudioData data{}; - - pcm.GetPcm(data.waveformLeft.data(), CHANNEL_L, WaveformSamples); - pcm.GetPcm(data.waveformRight.data(), CHANNEL_R, WaveformSamples); - pcm.GetSpectrum(data.spectrumLeft.data(), CHANNEL_L, SpectrumSamples); - pcm.GetSpectrum(data.spectrumRight.data(), CHANNEL_R, SpectrumSamples); - - data.vol = vol; - data.volAtt = volAtt; - - data.bass = bass; - data.bassAtt = bassAtt; - data.mid = mid; - data.midAtt = midAtt; - data.treb = treb; - data.trebAtt = trebAtt; - - return data; -} - -auto BeatDetect::CalculateBeatStatistics() -> void -{ - volOld = vol; - - std::array const freqL = - [this]() { - std::array freq{}; - pcm.GetSpectrum(freq.data(), CHANNEL_L, fftLength); - return freq; - }(); - std::array const freqR = - [this]() { - std::array freq{}; - pcm.GetSpectrum(freq.data(), CHANNEL_R, fftLength); - return freq; - }(); - - auto const intensityBetween = [&freqL, &freqR](size_t const from, size_t const to) { - return std::accumulate(&freqL[from], &freqL[to], 0.f) + - std::accumulate(&freqR[from], &freqR[to], 0.f); - }; - - auto const& updateBand = - [](float& band, float& bandAtt, LowPassFilter& bandSmoothed, float const bandIntensity) { - bandSmoothed.Update(bandIntensity); - band = bandIntensity / std::max(0.0001f, bandSmoothed.Get()); - bandAtt = .6f * bandAtt + .4f * band; - bandAtt = std::min(bandAtt, 100.f); - band = std::min(band, 100.f); - }; - - static_assert(fftLength >= 256, "fftLength too small"); - std::array constexpr ranges{0, 3, 23, 255}; - - float const bassIntensity = intensityBetween(ranges[0], ranges[1]); - updateBand(bass, bassAtt, bassSmoothed, bassIntensity); - - float const midIntensity = intensityBetween(ranges[1], ranges[2]); - updateBand(mid, midAtt, midSmoothed, midIntensity); - - float const trebIntensity = intensityBetween(ranges[2], ranges[3]); - updateBand(treb, trebAtt, trebSmoothed, trebIntensity); - - float const volIntensity = bassIntensity + midIntensity + trebIntensity; - updateBand(vol, volAtt, volSmoothed, volIntensity); -} - - - -auto BeatDetect::LowPassFilter::Update(float nextValue) noexcept -> void -{ - m_current -= m_buffer[m_bufferPos] / bufferLength; - m_current += nextValue / bufferLength; - m_buffer[m_bufferPos] = nextValue; - - ++m_bufferPos; - m_bufferPos %= bufferLength; -} - - -auto BeatDetect::LowPassFilter::Get() const noexcept -> float -{ - return m_current; -} - -} // namespace Audio -} // namespace libprojectM diff --git a/src/libprojectM/Audio/BeatDetect.hpp b/src/libprojectM/Audio/BeatDetect.hpp deleted file mode 100755 index c0a8bef61..000000000 --- a/src/libprojectM/Audio/BeatDetect.hpp +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @file BeatDetect.cpp - * @brief Beat detection class. Takes decompressed sound buffers and returns various characteristics. - * - * projectM -- Milkdrop-esque visualisation SDK - * Copyright (C)2003-2007 projectM Team - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * See 'LICENSE.txt' included within this release - * - */ -#pragma once - -#include "FrameAudioData.hpp" -#include "PCM.hpp" - -#include - -namespace libprojectM { -namespace Audio { - -class BeatDetect -{ -public: - // We should probably remove pcm from the constructor, - // and just pass it as an argument to CalculateBeatStatistics. - explicit BeatDetect(PCM& pcm); - - auto Reset() noexcept -> void; - - /** - * Calculates and updates information about the beat - */ - auto CalculateBeatStatistics() -> void; - - // getPCMScale() was added to address https://github.com/projectM-visualizer/projectm/issues/161 - // Returning 1.0 results in using the raw PCM data, which can make the presets look pretty unresponsive - // if the application volume is low. - [[nodiscard]] - auto GetPCMScale() const noexcept -> float; - - /** - * @brief Returns a filled FrameAudioData structure for the current frame. - * @return A FrameAudioData structure with waveform, spectrum beat detection data. - */ - [[nodiscard]] - auto GetFrameAudioData() const -> FrameAudioData; - - float beatSensitivity{1.f}; - - float treb{0.f}; - float mid{0.f}; - float bass{0.f}; - float volOld{0.f}; - - float trebAtt{0.f}; - float midAtt{0.f}; - float bassAtt{0.f}; - float vol{0.f}; - float volAtt{0.f}; - - PCM& pcm; - -private: - class LowPassFilter - { - public: - auto - Update(float nextValue) noexcept -> void; - - [[nodiscard]] auto - Get() const noexcept -> float; - - private: - static size_t constexpr bufferLength{80}; - size_t m_bufferPos{0}; - std::array m_buffer{0.f}; - float m_current{0.f}; - }; - - LowPassFilter bassSmoothed; - LowPassFilter midSmoothed; - LowPassFilter trebSmoothed; - LowPassFilter volSmoothed; -}; - -} // namespace Audio -} // namespace libprojectM diff --git a/src/libprojectM/Audio/CMakeLists.txt b/src/libprojectM/Audio/CMakeLists.txt index 3f34fbcbd..dfe5108a6 100644 --- a/src/libprojectM/Audio/CMakeLists.txt +++ b/src/libprojectM/Audio/CMakeLists.txt @@ -1,14 +1,14 @@ add_library(Audio OBJECT AudioConstants.hpp - BeatDetect.cpp - BeatDetect.hpp MilkdropFFT.cpp MilkdropFFT.hpp FrameAudioData.cpp FrameAudioData.hpp PCM.cpp PCM.hpp + Loudness.cpp + Loudness.hpp ) target_include_directories(Audio diff --git a/src/libprojectM/Audio/Loudness.cpp b/src/libprojectM/Audio/Loudness.cpp new file mode 100644 index 000000000..4403efbd0 --- /dev/null +++ b/src/libprojectM/Audio/Loudness.cpp @@ -0,0 +1,62 @@ +#include "Loudness.hpp" + +#include + +namespace libprojectM { +namespace Audio { + +Loudness::Loudness(Loudness::Band band) + : m_band(band) +{ +} + +void Loudness::Update(const std::array& spectrumSamples, double secondsSinceLastFrame, uint32_t frame) +{ + SumBand(spectrumSamples); + UpdateBandAverage(secondsSinceLastFrame, frame); +} + +auto Loudness::CurrentRelative() const -> float +{ + return m_currentRelative; +} + +auto Loudness::AverageRelative() const -> float +{ + return m_averageRelative; +} + +void Loudness::SumBand(const std::array& spectrumSamples) +{ + int start = SpectrumSamples * static_cast(m_band) / 6; + int end = SpectrumSamples * static_cast(m_band) / 6; + + m_current = 0.0f; + for (int sample = start; sample < end; sample++) + { + m_current += spectrumSamples[sample]; + } +} + +void Loudness::UpdateBandAverage(double secondsSinceLastFrame, uint32_t frame) +{ + float rate = AdjustRateToFps(m_current > m_average ? 0.2f : 0.5f, secondsSinceLastFrame); + m_average = m_average * rate + m_current * (1.0f - rate); + + rate = AdjustRateToFps(frame < 50 ? 0.9f : 0.992f, secondsSinceLastFrame); + m_longAverage = m_longAverage * rate + m_current * (1.0f - rate); + + m_currentRelative = std::fabs(m_longAverage) < 0.001f ? 1.0f : m_current / m_longAverage; + m_averageRelative = std::fabs(m_longAverage) < 0.001f ? 1.0f : m_average / m_longAverage; +} + +auto Loudness::AdjustRateToFps(float rate, double secondsSinceLastFrame) -> float +{ + float const perSecondDecayRateAtFps1 = std::pow(rate, 30.0f); + float const perFrameDecayRateAtFps2 = std::pow(perSecondDecayRateAtFps1, static_cast(secondsSinceLastFrame)); + + return perFrameDecayRateAtFps2; +} + +} // namespace Audio +} // namespace libprojectM diff --git a/src/libprojectM/Audio/Loudness.hpp b/src/libprojectM/Audio/Loudness.hpp new file mode 100644 index 000000000..55f3baf8c --- /dev/null +++ b/src/libprojectM/Audio/Loudness.hpp @@ -0,0 +1,97 @@ +/** + * @file Loudness.hpp + * @brief Calculates loudness values in relation to previous frames. +**/ + +#pragma once + +#include "AudioConstants.hpp" + +#include +#include + +namespace libprojectM { +namespace Audio { + +/** + * @brief Calculates beat-detection loudness relative to the previous frame(s). + */ +class Loudness +{ +public: + /** + * @brief Frequency bands. + * Only the first half of the spectrum is used for these bands, each using one third of this half. + */ + enum class Band : int + { + Bass = 0, //!< Bass band (first sixth of the spectrum) + Middles = 1, //!< Middles band (second sixth of the spectrum) + Treble = 2 //!< Treble band (third sixth of the spectrum) + }; + + /** + * @brief Constructor. + * @param band The band to use for this loudness instance. + */ + explicit Loudness(Band band); + + /** + * @brief Updates the beat detection values and averages. + * Must only be called once per frame. + * @param spectrumSamples The current frame's spectrum analyzer samples to use for the update. + * @param secondsSinceLastFrame (Fractional) seconds passed since the last frame. + * @param frame The number of frames already rendered since start. For the first few frames, a different dampening factor + * will be used to avoid "jumpy" behaviour of the values. + */ + void Update(const std::array& spectrumSamples, double secondsSinceLastFrame, uint32_t frame); + + /** + * @brief Returns the current frame's unattenuated loudness relative to the previous frame. + * This value will revolve around 1.0, with <0.7 being very silent and >1.3 very loud audio. + * @return The current frame's unattenuated loudness relative to the previous frame. + */ + auto CurrentRelative() const -> float; + + /** + * @brief Returns the attenuated loudness averaged over the previous frames. + * This value will revolve around 1.0, with <0.7 being very silent and >1.3 very loud audio. + * Does not change as much as the value returned by CurrentRelative(). + * @return The attenuated loudness averaged over the previous frames. + */ + auto AverageRelative() const -> float; + +private: + /** + * @brief Sums up the spectrum samples for the configured band. + * @param spectrumSamples The spectrum analyzer samples to use. + */ + void SumBand(const std::array& spectrumSamples); + + /** + * @brief Updates the short- and long-term averages and relative values. + * @param secondsSinceLastFrame (Fractional) seconds passed since the last frame. + * @param frame The number of frames already rendered since start. + */ + void UpdateBandAverage(double secondsSinceLastFrame, uint32_t frame); + + /** + * @brief Adjusts the dampening rate according the the current FPS. + * @param rate The rate to be dampened. + * @param secondsSinceLastFrame (Fractional) seconds passed since the last frame. + * @return The dampened rate value. + */ + static auto AdjustRateToFps(float rate, double secondsSinceLastFrame) -> float; + + Band m_band{Band::Bass}; //!< The frequency band to use for this instance. + + float m_current{}; //!< The current frame's sum of all frequency strengths in the current band. + float m_average{}; //!< The short-term averaged value of m_current. + float m_longAverage{}; //!< The long-term averaged value of m_current. + + float m_currentRelative{1.0f}; //!< The relative loudness value to the previous frame. + float m_averageRelative{1.0f}; //!< The attenuated relative loudness value. +}; + +} // namespace Audio +} // namespace libprojectM diff --git a/src/libprojectM/Audio/MilkdropFFT.cpp b/src/libprojectM/Audio/MilkdropFFT.cpp index 2c03bed5d..83dfe128d 100644 --- a/src/libprojectM/Audio/MilkdropFFT.cpp +++ b/src/libprojectM/Audio/MilkdropFFT.cpp @@ -34,11 +34,10 @@ namespace Audio { constexpr auto PI = 3.141592653589793238462643383279502884197169399f; -void MilkdropFFT::Init(size_t samplesIn, size_t samplesOut, bool equalize, float envelopePower) +MilkdropFFT::MilkdropFFT(size_t samplesIn, size_t samplesOut, bool equalize, float envelopePower) + : m_samplesIn(samplesIn) + , m_numFrequencies(samplesOut * 2) { - m_samplesIn = samplesIn; - m_numFrequencies = samplesOut * 2; - InitBitRevTable(); InitCosSinTable(); InitEnvelopeTable(envelopePower); diff --git a/src/libprojectM/Audio/MilkdropFFT.hpp b/src/libprojectM/Audio/MilkdropFFT.hpp index 4e359843e..27f4dab1e 100644 --- a/src/libprojectM/Audio/MilkdropFFT.hpp +++ b/src/libprojectM/Audio/MilkdropFFT.hpp @@ -52,7 +52,7 @@ public: * @param envelopePower Specifies the envelope power. Set to any negative value to disable the envelope. * See InitEnvelopeTable for more info. */ - void Init(size_t samplesIn, size_t samplesOut, bool equalize = true, float envelopePower = 1.0f); + MilkdropFFT(size_t samplesIn, size_t samplesOut, bool equalize = true, float envelopePower = 1.0f); /** * @breif Converts time-domain samples into frequency-domain samples. diff --git a/src/libprojectM/Audio/PCM.cpp b/src/libprojectM/Audio/PCM.cpp index b204d990f..e18894447 100755 --- a/src/libprojectM/Audio/PCM.cpp +++ b/src/libprojectM/Audio/PCM.cpp @@ -1,239 +1,130 @@ -/** - * @file PCM.cpp - * @brief Takes sound data from wherever and hands it back out. - * - * Returns PCM Data or spectrum data, or the derivative of the PCM data - * - * projectM -- Milkdrop-esque visualisation SDK - * Copyright (C)2003-2004 projectM Team - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * See 'LICENSE.txt' included within this release - * - */ - #include "PCM.hpp" -#include "MilkdropFFT.hpp" -#include "AudioConstants.hpp" - #include -#include namespace libprojectM { namespace Audio { -/* - * Here is where we try to do auto volume setting. Doing this here - * means that none of the code downstream (waveforms, beatdetect, etc) needs - * to worry about it. - * - * 1) Don't overreact to level changes within a song - * 2) Ignore silence/gaps - * - * I don't know if it's necessary to have both sum and max, but that makes - * it easier to experiment... - */ -auto PCM::AutoLevel::UpdateLevel( - size_t const samples, - double const sum, - double const max) -> double -{ - - // This is an arbitrary number that helps control - // a) how quickly the level can change and - // b) keeps code from being affected by how the caller provides data (lot of short buffers, or fewer long buffers) - size_t constexpr autolevelSegment = 4096; - - if (sum / static_cast(samples) < 0.00001) - { - return m_level; - } - m_levelSum += sum; - m_levelax = std::max(m_levelax, max * 1.02); - m_levelSamples += samples; - if (m_levelSamples >= autolevelSegment || m_l0 <= 0) - { - double const maxRecent = std::max(std::max(m_l0, m_l1), std::max(m_l2, m_levelax)); - m_l0 = m_l1; - m_l1 = m_l2; - m_l2 = m_levelax; - m_levelax *= 0.95; - m_levelSamples = 0; - m_levelSum = 0; - m_level = (m_l0 <= 0) ? maxRecent : m_level * 0.96 + maxRecent * 0.04; - m_level = std::max(m_level, 0.0001); - } - return m_level; -} - - template< - size_t lOffset, - size_t rOffset, - size_t stride, int signalAmplitude, int signalOffset, - class SampleType> -void PCM::AddPcm( + typename SampleType> +void PCM::AddToBuffer( SampleType const* const samples, - size_t const count) + uint32_t channels, + size_t const sampleCount) { - float sum = 0; - float max = 0; - for (size_t i = 0; i < count; i++) + if (channels == 0 || sampleCount == 0) { - size_t const j = (m_start + i) % maxSamples; - m_pcmL[j] = (samples[lOffset + i * stride] - signalOffset) / float(signalAmplitude); - m_pcmR[j] = (samples[rOffset + i * stride] - signalOffset) / float(signalAmplitude); - sum += std::abs(m_pcmL[j]) + std::abs(m_pcmR[j]); - max = std::max(std::max(max, std::abs(m_pcmL[j])), std::abs(m_pcmR[j])); + return; } - m_start = (m_start + count) % maxSamples; - m_level = m_leveler.UpdateLevel(count, sum / 2, max); -} - -void PCM::AddMono(float const* const samples, size_t const count) -{ - AddPcm<0, 0, 1, 1, 0>(samples, count); -} -void PCM::AddMono(uint8_t const* const samples, size_t const count) -{ - AddPcm<0, 0, 1, 128, 0>(samples, count); -} -void PCM::AddMono(int16_t const* const samples, size_t const count) -{ - AddPcm<0, 0, 1, 32768, 0>(samples, count); -} - - -void PCM::AddStereo(float const* const samples, size_t const count) -{ - AddPcm<0, 1, 2, 1, 0>(samples, count); -} -void PCM::AddStereo(uint8_t const* const samples, size_t const count) -{ - AddPcm<0, 1, 2, 128, 0>(samples, count); -} -void PCM::AddStereo(int16_t const* const samples, size_t const count) -{ - AddPcm<0, 1, 2, 32768, 0>(samples, count); -} - - -// puts sound data requested at provided pointer -// -// samples is number of PCM samples to return -// returned values are normalized from -1 to 1 - -void PCM::GetPcm( - float* const data, - CHANNEL const channel, - size_t const samples) const -{ - assert(channel == 0 || channel == 1); - - CopyPcm(data, channel, samples); -} - - -void PCM::GetSpectrum( - float* const data, - CHANNEL const channel, - size_t const samples) -{ - assert(channel == 0 || channel == 1); - UpdateFftChannel(channel); - - auto const& spectrum = channel == 0 ? m_spectrumL : m_spectrumR; - size_t const count = samples <= fftLength ? samples : fftLength; - for (size_t i = 0; i < count; i++) + for (size_t i = 0; i < sampleCount; i++) { - data[i] = spectrum[i]; - } - for (size_t i = count; i < samples; i++) - { - data[0] = 0; + size_t const bufferOffset = (m_start + i) % WaveformSamples; + m_inputBufferL[bufferOffset] = 128.0f * (static_cast(samples[0 + i * channels]) - float(signalOffset)) / float(signalAmplitude); + if (channels > 1) + { + m_inputBufferR[bufferOffset] = 128.0f * (static_cast(samples[1 + i * channels]) - float(signalOffset)) / float(signalAmplitude); + } + else + { + m_inputBufferR[bufferOffset] = m_inputBufferL[bufferOffset]; + } } + m_start = (m_start + sampleCount) % WaveformSamples; } -void PCM::ResetAutoLevel() +void PCM::Add(float const* const samples, uint32_t channels, size_t const count) { - m_leveler = AutoLevel(); - m_level = 1.0f; + AddToBuffer<1, 0>(samples, channels, count); +} +void PCM::Add(uint8_t const* const samples, uint32_t channels, size_t const count) +{ + AddToBuffer<128, 128>(samples, channels, count); +} +void PCM::Add(int16_t const* const samples, uint32_t channels, size_t const count) +{ + AddToBuffer<0, 32768>(samples, channels, count); +} + +void PCM::UpdateFrameAudioData(double secondsSinceLastFrame, uint32_t frame) +{ + // 1. Copy audio data from input buffer + CopyNewWaveformData(); + + // 2. Align waveforms + + // 3. Update spectrum analyzer data for both channels + UpdateFftChannel(0); + UpdateFftChannel(1); + + // 4. Update beat detection values + m_bass.Update(m_spectrumL, secondsSinceLastFrame, frame); + m_middles.Update(m_spectrumL, secondsSinceLastFrame, frame); + m_treble.Update(m_spectrumL, secondsSinceLastFrame, frame); +} + +auto PCM::GetFrameAudioData() const -> FrameAudioData +{ + FrameAudioData data{}; + + std::copy(m_waveformL.begin(), m_waveformL.end(), data.waveformLeft.begin()); + std::copy(m_waveformR.begin(), m_waveformR.end(), data.waveformRight.begin()); + std::copy(m_spectrumL.begin(), m_spectrumL.end(), data.spectrumLeft.begin()); + std::copy(m_spectrumR.begin(), m_spectrumR.end(), data.spectrumRight.begin()); + + data.bass = m_bass.CurrentRelative(); + data.mid = m_middles.CurrentRelative(); + data.treb = m_treble.CurrentRelative(); + + data.bassAtt = m_bass.AverageRelative(); + data.midAtt = m_middles.AverageRelative(); + data.trebAtt = m_treble.AverageRelative(); + + data.vol = (data.bass + data.mid + data.treb) * 0.333f; + data.volAtt = (data.bassAtt + data.midAtt + data.trebAtt) * 0.333f; + + return data; } void PCM::UpdateFftChannel(size_t const channel) { assert(channel == 0 || channel == 1); - // ToDo: Add as member, init only once. - MilkdropFFT fft; - fft.Init(WaveformSamples, fftLength, true); - std::vector waveformSamples(WaveformSamples); std::vector spectrumValues; - // Get waveform data from ring buffer - auto const& from = channel == 0 ? m_pcmL : m_pcmR; - for (size_t i = 0, pos = m_start; i < WaveformSamples; i++) + auto const& from = channel == 0 ? m_waveformL : m_waveformR; + auto& spectrum = channel == 0 ? m_spectrumL : m_spectrumR; + + size_t oldI{0}; + for (size_t i = 0; i < WaveformSamples; i++) { - if (pos == 0) - { - pos = maxSamples; - } - waveformSamples[i] = static_cast(from[--pos]); + // Damp the input into the FFT a bit, to reduce high-frequency noise: + waveformSamples[i] = 0.5f * (from[i] + from[oldI]); + oldI = i; } - fft.TimeToFrequencyDomain(waveformSamples, spectrumValues); + m_fft.TimeToFrequencyDomain(waveformSamples, spectrumValues); - auto& spectrum = channel == 0 ? m_spectrumL : m_spectrumR; std::copy(spectrumValues.begin(), spectrumValues.end(), spectrum.begin()); } -// pull data from circular buffer -void PCM::CopyPcm(float* const to, size_t const channel, size_t const count) const +void PCM::CopyNewWaveformData() { - assert(channel == 0 || channel == 1); - assert(count < maxSamples); - auto const& from = channel == 0 ? m_pcmL : m_pcmR; - const double volume = 1.0 / m_level; - for (size_t i = 0, pos = m_start; i < count; i++) + const auto& copyChannel = + [](size_t start, const std::array& inputSamples, std::array& outputSamples) { - if (pos == 0) + for (size_t i = 0; i < WaveformSamples; i++) { - pos = maxSamples; + outputSamples[i] = inputSamples[(start + i) % WaveformSamples]; } - to[i] = static_cast(from[--pos] * volume); - } -} + }; -void PCM::CopyPcm(double* const to, size_t const channel, size_t const count) const -{ - assert(channel == 0 || channel == 1); - auto const& from = channel == 0 ? m_pcmL : m_pcmR; - double const volume = 1.0 / m_level; - for (size_t i = 0, pos = m_start; i < count; i++) - { - if (pos == 0) - { - pos = maxSamples; - } - to[i] = from[--pos] * volume; - } + auto const bufferStartIndex = m_start.load(); + copyChannel(bufferStartIndex, m_inputBufferL, m_waveformL); + copyChannel(bufferStartIndex, m_inputBufferR, m_waveformR); } } // namespace Audio diff --git a/src/libprojectM/Audio/PCM.hpp b/src/libprojectM/Audio/PCM.hpp index bf46f632b..459bea36f 100755 --- a/src/libprojectM/Audio/PCM.hpp +++ b/src/libprojectM/Audio/PCM.hpp @@ -1,36 +1,22 @@ /** - * projectM -- Milkdrop-esque visualisation SDK - * Copyright (C)2003-2007 projectM Team + * @file PCM.hpp + * @brief Takes sound data from the outside, analyzes it and hands it back out. * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * See 'LICENSE.txt' included within this release - * - */ -/** - * $Id$ - * - * Encapsulation of raw sound buffer. Used in beat detection - * - * $Log$ - */ + * Returns waveform and spectrum data, as well as relative beat detection values. + **/ #pragma once -#include "projectM-4/projectM_export.h" +#include "AudioConstants.hpp" + +#include "FrameAudioData.hpp" +#include "Loudness.hpp" +#include "MilkdropFFT.hpp" + +#include #include +#include #include #include @@ -38,124 +24,89 @@ namespace libprojectM { namespace Audio { -// FFT_LENGTH is number of magnitude values available from getSpectrum(). -// Internally this is generated using 2xFFT_LENGTH samples per channel. -size_t constexpr fftLength = 512; - -enum CHANNEL -{ - CHANNEL_L = 0, - CHANNEL_0 = 0, - CHANNEL_R = 1, - CHANNEL_1 = 1 -}; - class PROJECTM_EXPORT PCM { public: - /* maximum number of sound samples that are actually stored. */ - static constexpr size_t maxSamples = 2048; - /** - * @brief Adds a mono pcm buffer to the storage + * @brief Adds new interleaved floating-point PCM data to the buffer. + * Left channel is expected at offset 0, right channel at offset 1. Other channels are ignored. * @param samples The buffer to be added + * @param channels The number of channels in the input data. * @param count The amount of samples in the buffer */ - void AddMono(const float* samples, size_t count); - void AddMono(const uint8_t* samples, size_t count); - void AddMono(const int16_t* samples, size_t count); + void Add(const float* samples, uint32_t channels, size_t count); /** - * @brief Adds a stereo pcm buffer to the storage - * @param samples The buffer to be added. - * The channels are expected to be interleaved, LRLR. - * @param count The amount of samples in each channel (not total samples) + * @brief Adds new mono unsigned 8-bit PCM data to the storage + * Left channel is expected at offset 0, right channel at offset 1. Other channels are ignored. + * @param samples The buffer to be added + * @param channels The number of channels in the input data. + * @param count The amount of samples in the buffer */ - void AddStereo(const float* samples, size_t count); - void AddStereo(const uint8_t* samples, size_t count); - void AddStereo(const int16_t* samples, size_t count); + void Add(const uint8_t* samples, uint32_t channels, size_t count); /** - * PCM data - * The returned data will 'wrap' if more than maxsamples are requested. + * @brief Adds new mono signed 16-bit PCM data to the storage + * Left channel is expected at offset 0, right channel at offset 1. Other channels are ignored. + * @param samples The buffer to be added + * @param channels The number of channels in the input data. + * @param count The amount of samples in the buffer */ - void GetPcm(float* data, CHANNEL channel, size_t samples) const; - - /** Spectrum data - * The returned data will be zero padded if more than FFT_LENGTH values are requested - */ - void GetSpectrum(float* data, CHANNEL channel, size_t samples); + void Add(const int16_t* samples, uint32_t channels, size_t count); /** - * @brief Resets the auto leveler state. - * Makes sense after pausing/resuming audio or other interruptions. + * @brief Updates the internal audio data values for rendering the next frame. + * This method must only be called once per frame, as it does some temporal blending + * and alignments using the previous frame(s) as reference. This function will perform: + * - Passing the waveform data into the spectrum analyzer + * - Aligning waveforms to a best-fit match to the previous frame to produce a calmer waveform shape. + * - Calculating the bass/mid/treb values and their attenuated (time-smoothed) versions. + * + * @param secondsSinceLastFrame Time passed since rendering the last frame. Basically 1.0/FPS. + * @param frame Frames rendered since projectM was started. */ - void ResetAutoLevel(); + void UpdateFrameAudioData(double secondsSinceLastFrame, uint32_t frame); -protected: - // CPP20: Could use a struct for the first 5 params to clarify on call site - // together with designated initializers + /** + * @brief Returns a class holding a copy of the current frame audio data. + * @return A FrameAudioData class with waveform, spectrum and other derived values. + */ + auto GetFrameAudioData() const -> FrameAudioData; + +private: template< - size_t lOffset, - size_t rOffset, - size_t stride, int signalAmplitude, int signalOffset, - class SampleType> - void AddPcm(const SampleType* samples, size_t count); - - // copy data out of the circular PCM buffer - void CopyPcm(float* to, size_t channel, size_t count) const; - void CopyPcm(double* to, size_t channel, size_t count) const; + typename SampleType> + void AddToBuffer(const SampleType* samples, uint32_t channel, size_t sampleCount); // Updates FFT data void UpdateFftChannel(size_t channel); -private: - // mem-usage: - // pcmd 2x2048*4b = 16K - // vdata 2x512x2*8b = 16K - // spectrum 2x512*4b = 4k - // w = 512*8b = 4k + /** + * Copies data out of the circular input buffer into the per-frame waveform buffer. + */ + void CopyNewWaveformData(); - // circular PCM buffer - // adjust "volume" of PCM data as we go, this simplifies everything downstream... - // normalize to range [-1.0,1.0] - std::array m_pcmL{0.f}; - std::array m_pcmR{0.f}; - size_t m_start{0}; - size_t m_newSamples{0}; + // External input buffer + std::array m_inputBufferL{0.f}; //!< Circular buffer for left-channel PCM data. + std::array m_inputBufferR{0.f}; //!< Circular buffer for right-channel PCM data. + std::atomic m_start{0}; //!< Circular buffer start index. - // raw FFT data - std::array m_freqL{0.0}; - std::array m_freqR{0.0}; - // magnitude data - std::array m_spectrumL{0.f}; - std::array m_spectrumR{0.f}; + // Frame waveform data + std::array m_waveformL{0.f}; //!< Left-channel waveform data, aligned. + std::array m_waveformR{0.f}; //!< Right-channel waveform data, aligned. - std::array m_w{0.0}; - std::array m_ip{0}; + // Frame spectrum data + std::array m_spectrumL{0.f}; //!< Left-channel spectrum data. + std::array m_spectrumR{0.f}; //!< Right-channel spectrum data. - // see https://github.com/projectM-visualizer/projectm/issues/161 - class PROJECTM_EXPORT AutoLevel - { - public: - auto UpdateLevel(size_t samples, double sum, double max) -> double; + MilkdropFFT m_fft{WaveformSamples, SpectrumSamples, true}; //!< Spectrum analyzer instance. - private: - double m_level{0.01}; - // accumulate sample data - size_t m_levelSamples{0}; - double m_levelSum{0.0}; - double m_levelax{0.0}; - double m_l0{-1.0}; - double m_l1{-1.0}; - double m_l2{-1.0}; - }; - - // state for tracking audio level - double m_level{1.f}; - AutoLevel m_leveler{}; + // Frame beat detection values + Loudness m_bass{Loudness::Band::Bass}; //!< Beat detection/volume for the "bass" band. + Loudness m_middles{Loudness::Band::Middles}; //!< Beat detection/volume for the "middles" band. + Loudness m_treble{Loudness::Band::Treble}; //!< Beat detection/volume for the "treble" band. }; } // namespace Audio diff --git a/src/libprojectM/MilkdropPreset/CustomShape.cpp b/src/libprojectM/MilkdropPreset/CustomShape.cpp index 5d40b2664..7a3405f89 100644 --- a/src/libprojectM/MilkdropPreset/CustomShape.cpp +++ b/src/libprojectM/MilkdropPreset/CustomShape.cpp @@ -2,7 +2,6 @@ #include "PresetFileParser.hpp" -#include "Audio/BeatDetect.hpp" #include "Renderer/TextureManager.hpp" #include diff --git a/src/libprojectM/MilkdropPreset/CustomWaveform.cpp b/src/libprojectM/MilkdropPreset/CustomWaveform.cpp index c34d09638..867f7baf3 100644 --- a/src/libprojectM/MilkdropPreset/CustomWaveform.cpp +++ b/src/libprojectM/MilkdropPreset/CustomWaveform.cpp @@ -93,6 +93,7 @@ void CustomWaveform::Draw(const PerFrameContext& presetPerFrameContext) InitPerPointEvaluationVariables(); int sampleCount = std::min(WaveformMaxPoints, static_cast(*m_perFrameContext.samples)); + sampleCount -= m_sep; // If there aren't enough samples to draw a single line or dot, skip drawing the waveform. if ((m_useDots && sampleCount < 1) || sampleCount < 2) @@ -107,8 +108,8 @@ void CustomWaveform::Draw(const PerFrameContext& presetPerFrameContext) ? m_presetState.audioData.spectrumRight.data() : m_presetState.audioData.waveformRight.data(); - //const float mult = m_scaling * m_presetState.waveScale * (m_spectrum ? 0.15f : 0.004f); - const float mult = m_scaling * m_presetState.waveScale * (m_spectrum ? 0.05f : 1.0f); + const float mult = m_scaling * m_presetState.waveScale * (m_spectrum ? 0.15f : 0.004f); + //const float mult = m_scaling * m_presetState.waveScale * (m_spectrum ? 0.05f : 1.0f); // PCM data smoothing const int offset1 = m_spectrum ? 0 : (maxSampleCount - sampleCount) / 2 - m_sep / 2; diff --git a/src/libprojectM/MilkdropPreset/Waveform.cpp b/src/libprojectM/MilkdropPreset/Waveform.cpp index 963672f5b..4b24bddc3 100644 --- a/src/libprojectM/MilkdropPreset/Waveform.cpp +++ b/src/libprojectM/MilkdropPreset/Waveform.cpp @@ -3,7 +3,6 @@ #include "PerFrameContext.hpp" #include "PresetState.hpp" -#include "Audio/BeatDetect.hpp" #include "projectM-opengl.h" #include @@ -284,15 +283,16 @@ void Waveform::WaveformMath(const PerFrameContext& presetPerFrameContext) // Tie size of waveform to beatSensitivity // ToDo: Beat sensitivity was probably not the correct value here? - const float volumeScale = m_presetState.audioData.vol; - if (volumeScale != 1.0) + //const float volumeScale = m_presetState.audioData.vol; + //if (volumeScale != 1.0) + //{ + // Scale waveform data to -1 .. 1 range + for (size_t i = 0; i < pcmDataL.size(); ++i) { - for (size_t i = 0; i < pcmDataL.size(); ++i) - { - pcmDataL[i] *= volumeScale; - pcmDataR[i] *= volumeScale; - } + pcmDataL[i] /= 128.0f; + pcmDataR[i] /= 128.0f; } + //} // Aspect multipliers float aspectX{1.0f}; diff --git a/src/libprojectM/ProjectM.cpp b/src/libprojectM/ProjectM.cpp index 9400cc73f..799a99639 100644 --- a/src/libprojectM/ProjectM.cpp +++ b/src/libprojectM/ProjectM.cpp @@ -25,7 +25,6 @@ #include "PresetFactoryManager.hpp" #include "TimeKeeper.hpp" -#include "Audio/BeatDetect.hpp" #include "Audio/PCM.hpp" //Sound data handler (buffering, FFT, etc.) #include "Renderer/CopyTexture.hpp" @@ -143,13 +142,16 @@ void ProjectM::RenderFrame() return; } - m_timeKeeper->UpdateTimers(); - m_beatDetect->CalculateBeatStatistics(); - #if PROJECTM_USE_THREADS std::lock_guard guard(m_presetSwitchMutex); #endif + // Update FPS and other timer values. + m_timeKeeper->UpdateTimers(); + + // Update and retrieve audio data + m_audioStorage.UpdateFrameAudioData(m_timeKeeper->SecondsSinceLastFrame(), m_frameCount); + auto audioData = m_audioStorage.GetFrameAudioData(); // Check if the preset isn't locked, and we've not already notified the user if (!m_presetChangeNotified) @@ -161,7 +163,8 @@ void ProjectM::RenderFrame() PresetSwitchRequestedEvent(false); } else if (m_hardCutEnabled && - (m_beatDetect->vol - m_beatDetect->volOld > m_hardCutSensitivity) && + m_frameCount > 50 && + (audioData.vol - m_previousFrameVolume > m_hardCutSensitivity) && m_timeKeeper->CanHardCut()) { m_presetChangeNotified = true; @@ -198,7 +201,6 @@ void ProjectM::RenderFrame() } auto renderContext = GetRenderContext(); - auto audioData = m_beatDetect->GetFrameAudioData(); if (m_transition != nullptr && m_transitioningPreset != nullptr) { @@ -231,6 +233,7 @@ void ProjectM::RenderFrame() } m_frameCount++; + m_previousFrameVolume = audioData.vol; } void ProjectM::Initialize() @@ -245,10 +248,6 @@ void ProjectM::Initialize() /** Initialise per-pixel matrix calculations */ /** We need to initialise this before the builtin param db otherwise bass/mid etc won't bind correctly */ - assert(!m_beatDetect); - - m_beatDetect = std::make_unique(m_pcm); - m_textureManager = std::make_unique(m_textureSearchPaths); m_transitionShaderManager = std::make_unique(); @@ -260,7 +259,6 @@ void ProjectM::Initialize() /* Set the seed to the current time in seconds */ srand(time(nullptr)); - ResetEngine(); LoadIdlePreset(); #if PROJECTM_USE_THREADS @@ -277,17 +275,6 @@ void ProjectM::LoadIdlePreset() assert(m_activePreset); } -/* Reinitializes the engine variables to a default (conservative and sane) value */ -void ProjectM::ResetEngine() -{ - - if (m_beatDetect != NULL) - { - m_beatDetect->Reset(); - m_beatDetect->beatSensitivity = m_beatSensitivity; - } -} - /** Resets OpenGL state */ void ProjectM::ResetOpenGL(size_t width, size_t height) { @@ -361,12 +348,12 @@ auto ProjectM::PresetLocked() const -> bool void ProjectM::SetBeatSensitivity(float sensitivity) { - m_beatDetect->beatSensitivity = std::min(std::max(0.0f, sensitivity), 2.0f); + m_beatSensitivity = std::min(std::max(0.0f, sensitivity), 2.0f); } auto ProjectM::GetBeatSensitivity() const -> float { - return m_beatDetect->beatSensitivity; + return m_beatSensitivity; } auto ProjectM::SoftCutDuration() const -> double @@ -481,7 +468,7 @@ void ProjectM::SetMeshSize(size_t meshResolutionX, size_t meshResolutionY) auto ProjectM::PCM() -> libprojectM::Audio::PCM& { - return m_pcm; + return m_audioStorage; } void ProjectM::Touch(float touchX, float touchY, int pressure, int touchType) diff --git a/src/libprojectM/ProjectM.hpp b/src/libprojectM/ProjectM.hpp index f850b3364..bf5cae969 100644 --- a/src/libprojectM/ProjectM.hpp +++ b/src/libprojectM/ProjectM.hpp @@ -212,8 +212,6 @@ public: private: void Initialize(); - void ResetEngine(); - void StartPresetTransition(std::unique_ptr&& preset, bool hardCut); void LoadIdlePreset(); @@ -226,7 +224,6 @@ private: #endif - class libprojectM::Audio::PCM m_pcm; //!< Audio data buffer and analyzer instance. size_t m_meshX{32}; //!< Per-point mesh horizontal resolution. size_t m_meshY{24}; //!< Per-point mesh vertical resolution. @@ -241,6 +238,7 @@ private: float m_beatSensitivity{1.0}; //!< General beat sensitivity modifier for presets. bool m_aspectCorrection{true}; //!< If true, corrects aspect ratio for non-rectangular windows. float m_easterEgg{0.0}; //!< Random preset duration modifier. See TimeKeeper class. + float m_previousFrameVolume{}; //!< Volume in previous frame, used for hard cuts. std::vector m_textureSearchPaths; ///!< List of paths to search for texture files @@ -252,10 +250,10 @@ private: std::unique_ptr m_presetFactoryManager; //!< Provides access to all available preset factories. + libprojectM::Audio::PCM m_audioStorage; //!< Audio data buffer and analyzer instance. std::unique_ptr m_textureManager; //!< The texture manager. std::unique_ptr m_transitionShaderManager; //!< The transition shader manager. std::unique_ptr m_textureCopier; //!< Class that copies textures 1:1 to another texture or framebuffer. - std::unique_ptr m_beatDetect; //!< The beat detection class. std::unique_ptr m_activePreset; //!< Currently loaded preset. std::unique_ptr m_transitioningPreset; //!< Destination preset when smooth preset switching. std::unique_ptr m_transition; //!< Transition effect used for blending. diff --git a/src/libprojectM/ProjectMCWrapper.cpp b/src/libprojectM/ProjectMCWrapper.cpp index 7e5da6c29..3baabe5fd 100644 --- a/src/libprojectM/ProjectMCWrapper.cpp +++ b/src/libprojectM/ProjectMCWrapper.cpp @@ -2,8 +2,9 @@ #include "projectM-4/projectM.h" +#include "Audio/AudioConstants.hpp" + #include -#include #include void projectMWrapper::PresetSwitchRequestedEvent(bool isHardCut) const @@ -337,7 +338,7 @@ void projectm_set_window_size(projectm_handle instance, size_t width, size_t hei unsigned int projectm_pcm_get_max_samples() { - return libprojectM::Audio::PCM::maxSamples; + return libprojectM::Audio::WaveformSamples; } template @@ -345,14 +346,7 @@ static auto PcmAdd(projectm_handle instance, const BufferType* samples, unsigned { auto* projectMInstance = handle_to_instance(instance); - if (channels == PROJECTM_MONO) - { - projectMInstance->PCM().AddMono(samples, count); - } - else - { - projectMInstance->PCM().AddStereo(samples, count); - } + projectMInstance->PCM().Add(samples, channels, count); } auto projectm_pcm_add_float(projectm_handle instance, const float* samples, unsigned int count, projectm_channels channels) -> void diff --git a/src/libprojectM/TimeKeeper.cpp b/src/libprojectM/TimeKeeper.cpp index 839401e6c..91c35a0ed 100644 --- a/src/libprojectM/TimeKeeper.cpp +++ b/src/libprojectM/TimeKeeper.cpp @@ -17,7 +17,9 @@ void TimeKeeper::UpdateTimers() { auto currentTime = std::chrono::high_resolution_clock::now(); - m_currentTime = std::chrono::duration(currentTime - m_startTime).count(); + double currentFrameTime = std::chrono::duration(currentTime - m_startTime).count(); + m_secondsSinceLastFrame = currentFrameTime - m_currentTime; + m_currentTime = currentFrameTime; m_presetFrameA++; m_presetFrameB++; } diff --git a/src/libprojectM/TimeKeeper.hpp b/src/libprojectM/TimeKeeper.hpp index c79e6c488..273f1c6ec 100644 --- a/src/libprojectM/TimeKeeper.hpp +++ b/src/libprojectM/TimeKeeper.hpp @@ -6,7 +6,6 @@ class TimeKeeper { public: - TimeKeeper(double presetDuration, double smoothDuration, double hardcutDuration, double easterEgg); void UpdateTimers(); @@ -79,27 +78,31 @@ public: m_easterEgg = value; } + inline auto SecondsSinceLastFrame() const -> double + { + return m_secondsSinceLastFrame; + } + private: - /* The first ticks value of the application */ - std::chrono::high_resolution_clock::time_point m_startTime{ std::chrono::high_resolution_clock::now() }; + std::chrono::high_resolution_clock::time_point m_startTime{std::chrono::high_resolution_clock::now()}; - double m_easterEgg{ 0.0 }; + double m_secondsSinceLastFrame{}; - double m_presetDuration{ 0.0 }; - double m_presetDurationA{ 0.0 }; - double m_presetDurationB{ 0.0 }; - double m_softCutDuration{ 0.0 }; - double m_hardCutDuration{ 0.0 }; + double m_easterEgg{}; - double m_currentTime{ 0.0 }; - double m_presetTimeA{ 0.0 }; - double m_presetTimeB{ 0.0 }; + double m_presetDuration{}; + double m_presetDurationA{}; + double m_presetDurationB{}; + double m_softCutDuration{}; + double m_hardCutDuration{}; - int m_presetFrameA{ 0 }; - int m_presetFrameB{ 0 }; - - bool m_isSmoothing{ false }; + double m_currentTime{}; + double m_presetTimeA{}; + double m_presetTimeB{}; + int m_presetFrameA{}; + int m_presetFrameB{}; + bool m_isSmoothing{false}; }; diff --git a/tests/libprojectM/CMakeLists.txt b/tests/libprojectM/CMakeLists.txt index 93346b459..963818ee2 100644 --- a/tests/libprojectM/CMakeLists.txt +++ b/tests/libprojectM/CMakeLists.txt @@ -1,7 +1,6 @@ find_package(GTest 1.10 REQUIRED NO_MODULE) add_executable(projectM-unittest - PCMTest.cpp PresetFileParserTest.cpp $ diff --git a/tests/libprojectM/PCMTest.cpp b/tests/libprojectM/PCMTest.cpp index 7a7e83dd9..eef7daa61 100644 --- a/tests/libprojectM/PCMTest.cpp +++ b/tests/libprojectM/PCMTest.cpp @@ -18,6 +18,24 @@ public: { PCM::CopyPcm(to, channel, count); } + + /** + * @brief Returns a copy of the current frame's PCM data. + * The returned data will 'wrap' if more than maxsamples are requested. + */ + void GetPcm(float* data, uint channel, size_t samples) const + { + PCM::GetPcm(data, channel, samples); + } + + /** Spectrum data + * The returned data will be zero padded if more than FFT_LENGTH values are requested + */ + void GetSpectrum(float* data, uint channel, size_t samples) const + { + PCM::GetSpectrum(data, channel, samples); + } + }; TEST(PCM, AddDataMonoFloat) @@ -32,11 +50,10 @@ TEST(PCM, AddDataMonoFloat) } for (size_t i = 0; i < 10; i++) { - pcm.AddMono(data.data(), samples); + pcm.Add(data.data(), 1, samples); } std::array copy{}; - pcm.ResetAutoLevel(); pcm.CopyPcm(copy.data(), 0, copy.size()); for (size_t i = 0; i < samples; i++) { @@ -62,12 +79,11 @@ TEST(PCM, AddDataStereoFloat) } for (size_t i = 0; i < 10; i++) { - pcm.AddStereo(data.data(), samples); + pcm.Add(data.data(), 2, samples); } std::array copy0{}; std::array copy1{}; - pcm.ResetAutoLevel(); pcm.CopyPcm(copy0.data(), 0, copy0.size()); pcm.CopyPcm(copy1.data(), 1, copy1.size()); for (size_t i = 0; i < samples; i++) @@ -89,22 +105,21 @@ TEST(PCM, DISABLED_FastFourierTransformLowFrequency) data[i * 2] = std::sin(f); data[i * 2 + 1] = std::sin(f + 1.f);// out of phase } - pcm.AddStereo(data.data(), samples); - pcm.AddStereo(data.data(), samples); + pcm.Add(data.data(), 2, samples); + pcm.Add(data.data(), 2, samples); - std::array freq0{}; - std::array freq1{}; - pcm.ResetAutoLevel(); - pcm.GetSpectrum(freq0.data(), CHANNEL_0, fftLength); - pcm.GetSpectrum(freq1.data(), CHANNEL_1, fftLength); + std::array freq0{}; + std::array freq1{}; + pcm.GetSpectrum(freq0.data(), 0, SpectrumSamples); + pcm.GetSpectrum(freq1.data(), 1, SpectrumSamples); // freq0 and freq1 should be equal - for (size_t i = 0; i < fftLength; i++) + for (size_t i = 0; i < SpectrumSamples; i++) { EXPECT_FLOAT_EQ(freq0[i], freq1[i]); } EXPECT_GT(freq0[0], 500); - for (size_t i = 1; i < fftLength; i++) + for (size_t i = 1; i < SpectrumSamples; i++) { EXPECT_LT(freq0[i], 0.1); } @@ -118,23 +133,22 @@ TEST(PCM, FastFourierTransformHighFrequency) std::array constexpr data{1.0, 0.0, 0.0, 1.0}; for (size_t i = 0; i < 1024; i++) { - pcm.AddStereo(data.data(), samples); + pcm.Add(data.data(), 2, samples); } - std::array freq0{}; - std::array freq1{}; - pcm.ResetAutoLevel(); - pcm.GetSpectrum(freq0.data(), CHANNEL_0, fftLength); - pcm.GetSpectrum(freq1.data(), CHANNEL_1, fftLength); + std::array freq0{}; + std::array freq1{}; + pcm.GetSpectrum(freq0.data(), 0, SpectrumSamples); + pcm.GetSpectrum(freq1.data(), 1, SpectrumSamples); // freq0 and freq1 should be equal - for (size_t i = 0; i < fftLength; i++) + for (size_t i = 0; i < SpectrumSamples; i++) { EXPECT_FLOAT_EQ(freq0[i], freq1[i]); } - for (size_t i = 0; i < fftLength - 1; i++) + for (size_t i = 0; i < SpectrumSamples - 1; i++) { EXPECT_FLOAT_EQ(freq0[i], 0); } - EXPECT_GT(freq0[fftLength - 1], 100000); + EXPECT_GT(freq0[SpectrumSamples - 1], 100000); }