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>
554 lines
26 KiB
JavaScript
554 lines
26 KiB
JavaScript
/**
|
|
* World 9 — "Síntesis Sustractiva Clásica" (Classic Subtractive Synthesis)
|
|
*
|
|
* Teaches: Moog-style synthesis, resonant filters, acid bass, PWM simulation
|
|
* 8 levels, boss challenges with complete subtractive synth
|
|
*/
|
|
|
|
export const WORLD_9 = {
|
|
id: 'w9',
|
|
name: 'Síntesis Sustractiva',
|
|
subtitle: 'Los sonidos clásicos del sintetizador',
|
|
icon: '▽~',
|
|
color: '#ff4466',
|
|
unlockStars: 96,
|
|
levels: [
|
|
// ─────────────── LEVEL 9.1 ───────────────
|
|
{
|
|
id: 'w9-1',
|
|
title: 'Lead Sawtooth',
|
|
subtitle: 'La onda más rica en armónicos',
|
|
description: 'El sawtooth es la onda fundamental de la síntesis sustractiva — contiene todos los armónicos. Conecta un oscilador sawtooth a un filtro lowpass para quitar brillo, y un VCA para controlar el volumen.',
|
|
concept: 'Osc sawtooth → Filter LP → VCA → Output. El filtro controla el brillo, el VCA controla la amplitud. Ajusta la frecuencia y el cutoff del filtro para explorar sonidos.',
|
|
availableModules: ['oscillator', 'filter', 'vca', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 700, y: 120, params: { volume: -6 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220, detune: 0 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 4000, Q: 1.2 },
|
|
envelope: { attack: 0.05, decay: 0.3, sustain: 0.4, release: 0.2 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Sawtooth básico',
|
|
desc: 'Osc sawtooth → 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 osc.params.waveform === 'sawtooth' &&
|
|
conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === flt.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Filtro activo',
|
|
desc: 'Filtro lowpass con cutoff controlable',
|
|
test: (mods, conns) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
if (!flt) return false;
|
|
return flt.params.type === 'lowpass' &&
|
|
(flt.params.frequency ?? 1000) > 500 &&
|
|
(flt.params.Q ?? 1) >= 1;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Lead completo',
|
|
desc: 'Sawtooth + LP + VCA + envelope + keyboard',
|
|
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 env = mods.find(m => m.type === 'envelope');
|
|
const kb = mods.find(m => m.type === 'keyboard');
|
|
if (!osc || !flt || !vca || !env || !kb) return false;
|
|
return osc.params.waveform === 'sawtooth' &&
|
|
flt.params.type === 'lowpass' &&
|
|
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === env.id && c.to.port === 'gate') &&
|
|
conns.some(c => c.from.moduleId === env.id && c.to.moduleId === vca.id && c.to.port === 'cv');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 9.2 ───────────────
|
|
{
|
|
id: 'w9-2',
|
|
title: 'Filtro Resonante',
|
|
subtitle: 'El corazón de Moog',
|
|
description: 'La resonancia (Q alto) en el filtro crea un pico característico en el cutoff frequency. Este es el sonido Moog: cuando bajas el cutoff con resonancia, el filtro empieza a auto-oscilar y cantar.',
|
|
concept: 'Osc sawtooth → Filter LP (Q > 4) → VCA → Output. Cuanto más alto el Q, más dramático el efecto. Baja el cutoff lentamente para escuchar la resonancia.',
|
|
availableModules: ['oscillator', 'filter', 'vca', 'lfo', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 700, y: 120, params: { volume: -6 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 165, detune: 0 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 2500, Q: 6 },
|
|
lfo: { frequency: 0.8, type: 'sine', min: 1000, max: 4500, target: 'frequency' },
|
|
envelope: { attack: 0.08, decay: 0.4, sustain: 0.3, release: 0.25 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Resonancia perceptible',
|
|
desc: 'Filtro LP con Q > 3',
|
|
test: (mods) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
if (!flt) return false;
|
|
return flt.params.type === 'lowpass' &&
|
|
(flt.params.Q ?? 1) > 3;
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Moog Resonante',
|
|
desc: 'Sawtooth + LP (Q > 5) + VCA + envelope',
|
|
test: (mods) => {
|
|
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 env = mods.find(m => m.type === 'envelope');
|
|
if (!osc || !flt || !vca || !env) return false;
|
|
return osc.params.waveform === 'sawtooth' &&
|
|
flt.params.type === 'lowpass' &&
|
|
(flt.params.Q ?? 1) > 5 &&
|
|
(env.params.attack ?? 0.01) < 0.1;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Barrido de Filtro',
|
|
desc: 'LFO modulando el cutoff del filtro con resonancia alta',
|
|
test: (mods, conns) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const lfo = mods.find(m => m.type === 'lfo');
|
|
if (!flt || !lfo) return false;
|
|
return flt.params.type === 'lowpass' &&
|
|
(flt.params.Q ?? 1) > 4 &&
|
|
conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 9.3 ───────────────
|
|
{
|
|
id: 'w9-3',
|
|
title: 'Brass Stab',
|
|
subtitle: 'El ataque metálico',
|
|
description: 'Un "brass stab" es un sonido de trompeta: square wave, filtro que se abre rápido en el ataque y luego se cierra. El envelope en el filtro crea el efecto de "toque" de la trompeta.',
|
|
concept: 'Osc square → Filter LP → VCA → Output. El truco: el envelope NO va al VCA sino al CUTOFF del filtro. Attack del env muy corto. El filtro sube y baja, no el volumen.',
|
|
availableModules: ['oscillator', 'filter', 'vca', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 700, y: 120, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'square', frequency: 330, detune: 0 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 1800, Q: 2 },
|
|
envelope: { attack: 0.01, decay: 0.35, sustain: 0.1, release: 0.15 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Square + Filtro',
|
|
desc: 'Osc square → 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');
|
|
if (!osc || !flt || !vca) return false;
|
|
return osc.params.waveform === 'square' &&
|
|
flt.params.type === 'lowpass' &&
|
|
conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === flt.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Envelope al Filtro',
|
|
desc: 'Envelope conectado al cutoff del filtro',
|
|
test: (mods, conns) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const env = mods.find(m => m.type === 'envelope');
|
|
if (!flt || !env) return false;
|
|
return conns.some(c => c.from.moduleId === env.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Brass Stab Perfecta',
|
|
desc: 'Square + LP, envelope (attack < 0.02s) al cutoff, keyboard gatea el env',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const env = mods.find(m => m.type === 'envelope');
|
|
const kb = mods.find(m => m.type === 'keyboard');
|
|
if (!osc || !flt || !env || !kb) return false;
|
|
return osc.params.waveform === 'square' &&
|
|
flt.params.type === 'lowpass' &&
|
|
(env.params.attack ?? 0.01) < 0.02 &&
|
|
conns.some(c => c.from.moduleId === env.id && c.to.moduleId === flt.id && c.to.port === 'cutoff') &&
|
|
conns.some(c => c.from.moduleId === kb.id && c.to.moduleId === env.id && c.to.port === 'gate');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 9.4 ───────────────
|
|
{
|
|
id: 'w9-4',
|
|
title: 'Acid Bass 303',
|
|
subtitle: 'El sonido de la danza',
|
|
description: 'El acid bass es el legendario sonido del sintetizador TB-303: oscilador a frecuencia grave, filtro lowpass muy resonante, y un envelope que modula el cutoff para crear el "slide" característico.',
|
|
concept: 'Osc sawtooth/square ~55 Hz → Sequencer freq. Filter LP (Q muy alto, ~8+) → VCA → Output. Envelope rápido al cutoff. El sequencer proporciona las notas; el filtro hace el sonido "acid".',
|
|
availableModules: ['oscillator', 'filter', 'vca', 'envelope', 'sequencer', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 700, y: 120, params: { volume: -6 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 55, detune: 0 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 800, Q: 9 },
|
|
envelope: { attack: 0.02, decay: 0.25, sustain: 0.05, release: 0.15 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Bajo + Secuenciador',
|
|
desc: 'Sequencer → Osc grave + Filter → Output',
|
|
test: (mods, conns) => {
|
|
const seq = mods.find(m => m.type === 'sequencer');
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
if (!seq || !osc || !flt) return false;
|
|
return (osc.params.frequency ?? 440) < 100 &&
|
|
conns.some(c => c.from.moduleId === seq.id && c.to.moduleId === osc.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Resonancia acid',
|
|
desc: 'Filtro LP con Q > 6, envelope al cutoff',
|
|
test: (mods, conns) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const env = mods.find(m => m.type === 'envelope');
|
|
if (!flt || !env) return false;
|
|
return flt.params.type === 'lowpass' &&
|
|
(flt.params.Q ?? 1) > 6 &&
|
|
conns.some(c => c.from.moduleId === env.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: '303 Clásico',
|
|
desc: 'Sequencer + osc < 60 Hz + LP (Q > 8) + envelope rápido al cutoff',
|
|
test: (mods, conns) => {
|
|
const seq = mods.find(m => m.type === 'sequencer');
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const env = mods.find(m => m.type === 'envelope');
|
|
if (!seq || !osc || !flt || !env) return false;
|
|
return (osc.params.frequency ?? 440) < 60 &&
|
|
flt.params.type === 'lowpass' &&
|
|
(flt.params.Q ?? 1) > 8 &&
|
|
(env.params.decay ?? 0.2) < 0.3 &&
|
|
conns.some(c => c.from.moduleId === seq.id && c.to.moduleId === osc.id);
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 9.5 ───────────────
|
|
{
|
|
id: 'w9-5',
|
|
title: 'String Pad Detuned',
|
|
subtitle: 'Capas de sierras',
|
|
description: 'Los string pads de las sinfonías electrónicas usan múltiples osciladores ligeramente detuned, un filtro suave, y un envelope lento. El detune crea una "chorusing" natural que emula el sonido de múltiples instrumentos.',
|
|
concept: '3 oscs sawtooth, cada uno con detune diferente (~0, +5, -7) → Mixer → Filter LP suave → VCA → Output. Envelope lento al VCA. Juntos crean una textura cálida y movible.',
|
|
availableModules: ['oscillator', 'mixer', 'filter', 'vca', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 800, y: 120, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 110, detune: 0 } },
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 110, detune: 5 } },
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 110, detune: -7 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 3500, Q: 0.9 },
|
|
envelope: { attack: 0.08, decay: 0.8, sustain: 0.5, release: 0.4 },
|
|
duration: 4,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Múltiples sierras',
|
|
desc: '3 osciladores sawtooth → Mixer → Output',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const mixer = mods.find(m => m.type === 'mixer');
|
|
if (oscs.length < 3 || !mixer) return false;
|
|
return oscs.every(o => o.params.waveform === 'sawtooth') &&
|
|
oscs.some(o => conns.some(c => c.from.moduleId === o.id && c.to.moduleId === mixer.id));
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Detune activo',
|
|
desc: 'Al menos 2 osciladores con detune diferente (|diff| > 3)',
|
|
test: (mods) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator' && m.params.waveform === 'sawtooth');
|
|
if (oscs.length < 3) return false;
|
|
const detunes = oscs.map(o => o.params.detune ?? 0);
|
|
const maxDiff = Math.max(...detunes) - Math.min(...detunes);
|
|
return maxDiff > 3;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'String Pad Completa',
|
|
desc: '3 saws detuned + mixer + LP + envelope lento al VCA',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator' && m.params.waveform === 'sawtooth');
|
|
const mixer = mods.find(m => m.type === 'mixer');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const vca = mods.find(m => m.type === 'vca');
|
|
const env = mods.find(m => m.type === 'envelope');
|
|
if (oscs.length < 3 || !mixer || !flt || !vca || !env) return false;
|
|
const detunes = oscs.map(o => o.params.detune ?? 0);
|
|
const maxDiff = Math.max(...detunes) - Math.min(...detunes);
|
|
return maxDiff > 3 &&
|
|
flt.params.type === 'lowpass' &&
|
|
(env.params.attack ?? 0.01) < 0.1 &&
|
|
(env.params.decay ?? 0.2) > 0.5 &&
|
|
conns.some(c => c.from.moduleId === env.id && c.to.moduleId === vca.id && c.to.port === 'cv');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 9.6 ───────────────
|
|
{
|
|
id: 'w9-6',
|
|
title: 'PWM Simulator',
|
|
subtitle: 'Pseudo Pulse Width Modulation',
|
|
description: 'El PWM (Pulse Width Modulation) es cuando varías el ancho del pulso de una onda square. Podemos simularla mezclando dos osciladores square ligeramente detuned — crean una "beating" que suena como PWM.',
|
|
concept: '2 oscs square, uno a frecuencia base, otro detuned ~3-5 cents → Mixer → Filter → VCA → Output. El beating de frecuencias crea la ilusión de PWM. Un LFO puede modular más aún.',
|
|
availableModules: ['oscillator', 'mixer', 'filter', 'vca', 'lfo', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 800, y: 120, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'square', frequency: 220, detune: 0 } },
|
|
{ type: 'oscillator', params: { waveform: 'square', frequency: 220, detune: 4 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 3000, Q: 1.5 },
|
|
lfo: { frequency: 0.6, type: 'sine', min: 2500, max: 4500, target: 'frequency' },
|
|
envelope: { attack: 0.06, decay: 0.35, sustain: 0.35, release: 0.2 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Dos squares',
|
|
desc: '2 osciladores square → Mixer → Output',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator' && m.params.waveform === 'square');
|
|
const mixer = mods.find(m => m.type === 'mixer');
|
|
if (oscs.length < 2 || !mixer) return false;
|
|
return oscs.some(o => conns.some(c => c.from.moduleId === o.id && c.to.moduleId === mixer.id));
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Beating audible',
|
|
desc: 'Detune entre squares > 2 cents para audible beating',
|
|
test: (mods) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator' && m.params.waveform === 'square');
|
|
if (oscs.length < 2) return false;
|
|
const detunes = oscs.map(o => o.params.detune ?? 0);
|
|
return Math.abs(detunes[0] - detunes[1]) > 2;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'PWM Dinámico',
|
|
desc: '2 squares detuned + mixer + filter + LFO al detune de un osc',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator' && m.params.waveform === 'square');
|
|
const mixer = mods.find(m => m.type === 'mixer');
|
|
const lfo = mods.find(m => m.type === 'lfo');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
if (oscs.length < 2 || !mixer || !lfo || !flt) return false;
|
|
const detunes = oscs.map(o => o.params.detune ?? 0);
|
|
const hasDetune = Math.abs(detunes[0] - detunes[1]) > 2;
|
|
const lfoToOsc = oscs.some(o =>
|
|
conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === o.id && c.to.port === 'detune')
|
|
);
|
|
return hasDetune && lfoToOsc && flt.params.type === 'lowpass';
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 9.7 ───────────────
|
|
{
|
|
id: 'w9-7',
|
|
title: 'Filter Sweep Técnica',
|
|
subtitle: 'Control dinámico del timbre',
|
|
description: 'El filter sweep es el corazón de la síntesis sustractiva: modular la frecuencia de cutoff con un LFO o envelope. Esto cambia el timbre del sonido en tiempo real. Es la vida de la síntesis.',
|
|
concept: 'Osc sawtooth → Filter LP → VCA → Output. LFO (frecuencia baja ~0.2-2 Hz) → Cutoff del filter. También conecta envelope al cutoff para un sweep más rápido. Keyboard dispara ambos.',
|
|
availableModules: ['oscillator', 'filter', 'vca', 'lfo', 'envelope', 'keyboard'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 700, y: 120, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 130, detune: 0 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 2000, Q: 2 },
|
|
lfo: { frequency: 1, type: 'sine', min: 500, max: 5000, target: 'frequency' },
|
|
envelope: { attack: 0.07, decay: 0.5, sustain: 0.2, release: 0.25 },
|
|
duration: 4,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'LFO al Cutoff',
|
|
desc: 'LFO conectado a cutoff del filtro',
|
|
test: (mods, conns) => {
|
|
const lfo = mods.find(m => m.type === 'lfo');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
if (!lfo || !flt) return false;
|
|
return conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'LFO lento',
|
|
desc: 'LFO con frecuencia < 2 Hz para sweep audible',
|
|
test: (mods) => {
|
|
const lfo = mods.find(m => m.type === 'lfo');
|
|
if (!lfo) return false;
|
|
return (lfo.params.frequency ?? 2) < 2 &&
|
|
(lfo.params.amplitude ?? 0.5) > 0.3;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Sweep Completo',
|
|
desc: 'Sawtooth + LP + LFO lento + envelope al cutoff',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const lfo = mods.find(m => m.type === 'lfo');
|
|
const env = mods.find(m => m.type === 'envelope');
|
|
if (!osc || !flt || !lfo || !env) return false;
|
|
return osc.params.waveform === 'sawtooth' &&
|
|
flt.params.type === 'lowpass' &&
|
|
(lfo.params.frequency ?? 2) < 2 &&
|
|
conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff') &&
|
|
conns.some(c => c.from.moduleId === env.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 9.8: BOSS ───────────────
|
|
{
|
|
id: 'w9-8',
|
|
title: 'Sintetizador Clásico',
|
|
subtitle: 'BOSS FINAL: Moog Completo',
|
|
description: 'Construye el sintetizador sustractivo completo: múltiples osciladores, filtro resonante, envelopes, LFO, y todo conectado para crear sonidos ricos y expressivos. Este es el verdadero sintetizador analógico.',
|
|
concept: 'Construye un synth con: 2+ osciladores (mezcla de saw/square), filtro LP resonante (Q > 4), 2+ envelopes, 1+ LFO, VCA, keyboard, y al menos un efecto. Todo debe sonar cohesivo y expressivo.',
|
|
availableModules: ['oscillator', 'filter', 'vca', 'lfo', 'envelope', 'mixer', 'keyboard', 'delay', 'distortion', 'reverb'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 900, y: 140, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 110, detune: 0 } },
|
|
{ type: 'oscillator', params: { waveform: 'square', frequency: 110, detune: 3 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 3000, Q: 6 },
|
|
lfo: { frequency: 0.5, type: 'sine', min: 1000, max: 4000, target: 'frequency' },
|
|
effects: [
|
|
{ type: 'reverb', decay: 2.5, wet: 0.4 },
|
|
],
|
|
envelope: { attack: 0.08, decay: 0.5, sustain: 0.3, release: 0.3 },
|
|
duration: 5,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Síntesis funcional',
|
|
desc: 'Múltiples oscs + filtro LP + VCA + envelope + keyboard',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
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 (oscs.length < 2 || !flt || !vca || !env || !kb || !out) return false;
|
|
return flt.params.type === 'lowpass' &&
|
|
conns.some(c => c.to.moduleId === out.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Moog característico',
|
|
desc: '2+ oscs + filtro LP resonante (Q > 4) + envelope modulando cutoff',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const env = mods.find(m => m.type === 'envelope');
|
|
if (oscs.length < 2 || !flt || !env) return false;
|
|
return flt.params.type === 'lowpass' &&
|
|
(flt.params.Q ?? 1) > 4 &&
|
|
conns.some(c => c.from.moduleId === env.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Maestro Sustractivo',
|
|
desc: '2+ oscs detuned + LP (Q > 5) + 2 envs + LFO + efecto + keyboard',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const envs = mods.filter(m => m.type === 'envelope');
|
|
const lfo = mods.find(m => m.type === 'lfo');
|
|
const kb = mods.find(m => m.type === 'keyboard');
|
|
const effects = mods.filter(m => ['delay', 'reverb', 'distortion'].includes(m.type));
|
|
if (oscs.length < 2 || !flt || envs.length < 2 || !lfo || !kb || effects.length < 1) return false;
|
|
const detunes = oscs.map(o => o.params.detune ?? 0);
|
|
const hasDetune = Math.max(...detunes) - Math.min(...detunes) > 2;
|
|
return flt.params.type === 'lowpass' &&
|
|
(flt.params.Q ?? 1) > 5 &&
|
|
hasDetune &&
|
|
conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff') &&
|
|
conns.length >= 12;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|