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; } }