Skip to main content

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 with api.render().
  • Before renderScore โ€” for hand-built scores, mutate the tree before you call api.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.nextBeat chain 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: true asks 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.enableLazyLoading is 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 firstChangedMasterBar undefined for a full render, for example after loading a different score.

Where to look nextโ€‹