diff --git a/src.csharp/AlphaTab.Windows/NAudioSynthOutput.cs b/src.csharp/AlphaTab.Windows/NAudioSynthOutput.cs index ee6e17d74..fac192b3a 100644 --- a/src.csharp/AlphaTab.Windows/NAudioSynthOutput.cs +++ b/src.csharp/AlphaTab.Windows/NAudioSynthOutput.cs @@ -128,13 +128,14 @@ private void RequestBuffers() public override int Read(float[] buffer, int offset, int count) { var read = new Float32Array(count); - _circularBuffer.Read(read, 0, System.Math.Min(read.Length, _circularBuffer.Count)); + + var samplesFromBuffer = _circularBuffer.Read(read, 0, System.Math.Min(read.Length, _circularBuffer.Count)); Buffer.BlockCopy(read.Data, 0, buffer, offset * sizeof(float), count * sizeof(float)); var samples = count / 2; - ((EventEmitterOfT) SamplesPlayed).Trigger(samples); + ((EventEmitterOfT) SamplesPlayed).Trigger(samples / SynthConstants.AudioChannels); RequestBuffers(); diff --git a/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidAudioWorker.kt b/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidAudioWorker.kt index bec7fd95f..c0220c6a4 100644 --- a/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidAudioWorker.kt +++ b/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidAudioWorker.kt @@ -48,12 +48,12 @@ internal class AndroidAudioWorker( private fun writeSamples() { while (!_stopped) { if (_track.playState == AudioTrack.PLAYSTATE_PLAYING) { - _output.read(_buffer, 0, _buffer.size) + val samplesFromBuffer = _output.read(_buffer, 0, _buffer.size) if (_previousPosition == -1) { _previousPosition = _track.playbackHeadPosition _track.getTimestamp(_timestamp) } - _track.write(_buffer, 0, _buffer.size, AudioTrack.WRITE_BLOCKING) + _track.write(_buffer, 0, samplesFromBuffer, AudioTrack.WRITE_BLOCKING) } else { _playingSemaphore.acquire() // wait for playing to start _playingSemaphore.release() // release semaphore for others diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index 6fa65c83f..fe220dd44 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -994,7 +994,7 @@ export class AlphaTabApiBase { if ( nextBeatBoundings && nextBeatBoundings.barBounds.masterBarBounds.staveGroupBounds === - barBoundings.staveGroupBounds + barBoundings.staveGroupBounds ) { nextBeatX = nextBeatBoundings.visualBounds.x; } @@ -1032,6 +1032,7 @@ export class AlphaTabApiBase { } private _beatMouseDown: boolean = false; + private _noteMouseDown: boolean = false; private _selectionStart: SelectionInfo | null = null; private _selectionEnd: SelectionInfo | null = null; @@ -1039,6 +1040,10 @@ export class AlphaTabApiBase { public beatMouseMove: IEventEmitterOfT = new EventEmitterOfT(); public beatMouseUp: IEventEmitterOfT = new EventEmitterOfT(); + public noteMouseDown: IEventEmitterOfT = new EventEmitterOfT(); + public noteMouseMove: IEventEmitterOfT = new EventEmitterOfT(); + public noteMouseUp: IEventEmitterOfT = new EventEmitterOfT(); + private onBeatMouseDown(originalEvent: IMouseEventArgs, beat: Beat): void { if (this._isDestroyed) { return; @@ -1057,6 +1062,16 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'beatMouseDown', beat, originalEvent); } + private onNoteMouseDown(originalEvent: IMouseEventArgs, note: Note): void { + if (this._isDestroyed) { + return; + } + + this._noteMouseDown = true; + (this.noteMouseDown as EventEmitterOfT).trigger(note); + this.uiFacade.triggerEvent(this.container, 'noteMouseDown', note, originalEvent); + } + private onBeatMouseMove(originalEvent: IMouseEventArgs, beat: Beat): void { if (this._isDestroyed) { return; @@ -1072,6 +1087,15 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'beatMouseMove', beat, originalEvent); } + private onNoteMouseMove(originalEvent: IMouseEventArgs, note: Note): void { + if (this._isDestroyed) { + return; + } + + (this.noteMouseMove as EventEmitterOfT).trigger(note); + this.uiFacade.triggerEvent(this.container, 'noteMouseMove', note, originalEvent); + } + private onBeatMouseUp(originalEvent: IMouseEventArgs, beat: Beat | null): void { if (this._isDestroyed) { return; @@ -1127,6 +1151,17 @@ export class AlphaTabApiBase { this._beatMouseDown = false; } + + private onNoteMouseUp(originalEvent: IMouseEventArgs, note: Note | null): void { + if (this._isDestroyed) { + return; + } + + (this.noteMouseUp as EventEmitterOfT).trigger(note); + this.uiFacade.triggerEvent(this.container, 'noteMouseUp', note, originalEvent); + this._noteMouseDown = false; + } + private updateSelectionCursor(range: PlaybackRange | null) { if (!this._tickCache) { return; @@ -1157,6 +1192,14 @@ export class AlphaTabApiBase { let beat: Beat | null = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null; if (beat) { this.onBeatMouseDown(e, beat); + + if (this.settings.core.includeNoteBounds) { + const note = this.renderer.boundsLookup?.getNoteAtPos(beat, relX, relY); + if (note) { + this.onNoteMouseDown(e, note); + } + } + } }); this.canvasElement.mouseMove.on(e => { @@ -1168,6 +1211,13 @@ export class AlphaTabApiBase { let beat: Beat | null = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null; if (beat) { this.onBeatMouseMove(e, beat); + + if (this._noteMouseDown) { + const note = this.renderer.boundsLookup?.getNoteAtPos(beat, relX, relY); + if (note) { + this.onNoteMouseMove(e, note); + } + } } }); this.canvasElement.mouseUp.on(e => { @@ -1181,6 +1231,16 @@ export class AlphaTabApiBase { let relY: number = e.getY(this.canvasElement); let beat: Beat | null = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null; this.onBeatMouseUp(e, beat); + + if (this._noteMouseDown) { + if (beat) { + const note = this.renderer.boundsLookup?.getNoteAtPos(beat, relX, relY) ?? null; + this.onNoteMouseUp(e, note); + } + else { + this.onNoteMouseUp(e, null); + } + } }); this.renderer.postRenderFinished.on(() => { if ( diff --git a/src/midi/MidiFileGenerator.ts b/src/midi/MidiFileGenerator.ts index 0060fc60e..d2b9c6ddd 100644 --- a/src/midi/MidiFileGenerator.ts +++ b/src/midi/MidiFileGenerator.ts @@ -1362,7 +1362,7 @@ export class MidiFileGenerator { this.prepareSingleBeat(note.beat); this.generateNote( note, - -note.beat.playbackStart, + 0, note.beat.playbackDuration, new Int32Array(note.beat.voice.bar.staff.tuning.length) ); diff --git a/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts b/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts index 949605229..be1e0e96e 100644 --- a/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts +++ b/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts @@ -3,6 +3,7 @@ import { Environment } from '@src/Environment'; import { Logger } from '@src/Logger'; import { AlphaSynthWorkerSynthOutput } from '@src/platform/javascript/AlphaSynthWorkerSynthOutput'; import { AlphaSynthWebAudioOutputBase } from '@src/platform/javascript/AlphaSynthWebAudioOutputBase'; +import { SynthConstants } from '@src/synth/SynthConstants'; /** * @target web @@ -58,7 +59,7 @@ export class AlphaSynthWebWorklet { constructor(...args: any[]) { super(...args); - Logger.info('WebAudio', 'creating processor'); + Logger.debug('WebAudio', 'creating processor'); this._bufferCount = Math.floor( (AlphaSynthWebWorkletProcessor.TotalBufferTimeInMilliseconds * @@ -110,7 +111,7 @@ export class AlphaSynthWebWorklet { buffer = new Float32Array(samples); this._outputBuffer = buffer; } - this._circularBuffer.read(buffer, 0, Math.min(buffer.length, this._circularBuffer.count)); + const samplesFromBuffer = 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++) { left[i] = buffer[s++]; @@ -118,7 +119,7 @@ export class AlphaSynthWebWorklet { } this.port.postMessage({ cmd: AlphaSynthWorkerSynthOutput.CmdOutputSamplesPlayed, - samples: left.length + samples: samplesFromBuffer / SynthConstants.AudioChannels }); this.requestBuffers(); diff --git a/src/platform/javascript/AlphaSynthScriptProcessorOutput.ts b/src/platform/javascript/AlphaSynthScriptProcessorOutput.ts index 1888909e5..2a8472c32 100644 --- a/src/platform/javascript/AlphaSynthScriptProcessorOutput.ts +++ b/src/platform/javascript/AlphaSynthScriptProcessorOutput.ts @@ -1,5 +1,6 @@ import { CircularSampleBuffer } from '@src/synth/ds/CircularSampleBuffer'; import { AlphaSynthWebAudioOutputBase } from '@src/platform/javascript/AlphaSynthWebAudioOutputBase'; +import { SynthConstants } from '@src/synth/SynthConstants'; // tslint:disable: deprecation @@ -86,13 +87,13 @@ export class AlphaSynthScriptProcessorOutput extends AlphaSynthWebAudioOutputBas buffer = new Float32Array(samples); this._outputBuffer = buffer; } - this._circularBuffer.read(buffer, 0, Math.min(buffer.length, this._circularBuffer.count)); + const samplesFromBuffer = 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++) { left[i] = buffer[s++]; right[i] = buffer[s++]; } - this.onSamplesPlayed(left.length); + this.onSamplesPlayed(samplesFromBuffer / SynthConstants.AudioChannels); this.requestBuffers(); } } diff --git a/src/platform/javascript/BrowserUiFacade.ts b/src/platform/javascript/BrowserUiFacade.ts index 3977abac4..b7179f6d0 100644 --- a/src/platform/javascript/BrowserUiFacade.ts +++ b/src/platform/javascript/BrowserUiFacade.ts @@ -472,7 +472,9 @@ export class BrowserUiFacade implements IUiFacade { // remember which bar is contained in which node for faster lookup // on highlight/unhighlight for (let i = renderResult.firstMasterBarIndex; i <= renderResult.lastMasterBarIndex; i++) { - this._barToElementLookup.set(i, placeholder); + if(i >= 0) { + this._barToElementLookup.set(i, placeholder); + } } if (this._api.settings.core.enableLazyLoading) { diff --git a/src/rendering/RenderFinishedEventArgs.ts b/src/rendering/RenderFinishedEventArgs.ts index cf4f65189..6db796581 100644 --- a/src/rendering/RenderFinishedEventArgs.ts +++ b/src/rendering/RenderFinishedEventArgs.ts @@ -42,12 +42,12 @@ export class RenderFinishedEventArgs { /** * Gets or sets the index of the first masterbar that was rendered in this result. */ - public firstMasterBarIndex: number = 0; + public firstMasterBarIndex: number = -1; /** * Gets or sets the index of the last masterbar that was rendered in this result. */ - public lastMasterBarIndex: number = 0; + public lastMasterBarIndex: number = -1; /** * Gets or sets the render engine specific result object which contains the rendered music sheet. diff --git a/src/rendering/glyphs/NoteNumberGlyph.ts b/src/rendering/glyphs/NoteNumberGlyph.ts index 80f98c040..d49484d97 100644 --- a/src/rendering/glyphs/NoteNumberGlyph.ts +++ b/src/rendering/glyphs/NoteNumberGlyph.ts @@ -112,6 +112,6 @@ export class NoteNumberGlyph extends Glyph { noteBounds.noteHeadBounds.y = cy + this.y - this.height/2; noteBounds.noteHeadBounds.w = this.width; noteBounds.noteHeadBounds.h = this.height; - this.renderer.scoreRenderer.boundsLookup!.addNote(noteBounds); + beatBounds.addNote(noteBounds); } } diff --git a/src/rendering/layout/PageViewLayout.ts b/src/rendering/layout/PageViewLayout.ts index c5ff89bde..1af47e076 100644 --- a/src/rendering/layout/PageViewLayout.ts +++ b/src/rendering/layout/PageViewLayout.ts @@ -122,8 +122,6 @@ export class PageViewLayout extends ScoreLayout { e.height = tuningHeight; e.totalWidth = this.width; e.totalHeight = totalHeight < 0 ? y + e.height : totalHeight; - e.firstMasterBarIndex = -1; - e.lastMasterBarIndex = -1; this.registerPartial(e, (canvas: ICanvas) => { canvas.color = res.scoreInfoColor; @@ -151,8 +149,6 @@ export class PageViewLayout extends ScoreLayout { e.height = diagramHeight; e.totalWidth = this.width; e.totalHeight = totalHeight < 0 ? y + diagramHeight : totalHeight; - e.firstMasterBarIndex = -1; - e.lastMasterBarIndex = -1; this.registerPartial(e, (canvas: ICanvas) => { canvas.color = res.scoreInfoColor; @@ -218,8 +214,6 @@ export class PageViewLayout extends ScoreLayout { e.height = infoHeight; e.totalWidth = this.width; e.totalHeight = totalHeight < 0 ? y + e.height : totalHeight; - e.firstMasterBarIndex = -1; - e.lastMasterBarIndex = -1; this.registerPartial(e, (canvas: ICanvas) => { canvas.color = res.scoreInfoColor; canvas.textAlign = TextAlign.Center; diff --git a/src/rendering/utils/BoundsLookup.ts b/src/rendering/utils/BoundsLookup.ts index b8bf1161e..4840bb5fc 100644 --- a/src/rendering/utils/BoundsLookup.ts +++ b/src/rendering/utils/BoundsLookup.ts @@ -128,7 +128,7 @@ export class BoundsLookup { return json; } - private _beatLookup: Map = new Map(); + private _beatLookup: Map = new Map(); private _masterBarLookup: Map = new Map(); private _currentStaveGroup: StaveGroupBounds | null = null; /** @@ -151,15 +151,6 @@ export class BoundsLookup { this.isFinished = true; } - /** - * Adds a new note to the lookup. - * @param bounds The note bounds to add. - */ - public addNote(bounds: NoteBounds): void { - let beat = this.findBeat(bounds.note.beat); - beat!.addNote(bounds); - } - /** * Adds a new stave group to the lookup. * @param bounds The stave group bounds to add. @@ -190,7 +181,10 @@ export class BoundsLookup { * @param bounds The beat bounds to add. */ public addBeat(bounds: BeatBounds): void { - this._beatLookup.set(bounds.beat.id, bounds); + if (!this._beatLookup.has(bounds.beat.id)) { + this._beatLookup.set(bounds.beat.id, []); + } + this._beatLookup.get(bounds.beat.id)?.push(bounds); } /** @@ -224,6 +218,16 @@ export class BoundsLookup { * @returns The beat bounds if it was rendered, or null if no boundary information is available. */ public findBeat(beat: Beat): BeatBounds | null { + const all = this.findBeats(beat); + return all ? all[0] : null; + } + + /** + * Tries to find the bounds of a given beat. + * @param beat The beat to find. + * @returns The beat bounds if it was rendered, or null if no boundary information is available. + */ + public findBeats(beat: Beat): BeatBounds[] | null { let id: number = beat.id; if (this._beatLookup.has(id)) { return this._beatLookup.get(id)!; @@ -281,10 +285,15 @@ export class BoundsLookup { * @returns The note at the given position within the beat. */ public getNoteAtPos(beat: Beat, x: number, y: number): Note | null { - let beatBounds: BeatBounds | null = this.findBeat(beat); - if (!beatBounds) { - return null; + const beatBounds = this.findBeats(beat); + if (beatBounds) { + for (const b of beatBounds) { + const note = b.findNoteAtPos(x, y); + if (note) { + return note; + } + } } - return beatBounds.findNoteAtPos(x, y); + return null; } } diff --git a/src/synth/AlphaSynth.ts b/src/synth/AlphaSynth.ts index 5769b95d5..eb1bbff05 100644 --- a/src/synth/AlphaSynth.ts +++ b/src/synth/AlphaSynth.ts @@ -35,6 +35,7 @@ export class AlphaSynth implements IAlphaSynth { private _countInVolume: number = 0; private _playedEventsQueue: Queue = new Queue(); private _midiEventsPlayedFilter: Set = new Set(); + private _notPlayedSamples: number = 0; /** * Gets the {@link ISynthOutput} used for playing the generated samples. @@ -111,7 +112,7 @@ export class AlphaSynth implements IAlphaSynth { } public set tickPosition(value: number) { - this.timePosition = this._sequencer.tickPositionToTimePosition(value); + this.timePosition = this._sequencer.mainTickPositionToTimePosition(value); } public get timePosition(): number { @@ -119,24 +120,27 @@ export class AlphaSynth implements IAlphaSynth { } public set timePosition(value: number) { - Logger.debug('AlphaSynth', `Seeking to position ${value}ms`); + Logger.debug('AlphaSynth', `Seeking to position ${value}ms (main)`); // tell the sequencer to jump to the given position - this._sequencer.seek(value); + this._sequencer.mainSeek(value); // update the internal position this.updateTimePosition(value, true); // tell the output to reset the already synthesized buffers and request data again - this.output.resetSamples(); + if(this._sequencer.isPlayingMain) { + this._notPlayedSamples = 0; + this.output.resetSamples(); + } } public get playbackRange(): PlaybackRange | null { - return this._sequencer.playbackRange; + return this._sequencer.mainPlaybackRange; } public set playbackRange(value: PlaybackRange | null) { - this._sequencer.playbackRange = value; + this._sequencer.mainPlaybackRange = value; if (value) { this.tickPosition = value.startTick; } @@ -181,38 +185,41 @@ export class AlphaSynth implements IAlphaSynth { this.checkReadyForPlayback(); }); this.output.sampleRequest.on(() => { - let samples: Float32Array = new Float32Array( - SynthConstants.MicroBufferSize * SynthConstants.MicroBufferCount * SynthConstants.AudioChannels - ); - let bufferPos: number = 0; - - for (let i = 0; i < SynthConstants.MicroBufferCount; i++) { - // synthesize buffer - this._sequencer.fillMidiEventQueue(); - const synthesizedEvents = this._synthesizer.synthesize( - samples, - bufferPos, - SynthConstants.MicroBufferSize + if (!this._sequencer.isFinished) { + let samples: Float32Array = new Float32Array( + SynthConstants.MicroBufferSize * SynthConstants.MicroBufferCount * SynthConstants.AudioChannels ); - bufferPos += SynthConstants.MicroBufferSize * SynthConstants.AudioChannels; - // push all processed events into the queue - // for informing users about played events - for (const e of synthesizedEvents) { - if (this._midiEventsPlayedFilter.has(e.event.command)) { - this._playedEventsQueue.enqueue(e); + let bufferPos: number = 0; + + for (let i = 0; i < SynthConstants.MicroBufferCount; i++) { + // synthesize buffer + this._sequencer.fillMidiEventQueue(); + const synthesizedEvents = this._synthesizer.synthesize( + samples, + bufferPos, + SynthConstants.MicroBufferSize + ); + bufferPos += SynthConstants.MicroBufferSize * SynthConstants.AudioChannels; + // push all processed events into the queue + // for informing users about played events + for (const e of synthesizedEvents) { + if (this._midiEventsPlayedFilter.has(e.event.command)) { + this._playedEventsQueue.enqueue(e); + } + } + // tell sequencer to check whether its work is done + if (this._sequencer.isFinished) { + break; } } - // tell sequencer to check whether its work is done - if (this._sequencer.isFinished) { - break; - } - } - // send it to output - if (bufferPos < samples.length) { - samples = samples.subarray(0, bufferPos); + // send it to output + if (bufferPos < samples.length) { + samples = samples.subarray(0, bufferPos); + } + this._notPlayedSamples += samples.length; + this.output.addSamples(samples); } - this.output.addSamples(samples); }); this.output.samplesPlayed.on(this.onSamplesPlayed.bind(this)); this.output.open(); @@ -238,6 +245,11 @@ export class AlphaSynth implements IAlphaSynth { } private playInternal() { + if (this._sequencer.isPlayingOneTimeMidi) { + Logger.debug('AlphaSynth', 'Cancelling one time midi'); + this.stopOneTimeMidi(); + } + Logger.debug('AlphaSynth', 'Starting playback'); this._synthesizer.setupMetronomeChannel(this.metronomeVolume); this.state = PlayerState.Playing; @@ -274,27 +286,36 @@ export class AlphaSynth implements IAlphaSynth { Logger.debug('AlphaSynth', 'Stopping playback'); this.state = PlayerState.Paused; this.output.pause(); + this._notPlayedSamples = 0; this._sequencer.stop(); this._synthesizer.noteOffAll(true); - this.tickPosition = this._sequencer.playbackRange ? this._sequencer.playbackRange.startTick : 0; + this.tickPosition = this._sequencer.mainPlaybackRange ? this._sequencer.mainPlaybackRange.startTick : 0; (this.stateChanged as EventEmitterOfT).trigger( new PlayerStateChangedEventArgs(this.state, true) ); } public playOneTimeMidiFile(midi: MidiFile): void { - // pause current playback. - this.pause(); + if (this._sequencer.isPlayingOneTimeMidi) { + this.stopOneTimeMidi(); + } else { + // pause current playback. + this.pause(); + } this._sequencer.loadOneTimeMidi(midi); - - this._sequencer.stop(); this._synthesizer.noteOffAll(true); - this.tickPosition = 0; + + // update the internal position + this.updateTimePosition(0, true); + + // tell the output to reset the already synthesized buffers and request data again + this._notPlayedSamples = 0; + this.output.resetSamples(); this.output.play(); } - + public resetSoundFonts(): void { this.stop(); this._synthesizer.resetPresets(); @@ -341,7 +362,13 @@ export class AlphaSynth implements IAlphaSynth { this._sequencer.loadMidi(midi); this._isMidiLoaded = true; (this.midiLoaded as EventEmitterOfT).trigger( - new PositionChangedEventArgs(0, this._sequencer.endTime, 0, this._sequencer.endTick, false) + new PositionChangedEventArgs( + 0, + this._sequencer.currentEndTime, + 0, + this._sequencer.currentEndTick, + false + ) ); Logger.debug('AlphaSynth', 'Midi successfully loaded'); this.checkReadyForPlayback(); @@ -373,13 +400,17 @@ export class AlphaSynth implements IAlphaSynth { } private onAudioSettingsUpdate() { // seeking to the currently known position, will ensure we - // clear all audio buffers and re-generate the audio - // which was not actually played yet. + // clear all audio buffers and re-generate the audio + // which was not actually played yet. this.timePosition = this.timePosition; } private onSamplesPlayed(sampleCount: number): void { + if (sampleCount === 0) { + return; + } let playedMillis: number = (sampleCount / this._synthesizer.outSampleRate) * 1000; + this._notPlayedSamples -= sampleCount * SynthConstants.AudioChannels; this.updateTimePosition(this._timePosition + playedMillis, false); this.checkForFinish(); } @@ -391,21 +422,23 @@ export class AlphaSynth implements IAlphaSynth { startTick = this.playbackRange.startTick; endTick = this.playbackRange.endTick; } else { - endTick = this._sequencer.endTick; + endTick = this._sequencer.currentEndTick; } - if (this._tickPosition >= endTick) { - Logger.debug('AlphaSynth', 'Finished playback'); + if (this._tickPosition >= endTick && this._notPlayedSamples <= 0) { + this._notPlayedSamples = 0; if (this._sequencer.isPlayingCountIn) { + Logger.debug('AlphaSynth', 'Finished playback (count-in)'); this._sequencer.resetCountIn(); this.timePosition = this._sequencer.currentTime; this.playInternal(); } else if (this._sequencer.isPlayingOneTimeMidi) { - this._sequencer.resetOneTimeMidi(); + Logger.debug('AlphaSynth', 'Finished playback (one time)'); + this.output.resetSamples(); this.state = PlayerState.Paused; - this.output.pause(); - this._synthesizer.noteOffAll(false); + this.stopOneTimeMidi(); } else { + Logger.debug('AlphaSynth', 'Finished playback (main)'); (this.finished as EventEmitter).trigger(); if (this.isLooping) { @@ -417,20 +450,35 @@ export class AlphaSynth implements IAlphaSynth { } } + private stopOneTimeMidi() { + this.output.pause(); + this._synthesizer.noteOffAll(true); + this._sequencer.resetOneTimeMidi(); + this.timePosition = this._sequencer.currentTime; + } + private updateTimePosition(timePosition: number, isSeek: boolean): void { // update the real positions const currentTime: number = timePosition; this._timePosition = currentTime; - const currentTick: number = this._sequencer.timePositionToTickPosition(currentTime); + const currentTick: number = this._sequencer.currentTimePositionToTickPosition(currentTime); this._tickPosition = currentTick; - const endTime: number = this._sequencer.endTime; - const endTick: number = this._sequencer.endTick; - if (!this._sequencer.isPlayingOneTimeMidi && !this._sequencer.isPlayingCountIn) { - Logger.debug( - 'AlphaSynth', - `Position changed: (time: ${currentTime}/${endTime}, tick: ${currentTick}/${endTick}, Active Voices: ${this._synthesizer.activeVoiceCount}` - ); + const endTime: number = this._sequencer.currentEndTime; + const endTick: number = this._sequencer.currentEndTick; + + const mode = this._sequencer.isPlayingMain + ? 'main' + : this._sequencer.isPlayingCountIn + ? 'count-in' + : 'one-time'; + + Logger.debug( + 'AlphaSynth', + `Position changed: (time: ${currentTime}/${endTime}, tick: ${currentTick}/${endTick}, Active Voices: ${this._synthesizer.activeVoiceCount} (${mode})` + ); + + if (this._sequencer.isPlayingMain) { (this.positionChanged as EventEmitterOfT).trigger( new PositionChangedEventArgs(currentTime, endTime, currentTick, endTick, isSeek) ); @@ -460,8 +508,12 @@ export class AlphaSynth implements IAlphaSynth { readonly soundFontLoadFailed: IEventEmitterOfT = new EventEmitterOfT(); readonly midiLoaded: IEventEmitterOfT = new EventEmitterOfT(); readonly midiLoadFailed: IEventEmitterOfT = new EventEmitterOfT(); - readonly stateChanged: IEventEmitterOfT = new EventEmitterOfT(); - readonly positionChanged: IEventEmitterOfT = new EventEmitterOfT(); - readonly midiEventsPlayed: IEventEmitterOfT = new EventEmitterOfT(); - readonly playbackRangeChanged: IEventEmitterOfT = new EventEmitterOfT(); + readonly stateChanged: IEventEmitterOfT = + new EventEmitterOfT(); + readonly positionChanged: IEventEmitterOfT = + new EventEmitterOfT(); + readonly midiEventsPlayed: IEventEmitterOfT = + new EventEmitterOfT(); + readonly playbackRangeChanged: IEventEmitterOfT = + new EventEmitterOfT(); } diff --git a/src/synth/MidiFileSequencer.ts b/src/synth/MidiFileSequencer.ts index c70d91fb0..6250162f8 100644 --- a/src/synth/MidiFileSequencer.ts +++ b/src/synth/MidiFileSequencer.ts @@ -49,6 +49,10 @@ export class MidiFileSequencer { private _oneTimeState: MidiSequencerState | null = null; private _countInState: MidiSequencerState | null = null; + public get isPlayingMain(): boolean { + return this._currentState == this._mainState; + } + public get isPlayingOneTimeMidi(): boolean { return this._currentState == this._oneTimeState; } @@ -63,15 +67,15 @@ export class MidiFileSequencer { this._currentState = this._mainState; } - public get playbackRange(): PlaybackRange | null { - return this._currentState.playbackRange; + public get mainPlaybackRange(): PlaybackRange | null { + return this._mainState.playbackRange; } - public set playbackRange(value: PlaybackRange | null) { - this._currentState.playbackRange = value; + public set mainPlaybackRange(value: PlaybackRange | null) { + this._mainState.playbackRange = value; if (value) { - this._currentState.playbackRangeStartTime = this.tickPositionToTimePositionWithSpeed(value.startTick, 1); - this._currentState.playbackRangeEndTime = this.tickPositionToTimePositionWithSpeed(value.endTick, 1); + this._mainState.playbackRangeStartTime = this.tickPositionToTimePositionWithSpeed(this._mainState, value.startTick, 1); + this._mainState.playbackRangeEndTime = this.tickPositionToTimePositionWithSpeed(this._mainState, value.endTick, 1); } } @@ -84,11 +88,11 @@ export class MidiFileSequencer { /** * Gets the duration of the song in ticks. */ - public get endTick() { + public get currentEndTick() { return this._currentState.endTick; } - public get endTime(): number { + public get currentEndTime(): number { return this._currentState.endTime / this.playbackSpeed; } @@ -97,51 +101,55 @@ export class MidiFileSequencer { */ public playbackSpeed: number = 1; - public seek(timePosition: number): void { + public mainSeek(timePosition: number): void { // map to speed=1 timePosition *= this.playbackSpeed; // ensure playback range - if (this.playbackRange) { - if (timePosition < this._currentState.playbackRangeStartTime) { - timePosition = this._currentState.playbackRangeStartTime; - } else if (timePosition > this._currentState.playbackRangeEndTime) { - timePosition = this._currentState.playbackRangeEndTime; + if (this.mainPlaybackRange) { + if (timePosition < this._mainState.playbackRangeStartTime) { + timePosition = this._mainState.playbackRangeStartTime; + } else if (timePosition > this._mainState.playbackRangeEndTime) { + timePosition = this._mainState.playbackRangeEndTime; } } - if (timePosition > this._currentState.currentTime) { - this.silentProcess(timePosition - this._currentState.currentTime); - } else if (timePosition < this._currentState.currentTime) { + if (timePosition > this._mainState.currentTime) { + this.mainSilentProcess(timePosition - this._mainState.currentTime); + } else if (timePosition < this._mainState.currentTime) { // we have to restart the midi to make sure we get the right state: instruments, volume, pan, etc - this._currentState.currentTime = 0; - this._currentState.eventIndex = 0; - let metronomeVolume: number = this._synthesizer.metronomeVolume; - this._synthesizer.noteOffAll(true); - this._synthesizer.resetSoft(); - this._synthesizer.setupMetronomeChannel(metronomeVolume); - this.silentProcess(timePosition); + this._mainState.currentTime = 0; + this._mainState.eventIndex = 0; + if (this.isPlayingMain) { + let metronomeVolume: number = this._synthesizer.metronomeVolume; + this._synthesizer.noteOffAll(true); + this._synthesizer.resetSoft(); + this._synthesizer.setupMetronomeChannel(metronomeVolume); + } + this.mainSilentProcess(timePosition); } } - private silentProcess(milliseconds: number): void { + private mainSilentProcess(milliseconds: number): void { if (milliseconds <= 0) { return; } let start: number = Date.now(); - let finalTime: number = this._currentState.currentTime + milliseconds; + let finalTime: number = this._mainState.currentTime + milliseconds; - while (this._currentState.currentTime < finalTime) { - if (this.fillMidiEventQueueLimited(finalTime - this._currentState.currentTime)) { - this._synthesizer.synthesizeSilent(SynthConstants.MicroBufferSize); + if(this.isPlayingMain) { + while (this._mainState.currentTime < finalTime) { + if (this.fillMidiEventQueueLimited(finalTime - this._mainState.currentTime)) { + this._synthesizer.synthesizeSilent(SynthConstants.MicroBufferSize); + } } } - - this._currentState.currentTime = finalTime; + + this._mainState.currentTime = finalTime; let duration: number = Date.now() - start; - Logger.debug('Sequencer', 'Silent seek finished in ' + duration + 'ms'); + Logger.debug('Sequencer', 'Silent seek finished in ' + duration + 'ms (main)'); } public loadOneTimeMidi(midiFile: MidiFile): void { @@ -272,21 +280,25 @@ export class MidiFileSequencer { return anyEventsDispatched; } - public tickPositionToTimePosition(tickPosition: number): number { - return this.tickPositionToTimePositionWithSpeed(tickPosition, this.playbackSpeed); + public mainTickPositionToTimePosition(tickPosition: number): number { + return this.tickPositionToTimePositionWithSpeed(this._mainState, tickPosition, this.playbackSpeed); } - public timePositionToTickPosition(timePosition: number): number { - return this.timePositionToTickPositionWithSpeed(timePosition, this.playbackSpeed); + public mainTimePositionToTickPosition(timePosition: number): number { + return this.timePositionToTickPositionWithSpeed(this._mainState, timePosition, this.playbackSpeed); + } + + public currentTimePositionToTickPosition(timePosition: number): number { + return this.timePositionToTickPositionWithSpeed(this._currentState, timePosition, this.playbackSpeed); } - private tickPositionToTimePositionWithSpeed(tickPosition: number, playbackSpeed: number): number { + private tickPositionToTimePositionWithSpeed(state: MidiSequencerState, tickPosition: number, playbackSpeed: number): number { let timePosition: number = 0.0; let bpm: number = 120.0; let lastChange: number = 0; // find start and bpm of last tempo change before time - for (const c of this._currentState.tempoChanges) { + for (const c of state.tempoChanges) { if (tickPosition < c.ticks) { break; } @@ -298,12 +310,12 @@ export class MidiFileSequencer { // add the missing millis tickPosition -= lastChange; - timePosition += tickPosition * (60000.0 / (bpm * this._currentState.division)); + timePosition += tickPosition * (60000.0 / (bpm * state.division)); return timePosition / playbackSpeed; } - private timePositionToTickPositionWithSpeed(timePosition: number, playbackSpeed: number): number { + private timePositionToTickPositionWithSpeed(state: MidiSequencerState, timePosition: number, playbackSpeed: number): number { timePosition *= playbackSpeed; let ticks: number = 0; @@ -311,7 +323,7 @@ export class MidiFileSequencer { let lastChange: number = 0; // find start and bpm of last tempo change before time - for (const c of this._currentState.tempoChanges) { + for (const c of state.tempoChanges) { if (timePosition < c.time) { break; } @@ -322,13 +334,17 @@ export class MidiFileSequencer { // add the missing ticks timePosition -= lastChange; - ticks += (timePosition / (60000.0 / (bpm * this._currentState.division))) | 0; + ticks += (timePosition / (60000.0 / (bpm * state.division))) | 0; // we add 1 for possible rounding errors.(floating point issuses) return ticks + 1; } private get internalEndTime(): number { - return !this.playbackRange ? this._currentState.endTime : this._currentState.playbackRangeEndTime; + if (this.isPlayingMain) { + return !this.mainPlaybackRange ? this._currentState.endTime : this._currentState.playbackRangeEndTime; + } else { + return this._currentState.endTime; + } } public get isFinished(): boolean { @@ -336,13 +352,13 @@ export class MidiFileSequencer { } public stop(): void { - if (!this.playbackRange) { + if (this.isPlayingMain && this.mainPlaybackRange) { + this._currentState.currentTime = this.mainPlaybackRange.startTick; + } else { this._currentState.currentTime = 0; - this._currentState.eventIndex = 0; - } else if (this.playbackRange) { - this._currentState.currentTime = this.playbackRange.startTick; - this._currentState.eventIndex = 0; } + + this._currentState.eventIndex = 0; } public resetOneTimeMidi() {