SNK-Master-IA / index.html
Snak80's picture
Upload index.html
0c80cf5 verified
Raw
History Blame Contribute Delete
55.8 kB
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SNK MASTER IA - Agente IA de Mastering</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: radial-gradient(1200px 700px at 70% -10%, #161a2e 0%, #0b0e1a 55%, #070910 100%); background-attachment: fixed; color: #dfe3f5; font-family: 'Segoe UI', system-ui, sans-serif; min-height: 100vh; display: flex; justify-content: center; }
.wrap { width: 1140px; max-width: 100%; padding: 18px 14px 40px; }
header { position: relative; display: flex; align-items: center; justify-content: center; margin-bottom: 16px; flex-wrap: wrap; gap: 12px; padding: 8px 0; }
.titulo { text-align: center; }
.vers { position: absolute; right: 0; top: 10px; font-size: 12px; color: #aab4ff; border: 1px solid #3d4a86; background: rgba(45, 55, 120, .25); padding: 3px 10px; border-radius: 20px; letter-spacing: 1px; box-shadow: 0 0 12px rgba(102, 126, 255, .18); white-space: nowrap; }
@media (max-width: 520px) { .vers { position: static; } }
h1 { font-size: 26px; letter-spacing: 1px; background: linear-gradient(90deg,#6ef0c8,#58c7ff,#a78bfa); -webkit-background-clip: text; background-clip: text; color: transparent; text-shadow: 0 0 26px rgba(110, 240, 200, .22); }
h1 small { display: block; font-size: 11px; color: #8892b4; letter-spacing: 2px; margin-top: 2px; }
.panel { background: linear-gradient(180deg, #151a2e 0%, #111526 100%); border: 1px solid #2a3152; border-radius: 14px; padding: 14px; margin-bottom: 14px; box-shadow: 0 6px 22px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(140, 160, 255, .05); }
.panel h2 { font-size: 13px; letter-spacing: 2px; color: #67c6ff; margin-bottom: 12px; text-transform: uppercase; }
.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; justify-content: center; }
button { background: #1d2440; color: #e7ebff; border: 1px solid #343d66; border-radius: 9px; padding: 9px 14px; cursor: pointer; font-size: 13px; transition: .18s; }
button:hover { background: #262f56; border-color: #67c6ff; box-shadow: 0 0 14px rgba(103, 198, 255, .28); }
button.acc { background: linear-gradient(180deg, #14b06a, #0d8f58); border-color: #2fd48f; }
button.acc:hover { background: linear-gradient(180deg, #1cc77a, #12a365); box-shadow: 0 0 16px rgba(47, 212, 143, .4); }
button.warn { background: linear-gradient(180deg, #c26a2f, #a34e22); border-color: #e0855a; }
button.warn:hover { background: linear-gradient(180deg, #d97a3a, #b85c2b); box-shadow: 0 0 16px rgba(224, 133, 90, .4); }
button:disabled { opacity: .38; cursor: not-allowed; box-shadow: none; }
button.active { background: linear-gradient(180deg, #4db8ff, #3d9ae0); border-color: #7fd6ff; color: #071018; font-weight: 600; box-shadow: 0 0 16px rgba(90, 200, 255, .4); }
button.active:hover { background: linear-gradient(180deg, #5fc4ff, #48a8ee); }
input[type=file] { display: none; }
.hint { font-size: 12px; color: #8f99bd; line-height: 1.6; }
.bandanze { display: flex; justify-content: space-between; align-items: stretch; gap: 6px; height: 200px; }
.band { flex: 1; text-align: center; height: 100%; display: flex; flex-direction: column; align-items: center; }
.band input[type=range] { -webkit-appearance: none; appearance: none; writing-mode: vertical-lr; direction: rtl; width: 26px; height: 145px; margin: 6px auto 2px; display: block; background: transparent; cursor: pointer; }
.band input[type=range]::-webkit-slider-runnable-track { width: 8px; background: #222a4a; border-radius: 4px; }
.band input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 22px; height: 8px; background: #67c6ff; border-radius: 4px; margin-left: -7px; box-shadow: 0 0 10px rgba(103, 198, 255, .55); }
.band .freq { font-size: 10px; color: #8f99bd; display: block; }
.band .val { font-size: 10px; color: #6ef0c8; display: block; height: 13px; }
#curva { width: 100%; height: 120px; margin-top: 10px; background: #0c0f1c; border: 1px solid #2a3152; border-radius: 9px; display: block; }
.compgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
.knob { text-align: center; }
.knob label { display: block; font-size: 11px; color: #8f99bd; margin-bottom: 4px; letter-spacing: 1px; }
.knob .out { font-size: 12px; color: #6ef0c8; margin-top: 4px; display: block; }
.knob input[type=range] { width: 100%; accent-color: #67c6ff; cursor: pointer; }
#pbarWrap { width: 100%; height: 18px; background: #0c0f1c; border: 1px solid #2a3152; border-radius: 6px; margin-top: 12px; overflow: hidden; }
#pbarFill { height: 100%; width: 0%; background: linear-gradient(90deg,#58c7ff,#6ef0c8); transition: width .1s linear; box-shadow: 0 0 12px rgba(94, 214, 210, .45); }
.viz { display: flex; gap: 12px; flex-wrap: wrap; }
canvas { background: #0c0f1c; border: 1px solid #2a3152; border-radius: 9px; }
canvas#spec { flex: 2; min-width: 300px; height: 140px; }
canvas#wave { flex: 1; min-width: 210px; height: 140px; }
.doscol { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 820px) { .doscol { grid-template-columns: 1fr; } }
#iaseg { font-size: 13px; line-height: 1.8; color: #ccd3ea; max-height: 360px; overflow-y: auto; }
#iaseg b { color: #6ef0c8; }
.estadistica { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: #98a2c4; margin-top: 8px; }
.estadistica b { color: #dfe3f5; }
footer { text-align: center; font-size: 11px; color: #59607c; margin-top: 8px; }
#agenteChat { height: 175px; overflow-y: auto; background: #0e1222; border: 1px solid #2a3152; border-radius: 9px; padding: 10px; font-size: 12px; line-height: 1.6; margin-top: 10px; }
.chatrow { display: flex; gap: 8px; margin-top: 8px; }
#agenteIn { flex: 1; background: #0c0f1c; border: 1px solid #343d66; color: #dfe3f5; border-radius: 7px; padding: 9px 12px; font-size: 13px; outline: none; }
#agenteIn:focus { border-color: #67c6ff; box-shadow: 0 0 12px rgba(103, 198, 255, .25); }
#agenteChat .m-user { color: #67c6ff; font-weight: 600; }
#agenteChat .m-ia { color: #8ce8c0; font-weight: 600; }
#agenteChat .chatmsg { margin-bottom: 6px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<span class="vers">Version: Beta 0.1v</span>
<div class="titulo">
<h1>SNK MASTER IA <small>INGENIERO DE AUDIO IA · RESTAURACIÓN DIGITAL DE CANCIONES</small></h1>
</div>
<div class="row">
<button id="btnCargar" class="acc">Cargar canción</button>
<button id="btnPlay" disabled>▶ Reproducir</button>
<button id="btnA" disabled>A · original</button>
<button id="btnB" disabled>B · master</button>
<button id="btnMaster" class="warn" disabled>Masterizar con IA</button>
<button id="btnSave" disabled>Descargar master</button>
<button id="btnReset" title="Limpiar todo y volver a empezar">Limpiar todo</button>
</div>
</header>
<div class="panel">
<h2>Etapas 1-2 · Limpieza &amp; Vocal Repair</h2>
<div class="compgrid">
<div class="knob"><label>DE-ESSER 5-8k</label><input type="range" id="deEss" min="0" max="100" step="1" value="50"><span class="out" id="deEssO">50%</span></div>
<div class="knob"><label>HPF RUMBLE</label><input type="range" id="hpfV" min="0" max="100" step="1" value="55"><span class="out" id="hpfVO">55%</span></div>
<div class="knob"><label>EQ DYN 300-500</label><input type="range" id="mudV" min="0" max="100" step="1" value="45"><span class="out" id="mudVO">45%</span></div>
<div class="knob"><label>DE-NOISE</label><input type="range" id="denoise" min="0" max="100" step="1" value="45"><span class="out" id="denoiseO">45%</span></div>
</div>
</div>
<div class="panel">
<h2>Etapas 3-4 · Rango dinámico, frecuencias y espacio</h2>
<div class="compgrid">
<div class="knob"><label>SUB 40-60Hz</label><input type="range" id="subAdd" min="0" max="100" step="1" value="40"><span class="out" id="subAddO">40%</span></div>
<div class="knob"><label>AIRE 12kHz+</label><input type="range" id="airEx" min="0" max="100" step="1" value="35"><span class="out" id="airExO">35%</span></div>
<div class="knob"><label>EXPANSOR</label><input type="range" id="expDyn" min="0" max="100" step="1" value="30"><span class="out" id="expDynO">30%</span></div>
<div class="knob"><label>LIMPIEZA MID-LOW</label><input type="range" id="midLow" min="0" max="100" step="1" value="30"><span class="out" id="midLowO">30%</span></div>
<div class="knob"><label>REVERB (Wet 0%)</label><input type="range" id="roomK" min="0" max="100" step="1" value="0"><span class="out" id="roomKO">0%</span></div>
</div>
</div>
<div class="panel">
<h2>Etapa 5 · Master final de estudio</h2>
<div class="compgrid">
<div class="knob"><label>VOLUMEN</label><input type="range" id="volT" min="0" max="100" step="1" value="50"><span class="out" id="volTO">0.0 dB</span></div>
<div class="knob"><label>PANEO</label><input type="range" id="panT" min="0" max="100" step="1" value="50"><span class="out" id="panTO">C</span></div>
<div class="knob"><label>GLUE BUS 2:1</label><input type="range" id="glueK" min="0" max="100" step="1" value="60"><span class="out" id="glueKO">60%</span></div>
<div class="knob"><label>LUFS OBJETIVO</label><input type="range" id="lufsT" min="0" max="100" step="1" value="0"><span class="out" id="lufsTO">-14 LU</span></div>
<div class="knob"><label>TRUE PEAK</label><input type="range" id="tpT" min="0" max="100" step="1" value="0"><span class="out" id="tpTO">-1.0 dBTP</span></div>
</div>
<div id="pbarWrap"><div id="pbarFill"></div></div>
</div>
<div class="panel">
<h2>Ecualizador gráfico · 10 bandas (LS / peaking / HS)</h2>
<div class="bandanze" id="eq"></div>
<canvas id="curva" width="1130" height="120"></canvas>
</div>
<div class="panel">
<h2>Vista en tiempo real</h2>
<div class="viz">
<canvas id="spec" width="640" height="200"></canvas>
<canvas id="wave" width="430" height="200"></canvas>
</div>
</div>
<div class="row">
<div class="panel doscol" style="width:100%">
<div>
<h2>Agente IA · pipeline</h2>
<p class="hint">Análisis de piso de ruido y aplicación secuencial del pipeline: 1) limpieza (de-noise + de-clip, expansor de piso) · 2) vocal repair (de-esser 5-8k, HPF de bus sutil 20-45Hz, EQ dinámica 300-500Hz) · 3) refuerzo sub 40-60Hz + aire 12k + expansor · 4) limpieza mid-low (reverb desactivado en el master: no añade eco) · 5) bus glue 2:1 + limitador + master a -14/-10 LUFS y -1.0 dBTP.</p>
<div class="estadistica" id="iaStats"></div>
</div>
<div id="iaseg">Carga una canción y pulsa "Masterizar con IA".</div>
</div>
</div>
<div class="panel">
<h2>Hable con la IA · control visual de faders y EQ</h2>
<div class="chatrow">
<input id="agenteIn" type="text" placeholder="Ej: «sube un poco los brillos», «baja los graves 2 dB»" maxlength="120">
<button id="btnEnviar">Enviar</button>
</div>
<div id="agenteChat"><div class="chatmsg"><span class="m-ia">IA:</span> Hola, soy el ingeniero de mastering. Pídeme ajustes como <b>«sube un poco los brillos»</b> o <b>«baja los graves»</b> y moveré los faders y el EQ en pantalla.</div></div>
</div>
<div class="panel">
<h2>Presets de estudio</h2>
<div class="row">
<button data-pre="radio">Radio hit</button>
<button data-pre="clean">Limpio</button>
<button data-pre="urban">Urban</button>
<button data-pre="loud">Max loud</button>
<button data-pre="vocal">Foco vocal</button>
<button data-pre="dream">Ambient dream</button>
</div>
</div>
<footer>MASTERIA PRO v3 · motor DSP propio (IIR + Schroeder + limitador) · proceso 100% local en tu navegador</footer>
<div style="background: linear-gradient(180deg, #151a2e 0%, #111526 100%); border: 1px solid #3d4a86; padding: 20px; text-align: center; border-radius: 14px; margin: 25px auto; max-width: 600px; font-family: 'Segoe UI', sans-serif; box-shadow: 0 6px 22px rgba(0,0,0,.45), 0 0 18px rgba(102,126,255,.15);">
<h3 style="color: #6ef0c8; margin-top: 0; letter-spacing: 1px;">🤖 Programa de Máster IA (Versión Beta)</h3>
<p style="color: #ccd3ea; font-size: 15px;">Ayúdame a mejorar este programa. Si encuentras errores o fallas, por favor repórtalos en la pestaña <strong style="color:#67c6ff;">"Community"</strong> de aquí arriba en Hugging Face.</p>
<p style="margin-bottom: 0; font-size: 16px;">
☕ Este proyecto es gratis. Si deseas apoyar voluntariamente su desarrollo, puedes dejar una donación en mi
<a href="https://www.paypal.com/paypalme/snakvdj" target="_blank" style="background: linear-gradient(180deg, #4db8ff, #3d9ae0); color: #071018; padding: 8px 15px; text-decoration: none; border-radius: 20px; font-weight: bold; display: inline-block; margin-left: 5px; box-shadow: 0 0 14px rgba(90,200,255,.35);">
PayPal.Me aquí
</a>
</p>
</div>
</div>
<input type="file" id="file" accept="audio/*,.mp3,.wav,.ogg,.m4a,.flac">
<script>
(function () {
'use strict';
var $ = function (id) { return document.getElementById(id); };
var eqBox = $('eq');
var curva = $('curva'), cctx = curva.getContext('2d');
var spec = $('spec'), sctx = spec.getContext('2d');
var wave = $('wave'), wctx = wave.getContext('2d');
var iaseg = $('iaseg'), iaStats = $('iaStats'), pbar = $('pbarFill');
var BANDS = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
var BAND_COLORS = ['#67c6ff', '#6ef0c8', '#6ef0c8', '#a9e34b', '#a9e34b', '#ffd08a', '#ffd08a', '#ff9d9d', '#ff9d9d', '#a78bfa'];
var AC = window.AudioContext || window.webkitAudioContext;
var actx = null;
var audioBuffer = null, masterBuffer = null;
var analyser = null;
var source = null, startTime = 0, offsetPos = 0;
var liveGain = null, liveSplit = null, liveGainL = null, liveGainR = null, liveMerge = null;
var analyserDestConectado = false;
var playing = false;
var masterizando = false;
var escuchandoMaster = false;
var wavePicos = null;
var estado = {
eq: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
deEss: 0.5, hpfV: 0.55, mudV: 0.45, denoise: 0.45,
subAdd: 0.4, airEx: 0.35, expDyn: 0.3, midLow: 0.3, roomK: 0,
glueK: 0.6, lufs: 0, tp: 0,
vol: 0.5, pan: 0.5
};
function clampF(v, a, b) { return v < a ? a : (v > b ? b : v); }
function db2ag(v) { return Math.pow(10, v / 20); }
function ag2db(v) { return 20 * Math.log10(Math.max(v, 1e-9)); }
function fmtT(s) { s = Math.max(0, Math.floor(s)); var m = Math.floor(s / 60), x = s % 60; return m + ':' + (x < 10 ? '0' : '') + x; }
function asegurar() { if (!actx && AC) actx = new AC(); }
function pararTodo() {
if (source) { try { source.stop(); } catch (e) {} try { source.disconnect(); } catch (e) {} source = null; }
playing = false;
}
// ---------------- log ----------------
function logIA(msg) { iaseg.innerHTML = '<div>' + msg + '</div>' + iaseg.innerHTML; }
function setStats(html) { iaStats.innerHTML = html; }
// ---------------- EQ UI ----------------
function construirEQ() {
for (var i = 0; i < BANDS.length; i++) {
(function (i) {
var f = BANDS[i];
var b = document.createElement('div'); b.className = 'band';
var val = document.createElement('span'); val.className = 'val'; val.textContent = '0';
var inp = document.createElement('input');
inp.type = 'range'; inp.min = -12; inp.max = 12; inp.step = 0.5; inp.value = 0;
inp.style.accentColor = BAND_COLORS[i];
inp.oninput = function () {
estado.eq[i] = parseFloat(inp.value);
val.textContent = (inp.value > 0 ? '+' : '') + inp.value + ' dB';
dibujarCurva();
};
var freq = document.createElement('span'); freq.className = 'freq';
var suf = (i === 0 ? 'LS' : (i === BANDS.length - 1 ? 'HS' : ''));
freq.textContent = (f >= 1000 ? (f / 1000) + 'k' : f) + (suf ? ' ' + suf : '');
b.appendChild(inp); b.appendChild(val); b.appendChild(freq);
eqBox.appendChild(b);
})(i);
}
}
function setEQ(i, v) {
estado.eq[i] = v;
var band = eqBox.children[i];
band.querySelector('input').value = v;
band.querySelector('.val').textContent = (v > 0 ? '+' : '') + v + ' dB';
}
function dibujarCurva() {
if (!cctx) return;
var w = curva.width, h = curva.height;
cctx.clearRect(0, 0, w, h);
cctx.strokeStyle = '#1e2540'; cctx.lineWidth = 1;
for (var gt = 1; gt < 5; gt++) { cctx.beginPath(); cctx.moveTo(0, h * gt / 5); cctx.lineTo(w, h * gt / 5); cctx.stroke(); }
cctx.strokeStyle = '#343d66'; cctx.beginPath(); cctx.moveTo(0, h / 2); cctx.lineTo(w, h / 2); cctx.stroke();
var min = -12, max = 12;
cctx.beginPath();
for (var j = 0; j <= 160; j++) {
var freq = 20 * Math.pow(20000 / 20, j / 160);
var db = 0;
for (var b = 0; b < 10; b++) {
var gcur = estado.eq[b];
if (Math.abs(gcur) < 0.05) continue;
if (b === 0 && freq < BANDS[0] * 2) db += gcur;
else if (b === 9 && freq > BANDS[9] / 2) db += gcur;
else {
var Q = 1.1;
var diff = Math.log2(freq / BANDS[b]);
var bwCur = Math.log2(2) / Q;
db += gcur / (1 + Math.pow(diff / (bwCur / 2), 2));
}
}
var x = (Math.log(freq / 20) / Math.log(20000 / 20)) * w;
var y = h - ((db - min) / (max - min)) * h;
if (j === 0) cctx.moveTo(x, y); else cctx.lineTo(x, y);
}
cctx.strokeStyle = '#6ef0c8'; cctx.lineWidth = 2; cctx.stroke();
}
// ---------------- knobs ----------------
var KNOBS = [
['deEss', 'deEss'], ['hpfV', 'hpfV'], ['mudV', 'mudV'], ['denoise', 'denoise'],
['subAdd', 'subAdd'], ['airEx', 'airEx'], ['expDyn', 'expDyn'],
['midLow', 'midLow'], ['roomK', 'roomK'], ['glueK', 'glueK'],
['volT', 'vol'], ['panT', 'pan'],
['lufsT', 'lufs'], ['tpT', 'tp']
];
function mostrarValor(sliderId, stateKey, v) {
v = (typeof v === 'number') ? v : clampF(parseFloat($(sliderId).value) / 100, 0, 1);
if (stateKey === 'lufs') { $('lufsTO').textContent = Math.round(-14 + v * 4) + ' LU'; }
else if (stateKey === 'tp') { $('tpTO').textContent = '-' + (1 + v * 1.5).toFixed(1) + ' dBTP'; }
else if (stateKey === 'vol') { $('volTO').textContent = (v * 2 - 1) * 12 >= 0 ? '+' : ''; $('volTO').textContent = ((v * 2 - 1) * 12).toFixed(1) + ' dB'; }
else if (stateKey === 'pan') { var pp = v * 2 - 1; $('panTO').textContent = Math.abs(pp) < 0.02 ? 'C' : (pp < 0 ? 'L ' + Math.round(-pp * 100) + '%' : 'R ' + Math.round(pp * 100) + '%'); }
else { var o = $(sliderId + 'O'); if (o) o.textContent = Math.round(v * 100) + '%'; }
}
KNOBS.forEach(function (pair) {
var sliderId = pair[0], stateKey = pair[1];
$(sliderId).oninput = function () {
var v = clampF(parseFloat($(sliderId).value) / 100, 0, 1);
estado[stateKey] = v;
mostrarValor(sliderId, stateKey, v);
if (stateKey === 'vol' || stateKey === 'pan') sincFadersLive();
};
});
function setKnobUI(stateKey, value) {
var sliderId = null;
for (var ki = 0; ki < KNOBS.length; ki++) {
if (KNOBS[ki][1] === stateKey) { sliderId = KNOBS[ki][0]; break; }
}
if (!sliderId) return;
$(sliderId).value = Math.round(value * 100);
mostrarValor(sliderId, stateKey, value);
if (stateKey === 'vol' || stateKey === 'pan') sincFadersLive();
}
// ---------------- presets ----------------
var presets = {
radio: { eq: [0, 0, 0, 0.5, -0.5, 0.5, 1, -1, 0.5, 1], deEss: 0.55, hpfV: 0.7, mudV: 0.5, denoise: 0.5, subAdd: 0.4, airEx: 0.5, expDyn: 0.45, midLow: 0.4, roomK: 0, glueK: 0.7 },
clean: { eq: [0, 0, 1, 0, 0, 1, 1, 0, 1, 1], deEss: 0.3, hpfV: 0.4, mudV: 0.3, denoise: 0.4, subAdd: 0.25, airEx: 0.5, expDyn: 0.5, midLow: 0.3, roomK: 0, glueK: 0.45 },
urban: { eq: [2, 1, -1, 0, 1, 0, -1, 0, 0, 0], deEss: 0.5, hpfV: 0.55, mudV: 0.55, denoise: 0.5, subAdd: 0.75, airEx: 0.3, expDyn: 0.4, midLow: 0.55, roomK: 0, glueK: 0.8 },
loud: { eq: [0, 0, 0, 0, 0, 0, 0.5, 0, 0.5, 0], deEss: 0.6, hpfV: 0.6, mudV: 0.35, denoise: 0.55, subAdd: 0.55, airEx: 0.6, expDyn: 0.7, midLow: 0.4, roomK: 0, glueK: 0.9 },
vocal: { eq: [-1, 0, 0, 1, 2, 3, 2, 0, -1, -1], deEss: 0.6, hpfV: 0.65, mudV: 0.55, denoise: 0.5, subAdd: 0.2, airEx: 0.5, expDyn: 0.5, midLow: 0.5, roomK: 0, glueK: 0.5 },
dream: { eq: [1, 0, 0, 0, 0, 1, 1, 1, 1, 1], deEss: 0.45, hpfV: 0.35, mudV: 0.3, denoise: 0.4, subAdd: 0.45, airEx: 0.55, expDyn: 0.5, midLow: 0.3, roomK: 0, glueK: 0.6 }
};
document.querySelectorAll('[data-pre]').forEach(function (btn) {
btn.onclick = function () {
var p = presets[btn.dataset.pre]; if (!p) return;
var keys = ['deEss', 'hpfV', 'mudV', 'denoise', 'subAdd', 'airEx', 'expDyn', 'midLow', 'roomK', 'glueK'];
keys.forEach(function (k) {
if (typeof p[k] !== 'undefined') {
estado[k] = p[k];
setKnobUI(k, p[k]);
}
});
for (var i = 0; i < 10; i++) setEQ(i, p.eq[i]);
dibujarCurva();
logIA('Preset aplicado: <b>' + btn.dataset.pre + '</b>.');
};
});
// ---------------- init UI ----------------
construirEQ();
dibujarCurva();
// ---------------- Agente IA en lenguaje natural ----------------
var agenteIn = $('agenteIn'), agenteChat = $('agenteChat');
function chatIA(html) {
var d = document.createElement('div'); d.className = 'chatmsg';
d.innerHTML = '<span class="m-ia">IA:</span> ' + html;
agenteChat.appendChild(d);
while (agenteChat.children.length > 200) agenteChat.removeChild(agenteChat.firstChild);
agenteChat.scrollTop = agenteChat.scrollHeight;
}
function chatUser(txt) {
var d = document.createElement('div'); d.className = 'chatmsg';
d.innerHTML = '<span class="m-user">Tú:</span> <span style="color:#ccd3ea">' + txt.replace(/</g, '&lt;') + '</span>';
agenteChat.appendChild(d);
agenteChat.scrollTop = agenteChat.scrollHeight;
}
// Ajustes que el agente puede ejecutar de forma bidireccional (UI + audio)
function agenteSetEQ(band, db) {
var nuevo = clampF(parseFloat((estado.eq[band] || 0)) + db, -12, 12);
setEQ(band, Math.round(nuevo * 10) / 10);
dibujarCurva();
return estado.eq[band];
}
function agenteSetKnob(stateKey, delta) {
var v = clampF((estado[stateKey] || 0) + delta, 0, 1);
estado[stateKey] = v;
setKnobUI(stateKey, v);
return v;
}
var ACCIONES = [ {
rx: /(brillos|agudos|aire|air|aguda)s?/i,
cls: 'eq',
aplicar: function (dir, n) {
var r1 = agenteSetEQ(9, dir * n), r2 = agenteSetEQ(8, dir * n * 0.6);
chatIA('Fader de agudos <b>' + (dir > 0 ? '+' : '') + (dir * n).toFixed(1) + ' dB</b> (tope 2 dB en 8 kHz, ' + (dir * n).toFixed(1) + ' dB en 16 kHz) y curva actualizada.');
return true;
}
}, {
rx: /(graves|bajos|bass|bass|bombo|low)/i,
cls: 'eq',
aplicar: function (dir, n) {
var r1 = agenteSetEQ(2, dir * n), r2 = agenteSetEQ(1, dir * n * 0.5);
chatIA('Fader de graves <b>' + (dir > 0 ? '+' : '') + (dir * n).toFixed(1) + ' dB</b> en 125 Hz y curva actualizada.');
return true;
}
}, {
rx: /(medios|presencia|mids)/i,
cls: 'eq',
aplicar: function (dir, n) {
var r3 = agenteSetEQ(5, dir * n);
chatIA('Fader de medios <b>' + (dir > 0 ? '+' : '') + (dir * n).toFixed(1) + ' dB</b> en 1 kHz y curva actualizada.');
return true;
}
}, {
rx: /(volumen|fade|master output|master de salida|sube.?el nive)/i,
cls: 'knob',
key: 'vol',
aplicar: function (dir, n) {
var v = agenteSetKnob('vol', dir * n * 0.08);
chatIA('Fader de <b>volumen master</b> movido a <b>' + ((v * 2 - 1) * 12 > 0 ? '+' : '') + ((v * 2 - 1) * 12).toFixed(1) + ' dB</b>.');
return true;
}
}, {
rx: /(paneo|pan|balance|balancear)/i,
cls: 'knob',
key: 'pan',
aplicar: function (dir, n, t) {
if (/(izquierda|izq|left|al centro|centro)/.test(t)) dir = -1;
else if (/(derecha|der|right)/.test(t)) dir = 1;
var v = agenteSetKnob('pan', dir * n * 0.08);
var pp = v * 2 - 1;
chatIA('Paneo ajustado a <b>' + (Math.abs(pp) < 0.02 ? 'centro (C)' : (pp < 0 ? 'L ' + Math.round(-pp * 100) + '%' : 'R ' + Math.round(pp * 100) + '%')) + '</b>.');
return true;
}
}, {
rx: /(de-esser|deesser|siseo|seseo)/i,
cls: 'knob',
key: 'deEss',
aplicar: function (dir, n) {
var v = agenteSetKnob('deEss', dir * n * 0.06);
chatIA('De-esser <b>' + Math.round(v * 100) + '%</b>.');
return true;
}
}, {
rx: /(denoise|de-noise|ruido|ruidito|silbido)/i,
cls: 'knob',
key: 'denoise',
aplicar: function (dir, n) {
var v = agenteSetKnob('denoise', dir * n * 0.06);
chatIA('De-noise <b>' + Math.round(v * 100) + '%</b>.');
return true;
}
}, {
rx: /(sub|subgrave|infra|60hz|40hz)/i,
cls: 'knob',
key: 'subAdd',
aplicar: function (dir, n) {
var v = agenteSetKnob('subAdd', dir * n * 0.06);
chatIA('Refuerzo de sub 40-60 Hz <b>' + Math.round(v * 100) + '%</b>.');
return true;
}
}, {
rx: /(glue|compresi|pegar|unir|bus)/i,
cls: 'knob',
key: 'glueK',
aplicar: function (dir, n) {
var v = agenteSetKnob('glueK', dir * n * 0.06);
chatIA('Compresor glue de bus <b>' + Math.round(v * 100) + '%</b>.');
return true;
}
}, {
rx: /(reverb|eco|sala|espacio)/i,
cls: 'none',
aplicar: function () {
chatIA('El reverb está <b>desactivado por diseño</b> en el master (Wet 0%): la exportación nunca añade eco. Si lo prefieres, puedo ajustar aire, medios o graves.');
return true;
}
}, {
rx: /(hpf|altavoz|monitor|infrasonido)/i,
cls: 'knob',
key: 'hpfV',
aplicar: function (dir, n) {
var v = agenteSetKnob('hpfV', dir * n * 0.06);
chatIA('HPF de bus <b>' + Math.round(v * 100) + '%</b>.');
return true;
}
}, {
rx: /(expansor|expander|dinámic|piso|gate)/i,
cls: 'knob',
key: 'expDyn',
aplicar: function (dir, n) {
var v = agenteSetKnob('expDyn', dir * n * 0.06);
chatIA('Expansor de piso <b>' + Math.round(v * 100) + '%</b>.');
return true;
}
} ];
function agenteComando(txt) {
var t = txt.toLowerCase();
// Intención: subir/bajar y cantidad en dB
var dir = 1, n = 1.5;
if (/(baja|bajar|reduce|reducir|menos|atenuar|quita|bajo el)/.test(t)) dir = -1;
var mDb = t.match(/(\d+(?:[.,]\d+)?)\s*db/i);
if (mDb) n = parseFloat(mDb[1].replace(',', '.'));
else if (/un poco|ligero|suave|pareje/i.test(t)) n = 1.5;
else if (/bastante|mucho|fuerte|más/i.test(t)) n = 3;
else if (/poquito|mínimo/i.test(t)) n = 0.5;
// Acciones especiales primero
if (/(masterizar|masteriza|haz el master|procesar|procesa|aplica el master)/.test(t)) {
if (!audioBuffer) { chatIA('Primero carga una canción para masterizar.'); return; }
if (masterizando) { chatIA('Ya hay una masterización en curso...'); return; }
chatIA('Entendido. Ejecuto el <b>pipeline completo de mastering</b> con los ajustes actuales...');
setTimeout(function () { doMaster(); }, 50);
return;
}
if (/(descarg|exportar|exporta|guardar el (master|audio)|wav)/.test(t)) {
guardarWAV();
chatIA('Exportando el master como <b>WAV estéreo</b>...');
return;
}
if (/(reproduc|escuchar|play|tocar)/.test(t) && audioBuffer) {
maestroPlay();
chatIA('Reproduciendo el <b>' + (escuchandoMaster && masterBuffer ? 'master' : 'original') + '</b>.');
return;
}
// Presets
for (var pn in presets) {
if (t.indexOf(pn) !== -1) {
var keys = ['deEss', 'hpfV', 'mudV', 'denoise', 'subAdd', 'airEx', 'expDyn', 'midLow', 'roomK', 'glueK'];
keys.forEach(function (k) { if (typeof presets[pn][k] !== 'undefined') { estado[k] = presets[pn][k]; setKnobUI(k, presets[pn][k]); } });
for (var i = 0; i < 10; i++) setEQ(i, presets[pn].eq[i]);
dibujarCurva();
chatIA('Preset <b>' + pn + '</b> aplicado: todos los faders y el EQ se movieron en pantalla.');
return;
}
}
// Ajustes numéricos de EQ/knobs
for (var a = 0; a < ACCIONES.length; a++) {
if (ACCIONES[a].rx.test(t)) {
ACCIONES[a].aplicar(dir, n, t);
logIA('<b>Agente:</b> orden <i>«' + txt + '»</i> ejecutada sobre la interfaz.');
return;
}
}
chatIA('No entendí el ajuste. Intenta por ejemplo: <b>«sube un poco los brillos»</b>, <b>«baja los graves 2 dB»</b> o <b>«más volumen»</b>.');
}
function enviarAgente() {
var txt = agenteIn.value.trim();
if (!txt) return;
chatUser(txt);
agenteIn.value = '';
agenteComando(txt);
}
$('btnEnviar').onclick = enviarAgente;
if (agenteIn && typeof agenteIn.addEventListener === 'function') {
agenteIn.addEventListener('keydown', function (e) { if (e.key === 'Enter') enviarAgente(); });
}
// ---------------- carga ----------------
var btnCargar = $('btnCargar'), fileIn = $('file');
btnCargar.onclick = function () { fileIn.click(); };
fileIn.onchange = function (e) { if (e.target.files && e.target.files[0]) cargarArchivo(e.target.files[0]); };
function esAudio(f) {
if (!f) return false;
if (f.type && f.type.indexOf('audio') === 0) return true;
var ext = (f.name || '').split('.').pop().toLowerCase();
return ['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac', 'webm'].indexOf(ext) !== -1;
}
document.addEventListener('dragover', function (e) { e.preventDefault(); });
document.addEventListener('drop', function (e) {
e.preventDefault();
var f = (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]) || null;
if (esAudio(f)) cargarArchivo(f);
});
function calcNoiseFloor(buf) {
var ch = buf.getChannelData(0);
var n = ch.length, bs = Math.max(2048, Math.floor(buf.sampleRate * 0.05));
var blocks = [];
for (var s = 0; s < n; s += bs) {
var sum = 0, cnt = 0;
for (var k = s; k < Math.min(s + bs, n); k++) { sum += ch[k] * ch[k]; cnt++; }
blocks.push(Math.sqrt(sum / (cnt || 1)));
}
blocks.sort(function (a, b) { return a - b; });
var idx = Math.max(0, Math.floor(blocks.length * 0.06) - 1);
return clampF(blocks[idx] || 0.001, 0.00003, 0.12);
}
function cargarArchivo(file) {
asegurar();
file.arrayBuffer()
.then(function (ab) { return actx.decodeAudioData(ab); })
.then(function (dec) {
audioBuffer = dec;
masterBuffer = null;
offsetPos = 0;
escuchandoMaster = false;
marcarAB(false);
pararTodo();
var warmup = calcNoiseFloor(dec);
var floorDb = ag2db(warmup);
wavePicos = construirPicos(dec);
dibujarWave();
$('btnPlay').disabled = false;
$('btnA').disabled = false;
$('btnB').disabled = false;
$('btnMaster').disabled = false;
$('btnSave').disabled = true;
pbar.style.width = '0%';
logIA('Canción cargada: <b>' + file.name + '</b> · ' + dec.duration.toFixed(1) + ' s · ' + dec.sampleRate + ' Hz · ' + dec.numberOfChannels + 'ch, piso de ruido ' + floorDb.toFixed(1) + ' dBFS.');
setStats('<span>Duración: <b>' + fmtT(dec.duration) + '</b></span><span>Piso ruido: <b>' + floorDb.toFixed(1) + ' dBFS</b></span>');
$('btnPlay').textContent = '▶ Reproducir';
fileIn.value = '';
})
.catch(function () { alert('No se pudo decodificar: ' + file.name); });
}
function limpiarTodo() {
if (confirm('¿Seguro que quieres limpiar todo? Se borrará la canción, el master y todos los ajustes.')) {
pararTodo();
audioBuffer = null;
masterBuffer = null;
wavePicos = null;
offsetPos = 0;
escuchandoMaster = false;
playing = false;
masterizando = false;
if (analyser) { try { analyser.disconnect(); } catch (e) {} analyser = null; }
liveGain = null; liveSplit = null; liveGainL = null; liveGainR = null; liveMerge = null;
analyserDestConectado = false;
pbar.style.width = '0%';
$('btnPlay').textContent = '▶ Reproducir';
$('btnPlay').disabled = true;
$('btnA').disabled = true;
$('btnB').disabled = true;
$('btnMaster').disabled = true;
$('btnSave').disabled = true;
$('btnPlay').classList.remove('active');
$('btnA').classList.remove('active');
$('btnB').classList.remove('active');
marcarAB(false);
estado = {
eq: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
deEss: 0.5, hpfV: 0.55, mudV: 0.45, denoise: 0.45,
subAdd: 0.4, airEx: 0.35, expDyn: 0.3, midLow: 0.3, roomK: 0,
glueK: 0.6, lufs: 0, tp: 0,
vol: 0.5, pan: 0.5
};
KNOBS.forEach(function (pair) { setKnobUI(pair[1], estado[pair[1]]); });
for (var i = 0; i < 10; i++) setEQ(i, 0);
dibujarCurva();
var sw = spec.width, sh = spec.height;
sctx.clearRect(0, 0, sw, sh);
sctx.fillStyle = '#0e1222'; sctx.fillRect(0, 0, sw, sh);
if (wctx) { var ww = wave.width, wh = wave.height; wctx.clearRect(0, 0, ww, wh); wctx.fillStyle = '#151a2e'; wctx.fillRect(0, 0, ww, wh); }
iaseg.innerHTML = 'Todo limpio. Carga una canción para empezar.';
iaStats.innerHTML = '';
agenteChat.innerHTML = '<div class="chatmsg"><span class="m-ia">IA:</span> Hola, soy el ingeniero de mastering. Pídeme ajustes como <b>«sube un poco los brillos»</b> o <b>«baja los graves»</b> y moveré los faders y el EQ en pantalla.</div>';
fileIn.value = '';
}
}
$('btnReset').onclick = limpiarTodo;
function construirPicos(buf) {
var ch = buf.getChannelData(0);
var n = ch.length, N = 480;
var out = new Float32Array(N);
var bs = Math.max(1, Math.floor(n / N));
for (var i = 0; i < N; i++) {
var s = i * bs, e = Math.min(s + bs, n);
var mn = 1, mx = -1, sum = 0;
for (var k = s; k < e; k++) { var v = ch[k]; if (v < mn) mn = v; if (v > mx) mx = v; sum += v * v; }
var rms = Math.sqrt(sum / ((e - s) || 1));
out[i] = clampF(Math.max(-mn, mx, rms), 0, 1);
}
return out;
}
function dibujarWave() {
if (!wctx) return;
var w = wave.width, h = wave.height;
wctx.clearRect(0, 0, w, h);
wctx.fillStyle = '#151a2e'; wctx.fillRect(0, 0, w, h);
if (!wavePicos) return;
wctx.fillStyle = '#67c6ff';
var bw = w / wavePicos.length;
for (var i = 0; i < wavePicos.length; i++) {
var bh = Math.max(2, wavePicos[i] * h);
wctx.fillRect(i * bw + 1, (h - bh) / 2, Math.max(1, bw - 2), bh);
}
}
// ---------------- espectro ----------------
var specData = new Uint8Array(256);
function loopRAF() {
requestAnimationFrame(loopRAF);
var w = spec.width, h = spec.height;
sctx.clearRect(0, 0, w, h);
sctx.fillStyle = '#0e1222'; sctx.fillRect(0, 0, w, h);
if (playing && analyser) {
analyser.getByteFrequencyData(specData);
sctx.fillStyle = '#67c6ff';
for (var i = 0; i < specData.length; i++) {
var bh = specData[i] / 255 * h;
sctx.fillRect(i * (w / specData.length) + 1, h, Math.max(1, (w / specData.length) - 2), -bh);
}
} else {
sctx.fillStyle = '#4a5578';
sctx.font = '12px Segoe UI';
sctx.fillText('ESPECTRO · reproduce la canción para verlo', 12, 20);
}
if (playing) {
var cxl = escuchandoMaster && masterBuffer ? masterBuffer : audioBuffer;
var p = (actx.currentTime - startTime) / Math.max(0.001, cxl.duration);
p = clampF(p, 0, 1);
sctx.fillStyle = '#6ef0c8';
var px = Math.round(p * w);
sctx.fillRect(px, h - 8, 3, 8);
}
}
loopRAF();
// ---------------- transporte ----------------
function marcarAB(useMaster) {
var a = $('btnA'), b = $('btnB');
if (a && a.classList) { if (useMaster) a.classList.remove('active'); else a.classList.add('active'); }
if (b && b.classList) { if (useMaster) b.classList.add('active'); else b.classList.remove('active'); }
}
$('btnPlay').onclick = function () {
if (!audioBuffer) return;
if (playing) maestroPausa(); else maestroPlay();
};
$('btnA').onclick = function () { escuchar(false); };
$('btnB').onclick = function () {
if (!masterBuffer) { logIA('Todavía no hay master. Pulsa <b>Masterizar con IA</b>.'); return; }
escuchar(true);
};
$('btnSave').onclick = guardarWAV;
function escuchar(useMaster) {
if (!audioBuffer) return;
offsetPos = 0;
escuchandoMaster = !!useMaster;
marcarAB(escuchandoMaster);
pararTodo();
maestroPlay();
}
function sincFadersLive() {
if (!liveGain) return;
var volGain = db2ag((estado.vol * 2 - 1) * 12);
var panV = estado.pan * 2 - 1; // -1..1
var panL = panV <= 0 ? 1 : 1 - panV * 0.5;
var panR = panV >= 0 ? 1 : 1 + panV * 0.5;
liveGain.gain.value = volGain;
if (liveGainL) liveGainL.gain.value = panL;
if (liveGainR) liveGainR.gain.value = panR;
}
function conectarCadenaVivo() {
if (liveGain) { sincFadersLive(); return; }
if (!analyserDestConectado) {
analyser.connect(actx.destination);
analyserDestConectado = true;
}
if (!actx.createGain || !actx.createChannelSplitter || !actx.createChannelMerger) return;
liveGain = actx.createGain();
liveSplit = actx.createChannelSplitter(2);
liveGainL = actx.createGain();
liveGainR = actx.createGain();
liveMerge = actx.createChannelMerger(2);
liveGainL.gain.value = 1; liveGainR.gain.value = 1;
liveGain.connect(liveSplit);
liveSplit.connect(liveGainL, 0);
liveSplit.connect(liveGainR, 1);
liveGainL.connect(liveMerge, 0, 0);
liveGainR.connect(liveMerge, 0, 1);
liveMerge.connect(analyser);
sincFadersLive();
}
function maestroPlay() {
if (!audioBuffer) return;
asegurar();
if (actx.state === 'suspended') { actx.resume(); }
if (!analyser) { analyser = actx.createAnalyser(); analyser.fftSize = 1024; analyser.smoothingTimeConstant = 0.82; }
marcarAB(escuchandoMaster);
var buf = escuchandoMaster && masterBuffer ? masterBuffer : audioBuffer;
pararTodo();
source = actx.createBufferSource();
source.buffer = buf;
conectarCadenaVivo();
source.connect(liveGain || analyser);
source.start(0, offsetPos);
startTime = actx.currentTime - offsetPos;
playing = true;
$('btnPlay').textContent = '❚❚ Pausar';
}
function maestroPausa() {
if (source && playing) offsetPos = actx.currentTime - startTime;
pararTodo();
$('btnPlay').textContent = '▶ Reproducir';
}
// ========== DSP ==========
function biquad(type, f, sr, Q, g) {
var w0 = 2 * Math.PI * f / sr, cs = Math.cos(w0), sn = Math.sin(w0);
var al = sn / (2 * Q), A, b0, b1, b2, a0, a1, a2;
if (type === 'hp') {
b0 = (1 + cs) / 2; b1 = -(1 + cs); b2 = (1 + cs) / 2;
a0 = 1 + al; a1 = -2 * cs; a2 = 1 - al;
} else if (type === 'lp') {
b0 = (1 - cs) / 2; b1 = 1 - cs; b2 = (1 - cs) / 2;
a0 = 1 + al; a1 = -2 * cs; a2 = 1 - al;
} else if (type === 'bp') {
b0 = al; b1 = 0; b2 = -al;
a0 = 1 + al; a1 = -2 * cs; a2 = 1 - al;
} else if (type === 'ls') {
A = Math.pow(10, g / 40);
b0 = A * ((A + 1) - (A - 1) * cs + 2 * Math.sqrt(A) * al);
b1 = 2 * A * ((A - 1) - (A + 1) * cs);
b2 = A * ((A + 1) - (A - 1) * cs - 2 * Math.sqrt(A) * al);
a0 = (A + 1) + (A - 1) * cs + 2 * Math.sqrt(A) * al;
a1 = -2 * ((A - 1) + (A + 1) * cs);
a2 = (A + 1) + (A - 1) * cs - 2 * Math.sqrt(A) * al;
} else if (type === 'hs') {
A = Math.pow(10, g / 40);
b0 = A * ((A + 1) + (A - 1) * cs + 2 * Math.sqrt(A) * al);
b1 = -2 * A * ((A - 1) + (A + 1) * cs);
b2 = A * ((A + 1) + (A - 1) * cs - 2 * Math.sqrt(A) * al);
a0 = (A + 1) - (A - 1) * cs + 2 * Math.sqrt(A) * al;
a1 = 2 * ((A - 1) - (A + 1) * cs);
a2 = (A + 1) - (A - 1) * cs - 2 * Math.sqrt(A) * al;
} else {
A = Math.pow(10, g / 40);
b0 = 1 + al * A; b1 = -2 * cs; b2 = 1 - al * A;
a0 = 1 + al / A; a1 = -2 * cs; a2 = 1 - al / A;
}
b0 /= a0; b1 /= a0; b2 /= a0; a1 /= a0; a2 /= a0;
var z1 = 0, z2 = 0;
return function (x) {
var y = b0 * x + z1;
z1 = b1 * x - a1 * y + z2;
z2 = b2 * x - a2 * y;
return y;
};
}
function S(x) { return Math.sqrt(x); }
function mkVerb(sr, shift) {
var tun = sr / 44100;
var cl = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
var al = [556, 441, 341, 225];
var o = shift || 0;
var combs = [], alls = [];
for (var i = 0; i < 8; i++) combs.push({ b: new Float32Array(Math.max(2, Math.round((cl[i] + o) * tun))), i: 0, f: 0, d: 0.15, fb: 0.88 });
for (var j = 0; j < 4; j++) alls.push({ b: new Float32Array(Math.max(2, Math.round((al[j] + o) * tun))), i: 0 });
return {
step: function (x) {
var w = 0;
for (var c0 = 0; c0 < 8; c0++) {
var kn = combs[c0];
var ov = kn.b[kn.i];
kn.f = ov * (1 - kn.d) + kn.f * kn.d;
kn.b[kn.i] = x + kn.f * kn.fb;
kn.i = (kn.i + 1) % kn.b.length;
w += ov;
}
w *= 0.125;
for (var a0 = 0; a0 < 4; a0++) {
var ap = alls[a0];
var ov2 = ap.b[ap.i];
ap.b[ap.i] = w + ov2 * 0.5;
w = -w + ov2;
ap.i = (ap.i + 1) % ap.b.length;
}
return w;
}
};
}
// ================================== MASTER ==================================
$('btnMaster').onclick = function () {
if (!audioBuffer || masterizando) return;
doMaster();
};
// ================================================================
// MOTOR SEGURO DE MASTERING
// Regla profesional: nunca distorsionar. Límites duros de seguridad
// ----------------------------------------------------------------
// 1) EQ quirúrgico: correcciones ≤ ±2 dB (bajo, medio, agudo)
// 2) Compresor sutil: ratio ≤ 2:1, ataque lento (respeta transitorios)
// 3) Preservar dinámica: ganancia de normalización limitada
// 4) Limitador brickwall final: el pico NUNCA supera -1.0 dBTP
// ================================================================
function analizarTonalidad(L, R, sr, n) {
var bLow = biquad('lp', 200, sr, 0.707, 0);
var bMid = biquad('bp', 1000, sr, 0.5, 0);
var bHigh = biquad('hp', 3500, sr, 0.707, 0);
var sl = 0, sm = 0, sh = 0, cnt = 0;
for (var i = 0; i < n; i += 128) {
var v = L[i];
sl += bLow(v) * bLow(v);
sm += bMid(v) * bMid(v);
sh += bHigh(v) * bHigh(v);
cnt++;
}
cnt = Math.max(1, cnt);
var dBL = 10 * Math.log10(sl / cnt + 1e-18);
var dBM = 10 * Math.log10(sm / cnt + 1e-18);
var dBH = 10 * Math.log10(sh / cnt + 1e-18);
return {
low: clampF(0 - (dBL - dBM), -2, 2),
high: clampF(-4 - (dBH - dBM), -2, 2)
};
}
function doMaster() {
masterizando = true;
$('btnMaster').disabled = true;
pbar.style.width = '2%';
var sr = audioBuffer.sampleRate;
var n = audioBuffer.length;
var inL = audioBuffer.getChannelData(0);
var inR = audioBuffer.numberOfChannels > 1 ? audioBuffer.getChannelData(1) : inL;
var L = new Float32Array(n), R = new Float32Array(n);
for (var i = 0; i < n; i++) { L[i] = inL[i]; R[i] = inR[i]; }
var cfg = {
sr: sr,
eq: estado.eq.slice(),
deEss: estado.deEss, hpfV: estado.hpfV, mudV: estado.mudV,
denoise: estado.denoise, subAdd: estado.subAdd, airEx: estado.airEx,
expDyn: estado.expDyn, roomK: 0, // Reverb siempre apagado en el master: sin eco
midLow: estado.midLow,
glueK: estado.glueK, lufs: estado.lufs, tp: estado.tp
};
logIA('=== Masterización con IA iniciada ===');
logIA('Análisis FFT y balance tonal (correcciones quirúrgicas ≤ ±2 dB)...');
// Balance tonal automático (análisis de frecuencias de la canción)
var auto = analizarTonalidad(L, R, sr, n);
var fSig = function (v) { return (v > 0 ? '+' : '') + v.toFixed(1) + ' dB'; };
logIA('Corrección baja: ' + fSig(auto.low) + ' · corrección aire: ' + fSig(auto.high));
var bm = function (t, f, Q, g) { return biquad(t, f, sr, Q, g); };
// EQ de usuari@ aplicado con tope de seguridad (±4 dB) - nunca excesivo
var eqF = [];
for (var b = 0; b < 10; b++) {
var g0 = clampF(cfg.eq[b], -4, 4);
if (Math.abs(g0) < 0.05) { eqF.push(null); continue; }
var tt = b === 0 ? 'ls' : (b === 9 ? 'hs' : 'pk');
eqF.push(bm(tt, BANDS[b], b === 0 || b === 9 ? 0.6 : 1.1, g0));
}
var fAutoLow = auto.low ? bm('ls', 90, 0.6, auto.low) : null;
var fAutoHigh = auto.high ? bm('hs', 11000, 0.6, auto.high) : null;
// HPF de bus SUTIL: solo limpia el retumbar infragrave (20-45Hz),
// respeta el bombear del bajo/batería (no corta 40-60Hz).
var fHpf = bm('hp', Math.min(25 + 30 * cfg.hpfV, 80), 0.707, 0);
var fEss = bm('bp', 7000, 0.7, 0);
var fMud = bm('bp', 450, 1.1, 0);
var fSub = bm('lp', 120, 0.707, 0);
var fAir = bm('hs', 12000, 0.6, 2 * cfg.airEx); // máximo +2 dB
var verbL = mkVerb(sr, 0);
var verbR = mkVerb(sr, 17);
// Limpieza mid-low: suave recorte dinámico de la opacidad 300-600 Hz
var fMidLow = bm('bp', 420, 1.0, 0);
var st = {
envEssL: 0, envEssR: 0, envMudL: 0, envMudR: 0,
envMidLowL: 0, envMidLowR: 0,
subEnvL: 0, subEnvR: 0, expEnvL: 0, expEnvR: 0,
envGlue: 0, subPhase: 0
};
var noiseThr = db2ag(-62 - 30 * cfg.denoise);
var aEss = 1 - Math.exp(-1 / (0.01 * sr));
var aExp = 1 - Math.exp(-1 / (0.01 * sr));
// Compresor sutil: ataque 20 ms (respeta transitorios), release 140 ms, ratio ≤ 2:1
var aGlue = 1 - Math.exp(-1 / (0.020 * sr));
var relGlue = Math.exp(-1 / (0.140 * sr));
var thrComp = -16;
var ratioComp = 2;
var kneeComp = 4;
var CHUNK = Math.max(16384, Math.floor(sr * 0.1));
procChunk(0);
function procChunk(off) {
var end = Math.min(n, off + CHUNK);
for (var k = off; k < end; k++) {
var xl = L[k], xr = R[k];
// EQ usuario + corrección tonal automática
for (var b0 = 0; b0 < 10; b0++) {
if (eqF[b0]) { xl = eqF[b0](xl); xr = eqF[b0](xr); }
}
if (fAutoLow) { xl = fAutoLow(xl); xr = fAutoLow(xr); }
if (fAutoHigh) { xl = fAutoHigh(xl); xr = fAutoHigh(xr); }
// Vocal repair: de-esser sutil
var se = fEss(xl);
st.envEssL += (Math.abs(se) - st.envEssL) * aEss;
xl -= se * cfg.deEss * clampF((st.envEssL - 0.06) * 3.0, 0, 1);
xl = fHpf(xl);
var ml = fMud(xl);
st.envMudL += (Math.abs(ml) - st.envMudL) * aEss;
xl -= ml * cfg.mudV * 0.5 * clampF((st.envMudL - 0.08) * 2.5, 0, 1);
var mlow = fMidLow(xl);
st.envMidLowL += (Math.abs(mlow) - st.envMidLowL) * aEss;
xl -= mlow * cfg.midLow * 0.4 * clampF((st.envMidLowL - 0.08) * 2.5, 0, 1);
var seR = fEss(xr);
st.envEssR += (Math.abs(seR) - st.envEssR) * aEss;
xr -= seR * cfg.deEss * clampF((st.envEssR - 0.06) * 3.0, 0, 1);
xr = fHpf(xr);
var mr = fMud(xr);
st.envMudR += (Math.abs(mr) - st.envMudR) * aEss;
xr -= mr * cfg.mudV * 0.5 * clampF((st.envMudR - 0.08) * 2.5, 0, 1);
var mlowR = fMidLow(xr);
st.envMidLowR += (Math.abs(mlowR) - st.envMidLowR) * aEss;
xr -= mlowR * cfg.midLow * 0.4 * clampF((st.envMidLowR - 0.08) * 2.5, 0, 1);
// Aire (máx +2 dB)
xl = fAir(xl); xr = fAir(xr);
// Sub sutil: sigue la envolvente real del bajo (no añade picos)
var lo = fSub(xl), loR = fSub(xr);
st.subEnvL += (Math.abs(lo) - st.subEnvL) * 0.0008;
st.subEnvR += (Math.abs(loR) - st.subEnvR) * 0.0008;
st.subPhase += 2 * Math.PI * (45 + 6 * cfg.subAdd) / sr;
if (st.subPhase > 6.2832) st.subPhase -= 6.2832;
var subAmt = cfg.subAdd * (st.subEnvL + st.subEnvR) * 0.3;
xl += Math.sin(st.subPhase) * subAmt * 0.4;
xr += Math.sin(st.subPhase) * subAmt * 0.4;
// Expansor de piso: solo reduce el ruido de fondo, preserva la señal
if (cfg.expDyn > 0) {
st.expEnvL += (Math.abs(xl) - st.expEnvL) * aExp;
if (st.expEnvL < noiseThr) xl *= Math.max(0.35, 1 - cfg.expDyn * 0.7);
st.expEnvR += (Math.abs(xr) - st.expEnvR) * aExp;
if (st.expEnvR < noiseThr) xr *= Math.max(0.35, 1 - cfg.expDyn * 0.7);
}
// Reverb de sala sutil
if (cfg.roomK > 0.01) {
xl += verbL.step(xl) * cfg.roomK * 0.3;
xr += verbR.step(xr) * cfg.roomK * 0.3;
}
// Compresor glue sutil (ataque lento, ratio ≤ 2:1, knee suave)
var mono = (Math.abs(xl) + Math.abs(xr)) * 0.5;
st.envGlue += (mono > st.envGlue ? (mono - st.envGlue) * aGlue : (mono - st.envGlue) * relGlue);
var detDb = ag2db(st.envGlue + 1e-9);
var gr2 = 0;
if (detDb > thrComp - kneeComp / 2) {
var overDb = detDb - thrComp;
var tx = clampF((overDb + kneeComp / 2) / kneeComp, 0, 1);
var ratioEff = 1 + (ratioComp - 1) * tx;
gr2 = -Math.max(0, overDb) * (1 - 1 / ratioEff);
}
var gm2 = db2ag(gr2);
xl *= 1 + (gm2 - 1) * (cfg.glueK * 0.55);
xr *= 1 + (gm2 - 1) * (cfg.glueK * 0.55);
L[k] = xl; R[k] = xr;
}
pbar.style.width = Math.round((end / n) * 92) + '%';
if (end < n) {
setTimeout(function () { procChunk(end); }, 0);
} else {
finishMaster(L, R);
}
}
}
// Limitador brickwall estricto: ningún sample supera el techo dado.
// Ataque casi instantáneo en los picos, release lento (no "aplasta").
function aplicarLimiterFinal(LL, RR, ceiling, sr) {
var rel = Math.exp(-1 / (0.05 * sr));
var g = 1;
for (var i = 0; i < LL.length; i++) {
var pk = Math.max(Math.abs(LL[i]), Math.abs(RR[i]));
var need = pk > 1e-9 ? ceiling / pk : 1;
var effective = need < g ? need : g + (1 - g) * rel;
LL[i] *= effective;
RR[i] *= effective;
g = effective;
}
}
function finishMaster(LL, RR) {
var sr = audioBuffer.sampleRate;
var lufs = medirLUFS(LL, RR, sr);
// Normalización a -14/-10 LUFS con ganancia CONSERVADORA (±3 dB, preserva dinámica)
var target = -14 + 4 * estado.lufs;
var gainDb = clampF(target - lufs, -3, 3);
var gain = db2ag(gainDb);
for (var i = 0; i < LL.length; i++) { LL[i] *= gain; RR[i] *= gain; }
// Limitador final estricto: el pico nunca pasa de -1.0 dBTP (o el elegido)
var ceiling = db2ag(-(1 + 1.5 * estado.tp));
aplicarLimiterFinal(LL, RR, ceiling, sr);
var pk = 0;
for (i = 0; i < LL.length; i++) pk = Math.max(pk, Math.abs(LL[i]), Math.abs(RR[i]));
var buf = actx.createBuffer(2, LL.length, sr);
buf.getChannelData(0).set(LL);
buf.getChannelData(1).set(RR);
masterBuffer = buf;
masterizando = false;
$('btnMaster').disabled = false;
$('btnB').disabled = false;
$('btnSave').disabled = false;
var lufsFinal = medirLUFS(LL, RR, sr);
logIA('<b style="color:#6ef0c8">✔ Master completo</b> · ' + lufsFinal.toFixed(1) + ' LUFS · pico real ' + ag2db(Math.max(pk, 1e-9)).toFixed(1) + ' dBTP (techo ' + ag2db(ceiling).toFixed(1) + ' dB): sin clipping, dinámica preservada.');
setStats('<span>LUFS: <b>' + lufsFinal.toFixed(1) + '</b></span><span>Ganancia: <b>' + gainDb.toFixed(1) + ' dB</b></span><span>Pico: <b>' + ag2db(Math.max(pk, 1e-9)).toFixed(1) + ' dBTP</b></span>');
pbar.style.width = '100%';
setTimeout(function () { pbar.style.width = '0%'; }, 2000);
dibujarWave();
}
function medirLUFS(LL, RR, sr) {
// Medición corregida a BS.1770: K-weighting (HP 40Hz + shelf +4dB @ 1.5kHz)
// y gate relativo -10 LU en 2 pasadas. Evita pedir ganancia excesiva.
var hpL = biquad('hp', 40, sr, 0.7, 0);
var hpR = biquad('hp', 40, sr, 0.7, 0);
var shL = biquad('hs', 1500, sr, 0.6, 4);
var shR = biquad('hs', 1500, sr, 0.6, 4);
var block = Math.floor(sr * 0.4);
var pows = [];
var acc = 0, cnt = 0;
var n = LL.length;
for (var i = 0; i < n; i++) {
var l = shL(hpL(LL[i]));
var r = shR(hpR(RR[i]));
acc += l * l + r * r;
cnt += 2;
if ((i + 1) % block === 0) {
pows.push(acc / cnt);
acc = 0; cnt = 0;
}
}
if (cnt > 0) pows.push(acc / cnt);
pows = pows.filter(function (p) { return p > 0; });
if (!pows.length) return -60;
function gate(pw) {
var mx = 0;
for (var m = 0; m < pw.length; m++) if (pw[m] > mx) mx = pw[m];
if (!mx) return [];
return pw.filter(function (px) { return px >= mx * Math.pow(10, -1); });
}
var keep = gate(gate(pows));
var mean = 0;
for (var q = 0; q < keep.length; q++) mean += keep[q];
mean /= keep.length || 1;
return -0.691 + 10 * Math.log10(mean);
}
// ========== WAV ==========
function buferConFaders(buf) {
var ch = Math.min(buf.numberOfChannels, 2);
var srB = buf.sampleRate;
var LL = buf.getChannelData(0);
var RR = buf.numberOfChannels > 1 ? buf.getChannelData(1) : buf.getChannelData(0);
var volGain = db2ag((estado.vol * 2 - 1) * 12);
var panV = estado.pan * 2 - 1;
var panL = panV <= 0 ? 1 : 1 - panV * 0.5;
var panR = panV >= 0 ? 1 : 1 + panV * 0.5;
var oL = new Float32Array(buf.length), oR = new Float32Array(buf.length);
for (var vi = 0; vi < buf.length; vi++) { oL[vi] = LL[vi] * volGain * panL; oR[vi] = RR[vi] * volGain * panR; }
var ceiling = db2ag(-(1 + 1.5 * estado.tp));
aplicarLimiterFinal(oL, oR, ceiling, srB);
var out = actx.createBuffer(ch, buf.length, srB);
out.getChannelData(0).set(oL);
if (ch > 1) out.getChannelData(1).set(oR);
return out;
}
function guardarWAV() {
if (!masterBuffer) { logIA('Todavía no hay master que descargar. Pulsa <b>Masterizar con IA</b> primero.'); return; }
var srPos = masterBuffer.sampleRate, chPos = Math.min(masterBuffer.numberOfChannels, 2);
var bufFader = buferConFaders(masterBuffer);
var wv = buildWAV(bufFader, srPos, chPos);
var blob = new Blob([wv], { type: 'audio/wav' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'master_ia.wav';
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 3000);
}
function buildWAV(buf, sr, ch) {
var n = buf.length;
var dataLen = n * 2 * ch;
var ab = new ArrayBuffer(44 + dataLen);
var dv = new DataView(ab);
function txtc(o, str) { for (var i = 0; i < str.length; i++) dv.setUint8(o + i, str.charCodeAt(i)); }
txtc(0, 'RIFF'); dv.setUint32(4, 36 + dataLen, true); txtc(8, 'WAVE');
txtc(12, 'fmt '); dv.setUint32(16, 16, true); dv.setUint16(20, 1, true);
dv.setUint16(22, ch, true); dv.setUint32(24, sr, true);
dv.setUint32(28, sr * ch * 2, true); dv.setUint16(32, ch * 2, true); dv.setUint16(34, 16, true);
txtc(36, 'data'); dv.setUint32(40, dataLen, true);
var o = 44;
for (var i = 0; i < n; i++) {
for (var c = 0; c < ch; c++) {
var v = clampF(buf.getChannelData(c)[i], -1, 1);
dv.setInt16(o, v < 0 ? v * 32768 : v * 32767, true);
o += 2;
}
}
return ab;
}
})();
</script>
</body>
</html>