Symptom
Sounds scheduled via Tone.getTransport().schedule() inside a Tone.Offline() callback are silently missing from the rendered AudioBuffer — but only the sounds whose Tone.Player/Tone.Synth was constructed inside the schedule callback. Sounds whose nodes were constructed in the Offline() closure body render fine. No error is thrown; the affected nodes may even audibly play through the speakers during the render. Deterministic across browsers (verified Chromium 152, tone@15.1.22).
Root cause (from tone source)
In Tone.Offline (core/context/Offline.js), the global context is restored before the render promise is awaited:
setContext(context); // offline context becomes global
await callback(context); // your closure runs here — OK
const bufferPromise = context.render(); // async; body defers at first await
setContext(originalContext); // live context restored SYNCHRONOUSLY here
const buffer = await bufferPromise; // tick loop runs AFTER the restoreOfflineContext.render() awaits workletsAreReady() before running _renderClock(), so the transport tick loop — which fires every transport.schedule callback — always executes after setContext(originalContext). Any new Tone.Player(...) constructed inside a callback therefore defaults its context to getContext() = the live context, and .toDestination() connects it to the speakers. It contributes zero signal to the offline buffer.
Fix
Create every node in the Offline() closure body (one player per scheduled hit if you need overlap), and only call .start(t) / .triggerAttackRelease(..., time) inside the callback:
await Tone.Offline(() => {
const players = times.map(() => new Tone.Player(buf).toDestination()); // closure: offline ctx
times.forEach((time, i) =>
Tone.getTransport().schedule((t) => players[i].start(t), time)); // callback: start only
Tone.getTransport().start(0);
}, duration);Verification pattern
Minimal probe: schedule one closure-created player and one callback-created player at known times, render, and compare per-window RMS. Closure-created: RMS ~0.29 (noise burst); callback-created: RMS exactly 0, and player.context === offlineCtx is false inside the callback.
How it hides
If the export/muxing layer has its own bug (ours did: an unrelated WebKit esds double-wrap made the whole AAC track silent on Apple players), fixing that layer 'partially' restores audio — the closure-created theme music appears, the callback-created SFX stay missing — which misreads as a platform-specific audio bug when it is actually a deterministic Tone.js context-binding bug present on every browser.