Skip to content

AudioBufferSourceNode: attribute definitions and the normative playback algorithm disagree on out-of-range loopStart/loopEnd #2689

Description

@cdumez

The loopStart/loopEnd attribute definitions specify clamping for out-of-range values, while the normative playback algorithm treats the same values as constraint violations and falls back to looping the entire buffer. The two produce observably different output, and nothing in the spec indicates which takes precedence. In one configuration the algorithm also fails to terminate.

Relevant sections: attribute definitions, playback algorithm, Looping.

The conflicting text

The attribute definitions:

loopStart — [...] If loopStart is less than 0, looping will begin at 0. If loopStart is greater than the duration of the buffer, looping will begin at the end of the buffer.

loopEnd — [...] If loopEnd is less than or equal to 0, or if loopEnd is greater than the duration of the buffer, looping will end at the end of the buffer.

The endpoint computation in the playback algorithm:

let actualLoopStart, actualLoopEnd;
if (loop && buffer != null) {
    if (loopStart >= 0 && loopEnd > 0 && loopStart < loopEnd) {
        actualLoopStart = loopStart;
        actualLoopEnd = Math.min(loopEnd, buffer.duration);
    } else {
        actualLoopStart = 0;
        actualLoopEnd = buffer.duration;
    }
}

The conflict cannot be resolved by reading the clamp into the variable binding, because the algorithm's preamble defines these as the raw attribute values:

The following variables capture attribute and AudioParam values for the node. They are updated on a k-rate basis, prior to each invocation of process().

The non-normative Looping section sides with the algorithm ("If any of these constraints are violated, the loop is considered to include the entire buffer contents"), but the attribute definitions are in a different section and are not covered by that section's non-normative disclaimer.

Case A — loopStart < 0

Effective loop
Attribute definition [0, loopEnd)
Algorithm [0, buffer.duration)

Differs whenever loopEnd < buffer.duration.

Case B — loopEnd <= 0

Effective loop
Attribute definition [loopStart, buffer.duration)
Algorithm [0, buffer.duration)

Differs whenever loopStart > 0.

Cases A and B are the same conflict, but implementations and WPT resolve them in opposite directions: A per the attribute definition, B per the algorithm. That alone suggests the spec needs to state one rule explicitly.

Case C — loopStart > buffer.duration (algorithm does not terminate)

With 0 <= loopStart < loopEnd, the guard passes, so actualLoopStart = loopStart (past the end of the buffer) while actualLoopEnd = Math.min(loopEnd, buffer.duration) = buffer.duration. The result is an inverted range, actualLoopStart > actualLoopEnd, which the algorithm never rejects. The wrap-around step then diverges, because the delta is negative:

while (bufferTime >= actualLoopEnd) {
    bufferTime -= actualLoopEnd - actualLoopStart;   // negative delta: bufferTime increases
}

Trace for a 3-second buffer with loopStart = 5, loopEnd = 6, loop = true, playbackRate = 1, start(0, 0):

  1. actualLoopStart = 5, actualLoopEnd = 3.
  2. offset (0) < actualLoopEnd (3), so once bufferTime reaches actualLoopStart (5) the enteredLoop condition is satisfied and enteredLoop becomes true. bufferTime gets there while output is silent, since it is already past buffer.duration.
  3. The first while loop then runs: 5 >= 3, so bufferTime -= (3 - 5) gives 7, then 9, then 11, increasing monotonically. It never terminates.

This is reachable from script, since neither attribute is clamped on assignment. Each implementation avoids it with a different clamp that the spec does not describe, which suggests the endpoint computation is under-specified rather than merely ambiguous.

Implementation and test status

Configuration Attribute definition Algorithm Chrome Firefox WebKit WPT
loopStart < 0 (A) [0, loopEnd) entire buffer [0, loopEnd) entire buffer entire buffer [0, loopEnd)
loopEnd <= 0 (B) [loopStart, duration) entire buffer entire buffer entire buffer entire buffer entire buffer
loopStart > duration (C) begin at end of buffer inverted range, diverges entire buffer [0, min(loopEnd, duration)) inverted range not covered
loopStart > loopEnd entire buffer entire buffer entire buffer entire buffer entire buffer
  • ChromeAudioBufferSourceHandler::UpdateEffectiveLoopPoints clamps loopStart to [0, buffer_duration], resolves loopEnd, then falls back to the entire buffer unless start < end.
  • FirefoxAudioBufferSourceNode::SendLoopParametersToTrack implements the algorithm's guard verbatim (mLoopStart >= 0.0 && mLoopEnd > 0.0 && mLoopStart < mLoopEnd), plus a separate special case mapping mLoopStart > length to 0.
  • WebKitAudioBufferSourceNode::renderFromBuffer implements the algorithm's guard, and inherits the inverted range in Case C.

Existing coverage in webaudio/the-audio-api/the-audiobuffersourcenode-interface/audiobuffersource-playbackrate-negative.html (added back in May 2026 by Mahesh Bharadwaj Kannan from Google):

  • AudioBufferSourceNode loops backwards using clamped loopStart when loopStart < 0 — asserts the attribute-definition reading for Case A.
  • AudioBufferSourceNode loops backwards using entire buffer when loopEnd < 0 — asserts the algorithm reading for Case B.
  • AudioBufferSourceNode loops backwards using entire buffer when loopStart > loopEnd — consistent with the algorithm.

Per the code above, engines implementing the algorithm's guard are expected to fail the first of these. Case C has no coverage.

I had written a PR in WebKit to get this WPT test to pass when I noticed Firefox was also failing most of this test and that the specification was unclear on this subject.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs DiscussionThe issue needs more discussion before it can be fixed.category: enhancementSubstantive changes that do not add new features. https://www.w3.org/policies/process/#class-3

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions