web: in-app SERVICE MANUAL overlay

Adds a docs overlay that lives inside the rack chrome, opened from a new
MANUAL hw-btn in the top bar (ESC also closes). Styled as a screen recess
matching the editor — amber section headers, engraved labels, recessed code
blocks — so it reads as a manual page of the instrument, not a help popup.

Covers patch language, execution model, every node type (osc, trig, adsr,
filter, noise, seq, delay, poly+voice) and the four control nodes (knob,
fader, step_seq, piano_roll), plus practical tips on gain staging and
hot-reload behavior. Each example block carries a TRY button that swaps the
editor contents with the snippet so you can hear it immediately (Ctrl/Cmd-Z
to bring your patch back).
This commit is contained in:
Jose Luis Montañes
2026-05-01 20:09:04 +02:00
parent 847a6c6881
commit d0a38959c0

View File

@@ -543,6 +543,152 @@
box-shadow: none; animation: none; } box-shadow: none; animation: none; }
.blink { animation: blink 1.1s step-end infinite; } .blink { animation: blink 1.1s step-end infinite; }
@keyframes blink { 50% { opacity: 0; } } @keyframes blink { 50% { opacity: 0; } }
/* ===================================================================== */
/* Manual overlay — in-app docs styled as a "service manual" */
/* Sits inside .hardware, between top-bar and bottom-bar. */
/* ===================================================================== */
#manual {
position: absolute;
/* sit between the top-bar and bottom-bar (each 30px + 10px gap = 40px). */
top: calc(14px + 30px + 10px);
left: 14px; right: 14px;
bottom: calc(14px + 30px + 10px);
z-index: 10;
display: none;
flex-direction: column;
background: var(--hw-screen);
border-radius: 4px;
box-shadow:
inset 0 2px 4px rgba(0,0,0,0.7),
inset 0 -1px 0 rgba(255,220,180,0.03),
inset 0 0 0 1px var(--hw-edge),
inset 0 6px 14px rgba(0,0,0,0.5);
overflow: hidden;
}
#manual.open { display: flex; }
.manual-head {
display: flex; align-items: center; gap: 12px;
padding: 10px 14px 8px;
border-bottom: 1px solid rgba(255,220,180,0.06);
flex: 0 0 auto;
}
.manual-head .title {
color: var(--hw-amber); font-size: 11px; letter-spacing: 0.22em;
text-transform: uppercase;
text-shadow: 0 0 5px var(--hw-amber-glow);
}
.manual-head .sub {
color: var(--hw-engrave); font-size: 9px; letter-spacing: 0.18em;
text-transform: uppercase; margin-left: auto;
}
.manual-close {
background: linear-gradient(180deg, #2c261f 0%, #14110d 100%);
border: 1px solid var(--hw-edge);
color: var(--hw-fg-hi); cursor: pointer;
width: 22px; height: 22px; border-radius: 3px;
font-family: inherit; font-size: 14px; line-height: 1;
box-shadow: inset 0 1px 0 rgba(255,220,180,0.10),
inset 0 -1px 0 rgba(0,0,0,0.55),
0 1px 1px rgba(0,0,0,0.4);
display: inline-flex; align-items: center; justify-content: center;
transition: color var(--t-fast);
}
.manual-close:hover { color: var(--hw-amber); }
.manual-body {
flex: 1; min-height: 0; overflow: auto;
padding: 14px 22px 24px;
color: var(--hw-fg);
font-size: 12px; line-height: 1.6;
max-width: 880px; margin: 0 auto;
}
.manual-body h2 {
color: var(--hw-amber);
font-size: 11px; letter-spacing: 0.20em;
text-transform: uppercase;
margin: 22px 0 8px;
padding-bottom: 4px;
border-bottom: 1px solid rgba(232, 160, 80, 0.22);
text-shadow: 0 0 4px var(--hw-amber-glow);
}
.manual-body h2:first-child { margin-top: 0; }
.manual-body h3 {
color: var(--hw-fg-hi); font-size: 11px;
letter-spacing: 0.10em; text-transform: uppercase;
margin: 14px 0 4px;
}
.manual-body p { margin: 6px 0 10px; color: var(--hw-fg); }
.manual-body code {
color: var(--hw-syn-num);
background: rgba(0,0,0,0.40);
padding: 1px 5px; border-radius: 2px;
font-size: 11px;
box-shadow: inset 0 1px 1px rgba(0,0,0,0.4);
}
.manual-body pre {
margin: 8px 0 14px;
background: linear-gradient(180deg, #0a0805 0%, #060403 100%);
border-radius: 3px;
padding: 10px 12px;
color: var(--hw-fg);
font-size: 11px; line-height: 1.55;
overflow-x: auto;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.85),
inset 0 0 0 1px var(--hw-edge);
position: relative;
}
.manual-body pre .syn-com { color: var(--hw-syn-com); font-style: italic; }
.manual-body pre .syn-kw { color: var(--hw-syn-kw); }
.manual-body pre .syn-num { color: var(--hw-syn-num); }
.manual-body pre .syn-fn { color: var(--hw-syn-fn); }
.manual-body pre .syn-id { color: var(--hw-syn-id); }
.manual-body pre .syn-op { color: var(--hw-syn-op); }
.manual-body pre .syn-arrow { color: var(--hw-amber); }
.manual-body pre .try-btn {
position: absolute; top: 6px; right: 6px;
background: linear-gradient(180deg, #2c261f 0%, #14110d 100%);
color: var(--hw-fg-dim); border: 1px solid var(--hw-edge);
padding: 2px 8px; border-radius: 2px;
font-family: inherit; font-size: 9px;
letter-spacing: 0.12em; text-transform: uppercase;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255,220,180,0.08),
inset 0 -1px 0 rgba(0,0,0,0.55);
transition: color var(--t-fast);
}
.manual-body pre .try-btn:hover { color: var(--hw-amber); }
.manual-body table {
border-collapse: collapse; margin: 8px 0 14px;
font-size: 11px; width: 100%;
}
.manual-body th, .manual-body td {
border-bottom: 1px solid rgba(255,220,180,0.06);
padding: 5px 8px; text-align: left; vertical-align: top;
}
.manual-body th {
color: var(--hw-engrave);
font-weight: normal; letter-spacing: 0.12em;
text-transform: uppercase; font-size: 9px;
}
.manual-body td code { font-size: 10px; }
.manual-body ul { margin: 4px 0 10px; padding-left: 18px; }
.manual-body li { margin: 2px 0; }
.manual-body em { color: var(--hw-amber); font-style: normal; }
.manual-body a { color: var(--hw-amber); text-decoration: none;
border-bottom: 1px dashed rgba(232,160,80,0.4); }
.manual-body a:hover { border-bottom-style: solid; }
.manual-toc {
display: flex; flex-wrap: wrap; gap: 4px 14px;
margin: 0 0 18px; padding: 8px 12px;
background: rgba(0,0,0,0.35);
border-radius: 3px;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.7);
font-size: 10px; letter-spacing: 0.10em;
}
.manual-toc a { color: var(--hw-fg-dim); border-bottom: none;
text-transform: uppercase; }
.manual-toc a:hover { color: var(--hw-amber); }
</style> </style>
</head> </head>
<body> <body>
@@ -555,6 +701,7 @@
<div class="top-bar"> <div class="top-bar">
<div class="btn-group"> <div class="btn-group">
<button class="hw-btn" id="start"><span class="ico-play"></span><span id="start-label">RUN</span></button> <button class="hw-btn" id="start"><span class="ico-play"></span><span id="start-label">RUN</span></button>
<button class="hw-btn" id="manual-toggle" title="Manual (?)">MANUAL</button>
</div> </div>
<span><span id="dot" class="dot"></span><span id="status">stopped</span></span> <span><span id="dot" class="dot"></span><span id="status">stopped</span></span>
<span class="header-sep"></span> <span class="header-sep"></span>
@@ -576,6 +723,251 @@
<div class="bottom-bar"> <div class="bottom-bar">
<span class="running-label idle" id="running"><span class="running-dot"></span><span id="running-text">IDLE</span><span class="blink">_</span></span> <span class="running-label idle" id="running"><span class="running-dot"></span><span id="running-text">IDLE</span><span class="blink">_</span></span>
</div> </div>
<!-- ===================== manual overlay ===================== -->
<section id="manual" aria-hidden="true">
<div class="manual-head">
<span class="title">SERVICE MANUAL</span>
<span class="sub">code · sinth · v1</span>
<button class="manual-close" id="manual-close" title="Close (Esc)">×</button>
</div>
<div class="manual-body">
<p>code-sinth es un sintetizador modular gobernado por un pequeño lenguaje
de patches. El editor de la izquierda es la fuente de verdad: cada
línea <code>node X = ...</code> declara un nodo del grafo, y
<code>out &lt;- ...</code> elige qué señal sale por los altavoces.
Cualquier cambio en el editor se recarga en caliente preservando el
estado interno (fases de osciladores, posición de secuenciadores,
envolventes en vuelo).</p>
<div class="manual-toc">
<a href="#manual-language">Lenguaje</a>
<a href="#manual-flow">Cómo se ejecuta</a>
<a href="#manual-osc">Osciladores</a>
<a href="#manual-envelopes">ADSR / Trig</a>
<a href="#manual-filter">Filter</a>
<a href="#manual-noise">Noise</a>
<a href="#manual-seq">Seq</a>
<a href="#manual-delay">Delay</a>
<a href="#manual-poly">Poly · Voices</a>
<a href="#manual-controls">Knobs · Faders · Pads</a>
<a href="#manual-tips">Consejos</a>
</div>
<h2 id="manual-language">Lenguaje del patch</h2>
<p>El patch es texto plano. Las construcciones son cuatro:</p>
<ul>
<li><code>node nombre = expresión</code> — declara un nodo con nombre.
Puedes referirlo por su nombre en cualquier expresión posterior.</li>
<li><code>out &lt;- expresión</code> — la señal que se manda al
<em>output</em>. Debe haber exactamente una.</li>
<li><code>voice nombre { ... }</code> — bloque que define una
<em>plantilla de voz</em> reutilizable. Dentro tiene su propio
<code>node</code> y un <code>out &lt;-</code> propio. Se instancia
desde <code>poly(voice=nombre, voices=N, ...)</code>.</li>
<li><code># comentario</code> — hasta fin de línea.</li>
</ul>
<p>Las expresiones admiten números (<code>0.5</code>, <code>440</code>),
identificadores, <code>+ - * /</code>, paréntesis, listas
<code>[1, 0, 1, 0]</code> y llamadas a funciones-nodo. Los argumentos
pueden ser posicionales o con nombre: <code>osc(saw, freq=220)</code>.
Las señales se mezclan sumándolas: <code>out &lt;- a*0.5 + b*0.3</code>.
Multiplicar por una envolvente actúa como amplificador controlado:
<code>o1 * env</code>.</p>
<h2 id="manual-flow">Cómo se ejecuta</h2>
<p>El motor corre en un <em>AudioWorklet</em> y procesa bloques de 128
muestras a la sample-rate del navegador (típicamente 48 kHz). En cada
bloque se evalúa el grafo en orden topológico: cada nodo lee los
buffers de sus entradas, produce su buffer de salida, y al final el
buffer de <code>out</code> sale por los altavoces.</p>
<p>Pulsa <em>RUN</em> para arrancar el audio (los navegadores requieren
un click del usuario antes de abrir el AudioContext). El indicador
<em>RUNNING_</em> abajo se enciende cuando el motor está procesando.</p>
<h2 id="manual-osc">Osciladores · <code>osc</code></h2>
<p>Genera una forma de onda continua a la frecuencia indicada.
<code>freq</code> puede ser un número fijo o cualquier señal
(otro nodo) — eso da modulación.</p>
<pre data-snippet="node lfo = osc(sine, freq=4)
node pit = 220 + lfo*30
node main = osc(saw, freq=pit)
out &lt;- main*0.4"><span class="syn-kw">node</span> <span class="syn-id">lfo</span> <span class="syn-op">=</span> <span class="syn-fn">osc</span><span class="syn-op">(</span><span class="syn-id">sine</span><span class="syn-op">,</span> <span class="syn-id">freq</span><span class="syn-op">=</span><span class="syn-num">4</span><span class="syn-op">)</span> <span class="syn-com"># 4 Hz</span>
<span class="syn-kw">node</span> <span class="syn-id">pit</span> <span class="syn-op">=</span> <span class="syn-num">220</span> <span class="syn-op">+</span> <span class="syn-id">lfo</span><span class="syn-op">*</span><span class="syn-num">30</span> <span class="syn-com"># vibrato +/-30 Hz</span>
<span class="syn-kw">node</span> <span class="syn-id">main</span> <span class="syn-op">=</span> <span class="syn-fn">osc</span><span class="syn-op">(</span><span class="syn-id">saw</span><span class="syn-op">,</span> <span class="syn-id">freq</span><span class="syn-op">=</span><span class="syn-id">pit</span><span class="syn-op">)</span>
<span class="syn-kw">out</span> <span class="syn-arrow">&lt;-</span> <span class="syn-id">main</span><span class="syn-op">*</span><span class="syn-num">0.4</span></pre>
<table>
<tr><th>Forma</th><th>Carácter</th><th>Uso típico</th></tr>
<tr><td><code>sine</code></td><td>limpio, sin armónicos</td><td>LFO, sub-bass, pads</td></tr>
<tr><td><code>saw</code></td><td>brillante, todos los armónicos</td><td>leads, bajos, strings</td></tr>
<tr><td><code>square</code></td><td>solo armónicos impares</td><td>chiptune, bajos huecos</td></tr>
<tr><td><code>tri</code></td><td>cálido, armónicos impares decrecientes</td><td>flautas, sub-leads</td></tr>
</table>
<h2 id="manual-envelopes">Envolventes · <code>adsr</code> y <code>trig</code></h2>
<p><code>trig(period, duration)</code> emite una <em>compuerta</em>
(gate) cíclica: alta durante <code>duration</code> segundos, luego
baja, repitiéndose cada <code>period</code> segundos.</p>
<p><code>adsr(a, d, s, r, gate)</code> es la envolvente clásica.
<code>a/d/r</code> en segundos, <code>s</code> nivel 0..1.
Mientras <code>gate</code> esté en 1 sube por la fase de attack,
cae al sustain y se queda; cuando vuelve a 0 entra en release.</p>
<pre data-snippet="node g1 = trig(period=0.5, duration=0.05)
node env = adsr(a=0.005, d=0.1, s=0.0, r=0.2, gate=g1)
node tone = osc(sine, freq=440)
out &lt;- tone * env"><span class="syn-kw">node</span> <span class="syn-id">g1</span> <span class="syn-op">=</span> <span class="syn-fn">trig</span><span class="syn-op">(</span><span class="syn-id">period</span><span class="syn-op">=</span><span class="syn-num">0.5</span><span class="syn-op">,</span> <span class="syn-id">duration</span><span class="syn-op">=</span><span class="syn-num">0.05</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">env</span> <span class="syn-op">=</span> <span class="syn-fn">adsr</span><span class="syn-op">(</span><span class="syn-id">a</span><span class="syn-op">=</span><span class="syn-num">0.005</span><span class="syn-op">,</span> <span class="syn-id">d</span><span class="syn-op">=</span><span class="syn-num">0.1</span><span class="syn-op">,</span> <span class="syn-id">s</span><span class="syn-op">=</span><span class="syn-num">0.0</span><span class="syn-op">,</span> <span class="syn-id">r</span><span class="syn-op">=</span><span class="syn-num">0.2</span><span class="syn-op">,</span> <span class="syn-id">gate</span><span class="syn-op">=</span><span class="syn-id">g1</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">tone</span> <span class="syn-op">=</span> <span class="syn-fn">osc</span><span class="syn-op">(</span><span class="syn-id">sine</span><span class="syn-op">,</span> <span class="syn-id">freq</span><span class="syn-op">=</span><span class="syn-num">440</span><span class="syn-op">)</span>
<span class="syn-kw">out</span> <span class="syn-arrow">&lt;-</span> <span class="syn-id">tone</span> <span class="syn-op">*</span> <span class="syn-id">env</span></pre>
<p>Truco: para una <em>percusión</em>, pon <code>s=0</code> y
<code>d</code> corto (~0.1 s). Para un <em>pad</em>, sube
<code>a</code> a 0.52 s y deja <code>s</code> alto.</p>
<h2 id="manual-filter">Filtro · <code>filter</code></h2>
<p>Biquad RBJ, tipos <code>lp</code> (low-pass), <code>hp</code>
(high-pass) o <code>bp</code> (band-pass). <code>cutoff</code> y
<code>q</code> (resonancia) pueden modularse a tasa de audio —
sumarle una envolvente al cutoff es la receta clásica del bajo
sintetizado.</p>
<pre data-snippet="node src = osc(saw, freq=110)
node g = trig(period=0.5, duration=0.2)
node e = adsr(a=0.005, d=0.2, s=0.0, r=0.1, gate=g)
node lp = filter(lp, in=src, cutoff=300 + e*2500, q=4)
out &lt;- lp * e"><span class="syn-kw">node</span> <span class="syn-id">src</span> <span class="syn-op">=</span> <span class="syn-fn">osc</span><span class="syn-op">(</span><span class="syn-id">saw</span><span class="syn-op">,</span> <span class="syn-id">freq</span><span class="syn-op">=</span><span class="syn-num">110</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">g</span> <span class="syn-op">=</span> <span class="syn-fn">trig</span><span class="syn-op">(</span><span class="syn-id">period</span><span class="syn-op">=</span><span class="syn-num">0.5</span><span class="syn-op">,</span> <span class="syn-id">duration</span><span class="syn-op">=</span><span class="syn-num">0.2</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">e</span> <span class="syn-op">=</span> <span class="syn-fn">adsr</span><span class="syn-op">(</span><span class="syn-id">a</span><span class="syn-op">=</span><span class="syn-num">0.005</span><span class="syn-op">,</span> <span class="syn-id">d</span><span class="syn-op">=</span><span class="syn-num">0.2</span><span class="syn-op">,</span> <span class="syn-id">s</span><span class="syn-op">=</span><span class="syn-num">0</span><span class="syn-op">,</span> <span class="syn-id">r</span><span class="syn-op">=</span><span class="syn-num">0.1</span><span class="syn-op">,</span> <span class="syn-id">gate</span><span class="syn-op">=</span><span class="syn-id">g</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">lp</span> <span class="syn-op">=</span> <span class="syn-fn">filter</span><span class="syn-op">(</span><span class="syn-id">lp</span><span class="syn-op">,</span> <span class="syn-id">in</span><span class="syn-op">=</span><span class="syn-id">src</span><span class="syn-op">,</span> <span class="syn-id">cutoff</span><span class="syn-op">=</span><span class="syn-num">300</span> <span class="syn-op">+</span> <span class="syn-id">e</span><span class="syn-op">*</span><span class="syn-num">2500</span><span class="syn-op">,</span> <span class="syn-id">q</span><span class="syn-op">=</span><span class="syn-num">4</span><span class="syn-op">)</span>
<span class="syn-kw">out</span> <span class="syn-arrow">&lt;-</span> <span class="syn-id">lp</span> <span class="syn-op">*</span> <span class="syn-id">e</span></pre>
<h2 id="manual-noise">Ruido · <code>noise</code></h2>
<p>Ruido blanco en [-1, 1]. Filtrado con <code>hp</code> alto y una
envolvente cortita, da un hi-hat. Filtrado con <code>bp</code> con
cutoff bajo, una caja.</p>
<pre data-snippet="node n = noise()
node g = trig(period=0.25, duration=0.02)
node e = adsr(a=0.001, d=0.04, s=0.0, r=0.03, gate=g)
node hp = filter(hp, in=n, cutoff=5000, q=1.5)
out &lt;- hp * e * 0.4"><span class="syn-kw">node</span> <span class="syn-id">n</span> <span class="syn-op">=</span> <span class="syn-fn">noise</span><span class="syn-op">()</span>
<span class="syn-kw">node</span> <span class="syn-id">g</span> <span class="syn-op">=</span> <span class="syn-fn">trig</span><span class="syn-op">(</span><span class="syn-id">period</span><span class="syn-op">=</span><span class="syn-num">0.25</span><span class="syn-op">,</span> <span class="syn-id">duration</span><span class="syn-op">=</span><span class="syn-num">0.02</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">e</span> <span class="syn-op">=</span> <span class="syn-fn">adsr</span><span class="syn-op">(</span><span class="syn-id">a</span><span class="syn-op">=</span><span class="syn-num">0.001</span><span class="syn-op">,</span> <span class="syn-id">d</span><span class="syn-op">=</span><span class="syn-num">0.04</span><span class="syn-op">,</span> <span class="syn-id">s</span><span class="syn-op">=</span><span class="syn-num">0</span><span class="syn-op">,</span> <span class="syn-id">r</span><span class="syn-op">=</span><span class="syn-num">0.03</span><span class="syn-op">,</span> <span class="syn-id">gate</span><span class="syn-op">=</span><span class="syn-id">g</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">hp</span> <span class="syn-op">=</span> <span class="syn-fn">filter</span><span class="syn-op">(</span><span class="syn-id">hp</span><span class="syn-op">,</span> <span class="syn-id">in</span><span class="syn-op">=</span><span class="syn-id">n</span><span class="syn-op">,</span> <span class="syn-id">cutoff</span><span class="syn-op">=</span><span class="syn-num">5000</span><span class="syn-op">,</span> <span class="syn-id">q</span><span class="syn-op">=</span><span class="syn-num">1.5</span><span class="syn-op">)</span>
<span class="syn-kw">out</span> <span class="syn-arrow">&lt;-</span> <span class="syn-id">hp</span> <span class="syn-op">*</span> <span class="syn-id">e</span> <span class="syn-op">*</span> <span class="syn-num">0.4</span></pre>
<h2 id="manual-seq">Secuenciador básico · <code>seq</code></h2>
<p>Recorre una lista de valores a <code>rate</code> pasos por segundo,
manteniendo cada valor hasta el siguiente. Útil para listas de
frecuencias o de niveles de control.</p>
<pre data-snippet="node freqs = seq(rate=4, steps=[220, 277, 330, 220])
node tone = osc(saw, freq=freqs)
node g = trig(period=0.25, duration=0.18)
node e = adsr(a=0.005, d=0.1, s=0.6, r=0.2, gate=g)
out &lt;- tone * e * 0.4"><span class="syn-kw">node</span> <span class="syn-id">freqs</span> <span class="syn-op">=</span> <span class="syn-fn">seq</span><span class="syn-op">(</span><span class="syn-id">rate</span><span class="syn-op">=</span><span class="syn-num">4</span><span class="syn-op">,</span> <span class="syn-id">steps</span><span class="syn-op">=[</span><span class="syn-num">220</span><span class="syn-op">,</span> <span class="syn-num">277</span><span class="syn-op">,</span> <span class="syn-num">330</span><span class="syn-op">,</span> <span class="syn-num">220</span><span class="syn-op">])</span>
<span class="syn-kw">node</span> <span class="syn-id">tone</span> <span class="syn-op">=</span> <span class="syn-fn">osc</span><span class="syn-op">(</span><span class="syn-id">saw</span><span class="syn-op">,</span> <span class="syn-id">freq</span><span class="syn-op">=</span><span class="syn-id">freqs</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">g</span> <span class="syn-op">=</span> <span class="syn-fn">trig</span><span class="syn-op">(</span><span class="syn-id">period</span><span class="syn-op">=</span><span class="syn-num">0.25</span><span class="syn-op">,</span> <span class="syn-id">duration</span><span class="syn-op">=</span><span class="syn-num">0.18</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">e</span> <span class="syn-op">=</span> <span class="syn-fn">adsr</span><span class="syn-op">(</span><span class="syn-id">a</span><span class="syn-op">=</span><span class="syn-num">0.005</span><span class="syn-op">,</span> <span class="syn-id">d</span><span class="syn-op">=</span><span class="syn-num">0.1</span><span class="syn-op">,</span> <span class="syn-id">s</span><span class="syn-op">=</span><span class="syn-num">0.6</span><span class="syn-op">,</span> <span class="syn-id">r</span><span class="syn-op">=</span><span class="syn-num">0.2</span><span class="syn-op">,</span> <span class="syn-id">gate</span><span class="syn-op">=</span><span class="syn-id">g</span><span class="syn-op">)</span>
<span class="syn-kw">out</span> <span class="syn-arrow">&lt;-</span> <span class="syn-id">tone</span> <span class="syn-op">*</span> <span class="syn-id">e</span> <span class="syn-op">*</span> <span class="syn-num">0.4</span></pre>
<h2 id="manual-delay">Delay · <code>delay</code></h2>
<p>Línea de delay con feedback. <code>time</code> en segundos,
<code>feedback</code> 0..0.99, <code>mix</code> 0..1
(0 = solo seco, 1 = solo retardado).</p>
<pre data-snippet="node src = osc(tri, freq=330)
node g = trig(period=1.0, duration=0.05)
node e = adsr(a=0.005, d=0.2, s=0, r=0.1, gate=g)
node dry = src * e
node dl = delay(in=dry, time=0.375, feedback=0.55, mix=0.5)
out &lt;- dl * 0.5"><span class="syn-kw">node</span> <span class="syn-id">src</span> <span class="syn-op">=</span> <span class="syn-fn">osc</span><span class="syn-op">(</span><span class="syn-id">tri</span><span class="syn-op">,</span> <span class="syn-id">freq</span><span class="syn-op">=</span><span class="syn-num">330</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">g</span> <span class="syn-op">=</span> <span class="syn-fn">trig</span><span class="syn-op">(</span><span class="syn-id">period</span><span class="syn-op">=</span><span class="syn-num">1.0</span><span class="syn-op">,</span> <span class="syn-id">duration</span><span class="syn-op">=</span><span class="syn-num">0.05</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">e</span> <span class="syn-op">=</span> <span class="syn-fn">adsr</span><span class="syn-op">(</span><span class="syn-id">a</span><span class="syn-op">=</span><span class="syn-num">0.005</span><span class="syn-op">,</span> <span class="syn-id">d</span><span class="syn-op">=</span><span class="syn-num">0.2</span><span class="syn-op">,</span> <span class="syn-id">s</span><span class="syn-op">=</span><span class="syn-num">0</span><span class="syn-op">,</span> <span class="syn-id">r</span><span class="syn-op">=</span><span class="syn-num">0.1</span><span class="syn-op">,</span> <span class="syn-id">gate</span><span class="syn-op">=</span><span class="syn-id">g</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">dry</span> <span class="syn-op">=</span> <span class="syn-id">src</span> <span class="syn-op">*</span> <span class="syn-id">e</span>
<span class="syn-kw">node</span> <span class="syn-id">dl</span> <span class="syn-op">=</span> <span class="syn-fn">delay</span><span class="syn-op">(</span><span class="syn-id">in</span><span class="syn-op">=</span><span class="syn-id">dry</span><span class="syn-op">,</span> <span class="syn-id">time</span><span class="syn-op">=</span><span class="syn-num">0.375</span><span class="syn-op">,</span> <span class="syn-id">feedback</span><span class="syn-op">=</span><span class="syn-num">0.55</span><span class="syn-op">,</span> <span class="syn-id">mix</span><span class="syn-op">=</span><span class="syn-num">0.5</span><span class="syn-op">)</span>
<span class="syn-kw">out</span> <span class="syn-arrow">&lt;-</span> <span class="syn-id">dl</span> <span class="syn-op">*</span> <span class="syn-num">0.5</span></pre>
<h2 id="manual-poly">Polifonía · <code>voice</code> + <code>poly</code></h2>
<p>Define una <em>plantilla de voz</em> con un bloque
<code>voice nombre { ... }</code>: dentro tendrás dos identificadores
especiales — <code>freq</code> (la frecuencia que el allocator
asigna a la voz) y <code>gate</code> (la compuerta que se abre
durante <code>gate_duration</code> segundos por cada nota).</p>
<p>Luego instancia con <code>poly(voice=nombre, voices=N, rate, notes,
gate_duration)</code>. <code>notes</code> es una lista de frecuencias;
<code>0</code> es silencio. El allocator asigna la voz menos reciente
(LRU) a cada nota nueva.</p>
<pre data-snippet="voice synth {
node o = osc(saw, freq=freq)
node e = adsr(a=0.005, d=0.2, s=0.4, r=0.3, gate=gate)
node f = filter(lp, in=o, cutoff=600 + e*1800, q=2.0)
out &lt;- f * e
}
node mel = poly(voice=synth, voices=4, rate=4, gate_duration=0.18,
notes=[220, 277, 330, 0, 220, 330, 277, 0])
out &lt;- mel * 0.4"><span class="syn-kw">voice</span> <span class="syn-id">synth</span> <span class="syn-op">{</span>
<span class="syn-kw">node</span> <span class="syn-id">o</span> <span class="syn-op">=</span> <span class="syn-fn">osc</span><span class="syn-op">(</span><span class="syn-id">saw</span><span class="syn-op">,</span> <span class="syn-id">freq</span><span class="syn-op">=</span><span class="syn-id">freq</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">e</span> <span class="syn-op">=</span> <span class="syn-fn">adsr</span><span class="syn-op">(</span><span class="syn-id">a</span><span class="syn-op">=</span><span class="syn-num">0.005</span><span class="syn-op">,</span> <span class="syn-id">d</span><span class="syn-op">=</span><span class="syn-num">0.2</span><span class="syn-op">,</span> <span class="syn-id">s</span><span class="syn-op">=</span><span class="syn-num">0.4</span><span class="syn-op">,</span> <span class="syn-id">r</span><span class="syn-op">=</span><span class="syn-num">0.3</span><span class="syn-op">,</span> <span class="syn-id">gate</span><span class="syn-op">=</span><span class="syn-id">gate</span><span class="syn-op">)</span>
<span class="syn-kw">node</span> <span class="syn-id">f</span> <span class="syn-op">=</span> <span class="syn-fn">filter</span><span class="syn-op">(</span><span class="syn-id">lp</span><span class="syn-op">,</span> <span class="syn-id">in</span><span class="syn-op">=</span><span class="syn-id">o</span><span class="syn-op">,</span> <span class="syn-id">cutoff</span><span class="syn-op">=</span><span class="syn-num">600</span> <span class="syn-op">+</span> <span class="syn-id">e</span><span class="syn-op">*</span><span class="syn-num">1800</span><span class="syn-op">,</span> <span class="syn-id">q</span><span class="syn-op">=</span><span class="syn-num">2.0</span><span class="syn-op">)</span>
<span class="syn-kw">out</span> <span class="syn-arrow">&lt;-</span> <span class="syn-id">f</span> <span class="syn-op">*</span> <span class="syn-id">e</span>
<span class="syn-op">}</span>
<span class="syn-kw">node</span> <span class="syn-id">mel</span> <span class="syn-op">=</span> <span class="syn-fn">poly</span><span class="syn-op">(</span><span class="syn-id">voice</span><span class="syn-op">=</span><span class="syn-id">synth</span><span class="syn-op">,</span> <span class="syn-id">voices</span><span class="syn-op">=</span><span class="syn-num">4</span><span class="syn-op">,</span> <span class="syn-id">rate</span><span class="syn-op">=</span><span class="syn-num">4</span><span class="syn-op">,</span> <span class="syn-id">gate_duration</span><span class="syn-op">=</span><span class="syn-num">0.18</span><span class="syn-op">,</span>
<span class="syn-id">notes</span><span class="syn-op">=[</span><span class="syn-num">220</span><span class="syn-op">,</span> <span class="syn-num">277</span><span class="syn-op">,</span> <span class="syn-num">330</span><span class="syn-op">,</span> <span class="syn-num">0</span><span class="syn-op">,</span> <span class="syn-num">220</span><span class="syn-op">,</span> <span class="syn-num">330</span><span class="syn-op">,</span> <span class="syn-num">277</span><span class="syn-op">,</span> <span class="syn-num">0</span><span class="syn-op">])</span>
<span class="syn-kw">out</span> <span class="syn-arrow">&lt;-</span> <span class="syn-id">mel</span> <span class="syn-op">*</span> <span class="syn-num">0.4</span></pre>
<h2 id="manual-controls">Knobs · faders · pads</h2>
<p>Hay cuatro tipos de nodo cuyo valor lo decide el panel derecho en
vez del código. Al declararlos aparece un widget físico al que
puedes interactuar con el ratón.</p>
<h3>Knob</h3>
<pre data-snippet="node cutoff = knob(min=200, max=4000, default=900)"><span class="syn-kw">node</span> <span class="syn-id">cutoff</span> <span class="syn-op">=</span> <span class="syn-fn">knob</span><span class="syn-op">(</span><span class="syn-id">min</span><span class="syn-op">=</span><span class="syn-num">200</span><span class="syn-op">,</span> <span class="syn-id">max</span><span class="syn-op">=</span><span class="syn-num">4000</span><span class="syn-op">,</span> <span class="syn-id">default</span><span class="syn-op">=</span><span class="syn-num">900</span><span class="syn-op">)</span></pre>
<p>Arrastra arriba/abajo para girarlo, doble-click para volver al
centro, <em>Shift</em> + arrastrar para ajuste fino.</p>
<h3>Fader</h3>
<pre data-snippet="node mix = fader(min=0, max=1, default=0.5)"><span class="syn-kw">node</span> <span class="syn-id">mix</span> <span class="syn-op">=</span> <span class="syn-fn">fader</span><span class="syn-op">(</span><span class="syn-id">min</span><span class="syn-op">=</span><span class="syn-num">0</span><span class="syn-op">,</span> <span class="syn-id">max</span><span class="syn-op">=</span><span class="syn-num">1</span><span class="syn-op">,</span> <span class="syn-id">default</span><span class="syn-op">=</span><span class="syn-num">0.5</span><span class="syn-op">)</span></pre>
<h3>Step sequencer</h3>
<pre data-snippet="node kick = step_seq(rate=8, steps=16,
default=[1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0])"><span class="syn-kw">node</span> <span class="syn-id">kick</span> <span class="syn-op">=</span> <span class="syn-fn">step_seq</span><span class="syn-op">(</span><span class="syn-id">rate</span><span class="syn-op">=</span><span class="syn-num">8</span><span class="syn-op">,</span> <span class="syn-id">steps</span><span class="syn-op">=</span><span class="syn-num">16</span><span class="syn-op">,</span>
<span class="syn-id">default</span><span class="syn-op">=[</span><span class="syn-num">1</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span> <span class="syn-num">1</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span> <span class="syn-num">1</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span> <span class="syn-num">1</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">,</span><span class="syn-num">0</span><span class="syn-op">])</span></pre>
<p>Click en una celda para encender/apagar; arrastra para pintar
muchas a la vez. La salida es 0/1, perfecta como
<code>gate=</code> de un ADSR.</p>
<h3>Piano roll</h3>
<pre data-snippet="node mel = piano_roll(voice=synth, voices=4,
rate=8, length=16, octaves=2, base=220,
gate_duration=0.18)"><span class="syn-kw">node</span> <span class="syn-id">mel</span> <span class="syn-op">=</span> <span class="syn-fn">piano_roll</span><span class="syn-op">(</span><span class="syn-id">voice</span><span class="syn-op">=</span><span class="syn-id">synth</span><span class="syn-op">,</span> <span class="syn-id">voices</span><span class="syn-op">=</span><span class="syn-num">4</span><span class="syn-op">,</span>
<span class="syn-id">rate</span><span class="syn-op">=</span><span class="syn-num">8</span><span class="syn-op">,</span> <span class="syn-id">length</span><span class="syn-op">=</span><span class="syn-num">16</span><span class="syn-op">,</span> <span class="syn-id">octaves</span><span class="syn-op">=</span><span class="syn-num">2</span><span class="syn-op">,</span> <span class="syn-id">base</span><span class="syn-op">=</span><span class="syn-num">220</span><span class="syn-op">,</span>
<span class="syn-id">gate_duration</span><span class="syn-op">=</span><span class="syn-num">0.18</span><span class="syn-op">)</span></pre>
<p>Es un <code>poly()</code> con las notas dispuestas en una rejilla
de pasos × semitonos. Click en cualquier celda para colocar una
nota. La columna 0 es <code>base</code> Hz, cada fila es un
semitono más arriba. Necesita un <code>voice</code> previo, igual
que <code>poly</code>.</p>
<h2 id="manual-tips">Consejos</h2>
<ul>
<li><em>Empieza bajo.</em> El gain del header es 0.30 por algo:
sumar varias señales sin escalar satura rápido.</li>
<li><em>Multiplica por la envolvente al final.</em>
<code>filter(...) * env</code> evita clics al inicio/final
mejor que poner la envolvente solo en el cutoff.</li>
<li><em>Hot-reload preserva el estado.</em> Los osciladores no
saltan de fase, los secuenciadores no resetean. Borra y reescribe
el nodo si quieres reiniciarlo.</li>
<li><em>El waveform inline después de cada nodo</em> es lo que está
saliendo realmente del grafo en ese punto. Útil para ver dónde
se está rompiendo el sonido.</li>
<li><em>Para mezclar varias fuentes</em>, súmalas con
<code>+</code> y multiplica el total por un factor pequeño:
<code>out &lt;- bass*0.6 + drum*0.5 + lead*0.4</code>.</li>
</ul>
<p style="color: var(--hw-engrave); font-size: 10px; margin-top: 28px;
letter-spacing: 0.18em; text-align: center; text-transform: uppercase;">
— fin del manual —
</p>
</div>
</section>
</div> </div>
<script type="module"> <script type="module">
@@ -1506,6 +1898,43 @@ requestAnimationFrame(tick);
try { localStorage.removeItem(KEY); } catch {} try { localStorage.removeItem(KEY); } catch {}
}); });
} }
// =====================================================================
// manual — in-app docs overlay. Toggle button in top bar, ESC closes.
// Each <pre data-snippet="..."> in the manual gets a TRY button that
// replaces the editor doc with that snippet (Ctrl/Cmd-Z to undo).
// =====================================================================
{
const manual = document.getElementById('manual');
const toggle = document.getElementById('manual-toggle');
const closeBtn = document.getElementById('manual-close');
function open() { manual.classList.add('open'); manual.setAttribute('aria-hidden', 'false'); }
function close() { manual.classList.remove('open'); manual.setAttribute('aria-hidden', 'true'); }
toggle.addEventListener('click', () => {
if (manual.classList.contains('open')) close(); else open();
});
closeBtn.addEventListener('click', close);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && manual.classList.contains('open')) close();
});
// Inject TRY buttons on every snippet block.
for (const pre of manual.querySelectorAll('pre[data-snippet]')) {
const btn = document.createElement('button');
btn.className = 'try-btn';
btn.textContent = 'TRY';
btn.title = 'Replace the editor with this snippet (Ctrl/Cmd-Z to undo)';
btn.addEventListener('click', () => {
const snippet = pre.getAttribute('data-snippet');
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: snippet + '\n' },
});
close();
view.focus();
});
pre.appendChild(btn);
}
}
</script> </script>
</body> </body>
</html> </html>