// deck_values.cpp — see deck_values.h. Pure value math; no host types. #include "core/instrument/ui/deck_values.h" #include #include #include "core/instrument/engine/filter/filter_morph.h" // MorphLaw (the law toggle's value) #include "core/util/clamp01.h" #include "core/util/curve_law.h" // the ONE curve-exponent domain namespace reasampler::instrument::ui { using engine::filter::MorphLaw; using util::clamp01; namespace { double secToNorm(double seconds) { return clamp01(seconds / kEnvTimeMaxSeconds); } double normToSec(double norm) { return clamp01(norm) * kEnvTimeMaxSeconds; } } // namespace double deckParamNorm(DeckParam id, const PlaySeconds& play) { switch (id) { case DeckParam::kPlayMode: return play.playMode == PlayMode::Trigger ? 1.0 : 0.0; case DeckParam::kAmpEnvMode: return play.ampSpline.mode == EnvMode::Spline ? 1.0 : 0.0; case DeckParam::kPitchEnvMode: return play.pitchSpline.mode == EnvMode::Spline ? 1.0 : 0.0; case DeckParam::kFilterEnvMode: return play.filterSpline.mode == EnvMode::Spline ? 1.0 : 0.0; case DeckParam::kPitchEngine: return play.pitchEngine == PitchEngine::Preserve ? 1.0 : 0.0; case DeckParam::kAttack: return secToNorm(play.adsr.attackSeconds); case DeckParam::kHold: return secToNorm(play.adsr.holdSeconds); case DeckParam::kDecay: return secToNorm(play.adsr.decaySeconds); case DeckParam::kSustain: return clamp01(play.adsr.sustainLevel); case DeckParam::kRelease: return secToNorm(play.adsr.releaseSeconds); case DeckParam::kAttackCurve: return util::knobNormFromCurve(play.adsr.attackCurve); case DeckParam::kDecayCurve: return util::knobNormFromCurve(play.adsr.decayCurve); case DeckParam::kReleaseCurve: return util::knobNormFromCurve(play.adsr.releaseCurve); case DeckParam::kTrigLength: return clamp01(play.trigger.lengthFraction); case DeckParam::kTrigAttack: return secToNorm(play.trigAhd.attackSeconds); case DeckParam::kTrigHold: return clamp01(play.trigAhd.holdFraction); case DeckParam::kTrigDecay: return secToNorm(play.trigAhd.decaySeconds); case DeckParam::kTrigAttackCurve: return util::knobNormFromCurve(play.trigAhd.attackCurve); case DeckParam::kTrigDecayCurve: return util::knobNormFromCurve(play.trigAhd.decayCurve); case DeckParam::kPitchEnvEnable: return play.pitchEnv.enabled ? 1.0 : 0.0; case DeckParam::kPitchEnvAttack: return secToNorm(play.pitchEnv.shape.attackSeconds); case DeckParam::kPitchEnvHold: return clamp01(play.pitchEnv.shape.holdFraction); case DeckParam::kPitchEnvDecay: return secToNorm(play.pitchEnv.shape.decaySeconds); case DeckParam::kPitchEnvAttackCurve: return util::knobNormFromCurve(play.pitchEnv.shape.attackCurve); case DeckParam::kPitchEnvDecayCurve: return util::knobNormFromCurve(play.pitchEnv.shape.decayCurve); case DeckParam::kPitchEnvDepth: // Signed depth centred at 0.5 (0.5 == 0 semitones). return clamp01(0.5 + play.pitchEnv.peakSemitones / (2.0 * kPitchDepthMaxSemis)); // Filter. The four tone controls ARE the module's normalized positions — stored and // shown as-is, so the knob travel is exactly filter_params' own law. case DeckParam::kFilterEnable: return play.filter.enabled ? 1.0 : 0.0; case DeckParam::kFilterLaw: return play.filter.settings.morphLaw == MorphLaw::HighNotchLow ? 1.0 : 0.0; case DeckParam::kFilterMorph: return clamp01(play.filter.settings.morphNorm); case DeckParam::kFilterCutoff: return clamp01(play.filter.settings.cutoffNorm); case DeckParam::kFilterQ: return clamp01(play.filter.settings.resonanceNorm); case DeckParam::kFilterDrive: return clamp01(play.filter.settings.driveNorm); case DeckParam::kFilterModAmt: return deckNormFromBipolar(play.filter.modAmount); case DeckParam::kFilterVel: return deckNormFromBipolar(play.filter.velAmount); case DeckParam::kFilterKeyTrack: return clamp01(play.filter.keyTrack / kKeyTrackMax); case DeckParam::kFilterEnvAttack: return secToNorm(play.filter.env.attackSeconds); case DeckParam::kFilterEnvHold: return secToNorm(play.filter.env.holdSeconds); case DeckParam::kFilterEnvDecay: return secToNorm(play.filter.env.decaySeconds); case DeckParam::kFilterEnvSustain: return clamp01(play.filter.env.sustainLevel); case DeckParam::kFilterEnvRelease: return secToNorm(play.filter.env.releaseSeconds); case DeckParam::kFilterEnvAttackCurve: return util::knobNormFromCurve(play.filter.env.attackCurve); case DeckParam::kFilterEnvDecayCurve: return util::knobNormFromCurve(play.filter.env.decayCurve); case DeckParam::kFilterEnvReleaseCurve: return util::knobNormFromCurve(play.filter.env.releaseCurve); case DeckParam::kFilterTrigAttack: return secToNorm(play.filter.trigEnv.attackSeconds); case DeckParam::kFilterTrigHold: return clamp01(play.filter.trigEnv.holdFraction); case DeckParam::kFilterTrigDecay: return secToNorm(play.filter.trigEnv.decaySeconds); case DeckParam::kFilterTrigAttackCurve: return util::knobNormFromCurve(play.filter.trigEnv.attackCurve); case DeckParam::kFilterTrigDecayCurve: return util::knobNormFromCurve(play.filter.trigEnv.decayCurve); default: return 0.0; } } void setDeckParam(DeckParam id, PlaySeconds& play, double value, int segment) { switch (id) { case DeckParam::kPlayMode: // Gate is refused while any EG is drawn — see splineActive (play_params.h). The // segment paints Disabled for the same reason, so the refusal is never a surprise. if (segment == 0 && splineActive(play)) break; play.playMode = (segment == 1) ? PlayMode::Trigger : PlayMode::Gate; break; // Switching TO Spline drops Gate, which the spline model has no place for. Switching // back does NOT restore it: the previous mode is not stored, and silently re-gating an // instrument the user has since heard as a one-shot is the worse surprise. case DeckParam::kAmpEnvMode: case DeckParam::kPitchEnvMode: case DeckParam::kFilterEnvMode: { const EnvMode m = (segment == 1) ? EnvMode::Spline : EnvMode::Staged; if (id == DeckParam::kAmpEnvMode) play.ampSpline.mode = m; else if (id == DeckParam::kPitchEnvMode) play.pitchSpline.mode = m; else play.filterSpline.mode = m; break; } case DeckParam::kPitchEngine: play.pitchEngine = (segment == 1) ? PitchEngine::Preserve : PitchEngine::Varispeed; break; case DeckParam::kAttack: play.adsr.attackSeconds = normToSec(value); break; case DeckParam::kHold: play.adsr.holdSeconds = normToSec(value); break; case DeckParam::kDecay: play.adsr.decaySeconds = normToSec(value); break; case DeckParam::kSustain: play.adsr.sustainLevel = clamp01(value); break; case DeckParam::kRelease: play.adsr.releaseSeconds = normToSec(value); break; case DeckParam::kAttackCurve: play.adsr.attackCurve = util::curveFromKnobNorm(value); break; case DeckParam::kDecayCurve: play.adsr.decayCurve = util::curveFromKnobNorm(value); break; case DeckParam::kReleaseCurve: play.adsr.releaseCurve = util::curveFromKnobNorm(value); break; case DeckParam::kTrigLength: // lengthFraction is (0,1]; keep a small floor so a zero-length trigger never plays // nothing. play.trigger.lengthFraction = (std::max)(0.01, clamp01(value)); break; case DeckParam::kTrigAttack: play.trigAhd.attackSeconds = normToSec(value); break; case DeckParam::kTrigHold: play.trigAhd.holdFraction = clamp01(value); break; case DeckParam::kTrigDecay: play.trigAhd.decaySeconds = normToSec(value); break; case DeckParam::kTrigAttackCurve: play.trigAhd.attackCurve = util::curveFromKnobNorm(value); break; case DeckParam::kTrigDecayCurve: play.trigAhd.decayCurve = util::curveFromKnobNorm(value); break; case DeckParam::kPitchEnvEnable: play.pitchEnv.enabled = (segment == 1); break; case DeckParam::kPitchEnvAttack: play.pitchEnv.shape.attackSeconds = normToSec(value); break; case DeckParam::kPitchEnvHold: play.pitchEnv.shape.holdFraction = clamp01(value); break; case DeckParam::kPitchEnvDecay: play.pitchEnv.shape.decaySeconds = normToSec(value); break; case DeckParam::kPitchEnvAttackCurve: play.pitchEnv.shape.attackCurve = util::curveFromKnobNorm(value); break; case DeckParam::kPitchEnvDecayCurve: play.pitchEnv.shape.decayCurve = util::curveFromKnobNorm(value); break; case DeckParam::kPitchEnvDepth: play.pitchEnv.peakSemitones = (clamp01(value) - 0.5) * 2.0 * kPitchDepthMaxSemis; break; case DeckParam::kFilterEnable: play.filter.enabled = (segment == 1); break; case DeckParam::kFilterLaw: play.filter.settings.morphLaw = (segment == 1) ? MorphLaw::HighNotchLow : MorphLaw::HighBandLow; break; case DeckParam::kFilterMorph: play.filter.settings.morphNorm = static_cast(clamp01(value)); break; case DeckParam::kFilterCutoff: play.filter.settings.cutoffNorm = static_cast(clamp01(value)); break; case DeckParam::kFilterQ: play.filter.settings.resonanceNorm = static_cast(clamp01(value)); break; case DeckParam::kFilterDrive: play.filter.settings.driveNorm = static_cast(clamp01(value)); break; case DeckParam::kFilterModAmt: play.filter.modAmount = deckBipolarFromNorm(value); break; case DeckParam::kFilterVel: play.filter.velAmount = deckBipolarFromNorm(value); break; case DeckParam::kFilterKeyTrack: play.filter.keyTrack = clamp01(value) * kKeyTrackMax; break; case DeckParam::kFilterEnvAttack: play.filter.env.attackSeconds = normToSec(value); break; case DeckParam::kFilterEnvHold: play.filter.env.holdSeconds = normToSec(value); break; case DeckParam::kFilterEnvDecay: play.filter.env.decaySeconds = normToSec(value); break; case DeckParam::kFilterEnvSustain: play.filter.env.sustainLevel = clamp01(value); break; case DeckParam::kFilterEnvRelease: play.filter.env.releaseSeconds = normToSec(value); break; case DeckParam::kFilterEnvAttackCurve: play.filter.env.attackCurve = util::curveFromKnobNorm(value); break; case DeckParam::kFilterEnvDecayCurve: play.filter.env.decayCurve = util::curveFromKnobNorm(value); break; case DeckParam::kFilterEnvReleaseCurve: play.filter.env.releaseCurve = util::curveFromKnobNorm(value); break; case DeckParam::kFilterTrigAttack: play.filter.trigEnv.attackSeconds = normToSec(value); break; case DeckParam::kFilterTrigHold: play.filter.trigEnv.holdFraction = clamp01(value); break; case DeckParam::kFilterTrigDecay: play.filter.trigEnv.decaySeconds = normToSec(value); break; case DeckParam::kFilterTrigAttackCurve: play.filter.trigEnv.attackCurve = util::curveFromKnobNorm(value); break; case DeckParam::kFilterTrigDecayCurve: play.filter.trigEnv.decayCurve = util::curveFromKnobNorm(value); break; default: break; } // ONE normalization point for every control that can flip splineActive — a mode toggle // (above) or an enable toggle (kPitchEnvEnable/kFilterEnable), whose enabling can make an // already-Spline pitch/filter envelope newly active. Applying it once here, rather than at // each site that could cause the flip, is what keeps a future such control from reopening // the same hole. `resolvePlay` (sample_map.cpp) is the other caller of the shared helper. enforceGateUnavailableWhileDrawn(play); } void resetDeckParam(DeckParam id, PlaySeconds& play) { const PlaySeconds defaults; setDeckParam(id, play, deckParamNorm(id, defaults), 0); } void formatEnvTimeMs(double seconds, char* buf, std::size_t len) { if (!buf || len == 0) return; const double ms = seconds * 1000.0; std::snprintf(buf, len, ms < 10.0 ? "%.1f ms" : "%.0f ms", ms); } } // namespace reasampler::instrument::ui