mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user