fix: restore waveform symmetry about the midline, cut deck_values' link to the bank model, and unit-test the column arithmetic
The waveform column's vertical extents move to pure component_geometry so the shared primitive stops being untested; PlaySeconds hoists into a header-only play_seconds target.
This commit is contained in:
@@ -291,6 +291,7 @@ anything for a trigger shape.
|
||||
### `map/`
|
||||
|
||||
- `sample_map` — the bank blob → selected capture resolve, the channel policy (downmix / dual-mono / L-R split), `InstrumentParams` (the ONE parameter set: root/loop/start overrides, keyTrack, velocity curve, `PlaySeconds`), the single override-beats-intrinsic fold (`resolveCapture`, shared by the bank and refs paths so they cannot drift), and the `SampleData` build. **Wall-clock times stored as rate-free SECONDS, resolved against the live project rate — NO hardcoded sample rates in `src/`** (Daniel's standing ruling, load-bearing). Deliberately does NOT link the voice engine: the build's product is plain `SampleData`.
|
||||
- `play_seconds` — the stored, wall-clock-SECONDS value layer (`PlaySeconds` + `AdsrSeconds` / `AhdSeconds` / `PitchEnvSeconds` / `FilterSeconds`), header-only and split from `sample_map` so a consumer that only edits those values reaches them without the bank model and the WAV codec. `resolvePlay`, which turns them into the engine's frame domain, stays with the rest of the mapping.
|
||||
- `component_state_io` (`core/instrument/map`) — the `ComponentState` envelope + params-payload binary codec (envelope v1…v11, params payload v1…v13), split out of `sample_map` (Q-W2v, T4-13 ≡ T2-07) so BOTH artifacts can link the codec without the extension pulling in the whole voice engine to serialize one preset blob — the extension's `instrument_drop` and the instrument's processor read/write the identical bytes, so the cross-artifact contract cannot drift. Payload v1…v7 are the RETIRED per-zone lists: still read, lifting by adopting zone one's capture + parameters (that first zone is what the old first-match resolve actually played, so it is also what supersedes the envelope's stored selection id). Payload v9 appends the per-voice filter tail; a v8 blob is a strict prefix of it and lifts to the off/neutral filter default. Every tail since is a strict suffix on the same discipline — v10 the staged curves, v11 the loop crossfade, v12 the velocity→pitch curve, v13 the dual Staged/Spline state (the three contours, plus hard-flag tails for the three velocity curves — their v7/v9/v12 blocks are frozen at 16 bytes/point and had no room for a per-point flag). v12 also RE-TAGS the y DOMAIN of one frozen slot inside the v9 filter tail — its velocity curve reads bipolar from v12 on, unipolar before — which needs no version branch, because a pre-v12 curve's y values are already valid bipolar ones; every other filter slot, `velAmount` included, keeps its meaning.
|
||||
- `params_payload` — the PARAMS-PAYLOAD half of that codec, split from the envelope half on the axis the format already has: the payload carries its own version and grows independently, so the two version ladders are two responsibilities. An INTERNAL seam — the public entry points stay `serialize`/`deserializeComponentState`. The prose ladder and every version constant stay in `component_state_io.h`, their one home.
|
||||
- `bank_sync` — generation change-detection + assignment-request consume: owns the yes/no decision logic so the rules are provable without a host. The processor shell owns cadence and side effects.
|
||||
@@ -318,9 +319,11 @@ anything for a trigger shape.
|
||||
`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.
|
||||
stored representation changes. Links the header-only `play_seconds`, deliberately not
|
||||
`sample_map`: `PlaySeconds` is the whole of what a deck edits, and linking the mapping would
|
||||
drag the bank model and the WAV codec in behind it. 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.
|
||||
|
||||
@@ -26,10 +26,20 @@ reasampler_pure_library(component_state_io
|
||||
# proof the codec is engine-free, which is what keeps engine object code out of the extension.
|
||||
reasampler_test(component_state_io LINK component_state_io)
|
||||
|
||||
# The stored seconds value layer, header-only (hence INTERFACE) — PlaySeconds and the four
|
||||
# stage-time structs it composes. Split from sample_map so a consumer that only edits those
|
||||
# values reaches them WITHOUT the bank model and the WAV codec: the editor's deck_values
|
||||
# binding is exactly that consumer, and linking sample_map for one value struct would put
|
||||
# bank_book + wav_codec into a test whose subject is a knob. Links the same value-layer set
|
||||
# play_params.h needs (velocity_curve's out-of-line zero() is a default member initializer).
|
||||
add_library(play_seconds INTERFACE)
|
||||
target_include_directories(play_seconds INTERFACE ${REASAMPLER_SRC_DIR})
|
||||
target_link_libraries(play_seconds INTERFACE velocity_curve peaks curve_law)
|
||||
|
||||
# The mapping's product is plain SampleData, so the voice engine is not a dependency.
|
||||
reasampler_pure_library(sample_map
|
||||
SOURCES sample_map.cpp
|
||||
LINK PUBLIC bank_book wav_codec velocity_curve peaks curve_law)
|
||||
LINK PUBLIC bank_book wav_codec play_seconds velocity_curve peaks curve_law)
|
||||
# Links only sample_map + component_state_io: the same plain-data-boundary proof, spanning
|
||||
# both halves of the mapping/codec split where the frozen-format assertions live.
|
||||
reasampler_test(sample_map LINK sample_map component_state_io)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
// play_seconds — the STORED, wall-clock-SECONDS value layer the instrument edits and
|
||||
// serializes: PlaySeconds and the four stage-time structs it composes. Header-only, and split
|
||||
// from sample_map so a consumer that only edits these values (the editor's deck binding) does
|
||||
// not link the bank model and the WAV codec to reach one value struct. `resolvePlay`, which
|
||||
// turns them into the engine's frame domain, stays in sample_map with the rest of the mapping.
|
||||
|
||||
#include "core/instrument/engine/play_params.h" // PlayMode / TriggerParams / SplineEnv / …
|
||||
|
||||
namespace reasampler::instrument::map {
|
||||
|
||||
using instrument::engine::VelocityCurve;
|
||||
|
||||
// Daniel's standing ruling: no hardcoded sample rate anywhere in the program. The
|
||||
// instrument stores/edits wall-clock performance times (AHDSR A/H/D/R, pitch-env A/D) as
|
||||
// SECONDS, rate-free; the engine receives FRAMES resolved from the LIVE sample rate at
|
||||
// build. Quantities anchored to the source file's timeline (start point, loop points,
|
||||
// Trigger %-length + fades) stay in source frames/fractions, carried through unchanged
|
||||
// (TriggerParams reused verbatim).
|
||||
//
|
||||
// The stored AHDSR times (seconds). sustainLevel is dimensionless (0..1), not a time; the
|
||||
// three curve exponents are dimensionless too (curve_law.h owns their domain).
|
||||
struct AdsrSeconds {
|
||||
double attackSeconds = 0.003; // tier-0 default
|
||||
double holdSeconds = 0.0;
|
||||
double decaySeconds = 0.0;
|
||||
double sustainLevel = 1.0;
|
||||
double releaseSeconds = 0.060; // tier-0 default
|
||||
double attackCurve = util::kCurveNeutral;
|
||||
double decayCurve = util::kCurveNeutral;
|
||||
double releaseCurve = util::kCurveNeutral;
|
||||
};
|
||||
|
||||
// The stored sustain-less AHD: wall-clock stage times in SECONDS, Hold as a FRACTION of the
|
||||
// span left after them (AhdParams owns why a fraction, not a time).
|
||||
struct AhdSeconds {
|
||||
double attackSeconds = 0.0;
|
||||
double decaySeconds = 0.0;
|
||||
double holdFraction = 1.0;
|
||||
double attackCurve = util::kCurveNeutral;
|
||||
double decayCurve = util::kCurveNeutral;
|
||||
};
|
||||
|
||||
// The stored AHD pitch envelope. enabled + peakSemitones are dimensionless. The hold fraction
|
||||
// defaults to 0 so an instance predating the stage plays as its attack-decay predecessor did.
|
||||
struct PitchEnvSeconds {
|
||||
bool enabled = false;
|
||||
double peakSemitones = 0.0; // signed depth at the peak
|
||||
AhdSeconds shape{0.0, 0.0, /*holdFraction=*/0.0, util::kCurveNeutral, util::kCurveNeutral};
|
||||
};
|
||||
|
||||
// The stored mirror of the engine's FilterParams (play_params.h, which owns what each field
|
||||
// MEANS). Only the envelope differs between the two: the control positions and depths are
|
||||
// rate-free already, so this block is a seconds/frames split of one field, not of the whole
|
||||
// struct. The env default is a flat unity, so `enabled` is the only thing standing between a
|
||||
// loaded blob and the pre-filter sound.
|
||||
struct FilterSeconds {
|
||||
bool enabled = false;
|
||||
engine::filter::FilterSettings settings;
|
||||
double modAmount = 0.0;
|
||||
double velAmount = 0.0;
|
||||
double keyTrack = 0.0;
|
||||
AdsrSeconds env{0.0, 0.0, 0.0, 1.0, 0.0}; // Gate
|
||||
AhdSeconds trigEnv; // Trigger
|
||||
VelocityCurve velocityCurve = VelocityCurve::zero();
|
||||
};
|
||||
|
||||
// The stored play bundle: wall-clock times in SECONDS, source-timeline quantities in
|
||||
// frames/fractions (TriggerParams). Instrument-owned, serialized, editor-facing — distinct
|
||||
// from the engine-facing PlayParams (frames).
|
||||
struct PlaySeconds {
|
||||
PlayMode playMode = PlayMode::Gate;
|
||||
AdsrSeconds adsr; // Gate amp: AHDSR (seconds)
|
||||
TriggerParams trigger; // Trigger play span (%-length)
|
||||
AhdSeconds trigAhd; // Trigger amp: AHD (seconds + fraction)
|
||||
PitchEngine pitchEngine = kDefaultPitchEngine; // product default: Preserve
|
||||
PitchEnvSeconds pitchEnv; // AHD pitch modulation, off by default
|
||||
VelocityCurve pitchVelocityCurve = VelocityCurve::zero(); // velocity -> pitch, off by default
|
||||
FilterSeconds filter; // per-voice filter, off by default
|
||||
// The three drawn contours, in the same slots the engine bundle carries them (play_params.h
|
||||
// owns why they sit beside the envelopes rather than inside them). Normalized over the
|
||||
// sample's own length, so resolvePlay needs no rate for them.
|
||||
SplineEnv ampSpline;
|
||||
SplineEnv pitchSpline;
|
||||
SplineEnv filterSpline;
|
||||
};
|
||||
|
||||
} // namespace reasampler::instrument::map
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "core/model/bank_book.h" // BankBook::deserialize (shared bank JSON parse)
|
||||
#include "core/instrument/engine/play_params.h" // SampleData, SampleLoop, PlayParams
|
||||
#include "core/instrument/map/play_seconds.h" // PlaySeconds (the stored seconds value layer)
|
||||
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
|
||||
|
||||
namespace reasampler::instrument::map {
|
||||
@@ -134,81 +135,7 @@ std::vector<AudioSample> downmixToMono(const std::vector<AudioSample>& interleav
|
||||
std::vector<AudioSample> extractChannel(const std::vector<AudioSample>& interleaved,
|
||||
int channelCount, int which);
|
||||
|
||||
// --- Stored (wall-clock SECONDS) play params ----------------------------------
|
||||
//
|
||||
// Daniel's standing ruling: no hardcoded sample rate anywhere in the program. The
|
||||
// instrument stores/edits wall-clock performance times (AHDSR A/H/D/R, pitch-env A/D) as
|
||||
// SECONDS, rate-free; the engine receives FRAMES resolved from the LIVE sample rate at
|
||||
// build. Quantities anchored to the source file's timeline (start point, loop points,
|
||||
// Trigger %-length + fades) stay in source frames/fractions, carried through unchanged
|
||||
// (TriggerParams reused verbatim).
|
||||
//
|
||||
// The stored AHDSR times (seconds). sustainLevel is dimensionless (0..1), not a time; the
|
||||
// three curve exponents are dimensionless too (curve_law.h owns their domain).
|
||||
struct AdsrSeconds {
|
||||
double attackSeconds = 0.003; // tier-0 default
|
||||
double holdSeconds = 0.0;
|
||||
double decaySeconds = 0.0;
|
||||
double sustainLevel = 1.0;
|
||||
double releaseSeconds = 0.060; // tier-0 default
|
||||
double attackCurve = util::kCurveNeutral;
|
||||
double decayCurve = util::kCurveNeutral;
|
||||
double releaseCurve = util::kCurveNeutral;
|
||||
};
|
||||
|
||||
// The stored sustain-less AHD: wall-clock stage times in SECONDS, Hold as a FRACTION of the
|
||||
// span left after them (AhdParams owns why a fraction, not a time).
|
||||
struct AhdSeconds {
|
||||
double attackSeconds = 0.0;
|
||||
double decaySeconds = 0.0;
|
||||
double holdFraction = 1.0;
|
||||
double attackCurve = util::kCurveNeutral;
|
||||
double decayCurve = util::kCurveNeutral;
|
||||
};
|
||||
|
||||
// The stored AHD pitch envelope. enabled + peakSemitones are dimensionless. The hold fraction
|
||||
// defaults to 0 so an instance predating the stage plays as its attack-decay predecessor did.
|
||||
struct PitchEnvSeconds {
|
||||
bool enabled = false;
|
||||
double peakSemitones = 0.0; // signed depth at the peak
|
||||
AhdSeconds shape{0.0, 0.0, /*holdFraction=*/0.0, util::kCurveNeutral, util::kCurveNeutral};
|
||||
};
|
||||
|
||||
// The stored mirror of the engine's FilterParams (play_params.h, which owns what each field
|
||||
// MEANS). Only the envelope differs between the two: the control positions and depths are
|
||||
// rate-free already, so this block is a seconds/frames split of one field, not of the whole
|
||||
// struct. The env default is a flat unity, so `enabled` is the only thing standing between a
|
||||
// loaded blob and the pre-filter sound.
|
||||
struct FilterSeconds {
|
||||
bool enabled = false;
|
||||
engine::filter::FilterSettings settings;
|
||||
double modAmount = 0.0;
|
||||
double velAmount = 0.0;
|
||||
double keyTrack = 0.0;
|
||||
AdsrSeconds env{0.0, 0.0, 0.0, 1.0, 0.0}; // Gate
|
||||
AhdSeconds trigEnv; // Trigger
|
||||
VelocityCurve velocityCurve = VelocityCurve::zero();
|
||||
};
|
||||
|
||||
// The stored play bundle: wall-clock times in SECONDS, source-timeline quantities in
|
||||
// frames/fractions (TriggerParams). Instrument-owned, serialized, editor-facing — distinct
|
||||
// from the engine-facing PlayParams (frames).
|
||||
struct PlaySeconds {
|
||||
PlayMode playMode = PlayMode::Gate;
|
||||
AdsrSeconds adsr; // Gate amp: AHDSR (seconds)
|
||||
TriggerParams trigger; // Trigger play span (%-length)
|
||||
AhdSeconds trigAhd; // Trigger amp: AHD (seconds + fraction)
|
||||
PitchEngine pitchEngine = kDefaultPitchEngine; // product default: Preserve
|
||||
PitchEnvSeconds pitchEnv; // AHD pitch modulation, off by default
|
||||
VelocityCurve pitchVelocityCurve = VelocityCurve::zero(); // velocity -> pitch, off by default
|
||||
FilterSeconds filter; // per-voice filter, off by default
|
||||
// The three drawn contours, in the same slots the engine bundle carries them (play_params.h
|
||||
// owns why they sit beside the envelopes rather than inside them). Normalized over the
|
||||
// sample's own length, so resolvePlay needs no rate for them.
|
||||
SplineEnv ampSpline;
|
||||
SplineEnv pitchSpline;
|
||||
SplineEnv filterSpline;
|
||||
};
|
||||
// --- Seconds -> frames --------------------------------------------------------
|
||||
|
||||
// Resolve a stored seconds bundle to the engine's frame-domain PlayParams against a live
|
||||
// sample rate (frames = round(seconds * rate)). Source-timeline fields carry through
|
||||
|
||||
@@ -7,7 +7,9 @@ reasampler_pure_library(sample_bands SOURCES sample_bands.cpp LINK PUBLIC editor
|
||||
reasampler_test(sample_bands LINK sample_bands)
|
||||
|
||||
reasampler_pure_library(sample_chrome SOURCES sample_chrome.cpp LINK PUBLIC sample_bands)
|
||||
reasampler_test(sample_chrome LINK sample_chrome)
|
||||
# knob_deck is linked for the test only: the knob size the shell hands chromeRects is
|
||||
# kDeckKnobSize, and the test reads the real constant rather than copying its value.
|
||||
reasampler_test(sample_chrome LINK sample_chrome knob_deck)
|
||||
|
||||
reasampler_pure_library(embed_strip SOURCES embed_strip.cpp LINK PUBLIC editor_geometry)
|
||||
reasampler_test(embed_strip LINK embed_strip)
|
||||
@@ -69,12 +71,12 @@ reasampler_pure_library(spline_edit
|
||||
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.
|
||||
# from knob_deck. Links the header-only play_seconds, NOT sample_map: PlaySeconds is all a deck
|
||||
# knob edits, and sample_map would drag the bank model and the WAV codec in behind it. 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)
|
||||
LINK PUBLIC deck_groups play_seconds envelope_overlay)
|
||||
reasampler_test(deck_values LINK deck_values)
|
||||
|
||||
reasampler_pure_library(curve_popup SOURCES curve_popup.cpp LINK PUBLIC editor_geometry)
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
// 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.
|
||||
// what each one's value MEANS.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "core/instrument/map/sample_map.h" // PlaySeconds (the deck's edit target)
|
||||
#include "core/instrument/map/play_seconds.h" // PlaySeconds (the deck's edit target)
|
||||
#include "core/instrument/ui/deck_groups.h" // DeckParam
|
||||
#include "core/instrument/ui/envelope_overlay.h" // kGateStageMaxSeconds
|
||||
|
||||
@@ -39,9 +39,11 @@ double deckParamNorm(DeckParam id, const PlaySeconds& play);
|
||||
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.
|
||||
// second table of defaults to drift from the real one. It arrives via the norm round trip, so
|
||||
// landing EXACTLY on a stage time (0.003 s attack, 0.060 s release) depends on
|
||||
// kEnvTimeMaxSeconds being a power of two — x/2^n*2^n is lossless, an arbitrary ceiling is not.
|
||||
// Move that ceiling off a power of two and a reset lands a mantissa bit off its own default.
|
||||
// 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
|
||||
|
||||
@@ -190,24 +190,19 @@ 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;
|
||||
bool inKnobFace(const Rect& knob, int x, int y) {
|
||||
const double dx = x - (knob.x + knob.width / 2.0);
|
||||
const double dy = y - (knob.y + knob.height / 2.0);
|
||||
const double r = knob.width / 2.0;
|
||||
return dx * dx + dy * dy < r * r;
|
||||
}
|
||||
} // 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};
|
||||
if (!inKnobFace(c.knob, x, y)) continue;
|
||||
return {c.id, inKnobFace(c.inner, x, y)};
|
||||
}
|
||||
return {}; // inside the group but off every dial
|
||||
}
|
||||
|
||||
@@ -153,11 +153,18 @@ struct DeckFaceHit {
|
||||
bool inner = false; // inside the concentric inner disc
|
||||
};
|
||||
|
||||
// A point inside the circle inscribed in `knob`, boundary-EXCLUSIVE. THE target rule for the
|
||||
// reset gesture wherever a radial knob is drawn — the deck's own faces and the chrome's
|
||||
// preview-velocity dial both resolve through it, so one gesture cannot grow two target rules.
|
||||
bool inKnobFace(const Rect& knob, int x, int y);
|
||||
|
||||
// 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.
|
||||
// band and the cell margins must miss where a drag grab deliberately does not. 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. Unlike hitTestDeck this runs NO toggle/radio precedence pass
|
||||
// first, which is only correct while no toggle rect overlaps a knob circle — a layout change
|
||||
// that lets them overlap has to give this the same precedence order.
|
||||
DeckFaceHit hitTestKnobFace(const DeckLayout& layout, int x, int y);
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
|
||||
@@ -97,4 +97,31 @@ int waveformColumnCount(const KitBox& box) {
|
||||
return w > 0 ? w : 0;
|
||||
}
|
||||
|
||||
WaveformBand waveformBand(int bandTop, int bandHeight) {
|
||||
WaveformBand b;
|
||||
b.top = bandTop;
|
||||
b.height = bandHeight;
|
||||
b.midY = bandTop + bandHeight / 2;
|
||||
b.halfSpan = (bandHeight / 2) - 2; // matches drawWaveform's 2px vertical breathing room
|
||||
return b;
|
||||
}
|
||||
|
||||
WaveformColumnSpan waveformColumnSpan(const WaveformBand& band,
|
||||
double compressedMax, double compressedMin) {
|
||||
const int lo = band.top;
|
||||
const int hi = band.top + band.height - 1;
|
||||
|
||||
WaveformColumnSpan s;
|
||||
s.top = band.midY - static_cast<int>(compressedMax * band.halfSpan);
|
||||
s.bottom = band.midY - static_cast<int>(compressedMin * band.halfSpan);
|
||||
s.topF = band.midY - compressedMax * band.halfSpan;
|
||||
s.bottomF = band.midY - compressedMin * band.halfSpan;
|
||||
|
||||
if (s.top < lo) s.top = lo;
|
||||
if (s.bottom > hi) s.bottom = hi;
|
||||
if (s.topF < lo) s.topF = lo;
|
||||
if (s.bottomF > hi) s.bottomF = hi;
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace reasampler::ui
|
||||
|
||||
@@ -75,11 +75,38 @@ ListRowBox computeListRow(const KitBox& list, int index, int rowHeight);
|
||||
// `rowCount` rows). rowCount bounds the hit so blank space past the last row is a clean miss.
|
||||
int hitTestListRow(int px, int py, const KitBox& list, int rowHeight, int rowCount);
|
||||
|
||||
// --- Waveform column count ---------------------------------------------------
|
||||
// --- Waveform columns --------------------------------------------------------
|
||||
|
||||
// Pixel columns drawWaveform renders inside `box` (its fixed 2px side insets), never negative.
|
||||
// Pass directly as peaks::computeEnvelope's binCount — one bin per column is correct resolution;
|
||||
// overbinning doesn't improve render quality and wastes memory/CPU.
|
||||
int waveformColumnCount(const KitBox& box);
|
||||
|
||||
// One channel band's shared vertical metrics: the zero line every column mirrors about, and the
|
||||
// pixel height a full-scale amplitude reaches (inset so a peak keeps the band's breathing room).
|
||||
struct WaveformBand {
|
||||
int top = 0;
|
||||
int height = 0;
|
||||
int midY = 0;
|
||||
double halfSpan = 0.0;
|
||||
};
|
||||
WaveformBand waveformBand(int bandTop, int bandHeight);
|
||||
|
||||
// The vertical extents of one column: the integer edges of the filled span, and the same edges
|
||||
// at sub-pixel precision for the antialiased outline stroke that joins a column's extremes to
|
||||
// its neighbour's. Both take the band clamp, so the stroke cannot leave the band the fill is
|
||||
// confined to. Amplitudes arrive already through the display curve.
|
||||
struct WaveformColumnSpan {
|
||||
int top = 0;
|
||||
int bottom = 0;
|
||||
double topF = 0.0;
|
||||
double bottomF = 0.0;
|
||||
};
|
||||
|
||||
// Truncation is applied to the SCALED AMPLITUDE and then mirrored about midY — never to the
|
||||
// resulting y, which would round the two edges in opposite directions and leave a column with
|
||||
// |max| == |min| a pixel taller above the zero line than below.
|
||||
WaveformColumnSpan waveformColumnSpan(const WaveformBand& band,
|
||||
double compressedMax, double compressedMin);
|
||||
|
||||
} // namespace reasampler::ui
|
||||
|
||||
Reference in New Issue
Block a user