From 9321666268827fe729770b9aaa8312e891a182e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=A4=E5=A2=A8=E6=B5=B7=E7=99=BD=E7=8E=89=E6=9C=88?= <937199794@qq.com> Date: Sun, 24 May 2026 10:36:18 +0800 Subject: [PATCH] fix(synth): prevent audio thread spin loops and GC storms on playback end [Root Cause] When AlphaSynth finishes generating all audio chunks, it previously sent a 0-length Float32Array to the output to signal the end of data. However, on platforms like Android, writing 0 samples to the `AudioTrack` is non-blocking and returns immediately. This causes the audio playback head to freeze (underrun), which in turn makes the native worker report `playedSamples = 0`. Since `_onSamplesPlayed(0)` short-circuits and ignores the update, the `_notPlayedSamples` counter never reaches 0. As a result, AlphaSynth never calls `stop()`, never emits the `playerFinished` event, and the native audio thread falls into an infinite 100% CPU spin loop (allocating massive arrays per second and triggering a severe GC storm). [Solution] Instead of sending a 0-length array when no data is left, we now send an array of silence matching the normal micro-buffer size. This allows the output device to block and consume data naturally, preventing the underrun. Consequently, `_notPlayedSamples` accurately drains to 0, allowing AlphaSynth to properly shut down and emit the `playerFinished` event. --- packages/alphatab/src/synth/AlphaSynth.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/alphatab/src/synth/AlphaSynth.ts b/packages/alphatab/src/synth/AlphaSynth.ts index 13a7e4e32..017522644 100644 --- a/packages/alphatab/src/synth/AlphaSynth.ts +++ b/packages/alphatab/src/synth/AlphaSynth.ts @@ -308,7 +308,8 @@ export class AlphaSynthBase implements IAlphaSynth { } } else { // Tell output that there is no data left for it. - const samples: Float32Array = new Float32Array(0); + // Send silence instead of 0-length array to prevent audio track underrun. + const samples: Float32Array = new Float32Array(SynthConstants.MicroBufferSize * SynthConstants.MicroBufferCount * SynthConstants.AudioChannels); this.output.addSamples(samples); } }