diff --git a/include/sst/basic-blocks/modulators/AHDSRShapedSC.h b/include/sst/basic-blocks/modulators/AHDSRShapedSC.h index 56ea17a..2f4d9f6 100644 --- a/include/sst/basic-blocks/modulators/AHDSRShapedSC.h +++ b/include/sst/basic-blocks/modulators/AHDSRShapedSC.h @@ -66,7 +66,21 @@ struct AHDSRShapedSC : DiscreteStagesEnvelope inline float dPhase(float x) { - return srProvider->envelope_rate_linear_nowrap(x * base_t::etScale + base_t::etMin); + if constexpr (RangeProvider::phaseStrategy == DPhaseStrategies::ENVTIME_2TWOX) + { + return srProvider->envelope_rate_linear_nowrap(x * base_t::etScale + base_t::etMin); + } + + if constexpr (RangeProvider::phaseStrategy == ENVTIME_EXP) + { + auto timeInSeconds = + (std::exp(RangeProvider::A + x * (RangeProvider::B - RangeProvider::A)) - + RangeProvider::C) / + RangeProvider::D; + auto dPhase = BLOCK_SIZE * srProvider->sampleRateInv / timeInSeconds; + + return dPhase; + } } // from https://martin.ankerl.com/2012/01/25/optimized-approximative-pow-in-c-and-cpp/ diff --git a/include/sst/basic-blocks/modulators/DAREnvelope.h b/include/sst/basic-blocks/modulators/DAREnvelope.h index f31019b..05bdb89 100644 --- a/include/sst/basic-blocks/modulators/DAREnvelope.h +++ b/include/sst/basic-blocks/modulators/DAREnvelope.h @@ -66,6 +66,26 @@ struct DAREnvelope : DiscreteStagesEnvelope base_t::resetCurrent(); } + // The value here is in natural units by now + inline float dPhase(float x) + { + if constexpr (RangeProvider::phaseStrategy == DPhaseStrategies::ENVTIME_2TWOX) + { + return srProvider->envelope_rate_linear_nowrap(x); + } + + if constexpr (RangeProvider::phaseStrategy == ENVTIME_EXP) + { + auto timeInSeconds = + (std::exp(RangeProvider::A + x * (RangeProvider::B - RangeProvider::A)) - + RangeProvider::C) / + RangeProvider::D; + auto dPhase = BLOCK_SIZE * srProvider->sampleRateInv / timeInSeconds; + + return dPhase; + } + } + template inline float stepDigital(const float d, const float a, const float r) { float target = 0; @@ -73,7 +93,7 @@ struct DAREnvelope : DiscreteStagesEnvelope { case base_t::s_delay: { - phase += srProvider->envelope_rate_linear_nowrap(d); + phase += dPhase(d); if (phase >= 1) { if constexpr (gated) @@ -91,7 +111,7 @@ struct DAREnvelope : DiscreteStagesEnvelope break; case base_t::s_attack: { - phase += srProvider->envelope_rate_linear_nowrap(a); + phase += dPhase(a); if (phase >= 1) { phase = 1; @@ -123,7 +143,7 @@ struct DAREnvelope : DiscreteStagesEnvelope break; case base_t::s_release: { - phase -= srProvider->envelope_rate_linear_nowrap(r); + phase -= dPhase(r); if (phase <= 0) { phase = 0; diff --git a/include/sst/basic-blocks/modulators/DiscreteStagesEnvelope.h b/include/sst/basic-blocks/modulators/DiscreteStagesEnvelope.h index b2508f3..c360733 100644 --- a/include/sst/basic-blocks/modulators/DiscreteStagesEnvelope.h +++ b/include/sst/basic-blocks/modulators/DiscreteStagesEnvelope.h @@ -29,25 +29,45 @@ namespace sst::basic_blocks::modulators { - +enum DPhaseStrategies +{ + /* + * SR Provider provides envelope_rate_linear_nowrap(f) = blockSize * 2^-f / sampleRate + * and we scale into units in the 2^x space + */ + ENVTIME_2TWOX, + /* + * SRProvider isn't consulted. Instead we use an exp table (probably. For now use exp) + */ + ENVTIME_EXP +}; struct TenSecondRange { // 0.0039s -> 10s + static constexpr DPhaseStrategies phaseStrategy{ENVTIME_2TWOX}; static constexpr float etMin{-8}, etMax{3.32192809489}; }; struct ThirtyTwoSecondRange { // 0.0039s -> 32s + static constexpr DPhaseStrategies phaseStrategy{ENVTIME_2TWOX}; static constexpr float etMin{-8}, etMax{5}; }; struct TwoMinuteRange { // 0.0039s -> 120s + static constexpr DPhaseStrategies phaseStrategy{ENVTIME_2TWOX}; static constexpr float etMin{-8}, etMax{6.90689059561}; }; +struct TwentyFiveSecondExp +{ + static constexpr DPhaseStrategies phaseStrategy{ENVTIME_EXP}; + static constexpr double A{0.6931471824646}, B{10.1267113685608}, C{-2.0}, D{1000.0}; +}; + template struct DiscreteStagesEnvelope { static constexpr float etMin{RangeProvider::etMin}, etMax{RangeProvider::etMax}, @@ -198,9 +218,28 @@ template struct DiscreteStagesEnvelope memset(outputCacheCubed, 0, sizeof(outputCacheCubed)); } - float rateFrom01(float r01) { return r01 * etScale + etMin; } - float rateTo01(float r) { return (r - etMin) / etScale; } - float deltaTo01(float d) { return d / etScale; } + float rateFrom01(float r01) + { + // EXP works entirely in normalized params + if constexpr (RangeProvider::phaseStrategy == DPhaseStrategies::ENVTIME_EXP) + return r01; + else + return r01 * etScale + etMin; + } + float rateTo01(float r) + { + if constexpr (RangeProvider::phaseStrategy == DPhaseStrategies::ENVTIME_EXP) + return r; + else + return (r - etMin) / etScale; + } + float deltaTo01(float d) + { + if constexpr (RangeProvider::phaseStrategy == DPhaseStrategies::ENVTIME_EXP) + return d; + else + return d / etScale; + } }; } // namespace sst::basic_blocks::modulators diff --git a/include/sst/basic-blocks/params/ParamMetadata.h b/include/sst/basic-blocks/params/ParamMetadata.h index 758ee52..91d63f8 100644 --- a/include/sst/basic-blocks/params/ParamMetadata.h +++ b/include/sst/basic-blocks/params/ParamMetadata.h @@ -219,11 +219,12 @@ struct ParamMetaData enum DisplayScale { - LINEAR, // out = A * r + B - A_TWO_TO_THE_B, // out = A 2^(B r + C) - CUBED_AS_DECIBEL, // the underlier is an amplitude applied as v*v*v and displayed as db - DECIBEL, // TODO - implement - UNORDERED_MAP, // out = discreteValues[(int)std::round(val)] + LINEAR, // out = A * r + B + A_TWO_TO_THE_B, // out = A 2^(B r + C) + CUBED_AS_DECIBEL, // the underlier is an amplitude applied as v*v*v and displayed as db + SCALED_OFFSET_EXP, // (exp(A + x ( B - A )) + C) / D + DECIBEL, // TODO - implement + UNORDERED_MAP, // out = discreteValues[(int)std::round(val)] MIDI_NOTE, // uses C4 etc.. notation. The octaveOffset has 0 -> 69=A4, 1 = A5, -1 = A3 USER_PROVIDED // TODO - implement } displayScale{LINEAR}; @@ -440,6 +441,20 @@ struct ParamMetaData return res; } + ParamMetaData withScaledOffsetExpFormatting(float A, float B, float C, float D, + const std::string &units) + { + auto res = *this; + res.svA = A; + res.svB = B; + res.svC = C; + res.svD = D; + res.unit = units; + res.displayScale = SCALED_OFFSET_EXP; + res.supportsStringConversion = true; + return res; + } + ParamMetaData withSemitoneZeroAt400Formatting() { return withATwoToTheBFormatting(440, 1.0 / 12.0, "Hz"); @@ -644,6 +659,17 @@ struct ParamMetaData } ParamMetaData asEnvelopeTime() { return asLog2SecondsRange(-8.f, 5.f, -1.f); } + // (exp(lerp(norm_val, 0.6931471824646, 10.1267113685608)) - 2.0)/1000.0 + ParamMetaData as25SecondExpTime() + { + return withType(FLOAT) + .withRange(0, 1) + .withDefault(0.1) + .temposyncable() + .withScaledOffsetExpFormatting(0.6931471824646, 10.1267113685608, -2.0, 1000.0, "s") + .withMilisecondsBelowOneSecond(); + } + ParamMetaData asAudibleFrequency() { return withType(FLOAT).withRange(-60, 70).withDefault(0).withSemitoneZeroAt400Formatting(); @@ -746,15 +772,6 @@ inline std::optional ParamMetaData::valueToString(float val, switch (displayScale) { case LINEAR: - if (val == minVal && !customMinDisplay.empty()) - { - return customMinDisplay; - } - if (val == maxVal && !customMaxDisplay.empty()) - { - return customMinDisplay; - } - if (alternateScaleWhen == NO_ALTERNATE) { return fmt::format("{:.{}f} {:s}", svA * val, @@ -780,15 +797,6 @@ inline std::optional ParamMetaData::valueToString(float val, } break; case A_TWO_TO_THE_B: - if (val == minVal && !customMinDisplay.empty()) - { - return customMinDisplay; - } - if (val == maxVal && !customMaxDisplay.empty()) - { - return customMinDisplay; - } - if (alternateScaleWhen == NO_ALTERNATE) { return fmt::format("{:.{}f} {:s}", svA * pow(2.0, svB * val + svC), @@ -813,6 +821,22 @@ inline std::optional ParamMetaData::valueToString(float val, } } break; + case SCALED_OFFSET_EXP: + { + auto dval = (std::exp(svA + val * (svB - svA)) + svC) / svD; + if (alternateScaleWhen == NO_ALTERNATE || + (alternateScaleWhen == SCALE_BELOW && dval > alternateScaleCutoff) || + (alternateScaleWhen == SCALE_ABOVE && dval < alternateScaleCutoff)) + { + return fmt::format("{:.{}f} {:s}", dval, + (fs.isHighPrecision ? (decimalPlaces + 4) : decimalPlaces), unit); + } + // We must be in an alternate case + return fmt::format("{:.{}f} {:s}", dval * alternateScaleRescaling, + (fs.isHighPrecision ? (decimalPlaces + 4) : decimalPlaces), + alternateScaleUnits); + } + break; case CUBED_AS_DECIBEL: { if (val <= 0) @@ -969,6 +993,39 @@ inline std::optional ParamMetaData::valueFromString(std::string_view v, s } } break; + case SCALED_OFFSET_EXP: + { + try + { + auto r = std::stof(std::string(v)); + + if (alternateScaleWhen != NO_ALTERNATE) + { + auto ps = v.find(alternateScaleUnits); + if (ps != std::string::npos && alternateScaleRescaling != 0.f) + { + // We have a string containing the alterante units + r = r / alternateScaleRescaling; + } + } + + // OK so its R = exp(A + X (B-A)) - C)/D + // D R + C = exp(A + X (B-a)) + // log(DR + C) = A + X (B-A) + // (log (DR + C) - A) / (B - A) = X + auto drc = std::max(svD * r + svC, 0.00000001f); + auto xv = (std::log(drc) - svA) / (svB - svA); + + return xv; + } + catch (const std::exception &) + { + errMsg = rangeMsg(); + return std::nullopt; + } + return 0.f; + } + break; case CUBED_AS_DECIBEL: { try @@ -1111,6 +1168,115 @@ ParamMetaData::modulationNaturalToString(float naturalBaseVal, float modulationN return result; } + case SCALED_OFFSET_EXP: + { + auto nvu = std::clamp(naturalBaseVal + modulationNatural, 0.f, 1.f); + auto nvd = std::clamp(naturalBaseVal - modulationNatural, 0.f, 1.f); + auto nv = std::clamp(naturalBaseVal, 0.f, 1.f); + + auto v = (exp(svA + nv * (svB - svA)) - svC) / svD; + auto vu = (exp(svA + nvu * (svB - svA)) - svC) / svD; + auto vd = (exp(svA + nvd * (svB - svA)) - svC) / svD; + + auto deltUp = vu - v; + auto deltDn = vd - v; + + auto dp = (fs.isHighPrecision ? (decimalPlaces + 4) : decimalPlaces); + result.value = fmt::format("{:.{}f} {}", deltUp, dp, unit); + if (isBipolar) + { + if (deltDn > 0) + { + result.summary = fmt::format("+/- {:.{}f} {}", deltUp, dp, unit); + } + else + { + result.summary = fmt::format("-/+ {:.{}f} {}", -deltUp, dp, unit); + } + } + else + { + result.summary = fmt::format("{:.{}f} {}", deltUp, dp, unit); + } + result.changeUp = fmt::format("{:.{}f}", deltUp, dp); + if (isBipolar) + result.changeDown = fmt::format("{:.{}f}", deltDn, dp); + result.valUp = fmt::format("{:.{}f}", vu, dp); + + if (isBipolar) + result.valDown = fmt::format("{:.{}f}", vd, dp); + auto v2s = valueToString(naturalBaseVal, fs); + if (v2s.has_value()) + result.baseValue = *v2s; + else + result.baseValue = "-ERROR-"; + + if (isBipolar) + result.singleLineModulationSummary = fmt::format( + "{} {} < {} > {} {}", result.valDown, unit, result.baseValue, result.valUp, unit); + else + result.singleLineModulationSummary = + fmt::format("{} > {} {}", result.baseValue, result.valUp, unit); + + return result; + } + break; + case CUBED_AS_DECIBEL: + { + auto nvu = std::max(naturalBaseVal + modulationNatural, 0.f); + auto nvd = std::max(naturalBaseVal - modulationNatural, 0.f); + auto v = std::max(naturalBaseVal, 0.f); + + nvu = nvu * nvu * nvu; + nvd = nvd * nvd * nvd; + + auto db = 20 * std::log10(v); + auto dbu = 20 * std::log10(nvu); + auto dbd = 20 * std::log10(nvd); + + auto deltUp = dbu - db; + auto deltDn = dbd - db; + + auto dp = (fs.isHighPrecision ? (decimalPlaces + 4) : decimalPlaces); + result.value = fmt::format("{:.{}f} {}", deltUp, dp, unit); + if (isBipolar) + { + if (deltDn > 0) + { + result.summary = fmt::format("+/- {:.{}f} {}", deltUp, dp, unit); + } + else + { + result.summary = fmt::format("-/+ {:.{}f} {}", -deltUp, dp, unit); + } + } + else + { + result.summary = fmt::format("{:.{}f} {}", deltUp, dp, unit); + } + result.changeUp = fmt::format("{:.{}f}", deltUp, dp); + if (isBipolar) + result.changeDown = fmt::format("{:.{}f}", deltDn, dp); + result.valUp = fmt::format("{:.{}f}", dbu, dp); + + if (isBipolar) + result.valDown = fmt::format("{:.{}f}", dbd, dp); + auto v2s = valueToString(naturalBaseVal, fs); + if (v2s.has_value()) + result.baseValue = *v2s; + else + result.baseValue = "-ERROR-"; + + if (isBipolar) + result.singleLineModulationSummary = fmt::format( + "{} {} < {} > {} {}", result.valDown, unit, result.baseValue, result.valUp, unit); + else + result.singleLineModulationSummary = + fmt::format("{} > {} {}", result.baseValue, result.valUp, unit); + + return result; + } + break; default: break; } diff --git a/tests/param_tests.cpp b/tests/param_tests.cpp index e42ff95..ca06dd3 100644 --- a/tests/param_tests.cpp +++ b/tests/param_tests.cpp @@ -328,3 +328,30 @@ TEST_CASE("Alternate Scales Above and Below", "[param]") REQUIRE(*(p.valueFromString("500.00 ms", em)) == Approx(-1.f)); } } + +TEST_CASE("25 Second Exp", "[param]") +{ + SECTION("To String") + { + auto p = pmd::ParamMetaData().as25SecondExpTime(); + REQUIRE(*(p.valueToString(0.8)) == "3.79 s"); + REQUIRE(*(p.valueToString(0.5)) == "221.62 ms"); + } + + SECTION("From String") + { + auto p = pmd::ParamMetaData().as25SecondExpTime(); + std::string em; + // remember these are truncated displays hence the margin + REQUIRE(*(p.valueFromString("3.79 s", em)) == Approx(0.8f).margin(0.01)); + REQUIRE(*(p.valueFromString("0.22162 s", em)) == Approx(0.5f).margin(0.01)); + REQUIRE(*(p.valueFromString("221.62 ms", em)) == Approx(0.5f).margin(0.01)); + } + + SECTION("Modulation") + { + auto p = pmd::ParamMetaData().as25SecondExpTime(); + auto md = p.modulationNaturalToString(0.5, 0.1, true, pmd::ParamMetaData::FeatureState()); + REQUIRE(md.has_value()); + } +}