<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Giang Pham Truong</title>
    <description>The latest articles on DEV Community by Giang Pham Truong (@ptrgiang).</description>
    <link>https://dev.to/ptrgiang</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4185832%2F28f2b61b-e9a4-4281-97f5-12017bf09519.jpg</url>
      <title>DEV Community: Giang Pham Truong</title>
      <link>https://dev.to/ptrgiang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ptrgiang"/>
    <language>en</language>
    <item>
      <title>Let Course Progress Follow Evidence, Not Session Endings</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Mon, 02 Nov 2026 02:55:47 +0000</pubDate>
      <link>https://dev.to/ptrgiang/let-course-progress-follow-evidence-not-session-endings-j8b</link>
      <guid>https://dev.to/ptrgiang/let-course-progress-follow-evidence-not-session-endings-j8b</guid>
      <description>&lt;p&gt;I build Pianory, a browser-based piano practice app. AI helped draft this article; I checked every implementation claim against the current course source and focused spec file.&lt;/p&gt;

&lt;p&gt;A finished player session tells you that playback ended. It does not, by itself, tell you that a course step met its completion rule. In Pianory’s course domain model, progress is tied to the evidence each step asks for.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;courseAttemptMeetsPolicy&lt;/code&gt; first requires a completed attempt with evidence. It then applies the step’s rule: a self-report needs the configured rating, pass count, and criteria, plus any configured delayed-review requirement; a quiz needs the minimum total and correct answers; measured evidence needs a passing observation with supported input, enough confidence, and the matching capability and benchmark version.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;advanceCourseEnrollment&lt;/code&gt; also checks that the attempt matches the active enrollment, pinned course version, current step, and phase. An ordinary partial result increments the revision while keeping the learner on the same cursor. A stuck self-report can enter an explanation phase only when that step has a configured recovery branch. A skip is different: it can advance the cursor, while its immutable attempt remains labeled &lt;code&gt;skipped&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is a persistence boundary too. &lt;code&gt;commitCourseEnrollment&lt;/code&gt; writes the progress snapshot and immutable attempt in one IndexedDB readwrite transaction; for signed-in accounts it also queues the sync operation in that transaction. Replaying the same attempt id with identical evidence is a no-op. Reusing it with different evidence conflicts.&lt;/p&gt;

&lt;p&gt;The focused course-journey spec includes cases for quiz evidence, partial attempts, and explicit skips. This article describes source and spec behavior; I did not run the test suite today, and these rules do not establish a learner outcome or prove every remote sync scenario. Sources: &lt;code&gt;src/lib/domain/course.ts&lt;/code&gt;, &lt;code&gt;src/lib/domain/course-journey.ts&lt;/code&gt;, &lt;code&gt;src/lib/client/storage/index.ts&lt;/code&gt;, and &lt;code&gt;src/lib/domain/course-journey.spec.ts&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Paginate an Append-Only Sync Feed with a Fixed Snapshot Boundary</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Sun, 01 Nov 2026 02:23:56 +0000</pubDate>
      <link>https://dev.to/ptrgiang/paginate-an-append-only-sync-feed-with-a-fixed-snapshot-boundary-1agh</link>
      <guid>https://dev.to/ptrgiang/paginate-an-append-only-sync-feed-with-a-fixed-snapshot-boundary-1agh</guid>
      <description>&lt;p&gt;I build Pianory, a browser-based piano practice app. AI helped draft this article; I checked the claims against the course-ledger client, server handler and focused tests.&lt;/p&gt;

&lt;p&gt;A course-attempt ledger can grow while a client is fetching more than one page. If every page treats the latest row as its endpoint, a new attempt can enter halfway through the pull. For this append-only feed, we keep two sequence values with different jobs: &lt;code&gt;after&lt;/code&gt; marks progress, while &lt;code&gt;through&lt;/code&gt; fixes the upper bound for one traversal.&lt;/p&gt;

&lt;p&gt;The server selects rows with &lt;code&gt;sequence &amp;gt; after&lt;/code&gt; and &lt;code&gt;sequence &amp;lt;= through&lt;/code&gt;. It fetches one row beyond the requested page size to decide &lt;code&gt;hasMore&lt;/code&gt;, then returns an advancing cursor. The client carries the same &lt;code&gt;through&lt;/code&gt; value on later pages and rejects a changed upper bound or a page that does not advance.&lt;/p&gt;

&lt;p&gt;The tests make the boundary concrete. In the client test, a sequence 104 is appended while the current traversal covers 1–103; the current pull ends at 103. In the server API test, five attempts added after the initial bound are excluded from that traversal, while a fresh traversal can see the later bound. &lt;code&gt;pnpm exec vitest run src/lib/client/course-ledger.spec.ts src/lib/server/api/course-ledger.spec.ts&lt;/code&gt; passed 15 tests in two files on 1 November 2026.&lt;/p&gt;

&lt;p&gt;This is a bounded design choice for Pianory's append-only course-attempt ledger, not a universal database snapshot guarantee. The focused client transport and server test harness verify these cases; they do not measure production sync latency or every deployment's concurrency behavior. Source: &lt;code&gt;src/lib/client/course-ledger.ts&lt;/code&gt;, &lt;code&gt;src/lib/server/api/course-ledger.ts&lt;/code&gt;, and their focused specs.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Keeping Browser Recordings Compatible in IndexedDB</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Sat, 31 Oct 2026 02:12:21 +0000</pubDate>
      <link>https://dev.to/ptrgiang/keeping-browser-recordings-compatible-in-indexeddb-1jac</link>
      <guid>https://dev.to/ptrgiang/keeping-browser-recordings-compatible-in-indexeddb-1jac</guid>
      <description>&lt;p&gt;I build Pianory, a browser-based piano practice app. AI helped draft this article; I checked the claims against the recording-store source and its focused tests.&lt;/p&gt;

&lt;p&gt;A browser recording can have metadata and audio bytes that need to move together. Our IndexedDB store saves the audio as an ArrayBuffer and reconstructs a Blob when reading it. The code also accepts legacy rows whose audio was stored as a Blob, so an older local recording remains readable through that path.&lt;/p&gt;

&lt;p&gt;The save path obtains the bytes before opening the transaction. Then it puts metadata and audio in one readwrite transaction. On quota failure, the transaction aborts instead of leaving a metadata row that points to missing audio. This is a practical boundary: asynchronous byte conversion should not stretch the lifetime of an IndexedDB transaction.&lt;/p&gt;

&lt;p&gt;The tests cover save, read and export behavior, plus a quota failure that leaves no metadata row. &lt;code&gt;pnpm exec vitest run src/lib/recordings/store.spec.ts src/lib/input/detector.spec.ts&lt;/code&gt; passed 11 tests in two files on 31 October 2026. These mocked-storage tests support the round-trip and rollback claims; they do not independently reproduce a Safari failure or prove behavior in every browser.&lt;/p&gt;

&lt;p&gt;The product lesson is to define both the current storage format and the old rows you still need to read. Source: &lt;code&gt;src/lib/recordings/store.ts&lt;/code&gt; and &lt;code&gt;src/lib/recordings/store.spec.ts&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>indexeddb</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Counting Only Active Practice Time in a Browser</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Fri, 30 Oct 2026 02:18:49 +0000</pubDate>
      <link>https://dev.to/ptrgiang/counting-only-active-practice-time-in-a-browser-35fd</link>
      <guid>https://dev.to/ptrgiang/counting-only-active-practice-time-in-a-browser-35fd</guid>
      <description>&lt;p&gt;I build Pianory, a browser-based piano practice app. AI helped draft this article; I checked the behavior against the timer and timed-run source and its focused tests.&lt;/p&gt;

&lt;p&gt;A practice timer should count time spent in the active session, not the whole interval since a tab was opened. Pianory's foreground timer reads &lt;code&gt;performance.now()&lt;/code&gt; to compute elapsed milliseconds. A 250 ms interval asks for updates, but those ticks are not added as 250 ms chunks; the elapsed duration comes from the clock. The callback reports only when the whole-second count changes.&lt;/p&gt;

&lt;p&gt;The page lifecycle matters. When the document becomes hidden or the page is leaving, the timed-practice component pauses the controller. A saved run with status &lt;code&gt;running&lt;/code&gt; is restored as &lt;code&gt;paused&lt;/code&gt; after reload. That avoids counting the time the browser was closed or in the background as active practice. One focused test runs for three seconds, hides the page for 60 seconds and still records only three seconds before resuming.&lt;/p&gt;

&lt;p&gt;Each measured delta is applied to the current unfinished activity. The domain function rejects invalid deltas, caps elapsed time at the session budget and marks a run expired at that limit. A five-minute test reaches 300 seconds while the current activity remains unfinished; duration does not pretend to grade the playing or complete a learning goal.&lt;/p&gt;

&lt;p&gt;Local storage can also change in another tab. Before writing, the controller compares the current stored string with the snapshot it loaded. If the value changed, it stops rather than silently overwriting that other state. This is conflict detection, not an atomic cross-tab lock. Tests cover storage failures and recovery as well.&lt;/p&gt;

&lt;p&gt;The design lesson is to define what a timer measures and what it does not. The focused suite &lt;code&gt;pnpm exec vitest run src/lib/components/practice/timed-run.spec.ts src/lib/components/stats/session-groups.spec.ts&lt;/code&gt; passed 10 tests on 30 October 2026. It verifies these units, not timing precision in every browser or a measured improvement in practice outcomes. Relevant source: &lt;code&gt;src/lib/components/practice/timing.ts&lt;/code&gt;, &lt;code&gt;timed-run.ts&lt;/code&gt;, &lt;code&gt;TimedPracticeSession.svelte&lt;/code&gt;, and &lt;code&gt;src/lib/domain/timed-session-run.ts&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
      <category>webdev</category>
      <category>music</category>
    </item>
    <item>
      <title>Exact Beat Fractions for Pickup and Closing Bars</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Thu, 29 Oct 2026 02:23:09 +0000</pubDate>
      <link>https://dev.to/ptrgiang/exact-beat-fractions-for-pickup-and-closing-bars-4b9o</link>
      <guid>https://dev.to/ptrgiang/exact-beat-fractions-for-pickup-and-closing-bars-4b9o</guid>
      <description>&lt;p&gt;I build Pianory, a browser-based piano learning app. AI helped draft this article; I checked the claims against &lt;code&gt;src/lib/domain/course-timeline.ts&lt;/code&gt; and its focused test suite.&lt;/p&gt;

&lt;p&gt;A score import needs a stable representation of musical time. In Pianory's course timeline domain contract, beat positions and durations are rational quarter-note values, rather than floating-point estimates. The fraction helper requires a reduced numerator and denominator, so equivalent values have one canonical form.&lt;/p&gt;

&lt;p&gt;Comparison uses &lt;code&gt;BigInt&lt;/code&gt; cross-products. That matters when numerator times denominator exceeds JavaScript's safe-integer range: rounding could make two distinct beat positions appear equal or reverse their order. The focused tests include such a large-integer comparison and reject noncanonical fractions.&lt;/p&gt;

&lt;p&gt;Pickup measures create a second edge case. An opening partial measure does not mean every later bar may be partial. The validator accepts a final partial bar when it exactly complements the opening pickup in the same meter. It rejects duration combinations that do not satisfy that relationship. That is a domain rule for a score timeline, not a claim that every imported MIDI file has perfect notation.&lt;/p&gt;

&lt;p&gt;The design lesson is to make the time unit and equality rule explicit before wiring import or playback adapters. A versioned JSON score contract with canonical fractions gives tests a precise target. &lt;code&gt;pnpm exec vitest run src/lib/domain/course-timeline.spec.ts&lt;/code&gt; passed 8 tests on 29 October 2026. This focused check verifies the domain model and tests only; I did not trace every production import or playback path for this article.&lt;/p&gt;

&lt;p&gt;Relevant source: &lt;code&gt;src/lib/domain/course-timeline.ts&lt;/code&gt; and &lt;code&gt;src/lib/domain/course-timeline.spec.ts&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
      <category>music</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A Recovery Branch That Keeps the Learner on the Same Step</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Wed, 28 Oct 2026 02:15:52 +0000</pubDate>
      <link>https://dev.to/ptrgiang/a-recovery-branch-that-keeps-the-learner-on-the-same-step-41ma</link>
      <guid>https://dev.to/ptrgiang/a-recovery-branch-that-keeps-the-learner-on-the-same-step-41ma</guid>
      <description>&lt;p&gt;I build Pianory, a browser-based piano learning app. AI helped draft this article; I checked the behavior against the current implementation and its focused tests.&lt;/p&gt;

&lt;p&gt;A course step can be hard without being finished or abandoned. Our course journey models that case with a recovery branch attached to the original step. The important invariant is that help changes the active activity, while the learner's step identity remains stable.&lt;/p&gt;

&lt;p&gt;The branch is deliberately narrow. A partial attempt with a self-report rating of &lt;code&gt;stuck&lt;/code&gt; can enter recovery only when the authored step has a recovery path and the cursor is in the original or retest phase. A failed quiz alone does not trigger it. When the condition matches, the enrollment cursor keeps its &lt;code&gt;stepId&lt;/code&gt; and preset, adds the recovery &lt;code&gt;branchId&lt;/code&gt;, and changes phase to &lt;code&gt;explanation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;courseActiveStep&lt;/code&gt; resolves the explanation phase to an authored lesson and the drill phase to an authored drill. Both are temporary activities under the original step. Passing the explanation moves to the drill; passing the drill moves to a retest of the original task. A failed recovery quiz keeps the learner in its current phase. The original completion policy, rather than the helper quiz, decides whether the course step is complete.&lt;/p&gt;

&lt;p&gt;This is a useful state-machine boundary: the learner can ask for help and return to the same passage rather than losing their place or silently advancing. In &lt;code&gt;course-journey.spec.ts&lt;/code&gt;, the recovery case checks preservation of step and preset, a failed explanation quiz, and the return to the original passage. The focused suite passed 11 tests on 28 October 2026. A separate browser test describes recovery surviving a reload; I did not rerun that end-to-end test for this article.&lt;/p&gt;

&lt;p&gt;The limitation is as important as the path. This behavior applies to steps with an authored recovery path and an explicit &lt;code&gt;stuck&lt;/code&gt; self-report. It is not a general promise that every failed attempt automatically selects a lesson.&lt;/p&gt;

&lt;p&gt;Implementation: &lt;code&gt;src/lib/domain/course-journey.ts&lt;/code&gt; (&lt;code&gt;courseActiveStep&lt;/code&gt; and attempt transition), with the focused test in &lt;code&gt;src/lib/domain/course-journey.spec.ts&lt;/code&gt;. The design lesson for me is to make help a reversible route through the original task, with the transition conditions visible in tests.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>When a Sustained MIDI Note Meets the Next Onset</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Tue, 27 Oct 2026 02:07:51 +0000</pubDate>
      <link>https://dev.to/ptrgiang/when-a-sustained-midi-note-meets-the-next-onset-ink</link>
      <guid>https://dev.to/ptrgiang/when-a-sustained-midi-note-meets-the-next-onset-ink</guid>
      <description>&lt;p&gt;A MIDI file can contain note events that overlap in time. A readable score needs a voice model, and that model makes choices about which overlaps it can represent. Pianory's MIDI-to-MusicXML converter currently builds one notation voice per hand. This article describes that constraint and a test that makes it visible.&lt;/p&gt;

&lt;p&gt;Within a hand, notes sharing the same start are grouped into one chord. The converter sorts those starts, inserts a rest when the next start is later than the current cursor, and limits each group's written length to the next onset in that hand. Even when an input note sustains longer, the generated one-voice score can end it at that next onset. This keeps the measures filled consistently, but it does not preserve independently overlapping voices within the same hand.&lt;/p&gt;

&lt;p&gt;The focused fixture is concrete. Three right-hand pitches, MIDI 60, 64 and 67, begin together at quarter 0 and last two quarters; the test expects C4, E4 and G4 in a chord. In the left hand, MIDI 48 begins at quarter 0 for four quarters and MIDI 55 begins at quarter 2 for two. The test states that the first left-hand whole note is cut where the next left-hand note starts, expects C3 then G3, and checks the measure sums.&lt;/p&gt;

&lt;p&gt;This is an explicit notation tradeoff, not a statement that MIDI always means legato or that the output captures every overlapping line. If the product needs independent same-hand voices, the representation must change rather than hiding the overlap. I make Pianory. AI helped draft this article; I checked the claims against src/lib/music/midi.ts and src/lib/music/midi.spec.ts. On 27 October, pnpm test:unit -- --run src/lib/music/midi.spec.ts passed all 16 focused tests.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
    </item>
    <item>
      <title>When a MIDI Note Ends One Sixteenth Early</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Mon, 26 Oct 2026 02:10:53 +0000</pubDate>
      <link>https://dev.to/ptrgiang/when-a-midi-note-ends-one-sixteenth-early-4mdd</link>
      <guid>https://dev.to/ptrgiang/when-a-midi-note-ends-one-sixteenth-early-4mdd</guid>
      <description>&lt;p&gt;MIDI gives us timed note events, not a finished score. When Pianory imports a MIDI file into its score player, it has to choose a readable written duration. A tiny release gap near the next note can be meaningful, or it can make notation look more staccato than the phrase suggests.&lt;/p&gt;

&lt;p&gt;Our converter snaps starts and durations to a sixteenth-note grid: four grid units equal one quarter note. It builds one voice per hand, groups simultaneous starts into a chord, and caps that chord at the hand's next onset. For a held note released just before that onset, it fills a gap only when the gap is at most one grid unit and no more than a quarter of the note length. Otherwise it leaves the gap.&lt;/p&gt;

&lt;p&gt;The focused test is concrete. A C5 note starts at quarter 0 and lasts 3.75 quarters; the next D5 starts at quarter 4. The first note spans 15 sixteenth units, followed by a one-unit gap. In a 4/4 bar of 16 units, that gap passes the rule, so the MusicXML contains a whole-note type and no ordinary rest. The test asserts that output.&lt;/p&gt;

&lt;p&gt;This is a notation heuristic, not a claim that every MIDI performance is represented exactly. Triplets and swing are approximate on this grid, and a larger gap should remain a rest. The useful design choice is to make the threshold explicit and test a boundary case instead of letting a seemingly harmless release create an unwanted sliver of silence on the page. I make Pianory, the piano-learning app behind this converter. AI helped draft this article; I checked the claims against src/lib/music/midi.ts and the focused test. On 26 October, pnpm exec vitest run src/lib/music/midi.spec.ts passed all 16 tests.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
    </item>
    <item>
      <title>A Loop Boundary Is an Audio Boundary Too</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Sun, 25 Oct 2026 02:03:25 +0000</pubDate>
      <link>https://dev.to/ptrgiang/a-loop-boundary-is-an-audio-boundary-too-21n5</link>
      <guid>https://dev.to/ptrgiang/a-loop-boundary-is-an-audio-boundary-too-21n5</guid>
      <description>&lt;p&gt;A score can contain a note that lasts beyond the passage someone selected for practice. If a player schedules the full note, the sound can spill past the loop or a finite stop even when the UI says the passage ended.&lt;/p&gt;

&lt;p&gt;Pianory resolves an inclusive measure selection into a score-time window and an exclusive event range. When scheduling an event, the player caps the note's end quarter at that window's end and limits the audio duration to the time remaining before the passage boundary. Its metronome scheduling uses the same endpoint, and a finite repetition ends there. The piano adapter receives that shortened duration when it starts the sample.&lt;/p&gt;

&lt;p&gt;A focused mock-clock test puts a note authored for eight quarters inside a four-quarter finite passage. It checks that the scheduled note ends before the passage endpoint and that the final metronome click does not run beyond it. Tests also cover finite one-pass and two-pass endings. The focused player suite passed locally: 20 tests.&lt;/p&gt;

&lt;p&gt;The useful contract is that a practice range controls the audio schedule, not just the playhead display. This test verifies planned durations with a mocked audio adapter and clock; it does not prove acoustic output, sample loading, timing on every device, or browser performance. I make Pianory, the piano-learning app behind this example. AI helped draft this article, and I checked the claims against the source and focused tests.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webaudio</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why a score's printed beat and playback BPM need separate units</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Sat, 24 Oct 2026 02:10:30 +0000</pubDate>
      <link>https://dev.to/ptrgiang/why-a-scores-printed-beat-and-playback-bpm-need-separate-units-23jh</link>
      <guid>https://dev.to/ptrgiang/why-a-scores-printed-beat-and-playback-bpm-need-separate-units-23jh</guid>
      <description>&lt;p&gt;A printed tempo mark can say eighth note = 120, while a playback timeline measures speed in quarter notes per minute. Treating both numbers as the same BPM would double the intended playback rate. Pianory keeps the score's beat unit for display and converts to a canonical quarter-note rate for timeline math.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;tempo.ts&lt;/code&gt;, a &lt;code&gt;TempoMark&lt;/code&gt; stores the printed unit, whether it is dotted, the printed per-minute value, and &lt;code&gt;quarterBpm&lt;/code&gt;. &lt;code&gt;beatQuarters&lt;/code&gt; maps half, quarter and eighth notes to 2, 1 and 0.5 quarter notes respectively, then multiplies by 1.5 for one dot. So an eighth-note mark at 120 beats per minute becomes 60 quarter notes per minute. The label function reverses that conversion for the player UI, leaving the score's printed &lt;code&gt;♪ = 120&lt;/code&gt; intact.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;readTempoMark&lt;/code&gt; deliberately accepts only the first simple MusicXML &lt;code&gt;&amp;lt;metronome&amp;gt;&lt;/code&gt; mark. Unsupported beat units, more than one dot, note-to-note relations and non-positive rates return no mark. The OSMD iterator supplies tempo points; &lt;code&gt;buildTimeline&lt;/code&gt; scales those points against the parsed mark's quarter-note rate and retains the printed beat for display. A focused test checks that eighth note = 120 produces 60 quarter BPM at the start and converts a later point of 60 to 30 quarter BPM. The UI also uses the preserved beat when displaying the tempo slider's accessible value.&lt;/p&gt;

&lt;p&gt;This design keeps arithmetic in one unit while showing musicians the beat the score actually printed. The focused conversion and timeline tests passed locally, but they do not measure playback accuracy in a real browser or establish how every MusicXML file behaves. I make Pianory; this example comes from its score player. AI assisted with drafting this article, and I checked the claims against the source and focused tests.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
      <category>architecture</category>
      <category>webaudio</category>
    </item>
    <item>
      <title>Make the first piano note ready without loading every sample</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Fri, 23 Oct 2026 02:15:24 +0000</pubDate>
      <link>https://dev.to/ptrgiang/make-the-first-piano-note-ready-without-loading-every-sample-4job</link>
      <guid>https://dev.to/ptrgiang/make-the-first-piano-note-ready-without-loading-every-sample-4job</guid>
      <description>&lt;p&gt;A sampled piano can have many recordings for different pitches and playing strengths. Loading them all before the first note would make preparation larger than many beginner scores need. In Pianory, the player builds a preset for the requested pitches, then makes a single velocity layer playable first.&lt;/p&gt;

&lt;p&gt;The fast layer covers velocity 68–84. &lt;code&gt;prepare(notes)&lt;/code&gt; adds missing requested pitches to that layer and resolves when it is loaded. On desktop, the remaining velocity layers for those pitches load in the background. Phones and tablets stay with the fast layer, because decoding all five layers is deliberately avoided there. Until the full layers are ready, playback clamps velocity into the fast range; a note that has not been prepared is queued for a background request.&lt;/p&gt;

&lt;p&gt;Sample selection happens before fetching. &lt;code&gt;presetFor(full, fast)&lt;/code&gt; chooses regions that cover the requested pitches. A focused test with seven pitches selects less than one quarter of the files in the full preset. Another test establishes the fixture's scale: 226 distinct files for all 88 keys versus 62 for its fast layer. Those are unit-test counts, not measurements of network bytes, decoded memory, startup time or perceived audio quality.&lt;/p&gt;

&lt;p&gt;The storage wrapper limits concurrent requests and retries network failures, HTTP 429 and server errors with backoff. Cache tests require a successful exact same-origin sample URL before counting it as cached. These checks protect the selection and storage rules, while an in-browser performance test would still be needed to claim a startup speed improvement.&lt;/p&gt;

&lt;p&gt;The implementation lesson is to define the first playable subset explicitly, load it for the score's notes, then improve fidelity as additional layers become available. I make Pianory, the piano-learning app used in this example. AI assisted with drafting this article; I checked the claims against the repository and focused tests.&lt;/p&gt;

</description>
      <category>webaudio</category>
      <category>typescript</category>
      <category>testing</category>
      <category>performance</category>
    </item>
    <item>
      <title>A loop tempo ramp belongs at the repeat boundary</title>
      <dc:creator>Giang Pham Truong</dc:creator>
      <pubDate>Thu, 22 Oct 2026 02:11:37 +0000</pubDate>
      <link>https://dev.to/ptrgiang/a-loop-tempo-ramp-belongs-at-the-repeat-boundary-1ici</link>
      <guid>https://dev.to/ptrgiang/a-loop-tempo-ramp-belongs-at-the-repeat-boundary-1ici</guid>
      <description>&lt;p&gt;A practice loop can start below a score's written tempo and rise a little on each pass. The useful boundary is the end of the selected passage: changing speed while notes from that pass are still being scheduled makes the transition harder to reason about.&lt;/p&gt;

&lt;p&gt;In Pianory's score player, the sheet UI supplies a five-percentage-point tempo step. The player reaches the end of the selected passage, checks whether it will repeat, then raises the percentage and caps the ramp at 100% of the written tempo. It reports the new percentage and anchors the next pass at the loop's start. A 90% start with a five-point step therefore reports 95%, then 100%, and stops rising.&lt;/p&gt;

&lt;p&gt;The passage range resolves to quarter-note positions with an exclusive end event. Notes that cross the passage end are clipped to that boundary before the next pass is scheduled. Those rules let the repeat restart at a defined score position instead of inheriting a note tail past the loop.&lt;/p&gt;

&lt;p&gt;One important distinction: 100% is the ramp's destination, not the tempo slider's maximum. The general tempo control permits 25%–125%. The optional ramp is a practice behavior layered on top of that control. It advances at a repeat boundary; it does not measure whether the player performed the passage cleanly.&lt;/p&gt;

&lt;p&gt;A focused unit test starts a one-measure loop at 90% with a five-point step and checks callbacks [95, 100] plus the final setting. Timeline tests cover the exclusive range. Those checks do not measure audible note-onset spacing after a tempo change, so I would add an audio-scheduling regression test before claiming that every ramped pass sounds exact in a browser.&lt;/p&gt;

&lt;p&gt;This is a narrow implementation lesson: make the repeat boundary explicit, update the tempo once there, and keep the next pass anchored to the selected score range.&lt;/p&gt;

&lt;p&gt;AI assisted with drafting this article; I checked its claims against the repository and focused tests. I make Pianory, the piano-learning app used in this example.&lt;/p&gt;

</description>
      <category>webaudio</category>
      <category>typescript</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
