diff --git a/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs b/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs index 076ee3cda..14893de52 100644 --- a/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs +++ b/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using AlphaTab.Collections; using AlphaTab.Core.EcmaScript; using AlphaTab.Midi; using AlphaTab.Synth; @@ -158,6 +159,11 @@ public void LoadMidiFile(MidiFile midi) DispatchOnWorkerThread(() => { Player.LoadMidiFile(midi); }); } + public void ApplyTranspositionPitches(IValueTypeMap transpositionPitches) + { + DispatchOnWorkerThread(() => { Player.ApplyTranspositionPitches(transpositionPitches); }); + } + public void SetChannelMute(double channel, bool mute) { DispatchOnWorkerThread(() => { Player.SetChannelMute(channel, mute); }); diff --git a/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt b/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt index 0183f1d0c..da3e3b855 100644 --- a/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt +++ b/src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt @@ -227,6 +227,10 @@ internal class AndroidThreadAlphaSynthWorkerPlayer : IAlphaSynth, Runnable { _workerQueue.add { _player?.loadMidiFile(midi) } } + override fun applyTranspositionPitches(transpositionPitches: alphaTab.collections.DoubleDoubleMap) { + _workerQueue.add { _player?.applyTranspositionPitches(transpositionPitches) } + } + override fun setChannelMute(channel: Double, mute: Boolean) { _workerQueue.add { _player?.setChannelMute(channel, mute) } } diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index 632f14608..7c6726248 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -191,9 +191,13 @@ export class AlphaTabApiBase { // enable/disable player if needed if (this.settings.player.enablePlayer) { this.setupPlayer(); + if (score) { + this.player?.applyTranspositionPitches(MidiFileGenerator.buildTranspositionPitches(score, this.settings)); + } } else { this.destroyPlayer(); } + this.onSettingsUpdated(); } @@ -331,8 +335,8 @@ export class AlphaTabApiBase { this._cursorWrapper.width = result.totalWidth; this._cursorWrapper.height = result.totalHeight; } - - if(result.width > 0 || result.height > 0) { + + if (result.width > 0 || result.height > 0) { this.uiFacade.beginAppendRenderResults(result); } } else { @@ -607,10 +611,15 @@ export class AlphaTabApiBase { let midiFile: MidiFile = new MidiFile(); let handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile); let generator: MidiFileGenerator = new MidiFileGenerator(this.score, this.settings, handler); + + // we pass the transposition pitches separately to alphaSynth. + generator.applyTranspositionPitches = false; + generator.generate(); this._tickCache = generator.tickLookup; this.onMidiLoad(midiFile); this.player.loadMidiFile(midiFile); + this.player.applyTranspositionPitches(generator.transpositionPitches); } /** diff --git a/src/midi/MidiFileGenerator.ts b/src/midi/MidiFileGenerator.ts index 9cf126f47..6ce068f6f 100644 --- a/src/midi/MidiFileGenerator.ts +++ b/src/midi/MidiFileGenerator.ts @@ -70,6 +70,17 @@ export class MidiFileGenerator { */ public readonly tickLookup: MidiTickLookup = new MidiTickLookup(); + /** + * Gets or sets whether transposition pitches should be applied to the individual midi events or not. + */ + public applyTranspositionPitches: boolean = true; + + + /** + * Gets the transposition pitches for the individual midi channels. + */ + public readonly transpositionPitches: Map = new Map(); + /** * Initializes a new instance of the {@link MidiFileGenerator} class. * @param score The score for which the midi file should be generated. @@ -87,6 +98,8 @@ export class MidiFileGenerator { * Starts the generation of the midi file. */ public generate(): void { + this.transpositionPitches.clear(); + // initialize tracks for (const track of this._score.tracks) { this.generateTrack(track); @@ -141,7 +154,24 @@ export class MidiFileGenerator { } } + public static buildTranspositionPitches(score: Score, settings: Settings): Map { + const transpositionPitches = new Map(); + for (const track of score.tracks) { + const transpositionPitch = track.index < settings.notation.transpositionPitches.length + ? settings.notation.transpositionPitches[track.index] + : 0; + transpositionPitches.set(track.playbackInfo.primaryChannel, transpositionPitch); + transpositionPitches.set(track.playbackInfo.secondaryChannel, transpositionPitch); + } + return transpositionPitches; + } + private generateChannel(track: Track, channel: number, playbackInfo: PlaybackInformation): void { + const transpositionPitch = track.index < this._settings.notation.transpositionPitches.length + ? this._settings.notation.transpositionPitches[track.index] + : 0; + this.transpositionPitches.set(channel, transpositionPitch); + let volume: number = MidiFileGenerator.toChannelShort(playbackInfo.volume); let balance: number = MidiFileGenerator.toChannelShort(playbackInfo.balance); this._handler.addControlChange(track.index, 0, channel, ControllerType.VolumeCoarse, volume); @@ -411,7 +441,7 @@ export class MidiFileGenerator { private generateNote(note: Note, beatStart: number, beatDuration: number, brushInfo: Int32Array): void { const track: Track = note.beat.voice.bar.staff.track; const staff: Staff = note.beat.voice.bar.staff; - let noteKey: number = note.realValue; + let noteKey: number = note.calculateRealValue(this.applyTranspositionPitches, true); if (note.isPercussion) { const articulation = PercussionMapper.getArticulation(note); if (articulation) { @@ -478,7 +508,7 @@ export class MidiFileGenerator { this.generateWhammy(note.beat, noteStart, noteDuration, channel); } else if (note.slideInType !== SlideInType.None || note.slideOutType !== SlideOutType.None) { this.generateSlide(note, noteStart, noteDuration, noteKey, channel); - } else if (note.vibrato !== VibratoType.None || (note.isTieDestination && note.tieOrigin!.vibrato !== VibratoType.None)) { + } else if (note.vibrato !== VibratoType.None || (note.isTieDestination && note.tieOrigin!.vibrato !== VibratoType.None)) { this.generateVibrato(note, noteStart, noteDuration, noteKey, channel); } @@ -669,8 +699,8 @@ export class MidiFileGenerator { let phaseLength: number = 0; let bendAmplitude: number = 0; const vibratoType = note.vibrato !== VibratoType.None ? note.vibrato : ( - note.isTieDestination ? note.tieOrigin!.vibrato : - VibratoType.Slight /* should never happen unless called wrongly */ + note.isTieDestination ? note.tieOrigin!.vibrato : + VibratoType.Slight /* should never happen unless called wrongly */ ); switch (vibratoType) { case VibratoType.Slight: @@ -691,7 +721,7 @@ export class MidiFileGenerator { } - public vibratoResolution:number = 16; + public vibratoResolution: number = 16; private generateVibratorWithParams( noteStart: number, noteDuration: number, @@ -790,7 +820,7 @@ export class MidiFileGenerator { case SlideOutType.Shift: playedBendPoints.push(new BendPoint(shiftSlideDurationOffset, 0)); // normal note values are in 1/2 tones, bends are in 1/4 tones - const dy = (note.slideTarget!.realValue - note.realValue) * 2; + const dy = (note.slideTarget!.calculateRealValue(this.applyTranspositionPitches, true) - note.calculateRealValue(this.applyTranspositionPitches, true)) * 2; playedBendPoints.push(new BendPoint(BendPoint.MaxPosition, dy)); break; case SlideOutType.OutDown: diff --git a/src/model/Note.ts b/src/model/Note.ts index 6031f4b78..7c3f7e4e6 100644 --- a/src/model/Note.ts +++ b/src/model/Note.ts @@ -504,28 +504,45 @@ export class Note { } public get realValue(): number { - let realValue = this.realValueWithoutHarmonic; - if (this.isStringed) { - if (this.harmonicType === HarmonicType.Natural) { - realValue = this.harmonicPitch + this.stringTuning - this.beat.voice.bar.staff.transpositionPitch; - } else { - realValue += this.harmonicPitch; - } - } - return realValue; + return this.calculateRealValue(true, true); } public get realValueWithoutHarmonic(): number { - if (this.isPercussion) { - return this.percussionArticulation; - } - if (this.isStringed) { - return this.fret + this.stringTuning - this.beat.voice.bar.staff.transpositionPitch; + return this.calculateRealValue(true, false); + } + + /** + * Calculates the real note value of this note as midi key respecting the given options. + * @param applyTranspositionPitch Whether or not to apply the transposition pitch of the current staff. + * @param applyHarmonic Whether or not to apply harmonic pitches to the note. + * @returns The calculated note value as midi key. + */ + public calculateRealValue(applyTranspositionPitch: boolean, applyHarmonic: boolean): number { + const transpositionPitch = applyTranspositionPitch ? this.beat.voice.bar.staff.transpositionPitch : 0; + + if (applyHarmonic) { + let realValue = this.calculateRealValue(applyTranspositionPitch, false); + if (this.isStringed) { + if (this.harmonicType === HarmonicType.Natural) { + realValue = this.harmonicPitch + this.stringTuning - transpositionPitch; + } else { + realValue += this.harmonicPitch; + } + } + return realValue; } - if (this.isPiano) { - return this.octave * 12 + this.tone - this.beat.voice.bar.staff.transpositionPitch; + else { + if (this.isPercussion) { + return this.percussionArticulation; + } + if (this.isStringed) { + return this.fret + this.stringTuning - transpositionPitch; + } + if (this.isPiano) { + return this.octave * 12 + this.tone - transpositionPitch; + } + return 0; } - return 0; } public get harmonicPitch(): number { diff --git a/src/platform/javascript/AlphaSynthWebWorker.ts b/src/platform/javascript/AlphaSynthWebWorker.ts index 96eebfad8..43cf93ff2 100644 --- a/src/platform/javascript/AlphaSynthWebWorker.ts +++ b/src/platform/javascript/AlphaSynthWebWorker.ts @@ -18,7 +18,7 @@ export class AlphaSynthWebWorker { private _player: AlphaSynth; private _main: IWorkerScope; - public constructor(main: IWorkerScope, bufferTimeInMilliseconds:number) { + public constructor(main: IWorkerScope, bufferTimeInMilliseconds: number) { this._main = main; this._main.addEventListener('message', this.handleMessage.bind(this)); @@ -84,7 +84,7 @@ export class AlphaSynthWebWorker { break; case 'alphaSynth.setCountInVolume': this._player.countInVolume = data.value; - break; + break; case 'alphaSynth.setMidiEventsPlayedFilter': this._player.midiEventsPlayedFilter = data.value; break; @@ -130,6 +130,9 @@ export class AlphaSynthWebWorker { cmd: 'alphaSynth.destroyed' }); break; + case 'alphaSynth.applyTranspositionPitches': + this._player.applyTranspositionPitches(new Map(JSON.parse(data.transpositionPitches))); + break; } } diff --git a/src/platform/javascript/AlphaSynthWebWorkerApi.ts b/src/platform/javascript/AlphaSynthWebWorkerApi.ts index abe26d142..a17b1be6e 100644 --- a/src/platform/javascript/AlphaSynthWebWorkerApi.ts +++ b/src/platform/javascript/AlphaSynthWebWorkerApi.ts @@ -312,6 +312,13 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { }); } + public applyTranspositionPitches(transpositionPitches: Map): void { + this._synth.postMessage({ + cmd: 'alphaSynth.applyTranspositionPitches', + transpositionPitches: JSON.stringify(Array.from(transpositionPitches.entries())) + }); + } + public setChannelMute(channel: number, mute: boolean): void { this._synth.postMessage({ cmd: 'alphaSynth.setChannelMute', diff --git a/src/synth/AlphaSynth.ts b/src/synth/AlphaSynth.ts index 6771ed930..405639598 100644 --- a/src/synth/AlphaSynth.ts +++ b/src/synth/AlphaSynth.ts @@ -381,6 +381,10 @@ export class AlphaSynth implements IAlphaSynth { } } + public applyTranspositionPitches(transpositionPitches: Map): void { + this._synthesizer.applyTranspositionPitches(transpositionPitches); + } + public setChannelMute(channel: number, mute: boolean): void { this._synthesizer.channelSetMute(channel, mute); } @@ -418,7 +422,7 @@ export class AlphaSynth implements IAlphaSynth { } else { endTick = this._sequencer.currentEndTick; } - + if (this._tickPosition >= endTick && this._notPlayedSamples <= 0) { this._notPlayedSamples = 0; if (this._sequencer.isPlayingCountIn) { diff --git a/src/synth/IAlphaSynth.ts b/src/synth/IAlphaSynth.ts index 125b98f56..a1aec3949 100644 --- a/src/synth/IAlphaSynth.ts +++ b/src/synth/IAlphaSynth.ts @@ -129,6 +129,12 @@ export interface IAlphaSynth { */ loadMidiFile(midi: MidiFile): void; + /** + * Applies the given transposition pitches to be used during playback. + * @param transpositionPitches a map defining the transposition pitches for midi channel. + */ + applyTranspositionPitches(transpositionPitches: Map): void; + /** * Sets the mute state of a channel. * @param channel The channel number diff --git a/src/synth/synthesis/TinySoundFont.ts b/src/synth/synthesis/TinySoundFont.ts index 97025f111..0da285aa2 100644 --- a/src/synth/synthesis/TinySoundFont.ts +++ b/src/synth/synthesis/TinySoundFont.ts @@ -41,6 +41,7 @@ export class TinySoundFont { private _mutedChannels: Map = new Map(); private _soloChannels: Map = new Map(); private _isAnySolo: boolean = false; + private _transpositionPitches: Map = new Map(); public currentTempo: number = 0; public timeSignatureNumerator: number = 0; @@ -94,9 +95,38 @@ export class TinySoundFont { public resetChannelStates(): void { this._mutedChannels = new Map(); this._soloChannels = new Map(); + + this.applyTranspositionPitches(new Map()); this._isAnySolo = false; } + public applyTranspositionPitches(transpositionPitches: Map): void { + // dynamically adjust actively playing voices to the new pitch they have. + // we are not updating the used preset and regions though. + const previousTransposePitches = this._transpositionPitches; + for (const voice of this._voices) { + if (voice.playingChannel >= 0 && voice.playingChannel !== 9 /*percussion*/) { + let pitchDifference = 0; + + if(previousTransposePitches.has(voice.playingChannel)) { + pitchDifference -= previousTransposePitches.get(voice.playingChannel)!; + } + + if(transpositionPitches.has(voice.playingChannel)) { + pitchDifference += transpositionPitches.get(voice.playingChannel)!; + } + + voice.playingKey += pitchDifference; + + if(this._channels) { + voice.updatePitchRatio(this._channels!.channelList[voice.playingChannel], this.outSampleRate); + } + } + } + + this._transpositionPitches = transpositionPitches; + } + public dispatchEvent(synthEvent: SynthEvent): void { this._midiEventQueue.enqueue(synthEvent); } @@ -581,6 +611,10 @@ export class TinySoundFont { return; } + if (this._transpositionPitches.has(channel)) { + key += this._transpositionPitches.get(channel)!; + } + this._channels.activeChannel = channel; this.noteOn(this._channels.channelList[channel].presetIndex, key, vel); } @@ -591,6 +625,10 @@ export class TinySoundFont { * @param key note value between 0 and 127 (60 being middle C) */ public channelNoteOff(channel: number, key: number): void { + if (this._transpositionPitches.has(channel)) { + key += this._transpositionPitches.get(channel)!; + } + const matches: Voice[] = []; let matchFirst: Voice | null = null; let matchLast: Voice | null = null; @@ -804,6 +842,10 @@ export class TinySoundFont { * @param pitchWheel pitch wheel position 0 to 16383 (default 8192 unpitched) */ public channelSetPerNotePitchWheel(channel: number, key: number, pitchWheel: number): void { + if (this._transpositionPitches.has(channel)) { + key += this._transpositionPitches.get(channel)!; + } + const c: Channel = this.channelInit(channel); if (c.perNotePitchWheel.has(key) && c.perNotePitchWheel.get(key) === pitchWheel) { return;