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

feat: add live transposition pitch changes #1642

Merged
merged 2 commits into from
Aug 24, 2024
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
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
16 changes: 16 additions & 0 deletions src/AlphaTabApiBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,22 @@ export class AlphaTabApiBase<TSettings> {
}
}

/**
* 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.
Expand Down
3 changes: 3 additions & 0 deletions src/platform/javascript/AlphaSynthWebWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/platform/javascript/AlphaSynthWebWorkerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions src/synth/AlphaSynth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
8 changes: 8 additions & 0 deletions src/synth/IAlphaSynth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ export interface IAlphaSynth {
*/
applyTranspositionPitches(transpositionPitches: Map<number, number>): 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
Expand Down
85 changes: 69 additions & 16 deletions src/synth/synthesis/TinySoundFont.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -41,7 +52,11 @@ 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;

// these are the transposition pitches applied generally on the song (via Settings or general transposition)
private _transpositionPitches: Map<number, number> = new Map<number, number>();
// these are the transposition pitches only applied on playback (adjusting the pitch only during playback)
private _liveTranspositionPitches: Map<number, number> = new Map<number, number>();

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

this.applyTranspositionPitches(new Map<number, number>());
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<number, number>): 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);
}
}
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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:
Expand Down Expand Up @@ -230,7 +274,6 @@ export class TinySoundFont {
}
}


public get masterVolume(): number {
return SynthHelper.decibelsToGain(this.globalGainDb);
}
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
Expand Down Expand Up @@ -749,7 +799,7 @@ export class TinySoundFont {
presetIndex = this.getPresetIndex(c.bank & 0x7ff, presetNumber);
}
c.presetIndex = presetIndex;
return (presetIndex !== -1);
return presetIndex !== -1;
}

/**
Expand Down Expand Up @@ -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) {
Expand Down