From 454c455f5be8542d41bf5d8ce64e1ed0c3a3ef55 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 15 Jan 2020 18:29:34 -0500 Subject: [PATCH] Add KBM and MPE PitchBend DAW streaming (#1482) Stream both the KBM and the mpePitchBend into the "DAW Extra" chunk which we persist away from parameters. Test streaming both directions in the unit tests. MPE needs some menu item fixing to be fully done but is streamed internally. Along the way, refactor the unit tests into separate files so as not to be as unwieldy Addresses #1355 Addresses #1041 --- CMakeLists.txt | 7 +- src/common/SurgePatch.cpp | 42 + src/common/SurgeStorage.h | 7 + src/common/SurgeSynthesizer.h | 26 + src/headless/HeadlessUtils.cpp | 4 +- src/headless/HeadlessUtils.h | 2 +- src/headless/Player.cpp | 12 +- src/headless/Player.h | 12 +- src/headless/Stress.cpp | 96 -- src/headless/Stress.h | 26 - src/headless/UnitTestUtilities.cpp | 87 ++ src/headless/UnitTestUtilities.h | 24 + src/headless/UnitTests.cpp | 1471 +--------------------------- src/headless/UnitTestsDSP.cpp | 216 ++++ src/headless/UnitTestsIO.cpp | 217 ++++ src/headless/UnitTestsMIDI.cpp | 56 ++ src/headless/UnitTestsMOD.cpp | 493 ++++++++++ src/headless/UnitTestsTUN.cpp | 604 ++++++++++++ src/headless/main.cpp | 6 +- 19 files changed, 1796 insertions(+), 1612 deletions(-) delete mode 100644 src/headless/Stress.cpp delete mode 100644 src/headless/Stress.h create mode 100644 src/headless/UnitTestUtilities.cpp create mode 100644 src/headless/UnitTestUtilities.h create mode 100644 src/headless/UnitTestsDSP.cpp create mode 100644 src/headless/UnitTestsIO.cpp create mode 100644 src/headless/UnitTestsMIDI.cpp create mode 100644 src/headless/UnitTestsMOD.cpp create mode 100644 src/headless/UnitTestsTUN.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d66c3b18a56..9a94c129b15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,8 +76,13 @@ add_executable(surge-headless src/headless/LinkFixesHeadless.cpp src/headless/HeadlessUtils.cpp src/headless/Player.cpp - src/headless/Stress.cpp src/headless/UnitTests.cpp + src/headless/UnitTestUtilities.cpp + src/headless/UnitTestsDSP.cpp + src/headless/UnitTestsIO.cpp + src/headless/UnitTestsMIDI.cpp + src/headless/UnitTestsMOD.cpp + src/headless/UnitTestsTUN.cpp ) target_compile_features(surge-headless diff --git a/src/common/SurgePatch.cpp b/src/common/SurgePatch.cpp index 4cb9d3da494..94b38a830ea 100644 --- a/src/common/SurgePatch.cpp +++ b/src/common/SurgePatch.cpp @@ -1437,6 +1437,12 @@ void SurgePatch::load_xml(const void* data, int datasize, bool is_preset) p->QueryIntAttribute("v",&ival) == TIXML_SUCCESS) dawExtraState.mpeEnabled = ival; + p = TINYXML_SAFE_TO_ELEMENT(de->FirstChild("mpePitchBendRange")); + if( p && + p->QueryIntAttribute("v",&ival) == TIXML_SUCCESS) + dawExtraState.mpePitchBendRange = ival; + + p = TINYXML_SAFE_TO_ELEMENT(de->FirstChild("hasTuning")); if( p && p->QueryIntAttribute("v",&ival) == TIXML_SUCCESS) @@ -1456,6 +1462,24 @@ void SurgePatch::load_xml(const void* data, int datasize, bool is_preset) } } + p = TINYXML_SAFE_TO_ELEMENT(de->FirstChild("hasMapping")); + if( p && + p->QueryIntAttribute("v",&ival) == TIXML_SUCCESS) + { + dawExtraState.hasMapping = (ival != 0); + } + + if( dawExtraState.hasMapping ) + { + p = TINYXML_SAFE_TO_ELEMENT(de->FirstChild("mappingContents")); + if( p && + (td = p->Attribute("v") )) + { + auto tc = base64_decode(td); + dawExtraState.mappingContents = tc; + } + } + } } @@ -1672,6 +1696,10 @@ unsigned int SurgePatch::save_xml(void** data) // allocates mem, must be freed b mpe.SetAttribute("v", dawExtraState.mpeEnabled ? 1 : 0 ); dawExtraXML.InsertEndChild(mpe); + TiXmlElement mppb("mpePitchBendRange"); + mppb.SetAttribute("v", dawExtraState.mpePitchBendRange ); + dawExtraXML.InsertEndChild(mppb); + TiXmlElement tun("hasTuning"); tun.SetAttribute("v", dawExtraState.hasTuning ? 1 : 0 ); dawExtraXML.InsertEndChild(tun); @@ -1685,6 +1713,20 @@ unsigned int SurgePatch::save_xml(void** data) // allocates mem, must be freed b tnc.SetAttribute("v", base64_encode( (unsigned const char *)dawExtraState.tuningContents.c_str(), dawExtraState.tuningContents.size() ).c_str() ); dawExtraXML.InsertEndChild(tnc); + + TiXmlElement hmp("hasMapping"); + hmp.SetAttribute("v", dawExtraState.hasMapping ? 1 : 0 ); + dawExtraXML.InsertEndChild(hmp); + + /* + ** we really want a cdata here but TIXML is ambiguous whether + ** it does the right thing when I read the code, and is kinda crufty + ** so just protect ourselves with a base 64 encoding. + */ + TiXmlElement mpc("mappingContents"); + mpc.SetAttribute("v", base64_encode( (unsigned const char *)dawExtraState.mappingContents.c_str(), + dawExtraState.mappingContents.size() ).c_str() ); + dawExtraXML.InsertEndChild(mpc); } patch.InsertEndChild(dawExtraXML); diff --git a/src/common/SurgeStorage.h b/src/common/SurgeStorage.h index b225c98a3b4..0bb765fac82 100644 --- a/src/common/SurgeStorage.h +++ b/src/common/SurgeStorage.h @@ -408,8 +408,13 @@ struct DAWExtraStateStorage int instanceZoomFactor = -1; bool mpeEnabled = false; + int mpePitchBendRange = -1; + bool hasTuning = false; std::string tuningContents = ""; + + bool hasMapping = false; + std::string mappingContents = ""; }; @@ -608,6 +613,8 @@ class alignas(16) SurgeStorage void note_to_omega(float, float&, float&); bool retuneToScale(const Surge::Storage::Scale& s); + bool retuneToStandardTuning() { init_tables(); return true; } + bool remapToKeyboard(const Surge::Storage::KeyboardMapping &k); bool remapToStandardKeyboard(); inline int scaleConstantNote() { return currentMapping.tuningConstantNote; } diff --git a/src/common/SurgeSynthesizer.h b/src/common/SurgeSynthesizer.h index 85b5be77b5d..d53e237d154 100644 --- a/src/common/SurgeSynthesizer.h +++ b/src/common/SurgeSynthesizer.h @@ -176,21 +176,47 @@ class alignas(16) SurgeSynthesizer void populateDawExtraState() { storage.getPatch().dawExtraState.isPopulated = true; storage.getPatch().dawExtraState.mpeEnabled = mpeEnabled; + storage.getPatch().dawExtraState.mpePitchBendRange = mpePitchBendRange; + storage.getPatch().dawExtraState.hasTuning = !storage.isStandardTuning; if( ! storage.isStandardTuning ) storage.getPatch().dawExtraState.tuningContents = storage.currentScale.rawText; else storage.getPatch().dawExtraState.tuningContents = ""; + + storage.getPatch().dawExtraState.hasMapping = !storage.isStandardMapping; + if( ! storage.isStandardMapping ) + storage.getPatch().dawExtraState.mappingContents = storage.currentMapping.rawText; + else + storage.getPatch().dawExtraState.mappingContents = ""; } + void loadFromDawExtraState() { if( ! storage.getPatch().dawExtraState.isPopulated ) return; mpeEnabled = storage.getPatch().dawExtraState.mpeEnabled; + if( storage.getPatch().dawExtraState.mpePitchBendRange > 0 ) + mpePitchBendRange = storage.getPatch().dawExtraState.mpePitchBendRange; + if( storage.getPatch().dawExtraState.hasTuning ) { auto sc = Surge::Storage::parseSCLData(storage.getPatch().dawExtraState.tuningContents ); storage.retuneToScale(sc); } + else + { + storage.retuneToStandardTuning(); + } + + if( storage.getPatch().dawExtraState.hasMapping ) + { + auto kb = Surge::Storage::parseKBMData(storage.getPatch().dawExtraState.mappingContents ); + storage.remapToKeyboard(kb); + } + else + { + storage.remapToStandardKeyboard(); + } } public: diff --git a/src/headless/HeadlessUtils.cpp b/src/headless/HeadlessUtils.cpp index 7020010a013..028b24fe307 100644 --- a/src/headless/HeadlessUtils.cpp +++ b/src/headless/HeadlessUtils.cpp @@ -13,11 +13,11 @@ namespace Surge namespace Headless { static std::unique_ptr parent = nullptr; -SurgeSynthesizer* createSurge(int sr) +std::shared_ptr createSurge(int sr) { if (parent.get()==nullptr) parent.reset(new HeadlessPluginLayerProxy()); - SurgeSynthesizer* surge = new SurgeSynthesizer(parent.get()); + auto surge = std::shared_ptr(new SurgeSynthesizer( parent.get() ) ); surge->setSamplerate(sr); surge->time_data.tempo = 120; surge->time_data.ppqPos = 0; diff --git a/src/headless/HeadlessUtils.h b/src/headless/HeadlessUtils.h index 0c3c9f307e4..466075ec36b 100644 --- a/src/headless/HeadlessUtils.h +++ b/src/headless/HeadlessUtils.h @@ -11,7 +11,7 @@ namespace Surge namespace Headless { -SurgeSynthesizer* createSurge(int sr); +std::shared_ptr createSurge(int sr); void writeToStream(const float* data, int nSamples, int nChannels, std::ostream& str); void writeToWav(const float* data, int nSamples, int nChannels, float sampleRate, std::string wavFileName); diff --git a/src/headless/Player.cpp b/src/headless/Player.cpp index 41328fdd293..0874c0adae1 100644 --- a/src/headless/Player.cpp +++ b/src/headless/Player.cpp @@ -73,7 +73,7 @@ playerEvents_t make120BPMCMajorQuarterNoteScale(long s0, int sr) return result; } -void playAsConfigured(SurgeSynthesizer* surge, +void playAsConfigured(std::shared_ptr surge, const playerEvents_t& events, float** data, int* nSamples, @@ -122,7 +122,7 @@ void playAsConfigured(SurgeSynthesizer* surge, } } -void playOnPatch(SurgeSynthesizer* surge, +void playOnPatch(std::shared_ptr surge, int patch, const playerEvents_t& events, float** data, @@ -134,7 +134,7 @@ void playOnPatch(SurgeSynthesizer* surge, } void playOnEveryPatch( - SurgeSynthesizer* surge, + std::shared_ptr surge, const playerEvents_t& events, std::function cb) @@ -166,7 +166,7 @@ void playOnEveryPatch( } void playOnNRandomPatches( - SurgeSynthesizer* surge, + std::shared_ptr surge, const playerEvents_t& events, int nPlays, std::function synth, std::string midiFileName, long callBackEvery, std::function dataCB) @@ -262,7 +262,7 @@ void playMidiFile(SurgeSynthesizer* synth, #endif } -void renderMidiFileToWav(SurgeSynthesizer* surge, +void renderMidiFileToWav(std::shared_ptr surge, std::string midiFileName, std::string outputWavFile) { diff --git a/src/headless/Player.h b/src/headless/Player.h index 055bfdd2f85..ebcfc66107e 100644 --- a/src/headless/Player.h +++ b/src/headless/Player.h @@ -57,7 +57,7 @@ playerEvents_t make120BPMCMajorQuarterNoteScale(long sample0 = 0, int sr = 44100 * * given a surge, play the events from first to last accumulating the result in the audiodata */ -void playAsConfigured(SurgeSynthesizer* synth, +void playAsConfigured(std::shared_ptr synth, const playerEvents_t& events, float** resultData, int* nSamples, @@ -69,7 +69,7 @@ void playAsConfigured(SurgeSynthesizer* synth, * given a surge and a patch, play the events accumulating the data. This is a convenience * for loadpatch / playAsConfigured */ -void playOnPatch(SurgeSynthesizer* synth, +void playOnPatch(std::shared_ptr synth, int patch, const playerEvents_t& events, float** resultData, @@ -83,7 +83,7 @@ void playOnPatch(SurgeSynthesizer* synth, * the result. */ void playOnEveryPatch( - SurgeSynthesizer* synth, + std::shared_ptr synth, const playerEvents_t& events, std::function @@ -96,7 +96,7 @@ void playOnEveryPatch( * the result. */ void playOnNRandomPatches( - SurgeSynthesizer* synth, + std::shared_ptr synth, const playerEvents_t& events, int nPlays, std::function synth, std::string midiFileName, long callBackEvery, std::function dataCB); @@ -121,7 +121,7 @@ void playMidiFile(SurgeSynthesizer* synth, * Given a surge synthesizer and MidiFile name, create a Wav file which results * from playing that midi file. */ -void renderMidiFileToWav(SurgeSynthesizer* synth, +void renderMidiFileToWav(std::shared_ptr synth, std::string midiFileName, std::string outputWavFile); diff --git a/src/headless/Stress.cpp b/src/headless/Stress.cpp deleted file mode 100644 index 5f99fa8b6a8..00000000000 --- a/src/headless/Stress.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "Stress.h" -#include "Player.h" -#include -#include -#include -#include - -void Surge::Headless::pullInitSamplesWithNoNotes(int sampleCount) -{ - SurgeSynthesizer* synth = Surge::Headless::createSurge(44100); - Surge::Headless::playerEvents_t events; - Surge::Headless::Event on; - on.type = Surge::Headless::Event::NO_EVENT; - on.atSample = sampleCount; - events.push_back(on); - - float *data; - int nSamples, nChannels; - Surge::Headless::playAsConfigured(synth, events, &data, &nSamples, &nChannels); - - float sumAbs = 0; - for(int i=0;i distribution(0, synth->storage.patch_list.size() - 1); - delete synth; - - int faultCount = 0; - - playerEvents_t scale = make120BPMCMajorQuarterNoteScale(); - for (int i = 0; i < timesToTry && faultCount < 3; ++i) - { - SurgeSynthesizer* synth = Surge::Headless::createSurge(44100); - int patch = distribution(generator); - std::cout << std::setw(5) << i << ": Patch " << std::setw(5) << patch << " = " << std::setw(50) - << synth->storage.patch_list[patch].name; - synth->loadPatch(patch); - - float* data = nullptr; - int nSamples, nChannels; - Surge::Headless::playAsConfigured(synth, scale, &data, &nSamples, &nChannels); - - 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 +#include "SurgeSynthesizer.h" +#include "Player.h" +#include "catch2.hpp" + +namespace Surge +{ +namespace Test +{ +double frequencyForNote( std::shared_ptr surge, int note, + int seconds, bool audioChannel, + int midiChannel ) +{ + auto events = Surge::Headless::makeHoldNoteFor( note, 44100 * seconds, 64, midiChannel ); + float *buffer; + int nS, nC; + Surge::Headless::playAsConfigured( surge, events, &buffer, &nS, &nC ); + + REQUIRE( nC == 2 ); + REQUIRE( nS >= 44100 * seconds ); + REQUIRE( nS <= 44100 * seconds + 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; +} + +void copyScenedataSubset(SurgeStorage *storage, int scene, int start, int end) { + int s = storage->getPatch().scene_start[scene]; + for(int i=start; igetPatch().scenedata[scene][i-s].i = + storage->getPatch().param_ptr[i]->val.i; + } +} + +void setupStorageRanges(Parameter *start, Parameter *endIncluding, + int &storage_id_start, int &storage_id_end) { + int min_id = 100000, max_id = -1; + Parameter *oap = start; + while( oap <= endIncluding ) + { + if( oap->id >= 0 ) + { + if( oap->id > max_id ) max_id = oap->id; + if( oap->id < min_id ) min_id = oap->id; + } + oap++; + } + + storage_id_start = min_id; + storage_id_end = max_id + 1; +} + + + +} +} diff --git a/src/headless/UnitTestUtilities.h b/src/headless/UnitTestUtilities.h new file mode 100644 index 00000000000..ae9bf30f408 --- /dev/null +++ b/src/headless/UnitTestUtilities.h @@ -0,0 +1,24 @@ +// #include "catch2.hpp" // do NOT include this here since we want it included by +// the includer so we can set CATCH_CONFIG_RUNNER properly + +#include "SurgeSynthesizer.h" + +namespace Surge { +namespace Test { + +/* +** 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, + int seconds = 2, bool audioChannel = 0, + int midiChannel = 0 ); + +void copyScenedataSubset(SurgeStorage *storage, int scene, int start, int end); +void setupStorageRanges(Parameter *start, Parameter *endIncluding, + int &storage_id_start, int &storage_id_end); + + +} +} diff --git a/src/headless/UnitTests.cpp b/src/headless/UnitTests.cpp index 0a0ed03ea3e..4af388be8b1 100644 --- a/src/headless/UnitTests.cpp +++ b/src/headless/UnitTests.cpp @@ -1,1479 +1,10 @@ -#include -#include -#include -#include - -#include "HeadlessUtils.h" -#include "Player.h" -#include "Stress.h" -#include "SurgeError.h" #define CATCH_CONFIG_RUNNER #include "catch2.hpp" - -TEST_CASE( "We can read a collection of wavetables", "[wav]" ) -{ - /* - ** ToDo: - ** .wt file - ** oneshot - ** srgmarkers - ** etc - */ - SurgeSynthesizer* surge = Surge::Headless::createSurge(44100); - REQUIRE( surge ); - - SECTION( "Wavetable.wav" ) - { - auto wt = &(surge->storage.getPatch().scene[0].osc[0].wt); - surge->storage.load_wt_wav_portable("test-data/wav/Wavetable.wav", wt); - REQUIRE( wt->size == 2048 ); - REQUIRE( wt->n_tables == 256 ); - REQUIRE( ( wt->flags & wtf_is_sample ) == 0 ); - } - - SECTION( "05_BELL.WAV" ) - { - auto wt = &(surge->storage.getPatch().scene[0].osc[0].wt); - surge->storage.load_wt_wav_portable("test-data/wav/05_BELL.WAV", wt); - REQUIRE( wt->size == 2048 ); - REQUIRE( wt->n_tables == 33 ); - REQUIRE( ( wt->flags & wtf_is_sample ) == 0 ); - } - - SECTION( "pluckalgo.wav" ) - { - auto wt = &(surge->storage.getPatch().scene[0].osc[0].wt); - surge->storage.load_wt_wav_portable("test-data/wav/pluckalgo.wav", wt); - REQUIRE( wt->size == 2048 ); - REQUIRE( wt->n_tables == 9 ); - REQUIRE( ( wt->flags & wtf_is_sample ) == 0 ); - } - - delete surge; -} - -TEST_CASE( "All .wt and .wav factory assets load", "[wav]" ) -{ - SurgeSynthesizer* surge = Surge::Headless::createSurge(44100); - REQUIRE( surge ); - for( auto p : surge->storage.wt_list ) - { - auto wt = &(surge->storage.getPatch().scene[0].osc[0].wt); - wt->size = -1; - wt->n_tables = -1; - surge->storage.load_wt(p.path.generic_string(), wt ); - REQUIRE( wt->size > 0 ); - REQUIRE( wt->n_tables > 0 ); - } - - delete surge; -} - -TEST_CASE( "Retune Surge to .scl files", "[tun]" ) -{ - SurgeSynthesizer* surge = Surge::Headless::createSurge(44100); - - auto n2f = [surge](int n) { return surge->storage.note_to_pitch(n); }; - - //Surge::Storage::Scale s = Surge::Storage::readSCLFile("/Users/paul/dev/music/test_scl/Q4.scl" ); - SECTION( "12-intune SCL file" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/12-intune.scl" ); - surge->storage.retuneToScale(s); - 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(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. +** All moved to UnitTestsBLAH.cpp */ -double frequencyForNote( std::shared_ptr surge, int note, - int seconds = 2, bool audioChannel = 0, - int midiChannel = 0 ) -{ - auto events = Surge::Headless::makeHoldNoteFor( note, 44100 * seconds, 64, midiChannel ); - float *buffer; - int nS, nC; - Surge::Headless::playAsConfigured( surge.get(), events, &buffer, &nS, &nC ); - - REQUIRE( nC == 2 ); - REQUIRE( nS >= 44100 * seconds ); - REQUIRE( nS <= 44100 * seconds + 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( "KBM File Parsing", "[tun]" ) -{ - SECTION( "Default Keyboard is Default" ) - { - auto k = Surge::Storage::KeyboardMapping(); - REQUIRE( k.isValid ); - REQUIRE( k.isStandardMapping ); - REQUIRE( k.count == 12 ); - REQUIRE( k.firstMidi == 0 ); - REQUIRE( k.lastMidi == 127 ); - REQUIRE( k.middleNote == 60 ); - REQUIRE( k.tuningConstantNote == 60 ); - REQUIRE( k.tuningFrequency == Approx( 261.62558 ) ); - REQUIRE( k.octaveDegrees == 12 ); - for( auto i=0; i values = { 0, 1, -1, 2, -1, 3, 4, -1, 5, -1, 6 }; - for( int i=0; istorage.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( 448.2 ).margin( .1 ) ); - unmapped[0] = f60; - unmapped[1] = f72; - unmapped[2] = f69; - } - - SECTION( "And remap to 440" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/marvel12.scl" ); - auto k = Surge::Storage::readKBMFile( "test-data/scl/mapping-a440-constant.kbm" ); - - surge->storage.retuneToScale(s); - surge->storage.remapToKeyboard(k); - - auto f60 = frequencyForNote( surge, 60 ); - auto f72 = frequencyForNote( surge, 72 ); - auto f69 = frequencyForNote( surge, 69 ); - REQUIRE( f69 == Approx( 440.0 ).margin(.1) ); - REQUIRE( unmapped[2]/440.0 == Approx( unmapped[0] / f60 ).margin(.001) ); - REQUIRE( unmapped[2]/440.0 == Approx( unmapped[1] / f72 ).margin(.001) ); - } - - // and back and then back again - SECTION( "Can Map and ReMap consistently" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/marvel12.scl" ); - auto k440 = Surge::Storage::readKBMFile( "test-data/scl/mapping-a440-constant.kbm" ); - - surge->storage.retuneToScale(s); - surge->storage.remapToStandardKeyboard(); - - auto f60std = frequencyForNote( surge, 60 ); - auto f69std = frequencyForNote( surge, 69 ); - - surge->storage.remapToKeyboard( k440 ); - auto f60map = frequencyForNote( surge, 60 ); - auto f69map = frequencyForNote( surge, 69 ); - - REQUIRE( f60std == Approx( 261.63 ).margin(0.1) ); - REQUIRE( f69map == Approx( 440.0 ).margin(0.1) ); - REQUIRE( f69std/f60std == Approx( f69map/f60map ).margin(.001 ) ); - - for( int i=0; i<50; ++i ) - { - auto fr = 1.0f * rand() / RAND_MAX; - if( fr > 0 ) - { - surge->storage.remapToKeyboard(k440); - auto f60 = frequencyForNote( surge, 60 ); - auto f69 = frequencyForNote( surge, 69 ); - REQUIRE( f60 == f60map ); - REQUIRE( f69 == f69map ); - } - else - { - surge->storage.remapToStandardKeyboard(); - auto f60 = frequencyForNote( surge, 60 ); - auto f69 = frequencyForNote( surge, 69 ); - REQUIRE( f60 == f60std ); - REQUIRE( f69 == f69std ); - } - } - } - - SECTION( "Scale Ratio is Unch" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/marvel12.scl" ); - auto k440 = Surge::Storage::readKBMFile( "test-data/scl/mapping-a440-constant.kbm" ); - - surge->storage.retuneToScale(s); - surge->storage.remapToStandardKeyboard(); - auto f60 = frequencyForNote( surge, 60 ); - REQUIRE( f60 == Approx( 261.63 ).margin( .1 ) ); - - std::vector ratios; - for( int i=61; i<72; ++i ) - ratios.push_back( frequencyForNote( surge, i ) / f60 ); - - surge->storage.remapToStandardKeyboard(); - auto f60map = frequencyForNote( surge, 60 ); - for( int i=61; i<72; ++i ) - { - auto fi = frequencyForNote( surge, i ); - REQUIRE( fi / f60map == Approx( ratios[i-61] ).margin( 0.001 ) ); - } - } -} - -TEST_CASE( "Non-uniform keyboard mapping", "[tun]" ) -{ - auto surge = surgeOnSine(); - REQUIRE( surge.get() ); - - auto mt = [](float c) { - auto t = Surge::Storage::Tone(); - t.type = Surge::Storage::Tone::kToneCents; - t.cents = c; - t.floatValue = c / 1200.0 + 1.0; - return t; - }; - // This is the "white keys" scale - Surge::Storage::Scale s; - s.count = 7; - s.tones.push_back( mt( 200 ) ); - s.tones.push_back( mt( 400 ) ); - s.tones.push_back( mt( 500 ) ); - s.tones.push_back( mt( 700 ) ); - s.tones.push_back( mt( 900 ) ); - s.tones.push_back( mt( 1100 ) ); - s.tones.push_back( mt( 1200 ) ); - - Surge::Storage::Scale sWonky; - sWonky.count = 7; - sWonky.tones.push_back( mt( 220 ) ); - sWonky.tones.push_back( mt( 390 ) ); - sWonky.tones.push_back( mt( 517 ) ); - sWonky.tones.push_back( mt( 682 ) ); - sWonky.tones.push_back( mt( 941 ) ); - sWonky.tones.push_back( mt( 1141 ) ); - sWonky.tones.push_back( mt( 1200 ) ); - - SECTION( "7 Note Scale" ) - { - std::vector frequencies; - // When I map it directly I get 440 at note 65 (since I skipped 4 black keys) - surge->storage.retuneToScale(s); - for( int i=0; i<8; ++i ) - { - frequencies.push_back( frequencyForNote( surge, 60+i ) ); - } - REQUIRE( frequencies[0] == Approx( 261.63 ).margin( 0.1 ) ); - REQUIRE( frequencies[5] == Approx( 440.00 ).margin( 0.1 ) ); - - auto k = Surge::Storage::readKBMFile( "test-data/scl/mapping-whitekeys-c261.kbm" ); - REQUIRE( ! k.isStandardMapping ); - REQUIRE( k.count == 12 ); - REQUIRE( k.octaveDegrees == 7 ); - surge->storage.remapToKeyboard(k); - auto f60 = frequencyForNote( surge, 60 ); - auto f69 = frequencyForNote( surge, 69 ); - - REQUIRE( f60 == Approx( 261.63 ).margin( 0.1 ) ); - REQUIRE( f69 == Approx( 440 ).margin( 0.1 ) ); - - // If we have remapped to white keys this will be true - REQUIRE( frequencyForNote( surge, 60 ) == Approx( frequencies[0] ).margin(0.1) ); - REQUIRE( frequencyForNote( surge, 62 ) == Approx( frequencies[1] ).margin(0.1) ); - REQUIRE( frequencyForNote( surge, 64 ) == Approx( frequencies[2] ).margin(0.1) ); - REQUIRE( frequencyForNote( surge, 65 ) == Approx( frequencies[3] ).margin(0.1) ); - REQUIRE( frequencyForNote( surge, 67 ) == Approx( frequencies[4] ).margin(0.1) ); - REQUIRE( frequencyForNote( surge, 69 ) == Approx( frequencies[5] ).margin(0.1) ); - REQUIRE( frequencyForNote( surge, 71 ) == Approx( frequencies[6] ).margin(0.1) ); - } - - SECTION( "7 Note Scale with Tuning Centers" ) - { - auto k261 = Surge::Storage::readKBMFile( "test-data/scl/mapping-whitekeys-c261.kbm" ); - auto k440 = Surge::Storage::readKBMFile( "test-data/scl/mapping-whitekeys-a440.kbm" ); - REQUIRE( ! k261.isStandardMapping ); - REQUIRE( ! k440.isStandardMapping ); - - surge->storage.retuneToScale(sWonky); - surge->storage.remapToKeyboard(k261); - auto f60 = frequencyForNote( surge, 60 ); - auto f69 = frequencyForNote( surge, 69 ); - - REQUIRE( f60 == Approx( 261.63 ).margin( 0.1 ) ); - REQUIRE( f69 != Approx( 440.0 ).margin( 0.1 ) ); - - surge->storage.retuneToScale(sWonky); - surge->storage.remapToKeyboard(k440); - auto f60_440 = frequencyForNote( surge, 60 ); - auto f69_440 = frequencyForNote( surge, 69 ); - - REQUIRE( f60_440 != Approx( 261.63 ).margin( 0.1 ) ); - REQUIRE( f69_440 == Approx( 440.0 ).margin( 0.1 ) ); - - REQUIRE( f69_440/f60_440 == Approx( f69/f60 ).margin( 0.001 ) ); - } -} - -TEST_CASE( "Zero Size Maps", "[tun]" ) -{ - auto surge = surgeOnSine(); - REQUIRE( surge.get() ); - - SECTION( "Note 61" ) - { - auto f60std = frequencyForNote( surge, 60 ); - auto f61std = frequencyForNote( surge, 61 ); - REQUIRE( f60std == Approx( 261.63 ).margin( 0.1 ) ); - - auto k61 = Surge::Storage::readKBMFile( "test-data/scl/empty-note61.kbm" ); - REQUIRE( !k61.isStandardMapping ); - REQUIRE( k61.count == 0 ); - surge->storage.remapToKeyboard( k61 ); - - auto f60map = frequencyForNote( surge, 60 ); - auto f61map = frequencyForNote( surge, 61 ); - REQUIRE( frequencyForNote( surge, 61 ) == Approx( 280 ).margin( 0.1 ) ); - REQUIRE( f61std / f60std == Approx( f61map / f60map ).margin( 0.001 ) ); - } - - SECTION( "Note 69" ) - { - auto f60std = frequencyForNote( surge, 60 ); - auto f69std = frequencyForNote( surge, 69 ); - REQUIRE( f60std == Approx( 261.63 ).margin( 0.1 ) ); - REQUIRE( f69std == Approx( 440.0 ).margin( 0.1 ) ); - - auto k69 = Surge::Storage::readKBMFile( "test-data/scl/empty-note69.kbm" ); - REQUIRE( !k69.isStandardMapping ); - REQUIRE( k69.count == 0 ); - surge->storage.remapToKeyboard( k69 ); - - auto f60map = frequencyForNote( surge, 60 ); - auto f69map = frequencyForNote( surge, 69 ); - REQUIRE( frequencyForNote( surge, 69 ) == Approx( 452 ).margin( 0.1 ) ); - REQUIRE( f69std / f60std == Approx( f69map / f60map ).margin( 0.001 ) ); - } - - SECTION( "Note 69 with Tuning" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/marvel12.scl" ); - surge->storage.retuneToScale(s); - auto f60std = frequencyForNote( surge, 60 ); - auto f69std = frequencyForNote( surge, 69 ); - REQUIRE( f60std == Approx( 261.63 ).margin( 0.1 ) ); - REQUIRE( f69std != Approx( 440.0 ).margin( 0.1 ) ); - - - auto k69 = Surge::Storage::readKBMFile( "test-data/scl/empty-note69.kbm" ); - REQUIRE( !k69.isStandardMapping ); - REQUIRE( k69.count == 0 ); - surge->storage.remapToKeyboard( k69 ); - - auto f60map = frequencyForNote( surge, 60 ); - auto f69map = frequencyForNote( surge, 69 ); - REQUIRE( frequencyForNote( surge, 69 ) == Approx( 452 ).margin( 0.1 ) ); - REQUIRE( f69std / f60std == Approx( f69map / f60map ).margin( 0.001 ) ); - } - -} - -TEST_CASE( "An Octave is an Octave", "[tun]" ) -{ - auto surge = surgeOnSine(); - REQUIRE( surge.get() ); - - SECTION( "Untuned OSC Octave" ) - { - auto f60 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = -1; - auto f60m1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = 1; - auto f60p1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = 0; - auto f60z = frequencyForNote(surge, 60); - REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); - } - - SECTION( "Untuned Scene Octave" ) - { - auto f60 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = -1; - auto f60m1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = 1; - auto f60p1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = 0; - auto f60z = frequencyForNote(surge, 60); - REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); - } - - SECTION( "Tuned to 12 OSC octave" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/12-intune.scl" ); - surge->storage.retuneToScale(s); - - auto f60 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = -1; - auto f60m1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = 1; - auto f60p1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = 0; - auto f60z = frequencyForNote(surge, 60); - REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); - } - - - SECTION( "Tuned to 12 Scene Octave" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/12-intune.scl" ); - surge->storage.retuneToScale(s); - - auto f60 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = -1; - auto f60m1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = 1; - auto f60p1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = 0; - auto f60z = frequencyForNote(surge, 60); - REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); - } - - SECTION( "22 note scale OSC Octave" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/zeus22.scl" ); - surge->storage.retuneToScale(s); - REQUIRE( s.count == 22 ); - - auto f60 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = -1; - auto f60m1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = 1; - auto f60p1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = 0; - auto f60z = frequencyForNote(surge, 60); - REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); - } - - SECTION( "22 note scale Scene Octave" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/zeus22.scl" ); - surge->storage.retuneToScale(s); - REQUIRE( s.count == 22 ); - - auto f60 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = -1; - auto f60m1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = 1; - auto f60p1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = 0; - auto f60z = frequencyForNote(surge, 60); - REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); - } - - SECTION( "6 note scale OSC Octave" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/6-exact.scl" ); - surge->storage.retuneToScale(s); - REQUIRE( s.count == 6 ); - - auto f60 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = -1; - auto f60m1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = 1; - auto f60p1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].osc[0].octave.val.i = 0; - auto f60z = frequencyForNote(surge, 60); - REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); - } - - SECTION( "6 note scale Scene Octave" ) - { - Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/6-exact.scl" ); - surge->storage.retuneToScale(s); - REQUIRE( s.count == 6 ); - - auto f60 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = -1; - auto f60m1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = 1; - auto f60p1 = frequencyForNote(surge, 60); - surge->storage.getPatch().scene[0].octave.val.i = 0; - auto f60z = frequencyForNote(surge, 60); - REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); - REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); - } -} - -TEST_CASE( "Simple Single Oscillator is Constant", "[dsp]" ) -{ - SurgeSynthesizer* surge = Surge::Headless::createSurge(44100); - REQUIRE( surge ); - // surge->storage.getPatch().scene[0].osc[0].type.val.i = ot_sinus; - - int len = 4410 * 5; - //int len = BLOCK_SIZE * 20; - Surge::Headless::playerEvents_t heldC = Surge::Headless::makeHoldMiddleC(len); - REQUIRE( heldC.size() == 2 ); - - float* data = NULL; - int nSamples, nChannels; - - Surge::Headless::playAsConfigured(surge, heldC, &data, &nSamples, &nChannels); - REQUIRE( data ); - REQUIRE( std::abs( nSamples - len ) <= BLOCK_SIZE ); - REQUIRE( nChannels == 2 ); - - float rms = 0; - for( int i=0; i 0.1 ); - REQUIRE( rms < 0.101 ); - - - int zeroCrossings = 0; - for( int i=0; i 0 && data[i+2] < 0 ) - zeroCrossings ++; - } - // Somewhere in here - REQUIRE( zeroCrossings > 130 ); - REQUIRE( zeroCrossings < 160 ); - - if (data) - delete[] data; - delete surge; - -} - -TEST_CASE( "Unison Absolute and Relative", "[osc]" ) -{ - auto surge = std::shared_ptr( Surge::Headless::createSurge(44100) ); - REQUIRE( surge ); - - auto assertRelative = [surge](const char* pn) { - REQUIRE( surge->loadPatchByPath( pn, -1, "Test" ) ); - auto f60_0 = frequencyForNote( surge, 60, 5, 0 ); - auto f60_1 = frequencyForNote( surge, 60, 5, 1 ); - - auto f60_avg = 0.5 * ( f60_0 + f60_1 ); - - auto f72_0 = frequencyForNote( surge, 72, 5, 0 ); - auto f72_1 = frequencyForNote( surge, 72, 5, 1 ); - auto f72_avg = 0.5 * ( f72_0 + f72_1 ); - - // In relative mode, the average frequencies should double, as should the individual outliers - REQUIRE( f72_avg / f60_avg == Approx( 2 ).margin( 0.01 ) ); - REQUIRE( f72_0 / f60_0 == Approx( 2 ).margin( 0.01 ) ); - REQUIRE( f72_1 / f60_1 == Approx( 2 ).margin( 0.01 ) ); - }; - - auto assertAbsolute = [surge](const char* pn, bool print = false) { - REQUIRE( surge->loadPatchByPath( pn, -1, "Test" ) ); - auto f60_0 = frequencyForNote( surge, 60, 5, 0 ); - auto f60_1 = frequencyForNote( surge, 60, 5, 1 ); - - auto f60_avg = 0.5 * ( f60_0 + f60_1 ); - - auto f72_0 = frequencyForNote( surge, 72, 5, 0 ); - auto f72_1 = frequencyForNote( surge, 72, 5, 1 ); - auto f72_avg = 0.5 * ( f72_0 + f72_1 ); - - // In absolute mode, the average frequencies should double, but the channels should have constant difference - REQUIRE( f72_avg / f60_avg == Approx( 2 ).margin( 0.01 ) ); - REQUIRE( ( f72_0 - f72_1 ) / ( f60_0 - f60_1 ) == Approx( 1 ).margin( 0.01 ) ); - if( print ) - { - std::cout << "F60 " << f60_avg << " " << f60_0 << " " << f60_1 << " " << f60_0 - f60_1 << std::endl; - std::cout << "F72 " << f72_avg << " " << f72_0 << " " << f72_1 << " " << f60_0 - f60_1 << std::endl; - } - }; - - SECTION( "Wavetable Oscillator" ) - { - assertRelative("test-data/patches/Wavetable-Sin-Uni2-Relative.fxp"); - assertAbsolute("test-data/patches/Wavetable-Sin-Uni2-Absolute.fxp"); - } - - SECTION( "Window Oscillator" ) - { - assertRelative("test-data/patches/Window-Sin-Uni2-Relative.fxp"); - assertAbsolute("test-data/patches/Window-Sin-Uni2-Absolute.fxp"); - } - - SECTION( "Classic Oscillator" ) - { - assertRelative("test-data/patches/Classic-Uni2-Relative.fxp"); - assertAbsolute("test-data/patches/Classic-Uni2-Absolute.fxp"); - } - - SECTION( "SH Oscillator" ) - { - assertRelative("test-data/patches/SH-Uni2-Relative.fxp"); - assertAbsolute("test-data/patches/SH-Uni2-Absolute.fxp"); - } - -} - -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]" ) -{ - 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; - surge->loadPatch(i); - ++i; - } - - delete surge; -} - -TEST_CASE( "lipol_ps class", "[dsp]" ) -{ - lipol_ps mypol; - float prevtarget = -1.0; - mypol.set_target(prevtarget); - mypol.instantize(); - - constexpr size_t nfloat = 64; - constexpr size_t nfloat_quad = 16; - float storeTarget alignas(16)[nfloat]; - mypol.store_block(storeTarget, nfloat_quad); - - for( auto i=0; igetPatch().scene_start[scene]; - for(int i=start; igetPatch().scenedata[scene][i-s].i = - storage->getPatch().param_ptr[i]->val.i; - } -} - -void setupStorageRanges(Parameter *start, Parameter *endIncluding, - int &storage_id_start, int &storage_id_end) { - int min_id = 100000, max_id = -1; - Parameter *oap = start; - while( oap <= endIncluding ) - { - if( oap->id >= 0 ) - { - if( oap->id > max_id ) max_id = oap->id; - if( oap->id < min_id ) min_id = oap->id; - } - oap++; - } - - storage_id_start = min_id; - storage_id_end = max_id + 1; -} - -TEST_CASE( "ADSR Envelope Behaviour", "[mod]" ) -{ - - std::shared_ptr surge( Surge::Headless::createSurge(44100) ); - REQUIRE( surge.get() ); - - /* - ** OK so lets set up a pretty simple setup - */ - - auto runAdsr = [surge](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 ) - { - auto* adsrstorage = &(surge->storage.getPatch().scene[0].adsr[0]); - std::shared_ptr adsr( new AdsrEnvelope() ); - adsr->init( &(surge->storage), adsrstorage, surge->storage.getPatch().scenedata[0], nullptr ); - REQUIRE( adsr.get() ); - - int ids, ide; - setupStorageRanges(&(adsrstorage->a), &(adsrstorage->mode), ids, ide ); - REQUIRE( ide > ids ); - REQUIRE( ide >= 0 ); - REQUIRE( ids >= 0 ); - - auto svn = [](Parameter *p, float vn) - { - p->set_value_f01( p->value_to_normalized( limit_range( vn, p->val_min.f, p->val_max.f ) ) ); - }; - - auto inverseEnvtime = [](float desiredTime) - { - // 2^x = desired time - auto x = log(desiredTime)/log(2.0); - return x; - }; - - svn(&(adsrstorage->a), inverseEnvtime(a)); - svn(&(adsrstorage->d), inverseEnvtime(d)); - svn(&(adsrstorage->s), s); - svn(&(adsrstorage->r), inverseEnvtime(r)); - - svn(&(adsrstorage->a_s), a_s); - svn(&(adsrstorage->d_s), d_s); - svn(&(adsrstorage->r_s), r_s); - - adsrstorage->mode.val.b = isAnalog; - - copyScenedataSubset(&(surge->storage), 0, ids, ide); - adsr->attack(); - - bool released = false; - bool pushSus = false; - int i = 0; - std::vector> res; - res.push_back(std::make_pair(0.f, 0.f)); - - while( true ) - { - auto t = 1.0 * (i+1) * BLOCK_SIZE * dsamplerate_inv; - i++; - if( t > runUntil || runUntil < 0 ) - break; - - if( t > releaseAfter && ! released ) - { - adsr->release(); - released = true; - } - - if( pushSusAt > 0 && ! pushSus && t > pushSusAt ) - { - pushSus = true; - svn(&(adsrstorage->s), pushSusTo); - copyScenedataSubset(&(surge->storage), 0, ids, ide); - } - - adsr->process_block(); - res.push_back( std::make_pair( (float)t, adsr->output ) ); - if( false && i > 270 && i < 290 ) - std::cout << i << " " << t << " " << adsr->output << " " << adsr->getEnvState() - << std::endl; - } - return res; - }; - - auto detectTurnarounds = [](std::vector> data) { - auto pv = -1000.0; - int dir = 1; - std::vector> turns; - turns.push_back( std::make_pair( 0, 1 ) ); - for( auto &p : data ) - { - auto t = p.first; - auto v = p.second; - if( pv >= 0 ) - { - int ldir = 0; - if( v > 0.999999f ) ldir = dir; // sometimes we get a double '1' - if( fabs( v - pv ) < 5e-6 && fabs( v ) < 1e-5) ldir = 0; // bouncing off of 0 is annoying - else if( fabs( v - pv ) < 5e-7 ) ldir = 0; - else if( v > pv ) ldir = 1; - else ldir = -1; - - if( v != 1 ) - { - if( ldir != dir ) - { - turns.push_back(std::make_pair(t, ldir) ); - } - dir = ldir; - } - } - pv = v; - } - return turns; - }; - - // With 0 sustain I should decay in decay time - auto runCompare = [&](float a, float d, float s, float r, int a_s, int d_s, int r_s, bool isAnalog ) - { - float sustime = 0.1; - float endtime = 0.1; - float totaltime = a + d + sustime + r + endtime; - - auto simple = runAdsr( a, d, s, r, a_s, d_s, r_s, isAnalog, a + d + sustime, totaltime ); - auto sturns = detectTurnarounds(simple); - if( false ) - std::cout << "ADSR: " << a << " " << d << " " << s << " " << r << " switches: " << a_s << " " << d_s << " " << r_s << std::endl; - if( s == 0 ) - { - if( sturns.size() != 3 ) - { - for( auto s : simple ) - std::cout << s.first << " " << s.second << std::endl; - for( auto s : sturns ) - std::cout << s.first << " " << s.second << std::endl; - } - REQUIRE( sturns.size() == 3 ); - REQUIRE( sturns[0].first == 0 ); - REQUIRE( sturns[0].second == 1 ); - REQUIRE( sturns[1].first == Approx( a ).margin( 0.01 ) ); - REQUIRE( sturns[1].second == -1 ); - REQUIRE( sturns[2].first == Approx( a + d ).margin( 0.01 ) ); - REQUIRE( sturns[2].second == 0 ); - } - else - { - if( sturns.size() != 5 ) - { - for( auto s : simple ) - std::cout << s.first << " " << s.second << std::endl; - for( auto s : sturns ) - std::cout << s.first << " " << s.second << std::endl; - } - REQUIRE( sturns.size() == 5 ); - REQUIRE( sturns[0].first == 0 ); - REQUIRE( sturns[0].second == 1 ); - REQUIRE( sturns[1].first == Approx( a ).margin( 0.01 ) ); - REQUIRE( sturns[1].second == -1 ); - if( d_s == 0 ) - { - // this equality only holds in the linear case; in the polynomial case you get faster reach to non-zero sustain - REQUIRE( sturns[2].first == Approx( a + d * ( 1.0 - s ) ).margin( 0.01 ) ); - } - else if( a + d * ( 1.0 - s ) > 0.1 && d > 0.05 ) - { - REQUIRE( sturns[2].first < a + d * ( 1.0 - s ) + 0.01 ); - } - REQUIRE( sturns[2].second == 0 ); - REQUIRE( sturns[3].first == Approx( a + d + sustime ).margin( 0.01 ) ); - REQUIRE( sturns[3].second == -1 ); - if( r_s == 0 || s > 0.1 && r > 0.05 ) // if we are in the non-linear releases at low sustain we get there early - { - REQUIRE( sturns[4].first == Approx( a + d + sustime + r ).margin( ( r_s == 0 ? 0.01 : ( r * 0.1 ) ) ) ); - REQUIRE( sturns[4].second == 0 ); - } - } - }; - - SECTION( "Test the Digital Envelope" ) - { - for( int as=0;as<3;++as ) - for( int ds=0; ds<3; ++ds ) - for( int rs=0; rs<3; ++rs ) - { - runCompare( 0.2, 0.3, 0.0, 0.1, as, ds, rs, false ); - runCompare( 0.2, 0.3, 0.5, 0.1, as, ds, rs, false ); - - for( int rc=0;rc<10; ++rc ) - { - auto a = rand() * 1.0 / RAND_MAX; - auto d = rand() * 1.0 / RAND_MAX; - auto s = 0.8 * rand() * 1.0 / RAND_MAX + 0.1; // we have tested the s=0 case above - auto r = rand() * 1.0 / RAND_MAX; - runCompare( a, d, s, r, as, ds, rs, false ); - } - } - } - - SECTION( "Test the Analog Envelope" ) - { - // OK so we can't check the same thing here since the turns aren't as tight in analog mode - // Also the analog ADSR sustains at half the given sustain. - auto testAnalog = [&](float a, float d, float s, float r ) - { - INFO( "ANALOG " << a << " " << d << " " << s << " " << r ); - auto holdFor = a + d + d + 0.5; - - auto ae = runAdsr( a, d, s, r, 0, 0, 0, true, holdFor, holdFor + 4 * r ); - auto aturns = detectTurnarounds(ae); - - float maxt=0, maxv=0; - float zerot=0; - float valAtRelEnd = -1; - std::vector heldPeriod; - for( auto obs : ae ) - { - //std::cout << obs.first << " " << obs.second << std::endl; - - if( obs.first > a + d + d * 0.95 && obs.first < holdFor && s > 0.05 ) // that 0.1 lets the delay ring off - { - REQUIRE( obs.second == Approx( s * s ).margin( 1e-3 ) ); - heldPeriod.push_back(obs.second); - } - if( obs.first > a + d && obs.second < 5e-5 && zerot == 0 ) - zerot = obs.first; - if( obs.first > holdFor + r && valAtRelEnd < 0 ) - valAtRelEnd = obs.second; - - if( obs.second > maxv ) - { - maxv = obs.second; - maxt = obs.first; - } - } - - // In the held period are we mostly constant - if( heldPeriod.size() > 10 ) - { - float sum = 0; - for( auto p : heldPeriod ) - sum += p; - float mean = sum / heldPeriod.size(); - float var = 0; - for( auto p : heldPeriod ) - var += ( p - mean ) * ( p - mean ); - var /= ( heldPeriod.size() - 1 ); - float stddev = sqrt( var ); - REQUIRE( stddev < d * 5e-3 ); - } - REQUIRE( maxt < a ); - REQUIRE( maxv > 0.99 ); - if( s > 0.05 ) - { - REQUIRE( zerot > holdFor + r * 0.9 ); - REQUIRE( valAtRelEnd < s * 0.025 ); - } - }; - - testAnalog( 0.1, 0.2, 0.5, 0.1 ); - testAnalog( 0.1, 0.2, 0.0, 0.1 ); - for( int rc=0;rc<50; ++rc ) - { - auto a = rand() * 1.0 / RAND_MAX + 0.03; - auto d = rand() * 1.0 / RAND_MAX + 0.03; - auto s = 0.7 * rand() * 1.0 / RAND_MAX + 0.2; // we have tested the s=0 case above - auto r = rand() * 1.0 / RAND_MAX + 0.03; - testAnalog( a, d, s, r); - } - - } - - // This is just a rudiemntary little test of this in digital mode - SECTION( "Test Digital Sus Push" ) - { - auto testSusPush = [&]( float s1, float s2 ) - { - auto digPush = runAdsr( 0.05, 0.05, s1, 0.1, 0, 0, 0, false, 0.5, s2, 0.25, s2 ); - int obs = 0; - for( auto s : digPush ) - { - if( s.first > 0.2 && obs == 0 ) - { - REQUIRE( s.second == Approx( s1 ).margin( 1e-5 ) ); - obs++; - } - if( s.first > 0.3 && obs == 1 ) - { - REQUIRE( s.second == Approx( s2 ).margin( 1e-5 ) ); - obs++; - } - } - }; - - for( auto i=0; i<10; ++i ) - { - auto s1 = 0.95f * rand() / RAND_MAX + 0.02; - auto s2 = 0.95f * rand() / RAND_MAX + 0.02; - testSusPush( s1, s2 ); - } - } - - /* - ** This section recreates the somewhat painful SSE code in readable stuff - */ - auto analogClone = [](float a_sec, float d_sec, float s, float r_sec, float releaseAfter, float runUntil, float pushSusAt = -1, float pushSusTo = 0 ) - { - float a = limit_range((float)( log(a_sec)/log(2.0) ), -8.f, 5.f); - float d = limit_range((float)( log(d_sec)/log(2.0) ), -8.f, 5.f); - float r = limit_range((float)( log(r_sec)/log(2.0) ), -8.f, 5.f); - - int i = 0; - bool released = false; - std::vector> res; - res.push_back(std::make_pair(0.f, 0.f)); - - float v_c1 = 0.f; - float v_c1_delayed = 0.f; - bool discharge = false; - const float v_cc = 1.5f; - - while( true ) - { - float t = 1.0 * (i+1) * BLOCK_SIZE * dsamplerate_inv; - i++; - if( t > runUntil || runUntil < 0 ) - break; - - if( t > releaseAfter && ! released ) - { - released = true; - } - - if( pushSusAt > 0 && t > pushSusAt ) - s = pushSusTo; - - auto gate = !released; - float v_gate = gate ? v_cc : 0.f; - - // discharge = _mm_and_ps(_mm_or_ps(_mm_cmpgt_ss(v_c1_delayed, one), discharge), v_gate); - discharge = ( ( v_c1_delayed > 1 ) || discharge ) && gate; - v_c1_delayed = v_c1; - - float S = s * s; - - float v_attack = discharge ? 0 : v_gate; - // OK so this line: - // __m128 v_decay = _mm_or_ps(_mm_andnot_ps(discharge, v_cc_vec), _mm_and_ps(discharge, S)); - // The semantic intent is discharge ? S : v_cc - // but in the ADSR discharge has a value of v_gate which is 1.5 (v_cc) not 1.0. - // That bitwise and with 1.5 acts as a binary filter (the mantissa) and a rounding operation - // (from the .5) so I need to duplicate it exactly here. - /* - __m128 sM = _mm_load_ss(&S); - __m128 dM = _mm_load_ss(&v_cc); - __m128 vdv = _mm_and_ps(dM, sM); - float v_decay = discharge ? vdv[0] : v_cc; - */ - // Alternately I can correct the SSE code for discharge - float v_decay = discharge ? S : v_cc; - float v_release = v_gate; - - float diff_v_a = std::max( 0.f, v_attack - v_c1 ); - float diff_v_d = ( discharge && gate ) ? v_decay - v_c1 : std::min( 0.f, v_decay - v_c1 ); - // float diff_v_d = std::min( 0.f, v_decay - v_c1 ); - float diff_v_r = std::min( 0.f, v_release - v_c1 ); - - const float shortest = 6.f; - const float longest = -2.f; - const float coeff_offset = 2.f - log( samplerate / BLOCK_SIZE ) / log( 2.f ); - - float coef_A = pow( 2.f, std::min( 0.f, coeff_offset - a ) ); - float coef_D = pow( 2.f, std::min( 0.f, coeff_offset - d ) ); - float coef_R = pow( 2.f, std::min( 0.f, coeff_offset - r ) ); - - v_c1 = v_c1 + diff_v_a * coef_A; - v_c1 = v_c1 + diff_v_d * coef_D; - v_c1 = v_c1 + diff_v_r * coef_R; - - - // adsr->process_block(); - float output = v_c1; - if( !gate && !discharge && v_c1 < 1e-6 ) - output = 0; - - res.push_back( std::make_pair( (float)t, output ) ); - } - return res; - }; - - SECTION( "Clone the Analog" ) - { - auto compareSrgRepl = [&](float a, float d, float s, float r ) - { - auto t = a + d + 0.5 + r * 3; - auto surgeA = runAdsr( a, d, s, r, 0, 0, 0, true, a + d + 0.5, t ); - auto replA = analogClone( a, d, s, r, a + d + 0.5, t ); - - REQUIRE( surgeA.size() == Approx( replA.size() ).margin( 3 ) ); - auto sz = std::min( surgeA.size(), replA.size() ); - for( auto i=0; i 0.2 && obs == 0 ) - { - REQUIRE( s.second == Approx( s1 * s1 ).margin( 1e-3 ) ); - obs++; - } - if( s.first > 0.4 && obs == 1 ) - { - REQUIRE( s.second == Approx( s2 * s2 ).margin( 1e-3 ) ); - obs++; - } - } - - - obs = 0; - INFO( "Analog SSE passes sus push" ); - for( auto s : aSurgePush ) - { - if( s.first > 0.2 && obs == 0 ) - { - REQUIRE( s.second == Approx( s1 * s1 ).margin( 1e-3 ) ); - obs++; - } - if( s.first > 0.4 && obs == 1 ) - { - REQUIRE( s.second == Approx( s2 * s2 ).margin( 1e-5 ) ); - obs++; - } - } - }; - - testSusPush( 0.3, 0.7 ); - for( auto i=0; i<10; ++i ) - { - auto s1 = 0.95f * rand() / RAND_MAX + 0.02; - auto s2 = 0.95f * rand() / RAND_MAX + 0.02; - testSusPush( s1, s2 ); - } - } - -} - - -TEST_CASE( "Channel Split Routes on Channel", "[midi]" ) -{ - auto surge = std::shared_ptr( Surge::Headless::createSurge(44100) ); - REQUIRE( surge ); - REQUIRE( surge->loadPatchByPath( "test-data/patches/ChannelSplit-Sin-2OctaveB.fxp", -1, "Test" ) ); - - SECTION( "Regular (non-MPE)" ) - { - surge->mpeEnabled = false; - for( auto splitChan = 2; splitChan < 14; splitChan ++ ) - { - auto smc = splitChan * 8; - surge->storage.getPatch().splitkey.val.i = smc; - for( auto mc=0; mc<16; ++mc ) - { - auto fr = frequencyForNote( surge, 69, 2, 0, mc ); - auto targetfr = mc <= splitChan ? 440 : 440 * 4; - REQUIRE( fr == Approx( targetfr ).margin( 0.1 ) ); - } - } - } - - - SECTION( "MPE Enabled" ) - { - surge->mpeEnabled = true; - for( auto splitChan = 2; splitChan < 14; splitChan ++ ) - { - auto smc = splitChan * 8; - surge->storage.getPatch().splitkey.val.i = smc; - for( auto mc=0; mc<16; ++mc ) - { - auto fr = frequencyForNote( surge, 69, 2, 0, mc ); - auto targetfr = mc <= splitChan ? 440 : 440 * 4; - REQUIRE( fr == Approx( targetfr ).margin( 0.1 ) ); - } - } - } -} - int runAllTests(int argc, char **argv) { diff --git a/src/headless/UnitTestsDSP.cpp b/src/headless/UnitTestsDSP.cpp new file mode 100644 index 00000000000..f254a89f8bf --- /dev/null +++ b/src/headless/UnitTestsDSP.cpp @@ -0,0 +1,216 @@ +#include +#include +#include +#include + +#include "HeadlessUtils.h" +#include "Player.h" +#include "SurgeError.h" + +#include "catch2.hpp" + +#include "UnitTestUtilities.h" + +using namespace Surge::Test; + +TEST_CASE( "Simple Single Oscillator is Constant", "[dsp]" ) +{ + auto surge = Surge::Headless::createSurge(44100); + REQUIRE( surge ); + // surge->storage.getPatch().scene[0].osc[0].type.val.i = ot_sinus; + + int len = 4410 * 5; + //int len = BLOCK_SIZE * 20; + Surge::Headless::playerEvents_t heldC = Surge::Headless::makeHoldMiddleC(len); + REQUIRE( heldC.size() == 2 ); + + float* data = NULL; + int nSamples, nChannels; + + Surge::Headless::playAsConfigured(surge, heldC, &data, &nSamples, &nChannels); + REQUIRE( data ); + REQUIRE( std::abs( nSamples - len ) <= BLOCK_SIZE ); + REQUIRE( nChannels == 2 ); + + float rms = 0; + for( int i=0; i 0.1 ); + REQUIRE( rms < 0.101 ); + + + int zeroCrossings = 0; + for( int i=0; i 0 && data[i+2] < 0 ) + zeroCrossings ++; + } + // Somewhere in here + REQUIRE( zeroCrossings > 130 ); + REQUIRE( zeroCrossings < 160 ); + + if (data) + delete[] data; + +} +TEST_CASE( "Unison Absolute and Relative", "[osc]" ) +{ + auto surge = Surge::Headless::createSurge(44100); + REQUIRE( surge ); + + auto assertRelative = [surge](const char* pn) { + REQUIRE( surge->loadPatchByPath( pn, -1, "Test" ) ); + auto f60_0 = frequencyForNote( surge, 60, 5, 0 ); + auto f60_1 = frequencyForNote( surge, 60, 5, 1 ); + + auto f60_avg = 0.5 * ( f60_0 + f60_1 ); + + auto f72_0 = frequencyForNote( surge, 72, 5, 0 ); + auto f72_1 = frequencyForNote( surge, 72, 5, 1 ); + auto f72_avg = 0.5 * ( f72_0 + f72_1 ); + + // In relative mode, the average frequencies should double, as should the individual outliers + REQUIRE( f72_avg / f60_avg == Approx( 2 ).margin( 0.01 ) ); + REQUIRE( f72_0 / f60_0 == Approx( 2 ).margin( 0.01 ) ); + REQUIRE( f72_1 / f60_1 == Approx( 2 ).margin( 0.01 ) ); + }; + + auto assertAbsolute = [surge](const char* pn, bool print = false) { + REQUIRE( surge->loadPatchByPath( pn, -1, "Test" ) ); + auto f60_0 = frequencyForNote( surge, 60, 5, 0 ); + auto f60_1 = frequencyForNote( surge, 60, 5, 1 ); + + auto f60_avg = 0.5 * ( f60_0 + f60_1 ); + + auto f72_0 = frequencyForNote( surge, 72, 5, 0 ); + auto f72_1 = frequencyForNote( surge, 72, 5, 1 ); + auto f72_avg = 0.5 * ( f72_0 + f72_1 ); + + // In absolute mode, the average frequencies should double, but the channels should have constant difference + REQUIRE( f72_avg / f60_avg == Approx( 2 ).margin( 0.01 ) ); + REQUIRE( ( f72_0 - f72_1 ) / ( f60_0 - f60_1 ) == Approx( 1 ).margin( 0.01 ) ); + if( print ) + { + std::cout << "F60 " << f60_avg << " " << f60_0 << " " << f60_1 << " " << f60_0 - f60_1 << std::endl; + std::cout << "F72 " << f72_avg << " " << f72_0 << " " << f72_1 << " " << f60_0 - f60_1 << std::endl; + } + }; + + SECTION( "Wavetable Oscillator" ) + { + assertRelative("test-data/patches/Wavetable-Sin-Uni2-Relative.fxp"); + assertAbsolute("test-data/patches/Wavetable-Sin-Uni2-Absolute.fxp"); + } + + SECTION( "Window Oscillator" ) + { + assertRelative("test-data/patches/Window-Sin-Uni2-Relative.fxp"); + assertAbsolute("test-data/patches/Window-Sin-Uni2-Absolute.fxp"); + } + + SECTION( "Classic Oscillator" ) + { + assertRelative("test-data/patches/Classic-Uni2-Relative.fxp"); + assertAbsolute("test-data/patches/Classic-Uni2-Absolute.fxp"); + } + + SECTION( "SH Oscillator" ) + { + assertRelative("test-data/patches/SH-Uni2-Relative.fxp"); + assertAbsolute("test-data/patches/SH-Uni2-Absolute.fxp"); + } + +} + +TEST_CASE( "All Patches have Bounded Output", "[dsp]" ) +{ + auto surge = Surge::Headless::createSurge(44100); + REQUIRE( surge.get() ); + + 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); +} + + +TEST_CASE( "lipol_ps class", "[dsp]" ) +{ + lipol_ps mypol; + float prevtarget = -1.0; + mypol.set_target(prevtarget); + mypol.instantize(); + + constexpr size_t nfloat = 64; + constexpr size_t nfloat_quad = 16; + float storeTarget alignas(16)[nfloat]; + mypol.store_block(storeTarget, nfloat_quad); + + for( auto i=0; i +#include +#include +#include + +#include "HeadlessUtils.h" +#include "Player.h" +#include "SurgeError.h" + +#include "catch2.hpp" + +#include "UnitTestUtilities.h" + +using namespace Surge::Test; + + +TEST_CASE( "We can read a collection of wavetables", "[io]" ) +{ + /* + ** ToDo: + ** .wt file + ** oneshot + ** srgmarkers + ** etc + */ + auto surge = Surge::Headless::createSurge(44100); + REQUIRE( surge.get() ); + + SECTION( "Wavetable.wav" ) + { + auto wt = &(surge->storage.getPatch().scene[0].osc[0].wt); + surge->storage.load_wt_wav_portable("test-data/wav/Wavetable.wav", wt); + REQUIRE( wt->size == 2048 ); + REQUIRE( wt->n_tables == 256 ); + REQUIRE( ( wt->flags & wtf_is_sample ) == 0 ); + } + + SECTION( "05_BELL.WAV" ) + { + auto wt = &(surge->storage.getPatch().scene[0].osc[0].wt); + surge->storage.load_wt_wav_portable("test-data/wav/05_BELL.WAV", wt); + REQUIRE( wt->size == 2048 ); + REQUIRE( wt->n_tables == 33 ); + REQUIRE( ( wt->flags & wtf_is_sample ) == 0 ); + } + + SECTION( "pluckalgo.wav" ) + { + auto wt = &(surge->storage.getPatch().scene[0].osc[0].wt); + surge->storage.load_wt_wav_portable("test-data/wav/pluckalgo.wav", wt); + REQUIRE( wt->size == 2048 ); + REQUIRE( wt->n_tables == 9 ); + REQUIRE( ( wt->flags & wtf_is_sample ) == 0 ); + } + +} + +TEST_CASE( "All .wt and .wav factory assets load", "[io]" ) +{ + auto surge = Surge::Headless::createSurge(44100); + REQUIRE( surge.get() ); + for( auto p : surge->storage.wt_list ) + { + auto wt = &(surge->storage.getPatch().scene[0].osc[0].wt); + wt->size = -1; + wt->n_tables = -1; + surge->storage.load_wt(p.path.generic_string(), wt ); + REQUIRE( wt->size > 0 ); + REQUIRE( wt->n_tables > 0 ); + } +} + + +TEST_CASE( "All Patches are Loadable", "[io]" ) +{ + auto surge = Surge::Headless::createSurge(44100); + REQUIRE( surge.get() ); + int i=0; + for( auto p : surge->storage.patch_list ) + { + surge->loadPatch(i); + ++i; + } +} + +TEST_CASE( "DAW Streaming and Unstreaming", "[io]" ) +{ + // The basic plan of attack is, in a section, set up two surges, + // stream onto data on the first and off of data on the second + // and voila + + auto fromto = [](std::shared_ptr src, + std::shared_ptr dest) + { + void *d = nullptr; + src->populateDawExtraState(); + auto sz = src->saveRaw( &d ); + REQUIRE( src->storage.getPatch().dawExtraState.isPopulated ); + + dest->loadRaw( d, sz, false ); + dest->loadFromDawExtraState(); + REQUIRE( dest->storage.getPatch().dawExtraState.isPopulated ); + + // Why does this crash macos? + // if(d) free(d); + }; + + SECTION( "MPE Enabled State Saves" ) + { + auto surgeSrc = Surge::Headless::createSurge(44100); + auto surgeDest = Surge::Headless::createSurge(44100); + + REQUIRE( surgeSrc->mpeEnabled == false ); + REQUIRE( surgeDest->mpeEnabled == false ); + + surgeSrc->mpeEnabled = true; + REQUIRE( surgeDest->mpeEnabled == false ); + + fromto( surgeSrc, surgeDest ); + REQUIRE( surgeDest->mpeEnabled == true ); + + surgeSrc->mpeEnabled = false; + REQUIRE( surgeSrc->mpeEnabled == false ); + REQUIRE( surgeDest->mpeEnabled == true ); + + fromto( surgeSrc, surgeDest ); + REQUIRE( surgeSrc->mpeEnabled == false ); + REQUIRE( surgeDest->mpeEnabled == false ); + } + + SECTION( "MPE Pitch Bend State Saves" ) + { + auto surgeSrc = Surge::Headless::createSurge(44100); + auto surgeDest = Surge::Headless::createSurge(44100); + + // I purposefully use two values here which are not my default + auto v1 = 54; + auto v2 = 13; + + // Test from defaulted dest + surgeSrc->mpePitchBendRange = v2; + fromto( surgeSrc, surgeDest ); + REQUIRE( surgeDest->mpePitchBendRange == v2 ); + + // Test from set dest + surgeSrc->mpePitchBendRange = v1; + surgeDest->mpePitchBendRange = v1; + REQUIRE( surgeSrc->mpePitchBendRange == v1 ); + REQUIRE( surgeDest->mpePitchBendRange == v1 ); + + surgeSrc->mpePitchBendRange = v2; + REQUIRE( surgeSrc->mpePitchBendRange == v2 ); + REQUIRE( surgeDest->mpePitchBendRange == v1 ); + + fromto( surgeSrc, surgeDest ); + REQUIRE( surgeDest->mpePitchBendRange == v2 ); + } + + SECTION( "SCL State Saves" ) + { + auto surgeSrc = Surge::Headless::createSurge(44100); + auto surgeDest = Surge::Headless::createSurge(44100); + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/zeus22.scl" ); + + REQUIRE( surgeSrc->storage.isStandardTuning ); + REQUIRE( surgeDest->storage.isStandardTuning ); + + surgeSrc->storage.retuneToScale( s ); + REQUIRE( !surgeSrc->storage.isStandardTuning ); + REQUIRE( surgeDest->storage.isStandardTuning ); + REQUIRE( surgeSrc->storage.currentScale.count != surgeDest->storage.currentScale.count ); + REQUIRE( surgeSrc->storage.currentScale.count == s.count ); + + fromto( surgeSrc, surgeDest ); + REQUIRE( !surgeSrc->storage.isStandardTuning ); + REQUIRE( !surgeDest->storage.isStandardTuning ); + + REQUIRE( surgeSrc->storage.currentScale.count == surgeDest->storage.currentScale.count ); + REQUIRE( surgeSrc->storage.currentScale.count == s.count ); + + REQUIRE( surgeSrc->storage.currentScale.rawText == surgeDest->storage.currentScale.rawText ); + } + + SECTION( "Save and Restore KBM" ) + { + auto surgeSrc = Surge::Headless::createSurge(44100); + auto surgeDest = Surge::Headless::createSurge(44100); + + auto k = Surge::Storage::readKBMFile( "test-data/scl/mapping-a440-constant.kbm" ); + + REQUIRE( surgeSrc->storage.isStandardMapping ); + REQUIRE( surgeDest->storage.isStandardMapping ); + + surgeSrc->storage.remapToKeyboard( k ); + REQUIRE( !surgeSrc->storage.isStandardMapping ); + REQUIRE( surgeDest->storage.isStandardMapping ); + + fromto( surgeSrc, surgeDest ); + REQUIRE( !surgeSrc->storage.isStandardMapping ); + REQUIRE( !surgeDest->storage.isStandardMapping ); + REQUIRE( surgeSrc->storage.currentMapping.tuningConstantNote == 69 ); + REQUIRE( surgeDest->storage.currentMapping.tuningConstantNote == 69 ); + + REQUIRE( surgeDest->storage.currentMapping.rawText == surgeSrc->storage.currentMapping.rawText ); + + surgeSrc->storage.remapToStandardKeyboard( ); + REQUIRE( surgeSrc->storage.isStandardMapping ); + REQUIRE( !surgeDest->storage.isStandardMapping ); + + fromto( surgeSrc, surgeDest ); + REQUIRE( surgeSrc->storage.isStandardMapping ); + REQUIRE( surgeDest->storage.isStandardMapping ); + + } + + +} diff --git a/src/headless/UnitTestsMIDI.cpp b/src/headless/UnitTestsMIDI.cpp new file mode 100644 index 00000000000..6ec58b91227 --- /dev/null +++ b/src/headless/UnitTestsMIDI.cpp @@ -0,0 +1,56 @@ +#include +#include +#include +#include + +#include "HeadlessUtils.h" +#include "Player.h" +#include "SurgeError.h" + +#include "catch2.hpp" + +#include "UnitTestUtilities.h" + +using namespace Surge::Test; + +TEST_CASE( "Channel Split Routes on Channel", "[midi]" ) +{ + auto surge = std::shared_ptr( Surge::Headless::createSurge(44100) ); + REQUIRE( surge ); + REQUIRE( surge->loadPatchByPath( "test-data/patches/ChannelSplit-Sin-2OctaveB.fxp", -1, "Test" ) ); + + SECTION( "Regular (non-MPE)" ) + { + surge->mpeEnabled = false; + for( auto splitChan = 2; splitChan < 14; splitChan ++ ) + { + auto smc = splitChan * 8; + surge->storage.getPatch().splitkey.val.i = smc; + for( auto mc=0; mc<16; ++mc ) + { + auto fr = frequencyForNote( surge, 69, 2, 0, mc ); + auto targetfr = mc <= splitChan ? 440 : 440 * 4; + REQUIRE( fr == Approx( targetfr ).margin( 0.1 ) ); + } + } + } + + + SECTION( "MPE Enabled" ) + { + surge->mpeEnabled = true; + for( auto splitChan = 2; splitChan < 14; splitChan ++ ) + { + auto smc = splitChan * 8; + surge->storage.getPatch().splitkey.val.i = smc; + for( auto mc=0; mc<16; ++mc ) + { + auto fr = frequencyForNote( surge, 69, 2, 0, mc ); + auto targetfr = mc <= splitChan ? 440 : 440 * 4; + REQUIRE( fr == Approx( targetfr ).margin( 0.1 ) ); + } + } + } +} + + diff --git a/src/headless/UnitTestsMOD.cpp b/src/headless/UnitTestsMOD.cpp new file mode 100644 index 00000000000..14517932a6f --- /dev/null +++ b/src/headless/UnitTestsMOD.cpp @@ -0,0 +1,493 @@ +#include +#include +#include +#include + +#include "HeadlessUtils.h" +#include "Player.h" +#include "SurgeError.h" + +#include "catch2.hpp" + +#include "UnitTestUtilities.h" + +using namespace Surge::Test; + + +TEST_CASE( "ADSR Envelope Behaviour", "[mod]" ) +{ + + std::shared_ptr surge( Surge::Headless::createSurge(44100) ); + REQUIRE( surge.get() ); + + /* + ** OK so lets set up a pretty simple setup + */ + + auto runAdsr = [surge](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 ) + { + auto* adsrstorage = &(surge->storage.getPatch().scene[0].adsr[0]); + std::shared_ptr adsr( new AdsrEnvelope() ); + adsr->init( &(surge->storage), adsrstorage, surge->storage.getPatch().scenedata[0], nullptr ); + REQUIRE( adsr.get() ); + + int ids, ide; + setupStorageRanges(&(adsrstorage->a), &(adsrstorage->mode), ids, ide ); + REQUIRE( ide > ids ); + REQUIRE( ide >= 0 ); + REQUIRE( ids >= 0 ); + + auto svn = [](Parameter *p, float vn) + { + p->set_value_f01( p->value_to_normalized( limit_range( vn, p->val_min.f, p->val_max.f ) ) ); + }; + + auto inverseEnvtime = [](float desiredTime) + { + // 2^x = desired time + auto x = log(desiredTime)/log(2.0); + return x; + }; + + svn(&(adsrstorage->a), inverseEnvtime(a)); + svn(&(adsrstorage->d), inverseEnvtime(d)); + svn(&(adsrstorage->s), s); + svn(&(adsrstorage->r), inverseEnvtime(r)); + + svn(&(adsrstorage->a_s), a_s); + svn(&(adsrstorage->d_s), d_s); + svn(&(adsrstorage->r_s), r_s); + + adsrstorage->mode.val.b = isAnalog; + + copyScenedataSubset(&(surge->storage), 0, ids, ide); + adsr->attack(); + + bool released = false; + bool pushSus = false; + int i = 0; + std::vector> res; + res.push_back(std::make_pair(0.f, 0.f)); + + while( true ) + { + auto t = 1.0 * (i+1) * BLOCK_SIZE * dsamplerate_inv; + i++; + if( t > runUntil || runUntil < 0 ) + break; + + if( t > releaseAfter && ! released ) + { + adsr->release(); + released = true; + } + + if( pushSusAt > 0 && ! pushSus && t > pushSusAt ) + { + pushSus = true; + svn(&(adsrstorage->s), pushSusTo); + copyScenedataSubset(&(surge->storage), 0, ids, ide); + } + + adsr->process_block(); + res.push_back( std::make_pair( (float)t, adsr->output ) ); + if( false && i > 270 && i < 290 ) + std::cout << i << " " << t << " " << adsr->output << " " << adsr->getEnvState() + << std::endl; + } + return res; + }; + + auto detectTurnarounds = [](std::vector> data) { + auto pv = -1000.0; + int dir = 1; + std::vector> turns; + turns.push_back( std::make_pair( 0, 1 ) ); + for( auto &p : data ) + { + auto t = p.first; + auto v = p.second; + if( pv >= 0 ) + { + int ldir = 0; + if( v > 0.999999f ) ldir = dir; // sometimes we get a double '1' + if( fabs( v - pv ) < 5e-6 && fabs( v ) < 1e-5) ldir = 0; // bouncing off of 0 is annoying + else if( fabs( v - pv ) < 5e-7 ) ldir = 0; + else if( v > pv ) ldir = 1; + else ldir = -1; + + if( v != 1 ) + { + if( ldir != dir ) + { + turns.push_back(std::make_pair(t, ldir) ); + } + dir = ldir; + } + } + pv = v; + } + return turns; + }; + + // With 0 sustain I should decay in decay time + auto runCompare = [&](float a, float d, float s, float r, int a_s, int d_s, int r_s, bool isAnalog ) + { + float sustime = 0.1; + float endtime = 0.1; + float totaltime = a + d + sustime + r + endtime; + + auto simple = runAdsr( a, d, s, r, a_s, d_s, r_s, isAnalog, a + d + sustime, totaltime ); + auto sturns = detectTurnarounds(simple); + if( false ) + std::cout << "ADSR: " << a << " " << d << " " << s << " " << r << " switches: " << a_s << " " << d_s << " " << r_s << std::endl; + if( s == 0 ) + { + if( sturns.size() != 3 ) + { + for( auto s : simple ) + std::cout << s.first << " " << s.second << std::endl; + for( auto s : sturns ) + std::cout << s.first << " " << s.second << std::endl; + } + REQUIRE( sturns.size() == 3 ); + REQUIRE( sturns[0].first == 0 ); + REQUIRE( sturns[0].second == 1 ); + REQUIRE( sturns[1].first == Approx( a ).margin( 0.01 ) ); + REQUIRE( sturns[1].second == -1 ); + REQUIRE( sturns[2].first == Approx( a + d ).margin( 0.01 ) ); + REQUIRE( sturns[2].second == 0 ); + } + else + { + if( sturns.size() != 5 ) + { + for( auto s : simple ) + std::cout << s.first << " " << s.second << std::endl; + for( auto s : sturns ) + std::cout << s.first << " " << s.second << std::endl; + } + REQUIRE( sturns.size() == 5 ); + REQUIRE( sturns[0].first == 0 ); + REQUIRE( sturns[0].second == 1 ); + REQUIRE( sturns[1].first == Approx( a ).margin( 0.01 ) ); + REQUIRE( sturns[1].second == -1 ); + if( d_s == 0 ) + { + // this equality only holds in the linear case; in the polynomial case you get faster reach to non-zero sustain + REQUIRE( sturns[2].first == Approx( a + d * ( 1.0 - s ) ).margin( 0.01 ) ); + } + else if( a + d * ( 1.0 - s ) > 0.1 && d > 0.05 ) + { + REQUIRE( sturns[2].first < a + d * ( 1.0 - s ) + 0.01 ); + } + REQUIRE( sturns[2].second == 0 ); + REQUIRE( sturns[3].first == Approx( a + d + sustime ).margin( 0.01 ) ); + REQUIRE( sturns[3].second == -1 ); + if( r_s == 0 || s > 0.1 && r > 0.05 ) // if we are in the non-linear releases at low sustain we get there early + { + REQUIRE( sturns[4].first == Approx( a + d + sustime + r ).margin( ( r_s == 0 ? 0.01 : ( r * 0.1 ) ) ) ); + REQUIRE( sturns[4].second == 0 ); + } + } + }; + + SECTION( "Test the Digital Envelope" ) + { + for( int as=0;as<3;++as ) + for( int ds=0; ds<3; ++ds ) + for( int rs=0; rs<3; ++rs ) + { + runCompare( 0.2, 0.3, 0.0, 0.1, as, ds, rs, false ); + runCompare( 0.2, 0.3, 0.5, 0.1, as, ds, rs, false ); + + for( int rc=0;rc<10; ++rc ) + { + auto a = rand() * 1.0 / RAND_MAX; + auto d = rand() * 1.0 / RAND_MAX; + auto s = 0.8 * rand() * 1.0 / RAND_MAX + 0.1; // we have tested the s=0 case above + auto r = rand() * 1.0 / RAND_MAX; + runCompare( a, d, s, r, as, ds, rs, false ); + } + } + } + + SECTION( "Test the Analog Envelope" ) + { + // OK so we can't check the same thing here since the turns aren't as tight in analog mode + // Also the analog ADSR sustains at half the given sustain. + auto testAnalog = [&](float a, float d, float s, float r ) + { + INFO( "ANALOG " << a << " " << d << " " << s << " " << r ); + auto holdFor = a + d + d + 0.5; + + auto ae = runAdsr( a, d, s, r, 0, 0, 0, true, holdFor, holdFor + 4 * r ); + auto aturns = detectTurnarounds(ae); + + float maxt=0, maxv=0; + float zerot=0; + float valAtRelEnd = -1; + std::vector heldPeriod; + for( auto obs : ae ) + { + //std::cout << obs.first << " " << obs.second << std::endl; + + if( obs.first > a + d + d * 0.95 && obs.first < holdFor && s > 0.05 ) // that 0.1 lets the delay ring off + { + REQUIRE( obs.second == Approx( s * s ).margin( 1e-3 ) ); + heldPeriod.push_back(obs.second); + } + if( obs.first > a + d && obs.second < 5e-5 && zerot == 0 ) + zerot = obs.first; + if( obs.first > holdFor + r && valAtRelEnd < 0 ) + valAtRelEnd = obs.second; + + if( obs.second > maxv ) + { + maxv = obs.second; + maxt = obs.first; + } + } + + // In the held period are we mostly constant + if( heldPeriod.size() > 10 ) + { + float sum = 0; + for( auto p : heldPeriod ) + sum += p; + float mean = sum / heldPeriod.size(); + float var = 0; + for( auto p : heldPeriod ) + var += ( p - mean ) * ( p - mean ); + var /= ( heldPeriod.size() - 1 ); + float stddev = sqrt( var ); + REQUIRE( stddev < d * 5e-3 ); + } + REQUIRE( maxt < a ); + REQUIRE( maxv > 0.99 ); + if( s > 0.05 ) + { + REQUIRE( zerot > holdFor + r * 0.9 ); + REQUIRE( valAtRelEnd < s * 0.025 ); + } + }; + + testAnalog( 0.1, 0.2, 0.5, 0.1 ); + testAnalog( 0.1, 0.2, 0.0, 0.1 ); + for( int rc=0;rc<50; ++rc ) + { + auto a = rand() * 1.0 / RAND_MAX + 0.03; + auto d = rand() * 1.0 / RAND_MAX + 0.03; + auto s = 0.7 * rand() * 1.0 / RAND_MAX + 0.2; // we have tested the s=0 case above + auto r = rand() * 1.0 / RAND_MAX + 0.03; + testAnalog( a, d, s, r); + } + + } + + // This is just a rudiemntary little test of this in digital mode + SECTION( "Test Digital Sus Push" ) + { + auto testSusPush = [&]( float s1, float s2 ) + { + auto digPush = runAdsr( 0.05, 0.05, s1, 0.1, 0, 0, 0, false, 0.5, s2, 0.25, s2 ); + int obs = 0; + for( auto s : digPush ) + { + if( s.first > 0.2 && obs == 0 ) + { + REQUIRE( s.second == Approx( s1 ).margin( 1e-5 ) ); + obs++; + } + if( s.first > 0.3 && obs == 1 ) + { + REQUIRE( s.second == Approx( s2 ).margin( 1e-5 ) ); + obs++; + } + } + }; + + for( auto i=0; i<10; ++i ) + { + auto s1 = 0.95f * rand() / RAND_MAX + 0.02; + auto s2 = 0.95f * rand() / RAND_MAX + 0.02; + testSusPush( s1, s2 ); + } + } + + /* + ** This section recreates the somewhat painful SSE code in readable stuff + */ + auto analogClone = [](float a_sec, float d_sec, float s, float r_sec, float releaseAfter, float runUntil, float pushSusAt = -1, float pushSusTo = 0 ) + { + float a = limit_range((float)( log(a_sec)/log(2.0) ), -8.f, 5.f); + float d = limit_range((float)( log(d_sec)/log(2.0) ), -8.f, 5.f); + float r = limit_range((float)( log(r_sec)/log(2.0) ), -8.f, 5.f); + + int i = 0; + bool released = false; + std::vector> res; + res.push_back(std::make_pair(0.f, 0.f)); + + float v_c1 = 0.f; + float v_c1_delayed = 0.f; + bool discharge = false; + const float v_cc = 1.5f; + + while( true ) + { + float t = 1.0 * (i+1) * BLOCK_SIZE * dsamplerate_inv; + i++; + if( t > runUntil || runUntil < 0 ) + break; + + if( t > releaseAfter && ! released ) + { + released = true; + } + + if( pushSusAt > 0 && t > pushSusAt ) + s = pushSusTo; + + auto gate = !released; + float v_gate = gate ? v_cc : 0.f; + + // discharge = _mm_and_ps(_mm_or_ps(_mm_cmpgt_ss(v_c1_delayed, one), discharge), v_gate); + discharge = ( ( v_c1_delayed > 1 ) || discharge ) && gate; + v_c1_delayed = v_c1; + + float S = s * s; + + float v_attack = discharge ? 0 : v_gate; + // OK so this line: + // __m128 v_decay = _mm_or_ps(_mm_andnot_ps(discharge, v_cc_vec), _mm_and_ps(discharge, S)); + // The semantic intent is discharge ? S : v_cc + // but in the ADSR discharge has a value of v_gate which is 1.5 (v_cc) not 1.0. + // That bitwise and with 1.5 acts as a binary filter (the mantissa) and a rounding operation + // (from the .5) so I need to duplicate it exactly here. + /* + __m128 sM = _mm_load_ss(&S); + __m128 dM = _mm_load_ss(&v_cc); + __m128 vdv = _mm_and_ps(dM, sM); + float v_decay = discharge ? vdv[0] : v_cc; + */ + // Alternately I can correct the SSE code for discharge + float v_decay = discharge ? S : v_cc; + float v_release = v_gate; + + float diff_v_a = std::max( 0.f, v_attack - v_c1 ); + float diff_v_d = ( discharge && gate ) ? v_decay - v_c1 : std::min( 0.f, v_decay - v_c1 ); + // float diff_v_d = std::min( 0.f, v_decay - v_c1 ); + float diff_v_r = std::min( 0.f, v_release - v_c1 ); + + const float shortest = 6.f; + const float longest = -2.f; + const float coeff_offset = 2.f - log( samplerate / BLOCK_SIZE ) / log( 2.f ); + + float coef_A = pow( 2.f, std::min( 0.f, coeff_offset - a ) ); + float coef_D = pow( 2.f, std::min( 0.f, coeff_offset - d ) ); + float coef_R = pow( 2.f, std::min( 0.f, coeff_offset - r ) ); + + v_c1 = v_c1 + diff_v_a * coef_A; + v_c1 = v_c1 + diff_v_d * coef_D; + v_c1 = v_c1 + diff_v_r * coef_R; + + + // adsr->process_block(); + float output = v_c1; + if( !gate && !discharge && v_c1 < 1e-6 ) + output = 0; + + res.push_back( std::make_pair( (float)t, output ) ); + } + return res; + }; + + SECTION( "Clone the Analog" ) + { + auto compareSrgRepl = [&](float a, float d, float s, float r ) + { + auto t = a + d + 0.5 + r * 3; + auto surgeA = runAdsr( a, d, s, r, 0, 0, 0, true, a + d + 0.5, t ); + auto replA = analogClone( a, d, s, r, a + d + 0.5, t ); + + REQUIRE( surgeA.size() == Approx( replA.size() ).margin( 3 ) ); + auto sz = std::min( surgeA.size(), replA.size() ); + for( auto i=0; i 0.2 && obs == 0 ) + { + REQUIRE( s.second == Approx( s1 * s1 ).margin( 1e-3 ) ); + obs++; + } + if( s.first > 0.4 && obs == 1 ) + { + REQUIRE( s.second == Approx( s2 * s2 ).margin( 1e-3 ) ); + obs++; + } + } + + + obs = 0; + INFO( "Analog SSE passes sus push" ); + for( auto s : aSurgePush ) + { + if( s.first > 0.2 && obs == 0 ) + { + REQUIRE( s.second == Approx( s1 * s1 ).margin( 1e-3 ) ); + obs++; + } + if( s.first > 0.4 && obs == 1 ) + { + REQUIRE( s.second == Approx( s2 * s2 ).margin( 1e-5 ) ); + obs++; + } + } + }; + + testSusPush( 0.3, 0.7 ); + for( auto i=0; i<10; ++i ) + { + auto s1 = 0.95f * rand() / RAND_MAX + 0.02; + auto s2 = 0.95f * rand() / RAND_MAX + 0.02; + testSusPush( s1, s2 ); + } + } + +} + diff --git a/src/headless/UnitTestsTUN.cpp b/src/headless/UnitTestsTUN.cpp new file mode 100644 index 00000000000..346e18a7dd9 --- /dev/null +++ b/src/headless/UnitTestsTUN.cpp @@ -0,0 +1,604 @@ +#include +#include +#include +#include + +#include "HeadlessUtils.h" +#include "Player.h" +#include "SurgeError.h" + +#include "catch2.hpp" + +#include "UnitTestUtilities.h" + +using namespace Surge::Test; + +TEST_CASE( "Retune Surge to .scl files", "[tun]" ) +{ + auto surge = Surge::Headless::createSurge(44100); + auto n2f = [surge](int n) { return surge->storage.note_to_pitch(n); }; + + //Surge::Storage::Scale s = Surge::Storage::readSCLFile("/Users/paul/dev/music/test_scl/Q4.scl" ); + SECTION( "12-intune SCL file" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/12-intune.scl" ); + surge->storage.retuneToScale(s); + 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(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() +{ + auto 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 surge; +} + +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( "KBM File Parsing", "[tun]" ) +{ + SECTION( "Default Keyboard is Default" ) + { + auto k = Surge::Storage::KeyboardMapping(); + REQUIRE( k.isValid ); + REQUIRE( k.isStandardMapping ); + REQUIRE( k.count == 12 ); + REQUIRE( k.firstMidi == 0 ); + REQUIRE( k.lastMidi == 127 ); + REQUIRE( k.middleNote == 60 ); + REQUIRE( k.tuningConstantNote == 60 ); + REQUIRE( k.tuningFrequency == Approx( 261.62558 ) ); + REQUIRE( k.octaveDegrees == 12 ); + for( auto i=0; i values = { 0, 1, -1, 2, -1, 3, 4, -1, 5, -1, 6 }; + for( int i=0; istorage.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( 448.2 ).margin( .1 ) ); + unmapped[0] = f60; + unmapped[1] = f72; + unmapped[2] = f69; + } + + SECTION( "And remap to 440" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/marvel12.scl" ); + auto k = Surge::Storage::readKBMFile( "test-data/scl/mapping-a440-constant.kbm" ); + + surge->storage.retuneToScale(s); + surge->storage.remapToKeyboard(k); + + auto f60 = frequencyForNote( surge, 60 ); + auto f72 = frequencyForNote( surge, 72 ); + auto f69 = frequencyForNote( surge, 69 ); + REQUIRE( f69 == Approx( 440.0 ).margin(.1) ); + REQUIRE( unmapped[2]/440.0 == Approx( unmapped[0] / f60 ).margin(.001) ); + REQUIRE( unmapped[2]/440.0 == Approx( unmapped[1] / f72 ).margin(.001) ); + } + + // and back and then back again + SECTION( "Can Map and ReMap consistently" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/marvel12.scl" ); + auto k440 = Surge::Storage::readKBMFile( "test-data/scl/mapping-a440-constant.kbm" ); + + surge->storage.retuneToScale(s); + surge->storage.remapToStandardKeyboard(); + + auto f60std = frequencyForNote( surge, 60 ); + auto f69std = frequencyForNote( surge, 69 ); + + surge->storage.remapToKeyboard( k440 ); + auto f60map = frequencyForNote( surge, 60 ); + auto f69map = frequencyForNote( surge, 69 ); + + REQUIRE( f60std == Approx( 261.63 ).margin(0.1) ); + REQUIRE( f69map == Approx( 440.0 ).margin(0.1) ); + REQUIRE( f69std/f60std == Approx( f69map/f60map ).margin(.001 ) ); + + for( int i=0; i<50; ++i ) + { + auto fr = 1.0f * rand() / RAND_MAX; + if( fr > 0 ) + { + surge->storage.remapToKeyboard(k440); + auto f60 = frequencyForNote( surge, 60 ); + auto f69 = frequencyForNote( surge, 69 ); + REQUIRE( f60 == f60map ); + REQUIRE( f69 == f69map ); + } + else + { + surge->storage.remapToStandardKeyboard(); + auto f60 = frequencyForNote( surge, 60 ); + auto f69 = frequencyForNote( surge, 69 ); + REQUIRE( f60 == f60std ); + REQUIRE( f69 == f69std ); + } + } + } + + SECTION( "Scale Ratio is Unch" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/marvel12.scl" ); + auto k440 = Surge::Storage::readKBMFile( "test-data/scl/mapping-a440-constant.kbm" ); + + surge->storage.retuneToScale(s); + surge->storage.remapToStandardKeyboard(); + auto f60 = frequencyForNote( surge, 60 ); + REQUIRE( f60 == Approx( 261.63 ).margin( .1 ) ); + + std::vector ratios; + for( int i=61; i<72; ++i ) + ratios.push_back( frequencyForNote( surge, i ) / f60 ); + + surge->storage.remapToStandardKeyboard(); + auto f60map = frequencyForNote( surge, 60 ); + for( int i=61; i<72; ++i ) + { + auto fi = frequencyForNote( surge, i ); + REQUIRE( fi / f60map == Approx( ratios[i-61] ).margin( 0.001 ) ); + } + } +} + +TEST_CASE( "Non-uniform keyboard mapping", "[tun]" ) +{ + auto surge = surgeOnSine(); + REQUIRE( surge.get() ); + + auto mt = [](float c) { + auto t = Surge::Storage::Tone(); + t.type = Surge::Storage::Tone::kToneCents; + t.cents = c; + t.floatValue = c / 1200.0 + 1.0; + return t; + }; + // This is the "white keys" scale + Surge::Storage::Scale s; + s.count = 7; + s.tones.push_back( mt( 200 ) ); + s.tones.push_back( mt( 400 ) ); + s.tones.push_back( mt( 500 ) ); + s.tones.push_back( mt( 700 ) ); + s.tones.push_back( mt( 900 ) ); + s.tones.push_back( mt( 1100 ) ); + s.tones.push_back( mt( 1200 ) ); + + Surge::Storage::Scale sWonky; + sWonky.count = 7; + sWonky.tones.push_back( mt( 220 ) ); + sWonky.tones.push_back( mt( 390 ) ); + sWonky.tones.push_back( mt( 517 ) ); + sWonky.tones.push_back( mt( 682 ) ); + sWonky.tones.push_back( mt( 941 ) ); + sWonky.tones.push_back( mt( 1141 ) ); + sWonky.tones.push_back( mt( 1200 ) ); + + SECTION( "7 Note Scale" ) + { + std::vector frequencies; + // When I map it directly I get 440 at note 65 (since I skipped 4 black keys) + surge->storage.retuneToScale(s); + for( int i=0; i<8; ++i ) + { + frequencies.push_back( frequencyForNote( surge, 60+i ) ); + } + REQUIRE( frequencies[0] == Approx( 261.63 ).margin( 0.1 ) ); + REQUIRE( frequencies[5] == Approx( 440.00 ).margin( 0.1 ) ); + + auto k = Surge::Storage::readKBMFile( "test-data/scl/mapping-whitekeys-c261.kbm" ); + REQUIRE( ! k.isStandardMapping ); + REQUIRE( k.count == 12 ); + REQUIRE( k.octaveDegrees == 7 ); + surge->storage.remapToKeyboard(k); + auto f60 = frequencyForNote( surge, 60 ); + auto f69 = frequencyForNote( surge, 69 ); + + REQUIRE( f60 == Approx( 261.63 ).margin( 0.1 ) ); + REQUIRE( f69 == Approx( 440 ).margin( 0.1 ) ); + + // If we have remapped to white keys this will be true + REQUIRE( frequencyForNote( surge, 60 ) == Approx( frequencies[0] ).margin(0.1) ); + REQUIRE( frequencyForNote( surge, 62 ) == Approx( frequencies[1] ).margin(0.1) ); + REQUIRE( frequencyForNote( surge, 64 ) == Approx( frequencies[2] ).margin(0.1) ); + REQUIRE( frequencyForNote( surge, 65 ) == Approx( frequencies[3] ).margin(0.1) ); + REQUIRE( frequencyForNote( surge, 67 ) == Approx( frequencies[4] ).margin(0.1) ); + REQUIRE( frequencyForNote( surge, 69 ) == Approx( frequencies[5] ).margin(0.1) ); + REQUIRE( frequencyForNote( surge, 71 ) == Approx( frequencies[6] ).margin(0.1) ); + } + + SECTION( "7 Note Scale with Tuning Centers" ) + { + auto k261 = Surge::Storage::readKBMFile( "test-data/scl/mapping-whitekeys-c261.kbm" ); + auto k440 = Surge::Storage::readKBMFile( "test-data/scl/mapping-whitekeys-a440.kbm" ); + REQUIRE( ! k261.isStandardMapping ); + REQUIRE( ! k440.isStandardMapping ); + + surge->storage.retuneToScale(sWonky); + surge->storage.remapToKeyboard(k261); + auto f60 = frequencyForNote( surge, 60 ); + auto f69 = frequencyForNote( surge, 69 ); + + REQUIRE( f60 == Approx( 261.63 ).margin( 0.1 ) ); + REQUIRE( f69 != Approx( 440.0 ).margin( 0.1 ) ); + + surge->storage.retuneToScale(sWonky); + surge->storage.remapToKeyboard(k440); + auto f60_440 = frequencyForNote( surge, 60 ); + auto f69_440 = frequencyForNote( surge, 69 ); + + REQUIRE( f60_440 != Approx( 261.63 ).margin( 0.1 ) ); + REQUIRE( f69_440 == Approx( 440.0 ).margin( 0.1 ) ); + + REQUIRE( f69_440/f60_440 == Approx( f69/f60 ).margin( 0.001 ) ); + } +} + +TEST_CASE( "Zero Size Maps", "[tun]" ) +{ + auto surge = surgeOnSine(); + REQUIRE( surge.get() ); + + SECTION( "Note 61" ) + { + auto f60std = frequencyForNote( surge, 60 ); + auto f61std = frequencyForNote( surge, 61 ); + REQUIRE( f60std == Approx( 261.63 ).margin( 0.1 ) ); + + auto k61 = Surge::Storage::readKBMFile( "test-data/scl/empty-note61.kbm" ); + REQUIRE( !k61.isStandardMapping ); + REQUIRE( k61.count == 0 ); + surge->storage.remapToKeyboard( k61 ); + + auto f60map = frequencyForNote( surge, 60 ); + auto f61map = frequencyForNote( surge, 61 ); + REQUIRE( frequencyForNote( surge, 61 ) == Approx( 280 ).margin( 0.1 ) ); + REQUIRE( f61std / f60std == Approx( f61map / f60map ).margin( 0.001 ) ); + } + + SECTION( "Note 69" ) + { + auto f60std = frequencyForNote( surge, 60 ); + auto f69std = frequencyForNote( surge, 69 ); + REQUIRE( f60std == Approx( 261.63 ).margin( 0.1 ) ); + REQUIRE( f69std == Approx( 440.0 ).margin( 0.1 ) ); + + auto k69 = Surge::Storage::readKBMFile( "test-data/scl/empty-note69.kbm" ); + REQUIRE( !k69.isStandardMapping ); + REQUIRE( k69.count == 0 ); + surge->storage.remapToKeyboard( k69 ); + + auto f60map = frequencyForNote( surge, 60 ); + auto f69map = frequencyForNote( surge, 69 ); + REQUIRE( frequencyForNote( surge, 69 ) == Approx( 452 ).margin( 0.1 ) ); + REQUIRE( f69std / f60std == Approx( f69map / f60map ).margin( 0.001 ) ); + } + + SECTION( "Note 69 with Tuning" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/marvel12.scl" ); + surge->storage.retuneToScale(s); + auto f60std = frequencyForNote( surge, 60 ); + auto f69std = frequencyForNote( surge, 69 ); + REQUIRE( f60std == Approx( 261.63 ).margin( 0.1 ) ); + REQUIRE( f69std != Approx( 440.0 ).margin( 0.1 ) ); + + + auto k69 = Surge::Storage::readKBMFile( "test-data/scl/empty-note69.kbm" ); + REQUIRE( !k69.isStandardMapping ); + REQUIRE( k69.count == 0 ); + surge->storage.remapToKeyboard( k69 ); + + auto f60map = frequencyForNote( surge, 60 ); + auto f69map = frequencyForNote( surge, 69 ); + REQUIRE( frequencyForNote( surge, 69 ) == Approx( 452 ).margin( 0.1 ) ); + REQUIRE( f69std / f60std == Approx( f69map / f60map ).margin( 0.001 ) ); + } + +} + +TEST_CASE( "An Octave is an Octave", "[tun]" ) +{ + auto surge = surgeOnSine(); + REQUIRE( surge.get() ); + + SECTION( "Untuned OSC Octave" ) + { + auto f60 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = -1; + auto f60m1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = 1; + auto f60p1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = 0; + auto f60z = frequencyForNote(surge, 60); + REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); + } + + SECTION( "Untuned Scene Octave" ) + { + auto f60 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = -1; + auto f60m1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = 1; + auto f60p1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = 0; + auto f60z = frequencyForNote(surge, 60); + REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); + } + + SECTION( "Tuned to 12 OSC octave" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/12-intune.scl" ); + surge->storage.retuneToScale(s); + + auto f60 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = -1; + auto f60m1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = 1; + auto f60p1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = 0; + auto f60z = frequencyForNote(surge, 60); + REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); + } + + + SECTION( "Tuned to 12 Scene Octave" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/12-intune.scl" ); + surge->storage.retuneToScale(s); + + auto f60 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = -1; + auto f60m1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = 1; + auto f60p1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = 0; + auto f60z = frequencyForNote(surge, 60); + REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); + } + + SECTION( "22 note scale OSC Octave" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/zeus22.scl" ); + surge->storage.retuneToScale(s); + REQUIRE( s.count == 22 ); + + auto f60 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = -1; + auto f60m1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = 1; + auto f60p1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = 0; + auto f60z = frequencyForNote(surge, 60); + REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); + } + + SECTION( "22 note scale Scene Octave" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/zeus22.scl" ); + surge->storage.retuneToScale(s); + REQUIRE( s.count == 22 ); + + auto f60 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = -1; + auto f60m1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = 1; + auto f60p1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = 0; + auto f60z = frequencyForNote(surge, 60); + REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); + } + + SECTION( "6 note scale OSC Octave" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/6-exact.scl" ); + surge->storage.retuneToScale(s); + REQUIRE( s.count == 6 ); + + auto f60 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = -1; + auto f60m1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = 1; + auto f60p1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].osc[0].octave.val.i = 0; + auto f60z = frequencyForNote(surge, 60); + REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); + } + + SECTION( "6 note scale Scene Octave" ) + { + Surge::Storage::Scale s = Surge::Storage::readSCLFile("test-data/scl/6-exact.scl" ); + surge->storage.retuneToScale(s); + REQUIRE( s.count == 6 ); + + auto f60 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = -1; + auto f60m1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = 1; + auto f60p1 = frequencyForNote(surge, 60); + surge->storage.getPatch().scene[0].octave.val.i = 0; + auto f60z = frequencyForNote(surge, 60); + REQUIRE( f60 == Approx( f60z ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60m1 * 2 ).margin( 0.1 ) ); + REQUIRE( f60 == Approx( f60p1 / 2 ).margin( 0.1 ) ); + } +} + + + diff --git a/src/headless/main.cpp b/src/headless/main.cpp index a5d35d854c1..10d3ef6672f 100644 --- a/src/headless/main.cpp +++ b/src/headless/main.cpp @@ -4,7 +4,6 @@ #include "HeadlessUtils.h" #include "Player.h" -#include "Stress.h" #include "SurgeError.h" #include "Tunings.h" @@ -16,7 +15,7 @@ void statsFromPlayingEveryPatch() ** and a scale then asking headless to map it onto every patch ** and call us back with a result */ - SurgeSynthesizer* surge = Surge::Headless::createSurge(44100); + auto surge = Surge::Headless::createSurge(44100); Surge::Headless::playerEvents_t scale = Surge::Headless::make120BPMCMajorQuarterNoteScale(0, 44100); @@ -57,13 +56,12 @@ void statsFromPlayingEveryPatch() }; Surge::Headless::playOnEveryPatch(surge, scale, callBack); - delete surge; } void playSomeBach() { - SurgeSynthesizer* surge = Surge::Headless::createSurge(44100); + auto surge = Surge::Headless::createSurge(44100); std::string tmpdir = "/tmp"; std::string fname = tmpdir + "/988-v05.mid";