Close Γ-W1-T7 re-review: pitch-sync cadence math, floor-model regression check, evidence-count fix, one-home comments
New cadence-collapse-band test at P=1470 shows PSOLA eliminates the corner rather than regressing it (18.52% -> 0.00%).
This commit is contained in:
@@ -394,10 +394,21 @@ static void testTheLoopStandsInForTheSourceOnlyWhenItCostsNoSearchBand() {
|
||||
periodAnalysisSpan(frames, 60000, 60000 + static_cast<std::int64_t>(minimum), true, rate);
|
||||
CHECK(atMinimum.from == 60000 && atMinimum.count == minimum);
|
||||
|
||||
// And the too-short loop still DETECTS through the wider span — refusing there would be a
|
||||
// regression against analysing the whole source, and a short sustain loop is common.
|
||||
const double p = static_cast<double>(rate) / 30.0;
|
||||
const std::vector<AudioSample> src = sineOfPeriod(frames, p);
|
||||
|
||||
// The span IS taken at the minimum, but that alone doesn't say detection BEHAVES there: at
|
||||
// exactly one probe block, detectPeriod takes the lone-probe carve-out (no agreement check
|
||||
// at all) — the span choice and the accept rule meet at this exact boundary, and that
|
||||
// meeting point is what needs to actually detect, not just be selected.
|
||||
const PeriodEstimate atMin = detectPeriod(src, rate, atMinimum.from, atMinimum.count);
|
||||
std::printf(" loop at the minimum span -> %s (%.3f, want %.3f)\n",
|
||||
atMin.valid() ? "detected" : "NONE", atMin.frames, p);
|
||||
CHECK(atMin.valid());
|
||||
if (atMin.valid()) CHECK(std::fabs(atMin.frames - p) < 0.5);
|
||||
|
||||
// And the too-short loop still DETECTS through the wider span — refusing there would be a
|
||||
// regression against analysing the whole source, and a short sustain loop is common.
|
||||
const PeriodEstimate est = detectPeriod(src, rate, shortLoop.from, shortLoop.count);
|
||||
std::printf(" short loop -> whole source: %s (%.3f)\n", est.valid() ? "detected" : "NONE",
|
||||
est.frames);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// The one gated case that runs the WHOLE load->voice wire through REAL detection, closing the
|
||||
// gap between two halves proven separately: testBuildSampleDataDetectsThirtyHertzSourcePeriod
|
||||
// (test_sample_map.cpp, detectPeriod -> SampleData) never renders, and
|
||||
// testSourcePeriodChangesTheRenderedStream (test_sampler_core.cpp, SampleData -> Voice -> audio)
|
||||
// sets sourcePeriodFrames by hand rather than detecting it from PCM. Deliberately its own
|
||||
// target: sample_map_tests and sampler_core_tests each keep their one-lib-only structural proof
|
||||
// (map doesn't link the voice engine, the engine doesn't link period_detect), so bridging the
|
||||
// two lives here instead of extending either.
|
||||
|
||||
#include "../src/core/instrument/map/sample_map.h"
|
||||
#include "../src/core/instrument/engine/voice_engine.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
using namespace reasampler;
|
||||
using namespace reasampler::instrument::engine;
|
||||
using namespace reasampler::instrument::map;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
|
||||
// An ADSR that stays fully open (level 1) forever while held — isolates the render from
|
||||
// envelope shaping (matches test_sampler_core.cpp's flatAdsr; not shared, both files stand
|
||||
// alone).
|
||||
static AdsrParams flatAdsr() {
|
||||
AdsrParams a;
|
||||
a.attackFrames = 0;
|
||||
a.decayFrames = 0;
|
||||
a.sustainLevel = 1.0;
|
||||
a.releaseFrames = 0;
|
||||
return a;
|
||||
}
|
||||
|
||||
// FNV-1a over the raw float bits (matches test_sampler_core.cpp's hashStream).
|
||||
static std::uint64_t hashStream(const std::vector<AudioSample>& v) {
|
||||
std::uint64_t h = 1469598103934665603ull;
|
||||
for (const AudioSample s : v) {
|
||||
std::uint32_t bits = 0;
|
||||
std::memcpy(&bits, &s, sizeof(bits));
|
||||
for (int b = 0; b < 4; ++b) {
|
||||
h ^= static_cast<std::uint64_t>((bits >> (8 * b)) & 0xffu);
|
||||
h *= 1099511628211ull;
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static void renderVoice(const SampleData& s, int note, std::int64_t window,
|
||||
std::size_t outFrames, std::vector<AudioSample>& out) {
|
||||
Voice v;
|
||||
v.presizePreserveShifters(window);
|
||||
v.start(note, 127, s, /*declickTakeover=*/false, /*rate=*/1.0);
|
||||
out.resize(outFrames);
|
||||
for (std::size_t i = 0; i < outFrames; ++i) out[i] = v.renderFrame();
|
||||
}
|
||||
|
||||
// 30 Hz @ 44.1k through the REAL wire: buildSampleData (sample_map.cpp:333-334) calls
|
||||
// detectPeriod itself, so this proves the detected period actually reaches and moves the
|
||||
// Preserve render — not just that a hand-set sourcePeriodFrames does (that is the sampler_core
|
||||
// half; this is the missing map->engine seam).
|
||||
static void testDetectedPeriodReachesAndMovesThePreserveRender() {
|
||||
const std::int64_t w = 2205; // the product window at 44.1k
|
||||
const int rate = 44100;
|
||||
const std::size_t frames = 30000;
|
||||
std::vector<AudioSample> pcm(frames);
|
||||
for (std::size_t i = 0; i < frames; ++i) {
|
||||
pcm[i] = static_cast<float>(
|
||||
std::sin(2.0 * kPi * 30.0 * static_cast<double>(i) / rate));
|
||||
}
|
||||
|
||||
SelectedSample ref;
|
||||
ref.relativePath = "b/a.wav";
|
||||
ref.rootNote = 60;
|
||||
SampleData on = buildSampleData(resolveCapture(ref, InstrumentParams{}),
|
||||
DecodedPcm{pcm, rate, {}});
|
||||
CHECK(std::fabs(on.sourcePeriodFrames - 1470.0) < 2.0); // 44100 / 30 Hz, real detection
|
||||
|
||||
on.play.adsr = flatAdsr();
|
||||
on.play.pitchEngine = PitchEngine::Preserve;
|
||||
SampleData off = on;
|
||||
off.sourcePeriodFrames = 0.0; // the fixed-window fallback the pre-wire render used
|
||||
|
||||
std::vector<AudioSample> outOn, outOff;
|
||||
renderVoice(on, /*note=*/67, w, 6000, outOn); // +7 st: real splices
|
||||
renderVoice(off, 67, w, 6000, outOff);
|
||||
for (AudioSample v : outOn) CHECK(std::isfinite(v));
|
||||
CHECK(hashStream(outOn) != hashStream(outOff)); // the detected period actually moved the render
|
||||
}
|
||||
|
||||
int main() {
|
||||
testDetectedPeriodReachesAndMovesThePreserveRender();
|
||||
|
||||
if (g_fail == 0) {
|
||||
std::printf("all period_render_integration tests passed\n");
|
||||
return 0;
|
||||
}
|
||||
std::printf("%d period_render_integration check(s) failed\n", g_fail);
|
||||
return 1;
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
using namespace reasampler;
|
||||
@@ -920,6 +921,9 @@ static void testThirtyHertzSplicesAlignOnceTheSourcePeriodIsKnown() {
|
||||
{"34 Hz rate 2.0", 34.0, 2.0, 0.0},
|
||||
};
|
||||
double controlWorst = 0.0, subjectWorst = 0.0, subjectOffWorst = 0.0;
|
||||
// std::max alone floors a NEGATIVE floor-relative excess to 0 — but a negative excess means
|
||||
// the floor model mismatches the render, not a clean one, so track the signed minimum too.
|
||||
double subjectWorstMin = std::numeric_limits<double>::infinity();
|
||||
for (const Row& r : rows) {
|
||||
const double period = 44100.0 / r.freq;
|
||||
std::vector<AudioSample> src(srcLen);
|
||||
@@ -943,6 +947,7 @@ static void testThirtyHertzSplicesAlignOnceTheSourcePeriodIsKnown() {
|
||||
controlWorst = std::max(controlWorst, pctOn);
|
||||
} else {
|
||||
subjectWorst = std::max(subjectWorst, pctOn);
|
||||
subjectWorstMin = std::min(subjectWorstMin, pctOn);
|
||||
subjectOffWorst = std::max(subjectOffWorst, pctOff);
|
||||
}
|
||||
}
|
||||
@@ -953,6 +958,8 @@ static void testThirtyHertzSplicesAlignOnceTheSourcePeriodIsKnown() {
|
||||
std::printf(" [30 Hz] worst subject excess %.2f%% vs worst control excess %.2f%%\n",
|
||||
subjectWorst, controlWorst);
|
||||
CHECK(subjectWorst < 0.10);
|
||||
CHECK(subjectWorstMin > -0.10); // a negative excess this large is a model
|
||||
// mismatch, not a clean render — catch it too
|
||||
CHECK(subjectWorst <= controlWorst + 0.05); // 0.05 absorbs the floor subtraction's sign noise
|
||||
// Vacuity guard, matching testTwentyNineHertzAtRateTwoKeepsItsPitch's sibling check: the
|
||||
// FIXED-WINDOW (no period set) arm is asserted too, so a setSourcePeriod that silently did
|
||||
@@ -1029,6 +1036,48 @@ static void testCadenceCornerIsUnmovedByAPitchSynchronousSplice() {
|
||||
}
|
||||
}
|
||||
|
||||
// M1 (Gamma-W1-T7 re-review): the corner above sits where periodAlignedJump narrows the jump
|
||||
// by only ~10% (2205 -> 2000/2400/2100 at P=500/600/700), never reaching the collapse band
|
||||
// (jump_->0.63-0.67*window) time_stretch.h's own derivation flags as where the recurrence
|
||||
// interval shrinks hardest. P=1470 (30 Hz at 44.1k) is the case that track exists for: n=2
|
||||
// overshoots jumpMax_ (2*1470=2940 > 2756), forcing n=1 and jump_=1470=0.667*window — 1.5x the
|
||||
// splice rate of the fixed-window fallback.
|
||||
//
|
||||
// MEASURED (Debug, this machine): metric floor 55.86% (P=1470's want-period of 5880 fr under a
|
||||
// 32768-frame segment leaks a lot of mainlobe, same effect as the P=500-700 rows, just larger),
|
||||
// fixed-window excess 18.52%, pitch-synchronous excess 0.00%. The faster cadence does NOT
|
||||
// regress this corner — every splice at n=1 lands exactly one source period away, so despite
|
||||
// firing 1.5x as often each one is phase-perfect rather than merely aligned-on-average, and the
|
||||
// corner clears rather than worsens. Recorded as read, not tuned: if a future change moves
|
||||
// these numbers, update this comment to match, don't loosen the bounds to hide it.
|
||||
static void testCadenceCollapseBandAtThirtyHertzUnderPitchSynchronousSplice() {
|
||||
using reasampler::test_support::energyOutsideFundamentalPercent;
|
||||
const std::int64_t w = 2205;
|
||||
const double rate = 2.0;
|
||||
const double shift = std::pow(2.0, -24.0 / 12.0); // -24 st, the same corner as above
|
||||
const double period = 1470.0; // 30 Hz at 44.1k
|
||||
const std::size_t outFrames = 60000, from = 20000, len = 32768;
|
||||
const std::size_t srcLen = 400000;
|
||||
std::vector<AudioSample> src(srcLen);
|
||||
for (std::size_t i = 0; i < srcLen; ++i) {
|
||||
src[i] = static_cast<AudioSample>(std::sin(2.0 * kPi * static_cast<double>(i) / period));
|
||||
}
|
||||
const std::vector<double> off = runStretch(src, w, rate, shift, outFrames, nullptr);
|
||||
const std::vector<double> on = runStretch(src, w, rate, shift, outFrames, nullptr, period);
|
||||
for (double v : on) CHECK(std::isfinite(v));
|
||||
const double want = period / shift;
|
||||
const double floor = idealToneFloorPercent(want, from, len);
|
||||
const double pctOff = energyOutsideFundamentalPercent(off, from, len, want);
|
||||
const double pctOn = energyOutsideFundamentalPercent(on, from, len, want);
|
||||
std::printf(" [cadence collapse band, PSOLA] period %.0f (want %.0f, metric floor %.2f%%): "
|
||||
"excess %.2f%% -> %.2f%%\n", period, want, floor, pctOff - floor, pctOn - floor);
|
||||
CHECK(pctOn - floor < 1.0); // measured 0.00%: phase-perfect at n=1, not merely aligned
|
||||
// Vacuity guard (shape of testThirtyHertzSplicesAlignOnceTheSourcePeriodIsKnown's :961): the
|
||||
// FIXED-WINDOW arm is asserted too (measured 18.52% excess), so a setSourcePeriod that
|
||||
// silently did nothing would render both arms identically and pass the bound above by luck.
|
||||
CHECK(pctOff - floor > pctOn - floor + 1.0);
|
||||
}
|
||||
|
||||
// The two new entry points on a shifter that was never configured (a Varispeed voice's) —
|
||||
// neither may touch the empty ring.
|
||||
static void testStretchEntryPointsOnPassThrough() {
|
||||
@@ -1056,6 +1105,7 @@ int main() {
|
||||
testThirtyHertzSplicesAlignOnceTheSourcePeriodIsKnown();
|
||||
testTwentyNineHertzAtRateTwoKeepsItsPitch();
|
||||
testCadenceCornerIsUnmovedByAPitchSynchronousSplice();
|
||||
testCadenceCollapseBandAtThirtyHertzUnderPitchSynchronousSplice();
|
||||
testStretchEntryPointsOnPassThrough();
|
||||
|
||||
if (g_fail == 0) {
|
||||
|
||||
@@ -592,8 +592,15 @@ static void reportFloorProbeMechanism() {
|
||||
int n = 0;
|
||||
const bool reach = alignmentReachable(period, lo, hi, &n);
|
||||
const double freq = static_cast<double>(sr) / period;
|
||||
// The cadence inequality from time_stretch.h, evaluated for this row.
|
||||
const double cadence = static_cast<double>(w) / std::fabs(rate - shift);
|
||||
// The cadence inequality from time_stretch.h, evaluated for this row against the
|
||||
// ACTUAL nominal jump this geometry resolves to — under g_pitchSynchronous that is
|
||||
// periodAlignedJump's answer, not always the fixed window, so the two passes of this
|
||||
// report (fixed-window / pitch-synchronous) must not print the same number.
|
||||
PitchShifter jumpProbe;
|
||||
jumpProbe.configure(w);
|
||||
jumpProbe.setSourcePeriod(g_pitchSynchronous ? period : 0.0);
|
||||
const double cadence =
|
||||
static_cast<double>(jumpProbe.spliceJump()) / std::fabs(rate - shift);
|
||||
const double outPeriod = period / shift;
|
||||
std::printf(" P=%5.0f (%.1f Hz): alignable in [%.0f,%.0f]? %s%s | cadence %.0f fr vs "
|
||||
"output period %.0f fr -> %s\n",
|
||||
|
||||
Reference in New Issue
Block a user