Status: ✅ Substantially Complete (Phase 31) Last Updated: January 2026 Related: ROADMAP.md Phase 22 (Synthesis Engine) and Phase 25 (remaining items)
This document consolidates all remaining music synthesis functionality from the roadmap into a comprehensive specification, analyzes Tone.js as an implementation option, and evaluates the costs and implications of adoption.
- Current State
- Requirements
- Tone.js Analysis
- Implementation Options
- Migration Analysis
- Recommendations
- Tone.js Best Practices
- Verification Sessions
- User Interface Requirements ← Critical for feature completion
- Musical Surface Area Expansion ← NEW: Capability documentation
- Example Sessions ← NEW: Showcase templates
The current audio system is built on raw Web Audio API with these components:
| Component | File | Description |
|---|---|---|
| SynthEngine | synth.ts |
16-voice polyphonic synthesizer with voice stealing |
| SynthVoice | synth.ts |
Single oscillator → filter → gain chain |
| AudioEngine | engine.ts |
Sample playback, track routing, master compression |
| Scheduler | scheduler.ts |
Drift-free lookahead scheduling (25ms timer, 100ms lookahead) |
| Samples | samples.ts |
16 procedurally-generated drum/synth samples |
Signal Chain:
Source (Oscillator/Sample)
→ Envelope Gain
→ Track Gain
→ Master Gain
→ Compressor
→ Destination
Updated January 2026 - Reflects Phase 31 implementation state
| Feature | Status | Notes |
|---|---|---|
| Basic oscillators | ✅ | sine, triangle, sawtooth, square |
| ADSR envelope | ✅ | Single envelope per voice |
| Lowpass filter | ✅ | With resonance (Q) control |
| 32 Web Audio synth presets | ✅ | bass, lead, pad, pluck, acid, rhodes, supersaw, wobble, etc. |
| 11 Tone.js synth presets | ✅ | fm-epiano, fm-bass, fm-bell, membrane-kick, duo-lead, etc. |
| 21 sampled instruments | ✅ | piano, 808 kit, acoustic kit, vibraphone, strings, guitars, sax |
| Effects chain | ✅ | Reverb, delay, chorus, distortion (with limiter) |
| Polymetric sequencing | ✅ | Per-track step counts (3-128, 26 options) |
| Swing | ✅ | Global 0-100% + per-track swing |
| Pitch shifting | ✅ | ±24 semitones via parameter locks |
| Parameter locks | ✅ | Per-step pitch, volume, and tie |
| Master compression | ✅ | Prevents clipping |
| Per-track loop length | ✅ | 3-128 steps for polyrhythms |
| FM synthesis | ✅ | Via Tone.js FM synths |
Total Sound Generators: 64 (32 Web Audio + 11 Tone.js + 21 sampled)
| Limitation | Impact |
|---|---|
| Single oscillator per Web Audio voice | Tone.js synths provide more complex timbres |
| No filter envelope on Web Audio synths | Tone.js synths have filter envelopes |
| No LFO on Web Audio synths | Chorus effect provides modulation, Tone.js has LFO |
| Fixed filter type (Web Audio) | Only lowpass available on basic synths |
interface OscillatorConfig {
waveform: 'sine' | 'sawtooth' | 'square' | 'triangle';
level: number; // 0 to 1 (mix between oscillators)
detune: number; // Cents (-100 to +100)
coarseDetune: number; // Semitones (-24 to +24)
noise: number; // 0 to 1 (noise mix)
}New sounds enabled:
- Detuned supersaw (trance/EDM)
- Layered octaves (full pads)
- PWM-style thickness
- Sub-oscillator bass
interface FilterConfig {
frequency: number; // 20 to 20000 Hz
resonance: number; // 0 to 30 (Q factor)
type: 'lowpass' | 'highpass' | 'bandpass';
envelopeAmount: number; // -1 to 1 (envelope → cutoff)
lfoAmount: number; // 0 to 1 (LFO → cutoff)
}
interface FilterEnvelope {
attack: number; // 0.001 to 2s
decay: number; // 0.001 to 2s
sustain: number; // 0 to 1
release: number; // 0.001 to 4s
}interface LFOConfig {
frequency: number; // 0.1 to 20 Hz
waveform: 'sine' | 'sawtooth' | 'square' | 'triangle';
destination: 'filter' | 'pitch' | 'amplitude';
amount: number; // 0 to 1
sync: boolean; // Sync to transport tempo
}New sounds enabled:
- Vibrato (LFO → pitch at 5-7 Hz)
- Tremolo (LFO → amplitude at 4-8 Hz)
- Filter sweeps (LFO → filter)
- Wobble bass (LFO → filter at 1-4 Hz)
interface SynthPreset {
name: string;
oscillators: [OscillatorConfig, OscillatorConfig];
amplitudeEnvelope: ADSREnvelope;
filter: FilterConfig;
filterEnvelope: ADSREnvelope;
lfo: LFOConfig;
}
⚠️ CRITICAL: Effects must be synchronized across multiplayer sessions. All effect parameters must be stored in session state and broadcast via WebSocket.
| Effect | Priority | Parameters | Use Case |
|---|---|---|---|
| Reverb | High | type, decay, mix | Space, depth |
| Delay | High | time, feedback, mix | Rhythmic interest |
| Chorus | Medium | rate, depth, mix | Stereo width, warmth |
| Distortion | Medium | amount, mix | Grit, edge |
| Compressor | Low | Already exists | Dynamics control |
interface EffectsChain {
reverb?: {
type: 'room' | 'hall' | 'plate';
decay: number; // 0.1 to 10s
mix: number; // 0 to 1
};
delay?: {
time: number; // ms or beat-synced ("8n")
feedback: number; // 0 to 0.95
mix: number; // 0 to 1
};
chorus?: {
rate: number; // 0.1 to 10 Hz
depth: number; // 0 to 1
mix: number; // 0 to 1
};
distortion?: {
amount: number; // 0 to 1
mix: number; // 0 to 1
};
}Effects must be:
- Stored in
SessionState(persisted to KV) - Broadcast via WebSocket on change
- Validated server-side
- Applied identically on all clients
| Requirement | Specification |
|---|---|
| Storage | R2 bucket (keyboardia-samples/instruments/) |
| Formats | MP3 (compressed) or WAV (quality) |
| Multi-sampling | 1 sample per octave minimum |
| Pitch shifting | Fill gaps between sampled notes |
| Lazy loading | Load on first use |
| Size budget | ~500KB-2MB per instrument |
| Instrument | Priority | Samples Needed | Estimated Size |
|---|---|---|---|
| Piano | High | C2, C3, C4, C5 | ~800KB |
| Strings | Medium | C2, C3, C4, C5 | ~1MB |
| Brass | Medium | C3, C4, C5 | ~600KB |
| Electric Piano | Medium | C3, C4, C5 | ~500KB |
interface InstrumentManifest {
name: string;
samples: {
note: string; // "C4", "G#3", etc.
url: string; // R2 URL
loopStart?: number; // For sustaining instruments
loopEnd?: number;
}[];
envelope?: ADSREnvelope;
defaultVelocity?: number;
}interface XYPadMapping {
parameter: 'filterFrequency' | 'filterResonance' | 'lfoRate' | 'lfoAmount' | 'oscMix' | 'attack' | 'release';
axis: 'x' | 'y';
min: number;
max: number;
curve: 'linear' | 'exponential';
}
interface XYPad {
mappings: XYPadMapping[];
x: number; // 0 to 1
y: number; // 0 to 1
}interface FMPreset {
carriers: OscillatorConfig[];
modulators: {
target: number; // Which carrier to modulate
ratio: number; // Frequency ratio
depth: number; // Modulation amount
envelope: ADSREnvelope;
}[];
}Sounds enabled: Electric piano (DX7-style), bells, metallic percussion
Tone.js is a Web Audio framework for creating interactive music in the browser. It provides DAW-like features including:
- Global transport for synchronization
- Prebuilt synthesizers and effects
- High-performance building blocks
- Musical time notation ("4n", "8t", "1m")
| Synth | Description | Polyphonic | Maps To |
|---|---|---|---|
| Synth | Single oscillator + ADSR | No | Basic tones |
| MonoSynth | Oscillator + filter + envelopes | No | Lead, bass |
| DuoSynth | Two MonoSynths in parallel | No | Rich leads |
| FMSynth | Frequency modulation | No | Bells, e-piano |
| AMSynth | Amplitude modulation | No | Tremolo tones |
| MembraneSynth | Frequency sweep | No | Kicks, toms |
| MetalSynth | 6 FM oscillators | No | Cymbals, metallic |
| NoiseSynth | Filtered noise | No | Hi-hats, snares |
| PluckSynth | Karplus-Strong | No | Plucked strings |
| PolySynth | Wrapper for polyphony | Yes | Chords, pads |
| Sampler | Note-mapped samples | Yes | Piano, instruments |
| Effect | Description | Parameters |
|---|---|---|
| Reverb | Convolution reverb | decay, preDelay, wet |
| Freeverb | Algorithmic reverb | roomSize, dampening, wet |
| JCReverb | Simple reverb | roomSize |
| FeedbackDelay | Delay with feedback | delayTime, feedback, wet |
| PingPongDelay | Stereo ping-pong | delayTime, feedback, wet |
| Chorus | Chorus effect | frequency, delayTime, depth, wet |
| Phaser | Phaser effect | frequency, octaves, baseFrequency |
| Tremolo | Amplitude modulation | frequency, depth, wet |
| Vibrato | Pitch modulation | frequency, depth, wet |
| Distortion | Waveshaping distortion | distortion, wet |
| BitCrusher | Bit depth reduction | bits |
| Chebyshev | Harmonic distortion | order |
| AutoFilter | LFO-controlled filter | frequency, baseFrequency, octaves |
| AutoPanner | LFO-controlled pan | frequency, depth |
| AutoWah | Envelope follower wah | baseFrequency, octaves, sensitivity |
| PitchShift | Pitch without speed change | pitch, windowSize, wet |
| StereoWidener | Stereo enhancement | width |
| Compressor | Dynamics compression | threshold, ratio, attack, release |
| Limiter | Hard limiting | threshold |
| Gate | Noise gate | threshold, attack, release |
| EQ3 | 3-band EQ | low, mid, high |
// Musical time notation
Tone.Transport.bpm.value = 120;
Tone.Transport.start();
// Schedule events
Tone.Transport.schedule((time) => {
synth.triggerAttackRelease("C4", "8n", time);
}, "0:0:0");
// Looping
const loop = new Tone.Loop((time) => {
// Called every quarter note
}, "4n").start(0);
// Tempo ramping
Tone.Transport.bpm.rampTo(140, 4); // Ramp to 140 BPM over 4 seconds// Sampler - automatic pitch shifting
const piano = new Tone.Sampler({
urls: {
C4: "C4.mp3",
"D#4": "Ds4.mp3",
"F#4": "Fs4.mp3",
A4: "A4.mp3",
},
baseUrl: "https://r2.example.com/piano/",
onload: () => console.log("Piano loaded")
}).toDestination();
piano.triggerAttackRelease("E4", "8n"); // Automatically repitched from nearest sample
// Player - direct playback
const player = new Tone.Player("kick.mp3").toDestination();
player.start();// LFO
const lfo = new Tone.LFO({
frequency: 4,
min: 200,
max: 4000
}).start();
// Connect to filter
lfo.connect(filter.frequency);
// AutoFilter (built-in LFO → filter)
const autoFilter = new Tone.AutoFilter("4n").toDestination().start();
synth.connect(autoFilter);| Metric | Value | Notes |
|---|---|---|
| Unpacked | ~2.8 MB | Full source |
| Minified | ~400-500 KB | Full library |
| Gzipped | ~100-120 KB | Compressed transfer |
| Tree-shaken | ~50-200 KB | Depends on imports |
Tree-shaking: Tone.js supports tree-shaking. Importing individual modules reduces bundle size significantly.
// Full import (~400KB minified)
import * as Tone from "tone";
// Selective import (~50-100KB depending on usage)
import { Synth, FeedbackDelay, Reverb } from "tone";- Chrome: ✅ Full support
- Firefox: ✅ Full support
- Safari: ✅ (requires user gesture for AudioContext)
- Edge: ✅ Full support
- iOS Safari: ✅ (requires user gesture)
- Chrome Android: ✅ (may need resume on visibility change)
Uses standardized-audio-context shim for maximum compatibility.
| Limitation | Workaround |
|---|---|
| Requires user gesture | Already handled in Keyboardia |
| BPM changes can affect scheduled events | Use Tone.now() for immediate scheduling |
| Reverb is async (IR generation) | Await reverb.ready promise |
| No MIDI file playback | Convert to JSON first |
Build new features on top of existing raw Web Audio API code.
Effort Estimate:
| Feature | Effort | Complexity |
|---|---|---|
| Dual oscillator | 2-3 days | Medium |
| Filter envelope | 1-2 days | Low |
| LFO system | 2-3 days | Medium |
| Reverb (ConvolverNode) | 2-3 days | Medium |
| Delay (DelayNode) | 1-2 days | Low |
| Chorus | 2-3 days | Medium |
| Sampler with pitch shifting | 3-5 days | High |
| FM synthesis | 3-5 days | High |
| Total | 16-26 days | — |
Pros:
- No new dependencies
- Full control over implementation
- No bundle size increase
- Existing patterns maintained
Cons:
- Significant development time
- Must handle edge cases ourselves
- No battle-tested abstractions
- Higher maintenance burden
Replace all audio code with Tone.js.
Effort Estimate:
| Task | Effort | Complexity |
|---|---|---|
| Learn Tone.js API | 2-3 days | — |
| Replace SynthEngine | 2-3 days | Medium |
| Replace AudioEngine | 2-3 days | Medium |
| Replace Scheduler | 3-5 days | High |
| Adapt multiplayer sync | 2-3 days | Medium |
| Add new synth types | 1-2 days | Low |
| Add effects | 1-2 days | Low |
| Add sampler | 1 day | Low |
| Testing & debugging | 3-5 days | — |
| Total | 17-26 days | — |
Pros:
- Rich built-in synths (FM, AM, Membrane, Metal, etc.)
- Full effects suite out of the box
- Battle-tested, well-documented
- Musical time notation ("4n", "8t")
- Built-in Sampler with auto-pitch-shifting
- Active community and maintenance
Cons:
- +100-120KB gzipped bundle size
- Learning curve for team
- Must adapt existing patterns
- Transport may conflict with custom scheduler
- Loss of fine-grained control
Keep existing scheduler and sample playback. Use Tone.js for:
- Advanced synth types (FMSynth, DuoSynth)
- Effects chain
- Sampler
Effort Estimate:
| Task | Effort | Complexity |
|---|---|---|
| Integrate Tone.js synths | 2-3 days | Medium |
| Integrate Tone.js effects | 1-2 days | Low |
| Integrate Tone.js Sampler | 1-2 days | Low |
| Wire to existing scheduler | 2-3 days | Medium |
| Multiplayer sync for new features | 2-3 days | Medium |
| Testing | 2-3 days | — |
| Total | 10-16 days | — |
Pros:
- Faster path to new features
- Keep battle-tested scheduler
- Incremental adoption
- Can cherry-pick Tone.js features
- Smaller bundle if tree-shaken
Cons:
- Two audio paradigms in codebase
- Potential timing conflicts
- More complex architecture
- May not fully utilize Tone.js Transport
| Current | Tone.js | Implication |
|---|---|---|
| Custom lookahead scheduler | Tone.Transport |
Different scheduling model |
setInterval + AudioContext time |
Tone.Loop, scheduleRepeat |
Must migrate all scheduling |
| Manual drift correction | Built-in | Simplifies code |
Polymetric via % |
Must implement manually | Transport doesn't natively support |
Critical: Our polymetric sequencing (tracks with different step counts) is not natively supported by Tone.Transport. We would need to either:
- Keep our scheduler and use Tone.js only for synthesis/effects
- Implement polymetric logic on top of Transport
| Current | Tone.js | Benefit |
|---|---|---|
SynthVoice class |
Tone.Synth, Tone.MonoSynth |
More presets, less code |
| Manual oscillator setup | Declarative config | Cleaner code |
| Single oscillator | DuoSynth, FMSynth, etc. |
Rich sounds |
| Manual ADSR | Tone.Envelope |
Built-in curves |
| Current | Tone.js | Benefit |
|---|---|---|
| Manual BufferSource | Tone.Player |
Simpler API |
| Manual pitch via playbackRate | Tone.Sampler auto-pitch |
Multi-sampled instruments |
| Custom sample loading | Tone.Buffer |
Progress callbacks |
| Current | Tone.js | Benefit |
|---|---|---|
| Only compression | Full effect suite | Reverb, delay, chorus, etc. |
| Manual node wiring | .connect() chaining |
Cleaner routing |
Both the current implementation and Tone.js require the same multiplayer sync approach:
- State in KV: Store synth/effect parameters in session state
- WebSocket broadcast: Send parameter changes to all clients
- Server validation: Validate bounds before applying
- Deterministic playback: Same inputs → same audio
Tone.js does NOT automatically handle multiplayer sync. We must implement the same patterns regardless of which option we choose.
| Scenario | Current | With Tone.js | Delta |
|---|---|---|---|
| Current bundle | ~250KB gzipped | — | — |
| Full Tone.js | — | ~370KB gzipped | +120KB |
| Tree-shaken | — | ~300-320KB | +50-70KB |
Impact: ~0.3-0.5 seconds additional load time on 3G.
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Timing conflicts between scheduler and Transport | Medium | High | Use hybrid approach or full migration |
| Bundle size affects mobile UX | Low | Medium | Tree-shake aggressively |
| Learning curve delays delivery | Medium | Medium | Spike first, document patterns |
| Tone.js bugs affect stability | Low | Medium | Pin version, test thoroughly |
| Polymetric breaks with Transport | High | High | Keep custom scheduler |
Rationale:
- Our custom scheduler is battle-tested and supports polymetric sequencing natively
- Tone.js Transport doesn't support polymetric without custom work
- We can incrementally adopt Tone.js features without full rewrite
- Lower risk, faster delivery
Add global effects bus using Tone.js:
import { Reverb, FeedbackDelay, Chorus } from "tone";
// Create effects
const reverb = new Reverb({ decay: 2, wet: 0.3 });
const delay = new FeedbackDelay({ delayTime: "8n", feedback: 0.3, wet: 0.2 });
// Connect master output through effects
masterGain.connect(reverb);
reverb.connect(delay);
delay.toDestination();Multiplayer sync: Add effects to SessionState, broadcast on change.
Replace manual pitch-shifting with Tone.Sampler:
import { Sampler } from "tone";
const piano = new Sampler({
urls: {
C2: "C2.mp3",
C3: "C3.mp3",
C4: "C4.mp3",
C5: "C5.mp3",
},
baseUrl: "/api/samples/piano/",
});
// In scheduler, trigger notes
piano.triggerAttackRelease(note, duration, time);Add Tone.js synth types as new presets:
import { FMSynth, DuoSynth, MembraneSynth, MetalSynth } from "tone";
const synthTypes = {
'fm-epiano': new FMSynth({ /* DX7-style config */ }),
'duo-lead': new DuoSynth({ /* rich lead config */ }),
'membrane-kick': new MembraneSynth({ /* 808-style config */ }),
'metal-cymbal': new MetalSynth({ /* cymbal config */ }),
};Add dual oscillator and LFO to our existing SynthEngine:
- Could use Tone.js
OscillatorandLFOcomponents - Or implement natively for full control
| Component | Keep | Replace | Notes |
|---|---|---|---|
| Scheduler | ✅ | Polymetric support, battle-tested | |
| SynthEngine (basic) | ✅ | Works well, 19 presets | |
| AudioEngine (samples) | ✅ | Procedural samples work | |
| Effects | ✅ Tone.js | Major capability gap | |
| Sampler (instruments) | ✅ Tone.js | Auto pitch-shifting | |
| FM/AM Synths | ✅ Tone.js | Complex to implement | |
| Master routing | Hybrid | Keep master, add effects |
- Reverb, delay, chorus, and distortion effects work locally (multiplayer sync deferred to Phase 25)
- Piano sampler plays full 4-octave range from 4 samples
- FM synth presets (fm-epiano, fm-bass, fm-bell) implemented
- Bundle size increase < 80KB gzipped (not yet measured)
- No timing drift introduced
- All 601 tests pass (was 443, added 158 new tests)
- New features have tests (60 tests for Tone.js integration)
Note: Effects multiplayer sync is tracked in Phase 25 remaining work. See ROADMAP.md.
| Phase | Duration | Deliverable |
|---|---|---|
| Effects integration | 2 days | Reverb + Delay in multiplayer |
| Sampler integration | 2 days | Piano instrument |
| Advanced synths | 3 days | FM, DuoSynth presets |
| Testing & polish | 2 days | Stability, edge cases |
| Total | 9 days | Full feature set |
// src/audio/toneEffects.ts
import { Reverb, FeedbackDelay, Chorus, getDestination, connect } from "tone";
export interface EffectsState {
reverb: { decay: number; wet: number };
delay: { time: string; feedback: number; wet: number };
chorus: { frequency: number; depth: number; wet: number };
}
export class ToneEffectsChain {
private reverb: Reverb;
private delay: FeedbackDelay;
private chorus: Chorus;
constructor() {
this.reverb = new Reverb({ decay: 2, wet: 0 });
this.delay = new FeedbackDelay({ delayTime: "8n", feedback: 0.3, wet: 0 });
this.chorus = new Chorus({ frequency: 1.5, depth: 0.5, wet: 0 });
// Chain: input → chorus → delay → reverb → destination
this.chorus.connect(this.delay);
this.delay.connect(this.reverb);
this.reverb.toDestination();
}
get input() {
return this.chorus;
}
setReverbWet(wet: number) {
this.reverb.wet.value = wet;
}
setDelayWet(wet: number) {
this.delay.wet.value = wet;
}
// ... other setters
applyState(state: EffectsState) {
this.reverb.decay = state.reverb.decay;
this.reverb.wet.value = state.reverb.wet;
this.delay.delayTime.value = state.delay.time;
this.delay.feedback.value = state.delay.feedback;
this.delay.wet.value = state.delay.wet;
// ...
}
}// Addition to GridState
interface GridState {
// ... existing fields
effects: {
reverb: { decay: number; wet: number };
delay: { time: string; feedback: number; wet: number };
chorus: { frequency: number; depth: number; wet: number };
};
}
// New WebSocket message types
type EffectsMessage =
| { type: 'set_reverb'; decay?: number; wet?: number }
| { type: 'set_delay'; time?: string; feedback?: number; wet?: number }
| { type: 'set_chorus'; frequency?: number; depth?: number; wet?: number };// CORRECT: Start Tone.js after user gesture
document.querySelector('button').addEventListener('click', async () => {
await Tone.start();
console.log('Audio context started, state:', Tone.context.state);
});
// WRONG: Don't call Tone.start() on page load
// The AudioContext will be suspended and audio won't playIntegration with Keyboardia: Our existing audioEngine.initialize() already handles user gesture requirements. When integrating Tone.js, call Tone.start() inside the same handler.
// For interactive applications (default)
Tone.setContext(new Tone.Context({ latencyHint: "interactive" }));
// For sustained playback (better stability, higher latency)
Tone.setContext(new Tone.Context({ latencyHint: "playback" }));
// Custom lookahead (default is 0.1 seconds)
Tone.context.lookAhead = 0.05; // 50ms for lower latencyRecommendation: Use "interactive" for step sequencer responsiveness.
// CORRECT: Schedule slightly in the future to avoid artifacts
Tone.Transport.start("+0.1"); // Start 100ms in the future
// CORRECT: Trigger synths with a small offset
synth.triggerAttackRelease("C4", "8n", Tone.now() + 0.05);
// WRONG: Immediate triggers can cause pops
synth.triggerAttackRelease("C4", "8n"); // May cause click| Node | CPU Cost | Recommendation |
|---|---|---|
| ConvolverNode (Reverb) | High | Use Freeverb for real-time, Reverb for quality |
| PannerNode (HRTF) | High | Use stereo panning instead |
| Multiple oscillators | Medium | Limit polyphony (Keyboardia: 16 voices max) |
| AutoFilter/AutoWah | Medium | Use sparingly |
// WRONG: DOM manipulation in audio callback
Tone.Transport.scheduleRepeat((time) => {
document.querySelector('.step').classList.add('active'); // BAD
}, "16n");
// CORRECT: Use Tone.Draw for visuals
Tone.Transport.scheduleRepeat((time) => {
Tone.Draw.schedule(() => {
document.querySelector('.step').classList.add('active');
}, time);
}, "16n");Note: Keyboardia uses its own scheduler with requestAnimationFrame for UI updates, which is equivalent.
// ALWAYS dispose Tone.js objects when done
const synth = new Tone.Synth().toDestination();
// ... use synth ...
synth.dispose(); // Free memory
// For effects chain
const reverb = new Tone.Reverb();
const delay = new Tone.FeedbackDelay();
// ... use effects ...
reverb.dispose();
delay.dispose();// CORRECT: Create effects once, reuse
class ToneEffectsChain {
private reverb: Reverb | null = null;
async initialize() {
this.reverb = new Reverb({ decay: 2 });
await this.reverb.ready; // Wait for IR generation
}
dispose() {
this.reverb?.dispose();
this.reverb = null;
}
}
// WRONG: Creating new effects per note
function playNote() {
const reverb = new Reverb(); // Memory leak!
synth.connect(reverb);
}Tone.js pre-allocates ~5MB for noise buffers (white, pink, brown). If not using NoiseSynth, this memory is wasted but unavoidable with full Tone.js import.
Mitigation: Use selective imports to avoid loading unused modules.
// iOS Safari may require additional unlock
const unlockAudio = async () => {
await Tone.start();
// Additional iOS workaround: play silent buffer
const buffer = Tone.context.createBuffer(1, 1, 22050);
const source = Tone.context.createBufferSource();
source.buffer = buffer;
source.connect(Tone.context.destination);
source.start(0);
};
// Attach to user gesture
document.addEventListener('touchstart', unlockAudio, { once: true });Safari-specific:
- Maximum 4 AudioContext instances per page
- Ringer switch mutes Web Audio (device must be unmuted)
- iOS 15+ may require
<audio>element playback first
// Reverb generates impulse response asynchronously
const reverb = new Tone.Reverb({ decay: 2 });
// WRONG: Use immediately (IR may not be ready)
synth.connect(reverb);
// CORRECT: Wait for ready
await reverb.ready;
synth.connect(reverb);
// Or use Freeverb (no async generation)
const freeverb = new Tone.Freeverb(); // Ready immediately// Pattern: Connect existing AudioEngine to Tone.js effects
class HybridAudioEngine {
private toneEffects: ToneEffectsChain | null = null;
private audioContext: AudioContext | null = null;
async initialize() {
// 1. Start Tone.js first (shares AudioContext)
await Tone.start();
// 2. Use Tone's context for our audio
this.audioContext = Tone.context.rawContext as AudioContext;
// 3. Initialize effects
this.toneEffects = new ToneEffectsChain();
await this.toneEffects.initialize();
// 4. Connect our master gain to Tone effects input
this.masterGain.connect(this.toneEffects.input);
}
}Purpose: Verify reverb, delay, and chorus work correctly and sync across multiplayer.
Session Configuration:
const effectsTestSession = {
name: "Effects Test Session",
tracks: [
{ sampleId: "kick", steps: [true, false, false, false, true, false, false, false, ...] },
{ sampleId: "synth:lead", steps: [false, false, true, false, false, false, true, false, ...] },
],
tempo: 120,
swing: 0,
effects: {
reverb: { decay: 2.5, wet: 0.4 },
delay: { time: "8n", feedback: 0.3, wet: 0.25 },
chorus: { frequency: 1.5, depth: 0.5, wet: 0 },
},
};Verification Checklist:
- Reverb adds audible space to dry signal
- Delay creates rhythmic echoes at 8th-note intervals
- Wet = 0 produces dry signal only
- Wet = 1 produces fully wet signal
- Changing reverb decay in one client updates all clients
- Effects persist after page refresh
- Effects work on mobile Safari/Chrome
Automated Tests:
describe('ToneEffectsChain', () => {
it('initializes with reverb ready', async () => {
const chain = new ToneEffectsChain();
await chain.initialize();
expect(chain.isReady()).toBe(true);
});
it('applies reverb wet correctly', () => {
chain.setReverbWet(0.5);
expect(chain.getState().reverb.wet).toBe(0.5);
});
it('serializes state for multiplayer sync', () => {
const state = chain.getState();
expect(state).toMatchObject({
reverb: { decay: expect.any(Number), wet: expect.any(Number) },
delay: { time: expect.any(String), feedback: expect.any(Number), wet: expect.any(Number) },
});
});
});Purpose: Verify Tone.Sampler plays piano across 4 octaves with correct pitch.
Session Configuration:
const pianoTestSession = {
name: "Piano Test Session",
tracks: [
{
sampleId: "sampler:piano",
steps: [true, false, true, false, true, false, true, false, ...],
parameterLocks: [
{ pitch: -12 }, // C3
null,
{ pitch: 0 }, // C4
null,
{ pitch: 12 }, // C5
null,
{ pitch: 24 }, // C6
null,
],
},
],
tempo: 90, // Slower to hear pitch clearly
swing: 0,
};Verification Checklist:
- All 4 octaves are audible and correctly pitched
- Intermediate notes (D4, E4, F#4) are repitched from nearest sample
- No audible artifacts from pitch shifting
- Sampler loads lazily (not on page load)
- Loading indicator shown while samples load
- Works offline after initial load (samples cached)
Automated Tests:
describe('ToneSampler', () => {
it('loads piano samples from R2', async () => {
const sampler = new ToneSamplerInstrument('piano');
await sampler.load();
expect(sampler.isLoaded()).toBe(true);
});
it('plays correct frequency for C4', () => {
const freq = sampler.noteToFrequency('C4');
expect(freq).toBeCloseTo(261.63, 1);
});
it('repitches D4 from C4 sample', () => {
// D4 is 2 semitones above C4
const playbackRate = sampler.getPlaybackRate('D4');
expect(playbackRate).toBeCloseTo(Math.pow(2, 2/12), 3);
});
});Purpose: Verify FMSynth produces DX7-style electric piano sound.
Session Configuration:
const fmTestSession = {
name: "FM E-Piano Test",
tracks: [
{
sampleId: "synth:fm-epiano",
steps: [true, false, false, true, false, false, true, false, ...],
parameterLocks: [
{ pitch: 0 },
null,
null,
{ pitch: 4 }, // E
null,
null,
{ pitch: 7 }, // G
null,
],
},
],
tempo: 100,
swing: 15,
};Verification Checklist:
- Sound has characteristic FM "bell" attack
- Tone is bright with harmonic complexity
- Envelope has percussive attack, medium decay
- Sounds similar to DX7 Rhodes preset
- No aliasing or digital artifacts
- Works at all pitch values (-24 to +24)
Audio Reference: Compare to Ableton Learning Synths FM examples
Automated Tests:
describe('ToneFMSynth presets', () => {
it('has fm-epiano preset with correct harmonicity', () => {
const preset = TONE_SYNTH_PRESETS['fm-epiano'];
expect(preset.harmonicity).toBeGreaterThan(1);
expect(preset.modulationIndex).toBeGreaterThan(5);
});
it('produces sound within 100ms of trigger', async () => {
const output = await measureAudioOutput(() => {
fmSynth.triggerAttackRelease('C4', '8n');
});
expect(output.firstSoundAt).toBeLessThan(0.1);
});
});Purpose: Verify effects work correctly with polymetric sequencing.
Session Configuration:
const polymetricEffectsSession = {
name: "Polymetric + Effects",
tracks: [
{ sampleId: "kick", stepCount: 4, steps: [true, false, false, false] },
{ sampleId: "hihat", stepCount: 8, steps: [true, true, true, true, true, true, true, true] },
{ sampleId: "synth:pad", stepCount: 16, steps: [true, ...Array(15).fill(false)] },
],
tempo: 120,
swing: 0,
effects: {
reverb: { decay: 3, wet: 0.6 },
delay: { time: "4n", feedback: 0.4, wet: 0.3 },
chorus: { frequency: 0.5, depth: 0.7, wet: 0.2 },
},
};Verification Checklist:
- Kick loops every beat (4 steps)
- Hi-hat loops every half bar (8 steps)
- Pad plays once per bar (16 steps)
- All tracks pass through effects chain
- No timing drift after 1 minute
- Pattern phases correctly over multiple bars
Purpose: Verify two clients hear identical audio with effects.
Test Procedure:
- Client A creates session with effects (reverb wet = 0.5)
- Client B joins session
- Verify Client B loads with reverb wet = 0.5
- Client A changes reverb wet to 0.8
- Verify Client B updates within 100ms
- Both clients start playback
- Record audio from both clients
- Compare waveforms (should be identical within tolerance)
Automated Tests:
describe('Multiplayer effects sync', () => {
it('broadcasts effect changes to all clients', async () => {
const clientA = await connectToSession(sessionId);
const clientB = await connectToSession(sessionId);
clientA.send({ type: 'set_reverb', wet: 0.8 });
await waitFor(() => {
expect(clientB.state.effects.reverb.wet).toBe(0.8);
});
});
it('applies effects identically on all clients', async () => {
// This requires audio comparison testing
const audioA = await recordAudio(clientA, 2000);
const audioB = await recordAudio(clientB, 2000);
expect(compareWaveforms(audioA, audioB)).toBeLessThan(0.01);
});
});Purpose: Verify effects don't cause performance issues on mobile.
Test Devices:
- iPhone 12 (Safari)
- iPhone SE (Safari)
- Pixel 6 (Chrome)
- Samsung Galaxy S21 (Chrome)
Session Configuration:
const mobileStressTest = {
name: "Mobile Stress Test",
tracks: Array(8).fill(null).map((_, i) => ({
sampleId: i < 4 ? ['kick', 'snare', 'hihat', 'clap'][i] : `synth:${['bass', 'lead', 'pad', 'pluck'][i-4]}`,
stepCount: 16,
steps: Array(16).fill(true), // All steps active
})),
tempo: 140, // Fast tempo
effects: {
reverb: { decay: 2, wet: 0.5 },
delay: { time: "16n", feedback: 0.5, wet: 0.4 },
chorus: { frequency: 2, depth: 0.5, wet: 0.3 },
},
};Verification Checklist:
- No audio glitches/crackles after 1 minute
- CPU usage < 50% on iPhone 12
- No dropped frames in UI
- Memory usage stable (no growth over time)
- Battery drain acceptable
CRITICAL: This section addresses the "Three Surfaces" alignment requirement from
lessons-learned.md. Every feature must have: API ✓, State ✓, UI ✓.
The original spec focused on backend architecture and TypeScript interfaces. It violated the core lesson:
"API, UI, and State must align" — A feature isn't done until all three support it.
Without UI designs:
- Effects were implemented but users can't control them
- New synths were added but users can't select them
- The "cockpit has no controls"
From UI-PHILOSOPHY.md, all UI must follow:
| Principle | Application to Effects/Synths |
|---|---|
| Controls live where they act | Effects are global → controls in Transport bar |
| Visual feedback is immediate | Slider movement = instant audio change |
| No confirmation dialogs | Drag slider = effect changes (no "Apply" button) |
| Modes are visible | Effect wet/dry is always shown |
| Progressive disclosure | Basic controls visible, advanced on expand |
Effects are global (affect all tracks), so controls belong in the Transport bar alongside BPM and Swing:
┌─────────────────────────────────────────────────────────────────────────┐
│ [▶] BPM [====120] Swing [====30%] │ [FX] ← Toggle effects panel │
└─────────────────────────────────────────────────────────────────────────┘
│
▼ (click expands)
┌─────────────────────────────────────────────────────────────────────────┐
│ Effects [×] │
├─────────────────────────────────────────────────────────────────────────┤
│ REVERB [======○====] 30% Decay [=====○] 2.0s │
│ DELAY [===○=======] 20% Time [8n ▼] Feedback [====○] │
│ CHORUS [○==========] 0% Rate [====○] Depth [====○] │
│ DISTORT [○==========] 0% Drive [====○] │
└─────────────────────────────────────────────────────────────────────────┘
| Action | Result | Sync |
|---|---|---|
| Drag wet slider | Immediate effect change | Broadcast to all clients |
| Drag parameter slider | Immediate parameter change | Broadcast to all clients |
| Click [FX] button | Toggle panel visibility | Local only (UI state) |
| Change delay time dropdown | Immediate tempo-sync change | Broadcast to all clients |
// app/src/components/EffectsPanel.tsx
interface EffectsPanelProps {
effects: EffectsState;
onEffectsChange: (effects: Partial<EffectsState>) => void;
disabled?: boolean; // True on published sessions
}All effects start dry (wet = 0):
- User must explicitly enable effects
- Prevents unexpected sound changes for new users
- Aligns with "no surprises" principle
Add Track:
Drums: [Kick] [Snare] [Hi-Hat] ...
Bass: [Bass] [Sub Bass]
Samples: [Lead] [Pluck] ...
FX: [Zap] [Noise]
Synth:
Core: [Bass] [Lead] [Pad] [Pluck] [Acid]
Keys: [Rhodes] [Organ] [Wurli] [Clav]
Genre: [Funk] [Disco] [Strings] [Brass] [Stab] [Sub]
Ambient: [Shimmer] [Jangle] [Dream] [Bell]
Add new categories for implemented synths:
Add Track:
Drums: [Kick] [Snare] [Hi-Hat] ...
Bass: [Bass] [Sub Bass]
Samples: [Lead] [Pluck] ...
FX: [Zap] [Noise]
Synth: ← Existing (Web Audio)
Core: [Bass] [Lead] [Pad] [Pluck] [Acid]
Keys: [Rhodes] [Organ] [Wurli] [Clav]
Genre: [Funk] [Disco] [Strings] [Brass] [Stab] [Sub]
Ambient: [Shimmer] [Jangle] [Dream] [Bell]
Advanced: ← NEW (Tone.js)
FM: [E-Piano] [FM Bass] [Bell]
Drum: [Membrane] [Tom] [Cymbal] [Hi-Hat]
Other: [Pluck] [Duo Lead]
Dual-Osc: ← NEW (Advanced Engine)
Leads: [Supersaw] [Thick] [Vibrato]
Bass: [Sub] [Wobble] [Acid]
Pads: [Warm] [Tremolo]
// app/src/components/sample-constants.ts
// NEW: Tone.js synth categories
export const TONE_SYNTH_CATEGORIES = {
fm: ['tone:fm-epiano', 'tone:fm-bass', 'tone:fm-bell'],
drum: ['tone:membrane-kick', 'tone:membrane-tom', 'tone:metal-cymbal', 'tone:metal-hihat'],
other: ['tone:pluck-string', 'tone:duo-lead', 'tone:am-bell', 'tone:am-tremolo'],
} as const;
export const TONE_SYNTH_NAMES: Record<string, string> = {
'tone:fm-epiano': 'E-Piano',
'tone:fm-bass': 'FM Bass',
'tone:fm-bell': 'Bell',
'tone:membrane-kick': 'Membrane',
'tone:membrane-tom': 'Tom',
'tone:metal-cymbal': 'Cymbal',
'tone:metal-hihat': 'Hi-Hat',
'tone:pluck-string': 'Pluck',
'tone:duo-lead': 'Duo Lead',
'tone:am-bell': 'AM Bell',
'tone:am-tremolo': 'Tremolo',
};
// NEW: Advanced synth categories
export const ADVANCED_SYNTH_CATEGORIES = {
leads: ['advanced:supersaw', 'advanced:thick-lead', 'advanced:vibrato-lead'],
bass: ['advanced:sub-bass', 'advanced:wobble-bass', 'advanced:acid-bass'],
pads: ['advanced:warm-pad', 'advanced:tremolo-strings'],
} as const;
export const ADVANCED_SYNTH_NAMES: Record<string, string> = {
'advanced:supersaw': 'Supersaw',
'advanced:thick-lead': 'Thick',
'advanced:vibrato-lead': 'Vibrato',
'advanced:sub-bass': 'Sub',
'advanced:wobble-bass': 'Wobble',
'advanced:acid-bass': 'Acid',
'advanced:warm-pad': 'Warm',
'advanced:tremolo-strings': 'Tremolo',
};Before marking any feature "done", verify all three surfaces:
| Surface | Requirement | Status |
|---|---|---|
| API | audioEngine.setEffects(state) |
✅ Implemented |
| State | SessionState.effects: EffectsState |
✅ Implemented |
| UI | EffectsPanel with sliders | ❌ NOT IMPLEMENTED |
| Surface | Requirement | Status |
|---|---|---|
| API | audioEngine.playToneSynth(preset, ...) |
✅ Implemented |
| State | Track.sampleId = "tone:fm-epiano" |
✅ Works |
| UI | Presets in SamplePicker | ❌ NOT IMPLEMENTED |
| Surface | Requirement | Status |
|---|---|---|
| API | advancedSynthEngine.playNote(preset, ...) |
✅ Implemented |
| State | Track.sampleId = "advanced:supersaw" |
✅ Works |
| UI | Presets in SamplePicker | ❌ NOT IMPLEMENTED |
Based on user value and implementation complexity:
| Priority | Feature | Effort | User Impact |
|---|---|---|---|
| P0 | Add synths to SamplePicker | 1 hour | High - unlocks 19 new sounds |
| P1 | Basic effects panel (wet sliders) | 2-3 hours | High - users can add reverb/delay |
| P2 | Full effects panel (all params) | 2-3 hours | Medium - power users |
| P3 | Sampled instruments | 1-2 days | Medium - piano, strings |
| P4 | XY Pad / Macros | 2-3 days | Low - advanced feature |
Effects panel must work on mobile using existing BottomSheet pattern:
Desktop (>768px): Inline panel below transport
┌──────────────────────────────────────────────┐
│ [▶] BPM [===120] Swing [====30%] [FX] │
├──────────────────────────────────────────────┤
│ Effects [×] │
├──────────────────────────────────────────────┤
│ REVERB [======○] 30% Decay [===○] 2.0s │
│ DELAY [===○====] 20% Time [8n▼] Fb[===] │
│ CHORUS [○=======] 0% Rate [===] Dp[===] │
│ DISTORT [○=======] 0% Drive [=========] │
└──────────────────────────────────────────────┘
Mobile (<768px): Full-width bottom sheet (stacked vertically)
┌──────────────────────┐
│ [▶] BPM Swing [FX] │
└──────────────────────┘
↓ (tap FX)
┌──────────────────────┐
│ Effects [×] │
├──────────────────────┤
│ REVERB │
│ Wet [==========○]30% │
│ Decay [========○]2.0s│
├──────────────────────┤
│ DELAY │
│ Wet [====○======]20% │
│ Time [8n ▼] │
│ Feedback [=====○]0.3 │
├──────────────────────┤
│ CHORUS │
│ Wet [○===========]0% │
│ Rate [=======○]1.5Hz │
│ Depth [======○] 0.5 │
├──────────────────────┤
│ DISTORT │
│ Wet [○===========]0% │
│ Drive [=======○]0.4 │
└──────────────────────┘
(Full-width bottom sheet, max-height: 80vh)
Following existing codebase patterns (UI-PHILOSOPHY.md + index.css):
/* Match existing TrackRow slider pattern */
.effect-slider {
width: 80-120px; /* Responsive */
height: 4px; /* Track height */
background: #444; /* Dark surface */
border-radius: 2px;
}
.effect-slider::-webkit-slider-thumb {
width: 14px; /* Match existing */
height: 14px;
border-radius: 50%;
background: var(--effect-color);
transition: transform 0.1s;
}
.effect-slider::-webkit-slider-thumb:hover {
transform: scale(1.15); /* Subtle feedback */
}Extend existing color language from index.css:
| Effect | Color | Rationale |
|---|---|---|
| Wet (all) | #e85a30 (accent) |
Primary action color |
| Reverb Decay | #4a9ece (info) |
Space/depth = blue |
| Delay Feedback | #d4a054 (secondary) |
Time-based = gold |
| Chorus Rate/Depth | #9b59b6 (purple) |
Modulation = purple |
| Distortion Drive | #e74c3c |
Destructive/heat = red |
- Effect labels: 10px, uppercase, letter-spacing 0.5px, color #666
- Values: System mono or inherit, 14px, color #fff
- Match existing Transport bar label styling
On published (immutable) sessions:
- Effects panel is visible (shows current settings)
- All controls are disabled (greyed out, opacity 0.5)
- Cursor shows
not-allowedon hover - Tooltip: "Remix to modify"
This aligns with existing published session behavior for steps/tracks.
This section documents how implementing this spec increases the range of music that can be created with Keyboardia.
| Dimension | Before Phase 25 | After Phase 25 | Expansion |
|---|---|---|---|
| Sound Sources | 35 | 62 | +77% |
| Synthesis Types | 1 (subtractive) | 4 (subtractive, FM, AM, drum) | +300% |
| Effects | 0 | 4 (reverb, delay, chorus, distortion) | ∞ |
| Genre Coverage | Limited | Comprehensive | See below |
| Genre | Key Sound | Before | After |
|---|---|---|---|
| House/Techno | 808 kick + stab | Partial | ✅ Full (membrane-kick, stab) |
| Trance/EDM | Supersaw lead | ❌ | ✅ (advanced:supersaw) |
| Dubstep | Wobble bass | ❌ | ✅ (advanced:wobble-bass + LFO) |
| Acid House | TB-303 bassline | ✅ Partial | ✅ Full (advanced:acid-bass) |
| Ambient | Reverb + pad | ❌ No reverb | ✅ (warm-pad + reverb) |
| Lo-Fi Hip Hop | Dusty keys + delay | ❌ | ✅ (fm-epiano + delay + chorus) |
| Jazz/Neo-Soul | Rhodes + warmth | ✅ Rhodes only | ✅ (fm-epiano + chorus) |
| Drum & Bass | Fast drums + sub | ✅ Partial | ✅ Full (sub-bass + metal-hihat) |
| Synthwave | Lush pads + delay | ❌ | ✅ (warm-pad + delay + reverb) |
| Industrial | Distorted drums | ❌ | ✅ (any drum + distortion) |
| Capability | Before | After |
|---|---|---|
| Detuned unison (supersaw) | ❌ | ✅ Dual oscillator |
| Filter modulation | ❌ | ✅ Filter envelope + LFO |
| Vibrato/tremolo | ❌ | ✅ LFO → pitch/amplitude |
| Rhythmic delay | ❌ | ✅ Tempo-synced delay (8n, 4n, etc.) |
| Spatial depth | ❌ | ✅ Reverb with decay control |
| Stereo width | ❌ | ✅ Chorus with depth |
| Harmonic distortion | ❌ | ✅ Waveshaping distortion |
| FM bells/keys | ❌ | ✅ FMSynth presets |
| Synthesized drums | ❌ | ✅ MembraneSynth, MetalSynth |
These sessions demonstrate the expanded musical capabilities and can serve as verification tests and showcase templates.
Musical Goal: Ethereal, floating ambient piece
const ambientDreamscape = {
tempo: 70,
swing: 0,
effects: {
reverb: { decay: 8, wet: 0.7 }, // Long, spacious reverb
delay: { time: "4n", feedback: 0.4, wet: 0.3 }, // Rhythmic echo
chorus: { frequency: 0.3, depth: 0.8, wet: 0.4 }, // Lush width
distortion: { amount: 0, wet: 0 }, // Clean
},
tracks: [
{ sampleId: "advanced:warm-pad", steps: [1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0], stepCount: 16, transpose: 0 },
{ sampleId: "advanced:warm-pad", steps: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], stepCount: 16, transpose: 7 }, // Fifth
{ sampleId: "tone:fm-bell", steps: [0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0], stepCount: 16, transpose: 12 },
{ sampleId: "advanced:tremolo-strings", steps: [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], stepCount: 32, transpose: -12 },
],
};Verification:
- Reverb creates sense of space (decay 8s audible)
- Delay echoes are tempo-synced (quarter notes at 70 BPM)
- Chorus adds stereo width (audible with headphones)
- Pads sustain smoothly across bar lines
Musical Goal: Hard-hitting acid techno
const acidWarehouse = {
tempo: 138,
swing: 10,
effects: {
reverb: { decay: 1.5, wet: 0.15 }, // Tight room
delay: { time: "16n", feedback: 0.2, wet: 0.2 }, // Slapback
chorus: { frequency: 0, depth: 0, wet: 0 }, // Off
distortion: { amount: 0.6, wet: 0.5 }, // Gritty
},
tracks: [
{ sampleId: "tone:membrane-kick", steps: [1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0], stepCount: 16 },
{ sampleId: "tone:metal-hihat", steps: [0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0], stepCount: 16 },
{ sampleId: "advanced:acid-bass", steps: [1,0,1,0,0,1,1,0,1,0,1,0,0,1,0,1], stepCount: 16, transpose: -12 },
{ sampleId: "synth:stab", steps: [0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0], stepCount: 16, transpose: 0 },
],
};Verification:
- Distortion adds grit to all sounds (especially 303 bass)
- Kick punches through (membrane synthesis)
- 303 resonance is audible on pitch slides (use p-locks)
- Overall mix is cohesive, not muddy
Musical Goal: Nostalgic 80s-inspired synthwave
const sunsetChillwave = {
tempo: 95,
swing: 15,
effects: {
reverb: { decay: 3, wet: 0.35 },
delay: { time: "8n", feedback: 0.5, wet: 0.4 }, // Prominent
chorus: { frequency: 1.2, depth: 0.7, wet: 0.5 }, // Lush
distortion: { amount: 0.1, wet: 0.1 }, // Subtle warmth
},
tracks: [
{ sampleId: "kick", steps: [1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0], stepCount: 16 },
{ sampleId: "snare", steps: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], stepCount: 16 },
{ sampleId: "tone:fm-epiano", steps: [1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0], stepCount: 16, transpose: 0 },
{ sampleId: "advanced:supersaw", steps: [0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0], stepCount: 16, transpose: 7 },
{ sampleId: "advanced:sub-bass", steps: [1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0], stepCount: 16, transpose: -12 },
],
};Verification:
- FM e-piano sounds authentic (DX7-style)
- Delay creates rhythmic interest on e-piano chords
- Supersaw lead is wide and detuned
- Sub bass provides foundation without mud
Musical Goal: Heavy dubstep with LFO-modulated bass
const dubstepDrop = {
tempo: 140,
swing: 0,
effects: {
reverb: { decay: 0.8, wet: 0.1 }, // Tight
delay: { time: "8t", feedback: 0.3, wet: 0.2 }, // Triplet feel
chorus: { frequency: 0, depth: 0, wet: 0 },
distortion: { amount: 0.4, wet: 0.35 },
},
tracks: [
{ sampleId: "kick", steps: [1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0], stepCount: 16 },
{ sampleId: "snare", steps: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], stepCount: 16 },
{ sampleId: "advanced:wobble-bass", steps: [1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,0], stepCount: 16, transpose: -12 },
{ sampleId: "tone:metal-cymbal", steps: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0], stepCount: 16 },
],
};Verification:
- Wobble bass LFO is audible and rhythmic
- Distortion adds weight without destroying low end
- Cymbal crash is metallic (MetalSynth)
- Half-time feel is correct
Musical Goal: Hypnotic, evolving minimal techno
const minimalTechno = {
tempo: 125,
swing: 5,
effects: {
reverb: { decay: 2.5, wet: 0.25 },
delay: { time: "8n", feedback: 0.55, wet: 0.3 },
chorus: { frequency: 0.8, depth: 0.3, wet: 0.15 },
distortion: { amount: 0.15, wet: 0.15 },
},
tracks: [
{
sampleId: "tone:membrane-kick",
steps: [1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0],
stepCount: 16,
parameterLocks: {
0: { pitch: 0 },
4: { pitch: -2 }, // Pitched down kick
8: { pitch: 0 },
12: { pitch: 3 }, // Pitched up kick
}
},
{ sampleId: "tone:metal-hihat", steps: [0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,1], stepCount: 16 },
{
sampleId: "synth:acid",
steps: [1,0,1,0,0,1,0,0,1,0,0,1,0,1,0,0],
stepCount: 16,
parameterLocks: {
0: { pitch: 0 },
2: { pitch: 12 }, // Octave jump
5: { pitch: 7 }, // Fifth
8: { pitch: 0 },
11: { pitch: 5 }, // Fourth
13: { pitch: -5 },
}
},
],
};Verification:
- Kick pitch variation creates movement
- Acid bass p-locks create melodic sequence
- Effects stack creates cohesive "glue"
- Delay feedback creates hypnotic trails