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:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user