Skip to content

Commit 4957f09

Browse files
committed
fix(midi): fade via expression and restore it with the voice's next note
Fade in, fade out and volume swell ramped the channel volume and never restored it, silencing the rest of the track and overriding volume automations. Fades now ramp the expression controller, and after a fade out or volume swell the expression is restored when the same voice plays its next non-rest beat. Fixes #2978
1 parent 1dd1db1 commit 4957f09

5 files changed

Lines changed: 228 additions & 146 deletions

File tree

‎packages/alphatab/src/midi/MidiFileGenerator.ts‎

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class PlayThroughContext {
9595
export class MidiFileGenerator {
9696
private static readonly _defaultDurationDead: number = 30;
9797
private static readonly _defaultDurationPalmMute: number = 80;
98+
private static readonly _fullExpression: number = 127;
9899

99100
private readonly _score: Score;
100101
private _settings: Settings;
@@ -103,6 +104,7 @@ export class MidiFileGenerator {
103104

104105
private _currentTime: number = 0;
105106
private _calculatedBeatTimers: Set<number> = new Set<number>();
107+
private _fadedVoices: Set<string> = new Set<string>();
106108

107109
/**
108110
* Gets a lookup object which can be used to quickly find beats and bars
@@ -143,6 +145,7 @@ export class MidiFileGenerator {
143145
public generate(): void {
144146
this.transpositionPitches.clear();
145147
this._calculatedBeatTimers.clear();
148+
this._fadedVoices.clear();
146149
this._currentTime = 0;
147150

148151
// initialize tracks
@@ -757,6 +760,14 @@ export class MidiFileGenerator {
757760
for (const automation of beat.automations) {
758761
this._generateNonTempoAutomation(beat, automation, barStartTick);
759762
}
763+
const voiceKey = MidiFileGenerator._voiceKey(beat);
764+
if (!beat.isRest && this._fadedVoices.has(voiceKey)) {
765+
this._fadedVoices.delete(voiceKey);
766+
// a fading beat sets the expression with its own ramp
767+
if (beat.fade === FadeType.None) {
768+
this._addExpression(track, barStartTick + beatStart, MidiFileGenerator._fullExpression);
769+
}
770+
}
760771
if (beat.isRest) {
761772
this._handler.addRest(track.index, barStartTick + beatStart, track.playbackInfo.primaryChannel);
762773
} else if (beat.deadSlapped) {
@@ -1314,56 +1325,39 @@ export class MidiFileGenerator {
13141325
const track: Track = beat.voice.bar.staff.track;
13151326
switch (beat.fade) {
13161327
case FadeType.FadeIn:
1317-
this._generateFadeSteps(
1318-
track,
1319-
beatStart,
1320-
beatDuration,
1321-
0,
1322-
MidiFileGenerator._toChannelShort(track.playbackInfo.volume)
1323-
);
1324-
break;
1328+
this._generateFadeSteps(track, beatStart, beatDuration, 0, MidiFileGenerator._fullExpression);
1329+
return;
13251330
case FadeType.FadeOut:
1326-
this._generateFadeSteps(
1327-
track,
1328-
beatStart,
1329-
beatDuration,
1330-
MidiFileGenerator._toChannelShort(track.playbackInfo.volume),
1331-
0
1332-
);
1331+
this._generateFadeSteps(track, beatStart, beatDuration, MidiFileGenerator._fullExpression, 0);
13331332
break;
13341333
case FadeType.VolumeSwell:
13351334
const half = (beatDuration / 2) | 0;
1336-
this._generateFadeSteps(
1337-
track,
1338-
beatStart,
1339-
half,
1340-
0,
1341-
MidiFileGenerator._toChannelShort(track.playbackInfo.volume)
1342-
);
1343-
this._generateFadeSteps(
1344-
track,
1345-
beatStart + half,
1346-
half,
1347-
MidiFileGenerator._toChannelShort(track.playbackInfo.volume),
1348-
0
1349-
);
1350-
1335+
this._generateFadeSteps(track, beatStart, half, 0, MidiFileGenerator._fullExpression);
1336+
this._generateFadeSteps(track, beatStart + half, half, MidiFileGenerator._fullExpression, 0);
13511337
break;
13521338
}
1339+
1340+
// the expression is restored with the voice's next non-rest beat, so the release of the faded notes stays silent
1341+
this._fadedVoices.add(MidiFileGenerator._voiceKey(beat));
1342+
}
1343+
1344+
private static _voiceKey(beat: Beat): string {
1345+
const staff = beat.voice.bar.staff;
1346+
return `${staff.track.index}:${staff.index}:${beat.voice.index}`;
13531347
}
13541348

13551349
private _generateFadeSteps(
13561350
track: Track,
13571351
start: number,
13581352
duration: number,
1359-
startVolume: number,
1360-
endVolume: number
1353+
startExpression: number,
1354+
endExpression: number
13611355
): void {
13621356
const tickStep: number = 120;
13631357
// we want to reach the target volume a bit earlier than the end of the note
13641358
duration = (duration * 0.8) | 0;
13651359

1366-
const volumeFactor: number = (endVolume - startVolume) / duration;
1360+
const expressionFactor: number = (endExpression - startExpression) / duration;
13671361

13681362
const steps: number = (duration / tickStep + 1) | 0;
13691363
const endTick: number = start + duration;
@@ -1372,25 +1366,30 @@ export class MidiFileGenerator {
13721366
// ensure final value at end depending on rounding we might not reach it exactly
13731367
const isLast = i === steps - 1;
13741368
const tick: number = isLast ? endTick : start + i * tickStep;
1375-
const volume: number = isLast ? endVolume : Math.round(startVolume + (tick - start) * volumeFactor);
1376-
1377-
this._handler.addControlChange(
1378-
track.index,
1379-
tick,
1380-
track.playbackInfo.primaryChannel,
1381-
ControllerType.VolumeCoarse,
1382-
volume
1383-
);
1384-
this._handler.addControlChange(
1385-
track.index,
1386-
tick,
1387-
track.playbackInfo.secondaryChannel,
1388-
ControllerType.VolumeCoarse,
1389-
volume
1390-
);
1369+
const expression: number = isLast
1370+
? endExpression
1371+
: Math.round(startExpression + (tick - start) * expressionFactor);
1372+
this._addExpression(track, tick, expression);
13911373
}
13921374
}
13931375

1376+
private _addExpression(track: Track, tick: number, expression: number): void {
1377+
this._handler.addControlChange(
1378+
track.index,
1379+
tick,
1380+
track.playbackInfo.primaryChannel,
1381+
ControllerType.ExpressionControllerCoarse,
1382+
expression
1383+
);
1384+
this._handler.addControlChange(
1385+
track.index,
1386+
tick,
1387+
track.playbackInfo.secondaryChannel,
1388+
ControllerType.ExpressionControllerCoarse,
1389+
expression
1390+
);
1391+
}
1392+
13941393
private _generateVibrato(
13951394
note: Note,
13961395
noteStart: number,
@@ -2446,7 +2445,12 @@ export class MidiFileGenerator {
24462445

24472446
public generateSingleBeat(beat: Beat) {
24482447
const tempo = this.prepareSingleBeat(beat);
2448+
this._fadedVoices.clear();
24492449
this._generateBeat(beat, -beat.playbackStart /* to bring it to 0*/, beat.voice.bar, tempo);
2450+
if (this._fadedVoices.size > 0) {
2451+
this._fadedVoices.clear();
2452+
this._addExpression(beat.voice.bar.staff.track, beat.playbackDuration, MidiFileGenerator._fullExpression);
2453+
}
24502454
}
24512455

24522456
public generateSingleNote(note: Note) {

0 commit comments

Comments
 (0)