Skip to content
Closed
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
59 changes: 39 additions & 20 deletions src/AlphaTabApiBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ export class AlphaTabApiBase<TSettings> {
});
this.player.stateChanged.on(e => {
this._playerState = e.state;
if (!e.stopped && e.state === PlayerState.Paused) {
if (!e.stopped && e.state === PlayerState.Paused && this.settings.player.seekToBeatStartOnPause) {
let currentBeat = this._currentBeat;
let tickCache = this._tickCache;
if (currentBeat && tickCache) {
Expand All @@ -885,14 +885,26 @@ export class AlphaTabApiBase<TSettings> {
* @param stop
* @param shouldScroll whether we should scroll to the bar (if scrolling is active)
*/
private cursorUpdateTick(tick: number, stop: boolean, shouldScroll: boolean = false, forceUpdate:boolean = false): void {
private cursorUpdateTick(
tick: number,
stop: boolean,
shouldScroll: boolean = false,
forceUpdate: boolean = false
): void {
this._previousTick = tick;

let cache: MidiTickLookup | null = this._tickCache;
if (cache) {
let tracks = this._trackIndexLookup;
if (tracks != null && tracks.size > 0) {
let beat: MidiTickLookupFindBeatResult | null = cache.findBeat(tracks, tick, this._currentBeat);
if (beat) {
this.cursorUpdateBeat(beat, stop, shouldScroll, forceUpdate || this.playerState === PlayerState.Paused);
this.cursorUpdateBeat(
beat,
stop,
shouldScroll,
forceUpdate || this.playerState === PlayerState.Paused
);
}
}
}
Expand Down Expand Up @@ -1053,12 +1065,33 @@ export class AlphaTabApiBase<TSettings> {
barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
}

let nextBeatX: number = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
// get position of next beat on same system
if (nextBeat && cursorMode == MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
// if we are moving within the same bar or to the next bar
// transition to the next beat, otherwise transition to the end of the bar.
let nextBeatBoundings: BeatBounds | null = cache.findBeat(nextBeat);
if (
nextBeatBoundings &&
nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds
) {
nextBeatX = nextBeatBoundings.onNotesX;
}
}

let startBeatX = beatBoundings.onNotesX;
if (beatCursor) {
// move beat to start position immediately
// relative positioning of the cursor
if (this.settings.player.enableAnimatedBeatCursor) {
beatCursor.stopAnimation();
const animationWidth = nextBeatX - beatBoundings.onNotesX;
const relativePosition = this._previousTick - this._currentBeat!.start;
const ratioPosition = relativePosition / this._currentBeat!.tickDuration;
startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition;
duration -= duration * ratioPosition;
beatCursor.transitionToX(0, startBeatX);
}
beatCursor.setBounds(beatBoundings.onNotesX, barBounds.y, 1, barBounds.h);

beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
}

// if playing, animate the cursor to the next beat
Expand All @@ -1082,20 +1115,6 @@ export class AlphaTabApiBase<TSettings> {
}

if (this.settings.player.enableAnimatedBeatCursor && beatCursor) {
let nextBeatX: number = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
// get position of next beat on same system
if (nextBeat && cursorMode == MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
// if we are moving within the same bar or to the next bar
// transition to the next beat, otherwise transition to the end of the bar.
let nextBeatBoundings: BeatBounds | null = cache.findBeat(nextBeat);
if (
nextBeatBoundings &&
nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds
) {
nextBeatX = nextBeatBoundings.onNotesX;
}
}

if (isPlayingUpdate) {
// we need to put the transition to an own animation frame
// otherwise the stop animation above is not applied.
Expand Down
11 changes: 11 additions & 0 deletions src/PlayerSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,15 @@ export class PlayerSettings {
* Smaller buffers can cause audio crackling due to constant buffering that is happening.
*/
public bufferTimeInMilliseconds:number = 500;

/**
* Whether alphaTab should seek back to the start of the currently played beat when the playback is paused.
* This will properly play the whole beat again when playback is restarted.
* When set to false the player will remain on the exact playback position which can result in slightly strange audio
* due buffering and the way samples are creating physical sound on speakers.
* When {@link enableAnimatedBeatCursor} is enabled, it is attempted to place the cursor at the relative position matching
* the exact playback position.
* @json_read_only
*/
public seekToBeatStartOnPause: boolean = true;
}
10 changes: 10 additions & 0 deletions src/generated/PlayerSettingsJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,14 @@ export interface PlayerSettingsJson {
* Smaller buffers can cause audio crackling due to constant buffering that is happening.
*/
bufferTimeInMilliseconds?: number;
/**
* Whether alphaTab should seek back to the start of the currently played beat when the playback is paused.
* This will properly play the whole beat again when playback is restarted.
* When set to false the player will remain on the exact playback position which can result in slightly strange audio
* due buffering and the way samples are creating physical sound on speakers.
* When {@link enableAnimatedBeatCursor} is enabled, it is attempted to place the cursor at the relative position matching
* the exact playback position.
* @json_read_only
*/
seekToBeatStartOnPause?: boolean;
}
3 changes: 3 additions & 0 deletions src/generated/PlayerSettingsSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ export class PlayerSettingsSerializer {
case "buffertimeinmilliseconds":
obj.bufferTimeInMilliseconds = v! as number;
return true;
case "seektobeatstartonpause":
obj.seekToBeatStartOnPause = v! as boolean;
return true;
}
if (["vibrato"].indexOf(property) >= 0) {
VibratoPlaybackSettingsSerializer.fromJson(obj.vibrato, v as Map<string, unknown>);
Expand Down
45 changes: 22 additions & 23 deletions src/platform/javascript/AlphaSynthAudioWorkletOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface AudioWorkletProcessor {
*/
declare var AudioWorkletProcessor: {
prototype: AudioWorkletProcessor;
new(options?: AudioWorkletNodeOptions): AudioWorkletProcessor;
new (options?: AudioWorkletNodeOptions): AudioWorkletProcessor;
};

// Bug 646: Safari 14.1 is buggy regarding audio worklets
Expand Down Expand Up @@ -64,8 +64,8 @@ export class AlphaSynthWebWorklet {

this._bufferCount = Math.floor(
(options.processorOptions.bufferTimeInMilliseconds * sampleRate) /
1000 /
AlphaSynthWebWorkletProcessor.BufferSize
1000 /
AlphaSynthWebWorkletProcessor.BufferSize
);
this._circularBuffer = new CircularSampleBuffer(
AlphaSynthWebWorkletProcessor.BufferSize * this._bufferCount
Expand Down Expand Up @@ -126,13 +126,13 @@ export class AlphaSynthWebWorklet {
right[i] = buffer[s++];
}

if(samplesFromBuffer < left.length) {
for(let i = samplesFromBuffer; i < left.length; i++) {
if (samplesFromBuffer < left.length) {
for (let i = samplesFromBuffer; i < left.length; i++) {
left[i] = 0;
right[i] = 0;
}
}

this.port.postMessage({
cmd: AlphaSynthWorkerSynthOutput.CmdOutputSamplesPlayed,
samples: samplesFromBuffer / SynthConstants.AudioChannels
Expand Down Expand Up @@ -185,11 +185,7 @@ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase {
public override open(bufferTimeInMilliseconds: number) {
super.open(bufferTimeInMilliseconds);
this._bufferTimeInMilliseconds = bufferTimeInMilliseconds;
this.onReady();
}

public override play(): void {
super.play();
let ctx = this._context!;
// create a script processor node which will replace the silence with the generated audio
Environment.createAudioWorklet(ctx, this._settings).then(
Expand All @@ -202,16 +198,31 @@ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase {
}
});
this._worklet.port.onmessage = this.handleMessage.bind(this);
this._source!.connect(this._worklet);
this._source!.start(0);
this._worklet.connect(ctx!.destination);
this.onReady();
},
reason => {
Logger.error('WebAudio', `Audio Worklet creation failed: reason=${reason}`);
}
);
}

protected override reconnectSourceNode(): void {
this._source!.connect(this._worklet!);
}

public override destroy(): void {
if (this._worklet) {
this._worklet.port.postMessage({
cmd: AlphaSynthWorkerSynthOutput.CmdOutputStop
});
this._worklet.port.onmessage = null;
this._worklet.disconnect();
}
super.destroy();
}

private handleMessage(e: MessageEvent) {
let data: any = e.data;
let cmd: any = data.cmd;
Expand All @@ -225,18 +236,6 @@ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase {
}
}

public override pause(): void {
super.pause();
if (this._worklet) {
this._worklet.port.postMessage({
cmd: AlphaSynthWorkerSynthOutput.CmdOutputStop
});
this._worklet.port.onmessage = null;
this._worklet.disconnect();
}
this._worklet = null;
}

public addSamples(f: Float32Array): void {
this._worklet?.port.postMessage({
cmd: AlphaSynthWorkerSynthOutput.CmdOutputAddSamples,
Expand Down
12 changes: 2 additions & 10 deletions src/platform/javascript/AlphaSynthScriptProcessorOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ export class AlphaSynthScriptProcessorOutput extends AlphaSynthWebAudioOutputBas
);
this._circularBuffer = new CircularSampleBuffer(AlphaSynthWebAudioOutputBase.BufferSize * this._bufferCount);
this.onReady();
}

public override play(): void {
super.play();
let ctx = this._context!;
// create a script processor node which will replace the silence with the generated audio
this._audioNode = ctx.createScriptProcessor(4096, 0, 2);
Expand All @@ -35,17 +32,12 @@ export class AlphaSynthScriptProcessorOutput extends AlphaSynthWebAudioOutputBas
this._source = ctx.createBufferSource();
this._source.buffer = this._buffer;
this._source.loop = true;
this._source.connect(this._audioNode, 0, 0);
this._source.start(0);
this._audioNode.connect(ctx.destination, 0, 0);
}

public override pause(): void {
super.pause();
if (this._audioNode) {
this._audioNode.disconnect(0);
}
this._audioNode = null;
protected override reconnectSourceNode(): void {
this._source!.connect(this._audioNode!, 0, 0);
}

public addSamples(f: Float32Array): void {
Expand Down
16 changes: 8 additions & 8 deletions src/platform/javascript/AlphaSynthWebAudioOutputBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ export abstract class AlphaSynthWebAudioOutputBase implements ISynthOutput {
if (ctx.state === 'suspended') {
this.registerResumeHandler();
}

this._buffer = ctx.createBuffer(2, AlphaSynthWebAudioOutputBase.BufferSize, ctx.sampleRate);
this._source = ctx.createBufferSource();
this._source!.buffer = this._buffer;
this._source!.loop = true;
}

private registerResumeHandler() {
Expand All @@ -120,21 +125,16 @@ export abstract class AlphaSynthWebAudioOutputBase implements ISynthOutput {
}

public play(): void {
let ctx = this._context!;
this.activate();
// create an empty buffer source (silence)
this._buffer = ctx.createBuffer(2, AlphaSynthWebAudioOutputBase.BufferSize, ctx.sampleRate);
this._source = ctx.createBufferSource();
this._source.buffer = this._buffer;
this._source.loop = true;
this.reconnectSourceNode();
}

protected abstract reconnectSourceNode(): void;

public pause(): void {
if (this._source) {
this._source.stop(0);
this._source.disconnect();
}
this._source = null;
}

public destroy(): void {
Expand Down