Skip to content

Guard TimeAxis against non-finite tempo values - #2347

Open
rokujyushi wants to merge 1 commit into
openutau:masterfrom
rokujyushi:fix/timeaxis-nonfinite-guard
Open

Guard TimeAxis against non-finite tempo values#2347
rokujyushi wants to merge 1 commit into
openutau:masterfrom
rokujyushi:fix/timeaxis-nonfinite-guard

Conversation

@rokujyushi

Copy link
Copy Markdown
Contributor

While testing the synthesis process of another PR, NaN (Not a Number) was passed to PhonemeCanvas during envelope rendering in a project set to 300 BPM, triggering an InvalidOperationException. The cause was that the lookup process in TimeAxis used First(predicate) with a condition that could not match NaN or infinity inputs. Consequently, if even a single inappropriate value was included, the "Sequence contains no matching element" exception was thrown, causing the rendering thread to stop.

The cause of the NaN is currently under investigation (the BPM was saved correctly in the project file itself). If NaN (or infinity) is passed to BuildSegments, msPerTick becomes infinite or NaN, contaminating the msPos of all subsequent segments. The previous fallback process only checked for bpm == 0, allowing NaN to slip through this condition. As a result, phonemes whose start position (PositionMs) was in a normal segment and end position (EndMs) overlapped with a contaminated segment ended up with a non-finite DurationMs (length), causing PhonemeCanvas.Render to throw an exception during envelope rendering.

Fixes included in this PR:

  • Updated the logic to reject all BPM values that cannot be used in division, not just zero, falling back to 120 if the first segment lacks a valid BPM.
  • Set the default time signature to 4/4 if the project lacks one, and set positive default values for both the denominator and numerator of the time signature, as they were also causing division by zero.
  • Replaced all First(predicate) lookups with a helper process that clamps to the last segment's value instead of throwing an exception.
  • Stopped relying on NaN-to-int casts (which silently returned 0 due to saturating casts) and directly clamped the result of MsPosToTickPos.

TimeAxis lookups use First(predicate) with predicates that cannot match a
NaN or infinite input, so a single bad value kills the render thread with
"Sequence contains no matching element".

Such values are produced by BuildSegments itself: a bpm of 0, NaN or
infinity makes msPerTick infinite or NaN, which poisons msPos of every
following segment. The existing fallback only tested `bpm == 0`, which NaN
silently passes. A phoneme whose PositionMs lands in a good segment and
whose EndMs lands in a poisoned one then gets a non-finite DurationMs, and
PhonemeCanvas.Render throws while drawing its envelope.

- Reject any bpm that cannot be divided by, not just zero, and fall back to
  120 when the first segment has none.
- Default to 4/4 when a project has no time signature, and to a positive
  beat unit and beat per bar, both of which divided by zero.
- Replace every First(predicate) lookup with a helper that clamps to the
  last segment instead of throwing.
- Clamp MsPosToTickPos's result instead of relying on the saturating cast
  of NaN to int, which silently returned 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants