1 #include <iostream>
2 #include <iomanip>
3 #include <sstream>
4 #include <algorithm>
5 
6 #include "HeadlessUtils.h"
7 #include "Player.h"
8 
9 #include "catch2/catch2.hpp"
10 
11 #include "UnitTestUtilities.h"
12 #include "effect/ModControl.h"
13 
14 using namespace Surge::Test;
15 
16 TEST_CASE("ADSR Envelope Behaviour", "[mod]")
17 {
18 
19     std::shared_ptr<SurgeSynthesizer> surge(Surge::Headless::createSurge(44100));
20     REQUIRE(surge.get());
21 
22     /*
23     ** OK so lets set up a pretty simple setup
24     */
25 
26     auto runAdsr = [surge](float a, float d, float s, float r, int a_s, int d_s, int r_s,
27                            bool isAnalog, float releaseAfter, float runUntil, float pushSusAt = -1,
__anona43a01a10102(float a, float d, float s, float r, int a_s, int d_s, int r_s, bool isAnalog, float releaseAfter, float runUntil, float pushSusAt = -1, float pushSusTo = 0) 28                            float pushSusTo = 0) {
29         auto *adsrstorage = &(surge->storage.getPatch().scene[0].adsr[0]);
30         std::shared_ptr<AdsrEnvelope> adsr(new AdsrEnvelope());
31         adsr->init(&(surge->storage), adsrstorage, surge->storage.getPatch().scenedata[0], nullptr);
32         REQUIRE(adsr.get());
33 
34         int ids, ide;
35         setupStorageRanges(&(adsrstorage->a), &(adsrstorage->mode), ids, ide);
36         REQUIRE(ide > ids);
37         REQUIRE(ide >= 0);
38         REQUIRE(ids >= 0);
39 
40         auto svn = [](Parameter *p, float vn) {
41             p->set_value_f01(p->value_to_normalized(limit_range(vn, p->val_min.f, p->val_max.f)));
42         };
43 
44         auto svni = [](Parameter *p, int vn) { p->val.i = vn; };
45 
46         auto inverseEnvtime = [](float desiredTime) {
47             // 2^x = desired time
48             auto x = log(desiredTime) / log(2.0);
49             return x;
50         };
51 
52         svn(&(adsrstorage->a), inverseEnvtime(a));
53         svn(&(adsrstorage->d), inverseEnvtime(d));
54         svn(&(adsrstorage->s), s);
55         svn(&(adsrstorage->r), inverseEnvtime(r));
56 
57         svni(&(adsrstorage->a_s), a_s);
58         svni(&(adsrstorage->d_s), d_s);
59         svni(&(adsrstorage->r_s), r_s);
60 
61         adsrstorage->mode.val.b = isAnalog;
62 
63         copyScenedataSubset(&(surge->storage), 0, ids, ide);
64         adsr->attack();
65 
66         bool released = false;
67         bool pushSus = false;
68         int i = 0;
69         std::vector<std::pair<float, float>> res;
70         res.push_back(std::make_pair(0.f, 0.f));
71 
72         while (true)
73         {
74             auto t = 1.0 * (i + 1) * BLOCK_SIZE * dsamplerate_inv;
75             i++;
76             if (t > runUntil || runUntil < 0)
77                 break;
78 
79             if (t > releaseAfter && !released)
80             {
81                 adsr->release();
82                 released = true;
83             }
84 
85             if (pushSusAt > 0 && !pushSus && t > pushSusAt)
86             {
87                 pushSus = true;
88                 svn(&(adsrstorage->s), pushSusTo);
89                 copyScenedataSubset(&(surge->storage), 0, ids, ide);
90             }
91 
92             adsr->process_block();
93             res.push_back(std::make_pair((float)t, adsr->output));
94             if (false && i > 270 && i < 290)
95                 std::cout << i << " " << t << " " << adsr->output << " " << adsr->getEnvState()
96                           << std::endl;
97         }
98         return res;
99     };
100 
__anona43a01a10502(std::vector<std::pair<float, float>> data) 101     auto detectTurnarounds = [](std::vector<std::pair<float, float>> data) {
102         auto pv = -1000.0;
103         int dir = 1;
104         std::vector<std::pair<float, int>> turns;
105         turns.push_back(std::make_pair(0, 1));
106         for (auto &p : data)
107         {
108             auto t = p.first;
109             auto v = p.second;
110             if (pv >= 0)
111             {
112                 int ldir = 0;
113                 if (v > 0.999999f)
114                     ldir = dir; // sometimes we get a double '1'
115                 if (turns.size() > 1 && fabs(v - pv) < 5e-6 && fabs(v) < 1e-5)
116                     ldir = 0; // bouncing off of 0 is annoying
117                 else if (fabs(v - pv) < 1e-7)
118                     ldir = 0;
119                 else if (v > pv)
120                     ldir = 1;
121                 else
122                     ldir = -1;
123 
124                 if (v != 1)
125                 {
126                     if (ldir != dir)
127                     {
128                         turns.push_back(std::make_pair(t, ldir));
129                     }
130                     dir = ldir;
131                 }
132             }
133             pv = v;
134         }
135         return turns;
136     };
137 
138     // With 0 sustain I should decay in decay time
139     auto runCompare = [&](float a, float d, float s, float r, int a_s, int d_s, int r_s,
__anona43a01a10602(float a, float d, float s, float r, int a_s, int d_s, int r_s, bool isAnalog) 140                           bool isAnalog) {
141         float sustime = 0.1;
142         float endtime = 0.1;
143         float totaltime = a + d + sustime + r + endtime;
144 
145         auto simple = runAdsr(a, d, s, r, a_s, d_s, r_s, isAnalog, a + d + sustime, totaltime);
146         auto sturns = detectTurnarounds(simple);
147         INFO("ADSR: " << a << " " << d << " " << s << " " << r << " switches: " << a_s << " " << d_s
148                       << " " << r_s);
149         if (s == 0)
150         {
151             if (sturns.size() != 3)
152             {
153                 for (auto s : simple)
154                     std::cout << s.first << " " << s.second << std::endl;
155                 for (auto s : sturns)
156                     std::cout << s.first << " " << s.second << std::endl;
157             }
158             REQUIRE(sturns.size() == 3);
159             REQUIRE(sturns[0].first == 0);
160             REQUIRE(sturns[0].second == 1);
161             REQUIRE(sturns[1].first == Approx(a).margin(0.01));
162             REQUIRE(sturns[1].second == -1);
163             REQUIRE(sturns[2].first == Approx(a + d).margin(0.01));
164             REQUIRE(sturns[2].second == 0);
165         }
166         else
167         {
168             if (sturns.size() != 5)
169             {
170                 for (auto s : simple)
171                     std::cout << s.first << " " << s.second << std::endl;
172                 std::cout << "TURNS" << std::endl;
173                 for (auto s : sturns)
174                     std::cout << s.first << " " << s.second << std::endl;
175             }
176             REQUIRE(sturns.size() == 5);
177             REQUIRE(sturns[0].first == 0);
178             REQUIRE(sturns[0].second == 1);
179             REQUIRE(sturns[1].first == Approx(a).margin(0.01));
180             REQUIRE(sturns[1].second == -1);
181             if (d_s == 0)
182             {
183                 // this equality only holds in the linear case; in the polynomial case you get
184                 // faster reach to non-zero sustain
185                 REQUIRE(sturns[2].first == Approx(a + d * (1.0 - s)).margin(0.01));
186             }
187             else if (a + d * (1.0 - s) > 0.1 && d > 0.05)
188             {
189                 REQUIRE(sturns[2].first < a + d * (1.0 - s) + 0.01);
190             }
191             REQUIRE(sturns[2].second == 0);
192             REQUIRE(sturns[3].first == Approx(a + d + sustime).margin(0.01));
193             REQUIRE(sturns[3].second == -1);
194             if (r_s == 0 || (s > 0.1 && r > 0.05)) // if we are in the non-linear releases at low
195                                                    // sustain we get there early
196             {
197                 REQUIRE(sturns[4].first ==
198                         Approx(a + d + sustime + r).margin((r_s == 0 ? 0.01 : (r * 0.1))));
199                 REQUIRE(sturns[4].second == 0);
200             }
201         }
202     };
203 
204     SECTION("Test the Digital Envelope")
205     {
206         for (int as = 0; as < 3; ++as)
207             for (int ds = 0; ds < 3; ++ds)
208                 for (int rs = 0; rs < 3; ++rs)
209                 {
210                     runCompare(0.2, 0.3, 0.0, 0.1, as, ds, rs, false);
211                     runCompare(0.2, 0.3, 0.5, 0.1, as, ds, rs, false);
212 
213                     for (int rc = 0; rc < 10; ++rc)
214                     {
215                         auto a = rand() * 1.0 / (float)RAND_MAX;
216                         auto d = rand() * 1.0 / (float)RAND_MAX;
217                         auto s = 0.8 * rand() * 1.0 / (float)RAND_MAX +
218                                  0.1; // we have tested the s=0 case above
219                         auto r = rand() * 1.0 / (float)RAND_MAX;
220                         runCompare(a, d, s, r, as, ds, rs, false);
221                     }
222                 }
223     }
224 
225     SECTION("Quadrtic Digital hits Zero")
226     {
227         auto res = runAdsr(0.1, 0.1, 0.0, 0.1, 0, 1, 0, false, 0.4, 0.5);
228         for (auto p : res)
229         {
230             if (p.first > 0.22)
231             {
232                 REQUIRE(p.second == 0.f);
233             }
234         }
235     }
236 
237     SECTION("Test the Analog Envelope")
238     {
239         // OK so we can't check the same thing here since the turns aren't as tight in analog mode
240         // Also the analog ADSR sustains at half the given sustain.
__anona43a01a10702(float a, float d, float s, float r) 241         auto testAnalog = [&](float a, float d, float s, float r) {
242             INFO("ANALOG " << a << " " << d << " " << s << " " << r);
243             auto holdFor = a + d + d + 0.5;
244 
245             auto ae = runAdsr(a, d, s, r, 0, 0, 0, true, holdFor, holdFor + 4 * r);
246             auto aturns = detectTurnarounds(ae);
247 
248             float maxt = 0, maxv = 0;
249             float zerot = 0;
250             float valAtRelEnd = -1;
251             std::vector<float> heldPeriod;
252             for (auto obs : ae)
253             {
254                 // std::cout << obs.first << " " << obs.second << std::endl;
255 
256                 if (obs.first > a + d + d * 0.95 && obs.first < holdFor &&
257                     s > 0.05) // that 0.1 lets the delay ring off
258                 {
259                     REQUIRE(obs.second == Approx(s * s).margin(1e-3));
260                     heldPeriod.push_back(obs.second);
261                 }
262                 if (obs.first > a + d && obs.second < 5e-5 && zerot == 0)
263                     zerot = obs.first;
264                 if (obs.first > holdFor + r && valAtRelEnd < 0)
265                     valAtRelEnd = obs.second;
266 
267                 if (obs.second > maxv)
268                 {
269                     maxv = obs.second;
270                     maxt = obs.first;
271                 }
272             }
273 
274             // In the held period are we mostly constant
275             if (heldPeriod.size() > 10)
276             {
277                 float sum = 0;
278                 for (auto p : heldPeriod)
279                     sum += p;
280                 float mean = sum / heldPeriod.size();
281                 float var = 0;
282                 for (auto p : heldPeriod)
283                     var += (p - mean) * (p - mean);
284                 var /= (heldPeriod.size() - 1);
285                 float stddev = sqrt(var);
286                 REQUIRE(stddev < d * 5e-3);
287             }
288             REQUIRE(maxt < a);
289             REQUIRE(maxv > 0.99);
290             if (s > 0.05)
291             {
292                 REQUIRE(zerot > holdFor + r * 0.9);
293                 REQUIRE(valAtRelEnd < s * 0.025);
294             }
295         };
296 
297         testAnalog(0.1, 0.2, 0.5, 0.1);
298         testAnalog(0.1, 0.2, 0.0, 0.1);
299         for (int rc = 0; rc < 50; ++rc)
300         {
301             auto a = rand() * 1.0 / (float)RAND_MAX + 0.03;
302             auto d = rand() * 1.0 / (float)RAND_MAX + 0.03;
303             auto s =
304                 0.7 * rand() * 1.0 / (float)RAND_MAX + 0.2; // we have tested the s=0 case above
305             auto r = rand() * 1.0 / (float)RAND_MAX + 0.03;
306             testAnalog(a, d, s, r);
307         }
308     }
309 
310     // This is just a rudiemntary little test of this in digital mode
311     SECTION("Test Digital Sus Push")
312     {
__anona43a01a10802(float s1, float s2) 313         auto testSusPush = [&](float s1, float s2) {
314             auto digPush = runAdsr(0.05, 0.05, s1, 0.1, 0, 0, 0, false, 0.5, s2, 0.25, s2);
315             int obs = 0;
316             for (auto s : digPush)
317             {
318                 if (s.first > 0.2 && obs == 0)
319                 {
320                     REQUIRE(s.second == Approx(s1).margin(1e-5));
321                     obs++;
322                 }
323                 if (s.first > 0.3 && obs == 1)
324                 {
325                     REQUIRE(s.second == Approx(s2).margin(1e-5));
326                     obs++;
327                 }
328             }
329         };
330 
331         for (auto i = 0; i < 10; ++i)
332         {
333             auto s1 = 0.95f * rand() / (float)RAND_MAX + 0.02;
334             auto s2 = 0.95f * rand() / (float)RAND_MAX + 0.02;
335             testSusPush(s1, s2);
336         }
337     }
338 
339     /*
340     ** This section recreates the somewhat painful SSE code in readable stuff
341     */
342     auto analogClone = [](float a_sec, float d_sec, float s, float r_sec, float releaseAfter,
__anona43a01a10902(float a_sec, float d_sec, float s, float r_sec, float releaseAfter, float runUntil, float pushSusAt = -1, float pushSusTo = 0) 343                           float runUntil, float pushSusAt = -1, float pushSusTo = 0) {
344         float a = limit_range((float)(log(a_sec) / log(2.0)), -8.f, 5.f);
345         float d = limit_range((float)(log(d_sec) / log(2.0)), -8.f, 5.f);
346         float r = limit_range((float)(log(r_sec) / log(2.0)), -8.f, 5.f);
347 
348         int i = 0;
349         bool released = false;
350         std::vector<std::pair<float, float>> res;
351         res.push_back(std::make_pair(0.f, 0.f));
352 
353         float v_c1 = 0.f;
354         float v_c1_delayed = 0.f;
355         bool discharge = false;
356         const float v_cc = 1.5f;
357 
358         while (true)
359         {
360             float t = 1.0 * (i + 1) * BLOCK_SIZE * dsamplerate_inv;
361             i++;
362             if (t > runUntil || runUntil < 0)
363                 break;
364 
365             if (t > releaseAfter && !released)
366             {
367                 released = true;
368             }
369 
370             if (pushSusAt > 0 && t > pushSusAt)
371                 s = pushSusTo;
372 
373             auto gate = !released;
374             float v_gate = gate ? v_cc : 0.f;
375 
376             // discharge = _mm_and_ps(_mm_or_ps(_mm_cmpgt_ss(v_c1_delayed, one), discharge),
377             // v_gate);
378             discharge = ((v_c1_delayed > 1) || discharge) && gate;
379             v_c1_delayed = v_c1;
380 
381             float S = s * s;
382 
383             float v_attack = discharge ? 0 : v_gate;
384             // OK so this line:
385             // __m128 v_decay = _mm_or_ps(_mm_andnot_ps(discharge, v_cc_vec), _mm_and_ps(discharge,
386             // S)); The semantic intent is discharge ? S : v_cc but in the ADSR discharge has a
387             // value of v_gate which is 1.5 (v_cc) not 1.0. That bitwise and with 1.5 acts as a
388             // binary filter (the mantissa) and a rounding operation (from the .5) so I need to
389             // duplicate it exactly here.
390             /*
391             __m128 sM = _mm_load_ss(&S);
392             __m128 dM = _mm_load_ss(&v_cc);
393             __m128 vdv = _mm_and_ps(dM, sM);
394             float v_decay = discharge ? vdv[0] : v_cc;
395             */
396             // Alternately I can correct the SSE code for discharge
397             float v_decay = discharge ? S : v_cc;
398             float v_release = v_gate;
399 
400             float diff_v_a = std::max(0.f, v_attack - v_c1);
401             float diff_v_d = (discharge && gate) ? v_decay - v_c1 : std::min(0.f, v_decay - v_c1);
402             // float diff_v_d = std::min( 0.f, v_decay   - v_c1 );
403             float diff_v_r = std::min(0.f, v_release - v_c1);
404 
405             const float shortest = 6.f;
406             const float longest = -2.f;
407             const float coeff_offset = 2.f - log(samplerate / BLOCK_SIZE) / log(2.f);
408 
409             float coef_A = pow(2.f, std::min(0.f, coeff_offset - a));
410             float coef_D = pow(2.f, std::min(0.f, coeff_offset - d));
411             float coef_R = pow(2.f, std::min(0.f, coeff_offset - r));
412 
413             v_c1 = v_c1 + diff_v_a * coef_A;
414             v_c1 = v_c1 + diff_v_d * coef_D;
415             v_c1 = v_c1 + diff_v_r * coef_R;
416 
417             // adsr->process_block();
418             float output = v_c1;
419             if (!gate && !discharge && v_c1 < 1e-6)
420                 output = 0;
421 
422             res.push_back(std::make_pair((float)t, output));
423         }
424         return res;
425     };
426 
427     SECTION("Clone the Analog")
428     {
__anona43a01a10a02(float a, float d, float s, float r) 429         auto compareSrgRepl = [&](float a, float d, float s, float r) {
430             auto t = a + d + 0.5 + r * 3;
431             auto surgeA = runAdsr(a, d, s, r, 0, 0, 0, true, a + d + 0.5, t);
432             auto replA = analogClone(a, d, s, r, a + d + 0.5, t);
433 
434             REQUIRE(surgeA.size() == Approx(replA.size()).margin(3));
435             auto sz = std::min(surgeA.size(), replA.size());
436             for (auto i = 0; i < sz; ++i)
437             {
438                 if (replA[i].second > 1e-5) // CI pipelines bounce around zero badly
439                     REQUIRE(replA[i].second == Approx(surgeA[i].second).margin(1e-2));
440             }
441         };
442 
443         compareSrgRepl(0.1, 0.2, 0.5, 0.1);
444         compareSrgRepl(0.1, 0.2, 0.0, 0.1);
445 
446         for (int rc = 0; rc < 100; ++rc)
447         {
448             float a = rand() * 1.0 / (float)RAND_MAX + 0.03;
449             float d = rand() * 1.0 / (float)RAND_MAX + 0.01;
450             float s =
451                 0.7 * rand() * 1.0 / (float)RAND_MAX + 0.1; // we have tested the s=0 case above
452             float r = rand() * 1.0 / (float)RAND_MAX +
453                       0.1; // smaller versions can get one bad point in the pipeline
454             INFO("Testing " << rc << " with ADSR=" << a << " " << d << " " << s << " " << r);
455             compareSrgRepl(a, d, s, r);
456         }
457     }
458 
459     SECTION("Test Analog Sus Push")
460     {
__anona43a01a10b02(float s1, float s2) 461         auto testSusPush = [&](float s1, float s2) {
462             auto aSurgePush = runAdsr(0.05, 0.05, s1, 0.1, 0, 0, 0, true, 0.5, s2, 0.25, s2);
463             auto aDupPush = analogClone(0.05, 0.05, s1, 0.1, 0.5, 0.7, 0.25, s2);
464 
465             int obs = 0;
466             INFO("Analog Dup passes sus push");
467             for (auto s : aDupPush)
468             {
469                 if (s.first > 0.2 && obs == 0)
470                 {
471                     REQUIRE(s.second == Approx(s1 * s1).margin(1e-2));
472                     obs++;
473                 }
474                 if (s.first > 0.4 && obs == 1)
475                 {
476                     REQUIRE(s.second == Approx(s2 * s2).margin(1e-2));
477                     obs++;
478                 }
479             }
480 
481             obs = 0;
482             INFO("Analog SSE passes sus push");
483             for (auto s : aSurgePush)
484             {
485                 if (s.first > 0.2 && obs == 0)
486                 {
487                     REQUIRE(s.second == Approx(s1 * s1).margin(1e-2));
488                     obs++;
489                 }
490                 if (s.first > 0.4 && obs == 1)
491                 {
492                     REQUIRE(s.second == Approx(s2 * s2).margin(1e-3));
493                     obs++;
494                 }
495             }
496         };
497 
498         testSusPush(0.3, 0.7);
499         for (auto i = 0; i < 10; ++i)
500         {
501             auto s1 = 0.95f * rand() / (float)RAND_MAX + 0.02;
502             auto s2 = 0.95f * rand() / (float)RAND_MAX + 0.02;
503             testSusPush(s1, s2);
504         }
505     }
506 }
507 
508 TEST_CASE("Non-MPE pitch bend", "[mod]")
509 {
510     SECTION("Simple Bend Distances")
511     {
512         auto surge = surgeOnSine();
513         surge->mpeEnabled = false;
514         surge->storage.getPatch().scene[0].pbrange_up.val.i = 2;
515         surge->storage.getPatch().scene[0].pbrange_dn.val.i = 2;
516 
517         auto f60 = frequencyForNote(surge, 60);
518         auto f62 = frequencyForNote(surge, 62);
519         auto f58 = frequencyForNote(surge, 58);
520 
521         surge->pitchBend(0, 8192);
522         auto f60bendUp = frequencyForNote(surge, 60);
523 
524         surge->pitchBend(0, -8192);
525         auto f60bendDn = frequencyForNote(surge, 60);
526 
527         REQUIRE(f62 == Approx(f60bendUp).margin(0.3));
528         REQUIRE(f58 == Approx(f60bendDn).margin(0.3));
529     }
530 
531     SECTION("Asymmetric Bend Distances")
532     {
533         auto surge = surgeOnSine();
534         surge->mpeEnabled = false;
535         for (int tests = 0; tests < 20; ++tests)
536         {
537             int bUp = rand() % 24 + 1;
538             int bDn = rand() % 24 + 1;
539 
540             surge->storage.getPatch().scene[0].pbrange_up.val.i = bUp;
541             surge->storage.getPatch().scene[0].pbrange_dn.val.i = bDn;
542             auto fUpD = frequencyForNote(surge, 60 + bUp);
543             auto fDnD = frequencyForNote(surge, 60 - bDn);
544 
545             // Bend pitch and let it get there
546             surge->pitchBend(0, 8192);
547             for (int i = 0; i < 100; ++i)
548                 surge->process();
549 
550             auto fUpB = frequencyForNote(surge, 60);
551 
552             // Bend pitch and let it get there
553             surge->pitchBend(0, -8192);
554             for (int i = 0; i < 100; ++i)
555                 surge->process();
556             auto fDnB = frequencyForNote(surge, 60);
557 
558             REQUIRE(fUpD ==
559                     Approx(fUpB).margin(3 * bUp)); // It can take a while for the midi lag to
560                                                    // normalize and my pitch detector is so so
561             REQUIRE(fDnD == Approx(fDnB).margin(3 * bDn));
562 
563             surge->pitchBend(0, 0);
564             for (int i = 0; i < 100; ++i)
565                 surge->process();
566         }
567     }
568 }
569 
570 TEST_CASE("Pitch Bend and Tuning", "[mod][tun]")
571 {
572     std::vector<std::string> testScales = {"test-data/scl/12-intune.scl",
573                                            "test-data/scl/zeus22.scl", "test-data/scl/6-exact.scl"};
574 
575     SECTION("Multi Scale Bend Distances")
576     {
577         auto surge = surgeOnSine();
578         surge->mpeEnabled = false;
579 
580         for (auto sclf : testScales)
581         {
582             INFO("Retuning pitch bend to " << sclf);
583             auto s = Tunings::readSCLFile(sclf);
584             surge->storage.retuneToScale(s);
585             for (int tests = 0; tests < 30; ++tests)
586             {
587                 int bUp = rand() % 24 + 1;
588                 int bDn = rand() % 24 + 1;
589 
590                 surge->storage.getPatch().scene[0].pbrange_up.val.i = bUp;
591                 surge->storage.getPatch().scene[0].pbrange_dn.val.i = bDn;
592                 auto fUpD = frequencyForNote(surge, 60 + bUp);
593                 auto fDnD = frequencyForNote(surge, 60 - bDn);
594 
595                 // Bend pitch and let it get there
596                 surge->pitchBend(0, 8192);
597                 for (int i = 0; i < 500; ++i)
598                     surge->process();
599 
600                 auto fUpB = frequencyForNote(surge, 60);
601 
602                 // Bend pitch and let it get there
603                 surge->pitchBend(0, -8192);
604                 for (int i = 0; i < 500; ++i)
605                     surge->process();
606                 auto fDnB = frequencyForNote(surge, 60);
607                 auto dup = surge->storage.note_to_pitch(60 + bUp + 2) -
608                            surge->storage.note_to_pitch(60 + bUp);
609                 dup = dup * 8.17;
610 
611                 auto ddn = surge->storage.note_to_pitch(60 - bDn) -
612                            surge->storage.note_to_pitch(60 - bDn - 2);
613                 ddn = ddn * 8.17;
614 
615                 INFO("Pitch Bend " << bUp << " " << bDn << " " << fUpD << " " << fUpB << " " << dup
616                                    << " " << ddn);
617                 REQUIRE(fUpD ==
618                         Approx(fUpB).margin(dup)); // It can take a while for the midi lag to
619                                                    // normalize and my pitch detector is so so
620                 REQUIRE(fDnD == Approx(fDnB).margin(ddn));
621 
622                 surge->pitchBend(0, 0);
623                 for (int i = 0; i < 100; ++i)
624                     surge->process();
625             }
626         }
627     }
628 }
629 
630 TEST_CASE("MPE pitch bend", "[mod]")
631 {
632     SECTION("Channel 0 bends should be a correct global bend")
633     { // note that this test actually checks if channel 0 bends behave like non-MPE bends
634         auto surge = surgeOnSine();
635         surge->mpeEnabled = true;
636         surge->storage.mpePitchBendRange = 48;
637 
638         surge->storage.getPatch().scene[0].pbrange_up.val.i = 2;
639         surge->storage.getPatch().scene[0].pbrange_dn.val.i = 2;
640 
641         auto f60 = frequencyForNote(surge, 60);
642         auto f62 = frequencyForNote(surge, 62);
643         auto f58 = frequencyForNote(surge, 58);
644 
645         surge->pitchBend(0, 8192);
646         auto f60bendUp = frequencyForNote(surge, 60);
647 
648         surge->pitchBend(0, -8192);
649         auto f60bendDn = frequencyForNote(surge, 60);
650 
651         REQUIRE(f62 == Approx(f60bendUp).margin(0.3));
652         REQUIRE(f58 == Approx(f60bendDn).margin(0.3));
653     }
654 
655     SECTION("Channel n bends should be a correct note bend")
656     {
657         auto surge = surgeOnSine();
658         surge->mpeEnabled = true;
659         auto pbr = 48;
660         auto sbs = 8192 * 1.f / pbr;
661 
662         surge->storage.mpePitchBendRange = pbr;
663 
664         surge->storage.getPatch().scene[0].pbrange_up.val.i = 2;
665         surge->storage.getPatch().scene[0].pbrange_dn.val.i = 2;
666 
667         // Play on channel 1 which is now an MPE bend channel and send bends on that chan
668         auto f60 = frequencyForNote(surge, 60, 2, 0, 1);
669         auto f62 = frequencyForNote(surge, 62, 2, 0, 1);
670         auto f58 = frequencyForNote(surge, 58, 2, 0, 1);
671 
672         // for MPE bend after the note starts to be the most accurate simulation
__anona43a01a10c02(int n, int b) 673         auto noteFreqWithBend = [surge](int n, int b) {
674             Surge::Headless::playerEvents_t events;
675 
676             Surge::Headless::Event on, off, bend;
677             on.type = Surge::Headless::Event::NOTE_ON;
678             on.channel = 1;
679             on.data1 = n;
680             on.data2 = 100;
681             on.atSample = 100;
682 
683             off.type = Surge::Headless::Event::NOTE_OFF;
684             off.channel = 1;
685             off.data1 = n;
686             off.data2 = 100;
687             off.atSample = 44100 * 2 + 100;
688 
689             bend.type = Surge::Headless::Event::LAMBDA_EVENT;
690             bend.surgeLambda = [b](std::shared_ptr<SurgeSynthesizer> s) { s->pitchBend(1, b); };
691             bend.atSample = 400;
692 
693             events.push_back(on);
694             events.push_back(bend);
695             events.push_back(off);
696 
697             return frequencyForEvents(surge, events, 0, 4000, 44100 * 2 - 8000);
698         };
699 
700         auto f60bendUp = noteFreqWithBend(60, 2 * sbs);
701         auto f60bendDn = noteFreqWithBend(60, -2 * sbs);
702 
703         REQUIRE(f62 == Approx(f60bendUp).margin(0.3));
704         REQUIRE(f58 == Approx(f60bendDn).margin(0.3));
705     }
706 }
707 
708 TEST_CASE("LfoTempoSync Latch Drift", "[mod]")
709 {
710     SECTION("Latch Drift")
711     {
712         auto surge = Surge::Headless::createSurge(44100);
713 
714         int64_t bpm = 120;
715         surge->time_data.tempo = bpm;
716 
717         REQUIRE(surge);
718 
719         auto lfo = std::make_unique<LfoModulationSource>();
720         auto ss = std::make_unique<StepSequencerStorage>();
721         auto lfostorage = &(surge->storage.getPatch().scene[0].lfo[0]);
722         lfostorage->rate.temposync = true;
723         SurgeSynthesizer::ID rid;
724         surge->fromSynthSideId(lfostorage->rate.id, rid);
725         surge->setParameter01(rid, 0.455068, false, false);
726         lfostorage->shape.val.i = lt_square;
727 
728         surge->storage.getPatch().copy_scenedata(surge->storage.getPatch().scenedata[0], 0);
729 
730         lfo->assign(&(surge->storage), lfostorage, surge->storage.getPatch().scenedata[0], nullptr,
731                     ss.get(), nullptr, nullptr);
732         lfo->attack();
733         lfo->process_block();
734 
735         float p = -1000;
736         for (int64_t i = 0; i < 10000000; ++i)
737         {
738             if (lfo->output > p)
739             {
740                 double time = i * dsamplerate_inv * BLOCK_SIZE;
741                 double beats = time * bpm / 60;
742                 int bt2 = round(beats * 2);
743                 double drift = fabs(beats * 2 - bt2);
744                 // std::cout << time / 60.0 <<  " " << drift << std::endl;
745             }
746             p = lfo->output;
747             lfo->process_block();
748         }
749     }
750 }
751 
752 TEST_CASE("CModulationSources", "[mod]")
753 {
754     SECTION("Legacy Mode")
755     {
756         auto surge = Surge::Headless::createSurge(44100);
757         REQUIRE(surge);
758         ControllerModulationSource a(ControllerModulationSource::SmoothingMode::LEGACY);
759         a.init(0.5f);
760         REQUIRE(a.output == 0.5f);
761         float t = 0.6;
762         a.set_target(t);
763         float priorO = a.output;
764         float dO = 100000;
765         for (int i = 0; i < 100; ++i)
766         {
767             a.process_block();
768             REQUIRE(t - a.output < t - priorO);
769             REQUIRE(a.output - priorO < dO);
770             dO = a.output - priorO;
771             priorO = a.output;
772         }
773     }
774 
775     SECTION("Fast Exp Mode gets there")
776     {
777         auto surge = Surge::Headless::createSurge(44100);
778         REQUIRE(surge);
779 
780         ControllerModulationSource a(ControllerModulationSource::SmoothingMode::FAST_EXP);
781         a.init(0.5f);
782         REQUIRE(a.output == 0.5f);
783         float t = 0.6;
784         a.set_target(t);
785         float priorO = a.output;
786         float dO = 100000;
787         for (int i = 0; i < 200; ++i)
788         {
789             a.process_block();
790             REQUIRE(t - a.output <= t - priorO);
791             REQUIRE(((a.output == t) || (a.output - priorO < dO)));
792             dO = a.output - priorO;
793             priorO = a.output;
794         }
795         REQUIRE(a.output == t);
796     }
797 
798     SECTION("Slow Exp Mode gets there eventually")
799     {
800         auto surge = Surge::Headless::createSurge(44100);
801         REQUIRE(surge);
802 
803         ControllerModulationSource a(ControllerModulationSource::SmoothingMode::SLOW_EXP);
804         a.init(0.5f);
805         REQUIRE(a.output == 0.5f);
806         float t = 0.6;
807         a.set_target(t);
808         int idx = 0;
809         while (a.output != t && idx < 10000)
810         {
811             a.process_block();
812             idx++;
813         }
814         REQUIRE(a.output == t);
815         REQUIRE(idx < 1000);
816     }
817 
818     SECTION("Go as a Line")
819     {
820         auto surge = Surge::Headless::createSurge(44100);
821         REQUIRE(surge);
822 
823         ControllerModulationSource a(ControllerModulationSource::SmoothingMode::FAST_LINE);
824         a.init(0.5f);
825         for (int i = 0; i < 10; ++i)
826         {
827             float r = rand() / ((float)RAND_MAX);
828             a.set_target(r);
829             for (int j = 0; j < 60; ++j)
830             {
831                 a.process_block();
832             }
833             REQUIRE(a.output == r);
834         }
835     }
836 
837     SECTION("Direct is Direct")
838     {
839         auto surge = Surge::Headless::createSurge(44100);
840         REQUIRE(surge);
841 
842         ControllerModulationSource a(ControllerModulationSource::SmoothingMode::DIRECT);
843         a.init(0.5f);
844         for (int i = 0; i < 100; ++i)
845         {
846             float r = rand() / ((float)RAND_MAX);
847             a.set_target(r);
848             a.process_block();
849             REQUIRE(a.output == r);
850         }
851     }
852 }
853 
854 TEST_CASE("Keytrack Morph", "[mod]")
855 {
856     INFO("See issue 3046");
857     SECTION("Run High Key")
858     {
859         auto surge = Surge::Headless::createSurge(44100);
860         REQUIRE(surge);
861         surge->loadPatchByPath("test-data/patches/Keytrack-Morph-3046.fxp", -1, "Test");
862         for (int i = 0; i < 100; ++i)
863             surge->process();
864 
865         surge->playNote(0, 100, 127, 0);
866         for (int i = 0; i < 10; ++i)
867         {
868             surge->process();
869             /*
870              * FIXME: Make this an assertive test. What we are really checking is is l_shape 3.33
871              * inside the oscillator but there's no easy way to assert that so just leave the test
872              * here as a debugging harness around issue 3046
873              */
874         }
875     }
876 }
877 
878 TEST_CASE("KeyTrack in Play Modes", "[mod]")
879 {
880     /*
881      * See issue 2892. In mono mode keytrack needs to follow held keys not just soloe playing voice
882      */
883     auto playSequence = [](std::shared_ptr<SurgeSynthesizer> surge, std::vector<int> notes,
__anona43a01a10e02(std::shared_ptr<SurgeSynthesizer> surge, std::vector<int> notes, bool mpe) 884                            bool mpe) {
885         std::unordered_map<int, int> noteToChan;
886         int cmpe = 1;
887         for (auto n : notes)
888         {
889             int chan = 0;
890             if (mpe)
891             {
892                 if (n < 0)
893                 {
894                     chan = noteToChan[-n];
895                 }
896                 else
897                 {
898                     cmpe++;
899                     if (cmpe > 15)
900                         cmpe = 1;
901                     noteToChan[n] = cmpe;
902                     chan = cmpe;
903                 }
904             }
905             if (n > 0)
906             {
907                 surge->playNote(chan, n, 127, 0);
908             }
909             else
910             {
911                 surge->releaseNote(chan, -n, 0);
912             }
913             for (int i = 0; i < 10; ++i)
914                 surge->process();
915         }
916     };
917 
918     auto modes = {pm_poly, pm_mono, pm_mono_st, pm_mono_fp, pm_mono_st_fp};
919     auto mpe = {false, true};
920     for (auto mp : mpe)
921     {
922         for (auto m : modes)
923         {
__anona43a01a10f02() 924             auto cs = [m, mp]() {
925                 auto surge = Surge::Headless::createSurge(44100);
926                 surge->mpeEnabled = mp;
927                 surge->storage.getPatch().scene[0].polymode.val.i = m;
928                 surge->storage.getPatch().scene[0].keytrack_root.val.i = 60;
929                 return surge;
930             };
931             auto checkModes = [](std::shared_ptr<SurgeSynthesizer> surge, float low, float high,
__anona43a01a11002(std::shared_ptr<SurgeSynthesizer> surge, float low, float high, float latest) 932                                  float latest) {
933                 REQUIRE(surge->storage.getPatch().scene[0].modsources[ms_lowest_key]->output ==
934                         low);
935                 REQUIRE(surge->storage.getPatch().scene[0].modsources[ms_highest_key]->output ==
936                         high);
937                 REQUIRE(surge->storage.getPatch().scene[0].modsources[ms_latest_key]->output ==
938                         latest);
939                 return true;
940             };
941             DYNAMIC_SECTION("KeyTrack Test mode=" << m << " mpe=" << mp)
942             {
943                 {
944                     auto surge = cs();
945                     REQUIRE(checkModes(surge, 0, 0, 0));
946                 }
947                 {
948                     auto surge = cs();
949                     playSequence(surge, {48}, mp);
950                     checkModes(surge, -1, -1, -1);
951                 }
952                 {
953                     auto surge = cs();
954                     playSequence(surge, {48, -48}, mp);
955                     checkModes(surge, -1, -1, -1); // This is the change in #3600 - keys are sticky
956                 }
957                 auto surge = cs();
958                 {
959                     auto surge = cs();
960                     playSequence(surge, {48, 84, 36, 72, -36}, mp);
961                     checkModes(surge, -1, 2, 1);
962                 }
963 
964                 {
965                     /*
966                      * This is the one which fails in mono mode with the naive just-voices
967                      * implementation
968                      */
969                     auto surge = cs();
970                     playSequence(surge, {48, 84, 72}, mp);
971                     checkModes(surge, -1, 2, 1);
972                 }
973             }
974         }
975     }
976 }
977 
978 TEST_CASE("High Low Latest Note Modulator in All Modes", "[mod]")
979 {
980     // See issue #3597
__anona43a01a11102(MonoVoicePriorityMode priomode, play_mode polymode) 981     auto setup = [](MonoVoicePriorityMode priomode, play_mode polymode) {
982         auto surge = Surge::Headless::createSurge(44100);
983 
984         // Set synth to mono and low note priority
985         surge->storage.getPatch().scene[0].polymode.val.i = polymode;
986         surge->storage.getPatch().scene[0].monoVoicePriorityMode = priomode;
987 
988         // Assign highest note keytrack to any parameter. Lets do this with highest latest and
989         // lowest
990         surge->setModulation(surge->storage.getPatch().scene[0].osc[0].pitch.id, ms_highest_key,
991                              0.2);
992         surge->setModulation(surge->storage.getPatch().scene[0].osc[0].p[0].id, ms_latest_key, 0.2);
993         surge->setModulation(surge->storage.getPatch().scene[0].osc[0].p[1].id, ms_lowest_key, 0.2);
994         return surge;
995     };
996 
997     auto rcmp = [](std::shared_ptr<SurgeSynthesizer> surge, int ch, int key, int vel, int low,
__anona43a01a11202(std::shared_ptr<SurgeSynthesizer> surge, int ch, int key, int vel, int low, int high, int latest) 998                    int high, int latest) {
999         if (vel == 0)
1000             surge->releaseNote(ch, key, vel);
1001         else
1002             surge->playNote(ch, key, vel, 0);
1003         for (int i = 0; i < 50; ++i)
1004             surge->process();
1005         for (auto v : surge->voices[0])
1006             if (v->state.gate)
1007             {
1008                 REQUIRE(v->modsources[ms_highest_key]->output * 12 + 60 == high);
1009                 REQUIRE(v->modsources[ms_latest_key]->output * 12 + 60 == latest);
1010                 REQUIRE(v->modsources[ms_lowest_key]->output * 12 + 60 == low);
1011             }
1012     };
1013 
1014     std::map<MonoVoicePriorityMode, std::string> lab;
1015     lab[NOTE_ON_LATEST_RETRIGGER_HIGHEST] = "legacy";
1016     lab[ALWAYS_HIGHEST] = "always highest";
1017     lab[ALWAYS_LATEST] = "always latest";
1018     lab[ALWAYS_LOWEST] = "always lowest";
1019 
1020     for (auto mpemode : {true, false})
1021         for (auto polymode : {pm_mono_st, pm_poly, pm_mono, pm_mono_fp, pm_mono_st, pm_mono_st_fp})
1022             for (auto priomode :
1023                  {NOTE_ON_LATEST_RETRIGGER_HIGHEST, ALWAYS_LOWEST, ALWAYS_HIGHEST, ALWAYS_LATEST})
1024             {
1025                 DYNAMIC_SECTION("Play Up Test " << lab[priomode] << " mpe=" << mpemode
1026                                                 << " poly=" << polymode)
1027                 {
1028                     // From the issue: Steps to reproduce the behavior:
1029                     auto surge = setup(priomode, polymode);
1030                     surge->mpeEnabled = mpemode;
1031 
1032                     // press and hold three increasing notes
1033                     int ch = 0;
1034 
1035                     if (mpemode)
1036                         ch = 1;
1037                     rcmp(surge, ch, 60, 127, 60, 60, 60);
1038 
1039                     if (mpemode)
1040                         ch++;
1041                     rcmp(surge, ch, 66, 127, 60, 66, 66);
1042 
1043                     if (mpemode)
1044                         ch++;
1045                     rcmp(surge, ch, 72, 127, 60, 72, 72);
1046                 }
1047                 DYNAMIC_SECTION("Play Down Test " << lab[priomode] << " mpe=" << mpemode
1048                                                   << " polymode=" << polymode)
1049                 {
1050                     auto surge = setup(priomode, polymode);
1051                     surge->mpeEnabled = mpemode;
1052 
1053                     // press and hold three decreasing notes
1054                     int ch = 0;
1055 
1056                     if (mpemode)
1057                         ch = 1;
1058                     rcmp(surge, ch, 60, 127, 60, 60, 60);
1059 
1060                     if (mpemode)
1061                         ch++;
1062                     rcmp(surge, ch, 54, 127, 54, 60, 54);
1063 
1064                     if (mpemode)
1065                         ch++;
1066                     rcmp(surge, ch, 48, 127, 48, 60, 48);
1067                 }
1068 
1069                 DYNAMIC_SECTION("Play V Test " << lab[priomode] << " mpe=" << mpemode
1070                                                << " polymode=" << polymode)
1071                 {
1072                     // From the issue: Steps to reproduce the behavior:
1073                     auto surge = setup(priomode, polymode);
1074                     surge->mpeEnabled = mpemode;
1075 
1076                     // press and hold three intersecting notes
1077                     int ch = 0;
1078 
1079                     if (mpemode)
1080                         ch = 1;
1081                     rcmp(surge, ch, 60, 127, 60, 60, 60);
1082 
1083                     if (mpemode)
1084                         ch++;
1085                     rcmp(surge, ch, 72, 127, 60, 72, 72);
1086 
1087                     if (mpemode)
1088                         ch++;
1089                     rcmp(surge, ch, 66, 127, 60, 72, 66);
1090                 }
1091 
1092                 DYNAMIC_SECTION("Releases one " << lab[priomode] << " mpe=" << mpemode
1093                                                 << " polymode=" << polymode)
1094                 {
1095                     // From the issue: Steps to reproduce the behavior:
1096                     auto surge = setup(priomode, polymode);
1097                     surge->mpeEnabled = mpemode;
1098 
1099                     // press and hold three intersecting notes
1100                     int ch = 0;
1101 
1102                     if (mpemode)
1103                         ch = 1;
1104                     rcmp(surge, ch, 60, 127, 60, 60, 60);
1105 
1106                     if (mpemode)
1107                         ch++;
1108                     rcmp(surge, ch, 72, 127, 60, 72, 72);
1109 
1110                     if (mpemode)
1111                         ch++;
1112                     rcmp(surge, ch, 66, 127, 60, 72, 66);
1113 
1114                     if (mpemode)
1115                         ch--;
1116                     rcmp(surge, ch, 72, 0, 60, 66, 66);
1117                 }
1118 
1119                 DYNAMIC_SECTION("Releases one " << lab[priomode] << " mpe=" << mpemode
1120                                                 << " polymode=" << polymode)
1121                 {
1122                     // From the issue: Steps to reproduce the behavior:
1123                     auto surge = setup(priomode, polymode);
1124                     surge->mpeEnabled = mpemode;
1125 
1126                     // press and hold three intersecting notes
1127                     int ch = 0;
1128 
1129                     if (mpemode)
1130                         ch = 1;
1131                     rcmp(surge, ch, 60, 127, 60, 60, 60);
1132 
1133                     if (mpemode)
1134                         ch++;
1135                     rcmp(surge, ch, 72, 127, 60, 72, 72);
1136 
1137                     if (mpemode)
1138                         ch++;
1139                     rcmp(surge, ch, 66, 127, 60, 72, 66);
1140 
1141                     // and unwind them
1142                     ch = 0;
1143 
1144                     if (mpemode)
1145                         ch = 1;
1146                     rcmp(surge, ch, 60, 0, 66, 72, 66);
1147 
1148                     if (mpemode)
1149                         ch++;
1150                     rcmp(surge, ch, 72, 0, 66, 66, 66);
1151 
1152                     if (mpemode)
1153                         ch++;
1154                     rcmp(surge, ch, 66, 0, 60, 60, 60); // 60 maps to 0
1155                 }
1156             }
1157 }
1158 
1159 TEST_CASE("High Low Latest with splits", "[mod]")
1160 {
__anona43a01a11302(MonoVoicePriorityMode priomode, play_mode polymode) 1161     auto setup = [](MonoVoicePriorityMode priomode, play_mode polymode) {
1162         auto surge = Surge::Headless::createSurge(44100);
1163 
1164         // Set synth to mono and low note priority
1165         surge->storage.getPatch().scene[0].polymode.val.i = polymode;
1166         surge->storage.getPatch().scene[0].monoVoicePriorityMode = priomode;
1167 
1168         // Assign highest note keytrack to any parameter. Lets do this with highest latest and
1169         // lowest
1170         surge->setModulation(surge->storage.getPatch().scene[0].osc[0].pitch.id, ms_highest_key,
1171                              0.2);
1172         surge->setModulation(surge->storage.getPatch().scene[0].osc[0].p[0].id, ms_latest_key, 0.2);
1173         surge->setModulation(surge->storage.getPatch().scene[0].osc[0].p[1].id, ms_lowest_key, 0.2);
1174 
1175         surge->setModulation(surge->storage.getPatch().scene[1].osc[0].pitch.id, ms_highest_key,
1176                              0.2);
1177         surge->setModulation(surge->storage.getPatch().scene[1].osc[0].p[0].id, ms_latest_key, 0.2);
1178         surge->setModulation(surge->storage.getPatch().scene[1].osc[0].p[1].id, ms_lowest_key, 0.2);
1179 
1180         return surge;
1181     };
1182 
__anona43a01a11402(std::shared_ptr<SurgeSynthesizer> surge, int ch, int key, int vel) 1183     auto play = [](std::shared_ptr<SurgeSynthesizer> surge, int ch, int key, int vel) {
1184         if (vel == 0)
1185             surge->releaseNote(ch, key, vel);
1186         else
1187             surge->playNote(ch, key, vel, 0);
1188         for (int i = 0; i < 50; ++i)
1189             surge->process();
1190     };
1191     auto compval = [](std::shared_ptr<SurgeSynthesizer> surge, int sc, int low, int high,
__anona43a01a11502(std::shared_ptr<SurgeSynthesizer> surge, int sc, int low, int high, int latest) 1192                       int latest) {
1193         for (auto v : surge->voices[sc])
1194             if (v->state.gate)
1195             {
1196                 REQUIRE(v->modsources[ms_highest_key]->output * 12 + 60 == high);
1197                 REQUIRE(v->modsources[ms_latest_key]->output * 12 + 60 == latest);
1198                 REQUIRE(v->modsources[ms_lowest_key]->output * 12 + 60 == low);
1199             }
1200     };
1201     for (auto mpemode : {true, false})
1202         for (auto polymode : {pm_mono_st, pm_poly, pm_mono, pm_mono_fp, pm_mono_st, pm_mono_st_fp})
1203             for (auto priomode :
1204                  {NOTE_ON_LATEST_RETRIGGER_HIGHEST, ALWAYS_LOWEST, ALWAYS_HIGHEST, ALWAYS_LATEST})
1205             {
1206                 DYNAMIC_SECTION("DUAL " << priomode << " mpe=" << mpemode << " poly=" << polymode)
1207 
1208                 {
1209                     auto surge = setup(priomode, polymode);
1210                     surge->mpeEnabled = mpemode;
1211 
1212                     surge->storage.getPatch().scenemode.val.i = sm_dual;
1213 
1214                     int ch = 0;
1215                     if (mpemode)
1216                         ch = 1;
1217 
1218                     play(surge, ch, 60, 127);
1219                     compval(surge, 0, 60, 60, 60);
1220                     compval(surge, 1, 60, 60, 60);
1221 
1222                     if (mpemode)
1223                         ch++;
1224                     play(surge, ch, 70, 127);
1225                     compval(surge, 0, 60, 70, 70);
1226                     compval(surge, 1, 60, 70, 70);
1227 
1228                     if (mpemode)
1229                         ch++;
1230                     play(surge, ch, 65, 127);
1231                     compval(surge, 0, 60, 70, 65);
1232                     compval(surge, 1, 60, 70, 65);
1233                 }
1234 
1235                 DYNAMIC_SECTION("KeySplit " << priomode << " mpe=" << mpemode
1236                                             << " poly=" << polymode)
1237 
1238                 {
1239                     auto surge = setup(priomode, polymode);
1240                     surge->mpeEnabled = mpemode;
1241 
1242                     surge->storage.getPatch().scenemode.val.i = sm_split;
1243                     surge->storage.getPatch().splitpoint.val.i = 70;
1244 
1245                     int ch = 0;
1246                     if (mpemode)
1247                         ch = 1;
1248 
1249                     play(surge, ch, 50, 127);
1250                     compval(surge, 0, 50, 50, 50);
1251                     compval(surge, 1, 60, 60, 60);
1252 
1253                     if (mpemode)
1254                         ch++;
1255                     play(surge, ch, 90, 127);
1256                     compval(surge, 0, 50, 50, 50);
1257                     compval(surge, 1, 90, 90, 90);
1258 
1259                     if (mpemode)
1260                         ch++;
1261                     play(surge, ch, 69, 127);
1262                     compval(surge, 0, 50, 69, 69);
1263                     compval(surge, 1, 90, 90, 90);
1264 
1265                     if (mpemode)
1266                         ch++;
1267                     play(surge, ch, 70, 127);
1268                     compval(surge, 0, 50, 69, 69);
1269                     compval(surge, 1, 70, 90, 70);
1270                 }
1271 
1272                 DYNAMIC_SECTION("ChSplit " << priomode << " mpe=" << mpemode
1273                                            << " poly=" << polymode)
1274 
1275                 {
1276                     auto surge = setup(priomode, polymode);
1277                     surge->mpeEnabled = mpemode;
1278 
1279                     surge->storage.getPatch().scenemode.val.i = sm_chsplit;
1280                     surge->storage.getPatch().splitpoint.val.i = 64;
1281 
1282                     int cha = 0;
1283                     int chb = 10;
1284                     if (mpemode)
1285                         cha = 1;
1286 
1287                     play(surge, cha, 50, 127);
1288                     compval(surge, 0, 50, 50, 50);
1289                     compval(surge, 1, 60, 60, 60);
1290 
1291                     play(surge, chb, 90, 127);
1292                     compval(surge, 0, 50, 50, 50);
1293                     compval(surge, 1, 90, 90, 90);
1294 
1295                     play(surge, cha + (mpemode ? 1 : 0), 69, 127);
1296                     compval(surge, 0, 50, 69, 69);
1297                     compval(surge, 1, 90, 90, 90);
1298 
1299                     play(surge, chb + (mpemode ? 1 : 0), 70, 127);
1300                     compval(surge, 0, 50, 69, 69);
1301                     compval(surge, 1, 70, 90, 70);
1302                 }
1303             }
1304 }
1305 
1306 TEST_CASE("ModLFO is well behaved", "[mod]")
1307 {
1308     for (int m = Surge::ModControl::mod_sine; m <= Surge::ModControl::mod_square; ++m)
1309     {
1310         DYNAMIC_SECTION("Mod Control Boundsd for type " << m)
1311         {
1312             for (int tries = 0; tries < 10; ++tries)
1313             {
1314                 float rate = (float)rand() / (float)RAND_MAX * 10;
1315                 float phase_offset = 0.1 * tries;
1316                 float depth = 1.0;
1317 
1318                 INFO("200 Samples at " << rate << " with po " << phase_offset);
1319                 Surge::ModControl mc;
1320                 for (int s = 0; s < 200; ++s)
1321                 {
1322                     mc.pre_process(m, rate, depth, phase_offset);
1323                     REQUIRE(mc.value() <= 1.0);
1324                     REQUIRE(mc.value() >= -1.0);
1325                 }
1326             }
1327         }
1328     }
1329 }