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
0 commit comments