Skip to content

Fix skipped notes at tempos above 20 ticks per second - #130

Open
zishounekonanoda wants to merge 5 commits into
koca2000:masterfrom
zishounekonanoda:fix/high-tempo-playback
Open

Fix skipped notes at tempos above 20 ticks per second#130
zishounekonanoda wants to merge 5 commits into
koca2000:masterfrom
zishounekonanoda:fix/high-tempo-playback

Conversation

@zishounekonanoda

@zishounekonanoda zishounekonanoda commented Aug 11, 2026

Copy link
Copy Markdown

Summary

  • replace the per-song asynchronous sleep loop with one synchronous task that runs once per server tick
  • advance playback from logical server ticks and process every NBS tick that becomes due, including multiple song ticks during one server tick
  • reset the playback clock when playback resumes, the position changes, or a song is selected, and cancel the task when the player is destroyed
  • execute doSync immediately when already on the primary thread so cancellable playback events are observed before playback control flow continues
  • normalize negative fine-pitch values when they cross an extended-octave sample boundary
  • add regression tests for 20, 31.86, 40, and 60 ticks per second, fractional timing, clock resets, and negative fine pitch

Root cause

The previous implementation advanced the mutable tick field in an asynchronous loop and then queued playback on the server thread:

plugin.doSync(() -> playTick(player, tick));

At song tempos above the server's 20 ticks per second, the asynchronous loop could advance tick multiple times before the server thread processed the queued callbacks. Those callbacks then read the same newer field value instead of the value from the iteration that scheduled them. Intermediate song ticks, including their notes and tick notifications, were therefore skipped.

An intermediate version used measured wall-clock time in the synchronous scheduler callback. That preserved song ticks, but normal callback jitter around the 50 ms boundary could turn a 20 TPS cadence into zero song ticks in one callback and two in the next. This sounded like a brief stall followed by a catch-up burst even while average TPS and MSPT were healthy.

The final playback clock advances by songTicksPerSecond / 20 for each logical server tick. This makes 20 TPS playback exactly one song tick per server tick and avoids converting scheduler timing jitter into audible stalls. Fractional and high tempos retain their accumulated remainder, so no due song tick is discarded.

Extended-octave regression

Moving note playback directly onto the synchronous playback task also exposed an existing fine-pitch arithmetic bug in the 10-octave path. Java's truncating division represented a note such as key 33 with a -30 cent offset as key 33 and remainder -30, causing getPitchInOctave to access pitches[-30]. The exception aborted the remaining notes in that song tick and sounded like random cutting out even at ordinary tempos.

Using Math.floorDiv and Math.floorMod represents the same note as key 32 with a +70 cent offset. This selects the previous octave sample and preserves the intended absolute pitch.

Observed behavior in the reporter's environments

The following observations come from the reporter's own servers and should not be read as a claim that every Paper- or Purpur-based server will show the same symptom:

  • With the existing NoteBlockAPI 1.7.0 build, the reporter's 13.04 ticks-per-second NBS song containing -30-cent fine-pitch notes remained audibly playable in their Paper test environment.
  • After applying the playback changes from this pull request, the same extended-octave song and enable10octave: true configuration consistently exposed ArrayIndexOutOfBoundsException: Index -30 at octave sample boundaries. The pitch arithmetic itself was not introduced by this pull request, but the failure became observable after the playback path changed, so the fine-pitch normalization is included here as a regression companion to the scheduler change.
  • With fast songs, the reporter observed dropped or interrupted notes on their Purpur server, while their separate Paper server mainly played the song too slowly. Although the audible symptoms differed between those two environments, both were consistent with the old asynchronous mutable-tick playback path failing to preserve the intended song-tick schedule.

These are environment-specific reproduction results. Server load, implementation, configuration, and scheduler timing may change how the underlying problem sounds.

Timing behavior

Bukkit schedules synchronous tasks in server ticks and cannot provide sub-tick sound spacing. Songs above 20 ticks per second therefore dispatch multiple ordered song ticks during some server ticks. If the server itself falls below 20 TPS, playback follows server time instead of emitting a wall-clock catch-up burst after the delay.

Related reports

Spliterash/MusicBox#27 reports choppy playback and sounds randomly cutting out with NoteBlockAPI 1.x, especially with the extended octave range. The negative fine-pitch fix addresses one concrete cause of that symptom. The broader NoteBlockAPI 2.0 migration topics in that issue remain separate.

Compatibility

The public API and Java 8 source target remain unchanged. Bukkit playback calls stay on the primary thread.

Validation

  • ./mvnw.cmd clean package on JDK 21 with the project's Java 8 source/target settings
  • 6 regression tests, all passing
  • shaded plugin JAR generated successfully

Fixes #116

@zishounekonanoda
zishounekonanoda marked this pull request as ready for review August 11, 2026 17:14
@sonarqubecloud

Copy link
Copy Markdown

@zishounekonanoda

Copy link
Copy Markdown
Author

Before/after playback comparison

This video compares the same NBS playback before and after #130 in my own test environments.

Order:

  1. Paper - Before Fix skipped notes at tempos above 20 ticks per second #130
  2. Paper - After Fix skipped notes at tempos above 20 ticks per second #130
  3. Purpur - Before Fix skipped notes at tempos above 20 ticks per second #130
  4. Purpur - After Fix skipped notes at tempos above 20 ticks per second #130

The full playback is included for each recording. The audio volume and timing were not edited.

These results are specific to my environments and are not intended to claim that every Paper or Purpur server will reproduce the same behavior.

NoteBlockAPI-PR130-full-comparison.mp4

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.

I want the sound to sound at any speed.

1 participant