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):
actualLoopStart = 5, actualLoopEnd = 3.
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.
- 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 |
- Chrome —
AudioBufferSourceHandler::UpdateEffectiveLoopPoints clamps loopStart to [0, buffer_duration], resolves loopEnd, then falls back to the entire buffer unless start < end.
- Firefox —
AudioBufferSourceNode::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.
- WebKit —
AudioBufferSourceNode::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.
The
loopStart/loopEndattribute 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:
The endpoint computation in the playback algorithm:
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 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[0, loopEnd)[0, buffer.duration)Differs whenever
loopEnd < buffer.duration.Case B —
loopEnd <= 0[loopStart, buffer.duration)[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, soactualLoopStart = loopStart(past the end of the buffer) whileactualLoopEnd = 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:Trace for a 3-second buffer with
loopStart = 5,loopEnd = 6,loop = true,playbackRate = 1,start(0, 0):actualLoopStart = 5,actualLoopEnd = 3.offset(0)< actualLoopEnd(3), so oncebufferTimereachesactualLoopStart(5) theenteredLoopcondition is satisfied andenteredLoopbecomestrue.bufferTimegets there while output is silent, since it is already pastbuffer.duration.whileloop then runs:5 >= 3, sobufferTime -= (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
loopStart < 0(A)[0, loopEnd)[0, loopEnd)[0, loopEnd)loopEnd <= 0(B)[loopStart, duration)loopStart > duration(C)[0, min(loopEnd, duration))loopStart > loopEndAudioBufferSourceHandler::UpdateEffectiveLoopPointsclampsloopStartto[0, buffer_duration], resolvesloopEnd, then falls back to the entire buffer unlessstart < end.AudioBufferSourceNode::SendLoopParametersToTrackimplements the algorithm's guard verbatim (mLoopStart >= 0.0 && mLoopEnd > 0.0 && mLoopStart < mLoopEnd), plus a separate special case mappingmLoopStart > lengthto0.AudioBufferSourceNode::renderFromBufferimplements 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.