Skip to content

[Bug]: Pitch Staircase: inverted ratio causes unbounded frequency growth #8338

Description

@Aditya30ag

Component: js/widgets/pitchstaircase.js - PitchStaircase._dissectStair()
Related upstream report: #8332 - "Pitch Staircase: Frequency multiplies beyond audible range (10^38 Hz), breaking stair layout and audio"

Summary

Clicking a stair step in the Pitch Staircase widget "dissects" it into a new pitch using the two ratio inputs (Divide into fields). The new frequency is computed as:

const inputNum = parseFloat(inputNum2 / inputNum1);
...
const newFrequency = parseFloat(frequency) / inputNum;

If the two ratio fields are entered so that ratio1 > ratio2 (e.g. 3 and 2, instead of the conventional 2 and 3), inputNum becomes < 1, so every click multiplies the frequency instead of dividing it. There is no upper or lower bound applied to newFrequency anywhere in the function.

Interestingly, clampNumber is already imported into this file and is used a few lines above to bound the visual width of a stair (clampNumber(rawWidth, 20, PitchStaircase.INNERWINDOWWIDTH)), but the same guard was never applied to the frequency value itself.

Steps to reproduce

  1. Open the Pitch Staircase widget with any starting pitch.
  2. Set the two ratio fields to 3 and 2 (instead of the default 2 and 3).
  3. Repeatedly click the same stair step (or any step whose resulting
    frequency keeps landing near the same row).

Actual behavior

Each click multiplies the frequency by ratio1 / ratio2 (1.5x in the example
above). This compounds exponentially:

Clicks Frequency (starting from 392 Hz)
5 2,976 Hz
10 22,604 Hz
20 1,303,500 Hz
40 4,334,474,269 Hz
50 249,947,628,083 Hz

This unclamped value then flows into three places, all of which break:

  • Audio this.activity.logo.synth.trigger(0, frequency, ...) is called with the runaway frequency, feeding an absurd value straight to the Web Audio oscillator.
  • UI the stair's label (frequency.toFixed(2)) renders an unreadable,
    huge number, and the stair layout breaks.
  • Project export the raw frequency is baked into generated "Hertz blocks" when exporting the stairs as blocks, corrupting the saved project. Note that frequencyToPitch() itself is properly clamped to [A0, C10],
    so pitch-name conversion never breaks it's specifically the raw stored/displayed/played frequency that is left unguarded.

Expected behavior

Dissecting a stair should always keep the resulting frequency within a sane, audible range, regardless of the order the ratio fields are entered in or how many times a stair is dissected.

Why this matters

This is not an edge case requiring malicious input it's a completely ordinary interaction (typing the ratio the "wrong" way round, or simply clicking a step several times while experimenting) in a widget explicitly
aimed at students exploring musical ratios. It reliably breaks both the UI and audio output within seconds of normal use, and can silently corrupt a saved project.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-BugFixes a bug or incorrect behavior

    Type

    No type

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions