Zero-lag beat lock: feed-forward timeline + latency lead
The pure exponential follower trailed the beat clock by its time constant (~110ms), which read as the dancer being late on the kick. The clip timeline now advances by itself at the metronome rate (feed-forward, zero structural lag) while a gentler follower only corrects drift, with a 50ms lead compensating beat-detection/display latency and a hard resync when the error exceeds 1.5 beats. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d17cd42958
commit
d905146b13
1 file changed
+13
-6
+13
-6
@@ -595,7 +595,7 @@ class ModelSim {
|
|||||||
// linear phase, allowed to run past 1 while waiting for the next beat:
|
// linear phase, allowed to run past 1 while waiting for the next beat:
|
||||||
// motion must never stall (the callers smooth any re-sync jumps)
|
// motion must never stall (the callers smooth any re-sync jumps)
|
||||||
const p = db.last >= 0 ? Math.max(0, (t - db.last)/db.period) : 0;
|
const p = db.last >= 0 ? Math.max(0, (t - db.last)/db.period) : 0;
|
||||||
return { n: db.n, p };
|
return { n: db.n, p, period: db.period };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dance director for multi-clip GLBs: the clip timeline is PHASE-LOCKED to
|
// Dance director for multi-clip GLBs: the clip timeline is PHASE-LOCKED to
|
||||||
@@ -616,17 +616,24 @@ class ModelSim {
|
|||||||
d.clip = next; d.n0 = bc.n; d.p0 = bc.p; d.sb = 0; d.lastSwitch = t;
|
d.clip = next; d.n0 = bc.n; d.p0 = bc.p; d.sb = 0; d.lastSwitch = t;
|
||||||
}
|
}
|
||||||
const SPB = 0.5*speed; // clip-seconds per music beat
|
const SPB = 0.5*speed; // clip-seconds per music beat
|
||||||
// fluid chase of the beat-locked timeline: absorbs metronome re-syncs
|
// Feed-forward + soft correction: the timeline advances by itself at the
|
||||||
// without the stop-and-go feel of hard phase clamping
|
// metronome rate (zero structural lag — a pure follower trailed the beat
|
||||||
const k = 1 - Math.exp(-dt*9);
|
// by its time constant), the follower only corrects the drift. LEAD
|
||||||
const tgt = Math.max(0, (bc.n - d.n0) + (bc.p - d.p0));
|
// compensates beat-detection/display latency.
|
||||||
|
const LEAD = 0.05;
|
||||||
|
const rate = 1/bc.period; // beats per second
|
||||||
|
const k = 1 - Math.exp(-dt*4);
|
||||||
|
const tgt = Math.max(0, (bc.n - d.n0) + (bc.p - d.p0) + LEAD*rate);
|
||||||
|
d.sb += dt*rate;
|
||||||
d.sb += (tgt - d.sb)*k;
|
d.sb += (tgt - d.sb)*k;
|
||||||
|
if (Math.abs(tgt - d.sb) > 1.5) d.sb = tgt; // hard resync if way off
|
||||||
const cur = this._sampleAnim(this.anims[d.clip], d.sb*SPB);
|
const cur = this._sampleAnim(this.anims[d.clip], d.sb*SPB);
|
||||||
const FADE = 0.45;
|
const FADE = 0.45;
|
||||||
const f = (t - d.lastSwitch)/FADE;
|
const f = (t - d.lastSwitch)/FADE;
|
||||||
if (f >= 1 || d.prev < 0) return cur;
|
if (f >= 1 || d.prev < 0) return cur;
|
||||||
// crossfade with the previous move (same beat-locked, chased timeline)
|
// crossfade with the previous move (same beat-locked, chased timeline)
|
||||||
const ptgt = Math.max(0, (bc.n - d.pn0) + (bc.p - d.pp0));
|
const ptgt = Math.max(0, (bc.n - d.pn0) + (bc.p - d.pp0) + LEAD*rate);
|
||||||
|
d.psb += dt*rate;
|
||||||
d.psb += (ptgt - d.psb)*k;
|
d.psb += (ptgt - d.psb)*k;
|
||||||
const old = this._sampleAnim(this.anims[d.prev], d.psb*SPB);
|
const old = this._sampleAnim(this.anims[d.prev], d.psb*SPB);
|
||||||
const w = f*f*(3 - 2*f);
|
const w = f*f*(3 - 2*f);
|
||||||
|
|||||||
Reference in new issue
Block a user