<?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: Makoto Tanji</title>
    <description>The latest articles on DEV Community by Makoto Tanji (@tan-z-tan).</description>
    <link>https://dev.to/tan-z-tan</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%2F4078436%2Fa4347f73-e270-4dad-b106-7d9251bd0aa9.png</url>
      <title>DEV Community: Makoto Tanji</title>
      <link>https://dev.to/tan-z-tan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tan-z-tan"/>
    <language>en</language>
    <item>
      <title>How I Added WebMCP to a Collaborative Sheet Music Editor</title>
      <dc:creator>Makoto Tanji</dc:creator>
      <pubDate>Thu, 05 Nov 2026 14:28:58 +0000</pubDate>
      <link>https://dev.to/tan-z-tan/how-i-added-webmcp-to-a-collaborative-sheet-music-editor-1dl</link>
      <guid>https://dev.to/tan-z-tan/how-i-added-webmcp-to-a-collaborative-sheet-music-editor-1dl</guid>
      <description>&lt;p&gt;"Rewrite these two bars, then let me hear them."&lt;/p&gt;

&lt;p&gt;That is a reasonable request to make while working on a score. For a browser agent, though, the visible page is a difficult interface: notes are SVG shapes, several voices can occupy the same beat, and a small change in duration can break the measure.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://scoretail.com/" rel="noopener noreferrer"&gt;ScoreTail&lt;/a&gt;, a browser-based sheet music editor with real-time collaboration. I recently added WebMCP tools so an agent can read and edit the score the musician already has open.&lt;/p&gt;

&lt;p&gt;The interesting work was deciding what an edit means: how to describe music to the agent, validate its changes, and put them into the same document and undo history as a person's edits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh44u0g8b51fcb8023g9t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh44u0g8b51fcb8023g9t.jpg" alt="ScoreTail's public score viewer displaying Canon in D with playback and Copy &amp;amp; Edit controls" width="799" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;ScoreTail's public score viewer, showing Pachelbel's Canon in D and the playback controls.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The page exposes the operations
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/webmachinelearning/webmcp" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; is a proposed browser API for registering page-local tools with descriptions and input schemas. In ScoreTail, those tools call the application's existing score operations. The user's current selection and loaded document are already available in the tab.&lt;/p&gt;

&lt;p&gt;There is no separate model call inside these tool handlers. The user's agent supplies the reasoning; ScoreTail supplies the score representation, validation, editing, and playback.&lt;/p&gt;

&lt;p&gt;I split the tool surface by the page and its editing permissions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Available tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Public score viewer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_score_info&lt;/code&gt;, &lt;code&gt;read_measures&lt;/code&gt;, &lt;code&gt;play&lt;/code&gt;, &lt;code&gt;stop_playback&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor with view access&lt;/td&gt;
&lt;td&gt;The viewer tools, plus &lt;code&gt;get_selection&lt;/code&gt; and &lt;code&gt;select_measures&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor with edit access&lt;/td&gt;
&lt;td&gt;All of the above, plus &lt;code&gt;rewrite_measures&lt;/code&gt;, &lt;code&gt;transpose&lt;/code&gt;, &lt;code&gt;undo&lt;/code&gt;, and &lt;code&gt;redo&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This lets someone explore a public score without signing in. Tools that change notation are only registered when the editor has edit access. Read operations also carry &lt;code&gt;readOnlyHint&lt;/code&gt;; that hint describes behavior, while the application's access controls remain responsible for permissions.&lt;/p&gt;

&lt;p&gt;The smaller surface is useful for the agent too. A public viewer has no reason to advertise a rewrite operation that it cannot perform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the agent a representation it can read back
&lt;/h2&gt;

&lt;p&gt;MusicXML is ScoreTail's interchange format. It is expressive, but it includes details an agent does not need for a small rewrite, including the sequential cursor movements used to encode simultaneous voices.&lt;/p&gt;

&lt;p&gt;For reading and writing notes, I reuse a compact text representation from ScoreTail's built-in AI editing feature. I call it &lt;strong&gt;lanes-seq&lt;/strong&gt;. A line identifies a part, staff, voice, and measure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P1 s1 v1 m2: D5=1/4 E5=1/4 F5=1/2
P2 s1 v1 m2: G2=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line contains a quarter-note D, a quarter-note E, and a half-note F. The second contains a whole-note G in another part. Durations are fractions of a whole note, and pitches are absolute: &lt;code&gt;C4&lt;/code&gt; means middle C. Chords use parentheses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P1 s1 v1 m3: (C5 E5 G5)=1/2 r=1/2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;read_measures&lt;/code&gt; returns this notation with a header describing the score's parts and musical context. The agent can then submit the same format to &lt;code&gt;rewrite_measures&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_measure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lanes_seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"P1 s1 v1 m2: D5=1/4 E5=1/4 F5=1/2"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example assumes a full 4/4 measure and the indicated existing part, staff, and voice. A useful workflow reads the score first rather than guessing them.&lt;/p&gt;

&lt;p&gt;The replacement unit is &lt;strong&gt;one staff in one measure&lt;/strong&gt;. Supplying its lines replaces all of that unit's voices; leaving a unit out keeps it unchanged. This matters when an agent is asked to change a melody while preserving an accompaniment on another staff.&lt;/p&gt;

&lt;p&gt;The tool description includes the format guide generated by the same function used for the built-in AI feature. Maintaining one guide helps keep the two interfaces consistent as the notation format evolves.&lt;/p&gt;

&lt;p&gt;There are also practical bounds: a read call accepts up to 64 measures, and a rewrite accepts up to 32. Measure insertion and deletion require an explicit &lt;code&gt;allow_measure_count_change&lt;/code&gt; argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parse, plan, then apply
&lt;/h2&gt;

&lt;p&gt;A JSON schema can establish that &lt;code&gt;lanes_seq&lt;/code&gt; is a string. Musical structure needs another layer of validation.&lt;/p&gt;

&lt;p&gt;For example, this is valid JSON but an incomplete replacement for a full 4/4 measure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_measure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lanes_seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"P1 s1 v1 m2: D5=1/4"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rewrite handler uses this pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tool arguments
  -&amp;gt; parse lanes-seq
  -&amp;gt; build and validate a rewrite plan
  -&amp;gt; start an undo gesture
  -&amp;gt; apply through existing score operations
  -&amp;gt; select the changed range
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The planning stage checks things such as note lines outside the requested measure or part scope and durations that do not fill the measure correctly. It also handles notation rules such as tie and slur repair.&lt;/p&gt;

&lt;p&gt;If parsing or planning fails, the tool returns an error starting with &lt;code&gt;Nothing was changed&lt;/code&gt;, followed by the validation errors. The agent can correct its input and try again without leaving a partial rewrite behind.&lt;/p&gt;

&lt;p&gt;After a successful edit, the changed range is selected in the editor. That gives the person a visible place to inspect before playing it back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool calls need their own undo boundaries
&lt;/h2&gt;

&lt;p&gt;ScoreTail already groups ordinary edits into undo gestures using input events such as &lt;code&gt;pointerdown&lt;/code&gt; and &lt;code&gt;keydown&lt;/code&gt;. A tool invocation arrives without either event.&lt;/p&gt;

&lt;p&gt;That creates a subtle problem: consecutive agent edits need separate undo entries even when there was no human input between them.&lt;/p&gt;

&lt;p&gt;The writing tools explicitly call &lt;code&gt;beginUndoGesture()&lt;/code&gt; before applying a change. The rewrite then passes through the same operation layer as other edits, as one operation. A successful rewrite can be undone in one step.&lt;/p&gt;

&lt;p&gt;One regression test specifically checks this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rewrite measure 2
save the document snapshot
transpose measure 3
undo once
assert that the document matches the saved snapshot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches accidental merging of two tool calls into one undo entry. Another test checks that a valid rewrite followed by one undo restores the original score.&lt;/p&gt;

&lt;p&gt;In the current implementation, rewrites apply immediately. There is no separate preview-and-approve stage in the WebMCP handler. That makes clear tool descriptions, visible changes, and predictable undo especially valuable. The agent should still obtain whatever approval the user's request requires before making an edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  One shared document
&lt;/h2&gt;

&lt;p&gt;ScoreTail stores the live score in Yjs. WebMCP changes go through the existing operation layer into that same document, so they follow the existing synchronization path to collaborators.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Human UI ----\
              -&amp;gt; score operations -&amp;gt; Yjs document -&amp;gt; rendering / sync
WebMCP -----/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no second AI-owned score that later needs to be merged into the editor.&lt;/p&gt;

&lt;p&gt;There is still a limit to what this guarantees. Shared document convergence does not tell us whether a change is a good arrangement, or whether another musician wanted it. Validation protects structural rules; the musician still judges the musical result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lifecycle and audio details
&lt;/h2&gt;

&lt;p&gt;The bridge feature-detects &lt;code&gt;document.modelContext&lt;/code&gt; and waits for the score data to be ready before dynamically loading the tools. If the API is unavailable, the normal editor continues without registering them.&lt;/p&gt;

&lt;p&gt;Registrations use an &lt;code&gt;AbortSignal&lt;/code&gt;. The bridge aborts it on cleanup, including when its permission-related dependencies change. Playback callbacks use a ref so registered tools can call the latest playback function without re-registering on every render.&lt;/p&gt;

&lt;p&gt;Playback also needs to respect browser audio policies. When the page has never received user activation, the handler returns an error asking for a click. Otherwise an &lt;code&gt;AudioContext.resume()&lt;/code&gt; can remain pending and leave the agent waiting for a result. The tool reports that situation instead of pretending playback started.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is verified, and what remains experimental
&lt;/h2&gt;

&lt;p&gt;The current WebMCP score-tool suite has 15 passing tests. It checks permission-dependent tool sets, scoped reads, successful and rejected rewrites, explicit measure-count changes, undo grouping, transposition, selection, and playback status handling.&lt;/p&gt;

&lt;p&gt;These tests call the handlers with a real in-memory score model; playback is mocked. They establish application behavior, rather than universal compatibility with every browser agent. The public demo also loads and enters playback through its normal UI.&lt;/p&gt;

&lt;p&gt;WebMCP itself is still experimental. &lt;a href="https://developer.chrome.com/docs/ai/webmcp" rel="noopener noreferrer"&gt;Chrome's setup documentation&lt;/a&gt; covers its origin trial and local testing flag. A supporting browser alone also does not guarantee that a particular assistant discovers and calls the tools. I would check the browser and agent combination before planning a demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try a public score
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://scoretail.com/en/sheet-music/canon-in-d-pachelbel-johann/view" rel="noopener noreferrer"&gt;Canon in D in ScoreTail&lt;/a&gt; with a compatible WebMCP setup. The public viewer exposes reading and playback tools without requiring an account.&lt;/p&gt;

&lt;p&gt;A starting request is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read the first eight measures and explain the chord progression, then play them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For editing, copy a score into the full editor with edit access. Read a small range first, make one bounded change, listen, and try undo. WebMCP is currently attached to the full editor and public score viewer; the guest quick-edit workspace does not expose it.&lt;/p&gt;

&lt;p&gt;I'm also working toward a library where public-domain sheet music can be opened, played, corrected, and arranged together, with OMR helping bring scanned scores into that workflow. These tools give an agent a way to assist with a specific passage while the musician stays in the same editor.&lt;/p&gt;

&lt;p&gt;If you are adding agent tools to a creative app, how are you handling changes to a document that another person may be editing at the same time?&lt;/p&gt;

</description>
      <category>webmcp</category>
      <category>javascript</category>
      <category>ai</category>
      <category>music</category>
    </item>
    <item>
      <title>I Built a Real-Time Collaborative Sheet Music Editor — Here's What MusicXML and CRDTs Taught Me</title>
      <dc:creator>Makoto Tanji</dc:creator>
      <pubDate>Sat, 15 Aug 2026 10:14:05 +0000</pubDate>
      <link>https://dev.to/tan-z-tan/i-built-a-real-time-collaborative-sheet-music-editor-heres-what-musicxml-and-crdts-taught-me-1dii</link>
      <guid>https://dev.to/tan-z-tan/i-built-a-real-time-collaborative-sheet-music-editor-heres-what-musicxml-and-crdts-taught-me-1dii</guid>
      <description>&lt;p&gt;I wanted a "Google Docs for sheet music": open a browser tab, start writing notation, share a URL, and have someone else edit the same score with you in real time. Nothing like that existed. MuseScore, Finale, and Sibelius are all excellent, but they're desktop apps built for people who already know notation software inside out.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://scoretail.com" rel="noopener noreferrer"&gt;ScoreTail&lt;/a&gt;, a browser-based score editor, mostly alone, with a lot of AI-assisted development. This post is about the parts that turned out to be genuinely hard: the MusicXML format, real-time collaborative editing over CRDTs, and rendering performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI-driven development bet
&lt;/h2&gt;

&lt;p&gt;MusicXML is a huge spec — pitch, duration, voices, staves, backup/forward, divisions, ties, slurs, ornaments, tuplets, lyrics, chord symbols, dynamics, repeat structures. Implementing all of it solo, the traditional way, is a multi-year project.&lt;/p&gt;

&lt;p&gt;Late last year I made a bet: let AI (Claude Code, ~90% of the code; Antigravity, ~10%) write most of the implementation, while I focused on defining what "correct" means — architecture decisions and test cases — rather than typing every line myself.&lt;/p&gt;

&lt;p&gt;The thing that made this workable wasn't the AI, though, it was the test harness: I collected around 250 real-world scores (Bach, Beethoven, Chopin, Brahms, Debussy, plus the LilyPond regression test corpus) and ran them through an import → export → re-import round-trip. Any structural drift between the first and second import is a bug. AI writes code, the round-trip suite catches regressions, AI fixes them, repeat. Humans stayed in the loop for the judgment calls; the AI did the volume work.&lt;/p&gt;

&lt;h2&gt;
  
  
  MusicXML's backup/forward problem
&lt;/h2&gt;

&lt;p&gt;The single hardest part of MusicXML to get right is &lt;code&gt;&amp;lt;backup&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;forward&amp;gt;&lt;/code&gt;. MusicXML is a flat, sequential XML structure, but a measure isn't sequential — it has multiple simultaneous voices, and a piano part has two staves (treble/bass) sharing one measure. To represent that in a flat stream, the format writes voice A's notes, then emits a &lt;code&gt;&amp;lt;backup&amp;gt;&lt;/code&gt; element to rewind the internal time cursor, then writes voice B's notes starting from the same beat.&lt;/p&gt;

&lt;p&gt;That means any code that reads or writes MusicXML has to maintain its own notion of "where am I on the timeline right now" independent of document order — and get it right across combinations of backup + multi-voice + multi-staff, which is where most real-world parser bugs live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping every edit "musically valid"
&lt;/h2&gt;

&lt;p&gt;Early on I decided every operation had to leave the score in a musically valid state — correctness over flexibility. Concretely: if a 4/4 measure has beat 4 occupied by a quarter note, you can't just change it to a half note, because that would overflow the measure boundary. For every edit, the editor has to decide: overflow into the next measure, auto-insert a tie, or reject the operation outright.&lt;/p&gt;

&lt;p&gt;The harder cases are things like inserting a note when another voice already has something at that beat, notes of different lengths sharing a beat, or replacing a rest with a note that doesn't fill the remaining space cleanly. This constraint exists because the target user isn't a notation expert — someone who doesn't know music theory should still end up with a structurally correct score, not a fragile one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-time collaboration: CRDTs weren't built for this
&lt;/h2&gt;

&lt;p&gt;For multiplayer editing I used &lt;a href="https://github.com/yjs/yjs" rel="noopener noreferrer"&gt;Yjs&lt;/a&gt;, a CRDT (Conflict-free Replicated Data Type) library — no server-side conflict resolution, edits from multiple clients converge automatically, the same approach Google Docs and Figma use.&lt;/p&gt;

&lt;p&gt;Text CRDTs only have to reason about character insertion and deletion in a 1D sequence. A score has two simultaneous temporal dimensions per note — its position (which beat) and its duration (how many beats it occupies) — and multiple users can be editing different voices in the same measure concurrently, or one user can be inserting a note while another is changing the measure's time signature or key signature mid-edit.&lt;/p&gt;

&lt;p&gt;My approach: map the MusicXML tree onto Yjs's &lt;code&gt;Y.Map&lt;/code&gt;/&lt;code&gt;Y.Array&lt;/code&gt; types 1:1. Yjs guarantees &lt;em&gt;structural&lt;/em&gt; convergence — every client ends up with the same tree — but it has no idea what a musically valid tree looks like, so structural convergence and musical validity are two separate problems. The safety net is an auto-undo: if an export ever produces an invalid document, the last operation is automatically rolled back rather than left in a broken state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering with Verovio, and the ID-mapping problem
&lt;/h2&gt;

&lt;p&gt;Rendering runs through &lt;a href="https://www.verovio.org" rel="noopener noreferrer"&gt;Verovio&lt;/a&gt;, an open-source C++ engraving engine compiled to WebAssembly. Two problems showed up once scores got non-trivial in size:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance.&lt;/strong&gt; Full-score re-rendering on every edit blocks the main thread for large scores. Fix: move rendering into a Web Worker, and stream rendering page-by-page, prioritizing whichever page the user is currently looking at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity.&lt;/strong&gt; Verovio has its own internal IDs for rendered SVG elements; Yjs has its own IDs for the underlying data. When a user clicks a note on screen, you need to resolve an SVG element back to the exact Yjs entry it came from. I ended up computing a fingerprint per note — part index, measure number, beat position, voice, pitch — and rebuilding the mapping table on each render pass. Building a notation renderer from scratch was on the table at one point; I shelved it as a multi-year project of its own and leaned on Verovio instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Click coordinates → beat position + pitch
&lt;/h2&gt;

&lt;p&gt;Turning a click into "beat X, pitch Y" is deceptively harder than the equivalent problem in a text editor. A whole note is visually wide, a sixteenth note is narrow, dotted notes are wider still, and note spacing varies measure-to-measure based on how packed the measure is — so the mapping has to be derived from Verovio's actual final layout, not from a fixed grid.&lt;/p&gt;

&lt;p&gt;For feedback, the editor shows a translucent "ghost" preview of the pending note before you commit the click: purple for a new note, blue for adding to a chord, green for replacing a rest, gray when the position conflicts with something already there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I chose not to build
&lt;/h2&gt;

&lt;p&gt;Notation software has effectively unlimited surface area — tablature, drum notation, transposing instruments, automatic part extraction, fine-grained layout control, and on. As a (mostly) solo project, the operative question stayed "what helps the most people right now," and a lot got deliberately cut or deferred. Current honest state: a 5–10 minute piano score performs well; large orchestral scores are still rough.&lt;/p&gt;

&lt;p&gt;MusicXML's other trap is that the spec and what software actually emits diverge — MuseScore and Dorico export structurally different XML for the same piece. There's no shortcut around this beyond feeding the parser a large, varied corpus of real files and absorbing the "dialects" one by one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://scoretail.com" rel="noopener noreferrer"&gt;ScoreTail&lt;/a&gt; is free, browser-based, and works without an account for a quick edit. If you've dealt with MusicXML, CRDT-based collaborative editing, or WASM rendering engines, I'd be curious to hear how you approached the same problems.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>music</category>
    </item>
  </channel>
</rss>
