diff --git a/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs b/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs index a81f0bf68..ef937cb9c 100644 --- a/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs +++ b/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs @@ -111,6 +111,11 @@ public void Pause() DispatchOnWorkerThread(() => { Player.Pause(); }); } + public void PlayOneTimeMidiFile(MidiFile midiFile) + { + DispatchOnWorkerThread(() => { Player.PlayOneTimeMidiFile(midiFile); }); + } + public void PlayPause() { DispatchOnWorkerThread(() => { Player.PlayPause(); }); diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index 2eb58eac8..14e62a2d7 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -40,6 +40,7 @@ import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { ModelUtils } from '@src/model/ModelUtils'; import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; +import { Note } from './model/Note'; class SelectionInfo { public beat: Beat; @@ -637,6 +638,43 @@ export class AlphaTabApiBase { this.player.stop(); } + /** + * Triggers the play of the given beat. This will stop the any other current ongoing playback. + * @param beat the single beat to play + */ + public playBeat(beat: Beat): void { + if (!this.player) { + return; + } + + // we generate a new midi file containing only the beat + let midiFile: MidiFile = new MidiFile(); + let handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile); + let generator: MidiFileGenerator = new MidiFileGenerator(beat.voice.bar.staff.track.score, this.settings, handler); + generator.generateSingleBeat(beat); + + this.player.playOneTimeMidiFile(midiFile); + } + + /** + * Triggers the play of the given note. This will stop the any other current ongoing playback. + * @param beat the single note to play + */ + public playNote(note: Note): void { + if (!this.player) { + return; + } + + // we generate a new midi file containing only the beat + let midiFile: MidiFile = new MidiFile(); + let handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile); + let generator: MidiFileGenerator = new MidiFileGenerator(note.beat.voice.bar.staff.track.score, this.settings, handler); + generator.generateSingleNote(note); + + this.player.playOneTimeMidiFile(midiFile); + } + + private _cursorWrapper: IContainer | null = null; private _barCursor: IContainer | null = null; private _beatCursor: IContainer | null = null; @@ -969,7 +1007,7 @@ export class AlphaTabApiBase { if (!this._tickCache) { return; } - if(range) { + if (range) { const startBeat = this._tickCache.findBeat(this.tracks, range.startTick); const endBeat = this._tickCache.findBeat(this.tracks, range.endTick); if (startBeat && endBeat) { @@ -980,7 +1018,7 @@ export class AlphaTabApiBase { } else { this.cursorSelectRange(null, null); } - + } private setupClickHandling(): void { diff --git a/src/midi/MidiFileGenerator.ts b/src/midi/MidiFileGenerator.ts index 68ef4f217..ad199fb35 100644 --- a/src/midi/MidiFileGenerator.ts +++ b/src/midi/MidiFileGenerator.ts @@ -1265,4 +1265,71 @@ export class MidiFileGenerator { break; } } + + public prepareSingleBeat(beat: Beat) { + // collect tempo and program at given beat + let tempo = -1; + let program = -1; + + // traverse to previous beats until we maybe hit the automations needed + let currentBeat: Beat | null = beat; + while (currentBeat && (tempo === -1 || program === -1)) { + for (const automation of beat.automations) { + switch (automation.type) { + case AutomationType.Instrument: + program = automation.value; + break; + case AutomationType.Tempo: + tempo = automation.value; + break; + } + } + currentBeat = currentBeat.previousBeat; + } + + const track = beat.voice.bar.staff.track; + const masterBar = beat.voice.bar.masterBar; + if (tempo === -1) { + tempo = masterBar.score.tempo; + } + + if (program === -1) { + program = track.playbackInfo.program; + } + + const volume = track.playbackInfo.volume; + + // setup channel + this.generateTrack(track); + this._handler.addTimeSignature(0, masterBar.timeSignatureNumerator, masterBar.timeSignatureDenominator); + this._handler.addTempo(0, tempo); + + + let volumeCoarse: number = MidiFileGenerator.toChannelShort(volume); + this._handler.addControlChange( + 0, + 0, + track.playbackInfo.primaryChannel, + ControllerType.VolumeCoarse, + volumeCoarse + ); + this._handler.addControlChange( + 0, + 0, + track.playbackInfo.secondaryChannel, + ControllerType.VolumeCoarse, + volumeCoarse + ); + } + + public generateSingleBeat(beat: Beat) { + this.prepareSingleBeat(beat); + + this.generateBeat(beat, -beat.playbackStart /* to bring it to 0*/, beat.voice.bar); + } + + public generateSingleNote(note: Note) { + this.prepareSingleBeat(note.beat); + this.generateNote(note, -note.beat.playbackStart, note.beat.playbackDuration, new Int32Array(note.beat.voice.bar.staff.tuning.length)); + } } diff --git a/src/platform/javascript/AlphaSynthWebWorker.ts b/src/platform/javascript/AlphaSynthWebWorker.ts index 40315cd9c..9d34aa40b 100644 --- a/src/platform/javascript/AlphaSynthWebWorker.ts +++ b/src/platform/javascript/AlphaSynthWebWorker.ts @@ -90,6 +90,9 @@ export class AlphaSynthWebWorker { case 'alphaSynth.stop': this._player.stop(); break; + case 'alphaSynth.playOneTimeMidiFile': + this._player.playOneTimeMidiFile(JsonConverter.jsObjectToMidiFile(data.midi)); + break; case 'alphaSynth.loadSoundFontBytes': this._player.loadSoundFont(data.data, data.append); break; diff --git a/src/platform/javascript/AlphaSynthWebWorkerApi.ts b/src/platform/javascript/AlphaSynthWebWorkerApi.ts index 9be6af58b..9eb022bc6 100644 --- a/src/platform/javascript/AlphaSynthWebWorkerApi.ts +++ b/src/platform/javascript/AlphaSynthWebWorkerApi.ts @@ -232,6 +232,13 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { }); } + public playOneTimeMidiFile(midi: MidiFile): void { + this._synth.postMessage({ + cmd: 'alphaSynth.playOneTimeMidiFile', + midi: JsonConverter.midiFileToJsObject(midi) + }); + } + public loadSoundFont(data: Uint8Array, append: boolean): void { this._synth.postMessage({ cmd: 'alphaSynth.loadSoundFontBytes', diff --git a/src/synth/AlphaSynth.ts b/src/synth/AlphaSynth.ts index 80f4a94f7..096f7a747 100644 --- a/src/synth/AlphaSynth.ts +++ b/src/synth/AlphaSynth.ts @@ -178,7 +178,7 @@ export class AlphaSynth implements IAlphaSynth { } public play(): boolean { - if (this.state === PlayerState.Playing || !this._isMidiLoaded) { + if (this.state !== PlayerState.Paused || !this._isMidiLoaded) { return false; } this.output.activate(); @@ -206,7 +206,7 @@ export class AlphaSynth implements IAlphaSynth { } public playPause(): void { - if (this.state === PlayerState.Playing || !this._isMidiLoaded) { + if (this.state !== PlayerState.Paused || !this._isMidiLoaded) { this.pause(); } else { this.play(); @@ -227,6 +227,22 @@ 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(); + } public resetSoundFonts(): void { this.stop(); @@ -304,7 +320,6 @@ export class AlphaSynth implements IAlphaSynth { private onSamplesPlayed(sampleCount: number): void { let playedMillis: number = (sampleCount / this._synthesizer.outSampleRate) * 1000; this.updateTimePosition(this._timePosition + playedMillis); - this.checkForFinish(); } @@ -320,12 +335,19 @@ export class AlphaSynth implements IAlphaSynth { if (this._tickPosition >= endTick) { Logger.debug('AlphaSynth', 'Finished playback'); - (this.finished as EventEmitter).trigger(); - - if (this.isLooping) { - this.tickPosition = startTick; + if(this._sequencer.isPlayingOneTimeMidi) { + this._sequencer.resetOneTimeMidi(); + this.state = PlayerState.Paused; + this.output.pause(); + this._synthesizer.noteOffAll(false); } else { - this.stop(); + (this.finished as EventEmitter).trigger(); + + if (this.isLooping) { + this.tickPosition = startTick; + } else { + this.stop(); + } } } } @@ -336,13 +358,16 @@ export class AlphaSynth implements IAlphaSynth { const currentTick: number = (this._tickPosition = this._sequencer.timePositionToTickPosition(currentTime)); const endTime: number = this._sequencer.endTime; const endTick: number = this._sequencer.endTick; - Logger.debug( - 'AlphaSynth', - `Position changed: (time: ${currentTime}/${endTime}, tick: ${currentTick}/${endTick}, Active Voices: ${this._synthesizer.activeVoiceCount}` - ); - (this.positionChanged as EventEmitterOfT).trigger( - new PositionChangedEventArgs(currentTime, endTime, currentTick, endTick) - ); + + if(!this._sequencer.isPlayingOneTimeMidi) { + Logger.debug( + 'AlphaSynth', + `Position changed: (time: ${currentTime}/${endTime}, tick: ${currentTick}/${endTick}, Active Voices: ${this._synthesizer.activeVoiceCount}` + ); + (this.positionChanged as EventEmitterOfT).trigger( + new PositionChangedEventArgs(currentTime, endTime, currentTick, endTick) + ); + } } readonly ready: IEventEmitter = new EventEmitter(); diff --git a/src/synth/IAlphaSynth.ts b/src/synth/IAlphaSynth.ts index e8fa7feb7..ac96d0f3c 100644 --- a/src/synth/IAlphaSynth.ts +++ b/src/synth/IAlphaSynth.ts @@ -91,6 +91,12 @@ 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 + */ + playOneTimeMidiFile(midi: MidiFile): void; /** * Loads a soundfont from the given data diff --git a/src/synth/MidiFileSequencer.ts b/src/synth/MidiFileSequencer.ts index b359512f0..4e84a16ef 100644 --- a/src/synth/MidiFileSequencer.ts +++ b/src/synth/MidiFileSequencer.ts @@ -21,36 +21,49 @@ export class MidiFileSequencerTempoChange { } } +class MidiSequencerState { + public tempoChanges: MidiFileSequencerTempoChange[] = []; + public firstProgramEventPerChannel: Map = new Map(); + public synthData: SynthEvent[] = []; + public division: number = 0; + public eventIndex: number = 0; + public currentTime: number = 0; + public playbackRange: PlaybackRange | null = null; + public playbackRangeStartTime: number = 0; + public playbackRangeEndTime: number = 0; + public endTick:number = 0; + public endTime:number = 0; +} + /** * This sequencer dispatches midi events to the synthesizer based on the current * synthesize position. The sequencer does not consider the playback speed. */ export class MidiFileSequencer { private _synthesizer: TinySoundFont; - private _tempoChanges: MidiFileSequencerTempoChange[] = []; - private _firstProgramEventPerChannel: Map = new Map(); - private _synthData: SynthEvent[] = []; - private _division: number = 0; - private _eventIndex: number = 0; - private _currentTime: number = 0; - private _playbackRange: PlaybackRange | null = null; - private _playbackRangeStartTime: number = 0; - private _playbackRangeEndTime: number = 0; - private _endTime: number = 0; + private _currentState: MidiSequencerState; + private _mainState: MidiSequencerState; + private _oneTimeState: MidiSequencerState | null = null; + + public get isPlayingOneTimeMidi(): boolean { + return this._currentState == this._oneTimeState; + } public constructor(synthesizer: TinySoundFont) { this._synthesizer = synthesizer; + this._mainState = new MidiSequencerState(); + this._currentState = this._mainState; } public get playbackRange(): PlaybackRange | null { - return this._playbackRange; + return this._currentState.playbackRange; } public set playbackRange(value: PlaybackRange | null) { - this._playbackRange = value; + this._currentState.playbackRange = value; if (value) { - this._playbackRangeStartTime = this.tickPositionToTimePositionWithSpeed(value.startTick, 1); - this._playbackRangeEndTime = this.tickPositionToTimePositionWithSpeed(value.endTick, 1); + this._currentState.playbackRangeStartTime = this.tickPositionToTimePositionWithSpeed(value.startTick, 1); + this._currentState.playbackRangeEndTime = this.tickPositionToTimePositionWithSpeed(value.endTick, 1); } } @@ -59,10 +72,12 @@ export class MidiFileSequencer { /** * Gets the duration of the song in ticks. */ - public endTick: number = 0; + public get endTick() { + return this._currentState.endTick; + } public get endTime(): number { - return this._endTime / this.playbackSpeed; + return this._currentState.endTime / this.playbackSpeed; } /** @@ -73,13 +88,13 @@ export class MidiFileSequencer { public seek(timePosition: number): void { // map to speed=1 timePosition *= this.playbackSpeed; - + // ensure playback range if (this.playbackRange) { - if (timePosition < this._playbackRangeStartTime) { - timePosition = this._playbackRangeStartTime; - } else if (timePosition > this._playbackRangeEndTime) { - timePosition = this._playbackRangeEndTime; + if (timePosition < this._currentState.playbackRangeStartTime) { + timePosition = this._currentState.playbackRangeStartTime; + } else if (timePosition > this._currentState.playbackRangeEndTime) { + timePosition = this._currentState.playbackRangeEndTime; } } @@ -89,12 +104,12 @@ export class MidiFileSequencer { timePosition = 0; } - if (timePosition > this._currentTime) { - this.silentProcess(timePosition - this._currentTime); - } else if (timePosition < this._currentTime) { + if (timePosition > this._currentState.currentTime) { + this.silentProcess(timePosition - this._currentState.currentTime); + } else if (timePosition < this._currentState.currentTime) { // we have to restart the midi to make sure we get the right state: instruments, volume, pan, etc - this._currentTime = 0; - this._eventIndex = 0; + this._currentState.currentTime = 0; + this._currentState.eventIndex = 0; let metronomeVolume: number = this._synthesizer.metronomeVolume; this._synthesizer.noteOffAll(true); this._synthesizer.resetSoft(); @@ -109,10 +124,10 @@ export class MidiFileSequencer { } let start: number = Date.now(); - let finalTime: number = this._currentTime + milliseconds; + let finalTime: number = this._currentState.currentTime + milliseconds; - while (this._currentTime < finalTime) { - if (this.fillMidiEventQueueLimited(finalTime - this._currentTime)) { + while (this._currentState.currentTime < finalTime) { + if (this.fillMidiEventQueueLimited(finalTime - this._currentState.currentTime)) { this._synthesizer.synthesizeSilent(SynthConstants.MicroBufferSize); } } @@ -121,15 +136,27 @@ export class MidiFileSequencer { Logger.debug('Sequencer', 'Silent seek finished in ' + duration + 'ms'); } + public loadOneTimeMidi(midiFile: MidiFile): void { + this._oneTimeState = this.createStateFromFile(midiFile); + this._currentState = this._oneTimeState; + } + public loadMidi(midiFile: MidiFile): void { - this._tempoChanges = []; + this._mainState = this.createStateFromFile(midiFile); + this._currentState = this._mainState; + } + + public createStateFromFile(midiFile: MidiFile): MidiSequencerState { + const state = new MidiSequencerState(); + + state.tempoChanges = []; - this._division = midiFile.division; - this._eventIndex = 0; - this._currentTime = 0; + state.division = midiFile.division; + state.eventIndex = 0; + state.currentTime = 0; // build synth events. - this._synthData = []; + state.synthData = []; // Converts midi to milliseconds for easy sequencing let bpm: number = 120; @@ -142,8 +169,8 @@ export class MidiFileSequencer { let previousTick: number = 0; for (let mEvent of midiFile.events) { - let synthData: SynthEvent = new SynthEvent(this._synthData.length, mEvent); - this._synthData.push(synthData); + let synthData: SynthEvent = new SynthEvent(state.synthData.length, mEvent); + state.synthData.push(synthData); let deltaTick: number = mEvent.tick - previousTick; absTick += deltaTick; @@ -153,8 +180,8 @@ export class MidiFileSequencer { if (metronomeLength > 0) { while (metronomeTick < absTick) { - let metronome: SynthEvent = SynthEvent.newMetronomeEvent(this._synthData.length); - this._synthData.push(metronome); + let metronome: SynthEvent = SynthEvent.newMetronomeEvent(state.synthData.length); + state.synthData.push(metronome); metronome.time = metronomeTime; metronomeTick += metronomeLength; metronomeTime += metronomeLength * (60000.0 / (bpm * midiFile.division)); @@ -164,20 +191,20 @@ export class MidiFileSequencer { if (mEvent.command === MidiEventType.Meta && mEvent.data1 === MetaEventType.Tempo) { let meta: MetaNumberEvent = mEvent as MetaNumberEvent; bpm = 60000000 / meta.value; - this._tempoChanges.push(new MidiFileSequencerTempoChange(bpm, absTick, absTime)); + state.tempoChanges.push(new MidiFileSequencerTempoChange(bpm, absTick, absTime)); } else if (mEvent.command === MidiEventType.Meta && mEvent.data1 === MetaEventType.TimeSignature) { let meta: MetaDataEvent = mEvent as MetaDataEvent; let timeSignatureDenominator: number = Math.pow(2, meta.data[1]); - metronomeLength = (this._division * (4.0 / timeSignatureDenominator)) | 0; + metronomeLength = (state.division * (4.0 / timeSignatureDenominator)) | 0; } else if (mEvent.command === MidiEventType.ProgramChange) { let channel: number = mEvent.channel; - if (!this._firstProgramEventPerChannel.has(channel)) { - this._firstProgramEventPerChannel.set(channel, synthData); + if (!state.firstProgramEventPerChannel.has(channel)) { + state.firstProgramEventPerChannel.set(channel, synthData); } } } - this._synthData.sort((a, b) => { + state.synthData.sort((a, b) => { if (a.time > b.time) { return 1; } @@ -186,8 +213,10 @@ export class MidiFileSequencer { } return a.eventIndex - b.eventIndex; }); - this._endTime = absTime; - this.endTick = absTick; + state.endTime = absTime; + state.endTick = absTick; + + return state; } public fillMidiEventQueue(): boolean { @@ -200,21 +229,21 @@ export class MidiFileSequencer { let endTime: number = this.internalEndTime; if (maxMilliseconds > 0) { // ensure that first microbuffer does not already exceed max time - if( maxMilliseconds < millisecondsPerBuffer) { + if (maxMilliseconds < millisecondsPerBuffer) { millisecondsPerBuffer = maxMilliseconds; } - endTime = Math.min(this.internalEndTime, this._currentTime + maxMilliseconds); + endTime = Math.min(this.internalEndTime, this._currentState.currentTime + maxMilliseconds); } let anyEventsDispatched: boolean = false; - this._currentTime += millisecondsPerBuffer; + this._currentState.currentTime += millisecondsPerBuffer; while ( - this._eventIndex < this._synthData.length && - this._synthData[this._eventIndex].time < this._currentTime && - this._currentTime < endTime + this._currentState.eventIndex < this._currentState.synthData.length && + this._currentState.synthData[this._currentState.eventIndex].time < this._currentState.currentTime && + this._currentState.currentTime < endTime ) { - this._synthesizer.dispatchEvent(this._synthData[this._eventIndex]); - this._eventIndex++; + this._synthesizer.dispatchEvent(this._currentState.synthData[this._currentState.eventIndex]); + this._currentState.eventIndex++; anyEventsDispatched = true; } @@ -235,7 +264,7 @@ export class MidiFileSequencer { let lastChange: number = 0; // find start and bpm of last tempo change before time - for (const c of this._tempoChanges) { + for (const c of this._currentState.tempoChanges) { if (tickPosition < c.ticks) { break; } @@ -247,7 +276,7 @@ export class MidiFileSequencer { // add the missing millis tickPosition -= lastChange; - timePosition += tickPosition * (60000.0 / (bpm * this._division)); + timePosition += tickPosition * (60000.0 / (bpm * this._currentState.division)); return timePosition / playbackSpeed; } @@ -260,7 +289,7 @@ export class MidiFileSequencer { let lastChange: number = 0; // find start and bpm of last tempo change before time - for (const c of this._tempoChanges) { + for (const c of this._currentState.tempoChanges) { if (timePosition < c.time) { break; } @@ -268,29 +297,34 @@ export class MidiFileSequencer { bpm = c.bpm; lastChange = c.time; } - + // add the missing ticks timePosition -= lastChange; - ticks += (timePosition / (60000.0 / (bpm * this._division))) | 0; + ticks += (timePosition / (60000.0 / (bpm * this._currentState.division))) | 0; // we add 1 for possible rounding errors.(floating point issuses) return ticks + 1; } private get internalEndTime(): number { - return !this.playbackRange ? this._endTime : this._playbackRangeEndTime; + return !this.playbackRange ? this._currentState.endTime : this._currentState.playbackRangeEndTime; } public get isFinished(): boolean { - return this._currentTime >= this.internalEndTime; + return this._currentState.currentTime >= this.internalEndTime; } public stop(): void { if (!this.playbackRange) { - this._currentTime = 0; - this._eventIndex = 0; + this._currentState.currentTime = 0; + this._currentState.eventIndex = 0; } else if (this.playbackRange) { - this._currentTime = this.playbackRange.startTick; - this._eventIndex = 0; + this._currentState.currentTime = this.playbackRange.startTick; + this._currentState.eventIndex = 0; } } + + public resetOneTimeMidi() { + this._oneTimeState = null; + this._currentState = this._mainState; + } }