fix(orbit): re-enable proportional speed correction for live testing

Flip SPEED_CORRECTION_ENABLED back to true so the proportional drift
controller drives the rate again. The disabled fallback (hold 1.0× +
manual Catch-Up button) stays one line away behind the same flag for a
quick switch-back if the pitch-preserving rate changes are audible again.

Restore the proportional planner tests that the disable commit had
rewritten for the held-rate path.
This commit is contained in:
Psychotoxical
2026-06-23 11:12:21 +02:00
parent 0286b9b161
commit 4c2a5b8853
2 changed files with 65 additions and 22 deletions
+59 -16
View File
@@ -1,34 +1,65 @@
import { describe, expect, it } from 'vitest';
import { planOrbitDriftCorrection, stepRateToward, type DriftCorrectionInput } from './driftCorrectionPlan';
import { RATE_STEP } from './driftCorrectionConstants';
import { RATE_MAX, RATE_MIN, RATE_STEP } from './driftCorrectionConstants';
function input(over: Partial<DriftCorrectionInput> = {}): DriftCorrectionInput {
return { driftMs: 0, hostIsPlaying: true, ...over };
}
// Speed correction is disabled (audible wobble in practice). The planner now
// only holds 1.0× or surfaces the manual Catch-Up button past a moderate drift.
describe('planOrbitDriftCorrection (speed correction disabled)', () => {
// Proportional speed correction is re-enabled (SPEED_CORRECTION_ENABLED = true)
// for another round of live testing. These cover the active controller.
describe('planOrbitDriftCorrection (proportional)', () => {
it('holds when the host is paused', () => {
expect(planOrbitDriftCorrection(input({ driftMs: -9000, hostIsPlaying: false }))).toEqual({ action: 'hold' });
expect(planOrbitDriftCorrection(input({ driftMs: -3000, hostIsPlaying: false }))).toEqual({ action: 'hold' });
});
it('holds for small and moderate drift — never nudges the rate', () => {
for (const d of [-2900, -1500, -1000, 0, 1000, 2900]) {
expect(planOrbitDriftCorrection(input({ driftMs: d }))).toEqual({ action: 'hold' });
}
it('holds inside the deadband', () => {
expect(planOrbitDriftCorrection(input({ driftMs: -900 }))).toEqual({ action: 'hold' }); // < 1000
expect(planOrbitDriftCorrection(input({ driftMs: 900 }))).toEqual({ action: 'hold' });
});
it('surfaces the catch-up button (seek) once drift passes the threshold', () => {
expect(planOrbitDriftCorrection(input({ driftMs: -3100 }))).toEqual({ action: 'seek' });
expect(planOrbitDriftCorrection(input({ driftMs: 4000 }))).toEqual({ action: 'seek' });
expect(planOrbitDriftCorrection(input({ driftMs: 13000 }))).toEqual({ action: 'seek' });
it('reaches the full cap at the full-scale drift', () => {
// FULL_SCALE = 4000: behind 4 s → +10%, ahead 4 s → 10%.
const behind = planOrbitDriftCorrection(input({ driftMs: -4000 }));
expect(behind).toEqual({ action: 'correct', targetRate: RATE_MAX });
const ahead = planOrbitDriftCorrection(input({ driftMs: 4000 }));
expect(ahead).toEqual({ action: 'correct', targetRate: RATE_MIN });
});
it('never proposes a speed correction while disabled', () => {
for (const d of [-50000, -3000, -1600, 1600, 3000, 50000]) {
expect(planOrbitDriftCorrection(input({ driftMs: d })).action).not.toBe('correct');
it('scales gently for a mid drift (no full whack near synced)', () => {
// Halfway between deadband (1000) and full-scale (4000) → ~half the cap.
const p = planOrbitDriftCorrection(input({ driftMs: -2500 }));
expect(p.action).toBe('correct');
if (p.action !== 'correct') return;
// (2500-1000)/(4000-1000) = 0.5 → 1 + 0.10×0.5 = 1.05
expect(p.targetRate).toBeCloseTo(1.05, 5);
});
it('is continuous across the deadband edge — a tiny over-deadband drift gets a tiny nudge', () => {
const p = planOrbitDriftCorrection(input({ driftMs: -1001 }));
expect(p.action).toBe('correct');
if (p.action !== 'correct') return;
expect(p.targetRate).toBeGreaterThan(1.0);
expect(p.targetRate).toBeLessThan(1.001); // essentially neutral right at the edge
});
it('clamps beyond full-scale to the cap', () => {
expect(planOrbitDriftCorrection(input({ driftMs: -7000 }))).toEqual({ action: 'correct', targetRate: RATE_MAX });
});
it('seeks (manual button) only past the hard threshold', () => {
expect(planOrbitDriftCorrection(input({ driftMs: -9000 }))).toEqual({ action: 'seek' }); // > 8000
expect(planOrbitDriftCorrection(input({ driftMs: 9000 }))).toEqual({ action: 'seek' });
});
it('never proposes a rate outside the ±10% cap', () => {
for (const d of [-50000, -3000, 3000, 50000]) {
const p = planOrbitDriftCorrection(input({ driftMs: d }));
if (p.action === 'correct') {
expect(p.targetRate).toBeGreaterThanOrEqual(RATE_MIN - 1e-9);
expect(p.targetRate).toBeLessThanOrEqual(RATE_MAX + 1e-9);
}
}
});
});
@@ -36,12 +67,15 @@ describe('planOrbitDriftCorrection (speed correction disabled)', () => {
describe('stepRateToward', () => {
it('moves at most one step and snaps on arrival', () => {
let rate = 1.0;
const seq = [rate];
for (let i = 0; i < 50 && rate !== 1.1; i += 1) {
const next = stepRateToward(rate, 1.1);
expect(Math.abs(next - rate)).toBeLessThanOrEqual(RATE_STEP + 1e-9);
rate = next;
seq.push(rate);
}
expect(rate).toBeCloseTo(1.1, 9);
expect(seq.length).toBe(11);
});
it('ramps back to exactly 1.0× without float dust', () => {
@@ -49,4 +83,13 @@ describe('stepRateToward', () => {
for (let i = 0; i < 50 && rate !== 1.0; i += 1) rate = stepRateToward(rate, 1.0);
expect(rate).toBe(1.0);
});
it('tracks a lowered target mid-ramp (no overshoot)', () => {
// Heading to 1.10 but the target drops to 1.03 — should settle at 1.03.
let rate = 1.0;
rate = stepRateToward(rate, 1.1); // 1.01
rate = stepRateToward(rate, 1.1); // 1.02
for (let i = 0; i < 10 && rate !== 1.03; i += 1) rate = stepRateToward(rate, 1.03);
expect(rate).toBeCloseTo(1.03, 9);
});
});
+6 -6
View File
@@ -25,13 +25,13 @@ import {
} from './driftCorrectionConstants';
/**
* Automatic speed nudging is **disabled**: in practice every pitch-preserving
* rate change is audible (tempo wobble / DSP distortion), and the host's sync
* at track change plus the manual Catch-Up button keep the guest aligned well
* enough. The proportional controller below is kept behind this flag in case the
* preserve-pitch DSP improves enough to revisit it.
* Toggle for the automatic proportional speed nudging. We disabled it once
* because every pitch-preserving rate change was audible in practice (tempo
* wobble / DSP distortion); it's re-enabled now for another round of live
* testing. Flip to `false` to fall back to "hold 1.0× + manual Catch-Up button
* past DRIFT_CATCHUP_BUTTON_MS" without touching the controller below.
*/
const SPEED_CORRECTION_ENABLED = false;
const SPEED_CORRECTION_ENABLED = true;
export interface DriftCorrectionInput {
/** Smoothed, signed drift: `> 0` guest ahead (slow down), `< 0` behind (speed up). */