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  */