diff --git a/src/common/SurgeStorage.h b/src/common/SurgeStorage.h index ae4a31b26b6..b0a05d3b2ac 100644 --- a/src/common/SurgeStorage.h +++ b/src/common/SurgeStorage.h @@ -608,9 +608,9 @@ class alignas(16) SurgeStorage void note_to_omega(float, float&, float&); bool retuneToScale(const Surge::Storage::Scale& s); - inline int scaleConstantNote() { return 48; } - inline float scaleConstantPitch() { return 16.0; } - inline float scaleConstantPitchInv() { return 0.0625; } // Obviously that's the inverse of the above + inline int scaleConstantNote() { return 60; } + inline float scaleConstantPitch() { return 32.0; } + inline float scaleConstantPitchInv() { return 0.03125; } // Obviously that's the inverse of the above Surge::Storage::Scale currentScale; bool isStandardTuning; diff --git a/src/headless/Player.cpp b/src/headless/Player.cpp index 2cb6a1268fb..4ebd11c2338 100644 --- a/src/headless/Player.cpp +++ b/src/headless/Player.cpp @@ -13,13 +13,18 @@ namespace Headless { playerEvents_t makeHoldMiddleC(int forSamples, int withTail) +{ + return makeHoldNoteFor(60, forSamples, withTail ); +} + +playerEvents_t makeHoldNoteFor( int note, int forSamples, int withTail ) { playerEvents_t result; Event on; on.type = Event::NOTE_ON; on.channel = 0; - on.data1 = 60; + on.data1 = note; on.data2 = 100; on.atSample = 0; @@ -160,6 +165,33 @@ void playOnEveryPatch( } } +void playOnNRandomPatches( + SurgeSynthesizer* surge, + const playerEvents_t& events, + int nPlays, + std::function cb) +{ + int nPresets = surge->storage.patch_list.size(); + int nCats = surge->storage.patch_category.size(); + + for (auto i = 0; i < nPlays; ++i) + { + int rp = (int)( 1.0 * rand() / RAND_MAX * ( surge->storage.patch_list.size() - 1 ) ); + Patch p = surge->storage.patch_list[rp]; + PatchCategory pc = surge->storage.patch_category[p.category]; + + float* data = NULL; + int nSamples, nChannels; + + playOnPatch(surge, i, events, &data, &nSamples, &nChannels); + cb(p, pc, data, nSamples, nChannels); + + if (data) + delete[] data; + } +} + void playMidiFile(SurgeSynthesizer* synth, std::string midiFileName, long callBackEvery, diff --git a/src/headless/Player.h b/src/headless/Player.h index a6b19a1b251..fcc2c97ace8 100644 --- a/src/headless/Player.h +++ b/src/headless/Player.h @@ -48,6 +48,8 @@ typedef std::vector playerEvents_t; // We assume these are monotonic in E */ playerEvents_t makeHoldMiddleC(int forSamples, int withTail = 0); +playerEvents_t makeHoldNoteFor(int note, int forSamples, int withTail = 0); + playerEvents_t make120BPMCMajorQuarterNoteScale(long sample0 = 0, int sr = 44100); /** @@ -87,6 +89,20 @@ void playOnEveryPatch( const Patch& p, const PatchCategory& c, const float* data, int nSamples, int nChannels)> completedCallback); +/** + * playOnEveryNRandomPatches + * + * Play the events on every patch Surge knows callign the callback for each one with + * the result. + */ +void playOnNRandomPatches( + SurgeSynthesizer* synth, + const playerEvents_t& events, + int nPlays, + std::function + completedCallback); + /** * playMidiFile * diff --git a/src/headless/UnitTests.cpp b/src/headless/UnitTests.cpp index a7f61045b11..f5d0d585bd8 100644 --- a/src/headless/UnitTests.cpp +++ b/src/headless/UnitTests.cpp @@ -80,21 +80,173 @@ TEST_CASE( "Retune Surge to .scl files", "[tun]" ) { Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/12-intune.scl" ); surge->storage.retuneToScale(s); - REQUIRE( n2f(48) == 16 ); - REQUIRE( n2f(48+12) == 16*2 ); - REQUIRE( n2f(48+12+12) == 16*4 ); - REQUIRE( n2f(48-12) == 16/2 ); + REQUIRE( n2f(surge->storage.scaleConstantNote()) == surge->storage.scaleConstantPitch() ); + REQUIRE( n2f(surge->storage.scaleConstantNote()+12) == surge->storage.scaleConstantPitch()*2 ); + REQUIRE( n2f(surge->storage.scaleConstantNote()+12+12) == surge->storage.scaleConstantPitch()*4 ); + REQUIRE( n2f(surge->storage.scaleConstantNote()-12) == surge->storage.scaleConstantPitch()/2 ); } SECTION( "Zeus 22" ) { Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/zeus22.scl" ); surge->storage.retuneToScale(s); - REQUIRE( n2f(48) == 16 ); - REQUIRE( n2f(48+s.count) == 16*2 ); - REQUIRE( n2f(48+2*s.count) == 16*4 ); - REQUIRE( n2f(48-s.count) == 16/2 ); + REQUIRE( n2f(surge->storage.scaleConstantNote()) == surge->storage.scaleConstantPitch() ); + REQUIRE( n2f(surge->storage.scaleConstantNote()+s.count) == surge->storage.scaleConstantPitch()*2 ); + REQUIRE( n2f(surge->storage.scaleConstantNote()+2*s.count) == surge->storage.scaleConstantPitch()*4 ); + REQUIRE( n2f(surge->storage.scaleConstantNote()-s.count) == surge->storage.scaleConstantPitch()/2 ); } + + SECTION( "6 exact" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/6-exact.scl" ); + surge->storage.retuneToScale(s); + REQUIRE( n2f(surge->storage.scaleConstantNote()) == surge->storage.scaleConstantPitch() ); + REQUIRE( n2f(surge->storage.scaleConstantNote()+s.count) == surge->storage.scaleConstantPitch()*2 ); + REQUIRE( n2f(surge->storage.scaleConstantNote()+2*s.count) == surge->storage.scaleConstantPitch()*4 ); + REQUIRE( n2f(surge->storage.scaleConstantNote()-s.count) == surge->storage.scaleConstantPitch()/2 ); + } +} + +/* +** Create a surge pointer on init sine +*/ +std::shared_ptr surgeOnSine() +{ + SurgeSynthesizer* surge = Surge::Headless::createSurge(44100); + + std::string otp = "Init Sine"; + bool foundInitSine = false; + for (int i = 0; i < surge->storage.patch_list.size(); ++i) + { + Patch p = surge->storage.patch_list[i]; + if (p.name == otp) + { + surge->loadPatch(i); + foundInitSine = true; + break; + } + } + if( ! foundInitSine ) + return nullptr; + else + return std::shared_ptr(surge); +} + +/* +** An approximation of the frequency of a signal using a simple zero crossing +** frequency measure (which works great for the sine patch and poorly for others +** At one day we could do this with autocorrelation instead but no need now. +*/ +double frequencyForNote( std::shared_ptr surge, int note ) +{ + auto events = Surge::Headless::makeHoldNoteFor( note, 44100 * 2, 64 ); + float *buffer; + int nS, nC; + Surge::Headless::playAsConfigured( surge.get(), events, &buffer, &nS, &nC ); + + REQUIRE( nC == 2 ); + REQUIRE( nS >= 44100 * 2 ); + REQUIRE( nS <= 44100 * 2 + 4 * BLOCK_SIZE ); + + // Trim off the leading and trailing + int nSTrim = (int)(nS / 2 * 0.8); + int start = (int)( nS / 2 * 0.05 ); + float *leftTrimmed = new float[nSTrim]; + + for( int i=0; i 0 ) + { + if( v > 0 ) + { + dSample += ( i - v ); + crosses ++; + } + v = i; + } + + float aSample = 1.f * dSample / crosses; + + float time = aSample / 44100.0; + float freq = 1.0 / time; + + + delete[] leftTrimmed; + delete[] buffer; + + return freq; +} + +TEST_CASE( "Notes at Appropriate Frequencies", "[tun]" ) +{ + auto surge = surgeOnSine(); + REQUIRE( surge.get() ); + + SECTION( "Untuned - so regular tuning" ) + { + auto f60 = frequencyForNote( surge, 60 ); + auto f72 = frequencyForNote( surge, 72 ); + auto f69 = frequencyForNote( surge, 69 ); + + REQUIRE( f60 == Approx( 261.63 ).margin( .1 ) ); + REQUIRE( f72 == Approx( 261.63 * 2 ).margin( .1 ) ); + REQUIRE( f69 == Approx( 440.0 ).margin( .1 ) ); + } + + SECTION( "Straight tuning scl file" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/12-intune.scl" ); + surge->storage.retuneToScale(s); + auto f60 = frequencyForNote( surge, 60 ); + auto f72 = frequencyForNote( surge, 72 ); + auto f69 = frequencyForNote( surge, 69 ); + + REQUIRE( f60 == Approx( 261.63 ).margin( .1 ) ); + REQUIRE( f72 == Approx( 261.63 * 2 ).margin( .1 ) ); + REQUIRE( f69 == Approx( 440.0 ).margin( .1 ) ); + + auto fPrior = f60; + auto twoToTwelth = pow( 2.0f, 1.0/12.0 ); + for( int i=61; i<72; ++i ) + { + auto fNow = frequencyForNote( surge, i ); + REQUIRE( fNow / fPrior == Approx( twoToTwelth ).margin( .0001 ) ); + fPrior = fNow; + } + } + + SECTION( "Zeus 22" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/zeus22.scl" ); + surge->storage.retuneToScale(s); + auto f60 = frequencyForNote( surge, 60 ); + auto fDouble = frequencyForNote( surge, 60 + s.count ); + auto fHalf = frequencyForNote( surge, 60 - s.count ); + + REQUIRE( f60 == Approx( 261.63 ).margin( .1 ) ); + REQUIRE( fDouble == Approx( 261.63 * 2 ).margin( .1 ) ); + REQUIRE( fHalf == Approx( 261.63 / 2 ).margin( .1 ) ); + } + + SECTION( "6 exact" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/6-exact.scl" ); + surge->storage.retuneToScale(s); + + auto f60 = frequencyForNote( surge, 60 ); + auto fDouble = frequencyForNote( surge, 60 + s.count ); + auto fHalf = frequencyForNote( surge, 60 - s.count ); + + REQUIRE( f60 == Approx( 261.63 ).margin( .1 ) ); + REQUIRE( fDouble == Approx( 261.63 * 2 ).margin( .1 ) ); + REQUIRE( fHalf == Approx( 261.63 / 2 ).margin( .1 ) ); + } + } TEST_CASE( "Simple Single Oscillator is Constant", "[dsp]" ) @@ -143,22 +295,70 @@ TEST_CASE( "Simple Single Oscillator is Constant", "[dsp]" ) } +TEST_CASE( "All Patches have Bounded Output", "[dsp]" ) +{ + SurgeSynthesizer* surge = Surge::Headless::createSurge(44100); + REQUIRE( surge ); + + Surge::Headless::playerEvents_t scale = + Surge::Headless::make120BPMCMajorQuarterNoteScale(0, 44100); + + auto callBack = [](const Patch& p, const PatchCategory& pc, const float* data, int nSamples, + int nChannels) -> void { + bool writeWav = false; // toggle this to true to write each sample to a wav file + REQUIRE( nSamples * nChannels > 0 ); + + if (nSamples * nChannels > 0) + { + const auto minmaxres = std::minmax_element(data, data + nSamples * nChannels); + auto mind = minmaxres.first; + auto maxd = minmaxres.second; + + float rms=0, L1=0; + for( int i=0; i= 0 ); + REQUIRE( *mind > -6 ); + REQUIRE( *mind <= 0 ); + + /* + std::cout << "cat/patch = " << pc.name << " / " << std::left << std::setw(30) << p.name; + std::cout << " range = [" << std::setw(10) + << std::fixed << *mind << ", " << std::setw(10) << std::fixed << *maxd << "]" + << " L1=" << L1 + << " rms=" << rms + << " samp=" << nSamples << " chan=" << nChannels << std::endl; + */ + + } + }; + + Surge::Headless::playOnNRandomPatches(surge, scale, 100, callBack); + delete surge; +} + TEST_CASE( "All Patches are Loadable", "[patch]" ) { -#if 0 - // FIXME - why doesn't this work? SurgeSynthesizer* surge = Surge::Headless::createSurge(44100); REQUIRE( surge ); int i=0; for( auto p : surge->storage.patch_list ) { - std::cout << i << " " << p.name << " " << p.path.generic_string() << std::endl; + // std::cout << i << " " << p.name << " " << p.path.generic_string() << std::endl; surge->loadPatch(i); ++i; } delete surge; -#endif } TEST_CASE( "lipol_ps class", "[dsp]" ) @@ -198,6 +398,8 @@ TEST_CASE( "lipol_ps class", "[dsp]" ) } + + int runAllTests(int argc, char **argv) { int result = Catch::Session().run( argc, argv ); diff --git a/test-data/scl/6-exact.scl b/test-data/scl/6-exact.scl new file mode 100644 index 00000000000..058a9baff78 --- /dev/null +++ b/test-data/scl/6-exact.scl @@ -0,0 +1,11 @@ +! 6 exact +! +HD2 06-12 - Harmonic division of 2: Harmonics 06-12 +6 +! +7/6 +4/3 +3/2 +5/3 +11/6 +2/1