Γ-W1-T7: make Preserve's splices pitch-synchronous — the jump is a whole number of the source's own period, detected once at load
30 Hz out-of-band energy 15.45% -> 0.00%; the 29 Hz rate-2.0 detune -133 -> +0 cents. An unknown period keeps the fixed-window geometry bit for bit. The detector cannot reach process(): sampler_core does not link it.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
// period_detect — the source's own fundamental period, estimated ONCE per load from decoded
|
||||
// PCM, for the Preserve splice's pitch-synchronous jump (pitch_shift.h's periodAlignedJump).
|
||||
//
|
||||
// Runs off the audio thread BY LINK GRAPH: sampler_core does not link this module, so no
|
||||
// translation unit on the render path can name detectPeriod. A sampler's source is fixed and
|
||||
// fully known at load, which is the whole reason a detector is affordable here at all.
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/peaks.h" // AudioSample (float)
|
||||
|
||||
namespace reasampler::instrument::engine {
|
||||
|
||||
using audio::AudioSample;
|
||||
|
||||
// The period the source repeats at, in SOURCE frames, or none. Derived from the audio, never
|
||||
// authored and never persisted — this is a cache, not state.
|
||||
struct PeriodEstimate {
|
||||
double frames = 0.0; // 0 = no single period (inharmonic, polyphonic, percussive, noise)
|
||||
double confidence = 0.0; // 1 - the accepted dissimilarity, [0,1]; 0 when frames == 0
|
||||
|
||||
bool valid() const { return frames > 0.0; }
|
||||
};
|
||||
|
||||
// Fundamental bounds the search runs over. The LOW bound is the load-bearing one: a period
|
||||
// only buys anything while it fits the splice's reachable jump (~1.25 windows, i.e. ~16 Hz at
|
||||
// the product's 50 ms window), so searching below it would return periods the shifter must
|
||||
// reject anyway. The high bound is generous — a period that short already has dozens of
|
||||
// aligned landing points inside the search interval, so alignment was never in question there.
|
||||
inline constexpr double kPeriodDetectMinHz = 15.0;
|
||||
inline constexpr double kPeriodDetectMaxHz = 2000.0;
|
||||
|
||||
// YIN's absolute threshold: the first dissimilarity dip below this IS the period. A source
|
||||
// that never dips below it has no single period, and detection returns none rather than the
|
||||
// global minimum — the difference between "quiet but real" and "the least bad of nothing".
|
||||
inline constexpr double kPeriodDetectThreshold = 0.12;
|
||||
|
||||
// How many blocks across the sample are estimated independently, and how far apart two of them
|
||||
// may land and still be called the same period. Agreement is what separates a genuinely
|
||||
// periodic source from one whose opening happens to look periodic.
|
||||
inline constexpr int kPeriodDetectProbes = 4;
|
||||
inline constexpr double kPeriodDetectAgreeTolerance = 0.02; // 2% of the median
|
||||
|
||||
// Estimates `pcm`'s fundamental period at `sampleRate`. Cost is bounded by the constants above,
|
||||
// not by the sample length: at most kPeriodDetectProbes blocks of ~2 x the longest searched lag
|
||||
// are analysed however long the source is. Allocates; never call from process().
|
||||
//
|
||||
// Returns an invalid estimate (frames == 0) for silence, noise, and anything whose probes
|
||||
// disagree — the caller's documented fallback is the fixed-window splice geometry.
|
||||
PeriodEstimate detectPeriod(const std::vector<AudioSample>& pcm, int sampleRate);
|
||||
|
||||
} // namespace reasampler::instrument::engine
|
||||
Reference in New Issue
Block a user