212 lines
13 KiB
C++
212 lines
13 KiB
C++
// deck_values.cpp — see deck_values.h. Pure value math; no host types.
|
|
|
|
#include "core/instrument/ui/deck_values.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
|
|
#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<float>(clamp01(value)); break;
|
|
case DeckParam::kFilterCutoff:
|
|
play.filter.settings.cutoffNorm = static_cast<float>(clamp01(value)); break;
|
|
case DeckParam::kFilterQ:
|
|
play.filter.settings.resonanceNorm = static_cast<float>(clamp01(value)); break;
|
|
case DeckParam::kFilterDrive:
|
|
play.filter.settings.driveNorm = static_cast<float>(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
|