I build Pianory, a browser-based piano practice app. AI helped draft this article; I checked its code claims against the current source.

A score can contain a quarter note on beat 1, two eighth-note attacks within beat 2, and quarters on beats 3 and 4. Playback needs to keep one unit for the musical positions and convert to seconds only when scheduling audio. Otherwise a faster note value can accidentally change the click pulse.

Consider this illustrative 4/4 bar: C4 quarter, D4 eighth, D4 eighth, E4 quarter, F4 quarter. In quarter-note units the inferred event starts and lengths are (0, 1), (1, 0.5), (1.5, 0.5), (2, 1), and (3, 1). The two D4 notes are distinct attacks at different positions. This tuple sequence is an example derived from the conversion, not a committed test fixture.

Pianory's score source converts OSMD iterator timestamps and note lengths by multiplying whole-note units by four. Its timeline stores positions and durations in quarter-note units. At quarter-note tempo B, a quarter lasts 60/B seconds and an eighth lasts 30/B seconds. The metronome grid for a complete 4/4 measure still walks quarter positions 0, 1, 2 and 3, with the first beat accented. The player schedules each note at its converted audio time and uses a 96% written-duration sounding length, subject to a 0.06-second floor and passage-end clipping. That small articulation gap does not move the next note's written onset.

I ran pnpm exec vitest run src/lib/sheet/player.spec.ts src/lib/sheet/timeline.spec.ts: 2 files, 39 tests passed. The tests cover quarter onsets, count-in and neighboring duration behavior, but do not assert this exact eighth-note bar or .5 duration. A useful follow-up test would assert both D4 attacks at quarter positions 1 and 1.5 while the click positions remain 0, 1, 2 and 3. That is a source-based expectation, not a measured audio-device result.

Relevant source: src/lib/sheet/types.ts, src/lib/sheet/osmd-source.ts, src/lib/sheet/timeline.ts and src/lib/sheet/player.ts.