Traversing and mutating the model
Applying user-configured transposition, batch-tinting notes by scale degree for a music-theory viewer, algorithmically boosting velocities on a target passage โ the moment a score is loaded, its full tree is walkable and mutable in JavaScript. Iterate Score โ Track โ Staff โ Bar โ Voice โ Beat โ Note, change fields, call api.render(), and the render reflects the changes.
Each button walks the six-level tree, mutates one field, and re-renders.
Three shapes cover most use cases:
- Analysis โ walk the tree read-only, collect statistics (note counts, effect usage, pitch range).
- Annotation โ walk the tree and set display-only fields (colors, styles, chord names) without changing the music.
- Batch transformation โ walk the tree and rewrite musical content (transpose every note, boost velocities across a passage, uppercase lyrics for a karaoke view).
The traversal patternโ
Every mutation follows the same structure: wait for scoreLoaded, walk from Score down to the level you care about, apply changes, call api.render() to redraw.
import * as alphaTab from '@coderline/alphatab';
const container = getAlphaTabContainer();
const api = new alphaTab.AlphaTabApi(container, {
core: { file: '/scores/song.gp' },
});
api.scoreLoaded.on((score) => {
// Transpose every note down one octave.
for (const track of score.tracks) {
for (const staff of track.staves) {
for (const bar of staff.bars) {
for (const voice of bar.voices) {
for (const beat of voice.beats) {
for (const note of beat.notes) {
if (!Number.isNaN(note.octave)) {
note.octave -= 1;
} else if (!Number.isNaN(note.fret)) {
note.fret = Math.max(0, note.fret - 12);
}
}
}
}
}
}
}
api.render();
});
getAlphaTabContainer() is a placeholder for the platform-specific step of obtaining the render target โ a <div> on Web, an AlphaTab control on .NET, or an AlphaTabView on Android.
When to fireโ
scoreLoadedโ fires once per load. Right for changes that apply to the whole score after import. Follow the mutation withapi.render().- Before
renderScoreโ for hand-built scores, mutate the tree before you callapi.renderScore(score)and skip the event. - After
renderScore/renderโ safe as long as you re-render afterward. Mutations to a rendered score do not appear until the next render.
Traversal shortcutsโ
Two shortcuts collapse the six-level loop:
score.tracks[i].staves[j].bars[k]is index-addressable at every level. Reach for a specific bar directly when you know its position.- The
beat.previousBeat/beat.nextBeatchain walks beats across voice, bar, and staff boundaries after the score has been finished. Use it for anything that needs to look at the beat that follows or precedes across bar lines.
For read-only analysis, a plain nested for loop is the clearest form and reads the whole score once. Do not build a generator or async iterator โ the model is fully materialized in memory.
What triggers a re-renderโ
Changes to display-only fields (style.colors, chord names, Beat.text) require api.render() to appear. Changes to musical content (note pitch, duration, added or removed beats) require api.render() and โ for scores that will be played โ a follow-up api.loadMidiForScore() so the synth picks up the new pitches. Structural changes (adding bars, changing time signature) require finishing the score again with score.finish(api.settings) before the render.
Re-render only what changedโ
Available since 1.8.3. A plain api.render() lays out every bar again. When an edit touches one bar, as in a live editor or a recording app, pass the index of the first changed master bar as firstChangedMasterBar in the RenderHints. alphaTab keeps the systems before that bar and lays out only the system containing it and everything after. api.render(renderHints), api.renderScore(score, trackIndexes, renderHints) and api.renderTracks(tracks, renderHints) all accept the hints.
import * as alphaTab from '@coderline/alphatab';
const container = getAlphaTabContainer();
const api = new alphaTab.AlphaTabApi(container);
api.tex(':4 5.3 7.3 5.3 7.3 | 5.3 7.3 5.3 7.3 | 5.3 7.3 5.3 7.3 | 5.3 7.3 5.3 7.3');
// Move every note of the third bar to fret 9 and redraw from that bar on.
function editThirdBar() {
const barIndex = 2;
const bar = api.score.tracks[0].staves[0].bars[barIndex];
for (const voice of bar.voices) {
for (const beat of voice.beats) {
for (const note of beat.notes) {
note.fret = 9;
}
}
}
bar.finish(api.settings);
api.render({ firstChangedMasterBar: barIndex, reuseViewport: true });
api.loadMidiForScore();
}
document.getElementById('edit-bar').addEventListener('click', editThirdBar);
- The hint affects rendering only. Call
api.loadMidiForScore()when the change must be audible. reuseViewport: trueasks alphaTab to keep the drawn notation on screen instead of clearing it first, which avoids flicker. alphaTab can still decide to clear.- The incremental path applies to the page and parchment layouts while
core.enableLazyLoadingis on (the default). The horizontal layout renders fully. - An index outside the rendered systems falls back to a full render. When you append bars, pass the index of the last bar that was already rendered.
- Leave
firstChangedMasterBarundefined for a full render, for example after loading a different score.
Where to look nextโ
- Per-element colors โ the annotation use case: walk the tree and paint sub-elements.
- Programmatic score construction โ the mirror workflow: build the tree in code instead of walking one that was loaded.
- Reacting to a loaded score โ the
scoreLoadedevent and adjacent post-load hooks. renderandloadMidiForScoreโ the two calls that push mutations into the visual and audio pipelines.RenderHintsโfirstChangedMasterBarandreuseViewportfor incremental renders.DynamicValueโ the enum used by the velocity-boost example.- The model reference โ every field on every level.