Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,787 changes: 895 additions & 892 deletions static/highway.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/js/beats_loaded.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test('beats:loaded emit is wired into the WS beats case', () => {
);
assert.match(
block,
/count:\s*beats\.length/,
/count:\s*hwState\.beats\.length/,
'beats:loaded payload must include count = beats.length',
);
});
Expand Down
4 changes: 2 additions & 2 deletions tests/js/handshapes_pipeline.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test('handshapes WS case accumulates incoming chunks into handShapes', () => {
const block = getCaseBlock(src, 'handshapes');
assert.match(
block,
/handShapes\s*=\s*handShapes\.concat\(\s*msg\.data\s*\)/,
/hwState\.handShapes\s*=\s*hwState\.handShapes\.concat\(\s*msg\.data\s*\)/,
'handshapes case must concat msg.data into the handShapes accumulator',
);
});
Expand Down Expand Up @@ -62,7 +62,7 @@ test('bundle exposes handShapes to renderers with flat-list fallback', () => {
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
assert.match(
src,
/\bhandShapes\s*[:=]\s*\([^)]*_filteredHandShapes[^)]*\)\s*\?\s*_filteredHandShapes\s*:\s*handShapes\b/,
/\bhandShapes\s*[:=]\s*\([^)]*hwState\._filteredHandShapes[^)]*\)\s*\?\s*hwState\._filteredHandShapes\s*:\s*hwState\.handShapes\b/,
'bundle must expose handShapes with the _filteredHandShapes-vs-handShapes ternary fallback',
);
});
2 changes: 1 addition & 1 deletion tests/js/highway_3d_lefty.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function src(file) {
test('highway renderer bundles surface the core lefty flag', () => {
assert.match(
src(HIGHWAY_JS),
/lefty\s*[:=]\s*_lefty/,
/lefty\s*[:=]\s*hwState\._lefty/,
'custom renderer bundles must include lefty: _lefty',
);
});
Expand Down
4 changes: 2 additions & 2 deletions tests/js/highway_3d_smooth_clock_pause.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ test('core _makeBundle exposes isPlaying derived from the chart-clock anchor', (
// within the interp cap.
assert.match(
fn,
/isPlaying\s*[:=]\s*!Number\.isNaN\(\s*_chartAnchorPerfNow\s*\)/,
/isPlaying\s*[:=]\s*!Number\.isNaN\(\s*hwState\._chartAnchorPerfNow\s*\)/,
'isPlaying must gate on a live anchor (_chartAnchorPerfNow not NaN)',
);
assert.match(
fn,
/_chartLastAdvanceAt\s*\)\s*<=\s*_CHART_MAX_INTERP_MS/,
/hwState\._chartLastAdvanceAt\s*\)\s*<=\s*_CHART_MAX_INTERP_MS/,
'isPlaying must require the clock advanced within _CHART_MAX_INTERP_MS',
);
});
Expand Down
10 changes: 5 additions & 5 deletions tests/js/highway_adaptive_scale.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function extractBlock(src, signature) {

test('highway declares adaptive-scale state with a floor', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
assert.match(src, /let\s+_autoScale\s*=\s*1/, 'missing _autoScale multiplier');
assert.match(src, /hwState\._autoScale\s*=\s*1/, 'missing _autoScale multiplier');
assert.match(src, /const\s+_AUTO_SCALE_MIN\s*=\s*0?\.25/, 'missing _AUTO_SCALE_MIN floor (0.25)');
assert.match(src, /const\s+_DRAW_BUDGET_HI_MS\s*=\s*\d+/, 'missing high draw budget');
assert.match(src, /const\s+_DRAW_BUDGET_LO_MS\s*=\s*\d+/, 'missing low draw budget');
Expand All @@ -49,18 +49,18 @@ test('_effectiveRenderScale clamps user ceiling * auto factor to [MIN, 1]', () =
test('min render scale floor is user-configurable + exposed on the api (#654)', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
// Hard floor constant kept; configurable floor read from localStorage.
assert.match(src, /let\s+_autoScaleMin\s*=/, 'missing configurable _autoScaleMin');
assert.match(src, /hwState\._autoScaleMin\s*=/, 'missing configurable _autoScaleMin');
assert.match(src, /localStorage\.getItem\('highwayMinRenderScale'\)/,
'configurable floor must load from localStorage.highwayMinRenderScale');
assert.match(src, /setMinRenderScale\(/, 'api.setMinRenderScale missing');
assert.match(src, /getMinRenderScale\(\)\s*\{\s*return\s+_autoScaleMin/, 'api.getMinRenderScale missing');
assert.match(src, /getMinRenderScale\(\)\s*\{\s*return\s+hwState\._autoScaleMin/, 'api.getMinRenderScale missing');
// Floor is clamped to the user ceiling so it can never exceed the manual cap.
const eff = extractBlock(src, 'function _effectiveRenderScale()');
assert.match(eff, /Math\.min\(\s*_autoScaleMin\s*,\s*user\s*\)/,
assert.match(eff, /Math\.min\(\s*hwState\._autoScaleMin\s*,\s*user\s*\)/,
'effective scale must clamp the floor to the user ceiling');
// _adaptRenderScale must cap the lo bound at 1 so _autoScale stays in [_,1].
const adapt = extractBlock(src, 'function _adaptRenderScale(');
assert.match(adapt, /Math\.min\(\s*1\s*,\s*_autoScaleMin\s*\/\s*_renderScale\s*\)/,
assert.match(adapt, /Math\.min\(\s*1\s*,\s*hwState\._autoScaleMin\s*\/\s*hwState\._renderScale\s*\)/,
'lo bound must be capped at 1 to keep _autoScale a [0,1] multiplier');
});

Expand Down
10 changes: 5 additions & 5 deletions tests/js/highway_chord_render_cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ test('_ensureChordRenderCache keys off src, _inverted, AND chordTemplates', () =
const neqEither = (a, b) => new RegExp(
`\\b${a}\\b\\s*!==\\s*\\b${b}\\b|\\b${b}\\b\\s*!==\\s*\\b${a}\\b`
);
assert.match(src, eqEither('_chordRenderCacheSrc', 'src'), 'cache must key on src');
assert.match(src, eqEither('_chordRenderCacheInverted', '_inverted'), 'cache must key on _inverted');
assert.match(src, neqEither('_chordRenderCacheTemplates', 'chordTemplates'),
assert.match(src, eqEither('hwState\\._chordRenderCacheSrc', 'src'), 'cache must key on src');
assert.match(src, eqEither('hwState\\._chordRenderCacheInverted', 'hwState\\._inverted'), 'cache must key on _inverted');
assert.match(src, neqEither('hwState\\._chordRenderCacheTemplates', 'hwState\\.chordTemplates'),
'cache must key on chordTemplates (detected via !== for change-flag)');
});

Expand All @@ -50,9 +50,9 @@ test('chordTemplates change resets fretline preview and frame-mismatch warner',
// block inside the `if (templatesChanged) { … }` branch (e.g. an
// inner conditional reset) doesn't break the match by introducing
// a `}` before the symbol we're checking for.
assert.match(src, /if\s*\(\s*templatesChanged\s*\)\s*\{[\s\S]*?_chordFretLineNotes\s*=\s*\[\][\s\S]*?\}/,
assert.match(src, /if\s*\(\s*templatesChanged\s*\)\s*\{[\s\S]*?hwState\._chordFretLineNotes\s*=\s*\[\][\s\S]*?\}/,
'templatesChanged branch must reset _chordFretLineNotes');
assert.match(src, /if\s*\(\s*templatesChanged\s*\)\s*\{[\s\S]*?_lastChordOnFretLine\s*=\s*null[\s\S]*?\}/,
assert.match(src, /if\s*\(\s*templatesChanged\s*\)\s*\{[\s\S]*?hwState\._lastChordOnFretLine\s*=\s*null[\s\S]*?\}/,
'templatesChanged branch must null _lastChordOnFretLine');
assert.match(src, /if\s*\(\s*templatesChanged\s*\)\s*\{[\s\S]*?_frameMismatchWarned\.clear\(\)[\s\S]*?\}/,
'templatesChanged branch must clear _frameMismatchWarned');
Expand Down
4 changes: 2 additions & 2 deletions tests/js/highway_filtered_notes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ test('getFilteredNotes falls through to notes when _filteredNotes is null', () =
const src = fs.readFileSync(highwayJs, 'utf8');
assert.match(
src,
/getFilteredNotes\s*\(\s*\)\s*\{[^}]*_filteredNotes[^}]*:\s*notes/,
/getFilteredNotes\s*\(\s*\)\s*\{[^}]*_filteredNotes[^}]*:\s*hwState\.notes/,
'getFilteredNotes must return notes as fallback',
);
});
Expand All @@ -41,7 +41,7 @@ test('getFilteredChords falls through to chords when _filteredChords is null', (
const src = fs.readFileSync(highwayJs, 'utf8');
assert.match(
src,
/getFilteredChords\s*\(\s*\)\s*\{[^}]*_filteredChords[^}]*:\s*chords/,
/getFilteredChords\s*\(\s*\)\s*\{[^}]*_filteredChords[^}]*:\s*hwState\.chords/,
'getFilteredChords must return chords as fallback',
);
});
Expand Down
50 changes: 28 additions & 22 deletions tests/js/highway_monotonic_clock.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ function extractBlock(src, signature) {
// + getTime methods so behavioral tests can exercise the real
// implementation in isolation.
function buildClockSandbox(perfNowImpl) {
const sandbox = {
// The lifted per-instance state now lives on `hwState` (the R3c H lift);
// the extracted setTime/getTime bodies reference hwState.<slot>. The const
// _CHART_MAX_INTERP_MS was NOT lifted, so it stays a top-level global here.
const hwState = {
chartTime: 0,
currentTime: 0,
avOffsetSec: 0,
Expand All @@ -50,6 +53,9 @@ function buildClockSandbox(perfNowImpl) {
_chartAnchorPerfNow: NaN,
_chartLastAdvanceAt: 0,
_chartObservedRate: 1,
};
const sandbox = {
hwState,
_CHART_MAX_INTERP_MS: 100,
performance: { now: perfNowImpl },
};
Expand All @@ -72,10 +78,10 @@ test('highway declares chart anchor + stall-detect + rate state', () => {
// particular MUST start as NaN, not 0, otherwise setTime(0) on the
// very first 60 Hz tick fails the `t !== _chartAnchorAudioT` check
// and never re-anchors, leaving the clock uninitialized.
assert.match(src, /let\s+_chartAnchorAudioT\s*=\s*NaN/, 'missing _chartAnchorAudioT (NaN sentinel)');
assert.match(src, /let\s+_chartAnchorPerfNow\s*=\s*NaN/, 'missing _chartAnchorPerfNow (NaN sentinel)');
assert.match(src, /let\s+_chartLastAdvanceAt\s*=\s*0/, 'missing _chartLastAdvanceAt (pause detection)');
assert.match(src, /let\s+_chartObservedRate\s*=\s*1/, 'missing _chartObservedRate (playback rate awareness)');
assert.match(src, /hwState\._chartAnchorAudioT\s*=\s*NaN/, 'missing _chartAnchorAudioT (NaN sentinel)');
assert.match(src, /hwState\._chartAnchorPerfNow\s*=\s*NaN/, 'missing _chartAnchorPerfNow (NaN sentinel)');
assert.match(src, /hwState\._chartLastAdvanceAt\s*=\s*0/, 'missing _chartLastAdvanceAt (pause detection)');
assert.match(src, /hwState\._chartObservedRate\s*=\s*1/, 'missing _chartObservedRate (playback rate awareness)');
assert.match(src, /const\s+_CHART_MAX_INTERP_MS\s*=\s*100/, 'missing _CHART_MAX_INTERP_MS cap');
});

Expand All @@ -86,7 +92,7 @@ test('getTime scales interpolation by _chartObservedRate (speed-slider safe)', (
const slice = m[0];
assert.match(
slice,
/_chartObservedRate\s*\*\s*elapsedMs/,
/hwState\._chartObservedRate\s*\*\s*elapsedMs/,
'getTime must scale interpolation by observed rate so audio.playbackRate != 1 stays accurate',
);
});
Expand All @@ -100,12 +106,12 @@ test('setTime re-anchors and updates _chartLastAdvanceAt only when t actually ch
// The implementation may capture performance.now() into a local
// (e.g. newPerfNow) and assign that to both fields; accept either
// direct or via-local writes.
const m = src.match(/if\s*\(\s*t\s*!==\s*_chartAnchorAudioT\s*\)\s*\{[\s\S]+?\}\s*\},/);
const m = src.match(/if\s*\(\s*t\s*!==\s*hwState\._chartAnchorAudioT\s*\)\s*\{[\s\S]+?\}\s*\},/);
assert.ok(m, 'if (t !== _chartAnchorAudioT) block not found inside setTime');
const block = m[0];
assert.match(block, /_chartAnchorAudioT\s*=\s*t/, 'must assign _chartAnchorAudioT = t');
assert.match(block, /_chartAnchorPerfNow\s*=/, 'must assign _chartAnchorPerfNow');
assert.match(block, /_chartLastAdvanceAt\s*=/, 'must assign _chartLastAdvanceAt');
assert.match(block, /hwState\._chartAnchorAudioT\s*=\s*t/, 'must assign _chartAnchorAudioT = t');
assert.match(block, /hwState\._chartAnchorPerfNow\s*=/, 'must assign _chartAnchorPerfNow');
assert.match(block, /hwState\._chartLastAdvanceAt\s*=/, 'must assign _chartLastAdvanceAt');
});

test('getTime falls back to chartTime when audio has stalled (paused)', () => {
Expand All @@ -119,15 +125,15 @@ test('getTime falls back to chartTime when audio has stalled (paused)', () => {
// Must check stall-since-last-advance against the cap.
assert.match(
slice,
/nowP\s*-\s*_chartLastAdvanceAt\s*>\s*_CHART_MAX_INTERP_MS/,
/nowP\s*-\s*hwState\._chartLastAdvanceAt\s*>\s*_CHART_MAX_INTERP_MS/,
'getTime must short-circuit when audio has stalled past the cap',
);
// Must interpolate when active.
assert.match(slice, /performance\.now\(\)|nowP/, 'getTime must use perfNow');
// Rate-scaled formula: _chartAnchorAudioT + (_chartObservedRate * elapsedMs) / 1000
assert.match(
slice,
/_chartAnchorAudioT\s*\+\s*\(\s*_chartObservedRate\s*\*\s*elapsedMs\s*\)\s*\/\s*1000/,
/_chartAnchorAudioT\s*\+\s*\(\s*hwState\._chartObservedRate\s*\*\s*elapsedMs\s*\)\s*\/\s*1000/,
'getTime must compute anchor + rate-scaled elapsed during play',
);
});
Expand All @@ -138,10 +144,10 @@ test('api.stop() clears the chart anchor state so re-init starts fresh', () => {
// the actual stop() body — a fixed-size slice would falsely match
// resets that landed in an adjacent method.
const stopBlock = extractBlock(src, 'stop() {');
assert.match(stopBlock, /_chartAnchorAudioT\s*=\s*NaN/, 'stop() must reset _chartAnchorAudioT to the NaN sentinel');
assert.match(stopBlock, /_chartAnchorPerfNow\s*=\s*NaN/, 'stop() must reset _chartAnchorPerfNow to the NaN sentinel');
assert.match(stopBlock, /_chartLastAdvanceAt\s*=\s*0/, 'stop() must reset _chartLastAdvanceAt');
assert.match(stopBlock, /_chartObservedRate\s*=\s*1/, 'stop() must reset _chartObservedRate to 1x');
assert.match(stopBlock, /hwState\._chartAnchorAudioT\s*=\s*NaN/, 'stop() must reset _chartAnchorAudioT to the NaN sentinel');
assert.match(stopBlock, /hwState\._chartAnchorPerfNow\s*=\s*NaN/, 'stop() must reset _chartAnchorPerfNow to the NaN sentinel');
assert.match(stopBlock, /hwState\._chartLastAdvanceAt\s*=\s*0/, 'stop() must reset _chartLastAdvanceAt');
assert.match(stopBlock, /hwState\._chartObservedRate\s*=\s*1/, 'stop() must reset _chartObservedRate to 1x');
});

// ── Behavioral tests (run extracted setTime/getTime in vm sandbox) ──────
Expand Down Expand Up @@ -195,12 +201,12 @@ test('behavior: seek discontinuity resets observed rate to 1x', () => {
sb.setTime(10);
now = 50;
sb.setTime(10.025); // observed rate ≈ 0.5
assert.ok(Math.abs(sb._chartObservedRate - 0.5) < 0.001, `prior segment must measure ≈0.5, got ${sb._chartObservedRate}`);
assert.ok(Math.abs(sb.hwState._chartObservedRate - 0.5) < 0.001, `prior segment must measure ≈0.5, got ${sb.hwState._chartObservedRate}`);
// Seek: large t jump in same perf delta — observed-rate clamp
// rejects this segment, resets to 1.
now = 70;
sb.setTime(120); // dPerf=20ms, dT=110s → observed=5500 (out of clamp)
assert.equal(sb._chartObservedRate, 1, 'seek must reset rate to 1x');
assert.equal(sb.hwState._chartObservedRate, 1, 'seek must reset rate to 1x');
});

test('behavior: getTime caps interpolation at _CHART_MAX_INTERP_MS', () => {
Expand All @@ -225,8 +231,8 @@ test('behavior: setTime(0) on first tick anchors correctly (boot edge case)', ()
const sb = buildClockSandbox(() => now);
sb.setTime(0);
// Anchor must now be initialized.
assert.equal(sb._chartAnchorAudioT, 0, 'setTime(0) on first tick must set anchor.audioT');
assert.equal(sb._chartAnchorPerfNow, 16, 'setTime(0) on first tick must set anchor.perfNow');
assert.equal(sb.hwState._chartAnchorAudioT, 0, 'setTime(0) on first tick must set anchor.audioT');
assert.equal(sb.hwState._chartAnchorPerfNow, 16, 'setTime(0) on first tick must set anchor.perfNow');
// getTime should return a finite value, not NaN.
const t = sb.getTime();
assert.ok(!Number.isNaN(t), `getTime must not return NaN after setTime(0); got ${t}`);
Expand All @@ -252,10 +258,10 @@ test('behavior: long anchor gap resets observed rate to 1x', () => {
sb.setTime(10);
now = 50;
sb.setTime(10.025); // observed rate ≈ 0.5
assert.ok(Math.abs(sb._chartObservedRate - 0.5) < 0.001, 'first segment measured 0.5x');
assert.ok(Math.abs(sb.hwState._chartObservedRate - 0.5) < 0.001, 'first segment measured 0.5x');
// Long gap (1 second) before next setTime — out of the dPerf < 0.5
// window, so the rate must reset to 1.
now = 1100;
sb.setTime(10.5);
assert.equal(sb._chartObservedRate, 1, 'long anchor gap must reset rate to 1x');
assert.equal(sb.hwState._chartObservedRate, 1, 'long anchor gap must reset rate to 1x');
});
12 changes: 6 additions & 6 deletions tests/js/highway_note_state.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ function extractBlock(src, signature) {

test('highway declares the note-state provider slot', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
assert.match(src, /let\s+_noteStateProvider\s*=\s*null/, 'missing _noteStateProvider (provider slot, null = none)');
assert.match(src, /hwState\._noteStateProvider\s*=\s*null/, 'missing _noteStateProvider (provider slot, null = none)');
});

test('public API exposes setNoteStateProvider / getNoteStateProvider / getNoteState / isDefaultRenderer', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
assert.match(src, /setNoteStateProvider\s*\(\s*fn\s*\)\s*\{[^}]*_noteStateProvider\s*=/, 'setNoteStateProvider must assign _noteStateProvider');
assert.match(src, /setNoteStateProvider\s*\(\s*fn\s*\)\s*\{[^}]*hwState\._noteStateProvider\s*=/, 'setNoteStateProvider must assign _noteStateProvider');
assert.match(src, /setNoteStateProvider\s*\(\s*fn\s*\)\s*\{[^}]*typeof\s+fn\s*===\s*['"]function['"][^}]*:\s*null/, 'setNoteStateProvider must coerce non-functions (incl. null) to null');
assert.match(src, /getNoteStateProvider\s*\(\s*\)\s*\{\s*return\s+_noteStateProvider/, 'getNoteStateProvider must return the slot');
assert.match(src, /getNoteStateProvider\s*\(\s*\)\s*\{\s*return\s+hwState\._noteStateProvider/, 'getNoteStateProvider must return the slot');
assert.match(src, /getNoteState\s*\(\s*note\s*,\s*chartTime\s*\)\s*\{\s*return\s+_noteState\s*\(/, 'getNoteState must delegate to _noteState');
assert.match(src, /isDefaultRenderer\s*\(\s*\)\s*\{\s*return\s+_renderer\s*===\s*_defaultRenderer\s*\|\|\s*_renderer\s*==\s*null/, 'isDefaultRenderer must be (_renderer === _defaultRenderer || _renderer == null)');
assert.match(src, /isDefaultRenderer\s*\(\s*\)\s*\{\s*return\s+hwState\._renderer\s*===\s*_defaultRenderer\s*\|\|\s*hwState\._renderer\s*==\s*null/, 'isDefaultRenderer must be (_renderer === _defaultRenderer || _renderer == null)');
});

test('_makeBundle exposes getNoteState (stable reference, no per-frame alloc)', () => {
Expand Down Expand Up @@ -72,15 +72,15 @@ test('_makeBundle exposes getNoteStateProvider as a stable reference (feedBack#2
// slot, so renderers see a live "is a provider registered?" view.
assert.match(
src,
/function\s+_getNoteStateProvider\s*\(\s*\)\s*\{\s*return\s+_noteStateProvider\s*;?\s*\}/,
/function\s+_getNoteStateProvider\s*\(\s*\)\s*\{\s*return\s+hwState\._noteStateProvider\s*;?\s*\}/,
'_getNoteStateProvider must be defined as a stable named function returning _noteStateProvider'
);
});

test('_noteState normalizes provider output as documented', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const fn = extractBlock(src, 'function _noteState(note, chartTime)');
assert.match(fn, /if\s*\(\s*!_noteStateProvider\s*\)\s*return\s+null/, 'must short-circuit when no provider is registered');
assert.match(fn, /if\s*\(\s*!hwState\._noteStateProvider\s*\)\s*return\s+null/, 'must short-circuit when no provider is registered');
assert.match(fn, /try\s*\{[\s\S]*_noteStateProvider\s*\([\s\S]*catch[\s\S]*return\s+null/, 'must call the provider inside try/catch and return null on throw');
assert.match(fn, /state\s*!==\s*['"]hit['"]\s*&&\s*state\s*!==\s*['"]active['"]\s*&&\s*state\s*!==\s*['"]miss['"]/, 'must reject states other than hit/active/miss');
assert.match(fn, /Math\.max\(\s*0\s*,\s*Math\.min\(\s*1\s*,\s*raw\.alpha\s*\)\s*\)/, 'must clamp alpha to [0,1]');
Expand Down
Loading
Loading