Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions playground-template/control.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,13 @@ export function setupControl(selector, customSettings) {

const settings = new alphaTab.Settings();
applyFonts(settings);
settings.fillFromJson(defaultSettings);
settings.fillFromJson({
...defaultSettings,
player: {
...defaultSettings.player,
scrollElement: viewPort
},
...customSettings
});
settings.fillFromJson(customSettings);

const at = new alphaTab.AlphaTabApi(el, settings);
at.error.on(function (e) {
Expand Down
10 changes: 10 additions & 0 deletions playground-template/youtube.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

.at-wrap {
height: calc(90vh - 360px);
margin: 0;
}
.youtube-wrap {
height: 360px;
display: flex;
justify-content: center;
}
144 changes: 144 additions & 0 deletions playground-template/youtube.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<title>AlphaTab Control Demo</title>

<script src="/node_modules/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="/node_modules/bootstrap/dist/js/bootstrap.min.js"></script>

<script src="/node_modules/handlebars/dist/handlebars.min.js"></script>

<link rel="stylesheet" href="control.css" />
<link rel="stylesheet" href="youtube.css" />
</head>

<body>
<div class="youtube-wrap">
<div id="youtube"></div>
</div>
<div id="placeholder"></div>

<script type="module">
import { setupControl } from './control.mjs';
import * as alphaTab from '../src/alphaTab.main';

const playerElement = document.getElementById('youtube');
const tag = document.createElement('script');
tag.src = "https://www.youtube.com/player_api";
playerElement.parentNode.insertBefore(tag, playerElement);

const youtubeApiReady = Promise.withResolvers();
window.onYouTubePlayerAPIReady = youtubeApiReady.resolve;

const req = new XMLHttpRequest();
req.onload = async (data) => {
document.getElementById('placeholder').outerHTML = req.responseText;
const at = setupControl('#alphaTab', {
core: {
file: '/test-data/guitarpro8/canon-audio-track.gp'
},
player: {
playerMode: alphaTab.PlayerMode.EnabledExternalMedia
}
});
window.at = at;


//
// Youtube

// Wait for Youtube API
await youtubeApiReady.promise; // wait for API

// Setup youtube player
const youtubePlayerReady = Promise.withResolvers();
let currentTimeInterval = undefined;
const player = new YT.Player(playerElement, {
height: '360',
width: '640',
videoId: 'by8oyJztzwo',
playerVars: { 'autoplay': 0 },
events: {
'onReady': (e) => {
youtubePlayerReady.resolve();
console.log('YT onReady', player, e)
},
'onStateChange': (e) => {
switch (e.data) {
case YT.PlayerState.PLAYING:
currentTimeInterval = setInterval(() => {
at.player.output.updatePosition(player.getCurrentTime() * 1000)
}, 50);
at.play();
break;
case YT.PlayerState.ENDED:
clearInterval(currentTimeInterval);
at.stop();
break;
case YT.PlayerState.PAUSED:
clearInterval(currentTimeInterval);
at.pause();
break;
default:
break;
}
},
'onPlaybackRateChange': (e) => {
at.playbackRate = e.data;
},
'onError': (e) => {
youtubePlayerReady.reject(e);
console.log('YT onError', player, e)
},
}
});

await youtubePlayerReady.promise;

// Setup alphaTab with youtube handler
const alphaTabYoutubeHandler = {
get backingTrackDuration() {
return player ? player?.getDuration() * 1000 : 0;
},
get playbackRate() {
return player ? player.getPlaybackRate() : 1;
},
set playbackRate(value) {
if (player) {
player.setPlaybackRate(value);
}
},
get masterVolume() {
return player ? player.getVolume() / 100 : 1;
},
set masterVolume(value) {
if (player) {
player.setVolume(value * 100);
}
},
seekTo(time) {
if (player) {
player.seekTo(time / 1000);
}
},
play() {
if (player) {
player.playVideo();
}
},
pause() {
if (player) {
player.pauseVideo();
}
}
};
at.player.output.handler = alphaTabYoutubeHandler;
};
req.open('GET', 'control-template.html');
req.send();
</script>
</body>

</html>
6 changes: 1 addition & 5 deletions src/AlphaTabApiBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,9 +931,6 @@ export class AlphaTabApiBase<TSettings> {
return this.renderer.boundsLookup;
}

// the previously configured player mode to detect changes
private _playerMode: PlayerMode = PlayerMode.Disabled;

/**
* The alphaSynth player used for playback.
* @remarks
Expand Down Expand Up @@ -1455,15 +1452,14 @@ export class AlphaTabApiBase<TSettings> {
}
}

if (mode !== this._playerMode) {
if (mode !== this._actualPlayerMode) {
this.destroyPlayer();
}
this.updateCursors();

this._actualPlayerMode = mode;
switch (mode) {
case PlayerMode.Disabled:
this._playerMode = PlayerMode.Disabled;
this.destroyPlayer();
return false;

Expand Down
116 changes: 98 additions & 18 deletions src/synth/ExternalMediaPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,120 @@ import type { BackingTrack } from '@src/model/BackingTrack';
import { type IBackingTrackSynthOutput, BackingTrackPlayer } from '@src/synth/BackingTrackPlayer';
import type { ISynthOutputDevice } from '@src/synth/ISynthOutput';

export interface IExternalMediaHandler {
/**
* The total duration of the backing track in milliseconds.
*/
readonly backingTrackDuration: number;
/**
* The playback rate at which the output should playback.
*/
playbackRate: number;
/**
* The volume at which the output should play (0-1)
*/
masterVolume: number;
/**
* Instructs the output to seek to the given time position.
* @param time The absolute time in milliseconds.
*/
seekTo(time: number): void;

play(): void;
pause(): void;
}

class ExternalMediaSynthOutput implements IBackingTrackSynthOutput {
// fake rate
public readonly sampleRate: number = 44100;

public backingTrackDuration: number = 0;
public playbackRate: number = 1;
public masterVolume: number = 1;
private _padding: number = 0;
private _seekPosition: number = 0;

private _handler?: IExternalMediaHandler;

public get handler(): IExternalMediaHandler | undefined {
return this._handler;
}

public set handler(value: IExternalMediaHandler | undefined) {
if (value) {
if (this._seekPosition !== 0) {
value.seekTo(this._seekPosition);
this._seekPosition = 0;
}
}

this._handler = value;
}

public get backingTrackDuration() {
return this.handler?.backingTrackDuration ?? 0;
}

public get playbackRate(): number {
return this.handler?.playbackRate ?? 1;
}

public set playbackRate(value: number) {
const handler = this.handler;
if (handler) {
handler.playbackRate = value;
}
}

public get masterVolume(): number {
return this.handler?.masterVolume ?? 1;
}

public set masterVolume(value: number) {
const handler = this.handler;
if (handler) {
handler.masterVolume = value;
}
}

public seekTo(time: number): void {}
public seekTo(time: number): void {
const handler = this.handler;
if (handler) {
handler.seekTo(time - this._padding);
} else {
this._seekPosition = time - this._padding;
}
}

public loadBackingTrack(backingTrack: BackingTrack) {}
public loadBackingTrack(backingTrack: BackingTrack) {
this._padding = backingTrack.padding;
}

public open(_bufferTimeInMilliseconds: number): void {
(this.ready as EventEmitter).trigger();
}

public updatePosition(currentTime: number) {
(this.timeUpdate as EventEmitterOfT<number>).trigger(currentTime);
(this.timeUpdate as EventEmitterOfT<number>).trigger(currentTime + this._padding);
}

public play(): void {}
public play(): void {
this.handler?.play();
}
public destroy(): void {}

public pause(): void {}

public addSamples(_samples: Float32Array): void {
// nobody will call this
}
public resetSamples(): void {
// nobody will call this
}
public activate(): void {
// nobody will call this
public pause(): void {
this.handler?.pause();
}

public addSamples(_samples: Float32Array): void {}
public resetSamples(): void {}
public activate(): void {}

public readonly ready: IEventEmitter = new EventEmitter();
public readonly samplesPlayed: IEventEmitterOfT<number> = new EventEmitterOfT<number>();
public readonly timeUpdate: IEventEmitterOfT<number> = new EventEmitterOfT<number>();
public readonly sampleRequest: IEventEmitter = new EventEmitter();

public async enumerateOutputDevices(): Promise<ISynthOutputDevice[]> {
const empty:ISynthOutputDevice[] = [];
const empty: ISynthOutputDevice[] = [];
return empty;
}
public async setOutputDevice(_device: ISynthOutputDevice | null): Promise<void> {}
Expand All @@ -55,6 +127,14 @@ class ExternalMediaSynthOutput implements IBackingTrackSynthOutput {
}

export class ExternalMediaPlayer extends BackingTrackPlayer {
public get handler(): IExternalMediaHandler | undefined {
return (this.output as ExternalMediaSynthOutput).handler;
}

public set handler(value: IExternalMediaHandler | undefined) {
(this.output as ExternalMediaSynthOutput).handler = value;
}

constructor(bufferTimeInMilliseconds: number) {
super(new ExternalMediaSynthOutput(), bufferTimeInMilliseconds);
}
Expand Down