diff --git a/playground-template/control.mjs b/playground-template/control.mjs index 3820d2c77..2a932b539 100644 --- a/playground-template/control.mjs +++ b/playground-template/control.mjs @@ -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) { diff --git a/playground-template/youtube.css b/playground-template/youtube.css new file mode 100644 index 000000000..a69ea898d --- /dev/null +++ b/playground-template/youtube.css @@ -0,0 +1,10 @@ + +.at-wrap { + height: calc(90vh - 360px); + margin: 0; +} +.youtube-wrap { + height: 360px; + display: flex; + justify-content: center; +} diff --git a/playground-template/youtube.html b/playground-template/youtube.html new file mode 100644 index 000000000..4a560c00c --- /dev/null +++ b/playground-template/youtube.html @@ -0,0 +1,144 @@ + + + + + + AlphaTab Control Demo + + + + + + + + + + + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index dbdbd985a..83fa84ffe 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -931,9 +931,6 @@ export class AlphaTabApiBase { return this.renderer.boundsLookup; } - // the previously configured player mode to detect changes - private _playerMode: PlayerMode = PlayerMode.Disabled; - /** * The alphaSynth player used for playback. * @remarks @@ -1455,7 +1452,7 @@ export class AlphaTabApiBase { } } - if (mode !== this._playerMode) { + if (mode !== this._actualPlayerMode) { this.destroyPlayer(); } this.updateCursors(); @@ -1463,7 +1460,6 @@ export class AlphaTabApiBase { this._actualPlayerMode = mode; switch (mode) { case PlayerMode.Disabled: - this._playerMode = PlayerMode.Disabled; this.destroyPlayer(); return false; diff --git a/src/synth/ExternalMediaPlayer.ts b/src/synth/ExternalMediaPlayer.ts index d59788d01..3a0c08148 100644 --- a/src/synth/ExternalMediaPlayer.ts +++ b/src/synth/ExternalMediaPlayer.ts @@ -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).trigger(currentTime); + (this.timeUpdate as EventEmitterOfT).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 = new EventEmitterOfT(); public readonly timeUpdate: IEventEmitterOfT = new EventEmitterOfT(); public readonly sampleRequest: IEventEmitter = new EventEmitter(); public async enumerateOutputDevices(): Promise { - const empty:ISynthOutputDevice[] = []; + const empty: ISynthOutputDevice[] = []; return empty; } public async setOutputDevice(_device: ISynthOutputDevice | null): Promise {} @@ -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); }