Skip to content

fix(synth): prevent audio thread spin loops and GC storms on playback end#2711

Open
xyfu66 wants to merge 1 commit into
CoderLine:developfrom
xyfu66:fix/audio-underrun-gc
Open

fix(synth): prevent audio thread spin loops and GC storms on playback end#2711
xyfu66 wants to merge 1 commit into
CoderLine:developfrom
xyfu66:fix/audio-underrun-gc

Conversation

@xyfu66
Copy link
Copy Markdown

@xyfu66 xyfu66 commented May 24, 2026

Issues

Fixes N/A (Discovered this bug while integrating AlphaTab on Android where it caused infinite GC storms).

Proposed changes

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.

Checklist

  • I consent that this change becomes part of alphaTab under it's current or any future open source license
  • Changes are implemented
  • New tests were added
    (No tests were added because this is a fix for a platform-specific AudioTrack underrun deadlock that occurs asynchronously between the TS state machine and the Android native UI thread, making it difficult to cover in standard headless unit tests.)

Further details

  • This is a breaking change
  • This change will require update of the documentation/website

… 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.
@Danielku15
Copy link
Copy Markdown
Member

  1. Please enter a issue if none exists.
  2. Your change will rather only break things, not fix anything. alphaTab internally has a ring-buffer from which the actual audio threads/outputs pull their data. With your change you will keep the playback endlessly running instead of having playback stopped properly. Generally it see the problem, especially during end of the song, it can likely happen that upon every write to the audio output device we keep requesting samples which are not produced as playback should run out. But wall-clock wise this should happen all within a few hundred milliseconds. There might be rather another problem (really in android specific code) that some things are not gracefully shutdown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants