diff --git a/playground-template/control-template.html b/playground-template/control-template.html
index 8b73db8a0..b9748be2a 100644
--- a/playground-template/control-template.html
+++ b/playground-template/control-template.html
@@ -88,6 +88,9 @@
+
+
+
diff --git a/playground-template/control.js b/playground-template/control.js
index 45b320d7a..23b7cbe95 100644
--- a/playground-template/control.js
+++ b/playground-template/control.js
@@ -201,6 +201,17 @@ function setupControl(selector) {
}
};
+ control.querySelector('.at-count-in').onclick = function (e) {
+ e.stopPropagation();
+ const link = e.target.closest('a');
+ link.classList.toggle('active');
+ if (link.classList.contains('active')) {
+ at.countInVolume = 1;
+ } else {
+ at.countInVolume = 0;
+ }
+ };
+
control.querySelectorAll('.at-speed-options a').forEach(function (a) {
a.onclick = function (e) {
e.preventDefault();
diff --git a/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs b/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs
index ef937cb9c..1315a4345 100644
--- a/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs
+++ b/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs
@@ -59,6 +59,12 @@ public double MasterVolume
set => DispatchOnWorkerThread(() => { Player.MasterVolume = value; });
}
+ public double CountInVolume
+ {
+ get => Player.CountInVolume;
+ set => DispatchOnWorkerThread(() => { Player.CountInVolume = value; });
+ }
+
public double MetronomeVolume
{
get => Player.MetronomeVolume;
diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts
index 14e62a2d7..046d30950 100644
--- a/src/AlphaTabApiBase.ts
+++ b/src/AlphaTabApiBase.ts
@@ -422,6 +422,19 @@ export class AlphaTabApiBase
{
}
}
+ public get countInVolume(): number {
+ if (!this.player) {
+ return 0;
+ }
+ return this.player.countInVolume;
+ }
+
+ public set countInVolume(value: number) {
+ if (this.player) {
+ this.player.countInVolume = value;
+ }
+ }
+
public get tickPosition(): number {
if (!this.player) {
return 0;
diff --git a/src/platform/javascript/AlphaSynthWebAudioOutput.ts b/src/platform/javascript/AlphaSynthWebAudioOutput.ts
index 4cc7a15db..c79a079a1 100644
--- a/src/platform/javascript/AlphaSynthWebAudioOutput.ts
+++ b/src/platform/javascript/AlphaSynthWebAudioOutput.ts
@@ -138,11 +138,16 @@ export class AlphaSynthWebAudioOutput implements ISynthOutput {
}
}
+ private _outputBuffer:Float32Array = new Float32Array(0);
private generateSound(e: AudioProcessingEvent): void {
let left: Float32Array = e.outputBuffer.getChannelData(0);
let right: Float32Array = e.outputBuffer.getChannelData(1);
let samples: number = left.length + right.length;
- let buffer: Float32Array = new Float32Array(samples);
+ let buffer = this._outputBuffer;
+ if(buffer.length != samples) {
+ buffer = new Float32Array(samples);
+ this._outputBuffer = buffer;
+ }
this._circularBuffer.read(buffer, 0, Math.min(buffer.length, this._circularBuffer.count));
let s: number = 0;
for (let i: number = 0; i < left.length; i++) {
diff --git a/src/platform/javascript/AlphaSynthWebWorker.ts b/src/platform/javascript/AlphaSynthWebWorker.ts
index 9d34aa40b..38afff512 100644
--- a/src/platform/javascript/AlphaSynthWebWorker.ts
+++ b/src/platform/javascript/AlphaSynthWebWorker.ts
@@ -78,6 +78,9 @@ export class AlphaSynthWebWorker {
case 'alphaSynth.setIsLooping':
this._player.isLooping = data.value;
break;
+ case 'alphaSynth.setCountInVolume':
+ this._player.countInVolume = data.value;
+ break;
case 'alphaSynth.play':
this._player.play();
break;
diff --git a/src/platform/javascript/AlphaSynthWebWorkerApi.ts b/src/platform/javascript/AlphaSynthWebWorkerApi.ts
index 9eb022bc6..b3a2f985c 100644
--- a/src/platform/javascript/AlphaSynthWebWorkerApi.ts
+++ b/src/platform/javascript/AlphaSynthWebWorkerApi.ts
@@ -27,6 +27,7 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth {
private _state: PlayerState = PlayerState.Paused;
private _masterVolume: number = 0;
private _metronomeVolume: number = 0;
+ private _countInVolume: number = 0;
private _playbackSpeed: number = 0;
private _tickPosition: number = 0;
private _timePosition: number = 0;
@@ -82,6 +83,18 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth {
value: value
});
}
+ public get countInVolume(): number {
+ return this._countInVolume;
+ }
+
+ public set countInVolume(value: number) {
+ value = SynthHelper.clamp(value, SynthConstants.MinVolume, SynthConstants.MaxVolume);
+ this._countInVolume = value;
+ this._synth.postMessage({
+ cmd: 'alphaSynth.setCountInVolume',
+ value: value
+ });
+ }
public get playbackSpeed(): number {
return this._playbackSpeed;
diff --git a/src/platform/javascript/JQueryAlphaTab.ts b/src/platform/javascript/JQueryAlphaTab.ts
index 74984959d..6dec01f5b 100644
--- a/src/platform/javascript/JQueryAlphaTab.ts
+++ b/src/platform/javascript/JQueryAlphaTab.ts
@@ -166,6 +166,13 @@ export class JQueryAlphaTab {
return context.metronomeVolume;
}
+ public countInVolume(element: jQuery, context: AlphaTabApi, countInVolume?: number): number {
+ if (typeof countInVolume === 'number') {
+ context.countInVolume = countInVolume;
+ }
+ return context.countInVolume;
+ }
+
public playbackSpeed(element: jQuery, context: AlphaTabApi, playbackSpeed?: number): number {
if (typeof playbackSpeed === 'number') {
context.playbackSpeed = playbackSpeed;
diff --git a/src/synth/AlphaSynth.ts b/src/synth/AlphaSynth.ts
index 096f7a747..ea35ee08d 100644
--- a/src/synth/AlphaSynth.ts
+++ b/src/synth/AlphaSynth.ts
@@ -27,6 +27,7 @@ export class AlphaSynth implements IAlphaSynth {
private _tickPosition: number = 0;
private _timePosition: number = 0;
private _metronomeVolume: number = 0;
+ private _countInVolume: number = 0;
/**
* Gets the {@link ISynthOutput} used for playing the generated samples.
@@ -68,6 +69,15 @@ export class AlphaSynth implements IAlphaSynth {
this._synthesizer.metronomeVolume = value;
}
+ public get countInVolume(): number {
+ return this._countInVolume;
+ }
+
+ public set countInVolume(value: number) {
+ value = SynthHelper.clamp(value, SynthConstants.MinVolume, SynthConstants.MaxVolume);
+ this._countInVolume = value;
+ }
+
public get playbackSpeed(): number {
return this._sequencer.playbackSpeed;
}
@@ -182,14 +192,27 @@ export class AlphaSynth implements IAlphaSynth {
return false;
}
this.output.activate();
- this._synthesizer.setupMetronomeChannel(this.metronomeVolume);
+
+ this.playInternal();
+
+ if (this._countInVolume > 0) {
+ Logger.debug('AlphaSynth', 'Starting countin');
+ this._sequencer.startCountIn();
+ this._synthesizer.setupMetronomeChannel(this._countInVolume);
+ this.tickPosition = 0;
+ }
+
+ this.output.play();
+ return true;
+ }
+
+ private playInternal() {
Logger.debug('AlphaSynth', 'Starting playback');
+ this._synthesizer.setupMetronomeChannel(this.metronomeVolume);
this.state = PlayerState.Playing;
(this.stateChanged as EventEmitterOfT).trigger(
new PlayerStateChangedEventArgs(this.state, false)
);
- this.output.play();
- return true;
}
public pause(): void {
@@ -227,20 +250,17 @@ export class AlphaSynth implements IAlphaSynth {
new PlayerStateChangedEventArgs(this.state, true)
);
}
-
+
public playOneTimeMidiFile(midi: MidiFile): void {
// pause current playback.
this.pause();
-
+
this._sequencer.loadOneTimeMidi(midi);
-
+
this._sequencer.stop();
this._synthesizer.noteOffAll(true);
this.tickPosition = 0;
- (this.stateChanged as EventEmitterOfT).trigger(
- new PlayerStateChangedEventArgs(this.state, false)
- );
this.output.play();
}
@@ -335,7 +355,11 @@ export class AlphaSynth implements IAlphaSynth {
if (this._tickPosition >= endTick) {
Logger.debug('AlphaSynth', 'Finished playback');
- if(this._sequencer.isPlayingOneTimeMidi) {
+ if (this._sequencer.isPlayingCountIn) {
+ this._sequencer.resetCountIn();
+ this.timePosition = this._sequencer.currentTime;
+ this.playInternal()
+ } else if (this._sequencer.isPlayingOneTimeMidi) {
this._sequencer.resetOneTimeMidi();
this.state = PlayerState.Paused;
this.output.pause();
@@ -359,7 +383,7 @@ export class AlphaSynth implements IAlphaSynth {
const endTime: number = this._sequencer.endTime;
const endTick: number = this._sequencer.endTick;
- if(!this._sequencer.isPlayingOneTimeMidi) {
+ if (!this._sequencer.isPlayingOneTimeMidi && !this._sequencer.isPlayingCountIn) {
Logger.debug(
'AlphaSynth',
`Position changed: (time: ${currentTime}/${endTime}, tick: ${currentTick}/${endTick}, Active Voices: ${this._synthesizer.activeVoiceCount}`
diff --git a/src/synth/IAlphaSynth.ts b/src/synth/IAlphaSynth.ts
index ac96d0f3c..5f0f68a23 100644
--- a/src/synth/IAlphaSynth.ts
+++ b/src/synth/IAlphaSynth.ts
@@ -66,6 +66,11 @@ export interface IAlphaSynth {
*/
isLooping: boolean;
+ /**
+ * Gets or sets volume of the metronome during count-in. (range: 0.0-3.0, default 0.0 - no count in)
+ */
+ countInVolume: number;
+
/**
* Destroys the synthesizer and all related components
*/
@@ -91,7 +96,7 @@ export interface IAlphaSynth {
* Stopps the playback
*/
stop(): void;
-
+
/**
* Stops any ongoing playback and plays the given midi file instead.
* @param midi The midi file to play
diff --git a/src/synth/MidiFileSequencer.ts b/src/synth/MidiFileSequencer.ts
index 4e84a16ef..b666b5c3d 100644
--- a/src/synth/MidiFileSequencer.ts
+++ b/src/synth/MidiFileSequencer.ts
@@ -24,6 +24,8 @@ export class MidiFileSequencerTempoChange {
class MidiSequencerState {
public tempoChanges: MidiFileSequencerTempoChange[] = [];
public firstProgramEventPerChannel: Map = new Map();
+ public firstTimeSignatureNumerator: number = 0;
+ public firstTimeSignatureDenominator: number = 0;
public synthData: SynthEvent[] = [];
public division: number = 0;
public eventIndex: number = 0;
@@ -31,8 +33,8 @@ class MidiSequencerState {
public playbackRange: PlaybackRange | null = null;
public playbackRangeStartTime: number = 0;
public playbackRangeEndTime: number = 0;
- public endTick:number = 0;
- public endTime:number = 0;
+ public endTick: number = 0;
+ public endTime: number = 0;
}
/**
@@ -44,11 +46,16 @@ export class MidiFileSequencer {
private _currentState: MidiSequencerState;
private _mainState: MidiSequencerState;
private _oneTimeState: MidiSequencerState | null = null;
-
+ private _countInState: MidiSequencerState | null = null;
+
public get isPlayingOneTimeMidi(): boolean {
return this._currentState == this._oneTimeState;
}
+ public get isPlayingCountIn(): boolean {
+ return this._currentState == this._countInState;
+ }
+
public constructor(synthesizer: TinySoundFont) {
this._synthesizer = synthesizer;
this._mainState = new MidiSequencerState();
@@ -69,6 +76,10 @@ export class MidiFileSequencer {
public isLooping: boolean = false;
+ public get currentTime() {
+ return this._currentState.currentTime;
+ }
+
/**
* Gets the duration of the song in ticks.
*/
@@ -98,12 +109,6 @@ export class MidiFileSequencer {
}
}
- // move back some ticks to ensure the on-time events are played
- timePosition -= 25;
- if (timePosition < 0) {
- timePosition = 0;
- }
-
if (timePosition > this._currentState.currentTime) {
this.silentProcess(timePosition - this._currentState.currentTime);
} else if (timePosition < this._currentState.currentTime) {
@@ -132,6 +137,8 @@ export class MidiFileSequencer {
}
}
+ this._currentState.currentTime = finalTime;
+
let duration: number = Date.now() - start;
Logger.debug('Sequencer', 'Silent seek finished in ' + duration + 'ms');
}
@@ -196,6 +203,10 @@ export class MidiFileSequencer {
let meta: MetaDataEvent = mEvent as MetaDataEvent;
let timeSignatureDenominator: number = Math.pow(2, meta.data[1]);
metronomeLength = (state.division * (4.0 / timeSignatureDenominator)) | 0;
+ if (state.firstTimeSignatureDenominator === 0) {
+ state.firstTimeSignatureNumerator = meta.data[0];
+ state.firstTimeSignatureDenominator = timeSignatureDenominator;
+ }
} else if (mEvent.command === MidiEventType.ProgramChange) {
let channel: number = mEvent.channel;
if (!state.firstProgramEventPerChannel.has(channel)) {
@@ -312,7 +323,7 @@ export class MidiFileSequencer {
public get isFinished(): boolean {
return this._currentState.currentTime >= this.internalEndTime;
}
-
+
public stop(): void {
if (!this.playbackRange) {
this._currentState.currentTime = 0;
@@ -326,5 +337,63 @@ export class MidiFileSequencer {
public resetOneTimeMidi() {
this._oneTimeState = null;
this._currentState = this._mainState;
- }
+ }
+
+ public resetCountIn() {
+ this._countInState = null;
+ this._currentState = this._mainState;
+ }
+
+ public startCountIn() {
+ this.generateCountInMidi();
+ this._currentState = this._countInState!;
+
+ this.stop();
+ this._synthesizer.noteOffAll(true);
+ }
+
+ generateCountInMidi() {
+ const state = new MidiSequencerState();
+ state.division = this._mainState.division;
+
+ let bpm :number = 120;
+ let timeSignatureNumerator = 4;
+ let timeSignatureDenominator = 4;
+ if(this._mainState.eventIndex === 0) {
+ bpm = this._mainState.tempoChanges[0].bpm;
+ timeSignatureNumerator = this._mainState.firstTimeSignatureNumerator;
+ timeSignatureDenominator = this._mainState.firstTimeSignatureDenominator;
+ } else {
+ bpm = this._synthesizer.currentTempo;
+ timeSignatureNumerator = this._synthesizer.timeSignatureNumerator;
+ timeSignatureDenominator = this._synthesizer.timeSignatureDenominator;
+ }
+
+ state.tempoChanges.push(new MidiFileSequencerTempoChange(bpm, 0, 0));
+
+ let metronomeLength: number = (state.division * (4.0 / timeSignatureDenominator)) | 0;
+ let metronomeTick: number = 0;
+ let metronomeTime: number = 0.0;
+
+ for (let i = 0; i < timeSignatureNumerator; i++) {
+ let metronome: SynthEvent = SynthEvent.newMetronomeEvent(state.synthData.length);
+ state.synthData.push(metronome);
+ metronome.time = metronomeTime;
+ metronomeTick += metronomeLength;
+ metronomeTime += metronomeLength * (60000.0 / (bpm * this._mainState.division));
+ }
+
+ state.synthData.sort((a, b) => {
+ if (a.time > b.time) {
+ return 1;
+ }
+ if (a.time < b.time) {
+ return -1;
+ }
+ return a.eventIndex - b.eventIndex;
+ });
+ state.endTime = metronomeTime;
+ state.endTick = metronomeTick;
+ this._countInState = state;
+ }
}
diff --git a/src/synth/synthesis/TinySoundFont.ts b/src/synth/synthesis/TinySoundFont.ts
index 19149d5f9..5916c4f2f 100644
--- a/src/synth/synthesis/TinySoundFont.ts
+++ b/src/synth/synthesis/TinySoundFont.ts
@@ -27,6 +27,9 @@ import { TypeConversions } from '@src/io/TypeConversions';
import { Logger } from '@src/Logger';
import { SynthConstants } from '@src/synth/SynthConstants';
import { Midi20PerNotePitchBendEvent } from '@src/midi/Midi20ChannelVoiceEvent';
+import { MetaEventType } from '@src/midi/MetaEvent';
+import { MetaNumberEvent } from '@src/midi/MetaNumberEvent';
+import { MetaDataEvent } from '@src/midi/MetaDataEvent';
/**
* This is a tiny soundfont based synthesizer.
@@ -42,6 +45,10 @@ export class TinySoundFont {
private _soloChannels: Map = new Map();
private _isAnySolo: boolean = false;
+ public currentTempo:number = 0;
+ public timeSignatureNumerator:number = 0;
+ public timeSignatureDenominator:number = 0;
+
public constructor(sampleRate: number) {
this.outSampleRate = sampleRate;
}
@@ -172,6 +179,17 @@ export class TinySoundFont {
perNotePitchWheel = (perNotePitchWheel * SynthConstants.MaxPitchWheel) / SynthConstants.MaxPitchWheel20;
this.channelSetPerNotePitchWheel(channel, midi20.noteKey, perNotePitchWheel);
break;
+ case MidiEventType.Meta:
+ switch (e.data1 as MetaEventType) {
+ case MetaEventType.Tempo:
+ this.currentTempo = 60000000 / (e as MetaNumberEvent).value;
+ break;
+ case MetaEventType.TimeSignature:
+ this.timeSignatureNumerator = (e as MetaDataEvent).data[0];
+ this.timeSignatureDenominator = Math.pow(2, (e as MetaDataEvent).data[1]);
+ break;
+ }
+ break;
}
}