Skip to content

Commit 6493e84

Browse files
committed
CrossoverFilterTest, DelayLineTest
1 parent 2b196bf commit 6493e84

3 files changed

Lines changed: 347 additions & 2 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//
2+
// ██╗██████╗ ██╗ ██╗██████╗ ███████╗
3+
// ██║██╔══██╗ ██║ ██║██╔══██╗██╔════╝ ** JPLSpatial **
4+
// ██║██████╔╝ ██║ ██║██████╔╝███████╗
5+
// ██ ██║██╔═══╝ ██║ ██║██╔══██╗╚════██║ https://github.com/Jaytheway/JPLSpatial
6+
// ╚█████╔╝██║ ███████╗██║██████╔╝███████║
7+
// ╚════╝ ╚═╝ ╚══════╝╚═╝╚═════╝ ╚══════╝
8+
//
9+
// Copyright Jaroslav Pevno, JPLSpatial is offered under the terms of the ISC license:
10+
//
11+
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
12+
// without fee is hereby granted, provided that the above copyright notice and this permission
13+
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
14+
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15+
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
16+
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
17+
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18+
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19+
20+
#pragma once
21+
22+
#include "JPLSpatial/Core.h"
23+
#include "JPLSpatial/Math/Math.h"
24+
#include "JPLSpatial/Auralization/CrossoverFilter.h"
25+
26+
#include <gtest/gtest.h>
27+
28+
#include <vector>
29+
#include <cmath>
30+
#include <random>
31+
32+
namespace JPL
33+
{
34+
// Helpers
35+
static inline float rms(const std::vector<float>& x)
36+
{
37+
long double acc = 0.0;
38+
for (float v : x)
39+
acc += (long double)v * v;
40+
return std::sqrt((double)(acc / std::max<size_t>(1, x.size())));
41+
}
42+
static inline simd rms(const std::vector<simd>& x)
43+
{
44+
simd acc = 0.0;
45+
for (simd v : x)
46+
acc += v * v;
47+
return Math::Sqrt(acc / simd(float(std::max<size_t>(1, x.size()))));
48+
}
49+
static inline float MaxAbsDiff(const std::vector<float>& a, const std::vector<float>& b)
50+
{
51+
float m = 0.0f;
52+
const size_t n = std::min(a.size(), b.size());
53+
for (size_t i = 0; i < n; ++i)
54+
m = std::max(m, std::abs(a[i] - b[i]));
55+
return m;
56+
}
57+
static inline void MmakeImpulse(std::vector<float>& x)
58+
{
59+
std::fill(x.begin(), x.end(), 0.0f);
60+
if (!x.empty())
61+
x[0] = 1.0f;
62+
}
63+
static inline void MakeSine(std::vector<float>& x, float sampleRate, float f)
64+
{
65+
const float w = JPL_TWO_PI * f / sampleRate;
66+
float p = 0.0f;
67+
for (size_t i = 0; i < x.size(); ++i)
68+
{
69+
x[i] = std::sin(p);
70+
p += w;
71+
}
72+
}
73+
74+
// TODO: more/better tests
75+
76+
TEST(FourBandLR4, RecombinationImpulseRMSNoChange)
77+
{
78+
FourBandCrossover split;
79+
static constexpr float sampleRate = 48000.0f;
80+
split.Prepare(sampleRate);
81+
static constexpr int N = 4096;
82+
std::vector<float> in(N), out(N);
83+
MmakeImpulse(in);
84+
85+
// Unity gains, fused path
86+
split.ProcessBlock(in, simd(1.0f), out);
87+
88+
const float diff = std::abs(rms(in) - rms(out));
89+
EXPECT_LT(diff, 1e-6f);
90+
}
91+
92+
TEST(FourBandLR4, EnergyExtractionVsRecombine_Noise)
93+
{
94+
FourBandCrossover split;
95+
static constexpr float sampleRate = 48000.0f;
96+
split.Prepare(sampleRate);
97+
98+
static constexpr int N = 8192;
99+
std::vector<float> in(N), out(N);
100+
std::vector<simd> b(N);
101+
102+
// white noise
103+
std::mt19937 rng(12345);
104+
std::uniform_real_distribution<float> U(-1.0f, 1.0f);
105+
for (int i = 0; i < N; ++i)
106+
in[i] = U(rng);
107+
108+
// SoA pass
109+
split.ProcessBlock(in, b);
110+
111+
// Recombine from SoA with unity
112+
for (int i = 0; i < N; ++i)
113+
out[i] = b[i].reduce();
114+
115+
// Compare to fused unity recombination (reference)
116+
std::vector<float> outRef(N);
117+
split.Reset();
118+
split.ProcessBlock(in, simd(1.0f), outRef);
119+
120+
const float err = rms(out) - rms(outRef); // magnitudes should match closely
121+
EXPECT_NEAR(err, 0.0f, 1e-4f);
122+
123+
const float maxErr = MaxAbsDiff(out, outRef);
124+
EXPECT_LT(maxErr, 1e-4f);
125+
}
126+
} // namespace JPL
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//
2+
// ██╗██████╗ ██╗ ██╗██████╗ ███████╗
3+
// ██║██╔══██╗ ██║ ██║██╔══██╗██╔════╝ ** JPLSpatial **
4+
// ██║██████╔╝ ██║ ██║██████╔╝███████╗
5+
// ██ ██║██╔═══╝ ██║ ██║██╔══██╗╚════██║ https://github.com/Jaytheway/JPLSpatial
6+
// ╚█████╔╝██║ ███████╗██║██████╔╝███████║
7+
// ╚════╝ ╚═╝ ╚══════╝╚═╝╚═════╝ ╚══════╝
8+
//
9+
// Copyright Jaroslav Pevno, JPLSpatial is offered under the terms of the ISC license:
10+
//
11+
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
12+
// without fee is hereby granted, provided that the above copyright notice and this permission
13+
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
14+
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15+
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
16+
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
17+
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18+
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19+
20+
#pragma once
21+
22+
#include "JPLSpatial/Auralization/DelayLine.h"
23+
24+
#include <gtest/gtest.h>
25+
26+
#include <span>
27+
#include <array>
28+
#include <format>
29+
30+
namespace JPL
31+
{
32+
class DelayLineTest : public testing::Test
33+
{
34+
protected:
35+
DelayLineTest() = default;
36+
37+
inline float saw(uint32_t n) { return static_cast<float>(n); }
38+
39+
// MaxDelay < WindowSize would be undefined for an interpolated delay line,
40+
// so we start at WindowSize+1
41+
// small (Ring = 8)
42+
// medium (Ring = 16)
43+
// large (Ring = 512)
44+
static constexpr std::array<uint32_t, 3> DelayLengths{ 7, 14, 500 }; // test delays
45+
46+
template<class Function>
47+
static void ForEachDelay(Function&& func)
48+
{
49+
for (uint32_t D : DelayLengths)
50+
{
51+
SCOPED_TRACE(std::format("Testing delay: {}", D));
52+
func(D);
53+
}
54+
}
55+
};
56+
57+
// Test 0 : check that the internal buffer of delay line is filled correctly
58+
TEST_F(DelayLineTest, DelayLineBufferFill)
59+
{
60+
ForEachDelay([this](uint32_t maxDelaySamples)
61+
{
62+
DelayLine<> dl(maxDelaySamples);
63+
64+
for (uint32_t n = 0; n < maxDelaySamples; ++n)
65+
{
66+
const float sample = saw(n);
67+
dl.Push(sample);
68+
}
69+
70+
for (uint32_t n = 0; n < maxDelaySamples; ++n)
71+
{
72+
const float sample = saw(n);
73+
const float sampleD = dl.GetReadWindow<0>(maxDelaySamples - 1 - n);
74+
EXPECT_FLOAT_EQ(sampleD, sample);
75+
}
76+
});
77+
}
78+
79+
// Test 1 : basic push/read with zero delay
80+
TEST_F(DelayLineTest, ZeroDelayReturnsLatestSample)
81+
{
82+
ForEachDelay([this](uint32_t maxDelaySamples)
83+
{
84+
DelayLine<> dl(maxDelaySamples);
85+
// pre-fill at least WindowSize samples so the saw() data is present
86+
for (uint32_t n = 0; n < DelayLine<>::WindowSize; ++n)
87+
dl.Push(saw(n));
88+
89+
for (uint32_t n = DelayLine<>::WindowSize; n < 200; ++n)
90+
{
91+
const float sample = saw(n);
92+
dl.Push(sample);
93+
std::span<const float> p = dl.GetReadWindow<DelayLine<>::WindowSize>(0);
94+
EXPECT_FLOAT_EQ(p[0], sample);
95+
}
96+
});
97+
}
98+
99+
// Test 2 : fixed integer delay, no wrap
100+
TEST_F(DelayLineTest, FixedDelayNoWrap)
101+
{
102+
ForEachDelay([this](uint32_t maxDelaySamples)
103+
{
104+
DelayLine<> dl(maxDelaySamples);
105+
const uint32_t D = dl.GetSize() / 2; // ( < MaxDelay )
106+
107+
const uint32_t preFillSize = D + DelayLine<>::WindowSize;
108+
109+
// pre-fill
110+
uint32_t n = 0;
111+
for (n = 0; n < preFillSize; ++n)
112+
{
113+
const float sample = saw(n);
114+
dl.Push(sample);
115+
}
116+
117+
for (uint32_t nb = 0; nb < preFillSize; ++nb)
118+
{
119+
float sampleD = dl.GetReadWindow<0>(preFillSize - 1 - nb);
120+
EXPECT_FLOAT_EQ(sampleD, saw(nb));
121+
}
122+
123+
for (; n < dl.GetSize() - 1; ++n)
124+
{
125+
dl.Push(saw(n));
126+
std::span<const float> p = dl.GetReadWindow<DelayLine<>::WindowSize>(n - 1);
127+
EXPECT_FLOAT_EQ(p[0], saw(DelayLine<>::WindowSize - 1));
128+
// last sample must equal to first sample pushed
129+
EXPECT_FLOAT_EQ(p[DelayLine<>::WindowSize - 1], saw(0));
130+
}
131+
});
132+
}
133+
134+
// Test 3 : wrap-around behaviour (delay near MaxDelay)
135+
TEST_F(DelayLineTest, WrapStillContiguous)
136+
{
137+
ForEachDelay([this](uint32_t maxDelaySamples)
138+
{
139+
SCOPED_TRACE(std::format("Testing wrap-around for delay: {}", maxDelaySamples));
140+
141+
constexpr uint32_t K = DelayLine<>::WindowSize;
142+
ASSERT_TRUE(maxDelaySamples >= K)
143+
<< "maxDelaySamples must be at least WindowSize (" << K << ")";
144+
145+
DelayLine<> dl(maxDelaySamples);
146+
const uint32_t Ring = dl.GetSize(); // 2^k >= maxDelay
147+
const uint32_t Mask = Ring - 1;
148+
149+
// Choose three delays: just >=K, middle, and almost Ring
150+
const uint32_t Dmin = K;
151+
const uint32_t Dmid = Ring / 2;
152+
const uint32_t Dmax = Ring - 3;
153+
const std::array<uint32_t, 3> delays{ Dmin, Dmid, Dmax };
154+
155+
// Pre-fill so every delay has valid history
156+
const uint32_t prime = Dmax + 2 * K;
157+
for (uint32_t n = 0; n < prime; ++n)
158+
dl.Push(saw(n));
159+
160+
// ───── Wrap the write pointer a few times
161+
const uint32_t total = prime + 4 * Ring;
162+
for (uint32_t n = prime; n < total; ++n)
163+
{
164+
dl.Push(saw(n));
165+
const uint32_t wr = dl.GetWriteIndex();
166+
167+
for (uint32_t D : delays)
168+
{
169+
// Reference ring-start index "inside the ring zone" (0…Ring-1)
170+
// Note: if we're decrementing index on write, we need to +offset
171+
const uint32_t offset = D + K;
172+
const uint32_t start = (wr + Ring + offset + 1) & Mask;
173+
174+
std::span<const float> p = dl.GetReadWindow<DelayLine<>::WindowSize>(D + K);
175+
176+
// 1) Pointer must address inside the whole allocation
177+
uintptr_t pAddr = reinterpret_cast<uintptr_t>(p.data());
178+
uintptr_t bufBase = reinterpret_cast<uintptr_t>(dl.raw());
179+
uintptr_t bufEnd = bufBase + sizeof(float) * (Ring + K);
180+
EXPECT_GE(pAddr, bufBase);
181+
EXPECT_LT(pAddr + sizeof(float) * K, bufEnd);
182+
183+
// 2) Next K samples must equal the buffer’s own data
184+
for (uint32_t i = 0; i < K; ++i)
185+
EXPECT_FLOAT_EQ(p[i], dl.raw()[start + i])
186+
<< "n=" << n
187+
<< " D=" << D
188+
<< " i=" << i
189+
<< " Ring=" << Ring;
190+
}
191+
}
192+
});
193+
}
194+
195+
TEST_F(DelayLineTest, Tap_LinearInterpolation)
196+
{
197+
DelayLine<20> dl(14);
198+
static constexpr float frac = 0.25f; // quarter of a sample
199+
200+
auto li = dl.CreateTap<LinearInterpolator>(0.0f);
201+
li.SetDelay(frac);
202+
203+
// push impulse
204+
dl.Push(0.0f);
205+
// push one more frame so wr == 2
206+
dl.Push(1.0f);
207+
208+
// window now spans slots [0,1] => contains 1.0 & 0.0
209+
std::span<const float> p = dl.GetReadWindow<DelayLine<>::WindowSize>(0);
210+
EXPECT_FLOAT_EQ(p[0], 1.0f); // 1.0 (newest)
211+
EXPECT_FLOAT_EQ(p[1], 0.0f); // 0.0 (oldest)
212+
213+
// get value 0.25 samples back in time
214+
auto y = li.Process(dl);
215+
EXPECT_FLOAT_EQ(y, 0.75f);
216+
}
217+
218+
// TODO: tests for other kinds of interpolators
219+
220+
221+
} // namespace JPL

SpatializationTests/src/main.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@
3535
#if JPL_HAS_PATH_TRACING
3636
#include "Tests/SpecularRayTracingTest.h"
3737
//#include "Tests/BDPTTest.h"
38-
#include "Tests/DelayLineTest.h"
39-
#include "Tests/CrossoverFilterTest.h"
4038
#endif
4139

4240
#undef max

0 commit comments

Comments
 (0)