/** * World 3 — "Envelopes" (ADSR) * * Teaches: attack, decay, sustain, release, VCA, amplitude shaping, sound design * 8 levels, progressive difficulty */ export const WORLD_3 = { id: 'w3', name: 'Envelopes', subtitle: 'Dale forma al sonido en el tiempo', icon: '⏤╲', color: '#aa55ff', unlockStars: 24, // Need 24 stars from World 1+2 to unlock levels: [ // ─────────────── LEVEL 3.1 ─────────────── { id: 'w3-1', title: 'El VCA', subtitle: 'Control de volumen', description: 'Un VCA (Voltage Controlled Amplifier) es un amplificador cuyo volumen se puede controlar con una señal externa. Pasa el oscilador por un VCA para poder controlar su volumen.', concept: 'Conecta: Oscilador → VCA (input "In") → Output. El knob "Gain" del VCA controla cuánto deja pasar. Es como un grifo para el sonido.', availableModules: [], preplacedModules: [ { id: 1, type: 'oscillator', x: 80, y: 80, params: { waveform: 'sawtooth', frequency: 220, detune: 0 }, locked: true }, { id: 2, type: 'vca', x: 340, y: 80, params: { gain: 0.5 }, locked: false }, { id: 3, type: 'output', x: 580, y: 100, params: { volume: -6 }, locked: true }, ], target: { build: [ { type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220 } }, ], duration: 2, }, checks: [ { star: 1, name: 'VCA conectado', desc: 'Conecta oscilador → VCA → salida', test: (mods, conns) => { const osc = mods.find(m => m.type === 'oscillator'); const vca = mods.find(m => m.type === 'vca'); const out = mods.find(m => m.type === 'output'); if (!osc || !vca || !out) return false; return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === vca.id && c.to.port === 'in') && conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id); }, }, { star: 2, name: 'Volumen moderado', desc: 'Gain del VCA por debajo de 0.7', test: (mods) => { const vca = mods.find(m => m.type === 'vca'); return vca && (vca.params.gain ?? 0.8) < 0.7; }, }, { star: 3, name: 'Medio volumen', desc: 'Gain cercano a 0.5 (±0.1)', test: (mods) => { const vca = mods.find(m => m.type === 'vca'); return vca && Math.abs((vca.params.gain ?? 0.8) - 0.5) <= 0.1; }, }, ], }, // ─────────────── LEVEL 3.2 ─────────────── { id: 'w3-2', title: 'ADSR', subtitle: 'Las 4 fases del sonido', description: 'Todo sonido tiene una forma en el tiempo: el Attack (subida), Decay (bajada), Sustain (mantenimiento) y Release (apagado). Un Envelope genera esa curva ADSR que puedes usar para controlar el VCA.', concept: 'Conecta el Envelope al VCA: la salida del Envelope → entrada CV del VCA. Conecta el Keyboard al Gate del Envelope para que se dispare al tocar. Toca notas y escucha cómo el Envelope da forma al volumen.', availableModules: ['envelope', 'keyboard'], preplacedModules: [ { id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'square', frequency: 440, detune: 0 }, locked: true }, { id: 2, type: 'vca', x: 400, y: 60, params: { gain: 0 }, locked: false }, { id: 3, type: 'output', x: 620, y: 80, params: { volume: -6 }, locked: true }, ], target: { build: [ { type: 'oscillator', params: { waveform: 'square', frequency: 440 } }, ], envelope: { attack: 0.2, decay: 0.15, sustain: 0.6, release: 0.5 }, duration: 2, }, checks: [ { star: 1, name: 'Cadena con VCA', desc: 'Oscilador → VCA → Salida', test: (mods, conns) => { const osc = mods.find(m => m.type === 'oscillator'); const vca = mods.find(m => m.type === 'vca'); const out = mods.find(m => m.type === 'output'); if (!osc || !vca || !out) return false; return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === vca.id) && conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id); }, }, { star: 2, name: 'Envelope al VCA', desc: 'Conecta Envelope → VCA (CV) y Keyboard → Envelope (Gate)', test: (mods, conns) => { const env = mods.find(m => m.type === 'envelope'); const vca = mods.find(m => m.type === 'vca'); const kb = mods.find(m => m.type === 'keyboard'); if (!env || !vca || !kb) return false; return conns.some(c => c.from.moduleId === env.id && c.to.moduleId === vca.id && c.to.port === 'cv') && conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === env.id && c.to.port === 'gate'); }, }, { star: 3, name: 'Keyboard controla frecuencia', desc: 'Keyboard → Osc (Freq) para tocar melodías', test: (mods, conns) => { const kb = mods.find(m => m.type === 'keyboard'); const osc = mods.find(m => m.type === 'oscillator'); if (!kb || !osc) return false; return conns.some(c => c.from.moduleId === kb.id && c.from.port === 'freq' && c.to.moduleId === osc.id && c.to.port === 'freq'); }, }, ], }, // ─────────────── LEVEL 3.3 ─────────────── { id: 'w3-3', title: 'Percusión', subtitle: 'Attack rápido, decay corto', description: 'Los sonidos percusivos tienen un attack instantáneo y un decay corto sin sustain. Piensa en un tambor, un clic, un bleep — el sonido aparece de golpe y muere rápido. Configura un envelope percusivo.', concept: 'Attack muy bajo (~0.001s), Decay corto (~0.1-0.2s), Sustain a 0, Release corto. Esto crea un "blip" percusivo. Perfecto para hi-hats, kicks sintéticos, y bleeps 8-bit.', availableModules: ['envelope', 'keyboard'], preplacedModules: [ { id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'sine', frequency: 440, detune: 0 }, locked: true }, { id: 2, type: 'vca', x: 400, y: 60, params: { gain: 0 }, locked: false }, { id: 3, type: 'output', x: 620, y: 80, params: { volume: -6 }, locked: true }, ], target: { build: [ { type: 'oscillator', params: { waveform: 'sine', frequency: 440 } }, ], envelope: { attack: 0.005, decay: 0.15, sustain: 0, release: 0.1 }, duration: 2, }, checks: [ { star: 1, name: 'Señal con envelope', desc: 'Osc → VCA → Out, con Envelope al CV y Keyboard al Gate', test: (mods, conns) => { const osc = mods.find(m => m.type === 'oscillator'); const vca = mods.find(m => m.type === 'vca'); const env = mods.find(m => m.type === 'envelope'); const kb = mods.find(m => m.type === 'keyboard'); const out = mods.find(m => m.type === 'output'); if (!osc || !vca || !env || !kb || !out) return false; return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === vca.id) && conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id) && conns.some(c => c.from.moduleId === env.id && c.to.moduleId === vca.id && c.to.port === 'cv') && conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === env.id); }, }, { star: 2, name: 'Sin sustain', desc: 'Sustain a 0 (o casi)', test: (mods) => { const env = mods.find(m => m.type === 'envelope'); return env && (env.params.sustain ?? 0.5) < 0.05; }, }, { star: 3, name: 'Blip perfecto', desc: 'Attack <0.01s, Decay 0.05-0.3s, Sustain ~0', test: (mods) => { const env = mods.find(m => m.type === 'envelope'); if (!env) return false; return (env.params.attack ?? 0.01) < 0.015 && (env.params.decay ?? 0.2) >= 0.05 && (env.params.decay ?? 0.2) <= 0.3 && (env.params.sustain ?? 0.5) < 0.05; }, }, ], }, // ─────────────── LEVEL 3.4 ─────────────── { id: 'w3-4', title: 'Pad Atmosférico', subtitle: 'Suave y envolvente', description: 'Los pads son sonidos largos y suaves que rellenan el fondo de una mezcla. Se consiguen con un attack lento (el sonido entra gradualmente), sustain alto, y release largo (se desvanece lentamente).', concept: 'Attack lento (~1-2s), Decay corto (~0.3s), Sustain alto (~0.7-0.9), Release largo (~2-4s). El sonido "respira" — entra suave y se queda flotando.', availableModules: ['envelope', 'keyboard'], preplacedModules: [ { id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'sawtooth', frequency: 220, detune: 0 }, locked: true }, { id: 2, type: 'vca', x: 400, y: 60, params: { gain: 0 }, locked: false }, { id: 3, type: 'output', x: 620, y: 80, params: { volume: -6 }, locked: true }, ], target: { build: [ { type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220 } }, ], envelope: { attack: 1.2, decay: 0.3, sustain: 0.75, release: 2.5 }, duration: 2, }, checks: [ { star: 1, name: 'Señal con envelope', desc: 'Osc → VCA → Out, Envelope al CV, Keyboard al Gate', test: (mods, conns) => { const env = mods.find(m => m.type === 'envelope'); const vca = mods.find(m => m.type === 'vca'); const kb = mods.find(m => m.type === 'keyboard'); if (!env || !vca || !kb) return false; return conns.some(c => c.from.moduleId === env.id && c.to.moduleId === vca.id && c.to.port === 'cv') && conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === env.id); }, }, { star: 2, name: 'Attack lento', desc: 'Attack mayor de 0.5 segundos', test: (mods) => { const env = mods.find(m => m.type === 'envelope'); return env && (env.params.attack ?? 0.01) > 0.5; }, }, { star: 3, name: 'Pad perfecto', desc: 'Attack >0.8s, Sustain >0.6, Release >1.5s', test: (mods) => { const env = mods.find(m => m.type === 'envelope'); if (!env) return false; return (env.params.attack ?? 0.01) > 0.8 && (env.params.sustain ?? 0.5) > 0.6 && (env.params.release ?? 0.5) > 1.5; }, }, ], }, // ─────────────── LEVEL 3.5 ─────────────── { id: 'w3-5', title: 'Pluck', subtitle: 'Cuerdas pulsadas', description: 'El sonido de una cuerda pulsada (guitarra, arpa) tiene un attack rápido y un decay medio. No tiene sustain real — el sonido decrece naturalmente. El filtro ayuda a que suene más natural.', concept: 'Envelope con Attack rápido (~0.001s), Decay medio (~0.4-0.8s), Sustain bajo (~0.1), Release ~0.3s. Usa una onda triangle o saw con un filtro lowpass para suavizar.', availableModules: ['envelope', 'keyboard', 'filter'], preplacedModules: [ { id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'triangle', frequency: 440, detune: 0 }, locked: false }, { id: 2, type: 'vca', x: 500, y: 60, params: { gain: 0 }, locked: false }, { id: 3, type: 'output', x: 760, y: 80, params: { volume: -6 }, locked: true }, ], target: { build: [ { type: 'oscillator', params: { waveform: 'triangle', frequency: 440 } }, ], filter: { type: 'lowpass', frequency: 3000, Q: 2 }, envelope: { attack: 0.008, decay: 0.5, sustain: 0.05, release: 0.2 }, duration: 2, }, checks: [ { star: 1, name: 'Cadena completa', desc: 'Osc → (Filter →) VCA → Out con Envelope y Keyboard', test: (mods, conns) => { const env = mods.find(m => m.type === 'envelope'); const vca = mods.find(m => m.type === 'vca'); const kb = mods.find(m => m.type === 'keyboard'); const out = mods.find(m => m.type === 'output'); if (!env || !vca || !kb || !out) return false; return conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id) && conns.some(c => c.from.moduleId === env.id && c.to.port === 'cv') && conns.some(c => c.from.moduleId === kb.id && c.to.port === 'gate'); }, }, { star: 2, name: 'Forma pluck', desc: 'Attack rápido (<0.02s), Sustain bajo (<0.2)', test: (mods) => { const env = mods.find(m => m.type === 'envelope'); if (!env) return false; return (env.params.attack ?? 0.01) < 0.02 && (env.params.sustain ?? 0.5) < 0.2; }, }, { star: 3, name: 'Pluck natural', desc: 'Pluck shape + filtro lowpass en la cadena', test: (mods, conns) => { const env = mods.find(m => m.type === 'envelope'); const flt = mods.find(m => m.type === 'filter'); if (!env || !flt) return false; return (env.params.attack ?? 0.01) < 0.02 && (env.params.sustain ?? 0.5) < 0.2 && (env.params.decay ?? 0.2) >= 0.3 && flt.params.type === 'lowpass'; }, }, ], }, // ─────────────── LEVEL 3.6 ─────────────── { id: 'w3-6', title: 'Filtro Dinámico', subtitle: 'Envelope → Cutoff', description: 'Los envelopes no solo controlan volumen — ¡también pueden controlar el filtro! Conectar un envelope al cutoff crea sonidos que se "abren" y "cierran" con cada nota. Es la técnica más importante de síntesis sustractiva.', concept: 'Conecta un segundo Envelope a la entrada Cutoff del filtro. Keyboard → Gate de ambos envelopes. Un envelope controla volumen (VCA), otro controla brillo (filtro cutoff).', availableModules: ['envelope', 'keyboard', 'filter'], preplacedModules: [ { id: 1, type: 'oscillator', x: 60, y: 40, params: { waveform: 'sawtooth', frequency: 220, detune: 0 }, locked: true }, { id: 2, type: 'vca', x: 520, y: 40, params: { gain: 0 }, locked: false }, { id: 3, type: 'output', x: 760, y: 80, params: { volume: -6 }, locked: true }, ], target: { build: [ { type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220 } }, ], filter: { type: 'lowpass', frequency: 800, Q: 4 }, envelope: { attack: 0.01, decay: 0.3, sustain: 0.4, release: 0.2 }, duration: 2, }, checks: [ { star: 1, name: 'Doble envelope', desc: 'Dos envelopes: uno al VCA, otro al filtro cutoff', test: (mods, conns) => { const envs = mods.filter(m => m.type === 'envelope'); const vca = mods.find(m => m.type === 'vca'); const flt = mods.find(m => m.type === 'filter'); if (envs.length < 2 || !vca || !flt) return false; const envToVca = envs.some(e => conns.some(c => c.from.moduleId === e.id && c.to.moduleId === vca.id && c.to.port === 'cv')); const envToFlt = envs.some(e => conns.some(c => c.from.moduleId === e.id && c.to.moduleId === flt.id && c.to.port === 'cutoff')); return envToVca && envToFlt; }, }, { star: 2, name: 'Gates conectados', desc: 'Keyboard → Gate de ambos envelopes', test: (mods, conns) => { const kb = mods.find(m => m.type === 'keyboard'); const envs = mods.filter(m => m.type === 'envelope'); if (!kb || envs.length < 2) return false; const gatedEnvs = envs.filter(e => conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === e.id && c.to.port === 'gate') ); return gatedEnvs.length >= 2; }, }, { star: 3, name: 'Envelopes distintos', desc: 'Los dos envelopes tienen decays diferentes (>0.1s diferencia)', test: (mods) => { const envs = mods.filter(m => m.type === 'envelope'); if (envs.length < 2) return false; const decays = envs.map(e => e.params.decay ?? 0.2); return Math.abs(decays[0] - decays[1]) > 0.1; }, }, ], }, // ─────────────── LEVEL 3.7 ─────────────── { id: 'w3-7', title: 'Tremolo', subtitle: 'LFO → Volumen', description: 'El tremolo es una variación rítmica del volumen. Se consigue conectando un LFO a la ganancia del VCA. Es un efecto clásico de guitarras, órganos y sintetizadores vintage.', concept: 'Conecta un LFO a la entrada CV del VCA (no del filtro). Un LFO a ~4-8 Hz con amplitud moderada crea un tremolo clásico. Más lento (~1-2 Hz) suena como un "pulso".', availableModules: ['lfo'], preplacedModules: [ { id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'sine', frequency: 440, detune: 0 }, locked: true }, { id: 2, type: 'vca', x: 340, y: 60, params: { gain: 0.7 }, locked: false }, { id: 3, type: 'output', x: 580, y: 80, params: { volume: -6 }, locked: true }, ], target: { build: [ { type: 'oscillator', params: { waveform: 'sine', frequency: 440 } }, ], lfo: { frequency: 6, type: 'sine', min: 0.2, max: 1.0, target: 'amplitude' }, duration: 3, }, checks: [ { star: 1, name: 'Cadena básica', desc: 'Oscilador → VCA → Salida', test: (mods, conns) => { const osc = mods.find(m => m.type === 'oscillator'); const vca = mods.find(m => m.type === 'vca'); const out = mods.find(m => m.type === 'output'); if (!osc || !vca || !out) return false; return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === vca.id) && conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id); }, }, { star: 2, name: 'LFO al VCA', desc: 'Conecta LFO → VCA (CV)', test: (mods, conns) => { const lfo = mods.find(m => m.type === 'lfo'); const vca = mods.find(m => m.type === 'vca'); if (!lfo || !vca) return false; return conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === vca.id && c.to.port === 'cv'); }, }, { star: 3, name: 'Tremolo rítmico', desc: 'LFO entre 3-10 Hz (tremolo audible)', test: (mods) => { const lfo = mods.find(m => m.type === 'lfo'); if (!lfo) return false; const rate = lfo.params.frequency ?? 2; return rate >= 3 && rate <= 10; }, }, ], }, // ─────────────── LEVEL 3.8: BOSS ─────────────── { id: 'w3-8', title: 'Synth Lead Completo', subtitle: 'BOSS: Ponlo todo junto', description: 'Es hora de construir un sonido de lead completo desde cero. Combina todo lo que has aprendido: oscilador, filtro con envelope, VCA con envelope, y keyboard para tocar. Es el patch clásico de síntesis sustractiva.', concept: 'Keyboard → Osc (freq) + Env1 (gate) + Env2 (gate). Osc → Filter → VCA → Output. Env1 → Filter cutoff (decay medio para "apertura"). Env2 → VCA cv (sustain para mantener). Ajusta para un lead expresivo.', availableModules: ['oscillator', 'filter', 'vca', 'envelope', 'keyboard'], preplacedModules: [ { id: 1, type: 'output', x: 800, y: 120, params: { volume: -6 }, locked: true }, ], target: { build: [ { type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220 } }, ], filter: { type: 'lowpass', frequency: 2000, Q: 6 }, envelope: { attack: 0.05, decay: 0.3, sustain: 0.5, release: 0.6 }, duration: 3, }, checks: [ { star: 1, name: 'Cadena sustractiva', desc: 'Osc → Filter → VCA → Output', test: (mods, conns) => { const osc = mods.find(m => m.type === 'oscillator'); const flt = mods.find(m => m.type === 'filter'); const vca = mods.find(m => m.type === 'vca'); const out = mods.find(m => m.type === 'output'); if (!osc || !flt || !vca || !out) return false; return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === flt.id) && conns.some(c => c.from.moduleId === flt.id && c.to.moduleId === vca.id) && conns.some(c => c.from.moduleId === vca.id && c.to.moduleId === out.id); }, }, { star: 2, name: 'Doble modulación', desc: 'Envelope al filtro cutoff Y envelope al VCA cv', test: (mods, conns) => { const envs = mods.filter(m => m.type === 'envelope'); const flt = mods.find(m => m.type === 'filter'); const vca = mods.find(m => m.type === 'vca'); if (envs.length < 2 || !flt || !vca) return false; const envToFlt = envs.some(e => conns.some(c => c.from.moduleId === e.id && c.to.moduleId === flt.id && c.to.port === 'cutoff')); const envToVca = envs.some(e => conns.some(c => c.from.moduleId === e.id && c.to.moduleId === vca.id && c.to.port === 'cv')); return envToFlt && envToVca; }, }, { star: 3, name: 'Lead expresivo', desc: 'Keyboard controla freq + gates, envelopes distintos', test: (mods, conns) => { const kb = mods.find(m => m.type === 'keyboard'); const osc = mods.find(m => m.type === 'oscillator'); const envs = mods.filter(m => m.type === 'envelope'); if (!kb || !osc || envs.length < 2) return false; // KB → osc freq const kbFreq = conns.some(c => c.from.moduleId === kb.id && c.from.port === 'freq' && c.to.moduleId === osc.id); // KB → both env gates const gated = envs.filter(e => conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === e.id && c.to.port === 'gate') ); // Envelopes have different settings const decays = envs.map(e => e.params.decay ?? 0.2); const diffDecay = Math.abs(decays[0] - decays[1]) > 0.05; return kbFreq && gated.length >= 2 && diffDecay; }, }, ], }, ], };