Summary
A voice can be initialised with an envelope segment of zero samples, which makes
VoiceEnvelope.nextSegment divide by zero. The resulting Infinity turns the
envelope level into NaN, and from then on the voice writes NaN into every
audio block for the rest of the session. Because NaN + x === NaN, a single
affected voice silences the entire mix.
It also cannot recover: a NaN voice is never released, because every
comparison against NaN is false, so it survives note-off, stop() and
play(). Only recreating the synthesizer clears it.
Cause
src/synth/synthesis/VoiceEnvelope.ts, nextSegment, attack segment:
this.samplesUntilNextSegment = this.parameters.attack * outSampleRate | 0;
if (this.samplesUntilNextSegment > 0) { // guard
if (!this.isAmpEnv)
this.samplesUntilNextSegment =
this.parameters.attack * ((145 - this.midiVelocity) / 144) * outSampleRate | 0; // recomputed AFTER the guard
...
this.slope = 1 / this.samplesUntilNextSegment; // can divide by 0
The positive-length check runs before the value is recomputed with the
velocity factor (145 - velocity) / 144. For a modulation envelope with a short
attack and a high velocity, that factor can round the length down to 0, so the
following division yields Infinity.
The same pattern (a division by samplesUntilNextSegment) appears in the decay
and release branches.
Symptoms
- Playback goes completely silent from one bar onward, while the cursor keeps
moving and playerState stays Playing.
- The output is not quiet but pinned: reading the destination with an
AnalyserNode gives getByteTimeDomainData all zeros (peak reads full scale,
mean 0, no movement between samples) and getFloatTimeDomainData is NaN for
every sample.
- Stopping and restarting playback does not help.
- It needs several tracks sounding together — no single track and no pair
reproduced it in our score.
Reproduction
alphaTab 1.8.1 and 1.8.4, Chromium and iOS Safari, useWorkers: true.
Reproduces reliably on a Guitar Pro import of The Kinks – Lola at bar 12: seek to
the start of bar 11, play, and the output turns to NaN within ~4 seconds.
Suggested fix
Clamp the segment length to at least one sample before dividing, in every branch
that divides by it:
this.slope = 1 / Math.max(1, this.samplesUntilNextSegment);
It would additionally be worth hardening the render loop so a voice whose state
becomes non-finite is killed rather than surviving forever and taking the whole
mix with it.
Workaround
We currently ship a patched build with those divisions clamped, plus a guard in
_fillWorkingBuffer that zeroes a block containing non-finite samples and kills
the voices responsible.
Summary
A voice can be initialised with an envelope segment of zero samples, which makes
VoiceEnvelope.nextSegmentdivide by zero. The resultingInfinityturns theenvelope level into
NaN, and from then on the voice writesNaNinto everyaudio block for the rest of the session. Because
NaN + x === NaN, a singleaffected voice silences the entire mix.
It also cannot recover: a
NaNvoice is never released, because everycomparison against
NaNis false, so it survives note-off,stop()andplay(). Only recreating the synthesizer clears it.Cause
src/synth/synthesis/VoiceEnvelope.ts,nextSegment, attack segment:The positive-length check runs before the value is recomputed with the
velocity factor
(145 - velocity) / 144. For a modulation envelope with a shortattack and a high velocity, that factor can round the length down to
0, so thefollowing division yields
Infinity.The same pattern (a division by
samplesUntilNextSegment) appears in the decayand release branches.
Symptoms
moving and
playerStatestaysPlaying.AnalyserNodegivesgetByteTimeDomainDataall zeros (peak reads full scale,mean 0, no movement between samples) and
getFloatTimeDomainDataisNaNforevery sample.
reproduced it in our score.
Reproduction
alphaTab 1.8.1 and 1.8.4, Chromium and iOS Safari,
useWorkers: true.Reproduces reliably on a Guitar Pro import of The Kinks – Lola at bar 12: seek to
the start of bar 11, play, and the output turns to
NaNwithin ~4 seconds.Suggested fix
Clamp the segment length to at least one sample before dividing, in every branch
that divides by it:
It would additionally be worth hardening the render loop so a voice whose state
becomes non-finite is killed rather than surviving forever and taking the whole
mix with it.
Workaround
We currently ship a patched build with those divisions clamped, plus a guard in
_fillWorkingBufferthat zeroes a block containing non-finite samples and killsthe voices responsible.