diff --git a/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs b/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs index 14893de52..24fcc2a5e 100644 --- a/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs +++ b/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs @@ -184,6 +184,11 @@ public void SetChannelVolume(double channel, double volume) DispatchOnWorkerThread(() => { Player.SetChannelVolume(channel, volume); }); } + public void SetChannelTranspositionPitch(double channel, double semitones) + { + DispatchOnWorkerThread(() => { Player.SetChannelTranspositionPitch(channel, semitones); }); + } + public IEventEmitter Ready { get; } = new EventEmitter(); public IEventEmitter ReadyForPlayback { get; } = new EventEmitter(); public IEventEmitter Finished { get; } = new EventEmitter(); diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt index da3e3b855..0bda1b011 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt @@ -247,6 +247,10 @@ internal class AndroidThreadAlphaSynthWorkerPlayer : IAlphaSynth, Runnable { _workerQueue.add { _player?.setChannelVolume(channel, volume) } } + override fun setChannelTranspositionPitch(channel: Double, semitones: Double) { + _workerQueue.add { _player?.setChannelTranspositionPitch(channel, semitones) } + } + override val ready: IEventEmitter = EventEmitter() override val readyForPlayback: IEventEmitter = EventEmitter() override val finished: IEventEmitter = EventEmitter() diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index 9be16773e..1a75d704b 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -672,6 +672,22 @@ export class AlphaTabApiBase { } } + /** + * Changes the pitch transpose applied to the given tracks. These pitches are additional to the ones + * applied to the song via the settings and allows a more live-update. + * @param tracks The list of tracks to change. + * @param semitones The number of semitones to apply as pitch offset. + */ + public changeTrackTranspositionPitch(tracks: Track[], semitones: number): void { + if (!this.player) { + return; + } + for (let track of tracks) { + this.player.setChannelTranspositionPitch(track.playbackInfo.primaryChannel, semitones); + this.player.setChannelTranspositionPitch(track.playbackInfo.secondaryChannel, semitones); + } + } + /** * Starts the playback of the current song. * @returns true if the playback was started, otherwise false. Reasons for not starting can be that the player is not ready or already playing. diff --git a/src/platform/javascript/AlphaSynthWebWorker.ts b/src/platform/javascript/AlphaSynthWebWorker.ts index 43cf93ff2..7a6cb2ab2 100644 --- a/src/platform/javascript/AlphaSynthWebWorker.ts +++ b/src/platform/javascript/AlphaSynthWebWorker.ts @@ -115,6 +115,9 @@ export class AlphaSynthWebWorker { case 'alphaSynth.setChannelMute': this._player.setChannelMute(data.channel, data.mute); break; + case 'alphaSynth.setChannelTranspositionPitch': + this._player.setChannelTranspositionPitch(data.channel, data.semitones); + break; case 'alphaSynth.setChannelSolo': this._player.setChannelSolo(data.channel, data.solo); break; diff --git a/src/platform/javascript/AlphaSynthWebWorkerApi.ts b/src/platform/javascript/AlphaSynthWebWorkerApi.ts index 0456a9267..38dbe014e 100644 --- a/src/platform/javascript/AlphaSynthWebWorkerApi.ts +++ b/src/platform/javascript/AlphaSynthWebWorkerApi.ts @@ -318,6 +318,14 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { }); } + public setChannelTranspositionPitch(channel: number, semitones: number): void { + this._synth.postMessage({ + cmd: 'alphaSynth.setChannelTranspositionPitch', + channel: channel, + semitones: semitones + }); + } + 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 73f5a32e0..4016000ed 100644 --- a/src/synth/AlphaSynth.ts +++ b/src/synth/AlphaSynth.ts @@ -390,6 +390,10 @@ export class AlphaSynth implements IAlphaSynth { this._synthesizer.applyTranspositionPitches(transpositionPitches); } + public setChannelTranspositionPitch(channel: number, semitones: number): void { + this._synthesizer.setChannelTranspositionPitch(channel, semitones); + } + public setChannelMute(channel: number, mute: boolean): void { this._synthesizer.channelSetMute(channel, mute); } diff --git a/src/synth/IAlphaSynth.ts b/src/synth/IAlphaSynth.ts index a1aec3949..232a9d325 100644 --- a/src/synth/IAlphaSynth.ts +++ b/src/synth/IAlphaSynth.ts @@ -135,6 +135,14 @@ export interface IAlphaSynth { */ applyTranspositionPitches(transpositionPitches: Map): void; + /** + * Sets the transposition pitch a channel. This pitch is additionally applied beside the + * ones applied already via {@link applyTranspositionPitches}. + * @param channel The channel number + * @param semitones The number of semitones to apply as pitch offset. + */ + setChannelTranspositionPitch(channel: number, semitones: number): 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 06d25a180..169fcc60c 100644 --- a/src/synth/synthesis/TinySoundFont.ts +++ b/src/synth/synthesis/TinySoundFont.ts @@ -2,7 +2,18 @@ // developed by Bernhard Schelling (https://github.com/schellingb/TinySoundFont) // TypeScript port for alphaTab: (C) 2020 by Daniel Kuschny // Licensed under: MPL-2.0 -import { ControlChangeEvent, MidiEvent, MidiEventType, NoteBendEvent, NoteOffEvent, NoteOnEvent, PitchBendEvent, ProgramChangeEvent, TempoChangeEvent, TimeSignatureEvent } from '@src/midi/MidiEvent'; +import { + ControlChangeEvent, + MidiEvent, + MidiEventType, + NoteBendEvent, + NoteOffEvent, + NoteOnEvent, + PitchBendEvent, + ProgramChangeEvent, + TempoChangeEvent, + TimeSignatureEvent +} from '@src/midi/MidiEvent'; import { Hydra, HydraIbag, @@ -41,7 +52,11 @@ export class TinySoundFont { private _mutedChannels: Map = new Map(); private _soloChannels: Map = new Map(); private _isAnySolo: boolean = false; + + // these are the transposition pitches applied generally on the song (via Settings or general transposition) private _transpositionPitches: Map = new Map(); + // these are the transposition pitches only applied on playback (adjusting the pitch only during playback) + private _liveTranspositionPitches: Map = new Map(); public currentTempo: number = 0; public timeSignatureNumerator: number = 0; @@ -95,30 +110,58 @@ export class TinySoundFont { public resetChannelStates(): void { this._mutedChannels = new Map(); this._soloChannels = new Map(); + this._liveTranspositionPitches = new Map(); this.applyTranspositionPitches(new Map()); this._isAnySolo = false; } + public setChannelTranspositionPitch(channel: number, semitones: number): void { + let previousTransposition = 0; + if(this._liveTranspositionPitches.has(channel)) { + previousTransposition = this._liveTranspositionPitches.get(channel)!; + } + + if (semitones === 0) { + this._liveTranspositionPitches.delete(channel); + } else { + this._liveTranspositionPitches.set(channel, semitones); + } + + for (const voice of this._voices) { + if (voice.playingChannel === channel && voice.playingChannel !== 9 /*percussion*/) { + let pitchDifference = 0; + pitchDifference -= previousTransposition; + pitchDifference += semitones; + + voice.playingKey += pitchDifference; + + if (this._channels) { + voice.updatePitchRatio(this._channels!.channelList[voice.playingChannel], this.outSampleRate); + } + } + } + } + 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. + // 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)) { + if (previousTransposePitches.has(voice.playingChannel)) { pitchDifference -= previousTransposePitches.get(voice.playingChannel)!; } - - if(transpositionPitches.has(voice.playingChannel)) { + + if (transpositionPitches.has(voice.playingChannel)) { pitchDifference += transpositionPitches.get(voice.playingChannel)!; } voice.playingKey += pitchDifference; - if(this._channels) { + if (this._channels) { voice.updatePitchRatio(this._channels!.channelList[voice.playingChannel], this.outSampleRate); } } @@ -157,8 +200,9 @@ export class TinySoundFont { const channel: number = voice.playingChannel; // channel is muted if it is either explicitley muted, or another channel is set to solo but not this one. // exception. metronome is implicitly added in solo - const isChannelMuted: boolean = this._mutedChannels.has(channel) - || (anySolo && channel != SynthConstants.MetronomeChannel && !this._soloChannels.has(channel)); + const isChannelMuted: boolean = + this._mutedChannels.has(channel) || + (anySolo && channel != SynthConstants.MetronomeChannel && !this._soloChannels.has(channel)); if (!buffer) { voice.kill(); @@ -172,14 +216,14 @@ export class TinySoundFont { } private processMidiMessage(e: MidiEvent): void { - Logger.debug('MIdi', 'Processing Midi message ' + MidiEventType[e.type] + '/' + e.tick) + Logger.debug('MIdi', 'Processing Midi message ' + MidiEventType[e.type] + '/' + e.tick); const command: MidiEventType = e.type; switch (command) { case MidiEventType.TimeSignature: - const timeSignature = (e as TimeSignatureEvent); + const timeSignature = e as TimeSignatureEvent; this.timeSignatureNumerator = timeSignature.numerator; this.timeSignatureDenominator = Math.pow(2, timeSignature.denominatorIndex); - break + break; case MidiEventType.NoteOn: const noteOn = e as NoteOnEvent; this.channelNoteOn(noteOn.channel, noteOn.noteKey, noteOn.noteVelocity / 127.0); @@ -197,7 +241,7 @@ export class TinySoundFont { this.channelSetPresetNumber(programChange.channel, programChange.program, programChange.channel === 9); break; case MidiEventType.TempoChange: - const tempoChange = e as TempoChangeEvent + const tempoChange = e as TempoChangeEvent; this.currentTempo = 60000000 / tempoChange.microSecondsPerQuarterNote; break; case MidiEventType.PitchBend: @@ -230,7 +274,6 @@ export class TinySoundFont { } } - public get masterVolume(): number { return SynthHelper.decibelsToGain(this.globalGainDb); } @@ -517,7 +560,7 @@ export class TinySoundFont { if (voice.playingPreset !== -1) { if (immediate) { voice.endQuick(this.outSampleRate); - } else if(voice.ampEnv.segment < VoiceEnvelopeSegment.Release) { + } else if (voice.ampEnv.segment < VoiceEnvelopeSegment.Release) { voice.end(this.outSampleRate); } } @@ -615,6 +658,10 @@ export class TinySoundFont { key += this._transpositionPitches.get(channel)!; } + if(this._liveTranspositionPitches.has(channel)) { + key += this._liveTranspositionPitches.get(channel)!; + } + this._channels.activeChannel = channel; this.noteOn(this._channels.channelList[channel].presetIndex, key, vel); } @@ -628,6 +675,9 @@ export class TinySoundFont { if (this._transpositionPitches.has(channel)) { key += this._transpositionPitches.get(channel)!; } + if (this._liveTranspositionPitches.has(channel)) { + key += this._liveTranspositionPitches.get(channel)!; + } const matches: Voice[] = []; let matchFirst: Voice | null = null; @@ -749,7 +799,7 @@ export class TinySoundFont { presetIndex = this.getPresetIndex(c.bank & 0x7ff, presetNumber); } c.presetIndex = presetIndex; - return (presetIndex !== -1); + return presetIndex !== -1; } /** @@ -845,6 +895,9 @@ export class TinySoundFont { if (this._transpositionPitches.has(channel)) { key += this._transpositionPitches.get(channel)!; } + if (this._liveTranspositionPitches.has(channel)) { + key += this._liveTranspositionPitches.get(channel)!; + } const c: Channel = this.channelInit(channel); if (c.perNotePitchWheel.has(key) && c.perNotePitchWheel.get(key) === pitchWheel) {