instrument: one VELOCITY deck for all three velocity curves, bipolar and off by default for pitch and filter

Payload v12 appends the new velocity->pitch curve and folds the retired filter velAmount into its now-bipolar curve, so pre-v12 projects reopen sounding identical. Preview button takes a drawn play triangle.
This commit is contained in:
2026-07-31 19:15:17 -04:00
parent 4fecb58c0a
commit 9d38f87a2d
37 changed files with 1020 additions and 320 deletions
+3 -1
View File
@@ -57,4 +57,6 @@ reasampler_pure_library(deck_groups
reasampler_test(deck_groups LINK deck_groups sample_bands)
reasampler_pure_library(curve_popup SOURCES curve_popup.cpp LINK PUBLIC editor_geometry)
reasampler_test(curve_popup LINK curve_popup)
# velocity_curve is linked for the test only: the sheet's geometry is domain-agnostic, and
# proving that takes a curve of each domain mapped through the one curveBox.
reasampler_test(curve_popup LINK curve_popup velocity_curve)
+2 -1
View File
@@ -1,4 +1,5 @@
// curve_popup.h — sheet geometry + dismissal test for the velocity-curve popup editor.
// curve_popup.h — sheet geometry + dismissal test for the velocity-curve popup editor, shared
// by all three curves: the sheet is domain-agnostic, and only the curve's own y map differs.
// Mirror of overflow_menu; the shell draws through the L1 kit and routes clicks via
// these rects.
//
+23 -2
View File
@@ -48,7 +48,6 @@ std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode) {
id(DeckParam::kFilterQ),
id(DeckParam::kFilterDrive),
id(DeckParam::kFilterModAmt),
id(DeckParam::kFilterVel),
id(DeckParam::kFilterKeyTrack)};
filter.rowToggle = {id(DeckParam::kFilterLaw), 44};
out.push_back(std::move(filter));
@@ -89,6 +88,17 @@ std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode) {
}
out.push_back(std::move(amp));
}
{
// The three velocity transfer curves share one home, immediately left of VOICE: they
// shape three different destinations but are one gesture, and MASTER is reserved for
// post-voice-mixer concerns.
DeckGroupDesc vel;
vel.id = kGroupVelocity;
vel.captionWidth = 54;
vel.cellIds = {id(DeckParam::kAmpVelCurve), id(DeckParam::kPitchVelCurve),
id(DeckParam::kFilterVelCurve)};
out.push_back(std::move(vel));
}
{
DeckGroupDesc voice;
voice.id = kGroupVoice;
@@ -108,6 +118,15 @@ std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode) {
return out;
}
CurveTarget curveTargetFor(int controlId) {
switch (static_cast<DeckParam>(controlId)) {
case DeckParam::kAmpVelCurve: return CurveTarget::kAmp;
case DeckParam::kPitchVelCurve: return CurveTarget::kPitch;
case DeckParam::kFilterVelCurve: return CurveTarget::kFilter;
default: return CurveTarget::kNone;
}
}
DeckParam curveParamFor(DeckParam knob) {
switch (knob) {
case DeckParam::kAttack: return DeckParam::kAttackCurve;
@@ -176,7 +195,9 @@ bool isLiveDeckParam(DeckParam id) {
case DeckParam::kPitchEnvEnable:
case DeckParam::kKeyTrack:
case DeckParam::kFilterEnable:
case DeckParam::kFilterVel:
case DeckParam::kAmpVelCurve:
case DeckParam::kPitchVelCurve:
case DeckParam::kFilterVelCurve:
case DeckParam::kFilterLaw:
case DeckParam::kAmpEnvSelect:
case DeckParam::kPitchEnvSelect:
+15 -4
View File
@@ -40,7 +40,6 @@ enum class DeckParam {
kFilterQ, // resonance
kFilterDrive, // in-loop drive depth
kFilterModAmt, // filter envelope -> cutoff, +/-100%
kFilterVel, // velocity -> cutoff, +/-100%
kFilterKeyTrack, // note -> cutoff, 0..200%
kFilterLaw, // morph law row toggle: HP-BP-LP | HP-notch-LP
kFilterEnvAttack, // filter AHDSR (Gate)
@@ -65,6 +64,11 @@ enum class DeckParam {
kFilterEnvReleaseCurve,
kFilterTrigAttackCurve,
kFilterTrigDecayCurve,
// VELOCITY: the three transfer-curve cells. Each opens the curve popup rather than
// dragging a value — see curveTargetFor, which is also what tells a cell apart from a knob.
kAmpVelCurve,
kPitchVelCurve,
kFilterVelCurve,
// Overlay selection radios — transient view state, not parameters.
kAmpEnvSelect,
kPitchEnvSelect,
@@ -86,10 +90,17 @@ enum DeckGroupId {
kGroupFilter,
kGroupFilterEnv,
kGroupAmpEnv,
kGroupVelocity,
kGroupVoice,
kGroupMaster,
};
// Which velocity curve a deck cell edits, or kNone when the control is an ordinary knob. THE
// one place the three curve cells are named, so paint (draw a curve thumbnail, not a dial),
// hit-test (open a popup, not start a drag) and the popup's own title all read from it.
enum class CurveTarget { kNone, kAmp, kPitch, kFilter };
CurveTarget curveTargetFor(int controlId);
// The deck's groups, left to right, in SIGNAL-FLOW order: pitch -> filter -> amp, then the
// two instance-wide groups. `playMode` picks the AMP and FILTER ENV groups' faces — AHDSR in
// Gate, AHD in Trigger — via knob_deck's blank-cell reservation (knob_deck.h) so a mode flip
@@ -113,9 +124,9 @@ DeckParam curveParamFor(DeckParam knob);
// name a different sound rather than a different setting of one;
// - the three capture-anchored overrides (root, loop span, start frame) name positions in
// the decoded PCM;
// - kKeyTrack and kFilterVel feed values a voice latches at note-on by design (the pitch
// ratio and the velocity-curve result), so live delivery would retune or re-gain a note
// already struck;
// - kKeyTrack and the three velocity-curve cells feed values a voice latches at note-on by
// design (the pitch ratio and the curve results), so live delivery would retune or re-gain
// a note already struck;
// - kTrigLength resolves playEnd_, a fact about the note, not a setting of it;
// - the overlay radios select what the editor DRAWS and reach no parameter at all.
// Both amp shapes are live: the Trigger fade pair that used to reload folded into the AHD and
+27 -8
View File
@@ -19,12 +19,15 @@ constexpr int kStripBandHeight = 30;
constexpr int kRunGap = 6; // between adjacent items of the toolbar run
constexpr int kChanSegW = 52;
constexpr int kChanSegH = 18;
constexpr int kCurveBtnSize = 24;
constexpr int kVelCellW = 44;
constexpr int kVelLabelH = 12;
constexpr int kPreviewBtnW = 64;
constexpr int kRunButtonH = 24; // Browse and Preview
constexpr int kPreviewGlyphMinH = 6;
constexpr int kPreviewGlyphMaxH = 14;
constexpr int kPreviewGlyphPad = 4; // clearance between the glyph and the button edge
} // namespace
ChromeRects chromeRects(const Rect& chrome, int knobSize) {
@@ -39,7 +42,8 @@ ChromeRects chromeRects(const Rect& chrome, int knobSize) {
const auto topFor = [&row](int h) { return row.y + (row.height - h) / 2; };
const auto leftOf = [&row](int edge, int w) { return std::max(row.x, edge - w); };
// The fixed run, right to left: Browse, Mono|Stereo, curve, velocity cell, preview.
// The fixed run, right to left: Browse, Mono|Stereo, velocity cell, preview. The
// velocity-curve button that used to sit here now lives in the deck's VELOCITY group.
const int navH = std::min(kRunButtonH, row.height);
const int navTop = topFor(navH);
const int navRight = std::max(row.x, row.right() - kPad);
@@ -53,14 +57,9 @@ ChromeRects chromeRects(const Rect& chrome, int knobSize) {
r.chanMono = Rect::ltrb(leftOf(r.chanStereo.x, kChanSegW), chanTop, r.chanStereo.x,
chanTop + kChanSegH);
const int curveTop = topFor(kCurveBtnSize);
const int curveRight = leftOf(r.chanMono.x, kRunGap);
r.curveBtn = Rect::ltrb(leftOf(curveRight, kCurveBtnSize), curveTop, curveRight,
curveTop + kCurveBtnSize);
const int cellH = std::min(row.height, knobSize + kVelLabelH);
const int cellTop = topFor(cellH);
const int cellRight = leftOf(r.curveBtn.x, kRunGap);
const int cellRight = leftOf(r.chanMono.x, kRunGap);
r.velCell = Rect::ltrb(leftOf(cellRight, kVelCellW), cellTop, cellRight, cellTop + cellH);
const int knobLeft = r.velCell.x + (r.velCell.width - knobSize) / 2;
r.velKnob = Rect::ltrb(knobLeft, r.velCell.y, knobLeft + knobSize,
@@ -89,4 +88,24 @@ ChromeRects chromeRects(const Rect& chrome, int knobSize) {
return r;
}
PreviewGlyph previewGlyph(const Rect& button) {
PreviewGlyph g;
if (button.empty()) return g;
// Even height so the apex lands exactly on the button's horizontal centre line rather
// than a half-pixel off it.
int h = std::min({kPreviewGlyphMaxH, button.height - 2 * kPreviewGlyphPad,
button.width - 2 * kPreviewGlyphPad});
h -= h % 2;
if (h < kPreviewGlyphMinH) return g;
const int w = std::max(2, (h * 7) / 8); // ~equilateral: the classic transport triangle
const int cx = button.x + button.width / 2;
const int cy = button.y + button.height / 2;
g.leftX = cx - w / 2;
g.apexX = g.leftX + w;
g.topY = cy - h / 2;
g.bottomY = g.topY + h;
g.apexY = cy;
return g;
}
} // namespace reasampler::instrument::ui
+16 -1
View File
@@ -21,7 +21,6 @@ struct ChromeRects {
Rect velCell; // preview-velocity knob cell (knob + label band)
Rect velKnob;
Rect velLabel;
Rect curveBtn; // opens the velocity-curve popup
Rect chanMono;
Rect chanStereo;
Rect navBrowse;
@@ -32,4 +31,20 @@ struct ChromeRects {
// `knobSize` is the deck knob square, passed in so this module does not depend on knob_deck.
ChromeRects chromeRects(const Rect& chrome, int knobSize);
// A right-pointing play triangle centered in the preview button: the button's whole label.
// Three vertices, handed straight to one filled-triangle draw. Drawn rather than embedded as a
// bitmap so it inherits the palette role of whatever interaction state the button is in.
struct PreviewGlyph {
int leftX = 0; // the vertical edge
int topY = 0;
int bottomY = 0;
int apexX = 0; // the point, on the button's vertical centre line
int apexY = 0;
bool empty() const { return apexX <= leftX || bottomY <= topY; }
};
// Sized off the button's shorter dimension and clamped, so the glyph stays legible in a squat
// button and never outgrows a generous one. An unusably small button yields an empty glyph.
PreviewGlyph previewGlyph(const Rect& button);
} // namespace reasampler::instrument::ui