// editor_controls.cpp — the ReaSamplerEditor's parameter plumbing: the band-stack layout // resolve every paint/hit-test path shares, the shell's half of the control-value binding (the // per-instance controls the parameter set does not carry — key-track, voice count, master gain, // preview velocity — plus the value labels), and the node-drag clamp bounds. The parameter-set // half is the pure `deck_values` module. The orthogonal half — which stored struct each editor // selection names — is editor_models. Value logic only: no painting, no window plumbing. #include "shell/instrument/reasampler_editor.h" #include #include #include // snprintf (deck value labels) #include #include #include "core/instrument/engine/filter/filter_params.h" // the filter's own control laws #include "core/instrument/engine/master_gain.h" // master-gain dB<->linear<->knob taper #include "core/instrument/ui/bake_hold.h" // the Hold knob's ladder map #include "core/instrument/ui/deck_groups.h" // sampleDeckGroups (the deck's composition) #include "core/instrument/ui/deck_values.h" // the parameter-set binding + its ms units #include "core/instrument/ui/knob_deck.h" // deckHeight / kDeckKnobSize (the band's own height) #include "core/util/clamp01.h" #include "core/util/curve_law.h" // the ONE curve-exponent domain #include "shell/instrument/editor_internal.h" #include "shell/instrument/reasampler_processor.h" namespace reasampler::vst { using namespace reasampler::instrument::map; // PlaySeconds vocabulary using instrument::ui::computeSampleBands; using instrument::ui::chromeRects; using instrument::ui::deckHeight; using instrument::ui::kDeckKnobSize; using instrument::ui::kPad; using instrument::ui::deckParamNorm; using instrument::ui::formatEnvTimeMs; using instrument::ui::kEnvTimeMaxSeconds; using instrument::ui::kKeyTrackMax; using instrument::ui::resetDeckParam; using instrument::ui::sampleDeckGroups; using instrument::ui::setDeckParam; using instrument::engine::formatMasterGainLabel; using instrument::engine::masterGainLinearFromNorm; using instrument::engine::masterGainNormFromLinear; using instrument::engine::filter::filterCutoffHzFromNorm; using instrument::engine::filter::filterDriveDepthFromNorm; using instrument::engine::filter::filterQFromNorm; using util::clamp01; namespace { // The raw stored curve exponent for a curve-dial control id, read DIRECTLY off the field — // never round-tripped through curveFromKnobNorm(knobNormFromCurve(x)): the knob-norm law's // centre detent (curve_law.h) snaps anything near-neutral back to exactly 1.0, so a round trip // can misreport a stored exponent that isn't neutral as "^1.00". double curveExponentFor(int id, const PlaySeconds& play) { using DeckParam = instrument::ui::DeckParam; // ReaSamplerEditor::ParamControl is an alias switch (static_cast(id)) { case DeckParam::kAttackCurve: return play.adsr.attackCurve; case DeckParam::kDecayCurve: return play.adsr.decayCurve; case DeckParam::kReleaseCurve: return play.adsr.releaseCurve; case DeckParam::kTrigAttackCurve: return play.trigAhd.attackCurve; case DeckParam::kTrigDecayCurve: return play.trigAhd.decayCurve; case DeckParam::kPitchEnvAttackCurve: return play.pitchEnv.shape.attackCurve; case DeckParam::kPitchEnvDecayCurve: return play.pitchEnv.shape.decayCurve; case DeckParam::kFilterEnvAttackCurve: return play.filter.env.attackCurve; case DeckParam::kFilterEnvDecayCurve: return play.filter.env.decayCurve; case DeckParam::kFilterEnvReleaseCurve: return play.filter.env.releaseCurve; case DeckParam::kFilterTrigAttackCurve: return play.filter.trigEnv.attackCurve; case DeckParam::kFilterTrigDecayCurve: return play.filter.trigEnv.decayCurve; default: return util::kCurveNeutral; } } } // namespace ReaSamplerEditor::FaceLayout ReaSamplerEditor::faceLayout(int w, int h) const { // The ONE resolve every paint and hit-test path goes through, so the band stack, the // chrome interior, and the deck descriptors can never be derived three different ways. // The deck's own wrapped height is the only interior measurement the allocator needs. FaceLayout fl; fl.deckDescs = sampleDeckGroups(params_.play.playMode); fl.bands = computeSampleBands(w, h, deckHeight(fl.deckDescs, w - 2 * kPad)); fl.chrome = chromeRects(fl.bands.chrome, kDeckKnobSize); return fl; } // The parameter-set binding is the pure deck_values module's; these three are the shell's thin // int-id adapters onto it (ParamControl is an alias of DeckParam). double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const { return deckParamNorm(static_cast(id), play); } void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value, int segment) const { setDeckParam(static_cast(id), play, value, segment); } void ReaSamplerEditor::resetParamControl(int id) { // kKeyTrack is the one knob whose value sits beside the play bundle, so it defaults from // InstrumentParams rather than from PlaySeconds — the same split applyParamControl draws. if (id == static_cast(ParamControl::kKeyTrack)) { params_.keyTrack = InstrumentParams{}.keyTrack; return; } resetDeckParam(static_cast(id), params_.play); } double ReaSamplerEditor::liveSampleRate() const { return processor_ ? processor_->sampleRate() : 0.0; } double ReaSamplerEditor::previewVelocity01() const { if (!processor_) return static_cast(kPreviewVelocityDefault) / 127.0; return static_cast(processor_->previewVelocity()) / 127.0; } double ReaSamplerEditor::bakeHoldNorm() const { return instrument::ui::bakeHoldNorm(params_.bakeHold); } double ReaSamplerEditor::deckControlNorm(int id) const { if (id == -2) return previewVelocity01(); // the chrome preview-velocity knob if (id == kBakeHoldKnobId) return bakeHoldNorm(); switch (static_cast(id)) { case ParamControl::kKeyTrack: return clamp01(params_.keyTrack / kKeyTrackMax); case ParamControl::kVoiceCount: return clamp01(static_cast(voiceCount_ - kMinVoiceCount) / static_cast(kMaxVoiceCount - kMinVoiceCount)); case ParamControl::kMasterGain: return masterGainNormFromLinear(processor_ ? processor_->masterGainLinear() : 1.0); default: return controlValue(id, params_.play); } } instrument::ui::DeckEnableState ReaSamplerEditor::deckEnableState() const { const PlaySeconds& play = params_.play; return instrument::ui::DeckEnableState{ play.pitchEnv.enabled, play.filter.enabled, play.ampSpline.mode == EnvMode::Spline, play.pitchSpline.mode == EnvMode::Spline, play.filterSpline.mode == EnvMode::Spline}; } void ReaSamplerEditor::applyDeckKnob(int id, double norm) { if (!processor_) return; norm = clamp01(norm); if (id == -2) { // Preview velocity: live processor write (persisted per-instance; the setter clamps // to MIDI 1..127 so the knob's bottom still strikes audibly). processor_->setPreviewVelocity(static_cast(norm * 127.0 + 0.5)); return; } if (id == kBakeHoldKnobId) { // A parameter-set edit like the deck's own knobs: the commit lands on release. params_.bakeHold = instrument::ui::bakeHoldFromNorm(norm); return; } switch (static_cast(id)) { case ParamControl::kVoiceCount: { // Stepped: quantize the continuous drag to the integer count and track it live // for the label/needle. The actual engine rebuild (setVoiceCount) fires ONCE on // WM_LBUTTONUP — not per step — so a full drag (~31 steps) costs one rebuild, // not thirty. const int count = kMinVoiceCount + static_cast(norm * (kMaxVoiceCount - kMinVoiceCount) + 0.5); voiceCount_ = count; return; } case ParamControl::kMasterGain: // Post-mixer gain: one atomic store; the audio thread picks it up next block. processor_->setMasterGainLinear(masterGainLinearFromNorm(norm)); return; default: applyParamControl(id, norm, 0); return; } } std::string ReaSamplerEditor::deckValueLabel(int id) const { char buf[24]; buf[0] = '\0'; const PlaySeconds& play = params_.play; switch (id < 0 ? ParamControl::kCount : static_cast(id)) { case ParamControl::kAttack: formatEnvTimeMs(play.adsr.attackSeconds, buf, sizeof(buf)); break; case ParamControl::kHold: formatEnvTimeMs(play.adsr.holdSeconds, buf, sizeof(buf)); break; case ParamControl::kDecay: formatEnvTimeMs(play.adsr.decaySeconds, buf, sizeof(buf)); break; case ParamControl::kSustain: snprintf(buf, sizeof(buf), "%.0f%%", play.adsr.sustainLevel * 100.0); break; case ParamControl::kRelease: formatEnvTimeMs(play.adsr.releaseSeconds, buf, sizeof(buf)); break; case ParamControl::kTrigLength: snprintf(buf, sizeof(buf), "%.0f%%", play.trigger.lengthFraction * 100.0); break; case ParamControl::kTrigAttack: formatEnvTimeMs(play.trigAhd.attackSeconds, buf, sizeof(buf)); break; case ParamControl::kTrigHold: snprintf(buf, sizeof(buf), "%.0f%%", play.trigAhd.holdFraction * 100.0); break; case ParamControl::kTrigDecay: formatEnvTimeMs(play.trigAhd.decaySeconds, buf, sizeof(buf)); break; case ParamControl::kPitchEnvAttack: formatEnvTimeMs(play.pitchEnv.shape.attackSeconds, buf, sizeof(buf)); break; case ParamControl::kPitchEnvHold: snprintf(buf, sizeof(buf), "%.0f%%", play.pitchEnv.shape.holdFraction * 100.0); break; case ParamControl::kPitchEnvDecay: formatEnvTimeMs(play.pitchEnv.shape.decaySeconds, buf, sizeof(buf)); break; case ParamControl::kPitchEnvDepth: snprintf(buf, sizeof(buf), "%+.1fst", play.pitchEnv.peakSemitones); break; case ParamControl::kKeyTrack: snprintf(buf, sizeof(buf), "%.0f%%", params_.keyTrack * 100.0); break; case ParamControl::kVoiceCount: snprintf(buf, sizeof(buf), "%d", voiceCount_); break; case ParamControl::kMasterGain: formatMasterGainLabel(deckControlNorm(id), buf, sizeof(buf)); break; // Filter readouts run the stored normalized positions back through the module's OWN // laws, so what the label says is what the kernel is solved for. case ParamControl::kFilterMorph: { const double m = play.filter.settings.morphNorm; snprintf(buf, sizeof(buf), "%.0f%%", m * 100.0); break; } case ParamControl::kFilterCutoff: { const float hz = filterCutoffHzFromNorm(play.filter.settings.cutoffNorm); if (hz >= 1000.0f) snprintf(buf, sizeof(buf), "%.2fk", hz / 1000.0f); else snprintf(buf, sizeof(buf), "%.0fHz", hz); break; } case ParamControl::kFilterQ: snprintf(buf, sizeof(buf), "%.2f", static_cast(filterQFromNorm(play.filter.settings.resonanceNorm))); break; case ParamControl::kFilterDrive: snprintf(buf, sizeof(buf), "%.2f", static_cast(filterDriveDepthFromNorm(play.filter.settings.driveNorm))); break; case ParamControl::kFilterModAmt: snprintf(buf, sizeof(buf), "%+.0f%%", play.filter.modAmount * 100.0); break; case ParamControl::kFilterVel: snprintf(buf, sizeof(buf), "%+.0f%%", play.filter.velAmount * 100.0); break; case ParamControl::kFilterKeyTrack: snprintf(buf, sizeof(buf), "%.0f%%", play.filter.keyTrack * 100.0); break; case ParamControl::kFilterEnvAttack: formatEnvTimeMs(play.filter.env.attackSeconds, buf, sizeof(buf)); break; case ParamControl::kFilterEnvHold: formatEnvTimeMs(play.filter.env.holdSeconds, buf, sizeof(buf)); break; case ParamControl::kFilterEnvDecay: formatEnvTimeMs(play.filter.env.decaySeconds, buf, sizeof(buf)); break; case ParamControl::kFilterEnvSustain: snprintf(buf, sizeof(buf), "%.0f%%", play.filter.env.sustainLevel * 100.0); break; case ParamControl::kFilterEnvRelease: formatEnvTimeMs(play.filter.env.releaseSeconds, buf, sizeof(buf)); break; case ParamControl::kFilterTrigAttack: formatEnvTimeMs(play.filter.trigEnv.attackSeconds, buf, sizeof(buf)); break; case ParamControl::kFilterTrigHold: snprintf(buf, sizeof(buf), "%.0f%%", play.filter.trigEnv.holdFraction * 100.0); break; case ParamControl::kFilterTrigDecay: formatEnvTimeMs(play.filter.trigEnv.decaySeconds, buf, sizeof(buf)); break; // Every curve exponent reads the same way: the neutral shows as 1.00. case ParamControl::kAttackCurve: case ParamControl::kDecayCurve: case ParamControl::kReleaseCurve: case ParamControl::kTrigAttackCurve: case ParamControl::kTrigDecayCurve: case ParamControl::kPitchEnvAttackCurve: case ParamControl::kPitchEnvDecayCurve: case ParamControl::kFilterEnvAttackCurve: case ParamControl::kFilterEnvDecayCurve: case ParamControl::kFilterEnvReleaseCurve: case ParamControl::kFilterTrigAttackCurve: case ParamControl::kFilterTrigDecayCurve: snprintf(buf, sizeof(buf), "^%.2f", curveExponentFor(id, play)); break; default: // The chrome knobs (preview velocity, bake Hold) are labeled at their own call // site; nothing else here. break; } return std::string(buf); } EnvClampBounds ReaSamplerEditor::envClampBounds() const { // Lives here, beside controlValue/applyControl, because it must MATCH them: a node drag can // never produce a param a knob couldn't. Every stage time caps at kEnvTimeMaxSeconds; the // Hold fractions and the sustain level are [0,1] by definition and need no bound here. EnvClampBounds b; b.maxAttackSeconds = kEnvTimeMaxSeconds; b.maxHoldSeconds = kEnvTimeMaxSeconds; b.maxDecaySeconds = kEnvTimeMaxSeconds; b.maxReleaseSeconds = kEnvTimeMaxSeconds; return b; } void ReaSamplerEditor::applyParamControl(int id, double value, int segment) { if (id == static_cast(ParamControl::kKeyTrack)) { // keyTrack sits beside the play bundle (0..200% over kKeyTrackMax); the knob maps 0..1. params_.keyTrack = clamp01(value) * kKeyTrackMax; } else { applyControl(id, params_.play, value, segment); } } } // namespace reasampler::vst