feat: run the per-voice filter between the pitch and amp stages, with its own deck
Params ride the one parameter set; payload v8 -> v9, off by default. Deck composition moves to a pure deck_groups module in pitch -> filter -> amp order.
This commit is contained in:
@@ -610,6 +610,57 @@ static void testResolvePlayConvertsWallClockAtTheRate() {
|
||||
CHECK(at96.trigger.fadeInFrames == 441); // still unconverted
|
||||
}
|
||||
|
||||
static void testResolvePlayCarriesTheFilterAndResolvesOnlyItsEnvelope() {
|
||||
// The filter's control positions are already rate-free, so only its envelope crosses the
|
||||
// seconds->frames boundary. A converted norm would be a bug in the other direction: the
|
||||
// same preset must sound identical at 48k and 96k.
|
||||
PlaySeconds st;
|
||||
st.filter.enabled = true;
|
||||
st.filter.settings.cutoffNorm = 0.25f;
|
||||
st.filter.settings.resonanceNorm = 0.75f;
|
||||
st.filter.settings.morphNorm = 0.5f;
|
||||
st.filter.settings.driveNorm = 0.125f;
|
||||
st.filter.settings.morphLaw = reasampler::instrument::engine::filter::MorphLaw::HighNotchLow;
|
||||
st.filter.modAmount = -0.5;
|
||||
st.filter.velAmount = 0.25;
|
||||
st.filter.keyTrack = 1.25;
|
||||
st.filter.env.attackSeconds = 0.01;
|
||||
st.filter.env.holdSeconds = 0.02;
|
||||
st.filter.env.decaySeconds = 0.03;
|
||||
st.filter.env.sustainLevel = 0.4;
|
||||
st.filter.env.releaseSeconds = 0.05;
|
||||
|
||||
const PlayParams at48 = resolvePlay(st, 48000);
|
||||
CHECK(at48.filter.enabled);
|
||||
CHECK(at48.filter.settings.cutoffNorm == 0.25f);
|
||||
CHECK(at48.filter.settings.resonanceNorm == 0.75f);
|
||||
CHECK(at48.filter.settings.morphNorm == 0.5f);
|
||||
CHECK(at48.filter.settings.driveNorm == 0.125f);
|
||||
CHECK(at48.filter.settings.morphLaw == reasampler::instrument::engine::filter::MorphLaw::HighNotchLow);
|
||||
CHECK(at48.filter.modAmount == -0.5);
|
||||
CHECK(at48.filter.velAmount == 0.25);
|
||||
CHECK(at48.filter.keyTrack == 1.25);
|
||||
CHECK(at48.filter.env.attackFrames == 480);
|
||||
CHECK(at48.filter.env.holdFrames == 960);
|
||||
CHECK(at48.filter.env.decayFrames == 1440);
|
||||
CHECK(at48.filter.env.sustainLevel == 0.4); // a level, not a time
|
||||
CHECK(at48.filter.env.releaseFrames == 2400);
|
||||
|
||||
const PlayParams at96 = resolvePlay(st, 96000);
|
||||
CHECK(at96.filter.env.attackFrames == 960);
|
||||
CHECK(at96.filter.env.releaseFrames == 4800);
|
||||
CHECK(at96.filter.settings.cutoffNorm == 0.25f); // rate-free: unchanged
|
||||
|
||||
// Off by default, and the default envelope is a flat unity so a disengaged filter has
|
||||
// nothing to modulate with either.
|
||||
const PlayParams bare = resolvePlay(PlaySeconds{}, 48000);
|
||||
CHECK(!bare.filter.enabled);
|
||||
CHECK(bare.filter.modAmount == 0.0);
|
||||
CHECK(bare.filter.velAmount == 0.0);
|
||||
CHECK(bare.filter.keyTrack == 0.0);
|
||||
CHECK(bare.filter.env.sustainLevel == 1.0);
|
||||
}
|
||||
|
||||
static void testResolvePlayRoundsAndFloorsNegatives() {
|
||||
PlaySeconds st;
|
||||
st.adsr.attackSeconds = 0.0001; // 4.41 frames at 44.1k -> rounds to 4
|
||||
@@ -845,6 +896,7 @@ int main() {
|
||||
testRetainRefsFiltersToPlayedSet();
|
||||
testLegacyLiftDecision();
|
||||
testResolvePlayConvertsWallClockAtTheRate();
|
||||
testResolvePlayCarriesTheFilterAndResolvesOnlyItsEnvelope();
|
||||
testResolvePlayRoundsAndFloorsNegatives();
|
||||
testResolveCaptureUsesIntrinsicsWhenNoOverride();
|
||||
testResolveCaptureOverridesBeatIntrinsics();
|
||||
|
||||
Reference in New Issue
Block a user