Skip to content

Commit 03efb03

Browse files
authored
fix(synth): ensure we end playback fast at end of loops and songs (#2647)
1 parent 590f6db commit 03efb03

File tree

6 files changed

+58
-23
lines changed

6 files changed

+58
-23
lines changed

packages/alphatab/src/synth/AlphaSynth.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
1+
import {
2+
EventEmitter,
3+
EventEmitterOfT,
4+
type IEventEmitter,
5+
type IEventEmitterOfT
6+
} from '@coderline/alphatab/EventEmitter';
7+
import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer';
8+
import { Logger } from '@coderline/alphatab/Logger';
9+
import type { LogLevel } from '@coderline/alphatab/LogLevel';
10+
import type { MidiEvent, MidiEventType } from '@coderline/alphatab/midi/MidiEvent';
111
import type { MidiFile } from '@coderline/alphatab/midi/MidiFile';
12+
import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils';
13+
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
14+
import type { Score } from '@coderline/alphatab/model/Score';
15+
import { Queue } from '@coderline/alphatab/synth/ds/Queue';
216
import type { BackingTrackSyncPoint, IAlphaSynth } from '@coderline/alphatab/synth/IAlphaSynth';
17+
import {
18+
AudioExportChunk,
19+
type AudioExportOptions,
20+
type IAudioExporter
21+
} from '@coderline/alphatab/synth/IAudioExporter';
22+
import type { IAudioSampleSynthesizer } from '@coderline/alphatab/synth/IAudioSampleSynthesizer';
323
import type { ISynthOutput } from '@coderline/alphatab/synth/ISynthOutput';
24+
import { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs';
425
import { MidiFileSequencer } from '@coderline/alphatab/synth/MidiFileSequencer';
526
import type { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange';
27+
import { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/PlaybackRangeChangedEventArgs';
628
import { PlayerState } from '@coderline/alphatab/synth/PlayerState';
729
import { PlayerStateChangedEventArgs } from '@coderline/alphatab/synth/PlayerStateChangedEventArgs';
830
import { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs';
931
import { Hydra } from '@coderline/alphatab/synth/soundfont/Hydra';
10-
import { TinySoundFont } from '@coderline/alphatab/synth/synthesis/TinySoundFont';
11-
import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@coderline/alphatab/EventEmitter';
12-
import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer';
13-
import { Logger } from '@coderline/alphatab/Logger';
14-
import type { LogLevel } from '@coderline/alphatab/LogLevel';
1532
import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants';
16-
import type { SynthEvent } from '@coderline/alphatab/synth/synthesis/SynthEvent';
17-
import { Queue } from '@coderline/alphatab/synth/ds/Queue';
18-
import { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs';
19-
import type { MidiEvent, MidiEventType } from '@coderline/alphatab/midi/MidiEvent';
20-
import { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/PlaybackRangeChangedEventArgs';
21-
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
22-
import type { Score } from '@coderline/alphatab/model/Score';
23-
import type { IAudioSampleSynthesizer } from '@coderline/alphatab/synth/IAudioSampleSynthesizer';
24-
import { AudioExportChunk, type IAudioExporter, type AudioExportOptions } from '@coderline/alphatab/synth/IAudioExporter';
2533
import type { Preset } from '@coderline/alphatab/synth/synthesis/Preset';
26-
import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils';
34+
import type { SynthEvent } from '@coderline/alphatab/synth/synthesis/SynthEvent';
35+
import { TinySoundFont } from '@coderline/alphatab/synth/synthesis/TinySoundFont';
2736

2837
/**
2938
* This is the base class for synthesizer components which can be used to
@@ -284,6 +293,20 @@ export class AlphaSynthBase implements IAlphaSynth {
284293
}
285294
this._notPlayedSamples += samples.length;
286295
this.output.addSamples(samples);
296+
297+
298+
// if the sequencer finished, we instantly force a noteOff on all
299+
// voices to complete playback and stop voices fast.
300+
// Doing this in the samplePlayed callback is too late as we might
301+
// continue generating audio for long-release notes (especially percussion like cymbals)
302+
303+
// we still have checkForFinish which takes care of the counterpart
304+
// on the sample played area to ensure we seek back.
305+
// but thanks to this code we ensure the output will complete fast as we won't
306+
// be adding more samples beside a 0.1s ramp-down
307+
if (this.sequencer.isFinished) {
308+
this.synthesizer.noteOffAll(true);
309+
}
287310
} else {
288311
// Tell output that there is no data left for it.
289312
const samples: Float32Array = new Float32Array(0);

packages/alphatab/src/synth/SynthConstants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@ export class SynthConstants {
3535

3636
public static readonly MicroBufferCount: number = 32;
3737
public static readonly MicroBufferSize: number = 64;
38+
39+
/**
40+
* approximately -60 dB, which is inaudible to humans
41+
*/
42+
public static readonly AudibleLevelThreshold: number = 1e-3;
3843
}

packages/alphatab/src/synth/synthesis/Voice.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -213,20 +213,19 @@ export class Voice {
213213
noteGain = SynthHelper.decibelsToGain(this.noteGainDb + this.modLfo.level * tmpModLfoToVolume);
214214
}
215215

216-
gainMono = noteGain * this.ampEnv.level;
216+
// Update EG.
217+
this.ampEnv.process(blockSamples, f.outSampleRate);
218+
if (updateModEnv) {
219+
this.modEnv.process(blockSamples, f.outSampleRate);
220+
}
217221

222+
gainMono = noteGain * this.ampEnv.level;
218223
if (isMuted) {
219224
gainMono = 0;
220225
} else {
221226
gainMono *= this.mixVolume;
222227
}
223228

224-
// Update EG.
225-
this.ampEnv.process(blockSamples, f.outSampleRate);
226-
if (updateModEnv) {
227-
this.modEnv.process(blockSamples, f.outSampleRate);
228-
}
229-
230229
// Update LFOs.
231230
if (updateModLFO) {
232231
this.modLfo.process(blockSamples);
@@ -321,7 +320,15 @@ export class Voice {
321320
break;
322321
}
323322

324-
if (tmpSourceSamplePosition >= tmpSampleEndDbl || this.ampEnv.segment === VoiceEnvelopeSegment.Done) {
323+
const inaudible =
324+
this.ampEnv.segment === VoiceEnvelopeSegment.Release &&
325+
Math.abs(gainMono) < SynthConstants.AudibleLevelThreshold;
326+
if (
327+
tmpSourceSamplePosition >= tmpSampleEndDbl ||
328+
this.ampEnv.segment === VoiceEnvelopeSegment.Done ||
329+
// Check if voice is inaudible during release to terminate early
330+
inaudible
331+
) {
325332
this.kill();
326333
return;
327334
}
Binary file not shown.
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)