Skip to content
Open
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
6 changes: 5 additions & 1 deletion packages/alphatab/src/synth/AlphaSynth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export class AlphaSynthBase implements IAlphaSynth {
this._countInVolume = value;
}

protected get supportsCountIn(): boolean {
return true;
}

public get midiEventsPlayedFilter(): MidiEventType[] {
return Array.from(this.midiEventsPlayedFilterSet);
}
Expand Down Expand Up @@ -321,7 +325,7 @@ export class AlphaSynthBase implements IAlphaSynth {

this._playInternal();

if (this._countInVolume > 0) {
if (this._countInVolume > 0 && this.supportsCountIn) {
Logger.debug('AlphaSynth', 'Starting countin');
this.sequencer.startCountIn();
this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this._countInVolume);
Expand Down
5 changes: 5 additions & 0 deletions packages/alphatab/src/synth/BackingTrackPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ export class BackingTrackPlayer extends AlphaSynthBase {
// should never be called
}

protected override get supportsCountIn(): boolean {
// not supported, the media plays as-is and there is no synthesizer for the count-in metronome
return false;
}

public override loadMidiFile(midi: MidiFile): void {
if (!this.isSoundFontLoaded) {
this.isSoundFontLoaded = true;
Expand Down
4 changes: 2 additions & 2 deletions packages/alphatab/src/synth/MidiFileSequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,8 @@ export class MidiFileSequencer {
}

public fillMidiEventQueueToEndTime(endTime: number) {
while (this._mainState.currentTime < endTime) {
if (this._fillMidiEventQueueLimited(endTime - this._mainState.currentTime)) {
while (this._currentState.currentTime < endTime) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sets the current clock, instead of the main one, to match fillMidiEventQueueToEndTime.

if (this._fillMidiEventQueueLimited(endTime - this._currentState.currentTime)) {
this._synthesizer.synthesizeSilent(SynthConstants.MicroBufferSize);
}
}
Expand Down
95 changes: 94 additions & 1 deletion packages/alphatab/test/audio/SyncPoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MidiFile } from '@coderline/alphatab/midi/MidiFile';
import { MidiFileGenerator } from '@coderline/alphatab/midi/MidiFileGenerator';
import type { BackingTrack } from '@coderline/alphatab/model/BackingTrack';
import { Settings } from '@coderline/alphatab/Settings';
import { AlphaSynth } from '@coderline/alphatab/synth/AlphaSynth';
import { BackingTrackPlayer, type IBackingTrackSynthOutput } from '@coderline/alphatab/synth/BackingTrackPlayer';
import {
ExternalMediaPlayer,
Expand All @@ -19,6 +20,7 @@ import type { PositionChangedEventArgs } from '@coderline/alphatab/synth/Positio
import type { Hydra } from '@coderline/alphatab/synth/soundfont/Hydra';
import type { SynthEvent } from '@coderline/alphatab/synth/synthesis/SynthEvent';
import { FlatMidiEventGenerator } from 'test/audio/FlatMidiEventGenerator';
import { TestOutput } from 'test/audio/TestOutput';
import { TestPlatform } from 'test/TestPlatform';

describe('SyncPointTests', () => {
Expand Down Expand Up @@ -356,6 +358,89 @@ describe('SyncPointTests', () => {
expect(events.map(e => `${e.currentTime},${e.originalTempo},${e.modifiedTempo}`)).toMatchSnapshot();
expect(testOutput.seekTimes).toMatchSnapshot();
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Danielku15, I noticed you simplified my latest tests, so I tried to keep it simple this time.

Let me know if this provides good coverage or if you would prefer a different testing approach.

/**
* See #2397: starting the playback with count-in must not rewind the media to the song start.
*/
it('count-in-keeps-position-backing-track', async () => {
const player = await prepareBackingTrackPlayer();
const testOutput = player.output as TestBackingTrackOutput;
player.timePosition = 30000;
player.countInVolume = 1;
const seekCount = testOutput.seekTimes.length;

player.play();

// no seek back to the song start, the next media update continues from 30s
testOutput.simulateSeek(testOutput.seekTimes[seekCount - 1] + 100);
expect(testOutput.seekTimes.length).toBe(seekCount);
expect(player.timePosition).toBeGreaterThan(30000);
});

it('count-in-keeps-position-external-media', async () => {
const player = await prepareExternalMediaPlayer();
const testOutput = (player.output as IExternalMediaSynthOutput).handler as TestExternalMediaHandler;
player.timePosition = 30000;
player.countInVolume = 1;
const seekCount = testOutput.seekTimes.length;

player.play();

// no seek back to the song start, the next media update continues from 30s
testOutput.simulateSeek(testOutput.seekTimes[seekCount - 1] + 100);
expect(testOutput.seekTimes.length).toBe(seekCount);
expect(player.timePosition).toBeGreaterThan(30000);
});

/**
* See #2397: filling the queue up to a time position must advance the active
* (count-in) state, otherwise the loop never ends.
*/
it('fill-to-end-time-during-count-in', () => {
const score = ScoreLoader.loadAlphaTex(`
.
C4 * 4
`);

const midi = new MidiFile();
const handler = new AlphaSynthMidiFileHandler(midi);
const generator = new MidiFileGenerator(score, new Settings(), handler);
generator.generate();

const sequencer = new MidiFileSequencer(new EmptyAudioSynthesizer());
sequencer.loadMidi(midi);
sequencer.startCountIn();

sequencer.fillMidiEventQueueToEndTime(1000);

expect(sequencer.isPlayingCountIn).toBe(true);
expect(sequencer.currentTime).toBe(1000);
});

/**
* See #2397: the synthesizer player still plays the count-in.
*/
it('count-in-playback-synthesizer', () => {
const score = ScoreLoader.loadAlphaTex(`
.
C4 * 4
`);

const midi = new MidiFile();
const handler = new AlphaSynthMidiFileHandler(midi);
const generator = new MidiFileGenerator(score, new Settings(), handler);
generator.generate();

const synth = new AlphaSynth(new TestOutput(), 500);
synth.loadMidiFile(midi);
synth.timePosition = 1000;
synth.countInVolume = 1;

expect(synth.play()).toBe(true);

// the count-in plays from its own start, the song continues at 1000ms after it
expect(synth.timePosition).toBe(0);
});
});

/**
Expand Down Expand Up @@ -450,7 +535,15 @@ class TestExternalMediaHandler implements IExternalMediaHandler {
class EmptyAudioSynthesizer implements IAudioSampleSynthesizer {
public masterVolume: number = 0;
public metronomeVolume: number = 0;
public outSampleRate: number = 44100;
private _sampleRateReads: number = 0;
public get outSampleRate(): number {
// the sequencer reads this once per fill iteration: fail instead of hanging if it stops making progress
this._sampleRateReads++;
if (this._sampleRateReads > 100000) {
throw new Error('Sequencer is not making progress');
}
return 44100;
}
public currentTempo: number = 120;
public timeSignatureNumerator: number = 4;
public timeSignatureDenominator: number = 4;
Expand Down
Loading