diff --git a/packages/alphatab/src/synth/AlphaSynth.ts b/packages/alphatab/src/synth/AlphaSynth.ts index 6a5366ca2..7746660e0 100644 --- a/packages/alphatab/src/synth/AlphaSynth.ts +++ b/packages/alphatab/src/synth/AlphaSynth.ts @@ -1,29 +1,38 @@ +import { + EventEmitter, + EventEmitterOfT, + type IEventEmitter, + type IEventEmitterOfT +} from '@coderline/alphatab/EventEmitter'; +import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; +import { Logger } from '@coderline/alphatab/Logger'; +import type { LogLevel } from '@coderline/alphatab/LogLevel'; +import type { MidiEvent, MidiEventType } from '@coderline/alphatab/midi/MidiEvent'; import type { MidiFile } from '@coderline/alphatab/midi/MidiFile'; +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import type { Score } from '@coderline/alphatab/model/Score'; +import { Queue } from '@coderline/alphatab/synth/ds/Queue'; import type { BackingTrackSyncPoint, IAlphaSynth } from '@coderline/alphatab/synth/IAlphaSynth'; +import { + AudioExportChunk, + type AudioExportOptions, + type IAudioExporter +} from '@coderline/alphatab/synth/IAudioExporter'; +import type { IAudioSampleSynthesizer } from '@coderline/alphatab/synth/IAudioSampleSynthesizer'; import type { ISynthOutput } from '@coderline/alphatab/synth/ISynthOutput'; +import { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs'; import { MidiFileSequencer } from '@coderline/alphatab/synth/MidiFileSequencer'; import type { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange'; +import { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/PlaybackRangeChangedEventArgs'; import { PlayerState } from '@coderline/alphatab/synth/PlayerState'; import { PlayerStateChangedEventArgs } from '@coderline/alphatab/synth/PlayerStateChangedEventArgs'; import { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs'; import { Hydra } from '@coderline/alphatab/synth/soundfont/Hydra'; -import { TinySoundFont } from '@coderline/alphatab/synth/synthesis/TinySoundFont'; -import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@coderline/alphatab/EventEmitter'; -import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; -import { Logger } from '@coderline/alphatab/Logger'; -import type { LogLevel } from '@coderline/alphatab/LogLevel'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; -import type { SynthEvent } from '@coderline/alphatab/synth/synthesis/SynthEvent'; -import { Queue } from '@coderline/alphatab/synth/ds/Queue'; -import { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs'; -import type { MidiEvent, MidiEventType } from '@coderline/alphatab/midi/MidiEvent'; -import { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/PlaybackRangeChangedEventArgs'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import type { Score } from '@coderline/alphatab/model/Score'; -import type { IAudioSampleSynthesizer } from '@coderline/alphatab/synth/IAudioSampleSynthesizer'; -import { AudioExportChunk, type IAudioExporter, type AudioExportOptions } from '@coderline/alphatab/synth/IAudioExporter'; import type { Preset } from '@coderline/alphatab/synth/synthesis/Preset'; -import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import type { SynthEvent } from '@coderline/alphatab/synth/synthesis/SynthEvent'; +import { TinySoundFont } from '@coderline/alphatab/synth/synthesis/TinySoundFont'; /** * This is the base class for synthesizer components which can be used to @@ -284,6 +293,20 @@ export class AlphaSynthBase implements IAlphaSynth { } this._notPlayedSamples += samples.length; this.output.addSamples(samples); + + + // if the sequencer finished, we instantly force a noteOff on all + // voices to complete playback and stop voices fast. + // Doing this in the samplePlayed callback is too late as we might + // continue generating audio for long-release notes (especially percussion like cymbals) + + // we still have checkForFinish which takes care of the counterpart + // on the sample played area to ensure we seek back. + // but thanks to this code we ensure the output will complete fast as we won't + // be adding more samples beside a 0.1s ramp-down + if (this.sequencer.isFinished) { + this.synthesizer.noteOffAll(true); + } } else { // Tell output that there is no data left for it. const samples: Float32Array = new Float32Array(0); diff --git a/packages/alphatab/src/synth/SynthConstants.ts b/packages/alphatab/src/synth/SynthConstants.ts index 805498f97..366a259aa 100644 --- a/packages/alphatab/src/synth/SynthConstants.ts +++ b/packages/alphatab/src/synth/SynthConstants.ts @@ -35,4 +35,9 @@ export class SynthConstants { public static readonly MicroBufferCount: number = 32; public static readonly MicroBufferSize: number = 64; + + /** + * approximately -60 dB, which is inaudible to humans + */ + public static readonly AudibleLevelThreshold: number = 1e-3; } diff --git a/packages/alphatab/src/synth/synthesis/Voice.ts b/packages/alphatab/src/synth/synthesis/Voice.ts index 3d06cfec2..150455d31 100644 --- a/packages/alphatab/src/synth/synthesis/Voice.ts +++ b/packages/alphatab/src/synth/synthesis/Voice.ts @@ -213,20 +213,19 @@ export class Voice { noteGain = SynthHelper.decibelsToGain(this.noteGainDb + this.modLfo.level * tmpModLfoToVolume); } - gainMono = noteGain * this.ampEnv.level; + // Update EG. + this.ampEnv.process(blockSamples, f.outSampleRate); + if (updateModEnv) { + this.modEnv.process(blockSamples, f.outSampleRate); + } + gainMono = noteGain * this.ampEnv.level; if (isMuted) { gainMono = 0; } else { gainMono *= this.mixVolume; } - // Update EG. - this.ampEnv.process(blockSamples, f.outSampleRate); - if (updateModEnv) { - this.modEnv.process(blockSamples, f.outSampleRate); - } - // Update LFOs. if (updateModLFO) { this.modLfo.process(blockSamples); @@ -321,7 +320,15 @@ export class Voice { break; } - if (tmpSourceSamplePosition >= tmpSampleEndDbl || this.ampEnv.segment === VoiceEnvelopeSegment.Done) { + const inaudible = + this.ampEnv.segment === VoiceEnvelopeSegment.Release && + Math.abs(gainMono) < SynthConstants.AudibleLevelThreshold; + if ( + tmpSourceSamplePosition >= tmpSampleEndDbl || + this.ampEnv.segment === VoiceEnvelopeSegment.Done || + // Check if voice is inaudible during release to terminate early + inaudible + ) { this.kill(); return; } diff --git a/packages/alphatab/test-data/audio/export-silent-with-metronome.pcm b/packages/alphatab/test-data/audio/export-silent-with-metronome.pcm index 36f55e7a3..8bccb95c4 100644 Binary files a/packages/alphatab/test-data/audio/export-silent-with-metronome.pcm and b/packages/alphatab/test-data/audio/export-silent-with-metronome.pcm differ diff --git a/packages/alphatab/test-data/audio/export-sync-points.pcm b/packages/alphatab/test-data/audio/export-sync-points.pcm index 06db056ce..39c6e11e2 100644 Binary files a/packages/alphatab/test-data/audio/export-sync-points.pcm and b/packages/alphatab/test-data/audio/export-sync-points.pcm differ diff --git a/packages/alphatab/test-data/audio/export-test.pcm b/packages/alphatab/test-data/audio/export-test.pcm index 31c501d84..e7d72fc87 100644 Binary files a/packages/alphatab/test-data/audio/export-test.pcm and b/packages/alphatab/test-data/audio/export-test.pcm differ