diff --git a/packages/alphatab/src/AlphaTabApiBase.ts b/packages/alphatab/src/AlphaTabApiBase.ts index 6efea4815..c53aafe8b 100644 --- a/packages/alphatab/src/AlphaTabApiBase.ts +++ b/packages/alphatab/src/AlphaTabApiBase.ts @@ -2434,7 +2434,8 @@ export class AlphaTabApiBase { this._isInitialBeatCursorUpdate || barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y || startBeatX < previousBeatBounds.onNotesX || - barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1; + barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1 || + barBounds.h !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.h; if (jumpCursor) { cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX); diff --git a/packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts b/packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts index 62038c4df..0702dfecb 100644 --- a/packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts +++ b/packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts @@ -82,9 +82,10 @@ export class AlphaTabWebWorker { break; case 'alphaTab.renderScore': this._updateFontSizes(data.fontSizes); + const renderHints:RenderHints = data.renderHints; const score: any = data.score == null ? null : JsonConverter.jsObjectToScore(data.score, this._renderer.settings); - this._renderMultiple(score, data.trackIndexes); + this._renderMultiple(score, data.trackIndexes, renderHints); break; case 'alphaTab.updateSettings': this._updateSettings(data.settings); diff --git a/packages/alphatab/src/rendering/IScoreRenderer.ts b/packages/alphatab/src/rendering/IScoreRenderer.ts index 0a071a9c7..da4daa400 100644 --- a/packages/alphatab/src/rendering/IScoreRenderer.ts +++ b/packages/alphatab/src/rendering/IScoreRenderer.ts @@ -19,6 +19,14 @@ export interface RenderHints { * internally it might still be decided to clear the viewport. */ reuseViewport?: boolean; + + /** + * Indicates the index of the first masterbar which was modified in the data model. + * @remarks + * AlphaTab will try to optimize the rendering and other updates to keep unchanged parts. + * At this point only the rendering is affected and the generated MIDI has to be updated separately. + */ + firstChangedMasterBar?: number; } /** diff --git a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts index 5962fab06..836110a99 100644 --- a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts +++ b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts @@ -44,6 +44,14 @@ export class HorizontalScreenLayout extends ScoreLayout { public doResize(): void { // not supported } + + public override doUpdateForBars(_renderHints: RenderHints): boolean { + // not supported yet, modifications likely cause anyhow full updates + // as we do not optimize effect bands yet. with effect bands being more + // isolated in bars we could try updating dynamically + return false; + } + protected doLayoutAndRender(renderHints: RenderHints | undefined): void { const score: Score = this.renderer.score!; @@ -150,7 +158,7 @@ export class HorizontalScreenLayout extends ScoreLayout { } this.height = this.layoutAndRenderBottomScoreInfo(this.height); - this.height = this.layoutAndRenderAnnotation(this.height); + this.height = this._layoutAndRenderAnnotation(this.height); this.height += this.pagePadding![3]; diff --git a/packages/alphatab/src/rendering/layout/ScoreLayout.ts b/packages/alphatab/src/rendering/layout/ScoreLayout.ts index 48b41ac17..cc6585301 100644 --- a/packages/alphatab/src/rendering/layout/ScoreLayout.ts +++ b/packages/alphatab/src/rendering/layout/ScoreLayout.ts @@ -29,14 +29,11 @@ import { Lazy } from '@coderline/alphatab/util/Lazy'; /** * @internal + * @record */ -class LazyPartial { - public args: RenderFinishedEventArgs; - public renderCallback: (canvas: ICanvas) => void; - public constructor(args: RenderFinishedEventArgs, renderCallback: (canvas: ICanvas) => void) { - this.args = args; - this.renderCallback = renderCallback; - } +interface LazyPartial { + args: RenderFinishedEventArgs; + renderCallback: (canvas: ICanvas) => void; } /** @@ -84,13 +81,10 @@ export abstract class ScoreLayout { } public abstract doResize(): void; + public abstract doUpdateForBars(renderHints: RenderHints): boolean; + public layoutAndRender(renderHints?: RenderHints): void { - this._lazyPartials.clear(); this.slurRegistry.clear(); - this.beamingRuleLookups.clear(); - this._barRendererLookup.clear(); - - this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile)!; const score: Score = this.renderer.score!; @@ -102,6 +96,19 @@ export abstract class ScoreLayout { this.lastBarIndex ); + const firstChangedMasterBar = renderHints?.firstChangedMasterBar; + if (firstChangedMasterBar !== undefined) { + if (this.doUpdateForBars(renderHints!)) { + return; + } + } + + this._lazyPartials.clear(); + this.beamingRuleLookups.clear(); + this._barRendererLookup.clear(); + + this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile)!; + this.pagePadding = this.renderer.settings.display.padding.map(p => p / this.renderer.settings.display.scale); if (!this.pagePadding) { this.pagePadding = [0, 0, 0, 0]; @@ -118,6 +125,10 @@ export abstract class ScoreLayout { private _lazyPartials: Map = new Map(); + protected getExistingPartialArgs(id:string): RenderFinishedEventArgs|undefined { + return this._lazyPartials.has(id) ? this._lazyPartials.get(id)!.args : undefined; + } + protected registerPartial(args: RenderFinishedEventArgs, callback: (canvas: ICanvas) => void) { if (args.height === 0) { return; @@ -137,7 +148,11 @@ export abstract class ScoreLayout { this._internalRenderLazyPartial(args, callback); } else { // in case of lazy loading -> first register lazy, then notify - this._lazyPartials.set(args.id, new LazyPartial(args, callback)); + const partial: LazyPartial = { + args, + renderCallback: callback + }; + this._lazyPartials.set(args.id, partial); (this.renderer.partialLayoutFinished as EventEmitterOfT).trigger(args); } } @@ -500,7 +515,7 @@ export abstract class ScoreLayout { } } - public layoutAndRenderAnnotation(y: number): number { + protected _layoutAndRenderAnnotation(y: number): number { // attention, you are not allowed to remove change this notice within any version of this library without permission! const msg: string = 'rendered by alphaTab'; const resources: RenderingResources = this.renderer.settings.display.resources; diff --git a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts index d1b88c302..dc2d53fc7 100644 --- a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts +++ b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts @@ -1,13 +1,14 @@ +import type { EventEmitterOfT } from '@coderline/alphatab/EventEmitter'; import { Logger } from '@coderline/alphatab/Logger'; import { ScoreSubElement } from '@coderline/alphatab/model/Score'; import { type ICanvas, TextAlign } from '@coderline/alphatab/platform/ICanvas'; -import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import type { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; import type { RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; import { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; import type { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; +import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; /** * Base layout for page and parchment style layouts where we have an endless @@ -21,11 +22,18 @@ export abstract class VerticalLayoutBase extends ScoreLayout { private _reuseViewPort: boolean = false; + private _preSystemPartialIds: string[] = []; + private _systemPartialIds: string[] = []; + protected doLayoutAndRender(renderHints: RenderHints | undefined): void { let y: number = this.pagePadding![1]; this.width = this.renderer.width; this._allMasterBarRenderers = []; + this._preSystemPartialIds = []; + this._systemPartialIds = []; + this._reuseViewPort = renderHints?.reuseViewport ?? false; + this._systems = []; // // 1. Score Info @@ -38,11 +46,11 @@ export abstract class VerticalLayoutBase extends ScoreLayout { y = this._layoutAndRenderChordDiagrams(y, -1); // // 4. One result per StaffSystem - y = this._layoutAndRenderScore(y); + y = this._layoutAndRenderScore(y, this.firstBarIndex); y = this.layoutAndRenderBottomScoreInfo(y); - y = this.layoutAndRenderAnnotation(y); + y = this._layoutAndRenderAnnotation(y); this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; } @@ -52,6 +60,15 @@ export abstract class VerticalLayoutBase extends ScoreLayout { super.registerPartial(args, callback); } + protected reregisterPartial(id: string) { + const args = this.getExistingPartialArgs(id); + if (!args) { + return; + } + args.reuseViewport = this._reuseViewPort; + (this.renderer.partialLayoutFinished as EventEmitterOfT).trigger(args); + } + public get supportsResize(): boolean { return true; } @@ -64,6 +81,52 @@ export abstract class VerticalLayoutBase extends ScoreLayout { return x; } + public override doUpdateForBars(renderHints: RenderHints): boolean { + this._reuseViewPort = renderHints.reuseViewport ?? false; + const firstModifiedMasterBar = renderHints.firstChangedMasterBar!; + + // first update existing systems as needed + const systemIndex = this._systems.findIndex(s => { + const first = s.masterBarsRenderers[0].masterBar.index; + const last = s.masterBarsRenderers[s.masterBarsRenderers.length - 1].masterBar.index; + return first <= firstModifiedMasterBar && firstModifiedMasterBar <= last; + }); + + if (systemIndex === -1 || !this.renderer.settings.core.enableLazyLoading) { + return false; + } + + // for now we do a full relayout from the first modified masterbar + // there is a lot of room for even more performant updates, but they come + // at a risk that features break. + // e.g. we could only shift systems where the content didn't change, + // but we might still have ties/slurs which have to be updated. + const removeSystems = this._systems.splice(systemIndex, this._systems.length - systemIndex); + this._systemPartialIds.splice(systemIndex, this._systemPartialIds.length - systemIndex); + const system = removeSystems[0]; + let y = system.y; + const firstBarIndex = system.masterBarsRenderers[0].masterBar.index; + + // signal all partials which didn't change + for (const preSystemPartial of this._preSystemPartialIds) { + this.reregisterPartial(preSystemPartial); + } + for (let i = 0; i < systemIndex; i++) { + this.reregisterPartial(this._systemPartialIds[i]); + } + + // new partials for all other prats + y = this._layoutAndRenderScore(y, firstBarIndex); + + y = this.layoutAndRenderBottomScoreInfo(y); + + y = this._layoutAndRenderAnnotation(y); + + this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; + + return true; + } + public doResize(): void { let y: number = this.pagePadding![1]; this.width = this.renderer.width; @@ -85,7 +148,7 @@ export abstract class VerticalLayoutBase extends ScoreLayout { y = this.layoutAndRenderBottomScoreInfo(y); - y = this.layoutAndRenderAnnotation(y); + y = this._layoutAndRenderAnnotation(y); this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; } @@ -115,6 +178,7 @@ export abstract class VerticalLayoutBase extends ScoreLayout { canvas.textAlign = TextAlign.Center; this.tuningGlyph!.paint(0, 0, canvas); }); + this._preSystemPartialIds.push(e.id); return y + tuningHeight; } @@ -143,6 +207,7 @@ export abstract class VerticalLayoutBase extends ScoreLayout { canvas.textAlign = TextAlign.Center; this.chordDiagrams!.paint(0, 0, canvas); }); + this._preSystemPartialIds.push(e.id); return y + diagramHeight; } @@ -197,6 +262,7 @@ export abstract class VerticalLayoutBase extends ScoreLayout { g.paint(0, 0, canvas); } }); + this._preSystemPartialIds.push(e.id); } return y + infoHeight; @@ -205,6 +271,7 @@ export abstract class VerticalLayoutBase extends ScoreLayout { private _resizeAndRenderScore(y: number, oldHeight: number): number { // if we have a fixed number of bars per row, we only need to refit them. const barsPerRowActive = this.getBarsPerSystem(0) > 0; + this._systemPartialIds = []; if (barsPerRowActive) { for (let i: number = 0; i < this._systems.length; i++) { const system: StaffSystem = this._systems[i]; @@ -270,12 +337,10 @@ export abstract class VerticalLayoutBase extends ScoreLayout { return y; } - private _layoutAndRenderScore(y: number): number { - const startIndex: number = this.firstBarIndex; + private _layoutAndRenderScore(y: number, startIndex: number): number { let currentBarIndex: number = startIndex; const endBarIndex: number = this.lastBarIndex; - this._systems = []; while (currentBarIndex <= endBarIndex) { // create system and align set proper coordinates const system: StaffSystem = this._createStaffSystem(currentBarIndex, endBarIndex); @@ -317,6 +382,7 @@ export abstract class VerticalLayoutBase extends ScoreLayout { // since we use partial drawing system.paint(0, -(args.y / this.renderer.settings.display.scale), canvas); }); + this._systemPartialIds.push(args.id); // calculate coordinates for next system return height; diff --git a/packages/csharp/src/AlphaTab/Core/TypeHelper.cs b/packages/csharp/src/AlphaTab/Core/TypeHelper.cs index 95dec6afe..da6406b30 100644 --- a/packages/csharp/src/AlphaTab/Core/TypeHelper.cs +++ b/packages/csharp/src/AlphaTab/Core/TypeHelper.cs @@ -70,6 +70,20 @@ public static T Find(this IList list, Func predicate) return list.FirstOrDefault(predicate); } + public static T FindIndex(this IList list, Func predicate) + { + var index = 0; + foreach (var item in list) + { + if (predicate(item)) + { + return index; + } + index++; + } + return -1; + } + public static bool Includes(this IList list, T item) { return list.Contains(item); diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/collections/List.kt b/packages/kotlin/src/android/src/main/java/alphaTab/collections/List.kt index 951ada471..0c0fdff6f 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/collections/List.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/collections/List.kt @@ -87,6 +87,10 @@ public class List : Iterable { }) } + internal fun findIndex(predicate: (T) -> Boolean): Double { + return _data.indexOfFirst(predicate).toDouble() + } + public fun some(predicate: (T) -> Boolean): Boolean { return _data.any(predicate) } @@ -165,24 +169,42 @@ public class List : Iterable { return _data.removeAt(0) } - public fun splice(start: Double, deleteCount: Double, vararg newElements: T) { + public fun splice(start: Double, deleteCount: Double, vararg newElements: T): List { var actualStart = start.toInt() if (actualStart < 0) { actualStart += _data.size } + val remove = if (deleteCount > 0) List( + ArrayListWithRemoveRange( + _data.subList( + start.toInt(), + _data.size + ) + ) + ) else List() _data.removeRange(start.toInt(), (start + deleteCount).toInt()) _data.addAll(start.toInt(), newElements.toList()) + return remove } - public fun splice(start: Double, deleteCount: Double, newElements: Iterable) { + public fun splice(start: Double, deleteCount: Double, newElements: Iterable): List { var actualStart = start.toInt() if (actualStart < 0) { actualStart += _data.size } + val remove = if (deleteCount > 0) List( + ArrayListWithRemoveRange( + _data.subList( + start.toInt(), + _data.size + ) + ) + ) else List() _data.removeRange(start.toInt(), (start + deleteCount).toInt()) _data.addAll(start.toInt(), newElements.toList()) + return remove } public fun join(separator: String): String { diff --git a/packages/playground/recorder.html b/packages/playground/recorder.html new file mode 100644 index 000000000..82b5ae436 --- /dev/null +++ b/packages/playground/recorder.html @@ -0,0 +1,18 @@ + + + + + + AlphaTab alphaTex Recorder Demo + + + + + +
+ + + + \ No newline at end of file diff --git a/packages/playground/recorder.ts b/packages/playground/recorder.ts new file mode 100644 index 000000000..ff02906b2 --- /dev/null +++ b/packages/playground/recorder.ts @@ -0,0 +1,233 @@ +import { setupControl } from './control'; +import * as alphaTab from '@coderline/alphatab'; + +const req = new XMLHttpRequest(); +req.onload = () => { + document.getElementById('placeholder')!.outerHTML = req.responseText; + + // this is a demo which builds a "recorder" with alphaTab. + // in this case we do not actually play the song but just use the rendering and player capabilities + // to get a display of the notes and a cursor. for the sake of simplicity we do not have a real recorder but we + // 1. fill a bar with quarter rests + // 2. when pressing a key 0-9 we put a note at this fret in the currently active quarter. + + // the overall recorder goes with various assumptions: + // 1. we only have one track/staff/voice being recorded + // -> would need more complex update of the data model. + // 2. we do not have any tempo, time signature or similar changes as we simply record lineary + // -> would need more complex handling of updating the lookups. + // 3. we do not have any re-recording flows (stop, seek and restart recording) + // -> would need further extensions. + + // we want bars dynamically being added as we record, to achieve this we use following tricks for rendering: + + // 1. we start with one full system + // -> this ensures the player/cursor doesn't think we have an end, but we still continue. + // 2. we add a new system when we reach a 80% of the second-last bar. + // -> this gives us always one empty future bar ensuring correct rendering and cursor behavior. + + // to get the cursor behaving as we want we do following: + + // 1. we generate an empty midi at start. this gives us a base Midi and MidiTickLookup to start with. + // 2. we extend this midi to the expected maximum recording length (multiple minutes of playback). + // this way the player will internally play quasi endlessly and allows us to extend the song. + // 3. When we extend we need to update the MidiTickLookup with the new bars to have correct cursor alignment. + + const api = setupControl('#alphaTab', { + core: { + file: undefined + }, + display: { + // parchment gives the best deterministic system creation without flickering as bars are added + layoutMode: alphaTab.LayoutMode.Parchment, + justifyLastSystem: true + } + }); + + // start with 2 bars to always have 1 future bar buffer + const score = alphaTab.importer.ScoreLoader.loadAlphaTex('r.4 r r r'); + score.tracks[0].defaultSystemsLayout = 5; + + // threshold indicating we need to insert a new bar, -1 as marker to not do anything + let insertTickThreshold = -1; + + function insertNewMasterBar() { + const newMasterBar = new alphaTab.model.MasterBar(); + score.addMasterBar(newMasterBar); + + // insert new bar to tick cache for cursor placement + const masterBarTickLookup = new alphaTab.midi.MasterBarTickLookup(); + masterBarTickLookup.tempoChanges.push( + new alphaTab.midi.MasterBarTickLookupTempoChange(newMasterBar.start, score.tempo) + ); + masterBarTickLookup.start = newMasterBar.start; + masterBarTickLookup.end = newMasterBar.start + newMasterBar.calculateDuration(); + masterBarTickLookup.masterBar = newMasterBar; + api.tickCache?.addMasterBar(masterBarTickLookup); + + return newMasterBar; + } + + function insertNewBar() { + const staff = score.tracks[0].staves[0]; + const previousBar = staff.bars[staff.bars.length - 1]; + const newBar = new alphaTab.model.Bar(); + newBar.clef = previousBar.clef; + newBar.clefOttava = previousBar.clefOttava; + newBar.keySignature = previousBar.keySignature; + newBar.keySignatureType = previousBar.keySignatureType; + + staff.addBar(newBar); + + const initialVoice = new alphaTab.model.Voice(); + newBar.addVoice(initialVoice); + + for (let i = 0; i < 4; i++) { + const emptyBeat = new alphaTab.model.Beat(); + emptyBeat.isEmpty = false; + emptyBeat.duration = alphaTab.model.Duration.Quarter; + initialVoice.addBeat(emptyBeat); + + api.tickCache?.addBeat(emptyBeat, i * 960, 960 /* midi quarter time */); + } + + return newBar; + } + + function insertNewSystem() { + // clear threshold after we create bar, will be set again after render + insertTickThreshold = -1; + + const currentSystemCount = Math.floor(score.masterBars.length / score.tracks[0].defaultSystemsLayout); + const neededSystemCount = currentSystemCount + 1; + const neededBars = neededSystemCount * score.tracks[0].defaultSystemsLayout; + const lastMasterBarIndex = score.masterBars.length - 1; + + let missingBars = neededBars - score.masterBars.length; + + while (missingBars > 0) { + const newMasterBar = insertNewMasterBar(); + const newBar = insertNewBar(); + + const sharedDataBag = new Map(); + newMasterBar.finish(sharedDataBag); + newBar.finish(api.settings, sharedDataBag); + + missingBars--; + } + + // + // update remaining bits and render + + updateInsertTickThreshold(); + + api.renderScore(score, undefined, { + reuseViewport: currentSystemCount > 0, + firstChangedMasterBar: currentSystemCount > 0 ? lastMasterBarIndex : undefined + }); + } + + function updateInsertTickThreshold() { + // assumption: due to recording we do not have any repeats but a linear score + const lastBar = score!.masterBars![score.masterBars.length - 2]; + const thresholdPercent = 0.8; + const lastBarDuration = lastBar.calculateDuration(); + insertTickThreshold = lastBar.start + lastBarDuration * thresholdPercent; + } + + api.scoreLoaded.on(() => { + updateInsertTickThreshold(); + }); + + // add second bar and setup + insertNewSystem(); + + // extend the midi to be very long + api.midiLoad.on(midi => { + // find last rest event as starting point to extend + let rest: alphaTab.midi.AlphaTabRestEvent | undefined = undefined; + for (let i = midi.tracks[0].events.length; i >= 0; i--) { + const e = midi.tracks[0].events[i]; + if (e instanceof alphaTab.midi.AlphaTabRestEvent) { + rest = e; + break; + } + } + + // should never happen assuming we start with an empty song like in this sample + if (!rest) { + return; + } + + // 30mins should be enough for everyone ;) + const midiQuarterTime = 960; + const desiredLengthInMilliseconds = 60 * 30 * 1000; + const tempo = api.tickCache!.masterBars[0].tempoChanges[0].tempo; + const desiredLengthInTicks = (desiredLengthInMilliseconds / (60000.0 / (tempo * midiQuarterTime))) | 0; + + const endOfTrack = midi.tracks[0].events.pop()! as alphaTab.midi.EndOfTrackEvent; + + // add rest events every quarter note + let tick = rest.tick + midiQuarterTime; + while (tick < desiredLengthInTicks) { + midi.tracks[0].events.push(new alphaTab.midi.AlphaTabRestEvent(rest.track, tick, rest.channel)); + tick += midiQuarterTime; + } + + // shift end message + endOfTrack.tick = tick; + }); + + api.playerPositionChanged.on(e => { + if (insertTickThreshold !== -1 && e.currentTick >= insertTickThreshold) { + insertNewSystem(); + } + }); + let currentBeat: alphaTab.model.Beat | undefined = undefined; + api.playedBeatChanged.on(beat => { + currentBeat = beat; + }); + + // very basic recording feature, keyboard digits cause a new beat + // with the fret 0-9 on the first string to be added + // this is likely the trickiest part in a real recording: compute the beat lengths and filling the model + document.addEventListener( + 'keydown', + e => { + if (!currentBeat) { + return; + } + + let fret = -1; + + if (e.code.startsWith('Digit') || e.code.startsWith('Numpad')) { + fret = Number.parseInt(e.code.substring(e.code.length - 1), 10); + } else { + return; + } + + e.preventDefault(); + + if (currentBeat.notes.length === 0) { + const newNote = new alphaTab.model.Note(); + newNote.string = 1; + newNote.fret = fret; + currentBeat.addNote(newNote); + } else { + currentBeat.notes[0].fret = fret; + } + + currentBeat.voice.bar.finish(api.settings, null); + + api.renderScore(score, undefined, { + reuseViewport: true, + firstChangedMasterBar: currentBeat.voice.bar.index + }); + }, + true + ); + + (window as any).at = api; +}; +req.open('GET', 'control-template.html'); +req.send();