fix(trigger-seam): thread startFrame into both envelope converters; extract pure triggerPlayLength module + tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -281,12 +281,16 @@ private:
|
||||
|
||||
// PACK (draw): zone play params -> AmpEnvelope. Copies AHDSR seconds directly; derives the
|
||||
// Trigger fade fractions from the source-frame fades over the played span.
|
||||
AmpEnvelope packEnvelope(const ZonePlaySeconds& play, std::int64_t frames) const;
|
||||
// `startFrame` is the zone's effective start point (zone.startPoint.value_or(0)).
|
||||
AmpEnvelope packEnvelope(const ZonePlaySeconds& play, std::int64_t frames,
|
||||
std::int64_t startFrame) const;
|
||||
|
||||
// UNPACK (commit): an edited AmpEnvelope -> the zone's play params. Copies AHDSR seconds
|
||||
// directly; converts the Trigger fade fractions back to source frames over the played span.
|
||||
// `startFrame` is the zone's effective start point (zone.startPoint.value_or(0)).
|
||||
// Mutates `play` in place; only the mode-relevant fields are written.
|
||||
void unpackEnvelope(const AmpEnvelope& env, std::int64_t frames, ZonePlaySeconds& play) const;
|
||||
void unpackEnvelope(const AmpEnvelope& env, std::int64_t frames, std::int64_t startFrame,
|
||||
ZonePlaySeconds& play) const;
|
||||
|
||||
// The clamp bounds envelope_edit uses, matching the control-panel sliders' own domains (so a
|
||||
// node drag can never produce a param a slider couldn't — the S-VIEW-F2 invariant).
|
||||
@@ -385,6 +389,7 @@ private:
|
||||
WaveMarker waveMarker_ = WaveMarker::kStart;
|
||||
SetupMarkers dragStartMarkers_;
|
||||
std::int64_t dragSampleFrames_ = 0; // decoded length of the sample under the drag
|
||||
std::int64_t dragStartFrame_ = 0; // zone startPoint at grab time (0 if absent); for env-node drag
|
||||
|
||||
// S12 scrollbar-thumb drag: the offset held at grab time (the pixel-delta resolver shifts
|
||||
// from it). S12/S15/S16 param-slider drag: which control id + the panel it lives in (the
|
||||
|
||||
@@ -570,15 +570,21 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
// one we consumed; fire it once, then latch the sequence so the same request never re-fires.
|
||||
// Preview note-on/off drive the SAME voice engine as host MIDI (a preview is just a note with
|
||||
// no MIDI wire) — off-thread posted, audio-thread consumed, no lock, no allocation.
|
||||
if (inst) {
|
||||
// Consume (advance the sequence) even when inst is null so a note-on posted while no instrument
|
||||
// is loaded does not re-fire stale on the next instrument load.
|
||||
{
|
||||
const std::uint32_t on = previewOnRequest_.load(std::memory_order_acquire);
|
||||
const std::uint16_t onSeq = static_cast<std::uint16_t>(on >> 16);
|
||||
if (onSeq != 0 && onSeq != previewOnConsumed_) {
|
||||
previewOnConsumed_ = onSeq;
|
||||
const int vel = static_cast<int>((on >> 8) & 0xFF);
|
||||
const int note = static_cast<int>(on & 0xFF);
|
||||
if (vel > 0) inst->engine.noteOn(note, vel);
|
||||
if (inst) {
|
||||
const int vel = static_cast<int>((on >> 8) & 0xFF);
|
||||
const int note = static_cast<int>(on & 0xFF);
|
||||
if (vel > 0) inst->engine.noteOn(note, vel);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inst) {
|
||||
const std::uint32_t off = previewOffRequest_.load(std::memory_order_acquire);
|
||||
const std::uint16_t offSeq = static_cast<std::uint16_t>(off >> 16);
|
||||
if (offSeq != 0 && offSeq != previewOffConsumed_) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// trigger_seam.cpp — PURE Trigger-mode frames↔fraction converter (see trigger_seam.h).
|
||||
|
||||
#include "trigger_seam.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
std::int64_t triggerPlayLength(double lengthFraction,
|
||||
std::int64_t frameCount,
|
||||
std::int64_t startFrame) {
|
||||
const std::int64_t postStart = (std::max)(std::int64_t{0}, frameCount - startFrame);
|
||||
if (postStart <= 0 || lengthFraction <= 0.0) return 0;
|
||||
return static_cast<std::int64_t>(lengthFraction * static_cast<double>(postStart) + 0.5);
|
||||
}
|
||||
|
||||
double framesToFadeFraction(std::int64_t fadeFrames, std::int64_t playLength) {
|
||||
if (playLength <= 0) return 0.0;
|
||||
return static_cast<double>(fadeFrames) / static_cast<double>(playLength);
|
||||
}
|
||||
|
||||
std::int64_t fadeFractionToFrames(double fadeFraction, std::int64_t playLength) {
|
||||
if (playLength <= 0) return 0;
|
||||
return static_cast<std::int64_t>(fadeFraction * static_cast<double>(playLength) + 0.5);
|
||||
}
|
||||
|
||||
} // namespace reasampler::vst
|
||||
@@ -0,0 +1,51 @@
|
||||
// trigger_seam.h — PURE Trigger-mode frames↔fraction converter for the S-VIEW-3 envelope seam.
|
||||
// NO VST3, NO REAPER, NO SWELL/LICE types at the boundary.
|
||||
//
|
||||
// The TRIGGER SEAM (documented in envelope_overlay.h) converts between the two representations
|
||||
// of Trigger fade lengths:
|
||||
//
|
||||
// ENGINE domain (TriggerParams / sampler_core): SOURCE FRAMES — int64_t absolute frame counts
|
||||
// that anchor directly to the voice's source-timeline read pointer.
|
||||
//
|
||||
// OVERLAY domain (AmpEnvelope / envelope_overlay): FRACTIONS — doubles in [0,1] of the played
|
||||
// span, where the played span is:
|
||||
// playLengthFrames = round(lengthFraction * (frameCount - startFrame))
|
||||
// The overlay stores fractions so the drawn shape stays invariant across sample-rate changes;
|
||||
// the engine stores frames so the voice advances correctly at the live rate.
|
||||
//
|
||||
// This module owns the one shared formula so the pack (frames->fractions) and unpack
|
||||
// (fractions->frames) paths are provably consistent and unit-tested independently of the shell.
|
||||
// The shell (reasampler_editor.cpp) calls these two functions from packEnvelope / unpackEnvelope.
|
||||
//
|
||||
// S-VIEW-F2 safety: the fractions produced here are in [0,1] by construction; a caller that
|
||||
// clamps the fractions to [0,1] before writing the AmpEnvelope preserves the slider-range
|
||||
// invariant (a drag can never produce a value a slider couldn't reach).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
// The source-frame length of the Trigger played span:
|
||||
// postStart = max(0, frameCount - startFrame)
|
||||
// playLength = round(lengthFraction * postStart)
|
||||
// `frameCount` is the total decoded sample length in source frames.
|
||||
// `startFrame` is the effective start point (zone.startPoint, or 0 when absent).
|
||||
// `lengthFraction` is TriggerParams::lengthFraction — (0,1], the fraction of the post-start span.
|
||||
// Returns 0 when postStart == 0 or lengthFraction <= 0.
|
||||
std::int64_t triggerPlayLength(double lengthFraction,
|
||||
std::int64_t frameCount,
|
||||
std::int64_t startFrame);
|
||||
|
||||
// Convert a source-frame fade count to a fraction of the play span (PACK direction, draw path).
|
||||
// Returns 0.0 when playLength == 0 (degenerate sample or zero %-length); the fraction is
|
||||
// NOT clamped — the caller clamps to [0,1] when filling AmpEnvelope so the overlay clamp logic
|
||||
// stays in envelope_edit, not here.
|
||||
double framesToFadeFraction(std::int64_t fadeFrames, std::int64_t playLength);
|
||||
|
||||
// Convert a fade fraction to a source-frame count (UNPACK direction, commit path).
|
||||
// Rounds to nearest integer frame. Returns 0 when playLength == 0.
|
||||
std::int64_t fadeFractionToFrames(double fadeFraction, std::int64_t playLength);
|
||||
|
||||
} // namespace reasampler::vst
|
||||
Reference in New Issue
Block a user