feat: legible ReaSampler 9000 editor — bigger knobs, ms time constants, per-ring double-click reset, and an antialiased draw pass
This commit is contained in:
@@ -309,7 +309,18 @@ anything for a trigger shape.
|
||||
- `browser_scroll` — scroll + type-to-filter layered over `capture_browser`: vertical scroll offset, scrollbar thumb, thumb-drag mapping, and name-substring search.
|
||||
- `param_slider` — parameter control-panel: vertical stack of TOGGLE (two-segment selector) and SLIDER (horizontal track) rows; maps normalized value to/from handle pixel.
|
||||
- `embed_strip` — compact single-row control layout for embed mode in the track FX chain.
|
||||
- `knob_deck` — pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap, `DeckLayout` / `DeckHit`. Mirror of `action_bar`/`param_slider`; no LICE or REAPER types. A group carries TWO caption-toggle slots, laid right-to-left: the second exists because a group whose knob row is wider than its caption row has caption slack a toggle can occupy for free, and the deck has six pixels of headroom on its first row at the editor's floor width — a `rowToggle` would widen the GROUP and wrap the deck to a fourth row, past what the minimum window holds. **A group's cell run is a RESERVED WIDTH, not a fixed cell size**: a `-1` id reserves one cell's width without a cell, and the cells present divide the whole run between them at one uniform integer width (residue in symmetric end margins). That is what lets a mode flip drop controls from a face — Trigger's AMP and FILTER ENV lose their Sustain/Release stages — without either reflowing the deck or leaving dead slots in the box; a face with fewer controls simply gets roomier cells. Do not reintroduce fixed-width cells with blank slots.
|
||||
- `knob_deck` — pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap, `DeckLayout` / `DeckHit`. Mirror of `action_bar`/`param_slider`; no LICE or REAPER types. Carries a SECOND hit-test, `hitTestKnobFace`, resolved against the drawn CIRCLES rather than the cell: a double-click reset is aimed at a dial, so the label band and the cell margins must miss where a drag grab deliberately does not, and only a radial resolve can tell the inner curve dial from the outer ring it sits inside. **The cell/knob/label sizes and `sample_bands`' editor floor move as a pair** — wider cells need a wider floor width or the deck wraps to a fourth row. A group carries TWO caption-toggle slots, laid right-to-left: the second exists because a group whose knob row is wider than its caption row has caption slack a toggle can occupy for free, and the deck has six pixels of headroom on its first row at the editor's floor width — a `rowToggle` would widen the GROUP and wrap the deck to a fourth row, past what the minimum window holds. **A group's cell run is a RESERVED WIDTH, not a fixed cell size**: a `-1` id reserves one cell's width without a cell, and the cells present divide the whole run between them at one uniform integer width (residue in symmetric end margins). That is what lets a mode flip drop controls from a face — Trigger's AMP and FILTER ENV lose their Sustain/Release stages — without either reflowing the deck or leaving dead slots in the box; a face with fewer controls simply gets roomier cells. Do not reintroduce fixed-width cells with blank slots.
|
||||
- `deck_values` — the deck's control-id ↔ parameter-set BINDING and its display units, split
|
||||
from the editor shell on the same axis `deck_groups` was split from `knob_deck`: `deck_groups`
|
||||
says which controls exist, this says what each one's value MEANS. Holds `deckParamNorm` /
|
||||
`setDeckParam` (the normalized ↔ stored-seconds/fraction/position maps and their clamps),
|
||||
`resetDeckParam` (the double-click reset — the defaults are READ off a default-constructed
|
||||
`PlaySeconds`, so there is no second table of defaults to drift), and `formatEnvTimeMs`, the
|
||||
ONE time-constant formatter: every displayed time constant reads in **ms**, never seconds, so
|
||||
two stage times are comparable at a glance. A display-unit decision only — nothing about the
|
||||
stored representation changes. Links the map layer because `PlaySeconds` is what a deck edits.
|
||||
The shell keeps only the controls the parameter set does not carry (key-track, voice count,
|
||||
master gain, preview velocity) and the labels for them.
|
||||
- `deck_groups` — also home to `isLiveDeckParam` and `liveCommitFor`, the editor's whole commit-tier routing decision (see "Live parameter delivery" above), and to `OverlayEnv` + `nextOverlaySelection`/`overlayEnvEnabled`/`overlayEnvInert`, the whole overlay-selection state machine (exclusivity, the none resting state, and which selections a disabled or DRAWN group makes inert); WHICH groups the Sample face's deck carries, split from `knob_deck`'s HOW they lay out: the `DeckParam` control-id space (the editor's `ParamControl` is an alias of it), the `DeckGroupId` list, `sampleDeckGroups` in signal-flow order (**pitch → filter → amp**, then velocity/voice/master), and the deck's bipolar-knob law. Reads `PlayMode` for the AMP group's Gate/Trigger face, which is why this and not `knob_deck` is the module that touches the engine's value layer. Also home to `CurveTarget` + `curveTargetFor` — the VELOCITY group's three cells are popup openers, not dials, and that predicate is the ONE place they are named, so paint, hit-test routing and the popup's title all agree. MASTER is reserved for post-voice-mixer concerns, which is why the curves sit in their own group immediately left of VOICE rather than there.
|
||||
- `spline_edit` — THE point-editing grammar, and the one place it is written down: left-click grabs a node and adds one in empty space, right-click deletes, control-click toggles hard/smooth. Both spline consumers — the velocity-curve popup and the spline EG overlay — route their mouse-down through `resolveSplineEdit`, so the two cannot drift into two grammars. The endpoint and point-count rules are NOT restated here: `deletePoint` and `addPoint` own them, and the caller applies the resolved action to the curve. Also home to `splineOverlayBox`, the contour's mapping box inside the waveform overlay — the FULL area, no inset, so the drawn contour stays 1:1 with the sample's time axis.
|
||||
- `curve_popup` — pure curve-popup geometry + dismissal test (FB1): centered sheet over the Sample face — width/height clamps, title row, Close button rect, curve-box rect, outside-sheet dismissal test. Mirror of `overflow_menu`; no LICE or REAPER types.
|
||||
|
||||
@@ -68,6 +68,15 @@ reasampler_pure_library(spline_edit
|
||||
# kWaveformMinHeight floor.
|
||||
reasampler_test(spline_edit LINK spline_edit waveform_view sample_bands)
|
||||
|
||||
# The deck's VALUE binding, split from its composition on the same axis deck_groups was split
|
||||
# from knob_deck. Links sample_map because PlaySeconds — the thing a deck knob edits — is
|
||||
# declared there; no sample_map symbol is called, only its value types. Same for the filter's
|
||||
# MorphLaw — an enum, so no filter symbol is linked.
|
||||
reasampler_pure_library(deck_values
|
||||
SOURCES deck_values.cpp
|
||||
LINK PUBLIC deck_groups sample_map envelope_overlay)
|
||||
reasampler_test(deck_values LINK deck_values)
|
||||
|
||||
reasampler_pure_library(curve_popup SOURCES curve_popup.cpp LINK PUBLIC editor_geometry)
|
||||
# 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.
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// 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
|
||||
@@ -0,0 +1,52 @@
|
||||
// deck_values.h — the deck's control-id <-> parameter-set BINDING and its display units: the
|
||||
// normalized 0..1 a knob shows, the write back into the stored seconds/fractions/positions, the
|
||||
// double-click reset, and the ms time-constant formatter. Split from the editor shell so the
|
||||
// whole domain map is provable without a host; deck_groups owns WHICH controls exist, this owns
|
||||
// what each one's value MEANS. Links the map layer because PlaySeconds is what a deck edits.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "core/instrument/map/sample_map.h" // PlaySeconds (the deck's edit target)
|
||||
#include "core/instrument/ui/deck_groups.h" // DeckParam
|
||||
#include "core/instrument/ui/envelope_overlay.h" // kGateStageMaxSeconds
|
||||
|
||||
namespace reasampler::instrument::ui {
|
||||
|
||||
using map::PlaySeconds;
|
||||
|
||||
// Every stage-time knob spans [0, kEnvTimeMaxSeconds] seconds — rate-free, exactly what the
|
||||
// parameter set stores. READ from the overlay's schematic scale rather than restated: the AHDSR
|
||||
// schematic anchors a maxed knob at the canvas edge, which only holds while the two agree.
|
||||
inline constexpr double kEnvTimeMaxSeconds = kGateStageMaxSeconds;
|
||||
|
||||
// Pitch depth throw: +/-kVelocityPitchRangeSemitones, centred. The one throw the pitch
|
||||
// envelope's peak and the velocity->pitch curve's full scale both speak (play_params.h).
|
||||
inline constexpr double kPitchDepthMaxSemis = kVelocityPitchRangeSemitones;
|
||||
|
||||
// Key-track knob ceiling (0..200%), shared by the pitch and filter key-track controls.
|
||||
inline constexpr double kKeyTrackMax = 2.0;
|
||||
|
||||
// The normalized [0,1] a control shows: seconds over the ceiling, levels and fractions as-is,
|
||||
// signed depths centred at 0.5, curve exponents over their logarithmic travel. Controls backed
|
||||
// by per-instance state rather than the parameter set (voice count, master gain, the pitch
|
||||
// key-track scalar, preview velocity) are not here — the shell reads those from the processor.
|
||||
double deckParamNorm(DeckParam id, const PlaySeconds& play);
|
||||
|
||||
// Applies a committed interaction: a knob's normalized `value`, or a toggle's `segment` (0/1).
|
||||
// Mutates `play` in place, touching exactly the one field the control names.
|
||||
void setDeckParam(DeckParam id, PlaySeconds& play, double value, int segment);
|
||||
|
||||
// Resets `id` to its default. The default IS what a fresh PlaySeconds carries, so there is no
|
||||
// second table of defaults to drift from the real one. Exact for every shipped default: they
|
||||
// are all 0, 1, or the curve neutral, and the seconds ceiling is a power of two, so the
|
||||
// norm round trip loses nothing. For knob-valued controls — a toggle has no reset gesture.
|
||||
void resetDeckParam(DeckParam id, PlaySeconds& play);
|
||||
|
||||
// A time constant as MILLISECONDS, e.g. "12 ms". Never switches to seconds: the editor reads in
|
||||
// one unit so two stage times are comparable at a glance. Sub-10 ms keeps one decimal so a short
|
||||
// attack is not rounded to a bare "0 ms". Writes at most `len` bytes including the terminator.
|
||||
void formatEnvTimeMs(double seconds, char* buf, std::size_t len);
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
@@ -190,4 +190,28 @@ DeckHit hitTestDeck(const DeckLayout& layout, int x, int y) {
|
||||
return {};
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Squared distance from a rect's centre, in the rect's own pixel units.
|
||||
double distSqFromCentre(const Rect& r, int x, int y) {
|
||||
const double dx = x - (r.x + r.width / 2.0);
|
||||
const double dy = y - (r.y + r.height / 2.0);
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
DeckFaceHit hitTestKnobFace(const DeckLayout& layout, int x, int y) {
|
||||
for (const DeckGroupLayout& g : layout.groups) {
|
||||
if (!contains(g.box, x, y)) continue;
|
||||
for (const DeckCellLayout& c : g.cells) {
|
||||
const double rOuter = c.knob.width / 2.0;
|
||||
if (distSqFromCentre(c.knob, x, y) >= rOuter * rOuter) continue;
|
||||
const double rInner = c.inner.width / 2.0;
|
||||
const bool inner = distSqFromCentre(c.inner, x, y) < rInner * rInner;
|
||||
return {c.id, inner};
|
||||
}
|
||||
return {}; // inside the group but off every dial
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
|
||||
@@ -22,11 +22,13 @@
|
||||
|
||||
namespace reasampler::instrument::ui {
|
||||
|
||||
// Fixed deck metrics, exposed so the shell and tests agree.
|
||||
inline constexpr int kDeckCellW = 48; // one knob cell
|
||||
inline constexpr int kDeckCellH = 58;
|
||||
inline constexpr int kDeckKnobSize = 28; // knob diameter inside the cell
|
||||
inline constexpr int kDeckCellLabelH = 12; // the Micro label band under the knob
|
||||
// Fixed deck metrics, exposed so the shell and tests agree. The cell/knob/label sizes were
|
||||
// raised together for legibility at high pixel densities; the editor's floor width
|
||||
// (sample_bands) is what absorbs the wider cells, so the two move as a pair.
|
||||
inline constexpr int kDeckCellW = 60; // one knob cell
|
||||
inline constexpr int kDeckCellH = 74;
|
||||
inline constexpr int kDeckKnobSize = 40; // knob diameter inside the cell
|
||||
inline constexpr int kDeckCellLabelH = 16; // the label band under the knob
|
||||
inline constexpr int kDeckCaptionH = 20; // the group caption row
|
||||
inline constexpr int kDeckToggleH = 18; // compact toggle segment height
|
||||
inline constexpr int kDeckGroupPadX = 6; // group box horizontal inner padding
|
||||
@@ -39,7 +41,7 @@ inline constexpr int kDeckRadioSize = 12; // the caption-row corner radio sq
|
||||
// The knob cell's INNER dial: a concentric sub-disc that edits a second, related value while
|
||||
// the outer ring keeps editing the cell's own. Geometry only — WHICH cells carry one is
|
||||
// deck_groups' call, so a cell without an inner value simply resolves an inner hit as a knob.
|
||||
inline constexpr int kDeckInnerDialSize = 14;
|
||||
inline constexpr int kDeckInnerDialSize = 20;
|
||||
// One group box: padding + caption + gap + cell row + padding.
|
||||
inline constexpr int kDeckGroupH =
|
||||
kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap + kDeckCellH + kDeckGroupPadY;
|
||||
@@ -145,4 +147,17 @@ struct DeckHit {
|
||||
// the caption-row corner radio. Everything else — fence, padding, outside — misses.
|
||||
DeckHit hitTestDeck(const DeckLayout& layout, int x, int y);
|
||||
|
||||
// The knob FACE a point lands on. id -1 is a miss.
|
||||
struct DeckFaceHit {
|
||||
int id = -1;
|
||||
bool inner = false; // inside the concentric inner disc
|
||||
};
|
||||
|
||||
// Resolved against the drawn CIRCLES, not the cell: a reset is aimed at a dial, so the label
|
||||
// band and the cell margins must miss where a drag grab deliberately does not. Both radii are
|
||||
// boundary-EXCLUSIVE, one rule for both rings — a point exactly on the inner radius is an
|
||||
// outer-ring hit, one exactly on the outer radius is a miss. Whether a cell actually carries an
|
||||
// inner value is deck_groups' call, exactly as with DeckHit::inner.
|
||||
DeckFaceHit hitTestKnobFace(const DeckLayout& layout, int x, int y);
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
|
||||
@@ -16,13 +16,13 @@ inline constexpr int kPad = 8;
|
||||
// exactly this, and there is no scroll, so anything smaller pushes the deck band off the
|
||||
// window bottom (computeSampleBands' waveform-floor-wins degrade). Growing is fine — the
|
||||
// waveform band is the elastic one. Both the enforced minimum and the opening rect read this.
|
||||
inline constexpr int kEditorMinWidth = 840;
|
||||
inline constexpr int kEditorMinHeight = 620;
|
||||
inline constexpr int kEditorMinWidth = 980;
|
||||
inline constexpr int kEditorMinHeight = 680;
|
||||
|
||||
// Chrome band: the toolbar row (title + nav) stacked over the control row (piano strip,
|
||||
// preview, velocity knob, channel toggle). sample_chrome partitions it.
|
||||
inline constexpr int kTitleHeight = 26;
|
||||
inline constexpr int kChromeRowHeight = 52;
|
||||
inline constexpr int kChromeRowHeight = 64;
|
||||
|
||||
// Waveform band floor: two stacked lanes plus the seam between them. The band never shrinks
|
||||
// below this — a window too short for it clips the bands beneath instead, so the waveform
|
||||
|
||||
@@ -13,14 +13,14 @@ namespace {
|
||||
// The toolbar row carries the whole control run, so it is taller than the Browse modal's
|
||||
// plain kTitleHeight bar — the velocity knob cell (knob over label) sets the floor. Both
|
||||
// rows still fit the band the allocator hands out (kTitleHeight + kChromeRowHeight).
|
||||
constexpr int kToolbarHeight = 44;
|
||||
constexpr int kToolbarHeight = 58;
|
||||
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 kVelCellW = 44;
|
||||
constexpr int kVelLabelH = 12;
|
||||
constexpr int kVelCellW = 56;
|
||||
constexpr int kVelLabelH = 16;
|
||||
constexpr int kPreviewBtnW = 64;
|
||||
constexpr int kRunButtonH = 24; // Browse and Preview
|
||||
|
||||
|
||||
Reference in New Issue
Block a user