Player

Basic setup

Now as we have the basic component ready, it is time to add some sound. AlphaTab comes with an audio synthesis engine named AlphaSynth to play the songs via Midi and a SoundFont2 file.

To get the player configured is also quite easy:

  1. We tell alphaTab that we want the player active and where to find the SoundFont2 file that should be used. alphaTab ships a free SoundFont2 file for usage on your website.
  2. We tell alphaTab how the cursors should look like
  3. We add some UI elements for the user to start/pause the playback and show the current position.
const settings = {
file: "https://www.alphatab.net/files/canon.gp",
player: {
enablePlayer: true,
soundFont: 'https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/soundfont/sonivox.sf2'
}
};
const api = new alphaTab.AlphaTabApi(main, settings);

Loading indicator

Just like alphaTab, alphaSynth might need some time to initialize and load all required data. Especially the SoundFonts can become quite big and might need some time to load. So we will add a small loading indicator to our footer so that the user will know once the player is ready for usage.

It is better to have an own loading indicator for the player to allow users already having a view at the music notation. From a typical user behavior they will first start looking a bit through the music sheet before pressing play. If we keep the main loading indicator for the music sheet visible until the player is ready, the users to not get the chance to have a look.

To keep the tutorial simple we will just show a percentage value but you might want to show some more nice looking progress indicator on your site.

We add a new container to the controls section and add a placeholder in which we will show the percentage value.

<div class="at-controls">
<div class="at-controls-left">
<span class="at-player-progress">0%</span>
...

Main player controls

Now we need to add the required controls for the users to play and pause the audio. We will add one button for play/pause which will change the icon depending on the current playback state. And we will also add one button which stops the playback and moves the cursor back to start.

We will use the playPause() and stop() to send the right signals to alphaTab and we use the playerStateChanged event to update the button icon. To ensure that the user does not interact with the player before it is ready, we will keep the buttons disabled until the player is ready.

Here we have the 2 new buttons:

<div class="at-controls">
<div class="at-controls-left">
<a class="btn at-player-stop disabled">
<i class="fas fa-step-backward"></i>
</a>
<a class="btn at-player-play-pause disabled">
<i class="fas fa-play"></i>
</a>
<span class="at-player-progress">0%</span>
...

The result looks like this:

Showing the current time

In this step we will add a textual indicator the current position of the playback. You might use the provided information to also build a progress bar indicator for the users which could be even interactive with seeking. This tutorial will skip this part.

As in most other steps we will add some new DOM elements, corresponding CSS and then hook it up with some the alphaTab event playerPositionChanged providing the necessary data.

...
<div class="at-song-info">
<span class="at-song-title"></span> -
<span class="at-song-artist"></span>
</div>
<div class="at-song-position">00:00 / 00:00</div>
...

Final File

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>AlphaTab Tutorial</title>
<script src="https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/alphaTab.js"></script>
<script src="https://kit.fontawesome.com/b43f0e512e.js"></script>
<style type="text/css">
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
}
.at-wrap {
width: 80vw;
height: 80vh;
margin: 0 auto;
border: 1px solid rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.at-content {
position: relative;
overflow: hidden;
flex: 1 1 auto;
}
/** Sidebar **/
.at-sidebar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
max-width: 70px;
width: auto;
display: flex;
align-content: stretch;
z-index: 1001;
overflow: hidden;
border-right: 1px solid rgba(0, 0, 0, 0.12);
background: #f7f7f7;
}
.at-sidebar:hover {
max-width: 400px;
transition: max-width 0.2s;
overflow-y: auto;
}
.at-viewport {
overflow-y: auto;
position: absolute;
top: 0;
left: 70px;
right: 0;
bottom: 0;
padding-right: 20px;
}
.at-footer {
flex: 0 0 auto;
background: #436d9d;
color: #fff;
}
/** Overlay **/
.at-overlay {
/** Fill Parent */
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1002;
/* Blurry dark shade */
backdrop-filter: blur(3px);
background: rgba(0, 0, 0, 0.5);
/** center content */
display: flex;
justify-content: center;
align-items: flex-start;
}
.at-overlay-content {
/* white box with drop-shadow */
margin-top: 20px;
background: #fff;
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
padding: 10px;
}
/** Track selector **/
.at-track {
display: flex;
position: relative;
padding: 5px;
transition: background 0.2s;
cursor: pointer;
}
.at-track:hover {
background: rgba(0, 0, 0, 0.1);
}
.at-track > .at-track-icon,
.at-track > .at-track-details {
display: flex;
flex-direction: column;
justify-content: center;
}
.at-track > .at-track-icon {
flex-shrink: 0;
font-size: 32px;
opacity: 0.5;
transition: opacity 0.2s;
width: 64px;
height: 64px;
margin-right: 5px;
align-items: center;
}
.at-track-name {
font-weight: bold;
margin-bottom: 5px;
}
.at-track:hover > .at-track-icon {
opacity: 0.8;
}
.at-track.active {
background: rgba(0, 0, 0, 0.03);
}
.at-track.active > .at-track-icon {
color: #4972a1;
opacity: 1;
}
.at-track > .at-track-name {
font-weight: 500;
}
/** Footer **/
.at-controls {
flex: 0 0 auto;
display: flex;
justify-content: space-between;
background: #436d9d;
color: #fff;
}
.at-controls > div {
display: flex;
justify-content: flex-start;
align-content: center;
align-items: center;
}
.at-controls > div > * {
display: flex;
text-align: center;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 4px;
margin: 0 3px;
}
.at-controls .btn {
color: #fff;
border-radius: 0;
height: 40px;
width: 40px;
height: 40px;
font-size: 16px;
}
.at-controls .btn.disabled {
cursor: progress;
opacity: 0.5;
}
.at-controls a.active {
background: #5588c7;
text-decoration: none;
}
.at-controls .btn i {
vertical-align: top;
}
.at-controls select {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
border: none;
width: 100%;
height: 40px;
background: #436d9d;
padding: 4px 10px;
color: #fff;
font-size: 16px;
text-align-last: center;
text-align: center;
-ms-text-align-last: center;
-moz-text-align-last: center;
cursor: pointer;
}
.at-song-title {
font-weight: bold;
}
.at-cursor-bar {
/* Defines the color of the bar background when a bar is played */
background: rgba(255, 242, 0, 0.25);
}
.at-selection div {
/* Defines the color of the selection background */
background: rgba(64, 64, 255, 0.1);
}
.at-cursor-beat {
/* Defines the beat cursor */
background: rgba(64, 64, 255, 0.75);
width: 3px;
}
.at-highlight * {
/* Defines the color of the music symbols when they are being played (svg) */
fill: #0078ff;
stroke: #0078ff;
}
</style>
</head>
<body>
<div class="at-wrap">
<div class="at-overlay">
<div class="at-overlay-content">
Music sheet is loading
</div>
</div>
<div class="at-content">
<div class="at-sidebar">
<div class="at-sidebar-content">
<div class="at-track-list"></div>
</div>
</div>
<div class="at-viewport">
<div class="at-main"></div>
</div>
</div>
<div class="at-controls">
<div class="at-controls-left">
<a class="btn at-player-stop disabled">
<i class="fas fa-step-backward"></i>
</a>
<a class="btn at-player-play-pause disabled">
<i class="fas fa-play"></i>
</a>
<span class="at-player-progress">0%</span>
<div class="at-song-info">
<span class="at-song-title"></span> -
<span class="at-song-artist"></span>
</div>
<div class="at-song-position">00:00 / 00:00</div>
</div>
<div class="at-controls-right">
<a class="btn at-metronome">
<i class="fas fa-edit"></i>
</a>
<a class="btn at-loop">
<i class="fas fa-retweet"></i>
</a>
<a class="btn at-print">
<i class="fas fa-print"></i>
</a>
<div class="at-zoom">
<i class="fas fa-search"></i>
<select>
<option value="25">25%</option>
<option value="50">50%</option>
<option value="75">75%</option>
<option value="90">90%</option>
<option value="100" selected>100%</option>
<option value="110">110%</option>
<option value="125">125%</option>
<option value="150">150%</option>
<option value="200">200%</option>
</select>
</div>
<div class="at-layout">
<select>
<option value="horizontal">Horizontal</option>
<option value="page" selected>Page</option>
</select>
</div>
</div>
</div>
</div>
<template id="at-track-template">
<div class="at-track">
<div class="at-track-icon">
<i class="fas fa-guitar"></i>
</div>
<div class="at-track-details">
<div class="at-track-name"></div>
</div>
</div>
</template>
<script type="text/javascript">
// load elements
const wrapper = document.querySelector(".at-wrap");
const main = wrapper.querySelector(".at-main");
// initialize alphatab
const settings = {
file: "https://www.alphatab.net/files/canon.gp",
player: {
enablePlayer: true,
soundFont:
"https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/soundfont/sonivox.sf2",
},
};
const api = new alphaTab.AlphaTabApi(main, settings);
// overlay logic
const overlay = wrapper.querySelector(".at-overlay");
api.renderStarted.on(() => {
overlay.style.display = "flex";
});
api.renderFinished.on(() => {
overlay.style.display = "none";
});
// track selector
function createTrackItem(track) {
const trackItem = document
.querySelector("#at-track-template")
.content.cloneNode(true).firstElementChild;
trackItem.querySelector(".at-track-name").innerText = track.name;
trackItem.track = track;
trackItem.onclick = (e) => {
e.stopPropagation();
api.renderTracks([track]);
};
return trackItem;
}
const trackList = wrapper.querySelector(".at-track-list");
api.scoreLoaded.on((score) => {
// clear items
trackList.innerHTML = "";
// generate a track item for all tracks of the score
score.tracks.forEach((track) => {
trackList.appendChild(createTrackItem(track));
});
});
api.renderStarted.on(() => {
// collect tracks being rendered
const tracks = new Map();
api.tracks.forEach((t) => {
tracks.set(t.index, t);
});
// mark the item as active or not
const trackItems = trackList.querySelectorAll(".at-track");
trackItems.forEach((trackItem) => {
if (tracks.has(trackItem.track.index)) {
trackItem.classList.add("active");
} else {
trackItem.classList.remove("active");
}
});
});
/** Controls **/
api.scoreLoaded.on((score) => {
wrapper.querySelector(".at-song-title").innerText = score.title;
wrapper.querySelector(".at-song-artist").innerText = score.artist;
});
const metronome = wrapper.querySelector(".at-controls .at-metronome");
metronome.onclick = () => {
metronome.classList.toggle("active");
if (metronome.classList.contains("active")) {
api.metronomeVolume = 1;
} else {
api.metronomeVolume = 0;
}
};
const loop = wrapper.querySelector(".at-controls .at-loop");
loop.onclick = () => {
loop.classList.toggle("active");
api.isLooping = loop.classList.contains("active");
};
wrapper.querySelector(".at-controls .at-print").onclick = () => {
api.print();
};
const zoom = wrapper.querySelector(".at-controls .at-zoom select");
zoom.onchange = () => {
const zoomLevel = parseInt(zoom.value) / 100;
api.settings.display.scale = zoomLevel;
api.updateSettings();
api.render();
};
const layout = wrapper.querySelector(".at-controls .at-layout select");
layout.onchange = () => {
switch (layout.value) {
case "horizontal":
api.settings.display.layoutMode = alphaTab.LayoutMode.Horizontal;
break;
case "page":
api.settings.display.layoutMode = alphaTab.LayoutMode.Page;
break;
}
api.updateSettings();
api.render();
};
// player loading indicator
const playerIndicator = wrapper.querySelector(
".at-controls .at-player-progress"
);
api.soundFontLoad.on((e) => {
const percentage = Math.floor((e.loaded / e.total) * 100);
playerIndicator.innerText = percentage + "%";
});
api.playerReady.on(() => {
playerIndicator.style.display = "none";
});
// main player controls
const playPause = wrapper.querySelector(
".at-controls .at-player-play-pause"
);
const stop = wrapper.querySelector(".at-controls .at-player-stop");
playPause.onclick = (e) => {
if (e.target.classList.contains("disabled")) {
return;
}
api.playPause();
};
stop.onclick = (e) => {
if (e.target.classList.contains("disabled")) {
return;
}
api.stop();
};
api.playerReady.on(() => {
playPause.classList.remove("disabled");
stop.classList.remove("disabled");
});
api.playerStateChanged.on((e) => {
const icon = playPause.querySelector("i.fas");
if (e.state === alphaTab.synth.PlayerState.Playing) {
icon.classList.remove("fa-play");
icon.classList.add("fa-pause");
} else {
icon.classList.remove("fa-pause");
icon.classList.add("fa-play");
}
});
// song position
function formatDuration(milliseconds) {
let seconds = milliseconds / 1000;
const minutes = (seconds / 60) | 0;
seconds = (seconds - minutes * 60) | 0;
return (
String(minutes).padStart(2, "0") +
":" +
String(seconds).padStart(2, "0")
);
}
const songPosition = wrapper.querySelector(".at-song-position");
let previousTime = -1;
api.playerPositionChanged.on((e) => {
// reduce number of UI updates to second changes.
const currentSeconds = (e.currentTime / 1000) | 0;
if (currentSeconds == previousTime) {
return;
}
songPosition.innerText =
formatDuration(e.currentTime) + " / " + formatDuration(e.endTime);
});
</script>
</body>
</html>