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>
491 lines
21 KiB
JavaScript
491 lines
21 KiB
JavaScript
/**
|
|
* World 2 — "Filtros" (Filters)
|
|
*
|
|
* Teaches: lowpass, highpass, bandpass, resonance, cutoff modulation
|
|
* 8 levels, progressive difficulty
|
|
*/
|
|
|
|
export const WORLD_2 = {
|
|
id: 'w2',
|
|
name: 'Filtros',
|
|
subtitle: 'Esculpe el timbre con filtros',
|
|
icon: '▽',
|
|
color: '#ff6644',
|
|
unlockStars: 12, // Need 12 stars from World 1 to unlock
|
|
levels: [
|
|
// ─────────────── LEVEL 2.1 ───────────────
|
|
{
|
|
id: 'w2-1',
|
|
title: 'El Paso Bajo',
|
|
subtitle: 'Quita los agudos',
|
|
description: 'Un filtro paso bajo (lowpass) deja pasar las frecuencias graves y elimina las agudas. Es el filtro más usado en síntesis — piensa en cómo suena la música debajo del agua. Conecta el oscilador a través del filtro.',
|
|
concept: 'Conecta: Oscillator → Filter → Output. El filtro ya está en modo lowpass. El knob "Cutoff" controla hasta qué frecuencia deja pasar. Bájalo para un sonido más oscuro.',
|
|
availableModules: [],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'oscillator', x: 80, y: 80, params: { waveform: 'sawtooth', frequency: 220, detune: 0 }, locked: true },
|
|
{ id: 2, type: 'filter', x: 340, y: 80, params: { type: 'lowpass', frequency: 2000, Q: 1 }, locked: false },
|
|
{ id: 3, type: 'output', x: 600, y: 100, params: { volume: -6 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 800 },
|
|
duration: 2.5,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Señal filtrada',
|
|
desc: 'Conecta oscilador → filtro → salida',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!osc || !flt || !out) return false;
|
|
const oscToFlt = conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === flt.id);
|
|
const fltToOut = conns.some(c => c.from.moduleId === flt.id && c.to.moduleId === out.id);
|
|
return oscToFlt && fltToOut;
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Cutoff bajo',
|
|
desc: 'Baja el cutoff por debajo de 1200 Hz',
|
|
test: (mods) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
return flt && (flt.params.frequency ?? 2000) < 1200;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Sonido oscuro',
|
|
desc: 'Cutoff cercano a 800 Hz (±200 Hz)',
|
|
test: (mods) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
if (!flt) return false;
|
|
return Math.abs((flt.params.frequency ?? 2000) - 800) <= 200;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 2.2 ───────────────
|
|
{
|
|
id: 'w2-2',
|
|
title: 'El Paso Alto',
|
|
subtitle: 'Solo los agudos',
|
|
description: 'El filtro paso alto (highpass) es lo opuesto: elimina los graves y deja pasar los agudos. Es perfecto para quitar el "barro" de un sonido o crear texturas etéreas y delgadas.',
|
|
concept: 'Cambia el tipo de filtro a "highpass". Sube el cutoff para que solo pasen las frecuencias altas. Un cutoff de ~2000 Hz eliminará todo lo grave.',
|
|
availableModules: ['filter'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'oscillator', x: 80, y: 80, params: { waveform: 'sawtooth', frequency: 220, detune: 0 }, locked: true },
|
|
{ id: 2, type: 'output', x: 600, y: 100, params: { volume: -6 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 220 } },
|
|
],
|
|
filter: { type: 'highpass', frequency: 2000 },
|
|
duration: 2.5,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Filtro conectado',
|
|
desc: 'Coloca un filtro entre oscilador y salida',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!osc || !flt || !out) return false;
|
|
const oscToFlt = conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === flt.id);
|
|
const fltToOut = conns.some(c => c.from.moduleId === flt.id && c.to.moduleId === out.id);
|
|
return oscToFlt && fltToOut;
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Modo highpass',
|
|
desc: 'Cambia el filtro a highpass',
|
|
test: (mods) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
return flt && flt.params.type === 'highpass';
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Cutoff preciso',
|
|
desc: 'Cutoff cercano a 2000 Hz (±300 Hz)',
|
|
test: (mods) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
if (!flt) return false;
|
|
return flt.params.type === 'highpass' && Math.abs((flt.params.frequency ?? 1000) - 2000) <= 300;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 2.3 ───────────────
|
|
{
|
|
id: 'w2-3',
|
|
title: 'Resonancia',
|
|
subtitle: 'El pico que canta',
|
|
description: 'La resonancia (Q) amplifica las frecuencias justo alrededor del punto de corte. Con poca resonancia el filtro es suave. Con mucha, el filtro "canta" — es el sonido ácido clásico del TB-303.',
|
|
concept: 'Sube el knob "Reso" (Q) del filtro a un valor alto (~8-12). Mantén el cutoff bajo (~600 Hz) con lowpass. Escucharás cómo el filtro enfatiza esa frecuencia.',
|
|
availableModules: [],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'oscillator', x: 80, y: 80, params: { waveform: 'sawtooth', frequency: 110, detune: 0 }, locked: true },
|
|
{ id: 2, type: 'filter', x: 340, y: 80, params: { type: 'lowpass', frequency: 1000, Q: 1 }, locked: false },
|
|
{ id: 3, type: 'output', x: 600, y: 100, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 110 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 600, Q: 10 },
|
|
duration: 2.5,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Señal filtrada',
|
|
desc: 'Conecta oscilador → filtro → salida',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!osc || !flt || !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 === out.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Resonancia alta',
|
|
desc: 'Sube la resonancia (Q) por encima de 5',
|
|
test: (mods) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
return flt && (flt.params.Q ?? 1) > 5;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Sonido ácido',
|
|
desc: 'Q alto (~8-12) y cutoff bajo (~600 Hz ±200)',
|
|
test: (mods) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
if (!flt) return false;
|
|
const q = flt.params.Q ?? 1;
|
|
const freq = flt.params.frequency ?? 1000;
|
|
return q >= 7 && q <= 15 && Math.abs(freq - 600) <= 200;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 2.4 ───────────────
|
|
{
|
|
id: 'w2-4',
|
|
title: 'Banda Pasante',
|
|
subtitle: 'Solo el medio',
|
|
description: 'El filtro bandpass deja pasar solo un rango estrecho de frecuencias alrededor del cutoff. Es como poner un lowpass y un highpass a la vez. Crea sonidos nasales, tipo telefono o walkie-talkie.',
|
|
concept: 'Cambia el tipo a "bandpass". El cutoff define el centro de la banda. Sube la Q para hacerla más estrecha (más nasal). Un cutoff de ~1000 Hz con Q alta suena como una voz por teléfono.',
|
|
availableModules: ['filter'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'noise', x: 80, y: 80, params: { type: 'white' }, locked: true },
|
|
{ id: 2, type: 'output', x: 600, y: 100, params: { volume: -10 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'noise', params: { type: 'white' } },
|
|
],
|
|
filter: { type: 'bandpass', frequency: 1000, Q: 8 },
|
|
duration: 2,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Ruido filtrado',
|
|
desc: 'Coloca filtro entre noise y salida',
|
|
test: (mods, conns) => {
|
|
const noise = mods.find(m => m.type === 'noise');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!noise || !flt || !out) return false;
|
|
return conns.some(c => c.from.moduleId === noise.id && c.to.moduleId === flt.id) &&
|
|
conns.some(c => c.from.moduleId === flt.id && c.to.moduleId === out.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Modo bandpass',
|
|
desc: 'Filtro en modo bandpass',
|
|
test: (mods) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
return flt && flt.params.type === 'bandpass';
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Nasal perfecto',
|
|
desc: 'Bandpass a ~1000 Hz con Q alta (>5)',
|
|
test: (mods) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
if (!flt) return false;
|
|
return flt.params.type === 'bandpass' &&
|
|
Math.abs((flt.params.frequency ?? 1000) - 1000) <= 300 &&
|
|
(flt.params.Q ?? 1) > 5;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 2.5 ───────────────
|
|
{
|
|
id: 'w2-5',
|
|
title: 'Filtro en Movimiento',
|
|
subtitle: 'LFO → Cutoff',
|
|
description: 'Los filtros estáticos son útiles, pero los filtros en movimiento son mágicos. Conectar un LFO al cutoff de un filtro crea un barrido cíclico — es el sonido "wah-wah" clásico del funk y la música electrónica.',
|
|
concept: 'Conecta un LFO a la entrada "Cutoff" del filtro. El LFO modulará el punto de corte automáticamente. Ajusta la velocidad del LFO (~2-4 Hz) para un wobble rítmico.',
|
|
availableModules: ['lfo'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'oscillator', x: 80, y: 60, params: { waveform: 'sawtooth', frequency: 110, detune: 0 }, locked: true },
|
|
{ id: 2, type: 'filter', x: 340, y: 60, params: { type: 'lowpass', frequency: 800, Q: 5 }, locked: false },
|
|
{ id: 3, type: 'output', x: 600, y: 80, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 110 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 800, Q: 5 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Cadena de audio',
|
|
desc: 'Oscilador → filtro → salida conectados',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!osc || !flt || !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 === out.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'LFO conectado',
|
|
desc: 'Conecta un LFO a la entrada 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: 3,
|
|
name: 'Wobble rítmico',
|
|
desc: 'LFO entre 1-6 Hz, resonancia > 3',
|
|
test: (mods) => {
|
|
const lfo = mods.find(m => m.type === 'lfo');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
if (!lfo || !flt) return false;
|
|
const rate = lfo.params.frequency ?? 2;
|
|
return rate >= 1 && rate <= 6 && (flt.params.Q ?? 1) > 3;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 2.6 ───────────────
|
|
{
|
|
id: 'w2-6',
|
|
title: 'Dos Filtros',
|
|
subtitle: 'Escultura sónica',
|
|
description: 'Los ingenieros de sonido encadenan filtros para obtener formas más complejas. Un highpass para quitar el subgrave seguido de un lowpass para suavizar los agudos es una técnica estándar de mezcla.',
|
|
concept: 'Conecta: Oscilador → Filtro 1 (highpass, ~200 Hz) → Filtro 2 (lowpass, ~3000 Hz) → Output. Esto deja solo las frecuencias medias — "limpia" el sonido.',
|
|
availableModules: ['filter'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'oscillator', x: 60, y: 80, params: { waveform: 'sawtooth', frequency: 110, detune: 0 }, locked: true },
|
|
{ id: 2, type: 'output', x: 720, y: 100, params: { volume: -6 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 110 } },
|
|
],
|
|
duration: 2.5,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Cadena doble',
|
|
desc: 'Oscilador → filtro → filtro → salida',
|
|
test: (mods, conns) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flts = mods.filter(m => m.type === 'filter');
|
|
const out = mods.find(m => m.type === 'output');
|
|
if (!osc || flts.length < 2 || !out) return false;
|
|
// Check chain exists
|
|
const oscToFlt = flts.some(f => conns.some(c => c.from.moduleId === osc.id && c.to.moduleId === f.id));
|
|
const fltToOut = flts.some(f => conns.some(c => c.from.moduleId === f.id && c.to.moduleId === out.id));
|
|
const fltToFlt = flts.some(f1 => flts.some(f2 =>
|
|
f1.id !== f2.id && conns.some(c => c.from.moduleId === f1.id && c.to.moduleId === f2.id)
|
|
));
|
|
return oscToFlt && fltToOut && fltToFlt;
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Highpass + Lowpass',
|
|
desc: 'Un filtro en highpass y otro en lowpass',
|
|
test: (mods) => {
|
|
const flts = mods.filter(m => m.type === 'filter');
|
|
if (flts.length < 2) return false;
|
|
const types = flts.map(f => f.params.type);
|
|
return types.includes('highpass') && types.includes('lowpass');
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Banda limpia',
|
|
desc: 'HP ~200 Hz (±100) + LP ~3000 Hz (±1000)',
|
|
test: (mods) => {
|
|
const flts = mods.filter(m => m.type === 'filter');
|
|
const hp = flts.find(f => f.params.type === 'highpass');
|
|
const lp = flts.find(f => f.params.type === 'lowpass');
|
|
if (!hp || !lp) return false;
|
|
return Math.abs((hp.params.frequency ?? 1000) - 200) <= 100 &&
|
|
Math.abs((lp.params.frequency ?? 1000) - 3000) <= 1000;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 2.7 ───────────────
|
|
{
|
|
id: 'w2-7',
|
|
title: 'Filtro + Mezcla',
|
|
subtitle: 'Timbres paralelos',
|
|
description: 'Filtra dos osciladores de forma diferente y mézclalos. Es la base del diseño de sonido: capas con diferentes caracteres tímbricos que juntas crean algo más rico que la suma de sus partes.',
|
|
concept: 'Dos osciladores, cada uno con su propio filtro (diferentes cutoffs), ambos al mixer, mixer al output. Uno oscuro y gordo (LP bajo), otro brillante (LP alto o sin filtro).',
|
|
availableModules: ['oscillator', 'filter', 'mixer'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 800, y: 140, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 110 } },
|
|
{ type: 'oscillator', params: { waveform: 'square', frequency: 220 } },
|
|
],
|
|
duration: 2.5,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Dos cadenas',
|
|
desc: 'Dos osciladores, cada uno filtrado, al mixer',
|
|
test: (mods, conns) => {
|
|
const oscs = mods.filter(m => m.type === 'oscillator');
|
|
const flts = mods.filter(m => m.type === 'filter');
|
|
const mixer = mods.find(m => m.type === 'mixer');
|
|
const out = mods.find(m => m.type === 'output');
|
|
return oscs.length >= 2 && flts.length >= 2 && mixer && out &&
|
|
conns.some(c => c.from.moduleId === mixer.id && c.to.moduleId === out.id);
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Filtros diferentes',
|
|
desc: 'Los dos filtros tienen cutoffs distintos (diferencia > 500 Hz)',
|
|
test: (mods) => {
|
|
const flts = mods.filter(m => m.type === 'filter');
|
|
if (flts.length < 2) return false;
|
|
const freqs = flts.map(f => f.params.frequency ?? 1000);
|
|
return Math.abs(freqs[0] - freqs[1]) > 500;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: 'Capas contrastadas',
|
|
desc: 'Un filtro oscuro (<600 Hz) y otro brillante (>3000 Hz)',
|
|
test: (mods) => {
|
|
const flts = mods.filter(m => m.type === 'filter');
|
|
if (flts.length < 2) return false;
|
|
const freqs = flts.map(f => f.params.frequency ?? 1000).sort((a, b) => a - b);
|
|
return freqs[0] < 600 && freqs[freqs.length - 1] > 3000;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
// ─────────────── LEVEL 2.8: BOSS ───────────────
|
|
{
|
|
id: 'w2-8',
|
|
title: 'Acid Bass',
|
|
subtitle: 'BOSS: El sonido TB-303',
|
|
description: 'El Roland TB-303 definió el acid house. Su secreto: un oscilador cuadrado/sierra a frecuencia baja, un filtro lowpass con MUCHA resonancia, y modulación del cutoff. Recrea ese sonido legendario.',
|
|
concept: 'Oscilador saw a ~55-110 Hz → Filtro lowpass con Q alta (~12-15) y cutoff medio (~400-800 Hz) → Output. Añade un LFO lento (~0.5-2 Hz) modulando el cutoff para el movimiento ácido.',
|
|
availableModules: ['oscillator', 'filter', 'lfo'],
|
|
preplacedModules: [
|
|
{ id: 1, type: 'output', x: 700, y: 120, params: { volume: -8 }, locked: true },
|
|
],
|
|
target: {
|
|
build: [
|
|
{ type: 'oscillator', params: { waveform: 'sawtooth', frequency: 82 } },
|
|
],
|
|
filter: { type: 'lowpass', frequency: 500, Q: 14 },
|
|
duration: 3,
|
|
},
|
|
checks: [
|
|
{
|
|
star: 1,
|
|
name: 'Cadena ácida',
|
|
desc: 'Oscilador → filtro → salida con LFO 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 out = mods.find(m => m.type === 'output');
|
|
if (!osc || !flt || !out || !lfo) 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 === out.id) &&
|
|
conns.some(c => c.from.moduleId === lfo.id && c.to.moduleId === flt.id && c.to.port === 'cutoff');
|
|
},
|
|
},
|
|
{
|
|
star: 2,
|
|
name: 'Resonancia ácida',
|
|
desc: 'Filtro lowpass con Q > 10',
|
|
test: (mods) => {
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
return flt && flt.params.type === 'lowpass' && (flt.params.Q ?? 1) > 10;
|
|
},
|
|
},
|
|
{
|
|
star: 3,
|
|
name: '303 auténtico',
|
|
desc: 'Saw/square baja (<130Hz), Q>10, cutoff 300-900Hz, LFO lento (<3Hz)',
|
|
test: (mods) => {
|
|
const osc = mods.find(m => m.type === 'oscillator');
|
|
const flt = mods.find(m => m.type === 'filter');
|
|
const lfo = mods.find(m => m.type === 'lfo');
|
|
if (!osc || !flt || !lfo) return false;
|
|
const freq = osc.params.frequency ?? 440;
|
|
const wave = osc.params.waveform;
|
|
const cutoff = flt.params.frequency ?? 1000;
|
|
const q = flt.params.Q ?? 1;
|
|
const rate = lfo.params.frequency ?? 2;
|
|
return freq < 130 && (wave === 'sawtooth' || wave === 'square') &&
|
|
flt.params.type === 'lowpass' && q > 10 &&
|
|
cutoff >= 300 && cutoff <= 900 && rate < 3;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|