Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow dynamic change of transposition pitches. #1309

Merged
merged 4 commits into from
Dec 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using AlphaTab.Collections;
using AlphaTab.Core.EcmaScript;
using AlphaTab.Midi;
using AlphaTab.Synth;
Expand Down Expand Up @@ -158,6 +159,11 @@ public void LoadMidiFile(MidiFile midi)
DispatchOnWorkerThread(() => { Player.LoadMidiFile(midi); });
}

public void ApplyTranspositionPitches(IValueTypeMap<double, double> transpositionPitches)
{
DispatchOnWorkerThread(() => { Player.ApplyTranspositionPitches(transpositionPitches); });
}

public void SetChannelMute(double channel, bool mute)
{
DispatchOnWorkerThread(() => { Player.SetChannelMute(channel, mute); });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
Expand Down
13 changes: 11 additions & 2 deletions src/AlphaTabApiBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,13 @@ export class AlphaTabApiBase<TSettings> {
// 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();
}

Expand Down Expand Up @@ -331,8 +335,8 @@ export class AlphaTabApiBase<TSettings> {
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 {
Expand Down Expand Up @@ -607,10 +611,15 @@ export class AlphaTabApiBase<TSettings> {
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);
}

/**
Expand Down
42 changes: 36 additions & 6 deletions src/midi/MidiFileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, number> = new Map<number, number>();

/**
* Initializes a new instance of the {@link MidiFileGenerator} class.
* @param score The score for which the midi file should be generated.
Expand All @@ -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);
Expand Down Expand Up @@ -141,7 +154,24 @@ export class MidiFileGenerator {
}
}

public static buildTranspositionPitches(score: Score, settings: Settings): Map<number, number> {
const transpositionPitches = new Map<number, number>();
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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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:
Expand All @@ -691,7 +721,7 @@ export class MidiFileGenerator {
}


public vibratoResolution:number = 16;
public vibratoResolution: number = 16;
private generateVibratorWithParams(
noteStart: number,
noteDuration: number,
Expand Down Expand Up @@ -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:
Expand Down
51 changes: 34 additions & 17 deletions src/model/Note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions src/platform/javascript/AlphaSynthWebWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -130,6 +130,9 @@ export class AlphaSynthWebWorker {
cmd: 'alphaSynth.destroyed'
});
break;
case 'alphaSynth.applyTranspositionPitches':
this._player.applyTranspositionPitches(new Map<number, number>(JSON.parse(data.transpositionPitches)));
break;
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/platform/javascript/AlphaSynthWebWorkerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,13 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth {
});
}

public applyTranspositionPitches(transpositionPitches: Map<number, number>): 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',
Expand Down
6 changes: 5 additions & 1 deletion src/synth/AlphaSynth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@ export class AlphaSynth implements IAlphaSynth {
}
}

public applyTranspositionPitches(transpositionPitches: Map<number, number>): void {
this._synthesizer.applyTranspositionPitches(transpositionPitches);
}

public setChannelMute(channel: number, mute: boolean): void {
this._synthesizer.channelSetMute(channel, mute);
}
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/synth/IAlphaSynth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, number>): void;

/**
* Sets the mute state of a channel.
* @param channel The channel number
Expand Down
42 changes: 42 additions & 0 deletions src/synth/synthesis/TinySoundFont.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class TinySoundFont {
private _mutedChannels: Map<number, boolean> = new Map<number, boolean>();
private _soloChannels: Map<number, boolean> = new Map<number, boolean>();
private _isAnySolo: boolean = false;
private _transpositionPitches: Map<number, number> = new Map<number, number>();

public currentTempo: number = 0;
public timeSignatureNumerator: number = 0;
Expand Down Expand Up @@ -94,9 +95,38 @@ export class TinySoundFont {
public resetChannelStates(): void {
this._mutedChannels = new Map<number, boolean>();
this._soloChannels = new Map<number, boolean>();

this.applyTranspositionPitches(new Map<number, number>());
this._isAnySolo = false;
}

public applyTranspositionPitches(transpositionPitches: Map<number, number>): 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);
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down