Skip to content

Commit c6b7c3e

Browse files
authored
docs: Update node.js guide (#107)
2 parents 906f4b1 + 74d0ea8 commit c6b7c3e

File tree

4 files changed

+292
-20
lines changed

4 files changed

+292
-20
lines changed

docs/guides/nodejs.mdx

Lines changed: 292 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,33 @@ import { SinceBadge } from '@site/src/components/SinceBadge';
77
<SinceBadge since="1.2.3" />
88

99
The JavaScript version of alphaTab is primarily optimized for usage in Browsers. At least the top level `AlphaTabApi`
10-
which is a UI focused interface to alphaTab. But this does not mean that alphaTab cannot be used in other
10+
which is a UI focused interface to alphaTab. But alphaTab can also be used in other
1111
JavaScript environments like Node.js or headless JavaScript engines. The core of alphaTab is platform independent
12-
and simply requires certain APIs to be available. If you are able to make a HTML5 canvas compatible API available
13-
to the runtime (e.g. via node-canvas, skia-canvas) the built-in HTML5 renderer might also work.
12+
and simply requires certain APIs to be available. With [alphaSkia](https://github.com/CoderLine/alphaSkia) you can
13+
even produce raster graphics output.
1414

15-
In this guide we will setup a Node.js example which is using alphaTab to render a given input file to an SVG.
16-
This SVG could then be sent to a browser for display.
15+
In this guide we will setup a Node.js example which is using alphaTab to render a given input file to an SVG and PNG.
1716

1817
## 1. Setup project
1918

2019
The start is quite simple:
2120

2221
1. Run `npm init` in a new directory
2322
2. Run `npm install @coderline/alphatab` to install alphaTab
24-
3. Create a new index.js into which our code will go .
23+
3. Create a new `index.mjs` into which our code will go .
2524

2625
## 2. The basic code structure
2726

2827
In our code we will first load alphaTab and load the `Score` from a given input file path:
2928

3029
```js
3130
// load alphaTab
32-
const alphaTab = require("@coderline/alphatab");
31+
import * as alphaTab from "@coderline/alphatab";
3332
// needed for file load
34-
const fs = require("fs");
33+
import * as fs from 'fs';
3534

3635
// Load the file
37-
const fileData = fs.readFileSync(process.argv[2]);
36+
const fileData = await fs.promises.readFile(process.argv[2]);
3837
const settings = new alphaTab.Settings();
3938
const score = alphaTab.importer.ScoreLoader.loadScoreFromBytes(
4039
new Uint8Array(fileData),
@@ -56,11 +55,13 @@ special needs for your project.
5655
The code below is mainly taken from the [Low Level API guide](/docs/guides/lowlevel-apis).
5756

5857
```js
59-
const alphaTab = require("@coderline/alphatab");
60-
const fs = require("fs");
58+
// load alphaTab
59+
import * as alphaTab from "@coderline/alphatab";
60+
// needed for file load
61+
import * as fs from 'fs';
6162

6263
// 1. Load File
63-
const fileData = fs.readFileSync(process.argv[2]);
64+
const fileData = await fs.promises.readFile(process.argv[2]);
6465
const settings = new alphaTab.Settings();
6566
const score = alphaTab.importer.ScoreLoader.loadScoreFromBytes(
6667
new Uint8Array(fileData),
@@ -74,33 +75,41 @@ renderer.width = 1200;
7475

7576
// 3. Listen to Events
7677
let svgChunks = [];
78+
79+
// clear on new rendering
7780
renderer.preRender.on((isResize) => {
78-
svgChunks = []; // clear on new rendering
81+
svgChunks = [];
7982
});
83+
84+
// Since 1.3.0:
85+
// alphaTab separates "layouting" and "rendering" of the individual
86+
// partial results. in this case we want to render directly any partial layouted
87+
// (in UI scenarios the rendering might be delayed to the point when partials become visible)
88+
renderer.partialLayoutFinished.on((r) => {
89+
renderer.renderResult(r.id);
90+
})
91+
92+
// whenever the rendering of a partial is finished, we remember all individual outputs.
8093
renderer.partialRenderFinished.on((r) => {
8194
svgChunks.push({
8295
svg: r.renderResult, // svg string
8396
width: r.width,
8497
height: r.height,
8598
});
8699
});
87-
renderer.renderFinished.on((r) => {
88-
displayResult(svgChunks, r.totalWidth, r.totalHeight);
89-
});
90100

91-
// 4. Fire off rendering
101+
// 4. Fire off rendering, this is synchronous so we can assume all results to be available after this call.
92102
renderer.renderScore(score, [0]);
93103

94-
// log the SVG chunks
104+
// 5. log the SVG chunks
95105
console.log(svgChunks.map((c) => c.svg).join("\n"));
96106
```
97107

98108
Running the script again, we can see the output.
99109

100110
<img src="/img/guides/nodejs/svg.png" />
101111

102-
103-
### 4. The CSS dependency
112+
## 4. The CSS dependency
104113
The rendered SVG assumes some CSS specific parts to be available for the Music Font. Normally alphaTab adds
105114
a `style` tag to the page which loads the Music Symbol Font (Bravura) via as Web Font. Without these
106115
styles the SVG will not display the symbols correctly.
@@ -138,3 +147,266 @@ The related CSS template is:
138147
overflow: visible !important;
139148
}
140149
```
150+
151+
## 5. PNG rendering <SinceBadge since="1.3.0" inline="true" />
152+
153+
While SVG can be nice for some use-cases, having PNGs as output is easier to handle in some environments.
154+
[`alphaSkia`](https://github.com/CoderLine/alphaSkia) is a custom wrapper we created around [Skia](https://skia.org/) to have best compatibility and consistent rendering
155+
across platforms.
156+
157+
First we need to install main alphaSkia library via `npm install @coderline/alphaskia`.
158+
Second we need to install the native operating system dependencies (skia libs) on which we plan to run our application:
159+
160+
* `npm install @coderline/alphaskia-windows`
161+
* `npm install @coderline/alphaskia-linux`
162+
* `npm install @coderline/alphaskia-macos`
163+
164+
This done we can update our alphaTab code with following parts:
165+
166+
1. We connect alphaTab and alphaSkia to enable the raster graphics rendering.
167+
2. We load the fonts we plan to use into alphaSkia.
168+
3. We change the rendering settings to produce PNG with our custom fonts.
169+
3. We combine all individual PNGs into one final one and save it.
170+
171+
172+
### Connecting alphaTab and alphaSkia
173+
174+
To enable alphaSkia we can call `alphaTab.Environment.enableAlphaSkia` where we pass in the Bravura font data to have the music notation font available.
175+
176+
```js
177+
import * as alphaTab from '@coderline/alphatab';
178+
import * as fs from 'fs';
179+
import * as alphaSkia from '@coderline/alphaskia';
180+
181+
// 1. Initialize alphaSkia
182+
alphaTab.Environment.enableAlphaSkia(
183+
(await fs.promises.readFile('node_modules/@coderline/alphatab/dist/font/Bravura.ttf')).buffer /* needs an ArrayBuffer */,
184+
alphaSkia
185+
);
186+
```
187+
188+
### Load the fonts we plan to use.
189+
190+
We want to use some custom fonts in alphaTab for normal texts so we load them for later use.
191+
192+
```js
193+
import * as alphaTab from '@coderline/alphatab';
194+
import * as fs from 'fs';
195+
import * as alphaSkia from '@coderline/alphaskia';
196+
197+
// 1. Initialize alphaSkia
198+
alphaTab.Environment.enableAlphaSkia(
199+
(await fs.promises.readFile('node_modules/@coderline/alphatab/dist/font/Bravura.ttf')).buffer /* needs an ArrayBuffer */,
200+
alphaSkia
201+
);
202+
203+
// 2. Load custom fonts
204+
const fontFiles = [
205+
'font/roboto/Roboto-Regular.ttf',
206+
'font/roboto/Roboto-Italic.ttf',
207+
'font/roboto/Roboto-Bold.ttf',
208+
'font/roboto/Roboto-BoldItalic.ttf',
209+
'font/ptserif/PTSerif-Regular.ttf',
210+
'font/ptserif/PTSerif-Italic.ttf',
211+
'font/ptserif/PTSerif-Bold.ttf',
212+
'font/ptserif/PTSerif-BoldItalic.ttf',
213+
];
214+
215+
const fontInfo = [];
216+
for (const fontFile of fontFiles) {
217+
const fontData = await fs.promises.readFile(fontFile);
218+
fontInfo.push(alphaTab.Environment.registerAlphaSkiaCustomFont(new Uint8Array(fontData)));
219+
}
220+
221+
// 3. Load File
222+
const fileData = await fs.promises.readFile(process.argv[2]);
223+
const settings = new alphaTab.Settings();
224+
const score = alphaTab.importer.ScoreLoader.loadScoreFromBytes(
225+
new Uint8Array(fileData),
226+
settings
227+
);
228+
```
229+
230+
### Change the rendering settings to produce PNG with our custom fonts.
231+
232+
```js
233+
import * as alphaTab from '@coderline/alphatab';
234+
import * as fs from 'fs';
235+
import * as alphaSkia from '@coderline/alphaskia';
236+
237+
// 1. Initialize alphaSkia
238+
alphaTab.Environment.enableAlphaSkia(
239+
(await fs.promises.readFile('node_modules/@coderline/alphatab/dist/font/Bravura.ttf')).buffer /* needs an ArrayBuffer */,
240+
alphaSkia
241+
);
242+
243+
// 2. Load custom fonts
244+
const fontFiles = [
245+
'path-to-fonts/roboto/Roboto-Regular.ttf',
246+
'path-to-fonts/roboto/Roboto-Italic.ttf',
247+
'path-to-fonts/roboto/Roboto-Bold.ttf',
248+
'path-to-fonts/roboto/Roboto-BoldItalic.ttf',
249+
'path-to-fonts/ptserif/PTSerif-Regular.ttf',
250+
'path-to-fonts/ptserif/PTSerif-Italic.ttf',
251+
'path-to-fonts/ptserif/PTSerif-Bold.ttf',
252+
'path-to-fonts/ptserif/PTSerif-BoldItalic.ttf',
253+
];
254+
255+
const fontInfo = [];
256+
for (const fontFile of fontFiles) {
257+
const fontData = await fs.promises.readFile(fontFile);
258+
fontInfo.push(alphaTab.Environment.registerAlphaSkiaCustomFont(new Uint8Array(fontData)));
259+
}
260+
261+
// 3. Load File
262+
const fileData = await fs.promises.readFile(process.argv[2]);
263+
const settings = new alphaTab.Settings();
264+
const score = alphaTab.importer.ScoreLoader.loadScoreFromBytes(
265+
new Uint8Array(fileData),
266+
settings
267+
);
268+
269+
// 4. Setup renderer
270+
settings.core.engine = "skia"; // ask for skia rendering
271+
272+
const sansFontName = fontInfo[0].families;
273+
const serifFontName = fontInfo[4].families;
274+
275+
settings.display.resources.copyrightFont.families = sansFontName;
276+
settings.display.resources.titleFont.families = serifFontName;
277+
settings.display.resources.subTitleFont.families = serifFontName;
278+
settings.display.resources.wordsFont.families = serifFontName;
279+
settings.display.resources.effectFont.families = serifFontName;
280+
settings.display.resources.fretboardNumberFont.families = sansFontName;
281+
settings.display.resources.tablatureFont.families = sansFontName;
282+
settings.display.resources.graceFont.families = sansFontName;
283+
settings.display.resources.barNumberFont.families = sansFontName;
284+
settings.display.resources.fingeringFont.families = serifFontName;
285+
settings.display.resources.markerFont.families = serifFontName;
286+
287+
const renderer = new alphaTab.rendering.ScoreRenderer(settings);
288+
renderer.width = 1200;
289+
```
290+
291+
### Combine all individual PNGs into one final one and save it.
292+
293+
Here it gets a bit more tricky: To draw the final image, we need to know the final size of it.
294+
295+
Technically we could collect all partial results and draw the full image at the end but this might result
296+
in quite some memory consumption as all partials have to be kept in-memory until combining them. But we want to do better by
297+
drawing any partial directly into the final image and then cleanup the partial.
298+
299+
Therefore we cannot directly ask for drawing when the layouting finished, instead we first wait for the layouting to be fully finished to have the total size.
300+
Then we request all partials to be rendered. This logic is implemented in the following part:
301+
302+
303+
```js
304+
import * as alphaTab from '@coderline/alphatab';
305+
import * as fs from 'fs';
306+
import * as alphaSkia from '@coderline/alphaskia';
307+
308+
// 1. Initialize alphaSkia
309+
alphaTab.Environment.enableAlphaSkia(
310+
(await fs.promises.readFile('node_modules/@coderline/alphatab/dist/font/Bravura.ttf')).buffer /* needs an ArrayBuffer */,
311+
alphaSkia
312+
);
313+
314+
// 2. Load custom fonts
315+
const fontFiles = [
316+
'font/roboto/Roboto-Regular.ttf',
317+
'font/roboto/Roboto-Italic.ttf',
318+
'font/roboto/Roboto-Bold.ttf',
319+
'font/roboto/Roboto-BoldItalic.ttf',
320+
'font/ptserif/PTSerif-Regular.ttf',
321+
'font/ptserif/PTSerif-Italic.ttf',
322+
'font/ptserif/PTSerif-Bold.ttf',
323+
'font/ptserif/PTSerif-BoldItalic.ttf',
324+
];
325+
326+
const fontInfo = [];
327+
for (const fontFile of fontFiles) {
328+
const fontData = await fs.promises.readFile(fontFile);
329+
fontInfo.push(alphaTab.Environment.registerAlphaSkiaCustomFont(new Uint8Array(fontData)));
330+
}
331+
332+
// 3. Load File
333+
const fileData = await fs.promises.readFile(process.argv[2]);
334+
const settings = new alphaTab.Settings();
335+
const score = alphaTab.importer.ScoreLoader.loadScoreFromBytes(
336+
new Uint8Array(fileData),
337+
settings
338+
);
339+
340+
// 4. Setup renderer
341+
settings.core.engine = "skia"; // ask for skia rendering
342+
343+
const sansFontName = fontInfo[0].families;
344+
const serifFontName = fontInfo[4].families;
345+
346+
settings.display.resources.copyrightFont.families = sansFontName;
347+
settings.display.resources.titleFont.families = serifFontName;
348+
settings.display.resources.subTitleFont.families = serifFontName;
349+
settings.display.resources.wordsFont.families = serifFontName;
350+
settings.display.resources.effectFont.families = serifFontName;
351+
settings.display.resources.fretboardNumberFont.families = sansFontName;
352+
settings.display.resources.tablatureFont.families = sansFontName;
353+
settings.display.resources.graceFont.families = sansFontName;
354+
settings.display.resources.barNumberFont.families = sansFontName;
355+
settings.display.resources.fingeringFont.families = serifFontName;
356+
settings.display.resources.markerFont.families = serifFontName;
357+
358+
const renderer = new alphaTab.rendering.ScoreRenderer(settings);
359+
renderer.width = 1200;
360+
361+
// 5. Listen to Events
362+
363+
// clear on new rendering
364+
let partialIds = [];
365+
renderer.preRender.on((isResize) => {
366+
partialIds = [];
367+
});
368+
369+
// alphaTab separates "layouting" and "rendering" of the individual
370+
// partial results. in this case we want to render directly any partial layouted
371+
// (in UI scenarios the rendering might be delayed to the point when partials become visible)
372+
renderer.partialLayoutFinished.on((r) => {
373+
partialIds.push(r.id);
374+
});
375+
376+
// due to the historic behavior of `renderFinished` the name can be misleading. But this event is
377+
// fired as soon the layouting of the song finished and all partials are available and ready for rendering.
378+
// there is no event anymore indicating that all partials have been rendered.
379+
const finalImageCanvas = new alphaSkia.AlphaSkiaCanvas();
380+
renderer.renderFinished.on((r) => {
381+
finalImageCanvas.beginRender(r.totalWidth, r.totalHeight);
382+
// just for visibility in this demo we fill the canvas with a white background, but you can keep it transparent if you like
383+
finalImageCanvas.color = alphaSkia.AlphaSkiaCanvas.rgbaToColor(255, 255, 255, 255);
384+
finalImageCanvas.fillRect(0,0, r.totalWidth, r.totalHeight);
385+
386+
for(const id of partialIds) {
387+
renderer.renderResult(id);
388+
}
389+
});
390+
391+
// whenever the rendering of a partial is finished, we draw it into the final image.
392+
renderer.partialRenderFinished.on((r) => {
393+
finalImageCanvas.drawImage(r.renderResult, r.x, r.y, r.width, r.height);
394+
395+
// free the partial image and release the memory used by it
396+
r.renderResult[Symbol.dispose]();
397+
});
398+
399+
// 6. Fire off rendering, this is synchronous so we can assume all results to be available after this call.
400+
renderer.renderScore(score, [0]);
401+
402+
// 7. save the final image as png
403+
const finalImage = finalImageCanvas.endRender();
404+
const outputFilePath = process.argv[2] + '.png';
405+
// encode as PNG and free the memory of the final image
406+
const png = finalImage.toPng();
407+
finalImage[Symbol.dispose]();
408+
finalImageCanvas[Symbol.dispose]();
409+
await fs.promises.writeFile(outputFilePath, new Uint8Array(png));
410+
```
411+
412+
<img src="/img/guides/nodejs/png.png" />
21.3 KB
Loading

static/img/guides/nodejs/png.png

102 KB
Loading

static/img/guides/nodejs/svg.png

25.9 KB
Loading

0 commit comments

Comments
 (0)