fix(trigger-seam): thread startFrame into both envelope converters; extract pure triggerPlayLength module + tests

This commit is contained in:
2026-07-27 14:37:05 -04:00
parent c12a374b88
commit 8a7dc4b1c6
7 changed files with 296 additions and 40 deletions
+26 -33
View File
@@ -27,6 +27,7 @@
#include "app_version.h" // vstPluginName (channel-derived editor title band, S18)
#include "sample_map.h"
#include "wav_trim.h" // parseWavLayout, extractFloatFrames
#include "trigger_seam.h" // triggerPlayLength / framesToFadeFraction / fadeFractionToFrames (S-VIEW-3)
#include "waveform_view.h" // frame<->pixel markers + zero-crossing snap (S11)
#ifdef _WIN32
@@ -462,19 +463,8 @@ EnvClampBounds ReaSamplerEditor::envClampBounds() const {
return b;
}
// The played-span source-frame length for the Trigger fade fraction conversion: the post-start
// span scaled by lengthFraction (envelope_overlay.h's TRIGGER SEAM math). `frames` is the total
// source frame count; startFrame comes from the zone's startPoint (0 when unset).
namespace {
std::int64_t triggerPlayLengthFrames(const ZonePlaySeconds& play, std::int64_t frames,
std::int64_t startFrame) {
const std::int64_t postStart = (std::max)(std::int64_t{0}, frames - startFrame);
const double len = play.trigger.lengthFraction * static_cast<double>(postStart);
return static_cast<std::int64_t>(len + 0.5);
}
} // namespace
AmpEnvelope ReaSamplerEditor::packEnvelope(const ZonePlaySeconds& play, std::int64_t frames) const {
AmpEnvelope ReaSamplerEditor::packEnvelope(const ZonePlaySeconds& play, std::int64_t frames,
std::int64_t startFrame) const {
AmpEnvelope env;
env.mode = (play.playMode == PlayMode::Trigger) ? EnvMode::Trigger : EnvMode::Gate;
// AHDSR seconds copy 1-to-1 (rate-free, the same domain the overlay draws).
@@ -484,21 +474,19 @@ AmpEnvelope ReaSamplerEditor::packEnvelope(const ZonePlaySeconds& play, std::int
env.sustainLevel = play.adsr.sustainLevel;
env.releaseSeconds = play.adsr.releaseSeconds;
// Trigger: lengthFraction copies 1-to-1; the fades are DERIVED — source frames over the played
// span (the TRIGGER SEAM converter, PACK direction). A zero play length yields 0 fractions.
// span (the TRIGGER SEAM converter, PACK direction). startFrame is the zone's effective start
// point so the fraction denominator matches the voice's actual post-start span. A zero play
// length yields 0 fractions.
env.lengthFraction = play.trigger.lengthFraction;
const std::int64_t playLen = triggerPlayLengthFrames(play, frames, /*startFrame=*/0);
if (playLen > 0) {
env.fadeInFraction = static_cast<double>(play.trigger.fadeInFrames) / static_cast<double>(playLen);
env.fadeOutFraction = static_cast<double>(play.trigger.fadeOutFrames) / static_cast<double>(playLen);
} else {
env.fadeInFraction = 0.0;
env.fadeOutFraction = 0.0;
}
const std::int64_t playLen =
triggerPlayLength(play.trigger.lengthFraction, frames, startFrame);
env.fadeInFraction = framesToFadeFraction(play.trigger.fadeInFrames, playLen);
env.fadeOutFraction = framesToFadeFraction(play.trigger.fadeOutFrames, playLen);
return env;
}
void ReaSamplerEditor::unpackEnvelope(const AmpEnvelope& env, std::int64_t frames,
ZonePlaySeconds& play) const {
std::int64_t startFrame, ZonePlaySeconds& play) const {
if (env.mode == EnvMode::Gate) {
play.adsr.attackSeconds = env.attackSeconds;
play.adsr.holdSeconds = env.holdSeconds;
@@ -507,14 +495,15 @@ void ReaSamplerEditor::unpackEnvelope(const AmpEnvelope& env, std::int64_t frame
play.adsr.releaseSeconds = env.releaseSeconds;
} else {
// Trigger: lengthFraction copies back; the fades convert fractions -> source frames over
// the played span (the TRIGGER SEAM converter, UNPACK direction). Keep the same (0,1] floor
// on lengthFraction the slider path enforces so a zero-length trigger never plays nothing.
// the played span (the TRIGGER SEAM converter, UNPACK direction). startFrame is the zone's
// effective start point so the frame denominator matches the voice's actual post-start span.
// Keep the same (0,1] floor on lengthFraction the slider path enforces so a zero-length
// trigger never plays nothing.
play.trigger.lengthFraction = (std::max)(0.01, env.lengthFraction);
const std::int64_t playLen = triggerPlayLengthFrames(play, frames, /*startFrame=*/0);
play.trigger.fadeInFrames =
static_cast<std::int64_t>(env.fadeInFraction * static_cast<double>(playLen) + 0.5);
play.trigger.fadeOutFrames =
static_cast<std::int64_t>(env.fadeOutFraction * static_cast<double>(playLen) + 0.5);
const std::int64_t playLen =
triggerPlayLength(play.trigger.lengthFraction, frames, startFrame);
play.trigger.fadeInFrames = fadeFractionToFrames(env.fadeInFraction, playLen);
play.trigger.fadeOutFrames = fadeFractionToFrames(env.fadeOutFraction, playLen);
}
}
@@ -1056,7 +1045,8 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveA
const double rate = liveSampleRate();
if (rate <= 0.0) return;
const double totalSeconds = static_cast<double>(frames) / rate;
const AmpEnvelope env = packEnvelope(zone.play, frames);
const std::int64_t startFrame = zone.startPoint.value_or(0);
const AmpEnvelope env = packEnvelope(zone.play, frames, startFrame);
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, waveArea, totalSeconds);
// Trace the polyline in the categorical secondary accent (teal) so it reads as a distinct
@@ -1687,7 +1677,8 @@ void ReaSamplerEditor::onMouseDown(int x, int y) {
const double rate = liveSampleRate();
if (rate > 0.0) {
const PerformanceZone zone = effectiveSampleZone();
const AmpEnvelope env = packEnvelope(zone.play, frames);
const std::int64_t startFrame = zone.startPoint.value_or(0);
const AmpEnvelope env = packEnvelope(zone.play, frames, startFrame);
const double totalSeconds = static_cast<double>(frames) / rate;
const NodeHit nh = nodeAtPoint(env, waveArea, totalSeconds, x, y);
if (nh.hit) {
@@ -1697,6 +1688,7 @@ void ReaSamplerEditor::onMouseDown(int x, int y) {
dragStartY_ = y;
dragStartEnv_ = env;
dragSampleFrames_ = frames;
dragStartFrame_ = startFrame;
dragStartMap_ = map_;
return; // node moves once the cursor drags
}
@@ -1941,7 +1933,8 @@ void ReaSamplerEditor::onMouseMove(int x, int y) {
totalSeconds, envClampBounds(), dx, dy);
const int zi = ensureSampleZone();
if (zi >= 0) {
unpackEnvelope(edited, frames, map_.zones[static_cast<std::size_t>(zi)].play);
unpackEnvelope(edited, frames, dragStartFrame_,
map_.zones[static_cast<std::size_t>(zi)].play);
selectedZone_ = zi;
}
invalidate(); // live feedback; commit on WM_LBUTTONUP