Skip to content

Commit 36fb09c

Browse files
committed
Implemented waveform alignment algorithm.
Also consolidated the waveform sample count constant to keep it aligned over all classes.
1 parent 69d2134 commit 36fb09c

File tree

10 files changed

+289
-193
lines changed

10 files changed

+289
-193
lines changed
+8-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
#pragma once
22

3+
#include <array>
4+
35
namespace libprojectM {
46
namespace Audio {
57

6-
static constexpr int WaveformSamples = 576; //!< Number of waveform data samples available for rendering a frame.
7-
static constexpr int SpectrumSamples = 512; //!< Number of spectrum analyzer samples.
8+
static constexpr int AudioBufferSamples = 576; //!< Number of waveform data samples stored in the buffer for analysis.
9+
static constexpr int WaveformSamples = 480; //!< Number of waveform data samples available for rendering a frame.
10+
static constexpr int SpectrumSamples = 512; //!< Number of spectrum analyzer samples.
11+
12+
using WaveformBuffer = std::array<float, AudioBufferSamples>; //!< Buffer with waveform data. Only the first WaveformSamples number of samples are valid.
13+
using SpectrumBuffer = std::array<float, SpectrumSamples>; //!< Buffer with spectrum data.
814

915
} // namespace Audio
1016
} // namespace libprojectM

src/libprojectM/Audio/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ add_library(Audio OBJECT
99
PCM.hpp
1010
Loudness.cpp
1111
Loudness.hpp
12+
WaveformAligner.cpp
13+
WaveformAligner.hpp
1214
)
1315

1416
target_include_directories(Audio

src/libprojectM/Audio/PCM.cpp

+14-10
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ void PCM::AddToBuffer(
2121

2222
for (size_t i = 0; i < sampleCount; i++)
2323
{
24-
size_t const bufferOffset = (m_start + i) % WaveformSamples;
24+
size_t const bufferOffset = (m_start + i) % AudioBufferSamples;
2525
m_inputBufferL[bufferOffset] = 128.0f * (static_cast<float>(samples[0 + i * channels]) - float(signalOffset)) / float(signalAmplitude);
2626
if (channels > 1)
2727
{
@@ -32,7 +32,7 @@ void PCM::AddToBuffer(
3232
m_inputBufferR[bufferOffset] = m_inputBufferL[bufferOffset];
3333
}
3434
}
35-
m_start = (m_start + sampleCount) % WaveformSamples;
35+
m_start = (m_start + sampleCount) % AudioBufferSamples;
3636
}
3737

3838
void PCM::Add(float const* const samples, uint32_t channels, size_t const count)
@@ -53,16 +53,19 @@ void PCM::UpdateFrameAudioData(double secondsSinceLastFrame, uint32_t frame)
5353
// 1. Copy audio data from input buffer
5454
CopyNewWaveformData();
5555

56-
// 2. Align waveforms
57-
58-
// 3. Update spectrum analyzer data for both channels
56+
// 2. Update spectrum analyzer data for both channels
5957
UpdateFftChannel(0);
6058
UpdateFftChannel(1);
6159

60+
// 3. Align waveforms
61+
m_alignL.Align(m_waveformL);
62+
m_alignR.Align(m_waveformR);
63+
6264
// 4. Update beat detection values
6365
m_bass.Update(m_spectrumL, secondsSinceLastFrame, frame);
6466
m_middles.Update(m_spectrumL, secondsSinceLastFrame, frame);
6567
m_treble.Update(m_spectrumL, secondsSinceLastFrame, frame);
68+
6669
}
6770

6871
auto PCM::GetFrameAudioData() const -> FrameAudioData
@@ -92,14 +95,14 @@ void PCM::UpdateFftChannel(size_t const channel)
9295
{
9396
assert(channel == 0 || channel == 1);
9497

95-
std::vector<float> waveformSamples(WaveformSamples);
98+
std::vector<float> waveformSamples(AudioBufferSamples);
9699
std::vector<float> spectrumValues;
97100

98101
auto const& from = channel == 0 ? m_waveformL : m_waveformR;
99102
auto& spectrum = channel == 0 ? m_spectrumL : m_spectrumR;
100103

101104
size_t oldI{0};
102-
for (size_t i = 0; i < WaveformSamples; i++)
105+
for (size_t i = 0; i < AudioBufferSamples; i++)
103106
{
104107
// Damp the input into the FFT a bit, to reduce high-frequency noise:
105108
waveformSamples[i] = 0.5f * (from[i] + from[oldI]);
@@ -114,11 +117,11 @@ void PCM::UpdateFftChannel(size_t const channel)
114117
void PCM::CopyNewWaveformData()
115118
{
116119
const auto& copyChannel =
117-
[](size_t start, const std::array<float, WaveformSamples>& inputSamples, std::array<float, WaveformSamples>& outputSamples)
120+
[](size_t start, const std::array<float, AudioBufferSamples>& inputSamples, std::array<float, AudioBufferSamples>& outputSamples)
118121
{
119-
for (size_t i = 0; i < WaveformSamples; i++)
122+
for (size_t i = 0; i < AudioBufferSamples; i++)
120123
{
121-
outputSamples[i] = inputSamples[(start + i) % WaveformSamples];
124+
outputSamples[i] = inputSamples[(start + i) % AudioBufferSamples];
122125
}
123126
};
124127

@@ -127,5 +130,6 @@ void PCM::CopyNewWaveformData()
127130
copyChannel(bufferStartIndex, m_inputBufferR, m_waveformR);
128131
}
129132

133+
130134
} // namespace Audio
131135
} // namespace libprojectM

src/libprojectM/Audio/PCM.hpp

+12-9
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88
#pragma once
99

1010
#include "AudioConstants.hpp"
11-
1211
#include "FrameAudioData.hpp"
1312
#include "Loudness.hpp"
1413
#include "MilkdropFFT.hpp"
14+
#include "WaveformAligner.hpp"
1515

1616
#include <projectM-4/projectM_export.h>
1717

18-
#include <array>
1918
#include <atomic>
2019
#include <cstdint>
2120
#include <cstdlib>
@@ -89,20 +88,24 @@ class PROJECTM_EXPORT PCM
8988
void CopyNewWaveformData();
9089

9190
// External input buffer
92-
std::array<float, WaveformSamples> m_inputBufferL{0.f}; //!< Circular buffer for left-channel PCM data.
93-
std::array<float, WaveformSamples> m_inputBufferR{0.f}; //!< Circular buffer for right-channel PCM data.
94-
std::atomic<size_t> m_start{0}; //!< Circular buffer start index.
91+
WaveformBuffer m_inputBufferL{0.f}; //!< Circular buffer for left-channel PCM data.
92+
WaveformBuffer m_inputBufferR{0.f}; //!< Circular buffer for right-channel PCM data.
93+
std::atomic<size_t> m_start{0}; //!< Circular buffer start index.
9594

9695
// Frame waveform data
97-
std::array<float, WaveformSamples> m_waveformL{0.f}; //!< Left-channel waveform data, aligned.
98-
std::array<float, WaveformSamples> m_waveformR{0.f}; //!< Right-channel waveform data, aligned.
96+
WaveformBuffer m_waveformL{0.f}; //!< Left-channel waveform data, aligned. Only the first WaveformSamples number of samples are valid.
97+
WaveformBuffer m_waveformR{0.f}; //!< Right-channel waveform data, aligned. Only the first WaveformSamples number of samples are valid.
9998

10099
// Frame spectrum data
101-
std::array<float, SpectrumSamples> m_spectrumL{0.f}; //!< Left-channel spectrum data.
102-
std::array<float, SpectrumSamples> m_spectrumR{0.f}; //!< Right-channel spectrum data.
100+
SpectrumBuffer m_spectrumL{0.f}; //!< Left-channel spectrum data.
101+
SpectrumBuffer m_spectrumR{0.f}; //!< Right-channel spectrum data.
103102

104103
MilkdropFFT m_fft{WaveformSamples, SpectrumSamples, true}; //!< Spectrum analyzer instance.
105104

105+
// Alignment data
106+
WaveformAligner m_alignL; //!< Left-channel waveform alignment.
107+
WaveformAligner m_alignR; //!< Left-channel waveform alignment.
108+
106109
// Frame beat detection values
107110
Loudness m_bass{Loudness::Band::Bass}; //!< Beat detection/volume for the "bass" band.
108111
Loudness m_middles{Loudness::Band::Middles}; //!< Beat detection/volume for the "middles" band.
+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#include "WaveformAligner.hpp"
2+
3+
#include <cmath>
4+
#include <iterator>
5+
6+
namespace libprojectM {
7+
namespace Audio {
8+
9+
WaveformAligner::WaveformAligner()
10+
{
11+
static const size_t maxOctaves{10};
12+
static const size_t numOctaves{static_cast<size_t>(std::floor(std::log(AudioBufferSamples - WaveformSamples) / std::log(2.0f)))};
13+
m_octaves = numOctaves > maxOctaves ? maxOctaves : numOctaves;
14+
15+
m_aligmentWeights.resize(m_octaves);
16+
m_firstNonzeroWeights.resize(m_octaves);
17+
m_lastNonzeroWeights.resize(m_octaves);
18+
m_octaveSamples.resize(m_octaves);
19+
m_octaveSampleSpacing.resize(m_octaves);
20+
m_oldWaveformMips.resize(m_octaves);
21+
22+
m_octaveSamples[0] = AudioBufferSamples;
23+
m_octaveSampleSpacing[0] = AudioBufferSamples - WaveformSamples;
24+
for (size_t octave = 1; octave < m_octaves; octave++)
25+
{
26+
m_octaveSamples[octave] = m_octaveSamples[octave - 1] / 2;
27+
m_octaveSampleSpacing[octave] = m_octaveSampleSpacing[octave - 1] / 2;
28+
}
29+
}
30+
31+
void WaveformAligner::Align(WaveformBuffer& newWaveform)
32+
{
33+
if (m_octaves < 4)
34+
{
35+
return;
36+
}
37+
38+
int alignOffset{};
39+
40+
std::vector<WaveformBuffer> newWaveformMips(m_octaves, WaveformBuffer());
41+
std::copy(newWaveform.begin(), newWaveform.end(), newWaveformMips[0].begin());
42+
43+
// Calculate mip levels
44+
for (size_t octave = 1; octave < m_octaves; octave++)
45+
{
46+
for (size_t sample = 0; sample < m_octaveSamples[octave]; sample++)
47+
{
48+
newWaveformMips[octave][sample] = 0.5f * (newWaveformMips[octave - 1][sample * 2] + newWaveformMips[octave - 1][sample * 2 + 1]);
49+
}
50+
}
51+
52+
if (!m_alignWaveReady)
53+
{
54+
m_alignWaveReady = true;
55+
for (size_t octave = 0; octave < m_octaves; octave++)
56+
{
57+
// For example:
58+
// m_octaveSampleSpacing[octave] == 4
59+
// m_octaveSamples[octave] == 36
60+
// (so we test 32 samples, w/4 offsets)
61+
size_t const compareSamples = m_octaveSamples[octave] - m_octaveSampleSpacing[octave];
62+
63+
for (size_t sample = 0; sample < compareSamples; sample++)
64+
{
65+
auto& tempVal = m_aligmentWeights[octave][sample];
66+
67+
// Start with pyramid-shaped PDF, from 0..1..0
68+
if (sample < compareSamples / 2)
69+
{
70+
tempVal = static_cast<float>(sample * 2) / static_cast<float>(compareSamples);
71+
}
72+
else
73+
{
74+
tempVal = static_cast<float>((compareSamples - 1 - sample) * 2) / static_cast<float>(compareSamples);
75+
}
76+
77+
// TWEAK how much the center matters, vs. the edges:
78+
tempVal = (tempVal - 0.8f) * 5.0f + 0.8f;
79+
80+
// Clip
81+
if (tempVal > 1.0f)
82+
{
83+
tempVal = 1.0f;
84+
}
85+
if (tempVal < 0.0f)
86+
{
87+
tempVal = 0.0f;
88+
}
89+
}
90+
91+
size_t sample{};
92+
while (m_aligmentWeights[octave][sample] == 0 && sample < compareSamples)
93+
{
94+
sample++;
95+
}
96+
m_firstNonzeroWeights[octave] = sample;
97+
98+
sample = compareSamples - 1;
99+
while (m_aligmentWeights[octave][sample] == 0 && sample >= 0)
100+
{
101+
sample--;
102+
}
103+
m_lastNonzeroWeights[octave] = sample;
104+
}
105+
}
106+
107+
int sample1{};
108+
int sample2{static_cast<int>(m_octaveSampleSpacing[m_octaves - 1])};
109+
110+
// Find best match for alignment
111+
for (int octave = static_cast<int>(m_octaves) - 1; octave >= 0; octave--)
112+
{
113+
int lowestErrorOffset{-1};
114+
float lowestErrorAmount{};
115+
116+
for (int sample = sample1; sample < sample2; sample++)
117+
{
118+
float errorSum{};
119+
120+
for (size_t i = m_firstNonzeroWeights[octave]; i <= m_lastNonzeroWeights[octave]; i++)
121+
{
122+
errorSum += std::abs((newWaveformMips[octave][i + sample] - m_oldWaveformMips[octave][i + sample]) * m_aligmentWeights[octave][i]);
123+
}
124+
125+
if (lowestErrorOffset == -1 || errorSum < lowestErrorAmount)
126+
{
127+
lowestErrorOffset = static_cast<int>(sample);
128+
lowestErrorAmount = errorSum;
129+
}
130+
}
131+
132+
// Now use 'lowestErrorOffset' to guide bounds of search in next octave:
133+
// m_octaveSampleSpacing[octave] == 8
134+
// m_octaveSamples[octave] == 72
135+
// -say 'lowestErrorOffset' was 2
136+
// -that corresponds to samples 4 & 5 of the next octave
137+
// -also, expand about this by 2 samples? YES.
138+
// (so we'd test 64 samples, w/8->4 offsets)
139+
if (octave > 0)
140+
{
141+
sample1 = lowestErrorOffset * 2 - 1;
142+
sample2 = lowestErrorOffset * 2 + 2 + 1;
143+
if (sample1 < 0)
144+
{
145+
sample1 = 0;
146+
}
147+
if (sample2 > static_cast<int>(m_octaveSampleSpacing[octave - 1]))
148+
{
149+
sample2 = static_cast<int>(m_octaveSampleSpacing[octave - 1]);
150+
}
151+
}
152+
else
153+
{
154+
alignOffset = lowestErrorOffset;
155+
}
156+
}
157+
158+
// Store mip levels for the next frame.
159+
m_oldWaveformMips.clear();
160+
std::copy(newWaveformMips.begin(), newWaveformMips.end(), std::back_inserter(m_oldWaveformMips));
161+
162+
// Finally, apply the results by scooting the aligned samples so that they start at index 0.
163+
if (alignOffset > 0)
164+
{
165+
for (size_t sample = 0; sample < WaveformSamples; sample++)
166+
{
167+
newWaveform[sample] = newWaveform[sample + alignOffset];
168+
}
169+
170+
// Set remaining samples to zero.
171+
std::fill_n(newWaveform.begin() + WaveformSamples, AudioBufferSamples - WaveformSamples, 0.0f);
172+
}
173+
}
174+
175+
176+
} // namespace Audio
177+
} // namespace libprojectM
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @file WaveformAligner.hpp
3+
* @brief Mip-based waveform alignment algorithm
4+
*
5+
* Calculates the absolute error between the previous and current waveforms over several octaves
6+
* and sample offsets, then shifts the new waveform forward to best align with the previous frame.
7+
* This will keep similar features in-place instead of randomly jumping around on each frame and creates
8+
* for a smoother-looking waveform visualization.
9+
*/
10+
11+
#pragma once
12+
13+
#include "AudioConstants.hpp"
14+
15+
#include <cstddef>
16+
#include <vector>
17+
18+
namespace libprojectM {
19+
namespace Audio {
20+
21+
/**
22+
* @brief Mip-based waveform alignment algorithm
23+
*
24+
* Calculates the absolute error between the previous and current waveforms over several octaves
25+
* and sample offsets, then shifts the new waveform forward to best align with the previous frame.
26+
* This will keep similar features in-place instead of randomly jumping around on each frame and creates
27+
* for a smoother-looking waveform visualization.
28+
*/
29+
class WaveformAligner
30+
{
31+
public:
32+
WaveformAligner();
33+
34+
/**
35+
* @brief Aligns waveforms to a best-fit match to the previous frame.
36+
* @param[in,out] newWaveform The new waveform to be aligned.
37+
*/
38+
void Align(WaveformBuffer& newWaveform);
39+
40+
private:
41+
bool m_alignWaveReady{false}; //!< Alignment needs special treatment for the first buffer fill.
42+
43+
std::vector<std::array<float, AudioBufferSamples>> m_aligmentWeights; //!< Sample weights per octave.
44+
45+
size_t m_octaves{}; //!< Number of mip-levels/octaves.
46+
std::vector<size_t> m_octaveSamples; //!< Samples per octave.
47+
std::vector<size_t> m_octaveSampleSpacing; //!< Space between samples per octave.
48+
49+
std::vector<WaveformBuffer> m_oldWaveformMips; //!< Mip levels of the previous frame's waveform.
50+
std::vector<size_t> m_firstNonzeroWeights; //!< First non-zero weight sample index for each octave.
51+
std::vector<size_t> m_lastNonzeroWeights; //!< Last non-zero weight sample index for each octave.
52+
};
53+
54+
} // namespace Audio
55+
} // namespace libprojectM

src/libprojectM/MilkdropPreset/Constants.hpp

-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,4 @@ static constexpr int TVarCount = 8; //!< Number of T variables available.
1212
static constexpr int CustomWaveformCount = 4; //!< Number of custom waveforms (expression-driven) which can be used in a preset.
1313
static constexpr int CustomShapeCount = 4; //!< Number of custom shapes (expression-driven) which can be used in a preset.
1414

15-
static constexpr int RenderWaveformSamples = 480; //!< Number of custom waveform data samples available for rendering a frame.
1615
static constexpr int WaveformMaxPoints = 512; //!< Maximum number of waveform points.

0 commit comments

Comments
 (0)