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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [Unreleased]

### Fixed

- **Cursor now advances on bass-only (left-hand) beats** — `_svBuildBeatTimeline`
previously walked only the first staff of the first track, so beats that
only existed on the bass staff were missing from the tick index and the
playback cursor would stall or skip during left-hand-only passages. It now
collects beats from all staves of the first track and dedupes by tick
(treble preferred at shared positions).
- **Final beat of a song is now reachable** — `_svSyncCursor`'s binary search
capped `hi` at `beats.length - 2`, making the last beat unreachable by the
search; it's now `beats.length - 1`.

---

## [0.3.0] — 2026-07-08

### Added
Expand Down
27 changes: 19 additions & 8 deletions screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -3201,15 +3201,16 @@ function createFactory() {
}

// ── Beat timeline (tick → Beat lookup) ────────────────────────
// Walks score.tracks[0].staves[0] only — the first staff of the
// first track is sufficient to build the absolute-tick index used
// for cursor sync.
// Collects beats from ALL staves of the first track so bass-only
// positions aren't skipped during cursor lookup, then dedupes by
// tick (treble preferred at shared positions).

function _svBuildBeatTimeline(score) {
const out = [];
try {
trackLoop: for (const track of (score.tracks || [])) {
for (const staff of (track.staves || [])) {
const tracks = score.tracks || [];
if (tracks.length) {
for (const staff of (tracks[0].staves || [])) {
for (const bar of (staff.bars || [])) {
for (const voice of (bar.voices || [])) {
for (const beat of (voice.beats || [])) {
Expand All @@ -3222,12 +3223,22 @@ function createFactory() {
}
}
}
break trackLoop; // first staff per track is sufficient; also exits trackLoop
}
}
} catch (_) {}
out.sort((a, b) => a.start - b.start);
return out;
// Dedupe by tick: keep the first entry at each start tick so
// treble beats win when both staves share the same position
// (staff 0's beats are pushed first and sort is stable).
const deduped = [];
let prevStart = -1;
for (const entry of out) {
if (entry.start !== prevStart) {
deduped.push(entry);
prevStart = entry.start;
}
}
return deduped;
}

function _svFindBeatAtTick(tick) {
Expand Down Expand Up @@ -3287,7 +3298,7 @@ function createFactory() {

// Binary search: largest i where beats[i].time <= currentTime.
if (currentTime < beats[0].time) return;
let lo = 0, hi = beats.length - 2, idx = 0;
let lo = 0, hi = beats.length - 1, idx = 0;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (beats[mid].time <= currentTime) { idx = mid; lo = mid + 1; }
Expand Down
Loading