fix(trigger-seam): thread startFrame into both envelope converters; extract pure triggerPlayLength module + tests
This commit is contained in:
+14
-1
@@ -762,6 +762,13 @@ add_library(param_slider STATIC src/vst/param_slider.cpp)
|
||||
target_include_directories(param_slider PUBLIC src/vst)
|
||||
target_link_libraries(param_slider PUBLIC editor_geometry)
|
||||
|
||||
# trigger_seam (Phase S-VIEW-3) — PURE Trigger-mode frames<->fraction converter for the TRIGGER
|
||||
# SEAM documented in envelope_overlay.h: triggerPlayLength / framesToFadeFraction /
|
||||
# fadeFractionToFrames. Owns the one formula so pack (draw) and unpack (commit) are provably
|
||||
# consistent. No shell/LICE/REAPER types — only <cstdint>. NEITHER SDK.
|
||||
add_library(trigger_seam STATIC src/vst/trigger_seam.cpp)
|
||||
target_include_directories(trigger_seam PUBLIC src/vst)
|
||||
|
||||
# envelope_overlay (Phase S-VIEW-3) — PURE amp-envelope -> polyline geometry for the Sample-view
|
||||
# envelope overlay: AHDSR (Gate) / fade+%-length (Trigger) params + the sample's wall-clock
|
||||
# duration -> a breakpoint polyline in the waveform rect, at the same time base waveform_view maps.
|
||||
@@ -835,6 +842,12 @@ add_executable(param_slider_tests tests/test_param_slider.cpp)
|
||||
target_link_libraries(param_slider_tests PRIVATE param_slider)
|
||||
add_test(NAME param_slider_tests COMMAND param_slider_tests)
|
||||
|
||||
# trigger_seam (S-VIEW-3): the pure Trigger-mode frames<->fraction converter.
|
||||
# Links ONLY trigger_seam — no editor_geometry dep, the plainest data-boundary proof possible.
|
||||
add_executable(trigger_seam_tests tests/test_trigger_seam.cpp)
|
||||
target_link_libraries(trigger_seam_tests PRIVATE trigger_seam)
|
||||
add_test(NAME trigger_seam_tests COMMAND trigger_seam_tests)
|
||||
|
||||
# envelope_overlay (S-VIEW-3): the pure amp-envelope -> polyline geometry (Gate AHDSR + Trigger
|
||||
# fade/%-length) at the waveform time base. Links ONLY envelope_overlay (+ its editor_geometry
|
||||
# dep) — NEITHER SDK — the plain-data-boundary proof.
|
||||
@@ -1057,7 +1070,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
target_link_libraries(reasampler_vst PRIVATE vst3_sdk editor_geometry bridge_marshal
|
||||
sample_map capture_paths embed_strip app_version capture_browser keyboard_strip
|
||||
waveform_view bank_sync browser_scroll note_entry param_slider
|
||||
theme component_geometry bank_grid envelope_overlay envelope_edit)
|
||||
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit)
|
||||
# SDK_INC gives reaper_vst3_interfaces.h + reaper_plugin_functions.h for the bridge;
|
||||
# WDL_INC gives LICE for the editor. The VST3 SDK headers come from vst3_sdk PUBLIC.
|
||||
target_include_directories(reasampler_vst PRIVATE ${SDK_INC} ${WDL_INC})
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,161 @@
|
||||
// Standalone tests for reasampler::vst::trigger_seam — no VST3, no REAPER, no framework.
|
||||
// Same fast assert loop as the sibling pure tests.
|
||||
//
|
||||
// Covers: triggerPlayLength (zero play length, startFrame set, startFrame past frameCount,
|
||||
// rounding); framesToFadeFraction (zero play length, basic ratio); fadeFractionToFrames
|
||||
// (zero play length, rounding); round-trip fidelity; the Finding 1 regression (start-point
|
||||
// set — the case that was broken before this module existed).
|
||||
|
||||
#include "../src/vst/trigger_seam.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using namespace reasampler::vst;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// --- triggerPlayLength --------------------------------------------------------
|
||||
|
||||
static void testPlayLengthNoStartPoint() {
|
||||
// startFrame == 0 (no start marker): postStart = frameCount.
|
||||
// 1000 frames, lengthFraction 1.0 -> 1000.
|
||||
CHECK(triggerPlayLength(1.0, 1000, 0) == 1000);
|
||||
// 1000 frames, lengthFraction 0.5 -> round(500.0) = 500.
|
||||
CHECK(triggerPlayLength(0.5, 1000, 0) == 500);
|
||||
// 1000 frames, lengthFraction 0.333 -> round(333.0) = 333.
|
||||
CHECK(triggerPlayLength(0.333, 1000, 0) == 333);
|
||||
}
|
||||
|
||||
static void testPlayLengthWithStartPoint() {
|
||||
// The Finding 1 regression: startFrame set, play length must be shorter.
|
||||
// frameCount=1000, startFrame=200 -> postStart=800.
|
||||
// lengthFraction 1.0 -> 800 (NOT 1000 as the pre-fix code produced).
|
||||
CHECK(triggerPlayLength(1.0, 1000, 200) == 800);
|
||||
// lengthFraction 0.5 -> round(400.0) = 400.
|
||||
CHECK(triggerPlayLength(0.5, 1000, 200) == 400);
|
||||
}
|
||||
|
||||
static void testPlayLengthZeroFrameCount() {
|
||||
// No decoded audio -> 0.
|
||||
CHECK(triggerPlayLength(1.0, 0, 0) == 0);
|
||||
}
|
||||
|
||||
static void testPlayLengthStartFramePastEnd() {
|
||||
// startFrame >= frameCount -> postStart clamped to 0 -> play length 0.
|
||||
CHECK(triggerPlayLength(1.0, 500, 500) == 0);
|
||||
CHECK(triggerPlayLength(1.0, 500, 600) == 0);
|
||||
}
|
||||
|
||||
static void testPlayLengthRounding() {
|
||||
// round(0.5) = 1 (round half up via +0.5 truncation: 0.5 + 0.5 = 1.0 -> 1).
|
||||
CHECK(triggerPlayLength(0.5, 1, 0) == 1);
|
||||
// round(lengthFraction * 3): 0.4 * 3 = 1.2 -> 1.
|
||||
CHECK(triggerPlayLength(0.4, 3, 0) == 1);
|
||||
// 0.6 * 3 = 1.8 -> 2.
|
||||
CHECK(triggerPlayLength(0.6, 3, 0) == 2);
|
||||
}
|
||||
|
||||
// --- framesToFadeFraction -----------------------------------------------------
|
||||
|
||||
static void testFramesToFadeFractionBasic() {
|
||||
// 100 frames fade over 1000 play length -> 0.1.
|
||||
const double frac = framesToFadeFraction(100, 1000);
|
||||
CHECK(frac > 0.0999 && frac < 0.1001);
|
||||
}
|
||||
|
||||
static void testFramesToFadeFractionZeroPlayLength() {
|
||||
// Degenerate: zero play length -> 0.0 (no division by zero).
|
||||
CHECK(framesToFadeFraction(100, 0) == 0.0);
|
||||
CHECK(framesToFadeFraction(0, 0) == 0.0);
|
||||
}
|
||||
|
||||
static void testFramesToFadeFractionFullSpan() {
|
||||
// fadeFrames == playLength -> fraction 1.0.
|
||||
const double frac = framesToFadeFraction(500, 500);
|
||||
CHECK(frac > 0.9999 && frac < 1.0001);
|
||||
}
|
||||
|
||||
// --- fadeFractionToFrames -----------------------------------------------------
|
||||
|
||||
static void testFadeFractionToFramesBasic() {
|
||||
// 0.1 of 1000 play length -> round(100.0) = 100.
|
||||
CHECK(fadeFractionToFrames(0.1, 1000) == 100);
|
||||
}
|
||||
|
||||
static void testFadeFractionToFramesZeroPlayLength() {
|
||||
// Degenerate: play length 0 -> 0 frames.
|
||||
CHECK(fadeFractionToFrames(0.5, 0) == 0);
|
||||
}
|
||||
|
||||
static void testFadeFractionToFramesRounding() {
|
||||
// 0.333... of 3 -> round(1.0) = 1.
|
||||
CHECK(fadeFractionToFrames(1.0 / 3.0, 3) == 1);
|
||||
// 0.5 of 3 -> round(1.5) = 2.
|
||||
CHECK(fadeFractionToFrames(0.5, 3) == 2);
|
||||
}
|
||||
|
||||
// --- Round-trip ---------------------------------------------------------------
|
||||
|
||||
static void testRoundTripNoStartPoint() {
|
||||
// Pack then unpack: fadeInFrames should survive (within 1 frame of rounding).
|
||||
// frameCount=44100, startFrame=0, lengthFraction=1.0 -> playLength=44100.
|
||||
// fadeInFrames = 2205 (5% of 44100).
|
||||
const std::int64_t fadeIn = 2205;
|
||||
const std::int64_t playLen = triggerPlayLength(1.0, 44100, 0);
|
||||
const double frac = framesToFadeFraction(fadeIn, playLen);
|
||||
const std::int64_t recovered = fadeFractionToFrames(frac, playLen);
|
||||
// Should be exact (2205 / 44100 * 44100 = 2205.0).
|
||||
CHECK(recovered == fadeIn);
|
||||
}
|
||||
|
||||
static void testRoundTripWithStartPoint() {
|
||||
// The Finding 1 case: startFrame set. frameCount=44100, startFrame=8820 (20%).
|
||||
// postStart=35280, lengthFraction=1.0 -> playLength=35280.
|
||||
// fadeInFrames = 1764 (5% of 35280).
|
||||
const std::int64_t frameCount = 44100;
|
||||
const std::int64_t startFrame = 8820;
|
||||
const std::int64_t fadeIn = 1764;
|
||||
const std::int64_t playLen = triggerPlayLength(1.0, frameCount, startFrame);
|
||||
CHECK(playLen == 35280);
|
||||
const double frac = framesToFadeFraction(fadeIn, playLen);
|
||||
const std::int64_t recovered = fadeFractionToFrames(frac, playLen);
|
||||
CHECK(recovered == fadeIn);
|
||||
}
|
||||
|
||||
static void testRoundTripFadeGreaterThanSpan() {
|
||||
// fadeFrames > playLength -> fraction > 1 (returned unclamped; the overlay clamps at draw).
|
||||
// The shell is responsible for clamping before writing AmpEnvelope.
|
||||
const std::int64_t playLen = 100;
|
||||
const std::int64_t fadeIn = 150;
|
||||
const double frac = framesToFadeFraction(fadeIn, playLen);
|
||||
CHECK(frac > 1.0); // intentionally unclamped from this module's perspective
|
||||
// The round-trip still recovers the original fade, so the shell can clamp after.
|
||||
const std::int64_t recovered = fadeFractionToFrames(frac, playLen);
|
||||
CHECK(recovered == fadeIn);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testPlayLengthNoStartPoint();
|
||||
testPlayLengthWithStartPoint();
|
||||
testPlayLengthZeroFrameCount();
|
||||
testPlayLengthStartFramePastEnd();
|
||||
testPlayLengthRounding();
|
||||
|
||||
testFramesToFadeFractionBasic();
|
||||
testFramesToFadeFractionZeroPlayLength();
|
||||
testFramesToFadeFractionFullSpan();
|
||||
|
||||
testFadeFractionToFramesBasic();
|
||||
testFadeFractionToFramesZeroPlayLength();
|
||||
testFadeFractionToFramesRounding();
|
||||
|
||||
testRoundTripNoStartPoint();
|
||||
testRoundTripWithStartPoint();
|
||||
testRoundTripFadeGreaterThanSpan();
|
||||
|
||||
if (g_fail == 0) std::printf("trigger_seam: all tests passed\n");
|
||||
else std::printf("trigger_seam: %d FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user