feat: run the per-voice filter between the pitch and amp stages, with its own deck
Params ride the one parameter set; payload v8 -> v9, off by default. Deck composition moves to a pure deck_groups module in pitch -> filter -> amp order.
This commit is contained in:
@@ -12,11 +12,13 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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/map/trigger_seam.h" // triggerPlayLength / fade fraction converters
|
||||
#include "core/instrument/ui/deck_groups.h" // sampleDeckGroups (the deck's composition)
|
||||
#include "core/instrument/ui/knob_deck.h" // deckHeight / kDeckKnobSize (the band's own height)
|
||||
#include "core/util/clamp01.h"
|
||||
#include "shell/instrument/editor_internal.h" // DeckGroup ids
|
||||
#include "shell/instrument/editor_internal.h"
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
|
||||
namespace reasampler::vst {
|
||||
@@ -28,9 +30,16 @@ using instrument::ui::chromeRects;
|
||||
using instrument::ui::deckHeight;
|
||||
using instrument::ui::kDeckKnobSize;
|
||||
using instrument::ui::kPad;
|
||||
using instrument::ui::deckBipolarFromNorm;
|
||||
using instrument::ui::deckNormFromBipolar;
|
||||
using instrument::ui::sampleDeckGroups;
|
||||
using instrument::engine::formatMasterGainLabel;
|
||||
using instrument::engine::masterGainLinearFromNorm;
|
||||
using instrument::engine::masterGainNormFromLinear;
|
||||
using instrument::engine::filter::MorphLaw;
|
||||
using instrument::engine::filter::filterCutoffHzFromNorm;
|
||||
using instrument::engine::filter::filterDriveDepthFromNorm;
|
||||
using instrument::engine::filter::filterQFromNorm;
|
||||
using util::clamp01;
|
||||
|
||||
namespace {
|
||||
@@ -53,7 +62,7 @@ ReaSamplerEditor::FaceLayout ReaSamplerEditor::faceLayout(int w, int h) const {
|
||||
// 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 = deckGroupDescs(params_.play);
|
||||
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;
|
||||
@@ -87,6 +96,23 @@ double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
|
||||
case ParamControl::kPitchEnvDepth:
|
||||
// Signed depth centered 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 ParamControl::kFilterEnable: return play.filter.enabled ? 1.0 : 0.0;
|
||||
case ParamControl::kFilterLaw:
|
||||
return play.filter.settings.morphLaw == MorphLaw::HighNotchLow ? 1.0 : 0.0;
|
||||
case ParamControl::kFilterMorph: return clamp01(play.filter.settings.morphNorm);
|
||||
case ParamControl::kFilterCutoff: return clamp01(play.filter.settings.cutoffNorm);
|
||||
case ParamControl::kFilterQ: return clamp01(play.filter.settings.resonanceNorm);
|
||||
case ParamControl::kFilterDrive: return clamp01(play.filter.settings.driveNorm);
|
||||
case ParamControl::kFilterModAmt: return deckNormFromBipolar(play.filter.modAmount);
|
||||
case ParamControl::kFilterVel: return deckNormFromBipolar(play.filter.velAmount);
|
||||
case ParamControl::kFilterKeyTrack:return clamp01(play.filter.keyTrack / kKeyTrackMax);
|
||||
case ParamControl::kFilterEnvAttack: return secToNorm(play.filter.env.attackSeconds);
|
||||
case ParamControl::kFilterEnvHold: return secToNorm(play.filter.env.holdSeconds);
|
||||
case ParamControl::kFilterEnvDecay: return secToNorm(play.filter.env.decaySeconds);
|
||||
case ParamControl::kFilterEnvSustain: return clamp01(play.filter.env.sustainLevel);
|
||||
case ParamControl::kFilterEnvRelease: return secToNorm(play.filter.env.releaseSeconds);
|
||||
default: return 0.0;
|
||||
}
|
||||
}
|
||||
@@ -126,6 +152,33 @@ void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
|
||||
case ParamControl::kPitchEnvDepth:
|
||||
play.pitchEnv.peakSemitones = (clamp01(value) - 0.5) * 2.0 * kPitchDepthMaxSemis;
|
||||
break;
|
||||
case ParamControl::kFilterEnable: play.filter.enabled = (segment == 1); break;
|
||||
case ParamControl::kFilterLaw:
|
||||
play.filter.settings.morphLaw =
|
||||
(segment == 1) ? MorphLaw::HighNotchLow : MorphLaw::HighBandLow;
|
||||
break;
|
||||
case ParamControl::kFilterMorph:
|
||||
play.filter.settings.morphNorm = static_cast<float>(clamp01(value)); break;
|
||||
case ParamControl::kFilterCutoff:
|
||||
play.filter.settings.cutoffNorm = static_cast<float>(clamp01(value)); break;
|
||||
case ParamControl::kFilterQ:
|
||||
play.filter.settings.resonanceNorm = static_cast<float>(clamp01(value)); break;
|
||||
case ParamControl::kFilterDrive:
|
||||
play.filter.settings.driveNorm = static_cast<float>(clamp01(value)); break;
|
||||
case ParamControl::kFilterModAmt: play.filter.modAmount = deckBipolarFromNorm(value); break;
|
||||
case ParamControl::kFilterVel: play.filter.velAmount = deckBipolarFromNorm(value); break;
|
||||
case ParamControl::kFilterKeyTrack:
|
||||
play.filter.keyTrack = clamp01(value) * kKeyTrackMax; break;
|
||||
case ParamControl::kFilterEnvAttack:
|
||||
play.filter.env.attackSeconds = normToSec(value); break;
|
||||
case ParamControl::kFilterEnvHold:
|
||||
play.filter.env.holdSeconds = normToSec(value); break;
|
||||
case ParamControl::kFilterEnvDecay:
|
||||
play.filter.env.decaySeconds = normToSec(value); break;
|
||||
case ParamControl::kFilterEnvSustain:
|
||||
play.filter.env.sustainLevel = clamp01(value); break;
|
||||
case ParamControl::kFilterEnvRelease:
|
||||
play.filter.env.releaseSeconds = normToSec(value); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -151,68 +204,6 @@ double ReaSamplerEditor::previewVelocity01() const {
|
||||
return static_cast<double>(processor_->previewVelocity()) / 127.0;
|
||||
}
|
||||
|
||||
std::vector<DeckGroupDesc> ReaSamplerEditor::deckGroupDescs(const PlaySeconds& play) const {
|
||||
// The deck band's groups, left to right. Group widths are mode-independent: AMP ENVELOPE
|
||||
// reserves its 5-cell Gate width (Trigger leaves two blank cells), so a Gate<->Trigger
|
||||
// flip repopulates in place and never reflows the neighbouring groups.
|
||||
std::vector<DeckGroupDesc> out;
|
||||
{
|
||||
DeckGroupDesc amp;
|
||||
amp.id = kGroupAmpEnv;
|
||||
amp.captionWidth = 78;
|
||||
amp.captionToggle = {static_cast<int>(ParamControl::kPlayMode), 44};
|
||||
if (play.playMode == PlayMode::Gate) {
|
||||
amp.cellIds = {static_cast<int>(ParamControl::kAttack),
|
||||
static_cast<int>(ParamControl::kHold),
|
||||
static_cast<int>(ParamControl::kDecay),
|
||||
static_cast<int>(ParamControl::kSustain),
|
||||
static_cast<int>(ParamControl::kRelease)};
|
||||
} else {
|
||||
// Trigger, time-ordered left-to-right (Fade In / Length % / Fade Out — matches
|
||||
// the drawn envelope), plus the two reserved blanks.
|
||||
amp.cellIds = {static_cast<int>(ParamControl::kTrigFadeIn),
|
||||
static_cast<int>(ParamControl::kTrigLength),
|
||||
static_cast<int>(ParamControl::kTrigFadeOut), -1, -1};
|
||||
}
|
||||
out.push_back(std::move(amp));
|
||||
}
|
||||
{
|
||||
DeckGroupDesc pitch;
|
||||
pitch.id = kGroupPitch;
|
||||
pitch.captionWidth = 38;
|
||||
pitch.captionToggle = {static_cast<int>(ParamControl::kPitchEngine), 48};
|
||||
pitch.cellIds = {static_cast<int>(ParamControl::kKeyTrack)};
|
||||
out.push_back(std::move(pitch));
|
||||
}
|
||||
{
|
||||
DeckGroupDesc penv;
|
||||
penv.id = kGroupPitchEnv;
|
||||
penv.captionWidth = 58;
|
||||
penv.captionToggle = {static_cast<int>(ParamControl::kPitchEnvEnable), 32};
|
||||
penv.cellIds = {static_cast<int>(ParamControl::kPitchEnvAttack),
|
||||
static_cast<int>(ParamControl::kPitchEnvDecay),
|
||||
static_cast<int>(ParamControl::kPitchEnvDepth)};
|
||||
out.push_back(std::move(penv));
|
||||
}
|
||||
{
|
||||
DeckGroupDesc voice;
|
||||
voice.id = kGroupVoice;
|
||||
voice.captionWidth = 38;
|
||||
voice.captionToggle = {static_cast<int>(ParamControl::kVoiceMode), 40};
|
||||
voice.cellIds = {static_cast<int>(ParamControl::kVoiceCount)};
|
||||
voice.rowToggle = {static_cast<int>(ParamControl::kMonoTrigger), 44};
|
||||
out.push_back(std::move(voice));
|
||||
}
|
||||
{
|
||||
DeckGroupDesc master;
|
||||
master.id = kGroupMaster;
|
||||
master.captionWidth = 46;
|
||||
master.cellIds = {static_cast<int>(ParamControl::kMasterGain)};
|
||||
out.push_back(std::move(master));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
double ReaSamplerEditor::deckControlNorm(int id) const {
|
||||
if (id == -2) return previewVelocity01(); // the chrome preview-velocity knob
|
||||
switch (static_cast<ParamControl>(id)) {
|
||||
@@ -294,6 +285,43 @@ std::string ReaSamplerEditor::deckValueLabel(int id) const {
|
||||
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<double>(filterQFromNorm(play.filter.settings.resonanceNorm)));
|
||||
break;
|
||||
case ParamControl::kFilterDrive:
|
||||
snprintf(buf, sizeof(buf), "%.2f",
|
||||
static_cast<double>(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:
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.attackSeconds); break;
|
||||
case ParamControl::kFilterEnvHold:
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.holdSeconds); break;
|
||||
case ParamControl::kFilterEnvDecay:
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.decaySeconds); break;
|
||||
case ParamControl::kFilterEnvSustain:
|
||||
snprintf(buf, sizeof(buf), "%.0f%%", play.filter.env.sustainLevel * 100.0); break;
|
||||
case ParamControl::kFilterEnvRelease:
|
||||
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.releaseSeconds); break;
|
||||
default:
|
||||
// -2 (preview velocity) is labeled at its chrome call site; nothing else here.
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user