diff --git a/src/common/dsp/SurgeVoice.cpp b/src/common/dsp/SurgeVoice.cpp index d302d4ab435..4522722f56c 100644 --- a/src/common/dsp/SurgeVoice.cpp +++ b/src/common/dsp/SurgeVoice.cpp @@ -431,11 +431,15 @@ template void SurgeVoice::calc_ctrldata(QuadFilterChainState* Q, in else pb *= (float)scene->pbrange_dn.val.i; + octaveSize = 12.0f; + if( ! storage->isStandardTuning ) + octaveSize = storage->currentScale.count; + state.pitch = state.pkey + pb + localcopy[pitch_id].f * (scene->pitch.extend_range ? 12.f : 1.f) + - (12.0f * localcopy[octave_id].i); + (octaveSize * localcopy[octave_id].i); modsources[ms_keytrack]->output = - (state.pitch - (float)scene->keytrack_root.val.i) * (1.0f / 12.0f); + (state.pitch - (float)scene->keytrack_root.val.i) * (1.0f / 12.0f); // I didn't change this for octaveSize, I think rightly if (scene->modsource_doprocess[ms_polyaftertouch]) { @@ -533,7 +537,7 @@ bool SurgeVoice::process_block(QuadFilterChainState& Q, int Qe) (osc1 && (FMmode == fm_2and3to1))) { osc[2]->process_block(noteShiftFromPitchParam( (scene->osc[2].keytrack.val.b ? state.pitch : ktrkroot) + - 12 * scene->osc[2].octave.val.i, + octaveSize * scene->osc[2].octave.val.i, 2 ), drift, is_wide); @@ -560,7 +564,7 @@ bool SurgeVoice::process_block(QuadFilterChainState& Q, int Qe) if (FMmode == fm_3to2to1) { osc[1]->process_block(noteShiftFromPitchParam((scene->osc[1].keytrack.val.b ? state.pitch : ktrkroot) + - 12 * scene->osc[1].octave.val.i, + octaveSize * scene->osc[1].octave.val.i, 1 ), drift, is_wide, true, @@ -569,7 +573,7 @@ bool SurgeVoice::process_block(QuadFilterChainState& Q, int Qe) else { osc[1]->process_block(noteShiftFromPitchParam((scene->osc[1].keytrack.val.b ? state.pitch : ktrkroot) + - 12 * scene->osc[1].octave.val.i, + octaveSize * scene->osc[1].octave.val.i, 1), drift, is_wide); } @@ -598,14 +602,14 @@ bool SurgeVoice::process_block(QuadFilterChainState& Q, int Qe) { add_block(osc[1]->output, osc[2]->output, fmbuffer, BLOCK_SIZE_OS_QUAD); osc[0]->process_block(noteShiftFromPitchParam((scene->osc[0].keytrack.val.b ? state.pitch : ktrkroot) + - 12 * scene->osc[0].octave.val.i, 0 ), + octaveSize * scene->osc[0].octave.val.i, 0 ), drift, is_wide, true, db_to_linear(localcopy[scene->fm_depth.param_id_in_scene].f)); } else if (FMmode) { osc[0]->process_block(noteShiftFromPitchParam((scene->osc[0].keytrack.val.b ? state.pitch : 60) + - 12 * scene->osc[0].octave.val.i, + octaveSize * scene->osc[0].octave.val.i, 0), drift, is_wide, true, @@ -614,7 +618,7 @@ bool SurgeVoice::process_block(QuadFilterChainState& Q, int Qe) else { osc[0]->process_block(noteShiftFromPitchParam((scene->osc[0].keytrack.val.b ? state.pitch : ktrkroot) + - 12 * scene->osc[0].octave.val.i, + octaveSize * scene->osc[0].octave.val.i, 0), drift, is_wide); } diff --git a/src/common/dsp/SurgeVoice.h b/src/common/dsp/SurgeVoice.h index e88f2229f0c..96dafbb693b 100644 --- a/src/common/dsp/SurgeVoice.h +++ b/src/common/dsp/SurgeVoice.h @@ -156,6 +156,8 @@ class alignas(16) SurgeVoice pdata* paramptr; int route[6]; + float octaveSize = 12.0f; + bool osc1, osc2, osc3, ring12, ring23, noise; int FMmode; float noisegenL[2], noisegenR[2]; diff --git a/src/headless/UnitTests.cpp b/src/headless/UnitTests.cpp index 5646f1e2e43..f30d7f2d398 100644 --- a/src/headless/UnitTests.cpp +++ b/src/headless/UnitTests.cpp @@ -568,6 +568,147 @@ TEST_CASE( "Zero Size Maps", "[tun]" ) } +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);