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>
577 lines
27 KiB
JavaScript
577 lines
27 KiB
JavaScript
/**
|
|
* World 10 — "Espacio y Stereo" (Space and Stereo)
|
|
*
|
|
* Teaches: Stereo imaging, spatial effects, delay for width, reverb placement
|
|
* 8 levels, boss challenges with complete stereo mix
|
|
*/
|
|
|
|
export const WORLD_10 = {
|
|
id: 'w10',
|
|
name: 'Espacio y Stereo',
|
|
subtitle: 'Profundidad y dimensión',
|
|
icon: '◉◉',
|
|
color: '#44ddaa',
|
|
unlockStars: 108,
|
|
levels: [
|
|
// ─────────────── LEVEL 10.1 ───────────────
|
|
{
|
|
id: 'w10-1',
|
|
title: 'Pan Left-Right',
|
|
subtitle: 'Los canales estéreo básicos',
|
|
description: 'La estéreo más simple: coloca una fuente en el canal izquierdo y otra en el derecho. El output tiene dos entradas: "left" y "right". Conecta diferentes osciladores a cada uno.',
|
|
concept: 'Osc 1 → Output (left). Osc 2 → Output (right). El output tiene dos canales separados. Juntos crean la ilusión de width — como si el sonido viniera de dos lugares diferentes.',
|
|
availableModules: ['oscillator', 'vca', 'envelope', 'keyboard', 'mixer'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 800, y: 140, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sine', frequency: 220, detune: 0 } },
|
|
{ type: 'oscillator', params: { waveform: 'sine', frequency: 330, detune: 0 } },
|
|
],
|
|
envelope: { attack: 0.05, decay: 0.3, sustain: 0.4, release: 0.2 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Estéreo básica',
|
|
desc: 'Dos osciladores, uno al left, uno al right',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (oscs.length < 2 || !out) return false;
|
|
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
|
|
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
|
|
return leftConn && rightConn;
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Estéreo con VCA',
|
|
desc: 'Cada oscilador con su VCA antes de output',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const vcas = mods.filter(m => m.type === 'vca');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (oscs.length < 2 || vcas.length < 2 || !out) return false;
|
|
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
|
|
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
|
|
return leftConn && rightConn &&
|
|
oscs.some(o => conns.some(c => c.from.moduleId === o.id && c.to.moduleId === vcas[0].id));
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Estéreo Controlada',
|
|
desc: 'Oscs left/right con envelopes separados gateados por keyboard',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const vcas = mods.filter(m => m.type === 'vca');
|
|
const envs = mods.filter(m => m.type === 'envelope');
|
|
const kb = mods.find(m => m.type === 'keyboard');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (oscs.length < 2 || vcas.length < 2 || envs.length < 2 || !kb || !out) return false;
|
|
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
|
|
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
|
|
const gated = envs.filter(e =>
|
|
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === e.id && c.to.port === 'gate')
|
|
);
|
|
return leftConn && rightConn && gated.length >= 2;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 10.2 ───────────────
|
|
{
|
|
id: 'w10-2',
|
|
title: 'Stereo Detune',
|
|
subtitle: 'Ancho con osciladores diferentes',
|
|
description: 'Coloca el mismo oscilador en ambos canales pero detuned: izquierda a la frecuencia exacta, derecha con un pequeño detune (+5 a +15 cents). Crea un "chorus" natural que te envuelve.',
|
|
concept: 'Osc 1 (detune 0) → Left. Osc 2 (detune +7) a misma nota → Right. Cuando están cerca pero no iguales, el beating crea width. Es como tener dos cantantes cantando casi al unísono.',
|
|
availableModules: ['oscillator', 'vca', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 800, y: 140, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sine', frequency: 220, detune: 0 } },
|
|
{ type: 'oscillator', params: { waveform: 'sine', frequency: 220, detune: 8 } },
|
|
],
|
|
envelope: { attack: 0.06, decay: 0.35, sustain: 0.35, release: 0.25 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Dos osciladores detuned',
|
|
desc: 'Oscs a misma frecuencia pero con detune diferente',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (oscs.length < 2 || !out) return false;
|
|
const detunes = oscs.map(o => o.params.detune ?? 0);
|
|
const freqs = oscs.map(o => o.params.frequency ?? 440);
|
|
const sameFreq = Math.abs(freqs[0] - freqs[1]) < 10;
|
|
const differentDetune = Math.abs(detunes[0] - detunes[1]) > 3;
|
|
return sameFreq && differentDetune;
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Stereo width audible',
|
|
desc: 'Detune entre oscs > 5 cents para efecto chorus',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
if (oscs.length < 2) return false;
|
|
const detunes = oscs.map(o => o.params.detune ?? 0);
|
|
return Math.abs(detunes[0] - detunes[1]) > 5;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Chorus Estéreo',
|
|
desc: 'Detuned oscs left/right con VCAs y envelopes',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const vcas = mods.filter(m => m.type === 'vca');
|
|
const envs = mods.filter(m => m.type === 'envelope');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (oscs.length < 2 || vcas.length < 2 || envs.length < 1 || !out) return false;
|
|
const detunes = oscs.map(o => o.params.detune ?? 0);
|
|
const freqs = oscs.map(o => o.params.frequency ?? 440);
|
|
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
|
|
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
|
|
return Math.abs(freqs[0] - freqs[1]) < 10 &&
|
|
Math.abs(detunes[0] - detunes[1]) > 5 &&
|
|
leftConn && rightConn;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 10.3 ───────────────
|
|
{
|
|
id: 'w10-3',
|
|
title: 'Delay para Ancho',
|
|
subtitle: 'La profundidad del eco',
|
|
description: 'El delay es uno de los mejores trucos para width: copia la señal, la envía al otro canal con un pequeño delay (20-80ms). El cerebro interpreta esto como "la misma fuente reflejada en espacio".',
|
|
concept: 'Osc → Left (seco). Osc → Delay (15-50ms) → Right. El delay crea la ilusión de distancia. Cuanto más delay, más separación. Mantén el feedback bajo para evitar caos.',
|
|
availableModules: ['oscillator', 'vca', 'delay', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 800, y: 140, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sine', frequency: 165, detune: 0 } },
|
|
],
|
|
effects: [
|
|
{ type: 'delay', delayTime: 0.035, feedback: 0.15, wet: 0.8 },
|
|
],
|
|
envelope: { attack: 0.08, decay: 0.4, sustain: 0.3, release: 0.3 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Delay en señal',
|
|
desc: 'Oscilador → Delay → Output',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const del = mods.find(m => m.type === 'delay');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!osc || !del || !out) return false;
|
|
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === del.id) &&
|
|
conns.some(c => c.from.moduleId === del.id && c.to.moduleId === out.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Delay corto',
|
|
desc: 'Delay con tiempo entre 20-80ms',
|
|
test: (mods) => {
|
|
const del = mods.find(m => m.type === 'delay');
|
|
if (!del) return false;
|
|
const time = del.params.delayTime ?? 0.3;
|
|
return time >= 0.02 && time <= 0.08;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Delay Estéreo',
|
|
desc: 'Osc left + Osc/Delay right con envelopes',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const del = mods.find(m => m.type === 'delay');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (oscs.length < 1 || !del || !out) return false;
|
|
const time = del.params.delayTime ?? 0.3;
|
|
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
|
|
const rightConn = conns.some(c => c.from.moduleId === del.id && c.to.moduleId === out.id && c.to.port === 'right');
|
|
return time >= 0.015 && time <= 0.1 &&
|
|
(del.params.feedback ?? 0.4) < 0.5 &&
|
|
leftConn && rightConn;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 10.4 ───────────────
|
|
{
|
|
id: 'w10-4',
|
|
title: 'Reverb Corta',
|
|
subtitle: 'La sala pequeña',
|
|
description: 'Una reverb corta (decay 1-2s) simula una habitación pequeña. No es mucha cola, solo lo suficiente para darle "espacio" al sonido sin que desaparezca en la distancia. Perfecto para síntesis.',
|
|
concept: 'Osc → VCA → Reverb (decay 1-2s, wet 0.3-0.5) → Output. La reverb enturbia ligeramente el sonido y lo coloca "en una sala". Mantén wet bajo para que no sea un sonido amortiguado.',
|
|
availableModules: ['oscillator', 'vca', 'reverb', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 700, y: 120, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sine', frequency: 262, detune: 0 } },
|
|
],
|
|
effects: [
|
|
{ type: 'reverb', decay: 1.5, wet: 0.4 },
|
|
],
|
|
envelope: { attack: 0.07, decay: 0.4, sustain: 0.35, release: 0.25 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Reverb en la cadena',
|
|
desc: 'Osc → Reverb → Output',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!osc || !rev || !out) return false;
|
|
return conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === rev.id) &&
|
|
conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Decay corta',
|
|
desc: 'Reverb con decay entre 1-2 segundos',
|
|
test: (mods) => {
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
if (!rev) return false;
|
|
const decay = rev.params.decay ?? 3;
|
|
return decay >= 1 && decay <= 2;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Sala Perfecta',
|
|
desc: 'Reverb (decay 1-2s, wet 0.3-0.5) + envelope al VCA',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const vca = mods.find(m => m.type === 'vca');
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
const env = mods.find(m => m.type === 'envelope');
|
|
if (!osc || !vca || !rev || !env) return false;
|
|
const decay = rev.params.decay ?? 3;
|
|
const wet = rev.params.wet ?? 0.4;
|
|
return decay >= 1 && decay <= 2 &&
|
|
wet >= 0.25 && wet <= 0.6 &&
|
|
conns.some(c => c.from.moduleId === env.id && c.to.moduleId === vca.id && c.to.port === 'cv');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 10.5 ───────────────
|
|
{
|
|
id: 'w10-5',
|
|
title: 'Catedral Reverb',
|
|
subtitle: 'Los espacios enormes',
|
|
description: 'Una catedral reverb es lo opuesto: decay largo (3+ segundos), wet alto. El sonido se desvanece lentamente, como si estuvieras en una basílica gigante. Crea atmósfera épica.',
|
|
concept: 'Osc → VCA → Reverb (decay > 3s, wet > 0.5) → Output. El sonido se desmorona lentamente en el aire. Usa notas largas para aprovechar la cola reverb. ¡Es mágico!',
|
|
availableModules: ['oscillator', 'vca', 'reverb', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 700, y: 120, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sine', frequency: 196, detune: 0 } },
|
|
],
|
|
effects: [
|
|
{ type: 'reverb', decay: 4.2, wet: 0.65 },
|
|
],
|
|
envelope: { attack: 0.06, decay: 0.8, sustain: 0.4, release: 0.5 },
|
|
duration: 4,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Reverb larga',
|
|
desc: 'Reverb con decay > 3 segundos',
|
|
test: (mods) => {
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
if (!rev) return false;
|
|
return (rev.params.decay ?? 3) > 3;
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Reverb mojada',
|
|
desc: 'Reverb con wet > 0.5 para efecto dramático',
|
|
test: (mods) => {
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
if (!rev) return false;
|
|
return (rev.params.decay ?? 3) > 3 &&
|
|
(rev.params.wet ?? 0.4) > 0.5;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Catedral Épica',
|
|
desc: 'Reverb (decay > 4s, wet > 0.6) con envelope lento al VCA',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const vca = mods.find(m => m.type === 'vca');
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
const env = mods.find(m => m.type === 'envelope');
|
|
const kb = mods.find(m => m.type === 'keyboard');
|
|
if (!osc || !vca || !rev || !env || !kb) return false;
|
|
return (rev.params.decay ?? 3) > 4 &&
|
|
(rev.params.wet ?? 0.4) > 0.6 &&
|
|
(env.params.attack ?? 0.01) < 0.1 &&
|
|
(env.params.decay ?? 0.2) > 0.5 &&
|
|
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === env.id && c.to.port === 'gate');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 10.6 ───────────────
|
|
{
|
|
id: 'w10-6',
|
|
title: 'Slapback Echo',
|
|
subtitle: 'Doblado rítmico',
|
|
description: 'El slapback echo es un delay muy corto (100-200ms) sin feedback, que crea un efecto de "doblado" — como si hubiera una copia del sonido muy cerca. Popular en rockabilly y sintetizadores.',
|
|
concept: 'Osc → Left (seco). Osc → Delay (100-200ms, feedback bajo) → Right. El delay corto mantiene la segunda "voz" identificable pero cercana. Es como tener un doblante.',
|
|
availableModules: ['oscillator', 'vca', 'delay', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 800, y: 140, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'square', frequency: 220, detune: 0 } },
|
|
],
|
|
effects: [
|
|
{ type: 'delay', delayTime: 0.15, feedback: 0.08, wet: 0.75 },
|
|
],
|
|
envelope: { attack: 0.05, decay: 0.35, sustain: 0.4, release: 0.2 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Delay rítmico',
|
|
desc: 'Delay entre 80-250ms',
|
|
test: (mods) => {
|
|
const del = mods.find(m => m.type === 'delay');
|
|
if (!del) return false;
|
|
const time = del.params.delayTime ?? 0.3;
|
|
return time >= 0.08 && time <= 0.25;
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Sin feedback',
|
|
desc: 'Delay con feedback < 0.2 para no crear repeticiones caóticas',
|
|
test: (mods) => {
|
|
const del = mods.find(m => m.type === 'delay');
|
|
if (!del) return false;
|
|
const time = del.params.delayTime ?? 0.3;
|
|
const fb = del.params.feedback ?? 0.4;
|
|
return time >= 0.08 && time <= 0.25 && fb < 0.2;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Doblante Perfecto',
|
|
desc: 'Delay (100-200ms, feedback < 0.1) en stereo left/right',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const del = mods.find(m => m.type === 'delay');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!osc || !del || !out) return false;
|
|
const time = del.params.delayTime ?? 0.3;
|
|
const fb = del.params.feedback ?? 0.4;
|
|
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
|
|
const rightConn = conns.some(c => c.from.moduleId === del.id && c.to.moduleId === out.id && c.to.port === 'right');
|
|
return time >= 0.1 && time <= 0.2 &&
|
|
fb < 0.1 &&
|
|
leftConn && rightConn;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 10.7 ───────────────
|
|
{
|
|
id: 'w10-7',
|
|
title: 'Orden de Efectos',
|
|
subtitle: 'La cadena de procesamiento',
|
|
description: 'El orden de los efectos es crítico: ¿delay antes o después de reverb? ¿Filtro antes que distortion? Aquí aprendes a construir cadenas de efectos que suenen coherentes y profesionales.',
|
|
concept: 'Construye: Osc → Filter → Distortion → Delay → Reverb → Output. Cada efecto transforma el anterior. El filtro quita brillo, distortion añade armónicos, delay añade movimiento, reverb añade espacio.',
|
|
availableModules: ['oscillator', 'filter', 'distortion', 'delay', 'reverb', 'vca', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 900, y: 140, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 280, detune: 0 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 3500, Q: 1.5 },
|
|
effects: [
|
|
{ type: 'distortion', distortion: 0.45, wet: 0.5 },
|
|
{ type: 'delay', delayTime: 0.3, feedback: 0.35, wet: 0.55 },
|
|
{ type: 'reverb', decay: 2.2, wet: 0.45 },
|
|
],
|
|
envelope: { attack: 0.08, decay: 0.45, sustain: 0.25, release: 0.3 },
|
|
duration: 4,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Cadena básica',
|
|
desc: 'Osc → Filter → Delay → Reverb → Output',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const del = mods.find(m => m.type === 'delay');
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!osc || !flt || !del || !rev || !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 === del.id) &&
|
|
conns.some(c => c.from.moduleId === del.id && c.to.moduleId === rev.id) &&
|
|
conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Con distortion',
|
|
desc: 'Cadena con filtro + distortion + delay + reverb',
|
|
test: (mods, conns) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const dist = mods.find(m => m.type === 'distortion');
|
|
const del = mods.find(m => m.type === 'delay');
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
if (!flt || !dist || !del || !rev) return false;
|
|
return conns.some(c => c.from.moduleId === dist.id && c.to.moduleId === del.id) ||
|
|
conns.some(c => c.from.moduleId === dist.id && c.to.moduleId === rev.id);
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Cadena Profesional',
|
|
desc: 'Osc → Filter → Distortion → Delay → Reverb con envelope',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const dist = mods.find(m => m.type === 'distortion');
|
|
const del = mods.find(m => m.type === 'delay');
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
const env = mods.find(m => m.type === 'envelope');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!osc || !flt || !dist || !del || !rev || !env || !out) return false;
|
|
const fltOsc = conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === flt.id);
|
|
const distFlt = conns.some(c => c.from.moduleId === flt.id && c.to.moduleId === dist.id);
|
|
const delDist = conns.some(c => c.from.moduleId === dist.id && c.to.moduleId === del.id);
|
|
const revDel = conns.some(c => c.from.moduleId === del.id && c.to.moduleId === rev.id);
|
|
const outRev = conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
|
|
return fltOsc && distFlt && delDist && revDel && outRev;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 10.8: BOSS ───────────────
|
|
{
|
|
id: 'w10-8',
|
|
title: 'Mix Espacial',
|
|
subtitle: 'BOSS FINAL: Orquesta Estéreo',
|
|
description: 'Construye una mezcla estéreo completa con múltiples fuentes, cada una con su propia posición en el espacio. Usa delay, reverb, y pan para colocar cada instrumento. Crea una orquesta de sintetizadores.',
|
|
concept: 'Múltiples osciladores/fuentes, algunos en left/right, algunos con delay, algunos con reverb, todos controlados por keyboard/sequencer. La mezcla final debe sonar amplia, profunda, y multidimensional.',
|
|
availableModules: ['oscillator', 'filter', 'vca', 'mixer', 'lfo', 'envelope', 'keyboard', 'sequencer', 'delay', 'reverb', 'distortion'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 950, y: 160, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sine', frequency: 110, detune: 0 } },
|
|
{ type: 'oscillator', params: { waveform: 'sine', frequency: 110, detune: 10 } },
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220, detune: 0 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 4000, Q: 1.3 },
|
|
lfo: { frequency: 0.6, type: 'sine', min: 2000, max: 5000, target: 'frequency' },
|
|
effects: [
|
|
{ type: 'delay', delayTime: 0.25, feedback: 0.4, wet: 0.6 },
|
|
{ type: 'reverb', decay: 3, wet: 0.55 },
|
|
],
|
|
envelope: { attack: 0.1, decay: 0.5, sustain: 0.3, release: 0.4 },
|
|
duration: 5,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Mezcla funcional',
|
|
desc: 'Múltiples fuentes en left y right del output',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (oscs.length < 2 || !out) return false;
|
|
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
|
|
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
|
|
return leftConn && rightConn && conns.length >= 8;
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Con efectos espaciales',
|
|
desc: 'Delay y Reverb en la mezcla creando profundidad',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const del = mods.find(m => m.type === 'delay');
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (oscs.length < 2 || !del || !rev || !out) return false;
|
|
const delToOut = conns.some(c => c.from.moduleId === del.id && c.to.moduleId === out.id);
|
|
const revToOut = conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
|
|
return delToOut && revToOut;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Orquesta Completa',
|
|
desc: '3+ oscs, stereo pan, delay + reverb, filter, envelope, keyboard/sequencer',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const flts = mods.filter(m => m.type === 'filter');
|
|
const envs = mods.filter(m => m.type === 'envelope');
|
|
const del = mods.find(m => m.type === 'delay');
|
|
const rev = mods.find(m => m.type === 'reverb');
|
|
const kb = mods.find(m => m.type === 'keyboard');
|
|
const seq = mods.find(m => m.type === 'sequencer');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (oscs.length < 3 || flts.length < 1 || envs.length < 1 || !del || !rev || !out) return false;
|
|
if (!kb && !seq) return false;
|
|
const leftConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'left');
|
|
const rightConn = conns.some(c => c.to.moduleId === out.id && c.to.port === 'right');
|
|
const delToOut = conns.some(c => c.from.moduleId === del.id && c.to.moduleId === out.id);
|
|
const revToOut = conns.some(c => c.from.moduleId === rev.id && c.to.moduleId === out.id);
|
|
return leftConn && rightConn && delToOut && revToOut && conns.length >= 15;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|