--- name: tonejs description: Use when adding audio, sound design, music sequencing, or audio-reactive animation to a project. Use when syncing Tone.js with GSAP timelines, creating beat-locked animations, playing sound effects on motion events, generating oscillator tones, or building audio visualizers with waveform/frequency data driving DOM elements. license: MIT --- # Tone.js ## When to Use This Skill Apply when any of the following are needed: - Playing audio files (SFX, VO, music) alongside GSAP animations - Beat-synchronized or BPM-locked animation (e.g. scene cuts on the beat) - Audio-reactive visuals: DOM/SVG elements animated by waveform or frequency data - Programmatic sound generation (synths, oscillators, UI sounds) - Adding audio effects (reverb, delay, distortion) to any source **Related skills:** For timeline sequencing use **gsap-timeline**, for performance use **gsap-performance**, and for scroll-sync use **gsap-scrolltrigger**. > ⚠️ **Browser Autoplay Policy**: `Tone.start()` MUST be called inside a user-gesture handler (click, keydown, etc.). Audio context will not resume otherwise. Always gate initialization behind an interaction. --- ## Setup **CDN (vanilla HTML projects):** ```html ``` **npm / bun:** ```bash bun add tone ``` ```js import * as Tone from 'tone'; ``` **Required initialization (always wrap in user gesture):** ```js document.getElementById('start-btn').addEventListener('click', async () => { await Tone.start(); // Resume AudioContext Tone.getTransport().start(); // Start master clock }, { once: true }); ``` --- ## Core Architecture ``` Tone.js signal graph Source → Effect(s) → Destination Synth Reverb toDestination() Player Delay Analyser Distortion ``` All nodes connect via `.connect()` or `.toDestination()` shorthand. --- ## Quick Reference | Task | API | |---|---| | Play a tone | `new Tone.Synth().toDestination().triggerAttackRelease("C4", "8n")` | | Load + play file | `new Tone.Player(url).toDestination(); player.start()` | | Add reverb | `const rev = new Tone.Reverb(2).toDestination(); synth.connect(rev)` | | Schedule on beat | `Tone.getTransport().scheduleRepeat(cb, "4n")` | | Read waveform | `new Tone.Analyser("waveform", 64).getValue()` | | Read frequency | `new Tone.Analyser("fft", 32).getValue()` | | Set BPM | `Tone.getTransport().bpm.value = 120` | | Sync visual to audio | `Tone.getDraw().schedule(() => { /* gsap here */ }, time)` | | Stop everything | `Tone.getTransport().stop(); Tone.getTransport().cancel()` | --- ## GSAP Sync: The Right Way **Problem:** Tone callbacks run on the audio thread. Calling `gsap.to()` directly inside them causes jitter. **Solution:** Always use `Tone.getDraw().schedule()` to bridge audio events to the visual frame. ```js // Correct: no jitter Tone.getTransport().scheduleRepeat((time) => { Tone.getDraw().schedule(() => { gsap.fromTo('#element', { scale: 1 }, { scale: 1.2, duration: 0.1 }); }, time); }, '4n'); // Wrong: triggers on audio thread, causes jitter Tone.getTransport().scheduleRepeat((time) => { gsap.to('#element', { scale: 1.2 }); // DO NOT do this }, '4n'); ``` --- ## Audio-Reactive Animation Wire a `Tone.Analyser` into the GSAP ticker to drive DOM elements in real time: ```js const analyser = new Tone.Analyser('waveform', 64); const synth = new Tone.Synth().connect(analyser).toDestination(); // Pull audio data every animation frame via GSAP ticker gsap.ticker.add(() => { const waveform = analyser.getValue(); // Float32Array, values in [-1, 1] const bars = document.querySelectorAll('.bar'); bars.forEach((bar, i) => { const amp = Math.abs(waveform[i] ?? 0); gsap.set(bar, { scaleY: 1 + amp * 15 }); // gsap.set: no tween overhead }); }); synth.triggerAttackRelease('C3', '1n'); ``` > Use `gsap.set()` (not `gsap.to()`) inside `ticker.add()` because creating new tweens every frame is expensive. --- ## BPM-Locked Scene Cuts ```js Tone.getTransport().bpm.value = 120; // Fire visual on every measure Tone.getTransport().scheduleRepeat((time) => { Tone.getDraw().schedule(() => { gsap.fromTo('#scene', { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.3 }); }, time); }, '1m'); // 1m = 1 measure Tone.getTransport().start(); ``` --- ## Playing Audio Files (SFX / VO) ```js const player = new Tone.Player({ url: 'assets/audio/whoosh.mp3', autostart: false, }).toDestination(); // Trigger on a GSAP event gsap.to('#card', { y: -200, onStart: () => player.start(), }); ``` --- ## Effects Chain ```js const reverb = new Tone.Reverb({ decay: 3, wet: 0.4 }).toDestination(); const delay = new Tone.FeedbackDelay('8n', 0.3).connect(reverb); const synth = new Tone.Synth().connect(delay); synth.triggerAttackRelease('A3', '4n'); ``` --- ## Common Mistakes | Mistake | Fix | |---|---| | Audio does not start | Call `await Tone.start()` inside a click or keydown handler first | | Jittery visuals | Never call `gsap.to()` directly in Tone callbacks. Use `Tone.getDraw().schedule()` | | `gsap.to()` inside ticker | Use `gsap.set()` inside `gsap.ticker.add()` instead of creating tweens per frame | | Transport events fire twice | Call `Tone.getTransport().cancel()` before re-scheduling | | Player not ready | Wrap `player.start()` in the `player.load()` promise or use `onsuccess` | | High CPU from analyser | Use a small buffer size (32 to 64) and avoid FFT when waveform is enough | --- ## Minimal Working Example (vanilla HTML) ```html ```