Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
node_modules/
dist/
canon.png

**/public/font/
**/public/soundfont/
85 changes: 81 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,84 @@
# AlphaTab Web Samples
# alphaTab Web Samples

This repository contains various samples and demos for using alphaTab from Web based technologies.
Samples for using [alphaTab](https://www.alphatab.net) in web projects: without a bundler, with the common bundlers, and with the common frameworks. Every sample renders the alphaTab demo song "Iron Waltz" and plays it, so each one exercises alphaTab's rendering worker, its synthesizer worker and its audio worklet. Each sample folder has a README with the setup details and the reasons behind them.

For feature demo and testing purposes all samples might use the `@alpha` (unreleased) version of alphaTab. For your projects always use the `@latest` (stable) version.
> **Version:** the samples target alphaTab **1.9** (`@coderline/alphatab`, `@coderline/alphatab-webpack`, `@coderline/alphatab-vite`), currently released as `@alpha`. For your own projects, use the `@latest` (stable) version unless you need an unreleased feature.

> :information_source: **Some samples might require to be run within a Web Server to work. You can use tools like https://github.com/http-party/http-server to quickly start one.**
## Running a sample

```sh
cd src/<sample>
npm install
npm run dev # development server
npm run build # production build; see the sample's README for how to serve it
```

The `browser-*` samples need no installation, but they must be served by a web server (e.g. `npx http-server`), not opened from the file system.

## How alphaTab is integrated

alphaTab runs its rendering and synthesizer in a Web Worker and its audio output in an AudioWorklet. With a bundler, all three must load alphaTab's code, ideally as **one shared chunk** ("core copies" below; fewer is better):

- **webpack, Rspack, Rsbuild:** [`@coderline/alphatab-webpack`](https://www.npmjs.com/package/@coderline/alphatab-webpack) (one plugin for webpack 5 and Rspack 1/2).
- **Vite** (and frameworks built on it): [`@coderline/alphatab-vite`](https://www.npmjs.com/package/@coderline/alphatab-vite).
- **Turbopack** (Next.js): no plugin; alphaTab detects Turbopack and Turbopack bundles its workers. The Audio Worklet loads alphaTab's unbundled ES module, copied into `public/`.
- **Other bundlers** (esbuild, Rollup, Parcel, Angular's application builder): the app owns small worker and worklet entry files that import `@coderline/alphatab/worker` and `@coderline/alphatab/worklet`, and initializes alphaTab from `@coderline/alphatab/core`, or alphaTab is loaded unbundled. The samples show the setup per tool.

## Samples

### Without a bundler

| Sample | Shows | Core copies |
|---|---|---|
| [`browser-simple-esm`](src/browser-simple-esm) | ES module from a CDN, render + play | 1 |
| [`browser-simple-umd`](src/browser-simple-umd) | `<script>` tag (UMD) from a CDN | 1 |
| [`browser-advanced-esm`](src/browser-advanced-esm) | ES module with a full player UI (tracks, transport, layout) | 1 |
| [`browser-advanced-umd`](src/browser-advanced-umd) | the same with the UMD build | 1 |
| [`node-simple-esm`](src/node-simple-esm) | Node.js (ESM): load a file and read the score model | – |
| [`node-simple-cjs`](src/node-simple-cjs) | the same with CommonJS | – |
| [`node-render-esm`](src/node-render-esm) | Node.js: render a score to PNG with alphaSkia | – |

### Bundlers

| Sample | Stack | Integration | Core copies |
|---|---|---|---|
| [`bundler-webpack-5`](src/bundler-webpack-5) | webpack 5 (≥ 5.90), HtmlWebpackPlugin | `@coderline/alphatab-webpack` | 1 |
| [`bundler-rspack-1`](src/bundler-rspack-1) | Rspack 1.7 | `@coderline/alphatab-webpack` | 1 |
| [`bundler-rspack-2`](src/bundler-rspack-2) | Rspack 2 | `@coderline/alphatab-webpack` | 1 |
| [`bundler-rsbuild-2`](src/bundler-rsbuild-2) | Rsbuild 2 (same config works on Rsbuild 1.7) | `@coderline/alphatab-webpack` via `tools.rspack` | 1 |
| [`bundler-vite-6`](src/bundler-vite-6) | Vite 6 | `@coderline/alphatab-vite` | 1 |
| [`bundler-vite-7`](src/bundler-vite-7) | Vite 7 | `@coderline/alphatab-vite` | 1 |
| [`bundler-vite-8`](src/bundler-vite-8) | Vite 8 | `@coderline/alphatab-vite` | 1 |
| [`bundler-esbuild`](src/bundler-esbuild) | esbuild | own worker/worklet entries, ESM splitting | 1 |
| [`bundler-rollup-4`](src/bundler-rollup-4) | Rollup 4 | own worker/worklet entries, ESM splitting | 1 |
| [`bundler-parcel-2`](src/bundler-parcel-2) | Parcel 2 | own worker/worklet entries (`worklet:` import) | 3 (Parcel bundles workers and worklets self-contained) |

### Frameworks

| Sample | Stack | Integration | Core copies |
|---|---|---|---|
| [`react-19-vite`](src/react-19-vite) | React 19 (React 18 works the same), Vite 8 | `@coderline/alphatab-vite` | 1 |
| [`nextjs-16-webpack`](src/nextjs-16-webpack) | Next.js 16 with `--webpack` | `@coderline/alphatab-webpack` | 1 (dev: 3) |
| [`nextjs-15-webpack`](src/nextjs-15-webpack) | Next.js 15 (webpack) | `@coderline/alphatab-webpack` | 1 (dev: 3) |
| [`nextjs-16-turbopack`](src/nextjs-16-turbopack) | Next.js 16 with Turbopack | workers bundled by Turbopack, worklet from the copied ES module (`core.scriptFile`) | 2 |
| [`vue-3-vite`](src/vue-3-vite) | Vue 3, Vite 8 | `@coderline/alphatab-vite` | 1 |
| [`nuxt-4`](src/nuxt-4) | Nuxt 4 (Vite 8), SSR | `@coderline/alphatab-vite` | 1 |
| [`nuxt-3`](src/nuxt-3) | Nuxt 3 (Vite 7), SSR | `@coderline/alphatab-vite` | 1 |
| [`angular-22-esbuild`](src/angular-22-esbuild) | Angular 22, application builder (esbuild), SSR | alphaTab external, import map, copied ES modules | 1 |
| [`angular-21-esbuild`](src/angular-21-esbuild) | Angular 21, application builder (esbuild) | alphaTab external, import map, copied ES modules | 1 |
| [`angular-20-esbuild`](src/angular-20-esbuild) | Angular 20, application builder (esbuild), Zone.js | alphaTab external, import map, copied ES modules | 1 |
| [`angular-22-webpack`](src/angular-22-webpack) | Angular 22, `@angular-builders/custom-webpack` (Angular's webpack builder is deprecated) | `@coderline/alphatab-webpack` with `alphaTabCore: false` | 2 |
| [`svelte-5-vite`](src/svelte-5-vite) | Svelte 5, Vite 8 | `@coderline/alphatab-vite` | 1 |
| [`sveltekit-3`](src/sveltekit-3) | SvelteKit 3 (Vite 8), SSR | `@coderline/alphatab-vite` | 1 |
| [`solid-1-vite`](src/solid-1-vite) | Solid 1.9, Vite 8 | `@coderline/alphatab-vite` | 1 |

## Things every setup needs

- **alphaTab is browser-only.** In frameworks with server-side rendering, create the API only on the client (an effect, `onMount`, `afterNextRender`, a `.client` component); each sample shows its framework's way.
- **Font and SoundFont.** The plugins copy them into the output; when the bundler writes scripts into a subfolder (Vite's `assets/`, Rsbuild's `static/js/`), set `core.fontDirectory` (and `player.soundFont`) to where they are served. The samples do.
- **All entry files must be loaded.** With webpack and Rspack the core is its own chunk of the page; HTML generated by HtmlWebpackPlugin, Rsbuild or Next.js includes it automatically, a hand-written `<script>` list must include it too.

## Not supported

- **Next.js with Turbopack and the webpack plugin:** Turbopack runs no webpack plugins and bundles no Audio Worklets. The [`nextjs-16-turbopack`](src/nextjs-16-turbopack) sample shows the setup without the plugin; with webpack (`next dev --webpack`, `next build --webpack`) the plugin works and alphaTab loads once.
- **Angular's application builder with alphaTab bundled:** the builder handles workers only in the app's own code and has no worklet support. The Angular esbuild samples keep alphaTab external instead.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.

# Compiled output
/dist
Expand Down Expand Up @@ -36,6 +36,7 @@ yarn-error.log
/libpeerconnection.log
testem.log
/typings
__screenshots__/

# System files
.DS_Store
Expand Down
61 changes: 61 additions & 0 deletions src/angular-20-esbuild/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# alphaTab with Angular 20 (application builder)

Renders the Guitar Pro file "Iron Waltz" (`public/iron-waltz.gp`) with alphaTab and plays it back, in an Angular
20 app built with the default application builder (`@angular/build:application`, esbuild).

- Angular 20, `@angular/build:application` and `@angular/build:dev-server`
- `@coderline/alphatab` 1.9
- Standalone component with Zone.js change detection (the Angular 20 default); `afterNextRender` for client-only creation

The alphaTab setup is the same as in `angular-22-esbuild` (which also shows server-side rendering).

## Run

```sh
npm install
npm start # dev server on http://localhost:4200
npm run build # production build to dist/angular-20-esbuild/browser
```

## How alphaTab is integrated

alphaTab renders in a Web Worker and synthesizes audio in an Audio Worklet, started from alphaTab's ES module
with `new Worker(new URL('./alphaTab.worker.mjs', import.meta.url))` and
`audioWorklet.addModule(new URL('./alphaTab.worklet.mjs', import.meta.url))`. Angular's application builder has
no plugin API and bundles `new Worker(new URL(...))` only in your own TypeScript files, not inside dependencies.
A plain import of alphaTab therefore works with `ng serve` (Vite pre-bundling resolves the URLs) but not in the
production build: the worker file is missing and the score never renders.

The sample loads alphaTab's own ES modules instead of bundling them; page, workers and worklet share one copy of
alphaTab's core:

1. **`angular.json` → `externalDependencies: ["@coderline/alphatab"]`** keeps the import of
`@coderline/alphatab` as a bare import in the output.
2. **`angular.json` → `assets`** copies from `node_modules/@coderline/alphatab/dist`:
`alphaTab.min.mjs`, `alphaTab.core.min.mjs`, `alphaTab.worker.min.mjs` and `alphaTab.worklet.min.mjs` to
`/alphatab/`,
`font/*` to `/font/` (`core.fontDirectory`) and `soundfont/sonivox.sf3` to `/soundfont/` (`player.soundFont`).
3. **`src/index.html`** maps `@coderline/alphatab` to `/alphatab/alphaTab.min.mjs` with an import map.
alphaTab then finds its worker and worklet next to that file.
4. **`angular.json` → `index.preloadInitial: false`** prevents a `<link rel="modulepreload" href="@coderline/alphatab">`
in the production `index.html`; browsers do not resolve it through the import map and report an error.

The initial bundle stays at about 110 kB, so the default budgets apply.

The component creates alphaTab in `afterNextRender` (browser only, also when server-side rendering is added
later) and destroys it via `DestroyRef`.

The app uses Zone.js. The component creates alphaTab with `NgZone.runOutsideAngular`, so alphaTab's timers and
frequent events (e.g. the playback position) do not trigger change detection; the signals the component sets
from alphaTab events still update the view.

## Trade-offs

- The files under `/alphatab/` are not content-hashed; make sure browsers fetch the new files after an alphaTab
update.
- The copied files are alphaTab's minified set: `alphaTab.min.mjs` imports `alphaTab.core.min.mjs` (1.2 MB,
about 300 kB gzip) and starts `alphaTab.worker.min.mjs` and `alphaTab.worklet.min.mjs`, which import the same
core. For debugging, copy `alphaTab.{mjs,core.mjs,worker.mjs,worklet.mjs}` and map `@coderline/alphatab` to
`/alphatab/alphaTab.mjs` instead; each set references only its own files, so copy and map one set completely.
- Bundling alphaTab and copying `alphaTab.{core,worker,worklet}.mjs` to the output root also works (verified with
Angular 22) but loads 2 copies of the core; see `angular-22-esbuild` for the comparison.
87 changes: 87 additions & 0 deletions src/angular-20-esbuild/angular.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"angular-20-esbuild": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"index": {
"input": "src/index.html",
"preloadInitial": false
},
"externalDependencies": [
"@coderline/alphatab"
],
"assets": [
{
"glob": "**/*",
"input": "public"
},
{
"glob": "**/*",
"input": "node_modules/@coderline/alphatab/dist/font",
"output": "font"
},
{
"glob": "sonivox.sf3",
"input": "node_modules/@coderline/alphatab/dist/soundfont",
"output": "soundfont"
},
{
"glob": "alphaTab.{min.mjs,core.min.mjs,worker.min.mjs,worklet.min.mjs}",
"input": "node_modules/@coderline/alphatab/dist",
"output": "alphatab"
}
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "angular-20-esbuild:build:production"
},
"development": {
"buildTarget": "angular-20-esbuild:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
}
}
}
}
}
26 changes: 26 additions & 0 deletions src/angular-20-esbuild/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "angular-20-esbuild",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build"
},
"private": true,
"dependencies": {
"@angular/common": "^20.3.0",
"@angular/compiler": "^20.3.0",
"@angular/core": "^20.3.0",
"@angular/platform-browser": "^20.3.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0",
"@coderline/alphatab": "^1.9.0-alpha.1965"
},
"devDependencies": {
"@angular/build": "^20.3.42",
"@angular/cli": "^20.3.42",
"@angular/compiler-cli": "^20.3.0",
"typescript": "~5.9.2"
}
}
Binary file added src/angular-20-esbuild/public/iron-waltz.gp
Binary file not shown.
5 changes: 5 additions & 0 deletions src/angular-20-esbuild/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
providers: [provideBrowserGlobalErrorListeners(), provideZoneChangeDetection({ eventCoalescing: true })],
};
43 changes: 43 additions & 0 deletions src/angular-20-esbuild/src/app/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { afterNextRender, Component, DestroyRef, ElementRef, inject, NgZone, signal, viewChild } from '@angular/core';
import * as alphaTab from '@coderline/alphatab';

@Component({
selector: 'app-root',
template: `
<button type="button" [disabled]="!playerReady()" (click)="playPause()">{{ playing() ? 'Pause' : 'Play' }}</button>
<div #alphaTab></div>
`,
})
export class App {
private readonly element = viewChild.required<ElementRef<HTMLDivElement>>('alphaTab');
private api?: alphaTab.AlphaTabApi;
protected readonly playerReady = signal(false);
protected readonly playing = signal(false);

constructor() {
const zone = inject(NgZone);
// afterNextRender never runs during server-side rendering: alphaTab needs the DOM, workers and Web Audio.
afterNextRender(() => {
// Outside the zone, alphaTab's timers and frequent events do not trigger change detection; the signals below still do.
const api = zone.runOutsideAngular(() => new alphaTab.AlphaTabApi(this.element().nativeElement, {
core: {
file: '/iron-waltz.gp',
fontDirectory: '/font/',
},
player: {
playerMode: alphaTab.PlayerMode.EnabledAutomatic,
enableCursor: true,
soundFont: '/soundfont/sonivox.sf3',
},
}));
api.playerReady.on(() => this.playerReady.set(true));
api.playerStateChanged.on((e) => this.playing.set(e.state === alphaTab.synth.PlayerState.Playing));
this.api = api;
});
inject(DestroyRef).onDestroy(() => this.api?.destroy());
}

protected playPause() {
this.api?.playPause();
}
}
15 changes: 15 additions & 0 deletions src/angular-20-esbuild/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>alphaTab Angular 20</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!-- @coderline/alphatab is external (angular.json) and copied to /alphatab/ as alphaTab's minified ES modules. -->
<script type="importmap">{ "imports": { "@coderline/alphatab": "/alphatab/alphaTab.min.mjs" } }</script>
</head>
<body>
<app-root></app-root>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { App } from './app/app';

bootstrapApplication(AppComponent, appConfig)
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));
15 changes: 15 additions & 0 deletions src/angular-20-esbuild/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
]
}
Loading