Version: reproduced identically on 2.0.0 and 2.2.0 (latest published at time of filing). Environment: Node v24.16.0, macOS darwin/arm64. The two versions produce a cell-for-cell identical grid on the same machine, so nothing about this changed between them.
Reference check: Safari/WebKit (Version/26.5.2 Safari) renders the identical fixture bit-exact at every phase, length and rate — no extra sample, no interior perturbation.
Summary: When an AudioBufferSourceNode's start time falls on a multiple of the 128-sample render quantum, rendering is a bit-exact copy of the buffer. At any other start it is not: the node emits one sample beyond the buffer's length, and interior samples differ by ~1 ULP. The extra sample carries the buffer's own final value once the buffer exceeds 108339 samples (at 48 kHz and 96 kHz; 44.1 kHz behaves differently and does not exhibit it in that range).
Minimal reproduction (no third-party code):
const { OfflineAudioContext } = require('node-web-audio-api')
const SR = 48000, L = 109000, START = 24064 + 1 // +1 => off the 128-sample grid
const ctx = new OfflineAudioContext(1, START + L + 512, SR)
const buf = ctx.createBuffer(1, L, SR), d = buf.getChannelData(0)
for (let i = 0; i < L; i++) d[i] = -0.1 * Math.pow(0.99991, i) // smooth decay
const src = ctx.createBufferSource(); src.buffer = buf
src.connect(ctx.destination); src.start(START / SR)
ctx.startRendering().then((out) => {
const o = out.getChannelData(0)
console.log('one past the buffer end:', o[START + L]) // expected 0, observed -5.4876e-6
console.log('buffer final value :', d[L - 1]) // -5.4881e-6
})
Set START = 24064 (a multiple of 128) and the same sample is exactly 0.
Expected: a start() at a sample-aligned time renders the buffer as a direct copy; nothing is emitted past start + buffer.length.
Actual: one extra sample, plus ~1 ULP interior differences, whenever the start is not quantum-aligned.
Grid. Start phase 0..127 × buffer lengths {4096, 108000, 108300, 108339, 108400, 109000} × rates {44.1k, 48k, 96k}. Reported value is the magnitude of the sample one past the buffer's end (phases 1 / 64 / 127 — identical across all three). Identical output on 2.0.0 and 2.2.0:
| rate |
L=4096 |
108000 |
108300 |
108339 |
108400 |
109000 |
| 44.1 kHz |
1.29e-11 |
0 |
0 |
0 |
0 |
0 |
| 48 kHz |
1.74e-11 |
7.03e-14 |
8.50e-14 |
5.82e-6 |
5.79e-6 |
5.49e-6 |
| 96 kHz |
1.74e-11 |
7.03e-14 |
8.50e-14 |
5.82e-6 |
5.79e-6 |
5.49e-6 |
Three things the grid shows that a single repro does not:
- Phase 0 is exactly zero at every length and every rate — not small, zero. All 127 non-zero phases produce the identical magnitude. The behaviour is binary on quantum-alignment, with no dependence on where inside the quantum the start falls. At
L=109000: 0/128 phases exceed 1e-9 at 44.1 kHz; 127/128 at 48 kHz and 96 kHz.
- The threshold is a fixed sample count, not a time.
108339 fires identically at 48 kHz and 96 kHz — same sample count, half the duration. 108300 does not fire at either. A time-based boundary would have moved.
- 44.1 kHz never fires in this range — 0/128 at every tested length, so the rate does not merely shift the boundary proportionally.
Why it matters to us: we render offline at 48 kHz and sum many stroke buffers into one stage-length buffer. Before we understood this, every mid-quantum stroke of long-tailed material was contributing one spurious trailing sample. We have since restructured to play a single source at start(0), which is quantum-aligned by construction and takes the direct-copy path — so we are not blocked. Filing because the divergence from WebKit on the same fixture looks like a real bug rather than a spec latitude, and the fixed-sample-count threshold suggests a specific code path rather than general float behaviour.
Version: reproduced identically on 2.0.0 and 2.2.0 (latest published at time of filing). Environment: Node v24.16.0, macOS darwin/arm64. The two versions produce a cell-for-cell identical grid on the same machine, so nothing about this changed between them.
Reference check: Safari/WebKit (
Version/26.5.2 Safari) renders the identical fixture bit-exact at every phase, length and rate — no extra sample, no interior perturbation.Summary: When an
AudioBufferSourceNode's start time falls on a multiple of the 128-sample render quantum, rendering is a bit-exact copy of the buffer. At any other start it is not: the node emits one sample beyond the buffer's length, and interior samples differ by ~1 ULP. The extra sample carries the buffer's own final value once the buffer exceeds 108339 samples (at 48 kHz and 96 kHz; 44.1 kHz behaves differently and does not exhibit it in that range).Minimal reproduction (no third-party code):
Set
START = 24064(a multiple of 128) and the same sample is exactly0.Expected: a
start()at a sample-aligned time renders the buffer as a direct copy; nothing is emitted paststart + buffer.length.Actual: one extra sample, plus ~1 ULP interior differences, whenever the start is not quantum-aligned.
Grid. Start phase
0..127× buffer lengths{4096, 108000, 108300, 108339, 108400, 109000}× rates{44.1k, 48k, 96k}. Reported value is the magnitude of the sample one past the buffer's end (phases 1 / 64 / 127 — identical across all three). Identical output on 2.0.0 and 2.2.0:Three things the grid shows that a single repro does not:
L=109000: 0/128 phases exceed 1e-9 at 44.1 kHz; 127/128 at 48 kHz and 96 kHz.108339fires identically at 48 kHz and 96 kHz — same sample count, half the duration.108300does not fire at either. A time-based boundary would have moved.Why it matters to us: we render offline at 48 kHz and sum many stroke buffers into one stage-length buffer. Before we understood this, every mid-quantum stroke of long-tailed material was contributing one spurious trailing sample. We have since restructured to play a single source at
start(0), which is quantum-aligned by construction and takes the direct-copy path — so we are not blocked. Filing because the divergence from WebKit on the same fixture looks like a real bug rather than a spec latitude, and the fixed-sample-count threshold suggests a specific code path rather than general float behaviour.