refactor: restructure to monorepo with npm workspaces (Phase 0)
Move frontend to packages/client/, server to packages/server/.
Root package.json uses npm workspaces to orchestrate both.
Structure:
reaktor/
packages/client/ (React + Vite + Tone.js frontend)
packages/server/ (static file server, future API)
dist/ (built output, shared)
docker-compose.yml (app + PostgreSQL for future backend)
- npm run dev → runs Vite dev server from client workspace
- npm run build → builds client, outputs to root dist/
- npm run start → runs server.js serving dist/
- Dockerfile updated for multi-stage monorepo build
- docker-compose.yml added with PostgreSQL service (ready for Phase 1)
- All imports and paths preserved, zero functionality change
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
511
packages/client/src/game/levels/world3.js
Normal file
511
packages/client/src/game/levels/world3.js
Normal file
@@ -0,0 +1,511 @@
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user