import { ITimingSource } from "../Common/Interfaces/ITimingSource"; import { IMessageViewer } from "../Common/Interfaces/IMessageViewer"; import { IAudioPlayer } from "../Common/Interfaces/IAudioPlayer"; import { MusicPartManager, MusicPartManagerIterator } from "../MusicalScore/MusicParts"; import { PlaybackIterator } from "../MusicalScore/Playback/PlaybackIterator"; import { Dictionary } from "typescript-collections"; import { Staff, SourceMeasure, VoiceEntry, Note, MidiInstrument } from "../MusicalScore/VoiceData"; import { Fraction } from "../Common/DataObjects"; import { MetronomeInstrument } from "./MetronomeInstrument"; import { CursorPosChangedData } from "../Common/DataObjects/CursorPosChangedData"; import { Repetition } from "../MusicalScore/MusicSource"; import { TextTranslation } from "../Common/Strings/TextTranslation"; import { NoteState, Instrument, SubInstrument, MusicSheet } from "../MusicalScore"; import { DynamicsContainer } from "../MusicalScore/VoiceData/HelperObjects"; import { PlaybackEntry } from "../MusicalScore/Playback/PlaybackEntry"; import { ContinuousDynamicExpression } from "../MusicalScore/VoiceData/Expressions/ContinuousExpressions"; import { PlaybackNote } from "../MusicalScore/Playback/PlaybackNote"; import log from "loglevel"; import { IAudioMetronomePlayer } from "../Common/Interfaces/IAudioMetronomePlayer"; import { ISettableInstrument } from "../Common/Interfaces/ISettableInstrument"; import { PlaybackState, MessageBoxType } from "../Common/Enums/PsEnums"; import { IPlaybackListener } from "../Common/Interfaces/IPlaybackListener"; import { IPlaybackParametersListener } from "../Common/Interfaces/IPlaybackParametersListener"; import { AbstractExpression } from "../MusicalScore/VoiceData/Expressions"; export class ChannelNote { public note: PlaybackNote; public key: number; public channel: number; constructor(k: number, c: number, n: PlaybackNote = undefined) { this.note = n; this.key = k; this.channel = c; } } export class PlaybackManager implements IPlaybackParametersListener { protected timingSource: ITimingSource; protected resetRequested: boolean; protected loopTriggeredReset: boolean; protected tempoUserFactor: number; protected currentBPM: number; protected listeners: IPlaybackListener[] = []; public addListener(listener: IPlaybackListener): void { this.listeners.push(listener); } private readonly percussionChannel: number = 9; // this is a definition of the midi interface (cannot be changed) private messageViewer: IMessageViewer; private audioMetronomePlayer: IAudioMetronomePlayer; private audioPlayer: IAudioPlayer; private musicPartManager: MusicPartManager; private cursorIterator: MusicPartManagerIterator; get Iterator(): MusicPartManagerIterator { return this.cursorIterator; } private playbackIterator: PlaybackIterator; //private Dictionary instrumentsPerMidiSoundDict = new Dictionary(); //private Dictionary midiSoundToChannelMappingDict = new Dictionary(); //private int[] midiChannelToSoundArray = new int[16]; //Staff is not considered Unique for key purposes here. Had to use something unique - staff ID private instrumentToStaffToMidiChannelDict: Dictionary = new Dictionary(); //store this data just in case private instrumentIdMapping: Dictionary = new Dictionary(); public get InstrumentIdMapping(): Dictionary { return this.instrumentIdMapping; } //private List staffIndexToMidiChannelMapping = new List(); private freeMidiChannels: number[] = []; private notesToStop: Dictionary = new Dictionary(); private metronomeNote: ChannelNote = new ChannelNote(88, this.percussionChannel); private metronomeNoteFirstBeat: ChannelNote = new ChannelNote(64, this.percussionChannel); private currentMeasure: SourceMeasure = undefined; private currentTimestamp: Fraction = undefined; private closestNextTimestamp: Fraction = undefined; private currentMetronomeBaseTimestamp: Fraction = undefined; private currentBeatDuration: Fraction = undefined; private currentIteratorSourceTimeStamp: Fraction = undefined; private beatCounter: number = 0; protected runningState: PlaybackState = PlaybackState.Stopped; private isRunning: boolean = false; private isInitialized: boolean = false; // make sure midi device gets opened only once private nextIteratorTimestamp: Fraction; private playNextMetronomeAt: Fraction; // private masterTranspose: number = 0; private isPlaying: boolean = false; private metronome: MetronomeInstrument; private metronomeSoundPlayed: boolean = false; private tempoImpactFactor: number = 1.0; private sheetStartBPM: number; private currentReferenceBPM: number; private readonly defaultVolume: number = 0.8; private currentVolume: number = this.defaultVolume; private dynamicImpactFactor: number = 1.0; private scorePositionChangedData: CursorPosChangedData = new CursorPosChangedData(); private tooManyInstruments: boolean = false; private currentRepetition: Repetition; private currentMeasureIndex: number; private metronomeOnlyBPM: number = 100; // private playbackThreadSyncObject = new object(); // TODO MB: Handle this. private readonly highlightPlayedNotes: boolean = false; private startRhythmBeats: number; private startRhythmDenominator: number; private isPreCounting: boolean; private fermataActive: boolean; private doPreCount: boolean = true; constructor (timingSource: ITimingSource, audioMetronomePlayer: IAudioMetronomePlayer, audioPlayer: IAudioPlayer, messageViewer: IMessageViewer) { const metronomeLabel: string = TextTranslation.translateText("Playback/LabelMetronome", "Metronome"); this.metronome = new MetronomeInstrument(-1, metronomeLabel, false, true, 0.0, MidiInstrument.Percussion); this.timingSource = timingSource; this.audioMetronomePlayer = audioMetronomePlayer; this.audioPlayer = audioPlayer; this.messageViewer = messageViewer; } public get RunningState(): PlaybackState { return this.runningState; } public set RunningState(value: PlaybackState) { this.runningState = value; } public DoPlayback: boolean; /** Do the initial pre-count */ public get DoPreCount(): boolean { return this.doPreCount; } public set DoPreCount(value: boolean) { if (this.doPreCount !== value) { this.doPreCount = value; } } public PreCountBeats: number; public get Metronome(): ISettableInstrument { return this.metronome; } public get MetronomeOnlyBPM(): number { return this.metronomeOnlyBPM; } public set MetronomeOnlyBPM(value: number) { this.metronomeOnlyBPM = value; } // public get Transpose(): number { // return this.masterTranspose; // } // public set Transpose(value: number) { // this.masterTranspose = value; // } public get OriginalBpm(): number { return this.currentReferenceBPM; } /** will be activated when any solo flag of an Instrument, Voice or Staff is set to true. */ public SoloActive: boolean; public SoloAttenuationValue: number = 0; // Only used for debug and scheduling precision measurements //private wantedNextIteratorTimestampMs: number = 0; public playVoiceEntry(voiceEntry: VoiceEntry): void { const ve: VoiceEntry = voiceEntry; if (ve !== undefined) { // lock(this.playbackThreadSyncObject) { this.stopAllCurrentlyPlayingNotes(); if (this.highlightPlayedNotes) { const notes: Note[] = []; for (const note of ve.Notes) { note.state = NoteState.Selected; notes.push(note); } //this.NotesPlaybackEventOccurred(notes); } //int staffIndex = this.musicPartManager.MusicSheet.getIndexFromStaff(ve.Notes[0].ParentStaff); const channel: number = this.instrumentToStaffToMidiChannelDict.getValue(ve.Notes[0].ParentStaff); const instrument: Instrument = ve.ParentVoice.Parent; const isPercussion: boolean = instrument.MidiInstrumentId === MidiInstrument.Percussion; const volume: number = 0.8; const notesToPlay: ChannelNote[] = []; const transpose: number = this.musicPartManager.MusicSheet.Transpose; const instrumentPlaybackTranspose: number = ve.ParentVoice.Parent.PlaybackTranspose; for (const note of ve.MainPlaybackEntry.Notes.filter(n => n.MidiKey !== 0)) { // play the note let key: number = note.MidiKey; if (!isPercussion) { key += instrumentPlaybackTranspose + transpose; } if (note.ParentNote.PlaybackInstrumentId !== undefined) { const notePlaybackInstrument: SubInstrument = instrument.getSubInstrument(note.ParentNote.PlaybackInstrumentId); if (notePlaybackInstrument !== undefined) { if (notePlaybackInstrument.fixedKey >= 0) { key = notePlaybackInstrument.fixedKey; } } } // calculate stop time and remember it // const stopAt: Fraction = Fraction.plus(this.cursorIterator.CurrentEnrolledTimestamp, note.Length); try { if (this.audioPlayer !== undefined) { //const noteLengthFraction: Fraction = Fraction.createFromFraction(note.Length); this.audioPlayer.playSound(channel, key, volume, 500); } } catch (ex) { log.info("PlaybackManager.playVoiceEntry: ", ex); } notesToPlay.push(new ChannelNote(key, channel, note)); } // TODO MB: Handle this // Task stopper = new Task(() => { // EventWaitHandle waiter = new EventWaitHandle(false, EventResetMode.AutoReset); // waiter.WaitOne(200); // lock(this.playbackThreadSyncObject) { // if (this.audioPlayer !== undefined) { // foreach(var n in notesToPlay) { // this.audioPlayer.stopSound(n.channel, n.key); // } // } // } // // redraw to color notes normal if highlighted in playback // //this.phonicScoreInterface.RedrawGraphicalMusicSheet(); // }); // stopper.Start(); // } } } public initialize(musicPartMng: MusicPartManager): void { // lock(this.playbackThreadSyncObject) { if (this.isInitialized) { this.stopAllCurrentlyPlayingNotes(); if (this.audioPlayer !== undefined) { this.audioPlayer.close(); } this.cursorIterator = undefined; this.playbackIterator = undefined; } this.isInitialized = false; this.musicPartManager = musicPartMng; if (this.musicPartManager !== undefined) { const musicSheet: MusicSheet = this.musicPartManager.MusicSheet; // TODO MB: Converted musicSheetParameterChanged to setBpm in this file. Handle following line. //musicSheet.MusicSheetParameterChanged += this.musicSheetParameterChanged; this.cursorIterator = this.musicPartManager.getIterator(); this.playbackIterator = new PlaybackIterator(musicSheet); if (this.audioPlayer !== undefined) { // TODO MB: I rewrote following line in line below. Does it do what it's supposed to do? Array.from() not supported in IE // List < MidiInstrument > uniqueMidiInstruments = musicSheet.Instruments.Select(item => item.MidiInstrumentId).Distinct().ToList(); const uniqueMidiInstruments: MidiInstrument[] = Array.from(new Set(musicSheet.Instruments.map(item => item.MidiInstrumentId))); this.audioPlayer.open(uniqueMidiInstruments, 16); // set drums: this.audioPlayer.setSound(this.percussionChannel, 115); } this.currentReferenceBPM = this.sheetStartBPM = musicSheet.getExpressionsStartTempoInBPM(); this.tempoUserFactor = musicSheet.userStartTempoInBPM / this.sheetStartBPM; let instrumentId: number = 0; this.tooManyInstruments = false; // reset the dicts and channel mappings //this.staffIndexToMidiChannelMapping.Clear(); this.instrumentToStaffToMidiChannelDict.clear(); this.instrumentIdMapping.clear(); for (let i: number = 0; i < this.percussionChannel; i++) { this.freeMidiChannels.push(i); } for (let i: number = this.percussionChannel + 1; i < 16; i++) { this.freeMidiChannels.push(i); } for (const instrument of musicSheet.Instruments) { this.instrumentIdMapping.setValue(instrumentId, instrument); for (const staff of instrument.Staves) { // just add a list element - calcMidiChannel() will provide the right value. //this.staffIndexToMidiChannelMapping.Add(-1); this.instrumentToStaffToMidiChannelDict.setValue(staff, -1); } this.setSound(instrumentId, instrument.MidiInstrumentId); instrumentId++; } if (this.audioPlayer !== undefined && this.tooManyInstruments) { const errorMsg: string = TextTranslation.translateText( "MidiNumberError", "This music sheet has more parts than are supported for midi playback. " + "Some parts will not be played with the desired instrument sounds." ); if (this.messageViewer !== undefined && this.messageViewer.MessageOccurred !== undefined) { this.messageViewer.MessageOccurred(MessageBoxType.Warning, errorMsg); } } this.checkForSoloDeactivated(); } this.isInitialized = true; // } this.reset(); } public async play(): Promise { if (this.cursorIterator !== undefined && this.cursorIterator.EndReached) { console.log("End reached, resetting"); this.reset(); } this.isPlaying = true; this.RunningState = PlaybackState.Running; await this.timingSource.start(); this.loop(); } public async pause(): Promise { // lock(this.playbackThreadSyncObject) { this.isPlaying = false; // stop all active midi notes: this.stopAllCurrentlyPlayingNotes(); // inform sample player to e.g. dispose used samples: if (this.audioPlayer !== undefined) { this.audioPlayer.playbackHasStopped(); } // notify delegates (coreContainer) that the playing has finished: this.RunningState = PlaybackState.Stopped; await this.timingSource.pause(); try { //bool endReached = this.iterator !== undefined && this.iterator.EndReached; for (const listener of this.listeners) { listener.pauseOccurred(undefined); } } catch (ex) { log.debug("PlaybackManager.pause: ", ex); } // } } public reset(): void { // lock(this.playbackThreadSyncObject) { //this.resetRequested = true; this.doReset(this.DoPreCount); if (this.musicPartManager === undefined) { return; } if (this.RunningState === PlaybackState.Stopped) { //this.isPlaying = true; } for (const listener of this.listeners) { listener.resetOccurred(undefined); } // } } public Dispose(): void { // lock(this.playbackThreadSyncObject) { this.listeners = []; this.isRunning = false; // stop all active midi notes: if (this.isInitialized) { this.stopAllCurrentlyPlayingNotes(); if (this.audioPlayer !== undefined) { this.audioPlayer.close(); } } // this.musicPartManager = undefined; // // } } public setSound(instrumentId: number, newSoundId: MidiInstrument): boolean { if (newSoundId <= MidiInstrument.None || newSoundId > MidiInstrument.Percussion) { return false; } // lock(this.playbackThreadSyncObject) { try { const isPercussionNow: boolean = newSoundId === MidiInstrument.Percussion; if (instrumentId === -1) { // Metronome if (this.audioPlayer !== undefined && !isPercussionNow) { this.audioPlayer.setSound(0, newSoundId); } } else { let neededLastChannel: boolean = false; const musicSheet: MusicSheet = this.musicPartManager.MusicSheet; let instrument: Instrument; if (instrumentId === -2) { instrument = musicSheet.Instruments.find(x => x.Id === instrumentId); } else { instrument = musicSheet.Instruments[instrumentId]; } this.instrumentIdMapping.setValue(instrument.Id, instrument); for (const staff of instrument.Staves) { //int staffIndex = musicSheet.getIndexFromStaff(staff); //int channel = this.staffIndexToMidiChannelMapping[staffIndex]; let channel: number = this.instrumentToStaffToMidiChannelDict.getValue(staff); const wasPercussion: boolean = channel === this.percussionChannel; if (isPercussionNow) { // if is now a percussion const oldChannel: number = channel; channel = this.percussionChannel; // check if this instrument has been initialized and was no percussion instrument: if (oldChannel > 0 && !wasPercussion) { this.freeMidiChannels.push(oldChannel); this.freeMidiChannels.sort((a, b) => a - b); //TODO MB: Does this .sort do the same thing as C# .Sort()? } } else { if (channel < 0 || wasPercussion) { // if is not initialized or was a percussion: if (this.freeMidiChannels.length > 0) { // if still a free channel exists // get the channel and remove in from the free channels list channel = this.freeMidiChannels[0]; this.freeMidiChannels.shift(); } else { // if no channel is free any more: this.tooManyInstruments = true; // use last channel channel = 15; this.instrumentToStaffToMidiChannelDict.setValue(staff, channel); //// use piano sound //newSoundId = 0; neededLastChannel = true; } } } this.instrumentToStaffToMidiChannelDict.setValue(staff, channel); if (this.audioPlayer !== undefined && !isPercussionNow) { // TODO: Uncomment when panaroma is supported in audio player // this.audioPlayer.setPanorama(channel, instrument.SubInstruments[0].pan); // TODO: Commented because AvailableComponents not defined // if (AvailableComponents.PLAYBACK_INSTRUMENTS_AVAILABLE) { // // only set instrument sounds in pro version: this.audioPlayer.setSound(channel, newSoundId); // } else { // play all as piano in free version: // this.audioPlayer.setSound(channel, 0); // } } } if (neededLastChannel) { return false; } } return true; } catch (ex) { log.info("PlaybackManager.setSound: ", ex); return false; } // } } // public mainParameterChanged(client: IPhonicScoreClient, settingType: ProgramParameters, currentValue, previousValue): void { // switch (settingType) { // case ProgramParameters.DynamicInstructionsImpact: // this.dynamicImpactFactor = Convert.ToSingle(currentValue); // break; // case ProgramParameters.TempoInstructionsImpact: { // this.tempoImpactFactor = Convert.ToSingle(currentValue); // this.setTempo(); // break; // } // } // } // TODO MB: Check if function setBpm() is sufficient for doing what commented function below does. // protected musicSheetParameterChanged(client: IPhonicScoreClient, parameter: MusicSheetParameters, currentValue, previousValue): void { // switch (parameter) { // case MusicSheetParameters.StartTempoInBPM: { // this.tempoUserFactor = Convert.ToSingle(currentValue) / this.sheetStartBPM; // this.setTempo(); // break; // } // } // } protected setBpm(bpm: number): void { this.tempoUserFactor = bpm / this.sheetStartBPM; this.setTempo(); } public handlePlaybackEvent(): void { // lock(this.playbackThreadSyncObject) { // initialize flags: const resetOccurred: boolean = this.resetRequested; this.resetRequested = false; // const resetMetronomeBeatCounter: boolean = resetOccurred; // @ts-ignore const resetMetronomeBeatCounter: boolean = resetOccurred; let updateCursorPosition: boolean = resetOccurred; let endHasBeenReached: boolean = false; if (resetOccurred) { const shallPrecount: boolean = this.DoPreCount; this.doReset(shallPrecount); } if (this.musicPartManager === undefined) { return; } /**********************************************/ // set the current values: this.currentTimestamp = this.timingSource.getCurrentTimestamp(); // console.log("TS ms: " + this.timingSource.getCurrentTimeInMs()); // console.log("TS ts: " + this.currentTimestamp); endHasBeenReached = this.cursorIterator.EndReached; /**********************************************/ // handle the currently pending instructions: // stop the notes that are already over now: this.stopFinishedNotes(); /***** process tempo instructions: *****/ this.processTempoInstructions(); if (this.RunningState === PlaybackState.Running) { // needed when resetting when in pause const newCursorTimestampReached: boolean = this.currentTimestamp.gte(this.cursorIterator.CurrentEnrolledTimestamp) && !endHasBeenReached; if (newCursorTimestampReached) { this.isPreCounting = false; /***** Metronome Beat Calculations *****/ // check if the measure has changed: if (this.currentMeasure !== this.cursorIterator.CurrentMeasure && this.cursorIterator.CurrentMeasure !== undefined) { // set current measure to the new measure this.currentMeasure = this.cursorIterator.CurrentMeasure; this.startRhythmBeats = this.cursorIterator.currentPlaybackSettings().Rhythm.Numerator; this.startRhythmDenominator = this.cursorIterator.currentPlaybackSettings().Rhythm.Denominator; // get the enrolled timestamp of this measure start: const relativeToMeasureTimestamp: Fraction = this.cursorIterator.CurrentRelativeInMeasureTimestamp; this.currentMetronomeBaseTimestamp = Fraction.minus(this.cursorIterator.CurrentEnrolledTimestamp, relativeToMeasureTimestamp); // calculate the new beat duration this.currentBeatDuration = new Fraction(1, this.currentMeasure.Duration.Denominator); // calculate which beat is next: const relativeNextMetronomeBeatTimestamp: Fraction = new Fraction(); this.beatCounter = 0; while (relativeNextMetronomeBeatTimestamp.lt(relativeToMeasureTimestamp)) { relativeNextMetronomeBeatTimestamp.Add(this.currentBeatDuration); this.beatCounter++; } this.playNextMetronomeAt = Fraction.plus( this.currentMetronomeBaseTimestamp, new Fraction(this.beatCounter, this.currentMeasure.Duration.Denominator) ); } /***** process dynamic instructions: *****/ const dynamicEntries: DynamicsContainer[] = this.cursorIterator.getCurrentDynamicChangingExpressions(); for (const dynamicEntry of dynamicEntries) { const staff: Staff = this.musicPartManager.MusicSheet.getStaffFromIndex(dynamicEntry.staffNumber); const channel: number = this.instrumentToStaffToMidiChannelDict.getValue(staff); //int channel = this.staffIndexToMidiChannelMapping[dynamicEntry.StaffNumber]; let volume: number = this.currentVolume; if (dynamicEntry.parMultiExpression().StartingContinuousDynamic !== undefined) { // dynamic expression is continuous: const currentDynamicValue: number = dynamicEntry.parMultiExpression().StartingContinuousDynamic.getInterpolatedDynamic( this.cursorIterator.CurrentSourceTimestamp); if (currentDynamicValue >= 0) { volume = this.calculateFinalVolume(currentDynamicValue); } } else { // dynamic Expression is instantanious - immediately set the volume: volume = this.calculateFinalVolume(dynamicEntry.parMultiExpression().InstantaneousDynamic.Volume); } try { if (this.audioPlayer !== undefined) { this.audioPlayer.setVolume(channel, volume); } } catch (ex) { log.info("PlaybackManager.handlePlaybackEvent: ", ex); } } dynamicEntries.clear(); } // check if the time has come to process the pending instructions: const dueEntries: { enrolledTimestamp: Fraction, playbackEntry: PlaybackEntry }[] = this.playbackIterator.Update(this.currentTimestamp); if (dueEntries.length > 0) { // play new notes if (this.DoPlayback) { const playbackedNotes: PlaybackNote[] = []; for (const entry of dueEntries) { const playbackEntry: PlaybackEntry = entry.playbackEntry; const voiceEntry: VoiceEntry = playbackEntry.ParentVoiceEntry; if (playbackEntry.Notes.length === 0) { continue; } const instrument: Instrument = voiceEntry.ParentVoice.Parent; const staff: Staff = voiceEntry.Notes[0].ParentStaff; const staffIndex: number = MusicSheet.getIndexFromStaff(staff); let channel: number = this.instrumentToStaffToMidiChannelDict.getValue(staff); const isPercussion: boolean = instrument.MidiInstrumentId === MidiInstrument.Percussion; // choose percussion channel if Selected if (isPercussion) { channel = this.percussionChannel; } const currentlyActiveExpression: AbstractExpression = this.cursorIterator.ActiveDynamicExpressions[staffIndex]; // adapt volume level for continuous expressions if (currentlyActiveExpression instanceof ContinuousDynamicExpression) { const currentDynamicValue: number = currentlyActiveExpression.getInterpolatedDynamic( this.cursorIterator.CurrentSourceTimestamp); if (currentDynamicValue >= 0) { const channelVolume: number = this.calculateFinalVolume(currentDynamicValue); try { if (this.audioPlayer !== undefined) { this.audioPlayer.setVolume(channel, channelVolume); } } catch (ex) { log.info("PlaybackManager.handlePlaybackEvent: ", ex); } } } // calculate volume from instrument volume, staff volume and voice volume: let volume: number = instrument.Volume * staff.Volume * voiceEntry.ParentVoice.Volume; // attenuate if in Solo mode an this voice is not soloed: const soloAttenuate: boolean = this.SoloActive && !(instrument.Solo || voiceEntry.ParentVoice.Solo || staff.Solo); if (soloAttenuate) { volume *= this.SoloAttenuationValue; } // increase volume if this is an accent: const entryIsAccent: boolean = voiceEntry.VolumeModifier !== undefined; if (entryIsAccent) { volume *= 1.3; volume = Math.min(1, volume); } const transpose: number = this.musicPartManager.MusicSheet.Transpose; const instrumentPlaybackTranspose: number = instrument.PlaybackTranspose ?? 0; for (const note of playbackEntry.Notes.filter(n => n.MidiKey !== 0)) { // play the note let key: number = note.MidiKey; if (!isPercussion) { key += instrumentPlaybackTranspose + transpose; } // if note has another explicitly given playback instrument: if (note.ParentNote.PlaybackInstrumentId !== undefined) { // playback with other instrument: const notePlaybackInstrument: SubInstrument = instrument.getSubInstrument(note.ParentNote.PlaybackInstrumentId); if (notePlaybackInstrument !== undefined) { if (notePlaybackInstrument.fixedKey >= 0) { key = notePlaybackInstrument.fixedKey; } } // recalculate Volume for this instrument: volume = notePlaybackInstrument.volume * staff.Volume * voiceEntry.ParentVoice.Volume; // attenuate if in Solo mode an this voice is not soloed: if (soloAttenuate) { volume *= this.SoloAttenuationValue; } if (entryIsAccent) { volume *= 1.3; volume = Math.min(1, volume); } } // calculate stop time and remember it let noteLength: Fraction = Fraction.createFromFraction(note.Length); let stopAt: Fraction; // ToDo MU: move this to PlaybackEntry const entryIsStaccato: boolean = voiceEntry.DurationModifier !== undefined; if (entryIsStaccato) { // Reduce length and stopAt time: noteLength = new Fraction(noteLength.Numerator * 2, noteLength.Denominator * 3); stopAt = Fraction.plus(entry.enrolledTimestamp, noteLength); } else { stopAt = Fraction.plus(entry.enrolledTimestamp, noteLength); } try { if (this.audioPlayer !== undefined) { this.audioPlayer.playSound( channel, key, volume, this.timingSource.getDurationInMs(noteLength)); } } catch (ex) { log.info("PlaybackManager.handlePlaybackEvent. Failed playing sound: ", ex); } if (!this.notesToStop.containsKey(stopAt)) { // this.notesToStop.Add(stopAt, new List()); this.notesToStop.setValue(stopAt, []); } this.notesToStop.getValue(stopAt).push(new ChannelNote(key, channel, note)); if (this.highlightPlayedNotes) { note.ParentNote.state = NoteState.Selected; } playbackedNotes.push(note); } } /*** Inform about which notes are now played *** * e.g. for updating graphics */ // TODO: Replace with generic event system // if (this.highlightPlayedNotes && this.NotesPlaybackEventOccurred !== undefined) { this.NotesPlaybackEventOccurred(playbackedNotes); } } if (newCursorTimestampReached) { // store current iterator parameters: this.currentIteratorSourceTimeStamp = this.cursorIterator.CurrentSourceTimestamp; this.currentMeasureIndex = this.cursorIterator.CurrentMeasureIndex; // this.currentRepetition = this.cursorIterator.CurrentRepetition; /************ Move to next sheet position ************/ // move iterator already to next position, to find out how long to wait or if the End has been reached: this.cursorIterator.moveToNext(); this.nextIteratorTimestamp = this.cursorIterator.CurrentEnrolledTimestamp; updateCursorPosition = true; } // Stop the sound of the last played metronome this.stopMetronomeSound(); // Check for "end has been reached" if (endHasBeenReached && this.currentTimestamp.gte(this.cursorIterator.CurrentEnrolledTimestamp)) { // notify possible listeners: for (const listener of this.listeners) { listener.selectionEndReached(undefined); } this.handleEndReached(); } else { /****** * Play Metronome if needed */ if (this.currentTimestamp.gte(this.playNextMetronomeAt)) { updateCursorPosition = true; const playFirstBeatSample: boolean = this.beatCounter % this.startRhythmBeats === 0; this.playMetronomeSound(playFirstBeatSample); this.beatCounter++; } // calculate the next metronome beat timestamp if (this.currentMetronomeBaseTimestamp !== undefined) { this.playNextMetronomeAt = Fraction.plus( this.currentMetronomeBaseTimestamp, new Fraction(this.beatCounter, this.startRhythmDenominator) ); } /*************************************/ this.calculateClosestNextTimestamp(); } } else { // needed when a reset was requested: reset parameters, fire score position changed and finally stop again this.isPlaying = false; } // Check for "updating the display" if (updateCursorPosition || endHasBeenReached && !this.loopTriggeredReset) { // set the play cursor in the display this.updateScoreCursorPosition(resetOccurred); } // } } private NotesPlaybackEventOccurred(notes: PlaybackNote[]): void { for (const listener of this.listeners) { listener.notesPlaybackEventOccurred(notes); } } public calculateFinalVolume(volume: number): number { return ((volume - this.defaultVolume) * this.dynamicImpactFactor + this.defaultVolume); } // TODO unused!? // @ts-ignore private loop(): void { // start playing: try { this.isRunning = true; // @ts-ignore const reset: boolean = false; if (this.isPlaying) { try { if (this.isRunning && this.isInitialized) { //console.log(`handlePlayback, timing deviation: ${Math.round(this.timingSource.getCurrentTimeInMs()) - Math.round(this.wantedNextIteratorTimestampMs)}`); this.handlePlaybackEvent(); if (this.closestNextTimestamp !== undefined) { const wantedNextElapsedMs: number = this.timingSource.getWaitingTimeForTimestampInMs(this.closestNextTimestamp); //this.wantedNextIteratorTimestampMs = this.timingSource.getCurrentTimeInMs() + wantedNextElapsedMs; window.setTimeout(() => { this.loop(); }, Math.max(0, wantedNextElapsedMs)); //this.interruptWaiting.WaitOne(Math.max(0, wantedNextElapsedMs)); } } } catch (ex) { this.pause(); this.reset(); const errorMsg: string = TextTranslation.translateText( "MidiPlaybackError", "An error occurred at the Midi Playback." ); log.info("PlaybackManager.loop: " + errorMsg + " ", ex); if (this.messageViewer !== undefined && this.messageViewer.MessageOccurred !== undefined) { this.messageViewer.MessageOccurred(MessageBoxType.Error, errorMsg); } } } } catch (ex) { const errorMsg: string = TextTranslation.translateText( "MidiPlaybackLoopError", "An error occurred at the Midi Playback. Please restart the program in order for the Playback to be availiable again." ); log.info("PlaybackManager.loop: " + errorMsg + " ", ex); if (this.messageViewer !== undefined && this.messageViewer.MessageOccurred !== undefined) { this.messageViewer.MessageOccurred(MessageBoxType.Error, errorMsg); } } this.isRunning = false; } private stopAllCurrentlyPlayingNotes(): void { try { // lock(this.playbackThreadSyncObject) { if (this.audioPlayer !== undefined) { // stop active metronome sound this.audioPlayer.stopSound(this.metronomeNoteFirstBeat.channel, this.metronomeNoteFirstBeat.key); this.audioPlayer.stopSound(this.metronomeNote.channel, this.metronomeNote.key); // stop active notes // TODO MB: check if port of following for..of is correct // check same in for..of below // for (const entry of this.notesToStop) { // for (const note of entry.Value) { // this.audioPlayer.stopSound(note.channel, note.key); // } // } for (const entry of this.notesToStop.values()) { for (const note of entry) { this.audioPlayer.stopSound(note.channel, note.key); } } } /*** Inform about which notes are now stopped *** * e.g. for updating graphics */ const notes: Note[] = []; for (const entry of this.notesToStop.values()) { for (const note of entry) { note.note.ParentNote.state = NoteState.Normal; notes.push(note.note.ParentNote); } } if (this.highlightPlayedNotes) { // TODO: Replace with generic even system // if (this.NotesPlaybackEventOccurred !== undefined) { // this.NotesPlaybackEventOccurred(notes); // } } this.notesToStop.clear(); // } } catch (ex) { log.info("PlaybackManager.stoppAllCurrentlyPlayingNotes: ", ex); } } protected doReset(shallPrecount: boolean): void { this.nextIteratorTimestamp = undefined; this.playNextMetronomeAt = undefined; this.closestNextTimestamp = undefined; this.currentMeasure = undefined; this.beatCounter = 0; this.fermataActive = false; this.stopAllCurrentlyPlayingNotes(); if (this.musicPartManager !== undefined) { this.cursorIterator = this.musicPartManager.getIterator(); } if (this.cursorIterator === undefined) { return; } this.playbackIterator.Reset(); this.currentIteratorSourceTimeStamp = this.cursorIterator.CurrentSourceTimestamp; this.nextIteratorTimestamp = this.cursorIterator.CurrentEnrolledTimestamp; this.currentMeasure = this.cursorIterator.CurrentMeasure; this.currentMeasureIndex = this.cursorIterator.CurrentMeasureIndex; // this.currentRepetition = this.cursorIterator.CurrentRepetition; this.startRhythmBeats = this.cursorIterator.currentPlaybackSettings().Rhythm.Numerator; this.startRhythmDenominator = this.cursorIterator.currentPlaybackSettings().Rhythm.Denominator; let preCountDuration: Fraction = new Fraction(); if (shallPrecount) { this.isPreCounting = true; const rhythmDuration: Fraction = new Fraction(this.startRhythmBeats, this.startRhythmDenominator); const duration: Fraction = Fraction.plus( this.musicPartManager.MusicSheet.SourceMeasures[this.currentMeasureIndex].AbsoluteTimestamp, this.musicPartManager.MusicSheet.SourceMeasures[this.currentMeasureIndex].Duration).Sub(this.currentIteratorSourceTimeStamp); preCountDuration = rhythmDuration; if (rhythmDuration.gte(duration)) { // make sure that missing duration can't get negative (e.g. if measure is longer that given rhythm. const missingDuration: Fraction = Fraction.minus(rhythmDuration, duration); if (missingDuration.RealValue / rhythmDuration.RealValue < 0.5) { preCountDuration.Add(missingDuration); } else { preCountDuration = missingDuration; } } } this.currentMetronomeBaseTimestamp = this.playNextMetronomeAt = Fraction.minus(this.cursorIterator.CurrentEnrolledTimestamp, preCountDuration); //this.timingSource.Reset(); this.timingSource.setTimeAndBpm(this.currentMetronomeBaseTimestamp, this.cursorIterator.currentPlaybackSettings().BeatsPerMinute); this.calculateClosestNextTimestamp(); } /// /// Calculate the closest next timestamp at which the next instruction has to be processed /// private calculateClosestNextTimestamp(): void { const timestamps: Fraction[] = []; // add next timestamp for stopping notes if (this.notesToStop.size() > 0) { // timestamps.push(this.notesToStop.keys().Min()); // TODO MB: Check if line below does what line above is supposed to do timestamps.push(this.notesToStop.keys().reduce( (a, b) => a.lt(b) ? a : b)); } // add next timestamp for next notes or other sheet instruction if (this.playbackIterator.NextEntryTimestamp !== undefined) { timestamps.push(this.playbackIterator.NextEntryTimestamp); } if (this.nextIteratorTimestamp !== undefined) { timestamps.push(this.nextIteratorTimestamp); } // add next timestamp for metronome tick if (this.playNextMetronomeAt !== undefined) { timestamps.push(this.playNextMetronomeAt); } // get the closest next timestamp if (timestamps.length > 0) { // this.closestNextTimestamp = timestamps.Min(); // TODO MB: Check if line below does what line above is supposed to do this.closestNextTimestamp = timestamps.reduce( (a, b) => a.lt(b) ? a : b); } else { this.closestNextTimestamp = undefined; } } /// /// Called when the end of the sheet or the selection has been reached /// protected handleEndReached(): void { this.pause(); } /// /// Fire a delegate to inform the display, that the cursor position has changed /// /// private updateScoreCursorPosition(resetOccurred: boolean): void { this.scorePositionChangedData.CurrentMeasureIndex = this.currentMeasureIndex; this.scorePositionChangedData.CurrentRepetition = this.currentRepetition; this.scorePositionChangedData.PredictedPosition = this.currentTimestamp; this.scorePositionChangedData.CurrentBpm = this.musicPartManager.MusicSheet.SheetPlaybackSetting.BeatsPerMinute; this.scorePositionChangedData.ResetOccurred = resetOccurred; for (const listener of this.listeners) { listener.cursorPositionChanged(this.currentIteratorSourceTimeStamp, this.scorePositionChangedData); } } private stopMetronomeSound(): void { if (this.metronomeSoundPlayed) { if (this.audioPlayer !== undefined) { this.audioPlayer.stopSound(this.metronomeNoteFirstBeat.channel, this.metronomeNoteFirstBeat.key); this.audioPlayer.stopSound(this.metronomeNote.channel, this.metronomeNote.key); } this.metronomeSoundPlayed = false; } } private playMetronomeSound(playFirstBeatSample: boolean): void { // play the metronome if needed: if (this.metronome.Audible || this.metronome.Solo || this.isPreCounting) { let volume: number = this.metronome.Volume; if (!this.isPreCounting && this.SoloActive && !this.metronome.Solo) { volume *= this.SoloAttenuationValue; } if (volume > 0) { if (playFirstBeatSample) { try { if (this.audioPlayer !== undefined) { this.audioPlayer.playSound(this.metronomeNoteFirstBeat.channel, this.metronomeNoteFirstBeat.key, volume, 1000); } if (this.audioMetronomePlayer !== undefined) { this.audioMetronomePlayer.playFirstBeatSample(volume); } } catch (ex) { log.info("PlaybackManager.playMetronomeSound: ", ex); } } else { try { if (this.audioPlayer !== undefined) { this.audioPlayer.playSound(this.metronomeNote.channel, this.metronomeNote.key, volume, 1000); } if (this.audioMetronomePlayer !== undefined) { this.audioMetronomePlayer.playBeatSample(volume); } } catch (ex) { log.info("PlaybackManager.playMetronomeSound: ", ex); } } this.metronomeSoundPlayed = true; } } } private stopFinishedNotes(): void { // do the pending note stops: let expiredKeys: Fraction[]; if (this.currentTimestamp !== undefined) { expiredKeys = this.notesToStop.keys().filter(ts => ts.lte(this.currentTimestamp)); } else { expiredKeys = this.notesToStop.keys(); } for (const timestamp of expiredKeys) { const notesToStop: ChannelNote[] = this.notesToStop.getValue(timestamp); if (this.audioPlayer !== undefined) { for (const note of notesToStop) { this.audioPlayer.stopSound(note.channel, note.key); } } /*** Inform about which notes are now stopped *** * e.g. for updating graphics */ const notes: Note[] = []; for (const note of notesToStop) { note.note.ParentNote.state = NoteState.Normal; notes.push(note.note.ParentNote); } if (this.highlightPlayedNotes) { // TODO: Replace with generic event system // if (this.NotesPlaybackEventOccurred !== undefined) { // this.NotesPlaybackEventOccurred(notes); // } } this.notesToStop.remove(timestamp); } } private processTempoInstructions(): void { // 1. check if the current bpm of the iterator have changed (significantly): if (Math.abs(this.currentReferenceBPM - this.cursorIterator.CurrentBpm) > 0.001) { this.changeTempo(this.cursorIterator.CurrentBpm); } // 2. check for possible fermatas and slow down for that entry: this.handleFermata(); } private handleFermata(): void { // check for fermatas: let fermataFound: boolean = false; if (!this.cursorIterator.EndReached) { if (this.currentTimestamp.gte(this.cursorIterator.CurrentEnrolledTimestamp)) { for (const ve of this.cursorIterator.CurrentVoiceEntries) { fermataFound = ve.Fermata !== undefined; } } } if (fermataFound) { if (!this.fermataActive) { this.fermataActive = true; this.changeTempo(this.cursorIterator.CurrentBpm / 3); } } else { if (this.fermataActive) { this.fermataActive = false; this.changeTempo(this.cursorIterator.CurrentBpm); } } } public bpmChanged(newBpm: number): void { this.currentBPM = newBpm; this.timingSource.setBpm(newBpm); } public volumeChanged(instrument: number, newVolume: number): void { this.currentVolume = newVolume / 100; if (instrument === -1) { this.metronome.Volume = this.currentVolume; } else { this.instrumentIdMapping.getValue(instrument).Volume = this.currentVolume; } } public volumeMute(instrument: number): void { if (instrument === -1) { this.metronome.Mute = true; } else { this.instrumentIdMapping.getValue(instrument).Audible = false; } } public volumeUnmute(instrument: number): void { if (instrument === -1) { this.metronome.Mute = false; } else { this.instrumentIdMapping.getValue(instrument).Audible = true; } } private changeTempo(newTempoInBPM: number): void { log.debug("PlaybackManager.changeTempo", `current tempo in BPM: ${newTempoInBPM}`); //Console.WriteLine(currTempoInBPM.ToString()); if (newTempoInBPM > 0) { this.currentReferenceBPM = newTempoInBPM; this.setTempo(); } } protected setTempo(): void { this.currentBPM = this.tempoUserFactor * this.getCurrentReferenceBPM(); this.timingSource.setBpm(this.currentBPM); } protected getCurrentReferenceBPM(): number { return ((this.currentReferenceBPM - this.sheetStartBPM) * this.tempoImpactFactor + this.sheetStartBPM); } public checkForSoloDeactivated(): void { if (this.musicPartManager.MusicSheet === undefined) { this.SoloActive = false; return; } let state: boolean = false; for (const instrument of this.musicPartManager.MusicSheet.Instruments) { for (const staff of instrument.Staves) { state = state || staff.Solo; } for (const voice of instrument.Voices) { state = state || voice.Solo; } } state = state || this.Metronome.Solo; if (!state) { this.SoloActive = false; } } //private class MidiChannelInfo //{ // public List subscribers = new List(); // public int channel; //} }