1 #include <iostream>
2 #include <iomanip>
3 #include <sstream>
4 #include <algorithm>
5
6 #include "HeadlessUtils.h"
7 #include "catch2/catch2.hpp"
8 #include "FastMath.h"
9 #include "MSEGModulationHelper.h"
10
11 struct msegObservation
12 {
msegObservationmsegObservation13 msegObservation(int ip, float fp, float va)
14 {
15 iPhase = ip;
16 fPhase = fp;
17 phase = ip + fp;
18 v = va;
19 }
20 int iPhase;
21 float fPhase;
22 float v;
23 float phase;
24 };
25
runMSEG(MSEGStorage * ms,float dPhase,float phaseMax,float deform=0,float releaseAfter=-1)26 std::vector<msegObservation> runMSEG(MSEGStorage *ms, float dPhase, float phaseMax,
27 float deform = 0, float releaseAfter = -1)
28 {
29 auto res = std::vector<msegObservation>();
30 double phase = 0.0;
31 int iphase = 0;
32 Surge::MSEG::EvaluatorState es;
33
34 while (phase + iphase < phaseMax)
35 {
36 bool release = false;
37 if (releaseAfter >= 0 && phase + iphase >= releaseAfter)
38 release = true;
39 es.released = release;
40
41 auto r = Surge::MSEG::valueAt(iphase, phase, deform, ms, &es, false);
42 res.push_back(msegObservation(iphase, phase, r));
43 phase += dPhase;
44 if (phase > 1)
45 {
46 phase -= 1;
47 iphase += 1;
48 }
49 }
50 return res;
51 }
52
resetCP(MSEGStorage * ms)53 void resetCP(MSEGStorage *ms)
54 {
55 Surge::MSEG::rebuildCache(ms);
56 for (int i = 0; i < ms->n_activeSegments; ++i)
57 {
58 Surge::MSEG::resetControlPoint(ms, i);
59 }
60 }
61
62 /*
63 * These tests test the relationship between configuration of MSEG Storage and the phase evaluator
64 */
65 TEST_CASE("Basic MSEG Evaluation", "[mseg]")
66 {
67 SECTION("Simple Square")
68 {
69 MSEGStorage ms;
70 ms.n_activeSegments = 4;
71 ms.endpointMode = MSEGStorage::EndpointMode::LOCKED;
72 ms.segments[0].duration = 0.5 - MSEGStorage::minimumDuration;
73 ms.segments[0].type = MSEGStorage::segment::LINEAR;
74 ms.segments[0].v0 = 1.0;
75
76 ms.segments[1].duration = MSEGStorage::minimumDuration;
77 ms.segments[1].type = MSEGStorage::segment::LINEAR;
78 ms.segments[1].v0 = 1.0;
79
80 ms.segments[2].duration = 0.5 - MSEGStorage::minimumDuration;
81 ms.segments[2].type = MSEGStorage::segment::LINEAR;
82 ms.segments[2].v0 = -1.0;
83
84 ms.segments[3].duration = MSEGStorage::minimumDuration;
85 ms.segments[3].type = MSEGStorage::segment::LINEAR;
86 ms.segments[3].v0 = -1.0;
87
88 resetCP(&ms);
89 Surge::MSEG::rebuildCache(&ms);
90
91 // OK so lets go ahead and run it at a variety of forward phases
92 auto runIt = runMSEG(&ms, 0.0321, 5);
93 for (auto c : runIt)
94 {
95 if (c.fPhase < 0.5 - MSEGStorage::minimumDuration)
96 REQUIRE(c.v == 1);
97 if (c.fPhase > 0.5 && c.fPhase < 1 - MSEGStorage::minimumDuration)
98 REQUIRE(c.v == -1);
99 }
100 }
101
102 SECTION("Longer Square")
103 {
104 MSEGStorage ms;
105 ms.n_activeSegments = 4;
106 ms.endpointMode = MSEGStorage::EndpointMode::LOCKED;
107 ms.segments[0].duration = 0.5 - MSEGStorage::minimumDuration;
108 ms.segments[0].type = MSEGStorage::segment::LINEAR;
109 ms.segments[0].v0 = 1.0;
110
111 ms.segments[1].duration = MSEGStorage::minimumDuration;
112 ms.segments[1].type = MSEGStorage::segment::LINEAR;
113 ms.segments[1].v0 = 1.0;
114
115 ms.segments[2].duration = 1.0 - MSEGStorage::minimumDuration;
116 ms.segments[2].type = MSEGStorage::segment::LINEAR;
117 ms.segments[2].v0 = -1.0;
118
119 ms.segments[3].duration = MSEGStorage::minimumDuration;
120 ms.segments[3].type = MSEGStorage::segment::LINEAR;
121 ms.segments[3].v0 = -1.0;
122
123 resetCP(&ms);
124 Surge::MSEG::rebuildCache(&ms);
125
126 // OK so lets go ahead and run it at a variety of forward phases
127 auto runIt = runMSEG(&ms, 0.0321, 14);
128 for (auto c : runIt)
129 {
130 auto dbphase = (int)(2 * c.phase) % 3;
131 INFO("phase is " << c.phase << " " << dbphase)
132 if (dbphase == 0)
133 REQUIRE(c.v == 1);
134 if (dbphase == 1 || dbphase == 2)
135 REQUIRE(c.v == -1);
136 }
137 }
138 }
139
140 TEST_CASE("Unlocked Endpoitns", "[mseg]")
141 {
142 SECTION("Simple Square Default Locks")
143 {
144 MSEGStorage ms;
145 ms.n_activeSegments = 3;
146 ms.endpointMode = MSEGStorage::EndpointMode::LOCKED;
147 ms.segments[0].duration = 0.5 - MSEGStorage::minimumDuration;
148 ms.segments[0].type = MSEGStorage::segment::LINEAR;
149 ms.segments[0].v0 = 1.0;
150
151 ms.segments[1].duration = MSEGStorage::minimumDuration;
152 ms.segments[1].type = MSEGStorage::segment::LINEAR;
153 ms.segments[1].v0 = 1.0;
154
155 ms.segments[2].duration = 0.5;
156 ms.segments[2].type = MSEGStorage::segment::LINEAR;
157 ms.segments[2].v0 = -1.0;
158
159 resetCP(&ms);
160 Surge::MSEG::rebuildCache(&ms);
161
162 // OK so lets go ahead and run it at a variety of forward phases
163 auto runIt = runMSEG(&ms, 0.0321, 5);
164 for (auto c : runIt)
165 {
166 if (c.fPhase < 0.5 - MSEGStorage::minimumDuration)
167 REQUIRE(c.v == 1);
168 if (c.fPhase > 0.5 && c.fPhase < 1 - MSEGStorage::minimumDuration)
169 REQUIRE(c.v >= -1);
170 }
171 }
172
173 SECTION("Free Endpoint is a Square")
174 {
175 MSEGStorage ms;
176 ms.n_activeSegments = 3;
177 ms.endpointMode = MSEGStorage::EndpointMode::FREE;
178 ms.segments[0].duration = 0.5 - MSEGStorage::minimumDuration;
179 ms.segments[0].type = MSEGStorage::segment::LINEAR;
180 ms.segments[0].v0 = 1.0;
181
182 ms.segments[1].duration = MSEGStorage::minimumDuration;
183 ms.segments[1].type = MSEGStorage::segment::LINEAR;
184 ms.segments[1].v0 = 1.0;
185
186 ms.segments[2].duration = 0.5;
187 ms.segments[2].type = MSEGStorage::segment::LINEAR;
188 ms.segments[2].v0 = -1.0;
189 ms.segments[2].nv1 = -1.0; // The free mode will preserve this
190
191 resetCP(&ms);
192 Surge::MSEG::rebuildCache(&ms);
193
194 // OK so lets go ahead and run it at a variety of forward phases
195 auto runIt = runMSEG(&ms, 0.0321, 5);
196 for (auto c : runIt)
197 {
198 if (c.fPhase < 0.5 - MSEGStorage::minimumDuration)
199 REQUIRE(c.v == 1);
200 if (c.fPhase > 0.5 && c.fPhase < 1 - MSEGStorage::minimumDuration)
201 REQUIRE(c.v == -1);
202 }
203 }
204 }
205
206 TEST_CASE("Deform per Segment", "[mseg]")
207 {
208 SECTION("Triangle with Deform")
209 {
210 MSEGStorage ms;
211 ms.n_activeSegments = 2;
212 ms.endpointMode = MSEGStorage::EndpointMode::LOCKED;
213 ms.segments[0].duration = 0.5;
214 ms.segments[0].type = MSEGStorage::segment::LINEAR;
215 ms.segments[0].v0 = -1.0;
216
217 ms.segments[1].duration = 0.5;
218 ms.segments[1].type = MSEGStorage::segment::LINEAR;
219 ms.segments[1].v0 = 1.0;
220
221 resetCP(&ms);
222 Surge::MSEG::rebuildCache(&ms);
223
224 // OK so lets go ahead and run it at a variety of forward phases
225 auto runIt = runMSEG(&ms, 0.0321, 3);
226 for (auto c : runIt)
227 {
228 INFO("At " << c.fPhase << " " << c.phase << " " << c.v);
229 if (c.fPhase < 0.5)
230 REQUIRE(c.v == Approx(2 * c.fPhase / 0.5 - 1));
231 if (c.fPhase > 0.5)
232 REQUIRE(c.v == Approx(1 - 2 * (c.fPhase - 0.5) / 0.5));
233 }
234
235 auto runDef = runMSEG(&ms, 0.0321, 3, 0.9);
236 for (auto c : runDef)
237 {
238 if (c.fPhase < 0.5 && c.v != -1)
239 REQUIRE(c.v < 2 * c.fPhase / 0.5 - 1);
240 if (c.fPhase > 0.5 && c.v != 1)
241 REQUIRE(c.v > 1 - 2 * (c.fPhase - 0.5) / 0.5);
242 }
243
244 ms.segments[0].useDeform = false;
245 auto runDefPartial = runMSEG(&ms, 0.0321, 3, 0.9);
246 for (auto c : runDefPartial)
247 {
248 if (c.fPhase < 0.5 && c.v != -1)
249 REQUIRE(c.v == Approx(2 * c.fPhase / 0.5 - 1));
250 if (c.fPhase > 0.5 && c.v != 1)
251 REQUIRE(c.v > 1 - 2 * (c.fPhase - 0.5) / 0.5);
252 }
253 }
254 }
255
256 TEST_CASE("OneShot vs Loop", "[mseg]")
257 {
258 SECTION("Triangle Loop")
259 {
260 MSEGStorage ms;
261 ms.n_activeSegments = 2;
262 ms.endpointMode = MSEGStorage::EndpointMode::LOCKED;
263 ms.segments[0].duration = 0.5;
264 ms.segments[0].type = MSEGStorage::segment::LINEAR;
265 ms.segments[0].v0 = -1.0;
266
267 ms.segments[1].duration = 0.5;
268 ms.segments[1].type = MSEGStorage::segment::LINEAR;
269 ms.segments[1].v0 = 1.0;
270
271 resetCP(&ms);
272 Surge::MSEG::rebuildCache(&ms);
273
274 // OK so lets go ahead and run it at a variety of forward phases
275 auto runIt = runMSEG(&ms, 0.0321, 3);
276 for (auto c : runIt)
277 {
278 if (c.fPhase < 0.5)
279 REQUIRE(c.v == Approx(2 * c.fPhase / 0.5 - 1));
280 if (c.fPhase > 0.5)
281 REQUIRE(c.v == Approx(1 - 2 * (c.fPhase - 0.5) / 0.5));
282 }
283
284 ms.loopMode = MSEGStorage::LoopMode::ONESHOT;
285 auto runOne = runMSEG(&ms, 0.0321, 3);
286 for (auto c : runOne)
287 {
288 if (c.phase < 1)
289 {
290 INFO("At " << c.fPhase << " Value is " << c.v << " " << c.phase)
291 if (c.fPhase < 0.5)
292 REQUIRE(c.v == Approx(2 * c.fPhase / 0.5 - 1));
293 if (c.fPhase > 0.5)
294 REQUIRE(c.v == Approx(1 - 2 * (c.fPhase - 0.5) / 0.5));
295 }
296 else
297 {
298 REQUIRE(c.v == -1);
299 }
300 }
301 }
302 }
303
304 TEST_CASE("Loop Test Cases", "[mseg]")
305 {
306 SECTION("Non-Gated Loop over a Ramp Pair")
307 {
308 // This is an attack up from -1 over 0.5, then a ramp in 0,1and we loop over the end
309 MSEGStorage ms;
310 ms.n_activeSegments = 5;
311 ms.endpointMode = MSEGStorage::EndpointMode::FREE;
312 ms.segments[0].duration = 0.5;
313 ms.segments[0].type = MSEGStorage::segment::LINEAR;
314 ms.segments[0].v0 = -1.0;
315
316 ms.segments[1].duration = 0.25;
317 ms.segments[1].type = MSEGStorage::segment::LINEAR;
318 ms.segments[1].v0 = 1.0;
319
320 ms.segments[2].duration = MSEGStorage::minimumDuration;
321 ms.segments[2].type = MSEGStorage::segment::LINEAR;
322 ms.segments[2].v0 = 0.0;
323
324 ms.segments[3].duration = 0.25;
325 ms.segments[3].type = MSEGStorage::segment::LINEAR;
326 ms.segments[3].v0 = 1.0;
327
328 ms.segments[4].duration = MSEGStorage::minimumDuration;
329 ms.segments[4].type = MSEGStorage::segment::LINEAR;
330 ms.segments[4].v0 = 0.0;
331 ms.segments[4].nv1 = 1.0;
332
333 ms.loop_start = 1;
334 ms.loop_end = 4;
335
336 resetCP(&ms);
337 Surge::MSEG::rebuildCache(&ms);
338
339 auto runOne = runMSEG(&ms, 0.0321, 3);
340 for (auto c : runOne)
341 {
342 // make this more robust obviously but for now this will fail
343 if (c.phase > 0.5)
344 {
345 float rampPhase = (c.phase - 0.5) * 4;
346 rampPhase = rampPhase - (int)rampPhase;
347 float rampValue = 1.0 - rampPhase;
348 INFO("At " << c.phase << " " << rampPhase << " comparing with ramp");
349 REQUIRE(c.v == Approx(rampValue));
350 }
351 }
352 }
353
354 SECTION("Gated Loop over a Ramp")
355 {
356 // This is an attack up from -1 over 0.5, then a ramp in 0,1and we loop over the end
357 MSEGStorage ms;
358 ms.n_activeSegments = 5;
359 ms.loopMode = MSEGStorage::LoopMode::GATED_LOOP;
360
361 ms.endpointMode = MSEGStorage::EndpointMode::FREE;
362 ms.segments[0].duration = 0.5;
363 ms.segments[0].type = MSEGStorage::segment::LINEAR;
364 ms.segments[0].v0 = -1.0;
365
366 ms.segments[1].duration = 0.25;
367 ms.segments[1].type = MSEGStorage::segment::LINEAR;
368 ms.segments[1].v0 = 1.0;
369
370 ms.segments[2].duration = MSEGStorage::minimumDuration;
371 ms.segments[2].type = MSEGStorage::segment::LINEAR;
372 ms.segments[2].v0 = 0.0;
373
374 ms.segments[3].duration = 0.25;
375 ms.segments[3].type = MSEGStorage::segment::LINEAR;
376 ms.segments[3].v0 = 1.0;
377
378 ms.segments[4].duration = 0.25;
379 ms.segments[4].type = MSEGStorage::segment::LINEAR;
380 ms.segments[4].v0 = -1.0;
381 ms.segments[4].nv1 = 0.0;
382
383 ms.loop_start = 1;
384 ms.loop_end = 2;
385
386 resetCP(&ms);
387 Surge::MSEG::rebuildCache(&ms);
388
389 auto runOne =
390 runMSEG(&ms, 0.025, 3, 0,
391 1); /* pick a round dPhase so we hit releaseAfter exactly in this test */
392 for (auto c : runOne)
393 {
394 // make this more robust obviously but for now this will fail
395 if (c.phase > 0.5 && c.phase < 1.0)
396 {
397 float rampPhase = (c.phase - 0.5) * 4;
398 rampPhase = rampPhase - (int)rampPhase;
399 if (c.phase == 0.75)
400 rampPhase = 1; // Special case which side we pick
401 float rampValue = 1.0 - rampPhase;
402 INFO("At " << c.phase << " " << rampPhase << " comparing with ramp");
403 REQUIRE(c.v == Approx(rampValue));
404 }
405 if (c.phase > 1.0)
406 {
407 if (c.phase > 1.50)
408 {
409 // Past the end
410 REQUIRE(c.v == 0);
411 }
412 else if (c.phase > 1.25)
413 {
414 // release segment 2 (segment 4) from -1 to 0 in 0.25
415 auto rampPhase = (c.phase - 1.25) * 4;
416 auto rampValue = -1.0 + rampPhase;
417 INFO("At " << c.phase << " and rampPhase " << rampPhase);
418 REQUIRE(c.v == Approx(rampValue).margin(1e-4));
419 }
420 else
421 {
422 // release segment 1 but the ramp segment will be started at 0
423 // so we should be a ramp from 0 to -1, not from 1 to -1
424 // This is a bit fragile though since we run every .025 the release
425 // will not be when we've exactly hit 0 but rather when we are at
426 // 0.09999 still
427 auto rampPhase = (c.phase - 1.0) * 4;
428 auto rampValue = -rampPhase * 1.0999999 + 0.0999999;
429 INFO("In first envelope At " << c.phase << " and rampPhase " << rampPhase);
430 REQUIRE(c.v == Approx(rampValue).margin(1e-4));
431 }
432 }
433 }
434 }
435 }
436
437 TEST_CASE("Zero size loops", "[mseg]")
438 {
439 SECTION("Zero Size Non Gated")
440 {
441 MSEGStorage ms;
442 ms.n_activeSegments = 3;
443 ms.loopMode = MSEGStorage::LoopMode::LOOP;
444 ms.endpointMode = MSEGStorage::EndpointMode::FREE;
445
446 // A simple ADSR where the S is made by a size 0 loop after the decay
447 ms.segments[0].duration = 0.25;
448 ms.segments[0].type = MSEGStorage::segment::LINEAR;
449 ms.segments[0].v0 = 0;
450
451 ms.segments[1].duration = 0.25;
452 ms.segments[1].type = MSEGStorage::segment::LINEAR;
453 ms.segments[1].v0 = 1.0;
454
455 ms.segments[2].duration = 0.25;
456 ms.segments[2].type = MSEGStorage::segment::LINEAR;
457 ms.segments[2].v0 = 0.75;
458 ms.segments[2].nv1 = 0.0;
459
460 ms.loop_start = 2;
461 ms.loop_end = 1;
462
463 resetCP(&ms);
464 Surge::MSEG::rebuildCache(&ms);
465
466 // OK so this is loop so it should just push the 0.75 out the back after 0.5
467 auto runOne =
468 runMSEG(&ms, 0.025, 2, 0,
469 1); /* pick a round dPhase so we hit releaseAfter exactly in this test */
470 for (auto c : runOne)
471 {
472 if (c.phase > 0.5)
473 REQUIRE(c.v == 0.75);
474 }
475 }
476
477 SECTION("Zero Size Gated")
478 {
479 MSEGStorage ms;
480 ms.n_activeSegments = 3;
481 ms.loopMode = MSEGStorage::LoopMode::GATED_LOOP;
482 ms.endpointMode = MSEGStorage::EndpointMode::FREE;
483
484 // A simple ADSR where the S is made by a size 0 loop after the decay
485 ms.segments[0].duration = 0.25;
486 ms.segments[0].type = MSEGStorage::segment::LINEAR;
487 ms.segments[0].v0 = 0;
488
489 ms.segments[1].duration = 0.25;
490 ms.segments[1].type = MSEGStorage::segment::LINEAR;
491 ms.segments[1].v0 = 1.0;
492
493 ms.segments[2].duration = 0.25;
494 ms.segments[2].type = MSEGStorage::segment::LINEAR;
495 ms.segments[2].v0 = 0.75;
496 ms.segments[2].nv1 = 0.0;
497
498 ms.loop_start = 2;
499 ms.loop_end = 1;
500
501 resetCP(&ms);
502 Surge::MSEG::rebuildCache(&ms);
503
504 // OK so this is loop so it should just push the 0.75 out the back after 0.5
505 auto runOne =
506 runMSEG(&ms, 0.025, 2, 0,
507 1); /* pick a round dPhase so we hit releaseAfter exactly in this test */
508 for (auto c : runOne)
509 {
510 if (c.phase > 0.5 && c.phase <= 1.0)
511 REQUIRE(c.v == 0.75);
512 if (c.phase > 1.0 && c.phase <= 1.25)
513 {
514 auto rp = (c.phase - 1.0) * 4;
515 REQUIRE(c.v == Approx(0.75 * (1.0 - rp)).margin(1e-4));
516 }
517 if (c.phase > 1.25)
518 REQUIRE(c.v == 0);
519 }
520 }
521 }
522
523 /*
524 * Tests to add
525 * - loop point 0 (start = end + 1)
526 * - loop start at 0
527 * - maniulation routines
528 * - insert
529 * - delete
530 * - split
531 * - unsplit
532 * - each of those with loop preservation also
533 */