{"id":420,"date":"2026-03-20T19:00:55","date_gmt":"2026-03-21T00:00:55","guid":{"rendered":"https:\/\/marcellolaquale.com\/?page_id=420"},"modified":"2026-03-23T01:13:54","modified_gmt":"2026-03-23T06:13:54","slug":"frecuencia-de-muestreo-y-bit-depth","status":"publish","type":"page","link":"https:\/\/marcellolaquale.com\/index.php\/frecuencia-de-muestreo-y-bit-depth\/","title":{"rendered":"Frecuencia de Muestreo y Bit Depth"},"content":{"rendered":"<!DOCTYPE html>\r\n<html lang=\"es\">\r\n<head>\r\n<meta charset=\"UTF-8\">\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n<title>Frecuencia de Muestreo y Bit Depth<\/title>\r\n<style>\r\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}\r\n\r\n\/* \u2500\u2500 Color tokens \u2014 work on both dark and light backgrounds \u2500\u2500 *\/\r\n:root{\r\n  --bg:          #1c1c1a;\r\n  --bg-card:     #252522;\r\n  --bg-inner:    #2e2e2b;\r\n  --bg-canvas:   #2e2e2b;\r\n  --border:      rgba(255,255,255,0.12);\r\n  --text:        #e8e6df;\r\n  --text-sub:    #b8b4ac;\r\n  --text-muted:  #9a9690;\r\n  --text-dim:    #7a7672;\r\n  --divider:     rgba(255,255,255,0.1);\r\n  --section-ttl: #c8c4bc;\r\n  --legend-txt:  #b0aca4;\r\n  --stat-label:  #a0a09a;\r\n  --ctrl-val:    #e8e6df;\r\n  --footer:      #888480;\r\n}\r\n\r\nbody{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;padding:2rem 1rem;}\r\n.wrapper{max-width:1000px;margin:0 auto;}\r\nheader{margin-bottom:2rem;}\r\n#adc-interactivo h1{!important;font-size:16px;!important;font-weight:600;!important;margin-bottom:4px;!important;color:#9a9690;}\r\n.subtitle{font-size:14px;color:var(--text-muted);}\r\n\r\n\/* Cards *\/\r\n.card{background:var(--bg-card);border:0.5px solid var(--border);border-radius:12px;padding:1.5rem;margin-bottom:1.5rem;}\r\n\r\n\/* Stats *\/\r\n.stat-row{display:flex;gap:10px;margin-bottom:1.25rem;flex-wrap:wrap;}\r\n.stat{background:var(--bg-inner);border-radius:8px;padding:0.65rem 0.9rem;flex:1;min-width:100px;border:0.5px solid var(--border);}\r\n.stat-label{font-size:11px;color:var(--stat-label);margin-bottom:3px;text-transform:uppercase;letter-spacing:0.05em;}\r\n.stat-val{font-size:17px;font-weight:500;color:var(--text);}\r\n\r\n\/* Controls *\/\r\n.ctrl-row{display:flex;align-items:center;gap:12px;margin-bottom:0.85rem;}\r\n.ctrl-label{font-size:13px;min-width:160px;font-weight:500;}\r\n.ctrl-val{font-size:13px;font-weight:500;min-width:90px;text-align:right;color:var(--ctrl-val);}\r\ninput[type=range]{flex:1;cursor:pointer;accent-color:#1d9e75;}\r\n.noise-ctrl{display:flex;align-items:center;gap:12px;margin-bottom:0.85rem;padding:0.75rem 1rem;border:0.5px solid var(--border);border-radius:8px;background:var(--bg-inner);}\r\n.noise-ctrl input[type=range]{accent-color:#ef9f27;}\r\n\r\n\/* Animations *\/\r\n@keyframes dropPulse{\r\n  0%,100%{border-color:rgba(255,230,0,0.3);box-shadow:0 0 0 0 rgba(255,230,0,0);color:rgba(255,230,0,0.6);}\r\n  50%{border-color:rgba(255,230,0,1);box-shadow:0 0 12px 4px rgba(255,230,0,0.4);color:rgba(255,230,0,1);}\r\n}\r\n@keyframes iconGlow{\r\n  0%,100%{filter:drop-shadow(0 0 4px rgba(255,230,0,0.4));transform:scale(1);}\r\n  50%{filter:drop-shadow(0 0 14px rgba(255,230,0,1));transform:scale(1.12);}\r\n}\r\n\r\n\/* Badges, section titles, legend *\/\r\n.noise-badge{font-size:11px;font-weight:500;padding:2px 8px;border-radius:4px;white-space:nowrap;background:rgba(186,117,23,0.22);color:#ef9f27;border:0.5px solid rgba(239,159,39,0.3);}\r\n.section-title{font-size:13px;font-weight:600;color:var(--section-ttl);margin-bottom:0.6rem;letter-spacing:0.04em;text-transform:uppercase;}\r\n.legend{display:flex;gap:16px;flex-wrap:wrap;margin-bottom:0.75rem;}\r\n.legend-item{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--legend-txt);}\r\n.legend-line{width:22px;height:3px;border-radius:2px;}\r\n.legend-dot{width:10px;height:10px;border-radius:50%;}\r\n\r\n\/* Canvas *\/\r\n.canvas-wrap{border:0.5px solid var(--border);border-radius:12px;overflow:hidden;background:var(--bg-canvas);margin-bottom:1.25rem;}\r\n\r\n\/* Info grid *\/\r\n.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:0;}\r\n.info-box{border:0.5px solid var(--border);border-radius:8px;padding:0.65rem 0.85rem;background:var(--bg-inner);font-size:12px;color:var(--text-sub);line-height:1.6;}\r\n.info-box strong{font-weight:600;font-size:12px;display:block;margin-bottom:3px;color:var(--text);}\r\n\r\n\/* Divider & footer *\/\r\n.divider{border:none;border-top:0.5px solid var(--divider);margin:1.5rem 0;}\r\n.footer{font-size:11px;color:var(--footer);text-align:center;margin-top:1rem;padding-bottom:1rem;}\r\n\r\n\/* Responsive *\/\r\n@media(max-width:520px){\r\n  .info-grid{grid-template-columns:1fr;}\r\n  .ctrl-label{min-width:110px;font-size:12px;}\r\n  .stat{min-width:80px;}\r\n}\r\n<\/style>\r\n<\/head>\r\n<body>\r\n<div class=\"wrapper\">\r\n  <header>\r\n    <h1>Frecuencia de Muestreo y Bit Depth<\/h1>\r\n    <p class=\"subtitle\">Herramienta educativa de digitalizaci\u00f3n de audio \u2014 modifique los par\u00e1metros y observe el impacto en la se\u00f1al<\/p>\r\n  <\/header>\r\n\r\n  <div class=\"card\" style=\"margin-bottom:1rem;border-left:2.5px solid #1d9e75;\">\r\n    <div style=\"font-size:13px;font-weight:500;color:#e8e6df;margin-bottom:0.6rem;\">C\u00f3mo usar este interactivo<\/div>\r\n    <div style=\"font-size:12px;color:#c8c4bc;line-height:1.7;\">\r\n      Esta herramienta permite explorar los dos par\u00e1metros que definen la calidad del audio digital: la <strong style=\"color:#85b7eb;font-weight:500;\">frecuencia de muestreo<\/strong> (cu\u00e1ntas veces por segundo se toma una muestra de la se\u00f1al) y el <strong style=\"color:#ef9f27;font-weight:500;\">bit depth<\/strong> (con cu\u00e1nta precisi\u00f3n se mide cada muestra).<br><br>\r\n      <strong style=\"color:#e8e6df;font-weight:600;\">Gr\u00e1fico 1 \u2014 Onda digitalizada:<\/strong> la l\u00ednea azul es la onda anal\u00f3gica original. La escalera verde es la se\u00f1al reconstruida digitalmente \u2014 sus escalones reflejan la resoluci\u00f3n del <span style=\"color:#ef9f27;font-weight:500;\">bit depth<\/span>. Los puntos \u00e1mbar son las muestras tomadas a la frecuencia elegida. A menor <span style=\"color:#85b7eb;font-weight:500;\">frecuencia de muestreo<\/span> o menor <span style=\"color:#ef9f27;font-weight:500;\">bit depth<\/span>, mayor es la distorsi\u00f3n visible.<br><br>\r\n      <strong style=\"color:#e8e6df;font-weight:600;\">Gr\u00e1fico 2 \u2014 Rango din\u00e1mico:<\/strong> muestra el espacio disponible entre el piso de ruido (zona roja) y el techo de saturaci\u00f3n en 0 dB. La zona verde es el headroom \u00fatil. Cada bit agrega 6 dB de rango. El control de ruido de fondo simula el ambiente ac\u00fastico real de la grabaci\u00f3n.<br><br>\r\n      <strong style=\"color:#e8e6df;font-weight:600;\">Laboratorio ADC:<\/strong> importe un archivo de audio real para escuchar y visualizar los efectos en tiempo real. El medidor muestra el nivel en dBFS y el analizador de espectro indica qu\u00e9 frecuencias son capturadas (azul) y cu\u00e1les son eliminadas (rojo) seg\u00fan el l\u00edmite de Nyquist.<br><br>\r\n      <span style=\"color:#9a9690;font-style:italic;\">Sugerencia did\u00e1ctica: comience con 11 025 Hz y 8 bits \u2014 la degradaci\u00f3n es clara pero la se\u00f1al sigue siendo reconocible. Luego baje a 4 000 Hz y 4 bits para demostrar el colapso del espectro.<\/span>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <div class=\"card\">\r\n    <div class=\"stat-row\">\r\n      <div class=\"stat\"><div class=\"stat-label\">Frec. muestreo<\/div><div class=\"stat-val\" id=\"sr-display\">11 025 Hz<\/div><\/div>\r\n      <div class=\"stat\"><div class=\"stat-label\">Bit depth<\/div><div class=\"stat-val\" id=\"bd-display\">8 bits<\/div><\/div>\r\n      <div class=\"stat\"><div class=\"stat-label\">Rango total<\/div><div class=\"stat-val\" id=\"dr-display\">48 dB<\/div><\/div>\r\n      <div class=\"stat\"><div class=\"stat-label\">Ruido de fondo<\/div><div class=\"stat-val\" id=\"noise-stat\" style=\"color:#ef9f27;\">20 dB<\/div><\/div>\r\n      <div class=\"stat\"><div class=\"stat-label\">Headroom \u00fatil<\/div><div class=\"stat-val\" id=\"headroom-display\" style=\"color:#5dcaa5;\">28 dB<\/div><\/div>\r\n    <\/div>\r\n\r\n    <div class=\"ctrl-row\">\r\n      <span class=\"ctrl-label\" style=\"color:#85b7eb;\">Frecuencia de muestreo<\/span>\r\n      <input type=\"range\" id=\"sr-slider\" min=\"0\" max=\"7\" step=\"1\" value=\"2\">\r\n      <span class=\"ctrl-val\" id=\"sr-label\">11 025 Hz<\/span>\r\n    <\/div>\r\n    <div class=\"ctrl-row\">\r\n      <span class=\"ctrl-label\" style=\"color:#ef9f27;\">Bit depth<\/span>\r\n      <input type=\"range\" id=\"bd-slider\" min=\"0\" max=\"5\" step=\"1\" value=\"1\">\r\n      <span class=\"ctrl-val\" id=\"bd-label\">8 bits<\/span>\r\n    <\/div>\r\n\r\n    <div class=\"legend\">\r\n      <div class=\"legend-item\"><div class=\"legend-line\" style=\"background:#85b7eb;\"><\/div>Onda original<\/div>\r\n      <div class=\"legend-item\"><div class=\"legend-line\" style=\"border-top:2px dashed #5dcaa5;background:none;height:0;\"><\/div>Se\u00f1al digital reconstruida<\/div>\r\n      <div class=\"legend-item\"><div class=\"legend-dot\" style=\"background:#ef9f27;\"><\/div>Muestras (frec. de muestreo)<\/div>\r\n      <div class=\"legend-item\"><div class=\"legend-line\" style=\"background:#ef9f27;opacity:0.4;\"><\/div>Niveles de cuantizaci\u00f3n (<span style=\"color:#ef9f27;font-weight:500;\">bit depth<\/span>)<\/div>\r\n    <\/div>\r\n\r\n    <div class=\"section-title\">Gr\u00e1fico 1 \u2014 Onda original, muestreo y cuantizaci\u00f3n<\/div>\r\n    <div class=\"canvas-wrap\">\r\n      <canvas id=\"main-canvas\" height=\"260\" style=\"width:100%;display:block;\"><\/canvas>\r\n    <\/div>\r\n\r\n    <div class=\"info-grid\">\r\n      <div class=\"info-box\">\r\n        <strong id=\"sr-title\">Frecuencia de muestreo: 11 025 Hz<\/strong>\r\n        <span id=\"sr-info\">Radio AM digital. Nyquist en ~5,5 kHz. La m\u00fasica suena opaca; los agudos desaparecen.<\/span>\r\n      <\/div>\r\n      <div class=\"info-box\">\r\n        <strong id=\"bd-title\">Bit depth: 8 bits<\/strong>\r\n        <span id=\"bd-info\">256 niveles de amplitud (2\u2078). ~48 dB de rango din\u00e1mico. El ruido de cuantizaci\u00f3n es audible en pasajes suaves.<\/span>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <hr class=\"divider\">\r\n\r\n    <div class=\"section-title\">Gr\u00e1fico 2 \u2014 Rango din\u00e1mico, ruido de fondo y headroom<\/div>\r\n    <div class=\"canvas-wrap\">\r\n      <canvas id=\"dr-canvas\" style=\"width:100%;display:block;\"><\/canvas>\r\n    <\/div>\r\n\r\n    <div class=\"noise-ctrl\" style=\"margin-top:0.85rem;\">\r\n      <span class=\"ctrl-label\" style=\"color:#f09595;\">Ruido de fondo (ambiente)<\/span>\r\n      <input type=\"range\" id=\"noise-slider\" min=\"5\" max=\"80\" step=\"1\" value=\"20\" style=\"flex:1;accent-color:#ef9f27;\">\r\n      <span class=\"ctrl-val\" id=\"noise-label\">20 dB<\/span>\r\n      <span class=\"noise-badge\" id=\"noise-badge\">Estudio pro<\/span>\r\n    <\/div>\r\n\r\n    <div class=\"info-grid\">\r\n      <div class=\"info-box\">\r\n        <strong>C\u00f3mo leer este gr\u00e1fico<\/strong>\r\n        El techo es siempre 0 dB (saturaci\u00f3n). El rango total crece hacia abajo \u2014 cada bit suma 6 dB. La zona roja es el piso de ruido; la zona verde es el headroom \u00fatil donde debe vivir la se\u00f1al. Suba el ruido de fondo para simular distintos ambientes de grabaci\u00f3n.\r\n      <\/div>\r\n      <div class=\"info-box\">\r\n        <strong id=\"dr-info-title\">8 bits \u2192 48 dB totales<\/strong>\r\n        <span id=\"dr-info-text\">Con 20 dB de ruido de fondo, la l\u00ednea de ruido queda en -61 dB. Quedan 61 dB de headroom \u00fatil hasta saturar en 0 dB.<\/span>\r\n      <\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n\r\n\r\n  <!-- \u2500\u2500 CARD 2: LABORATORIO ADC \u2500\u2500 -->\r\n  <div class=\"card\" style=\"margin-bottom:1rem;border-left:2.5px solid #1a6fa8;\">\r\n    <div style=\"font-size:15px;font-weight:500;color:#e8e6df;margin-bottom:0.5rem;\">\ud83d\udd2c Laboratorio de conversi\u00f3n ADC<\/div>\r\n\r\n    <div style=\"font-size:12px;color:#c0bcb4;line-height:1.7;margin-bottom:1.1rem;padding:0.65rem 0.85rem;border-radius:8px;background:rgba(255,255,255,0.04);border:0.5px solid #3a3a36;\">\r\n      Importe un archivo de audio real y escuche el efecto de cada par\u00e1metro. El medidor y el analizador de espectro responden en tiempo real a cualquier cambio de <span style=\"color:#85b7eb;font-weight:500;\">frecuencia de muestreo<\/span> o <span style=\"color:#ef9f27;font-weight:500;\">bit depth<\/span>.<br><br>\r\n      <strong style=\"color:#e8e6df;font-weight:600;\">Pasos:<\/strong> importe un archivo (di\u00e1logo o m\u00fasica) \u2192 presione <em>Activar<\/em> \u2192 mueva los sliders del primer apartado (<span style=\"color:#85b7eb;font-weight:500;\">Frecuencia de muestreo<\/span> y <span style=\"color:#ef9f27;font-weight:500;\">Bit depth<\/span>) y observe los cambios.<br><br>\r\n      <strong style=\"color:#e8e6df;font-weight:600;\">Medidor de nivel (dBFS):<\/strong> la barra sube hacia 0 dB cuando la se\u00f1al es m\u00e1s fuerte. La l\u00ednea azul marca el piso del rango disponible seg\u00fan el <span style=\"color:#ef9f27;font-weight:500;\">bit depth<\/span>. Si la barra supera 0 dB, el audio satura.<br><br>\r\n      <strong style=\"color:#e8e6df;font-weight:600;\">Analizador de espectro:<\/strong> las frecuencias en <strong style=\"color:#85b7eb;\">azul<\/strong> son capturadas correctamente. Las frecuencias en <strong style=\"color:#f09595;\">rojo<\/strong> quedan por encima del l\u00edmite de Nyquist (<span style=\"color:#85b7eb;font-weight:500;\">Frecuencia de muestreo<\/span> \u00f7 2) y son eliminadas por el filtro anti-aliasing. A menor <span style=\"color:#85b7eb;font-weight:500;\">frecuencia de muestreo<\/span>, m\u00e1s espectro se vuelve rojo.\r\n    <\/div>\r\n\r\n    <!-- Controls bar -->\r\n    <div style=\"display:flex;align-items:center;gap:10px;margin-bottom:1.1rem;flex-wrap:wrap;\">\r\n      <div style=\"display:flex;align-items:center;gap:6px;padding:0.35rem 0.75rem;border-radius:6px;border:0.5px solid #3a3a36;background:#1c1c1a;\">\r\n        <div id=\"rec-clip-led\" style=\"width:9px;height:9px;border-radius:50%;background:#555;transition:background 0.08s;\"><\/div>\r\n        <span style=\"font-size:11px;color:#9a9690;\" id=\"rec-clip-txt\">Sin saturaci\u00f3n<\/span>\r\n      <\/div>\r\n      <span style=\"font-size:11px;color:#7a7672;\" id=\"rec-bd-info\"><\/span>\r\n    <\/div>\r\n\r\n    <!-- Two-column layout: channel strip | spectrum -->\r\n    <div style=\"display:flex;gap:14px;align-items:flex-start;flex-wrap:wrap;\">\r\n\r\n      <!-- LEFT: Channel strip -->\r\n      <div style=\"flex:0 0 240px;min-width:200px;\">\r\n        <div style=\"background:#1e1e1c;border-radius:12px;border:0.5px solid rgba(133,183,235,0.2);padding:1rem;display:flex;flex-direction:column;align-items:center;gap:10px;\">\r\n          <div style=\"font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:#85b7eb;\">Se\u00f1al de entrada<\/div>\r\n\r\n          <div id=\"rec-ch0-drop\" onclick=\"document.getElementById('rec-ch0-file').click()\"\r\n            ondragover=\"event.preventDefault();this.style.borderColor='#85b7eb';this.style.animation='none';\"\r\n            ondragleave=\"this.style.borderColor='';this.style.animation='dropPulse 2s ease-in-out infinite';\"\r\n            ondrop=\"event.preventDefault();this.style.animation='none';recLoad(0,event.dataTransfer.files[0]);\"\r\n            style=\"width:100%;text-align:center;border:1.5px dashed rgba(255,230,0,0.3);border-radius:8px;padding:0.75rem 0.5rem;cursor:pointer;font-size:11px;color:rgba(255,230,0,0.6);transition:color 0.2s;animation:dropPulse 2s ease-in-out infinite;\">\r\n            <div style=\"font-size:38px;margin-bottom:6px;filter:drop-shadow(0 0 8px rgba(255,230,0,0.8));animation:iconGlow 2s ease-in-out infinite;\">\ud83c\udfb5<\/div>\r\n            <div id=\"rec-ch0-name\" style=\"font-weight:500;\">Arrastre un archivo o haga clic<\/div>\r\n            <div style=\"font-size:10px;opacity:0.6;margin-top:3px;\">MP3, WAV, OGG, FLAC<\/div>\r\n          <\/div>\r\n          <input type=\"file\" id=\"rec-ch0-file\" accept=\"audio\/*\" style=\"display:none;\" onchange=\"recLoad(0,this.files[0])\">\r\n          <div style=\"font-size:10px;color:#85b7eb;font-weight:600;\" id=\"rec-ch0-info\"><\/div>\r\n\r\n          <div style=\"display:flex;gap:8px;width:100%;\">\r\n            <button id=\"rec-ch0-active\" onclick=\"recToggleActive(0)\"\r\n              style=\"flex:1;padding:0.45rem 0.5rem;border-radius:6px;border:0.5px solid #5a5a56;background:transparent;color:#a8a49c;font-size:11px;font-weight:500;cursor:pointer;font-family:inherit;\">\u25b6 Activar<\/button>\r\n            <button id=\"rec-ch0-loop\" onclick=\"recToggleLoop(0)\"\r\n              style=\"flex:1;padding:0.45rem 0.5rem;border-radius:6px;border:0.5px solid rgba(93,202,165,0.4);background:rgba(93,202,165,0.08);color:#5dcaa5;font-size:11px;font-weight:500;cursor:pointer;font-family:inherit;\">\u27f3 Loop: ON<\/button>\r\n          <\/div>\r\n\r\n          <div style=\"width:100%;background:#1a1a18;border-radius:8px;padding:8px 10px;border:0.5px solid #3a3a36;\">\r\n            <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\">\r\n              <span style=\"font-size:9px;color:#9a9690;letter-spacing:0.05em;text-transform:uppercase;font-weight:500;\">Nivel de salida<\/span>\r\n              <button id=\"rec-ch0-mute\" onclick=\"recToggleMute(0)\"\r\n                style=\"padding:2px 8px;border-radius:4px;border:0.5px solid #5a5a56;background:transparent;color:#9a9690;font-size:9px;cursor:pointer;font-family:inherit;\">MUTE<\/button>\r\n            <\/div>\r\n            <div style=\"text-align:center;margin-bottom:6px;line-height:1;\">\r\n              <span style=\"font-size:28px;font-weight:700;font-variant-numeric:tabular-nums;color:#85b7eb;letter-spacing:-0.02em;\" id=\"rec-ch0-gainval\">0 dB<\/span>\r\n            <\/div>\r\n            <input type=\"range\" id=\"rec-ch0-gain\" min=\"0\" max=\"100\" step=\"1\" value=\"100\"\r\n              oninput=\"recUpdateGain(0,this.value)\"\r\n              style=\"width:100%;accent-color:#85b7eb;height:5px;\">\r\n            <div style=\"display:flex;justify-content:space-between;margin-top:3px;\">\r\n              <span style=\"font-size:8px;color:#9a9690;\">\u2212\u221e dB<\/span>\r\n              <span style=\"font-size:8px;color:#9a9690;\">0 dB<\/span>\r\n            <\/div>\r\n          <\/div>\r\n\r\n          <canvas id=\"rec-ch0-meter\" height=\"260\" style=\"width:100%;display:block;border-radius:6px;\"><\/canvas>\r\n        <\/div>\r\n      <\/div><!-- end left -->\r\n\r\n      <!-- RIGHT: Spectrum analyser -->\r\n      <div style=\"flex:1;min-width:200px;display:flex;flex-direction:column;gap:8px;\">\r\n        <div style=\"background:#1e1e1c;border-radius:12px;border:0.5px solid rgba(133,183,235,0.2);padding:1rem;\">\r\n          <div style=\"font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:0.06em;color:#85b7eb;margin-bottom:6px;\">Espectro de frecuencias<\/div>\r\n          <div style=\"display:flex;align-items:center;gap:10px;margin-bottom:8px;flex-wrap:wrap;\">\r\n            <div style=\"display:flex;align-items:center;gap:5px;\">\r\n              <div style=\"width:18px;height:2px;background:#85b7eb;border-radius:1px;\"><\/div>\r\n              <span style=\"font-size:10px;color:#85b7eb;font-weight:600;\">Frecuencias captadas<\/span>\r\n            <\/div>\r\n            <div style=\"display:flex;align-items:center;gap:5px;\">\r\n              <div style=\"width:18px;height:2px;background:#f09595;border-radius:1px;\"><\/div>\r\n              <span style=\"font-size:10px;color:#f09595;font-weight:600;\">L\u00edmite de Nyquist \u2014 frecuencias eliminadas<\/span>\r\n            <\/div>\r\n          <\/div>\r\n          <canvas id=\"rec-spectrum\" height=\"260\" style=\"width:100%;display:block;border-radius:6px;background:#0e100e;\"><\/canvas>\r\n          <div style=\"display:flex;justify-content:space-between;margin-top:4px;padding:0 2px;\">\r\n            <span style=\"font-size:8px;color:#9a9690;\">20 Hz<\/span>\r\n            <span style=\"font-size:8px;color:#9a9690;\">200 Hz<\/span>\r\n            <span style=\"font-size:8px;color:#9a9690;\">2 kHz<\/span>\r\n            <span style=\"font-size:8px;color:#9a9690;\">20 kHz<\/span>\r\n          <\/div>\r\n        <\/div>\r\n      <\/div><!-- end right -->\r\n\r\n    <\/div><!-- end two-col -->\r\n  <\/div><!-- end lab card -->\r\n\r\n  <p class=\"footer\">Interactivo educativo \u2014 Frecuencia de Muestreo &amp; Bit Depth<\/p>\r\n  <p class=\"footer\">\u00a9 Marcello Laquale - 2026<\/p>\r\n  <p class=\"footer\" style=\"margin-top:0.3rem;\">\r\n   \r\n<\/div>\r\n\r\n<script>\r\nconst srOptions=[4000,8000,11025,22050,44100,48000,96000,192000];\r\nconst srLabels=['4 000 Hz','8 000 Hz','11 025 Hz','22 050 Hz','44 100 Hz','48 000 Hz','96 000 Hz','192 000 Hz'];\r\nconst srInfos=[\r\n  'Calidad extremadamente baja. Nyquist en 2 kHz \u2014 por debajo de la voz hablada. Solo audible como efecto de demostraci\u00f3n de aliasing severo.',\r\n  'Calidad telef\u00f3nica b\u00e1sica. Nyquist en 4 kHz. Solo voz muy limitada; la m\u00fasica es irreconocible.',\r\n  'Radio AM digital. Nyquist en ~5,5 kHz. La voz es inteligible pero los agudos desaparecen. \u00datil para mostrar la degradaci\u00f3n sin llegar al colapso total.',\r\n  'Multimedia est\u00e1ndar. Nyquist en 11 kHz. Audible en parlantes peque\u00f1os, pero falta definici\u00f3n en agudos.',\r\n  'Est\u00e1ndar CD y streaming. Nyquist en 22 kHz \u2014 cubre todo el rango auditivo humano (20\u201320.000 Hz). Referencia de calidad para consumo.',\r\n  'Est\u00e1ndar profesional de video y broadcast. Nyquist en 24 kHz. Margen m\u00ednimo adicional sobre 44,1 kHz.',\r\n  'Estudio profesional. Nyquist en 48 kHz. Margen amplio para procesamiento sin degradaci\u00f3n acumulativa.',\r\n  'Masterizaci\u00f3n Hi-Res. Nyquist en 96 kHz. M\u00e1xima fidelidad te\u00f3rica; el beneficio auditivo sobre 96 kHz es t\u00e9cnicamente debatido.'\r\n];\r\nconst bdOptions=[4,8,12,16,24,32];\r\nconst bdInfos=[\r\n  'Solo 16 niveles (2\u2074). ~24 dB de rango. Distorsi\u00f3n severa y escalones muy notorios. \u00datil solo como demostraci\u00f3n extrema.',\r\n  '256 niveles (2\u2078). ~48 dB de rango. El ruido de cuantizaci\u00f3n es claramente audible en pasajes suaves y silencios.',\r\n  '4.096 niveles (2\u00b9\u00b2). ~72 dB de rango. Calidad media; se nota en se\u00f1ales d\u00e9biles pero aceptable para aplicaciones b\u00e1sicas.',\r\n  '65.536 niveles (2\u00b9\u2076). ~96 dB de rango. Est\u00e1ndar CD. Pr\u00e1cticamente transparente para escucha cotidiana.',\r\n  '16,7 millones de niveles (2\u00b2\u2074). ~144 dB de rango. Est\u00e1ndar de grabaci\u00f3n profesional; captura tanto los sonidos m\u00e1s suaves como los m\u00e1s fuertes sin artefactos.',\r\n  'Punto flotante de 32 bits. ~192 dB te\u00f3rico. No hay cuantizaci\u00f3n audible. Usado internamente en procesamiento y mezcla profesional.'\r\n];\r\nconst NOISE_CONTEXTS=[\r\n  {max:15,badge:'Sala anecoica'},{max:25,badge:'Estudio pro'},{max:35,badge:'Sala normal'},\r\n  {max:45,badge:'Oficina tranquila'},{max:55,badge:'Caf\u00e9 \/ bar'},{max:65,badge:'Tr\u00e1fico urbano'},{max:80,badge:'Concierto'}\r\n];\r\nconst REF_SOUNDS=[\r\n  {label:'Concierto rock',     db:10,  warn:true },\r\n  {label:'Tr\u00e1fico ciudad',     db:40,  warn:true },\r\n  {label:'Conversaci\u00f3n normal',db:60,  warn:false},\r\n  {label:'Sala silenciosa',    db:80,  warn:false},\r\n  {label:'Susurro',            db:100, warn:false},\r\n  {label:'Umbral auditivo',    db:140, warn:false}\r\n];\r\n\r\nlet srIdx=2,bdIdx=1,noiseDb=20;\r\n\r\nfunction getC(){\r\n  return{\r\n    orig:'#85b7eb', origFill:'rgba(133,183,235,0.12)',\r\n    digital:'#5dcaa5', sample:'#ef9f27',\r\n    sampLine:'rgba(239,159,39,0.5)',\r\n    qLine:'rgba(239,159,39,0.18)', qFill:'rgba(239,159,39,0.07)',\r\n    grid:'rgba(255,255,255,0.06)', axis:'rgba(255,255,255,0.15)',\r\n    tMuted:'rgba(255,255,255,0.35)', tSub:'rgba(255,255,255,0.5)',\r\n    satLine:'#f09595', satText:'#f09595',\r\n    headFill:'rgba(93,202,165,0.18)', headStrk:'rgba(93,202,165,0.5)', headText:'#5dcaa5',\r\n    quantFill:'rgba(239,159,39,0.22)', quantStrk:'rgba(239,159,39,0.6)', quantText:'#ef9f27',\r\n    ambFill:'rgba(240,149,149,0.20)', ambStrk:'rgba(240,149,149,0.65)', ambText:'#f09595',\r\n    refLine:'rgba(255,255,255,0.09)', refText:'rgba(255,255,255,0.38)',\r\n    refWarn:'rgba(240,149,149,0.75)', refWarnLine:'rgba(240,149,149,0.22)',\r\n    b6alt:'rgba(93,202,165,0.09)', b6line:'rgba(93,202,165,0.22)'\r\n  };\r\n}\r\n\r\nfunction waveAt(t){return 0.55*Math.sin(t)+0.28*Math.sin(t*2.3)+0.12*Math.sin(t*5.7);}\r\n\r\nfunction drawWave(){\r\n  const cv=document.getElementById('main-canvas');\r\n  const W=cv.offsetWidth||700; cv.width=W; cv.height=260;\r\n  const ctx=cv.getContext('2d'),C=getC();\r\n  const bd=bdOptions[bdIdx],levels=Math.pow(2,bd),sr=srOptions[srIdx];\r\n  const H=260,mid=H\/2,amp=mid*0.82,cycles=3;\r\n  ctx.clearRect(0,0,W,H);\r\n  \/\/ Quantization bands: each bit depth shows a distinct number of bands so the\r\n  \/\/ difference is always clearly visible \u2014 4bit=16, 8bit=32, 12bit=48, 16bit=64, 24bit=80, 32bit=96\r\n  const displayCaps=[16,32,48,64,80,96];\r\n  const dL=Math.min(levels,displayCaps[bdIdx]);\r\n  const bH=(2*amp)\/dL;\r\n  for(let i=0;i<dL;i++){\r\n    const y=mid-amp+i*bH;\r\n    if(i%2===0){ctx.fillStyle=C.qFill;ctx.fillRect(0,y,W,bH);}\r\n    ctx.strokeStyle=C.qLine;ctx.lineWidth=0.5;\r\n    ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();\r\n  }\r\n  ctx.strokeStyle=C.grid;ctx.lineWidth=0.5;\r\n  for(let g=-2;g<=2;g++){const y=mid-(g\/2)*amp*0.95;ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}\r\n  ctx.strokeStyle=C.axis;ctx.lineWidth=0.75;\r\n  ctx.beginPath();ctx.moveTo(0,mid);ctx.lineTo(W,mid);ctx.stroke();\r\n  ctx.beginPath();\r\n  for(let x=0;x<=W;x++){const t=(x\/W)*2*Math.PI*cycles,y=mid-amp*waveAt(t);x===0?ctx.moveTo(x,y):ctx.lineTo(x,y);}\r\n  ctx.lineTo(W,mid);ctx.lineTo(0,mid);ctx.closePath();\r\n  ctx.fillStyle=C.origFill;ctx.fill();\r\n  ctx.beginPath();ctx.strokeStyle=C.orig;ctx.lineWidth=2;\r\n  for(let x=0;x<=W;x++){const t=(x\/W)*2*Math.PI*cycles,y=mid-amp*waveAt(t);x===0?ctx.moveTo(x,y):ctx.lineTo(x,y);}\r\n  ctx.stroke();\r\n  const sS=Math.max(6,Math.min(Math.round(sr\/441*(W\/640)),90));\r\n  const dx=W\/sS,qS=(2*amp)\/levels,pts=[];\r\n  for(let i=0;i<=sS;i++){const x=i*dx,t=(x\/W)*2*Math.PI*cycles;pts.push({x,y:mid-Math.round(amp*waveAt(t)\/qS)*qS});}\r\n  ctx.lineWidth=1;ctx.setLineDash([3,3]);\r\n  for(const p of pts){ctx.strokeStyle=C.sampLine;ctx.beginPath();ctx.moveTo(p.x,mid);ctx.lineTo(p.x,p.y);ctx.stroke();}\r\n  ctx.setLineDash([]);\r\n  if(sS<=70){\r\n    ctx.beginPath();ctx.strokeStyle=C.digital;ctx.lineWidth=2;ctx.setLineDash([5,3]);\r\n    for(let i=0;i<pts.length-1;i++){const a=pts[i],b=pts[i+1];i===0?ctx.moveTo(a.x,a.y):null;ctx.lineTo(b.x,a.y);ctx.lineTo(b.x,b.y);}\r\n    ctx.stroke();ctx.setLineDash([]);\r\n  }else{\r\n    ctx.beginPath();ctx.strokeStyle=C.digital;ctx.lineWidth=1.5;ctx.setLineDash([4,3]);\r\n    pts.forEach((p,i)=>i===0?ctx.moveTo(p.x,p.y):ctx.lineTo(p.x,p.y));\r\n    ctx.stroke();ctx.setLineDash([]);\r\n  }\r\n  for(const p of pts){ctx.beginPath();ctx.arc(p.x,p.y,sS>50?2:3.5,0,Math.PI*2);ctx.fillStyle=C.sample;ctx.fill();}\r\n  ctx.font='11px -apple-system,sans-serif';\r\n  ctx.fillStyle=C.tMuted;ctx.textAlign='left';ctx.fillText('Muestras visibles: '+sS,10,H-9);\r\n  ctx.textAlign='right';\r\n  const lvlLabel = levels<=128 ? levels+' niveles' : levels.toLocaleString('es')+' niveles (2^'+bd+')';\r\n  ctx.fillText(lvlLabel,W-10,H-9);\r\n}\r\n\r\nfunction drawDR(){\r\n  const cv=document.getElementById('dr-canvas');\r\n  const W=cv.offsetWidth||700;\r\n  const bd=bdOptions[bdIdx],totalDb=bd*6;\r\n  const noiseCeiling=totalDb-noiseDb,headroom=Math.max(0,noiseCeiling);\r\n  const LABEL_W=62,RIGHT_W=148,BAR_W=W-LABEL_W-RIGHT_W-24;\r\n  const PX=2.4,QUANT_H=14,TOP_PAD=42,BOT_PAD=24;\r\n  const BAR_H=Math.round(totalDb*PX),H=TOP_PAD+BAR_H+QUANT_H+BOT_PAD;\r\n  cv.width=W;cv.height=H;\r\n  const ctx=cv.getContext('2d'),C=getC();\r\n  ctx.clearRect(0,0,W,H);\r\n  const bx=LABEL_W+8,by=TOP_PAD;\r\n  const noiseZonePx=Math.min(Math.round(noiseDb*PX),BAR_H);\r\n  const headPx=BAR_H-noiseZonePx,noiseZoneY=by+headPx;\r\n\r\n  \/\/ 6 dB stripes\r\n  for(let b=0;b<bd;b++){\r\n    const sH=Math.round(6*PX),sY=by+b*sH;\r\n    if(b%2===0){ctx.fillStyle=C.b6alt;ctx.fillRect(bx,sY,BAR_W,Math.min(sH,BAR_H-(b*sH)));}\r\n    if(b>0){ctx.strokeStyle=C.b6line;ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(bx,sY);ctx.lineTo(bx+BAR_W,sY);ctx.stroke();}\r\n  }\r\n  \/\/ fills\r\n  if(headPx>0){ctx.fillStyle=C.headFill;ctx.fillRect(bx,by,BAR_W,headPx);}\r\n  ctx.fillStyle=C.ambFill;ctx.fillRect(bx,noiseZoneY,BAR_W,noiseZonePx);\r\n  ctx.fillStyle=C.quantFill;ctx.fillRect(bx,by+BAR_H,BAR_W,QUANT_H);\r\n  \/\/ borders\r\n  ctx.strokeStyle=C.headStrk;ctx.lineWidth=1;ctx.strokeRect(bx,by,BAR_W,BAR_H);\r\n  \/\/ sat line\r\n  ctx.strokeStyle=C.satLine;ctx.lineWidth=2;\r\n  ctx.beginPath();ctx.moveTo(bx-4,by);ctx.lineTo(bx+BAR_W+4,by);ctx.stroke();\r\n  \/\/ noise divider\r\n  ctx.strokeStyle=C.ambStrk;ctx.lineWidth=2;ctx.setLineDash([6,3]);\r\n  ctx.beginPath();ctx.moveTo(bx-4,noiseZoneY);ctx.lineTo(bx+BAR_W+4,noiseZoneY);ctx.stroke();\r\n  ctx.setLineDash([]);\r\n  \/\/ quant line\r\n  ctx.strokeStyle=C.quantStrk;ctx.lineWidth=1;ctx.setLineDash([3,3]);\r\n  ctx.beginPath();ctx.moveTo(bx-4,by+BAR_H);ctx.lineTo(bx+BAR_W+4,by+BAR_H);ctx.stroke();\r\n  ctx.setLineDash([]);\r\n\r\n  \/\/ --- Reference sounds on the RIGHT ---\r\n  let lastY=-999;\r\n  ctx.font='10px -apple-system,sans-serif';\r\n  for(const ref of REF_SOUNDS){\r\n    if(ref.db>totalDb+8)continue;\r\n    const ry=by+ref.db*PX;\r\n    if(ry<by||ry>by+BAR_H+4)continue;\r\n    \/\/ dashed horizontal line\r\n    ctx.strokeStyle=ref.warn?C.refWarnLine:C.refLine;\r\n    ctx.lineWidth=ref.warn?1:0.75;ctx.setLineDash([3,4]);\r\n    ctx.beginPath();ctx.moveTo(bx,ry);ctx.lineTo(bx+BAR_W,ry);ctx.stroke();\r\n    ctx.setLineDash([]);\r\n    \/\/ tick on right edge\r\n    ctx.strokeStyle=ref.warn?C.refWarn:C.refText;\r\n    ctx.lineWidth=1.5;\r\n    ctx.beginPath();ctx.moveTo(bx+BAR_W,ry);ctx.lineTo(bx+BAR_W+8,ry);ctx.stroke();\r\n    \/\/ label \u2014 push down if overlap\r\n    let ly=ry;\r\n    if(ly<lastY+13)ly=lastY+13;\r\n    \/\/ connector if pushed\r\n    if(Math.abs(ly-ry)>3){\r\n      ctx.strokeStyle=ref.warn?C.refWarn:C.refText;\r\n      ctx.lineWidth=0.5;ctx.globalAlpha=0.4;\r\n      ctx.beginPath();ctx.moveTo(bx+BAR_W+8,ry);ctx.lineTo(bx+BAR_W+8,ly);ctx.stroke();\r\n      ctx.globalAlpha=1;\r\n    }\r\n    ctx.fillStyle=ref.warn?C.refWarn:C.refText;\r\n    ctx.textAlign='left';\r\n    ctx.fillText(ref.label,bx+BAR_W+12,ly+4);\r\n    lastY=ly;\r\n  }\r\n\r\n  \/\/ Y-axis dB labels\r\n  const step=totalDb<=48?6:totalDb<=96?12:totalDb<=144?24:32;\r\n  ctx.font='10px -apple-system,sans-serif';\r\n  for(let db=0;db<=totalDb;db+=step){\r\n    const y=by+db*PX;\r\n    ctx.fillStyle=C.tSub;ctx.textAlign='right';ctx.fillText('-'+db+' dB',bx-4,y+4);\r\n    if(db>0){ctx.strokeStyle=C.grid;ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(bx,y);ctx.lineTo(bx+BAR_W,y);ctx.stroke();}\r\n  }\r\n  \/\/ 0 dB \/ SAT label\r\n  ctx.font='500 11px -apple-system,sans-serif';\r\n  ctx.fillStyle=C.satText;ctx.textAlign='right';ctx.fillText('0 dB',bx-4,by+4);\r\n  ctx.textAlign='left';ctx.fillText('SATURACI\u00d3N',bx+6,by-10);\r\n  \/\/ noise ceiling label\r\n  ctx.font='500 10px -apple-system,sans-serif';\r\n  ctx.fillStyle=C.ambText;ctx.textAlign='right';ctx.fillText('-'+noiseCeiling+' dB',bx-4,noiseZoneY+4);\r\n  \/\/ bottom label\r\n  ctx.font='10px -apple-system,sans-serif';\r\n  ctx.fillStyle=C.quantText;ctx.textAlign='right';ctx.fillText('-'+totalDb+' dB',bx-4,by+BAR_H+QUANT_H\/2+4);\r\n  ctx.textAlign='left';ctx.fillText('piso de cuantizaci\u00f3n',bx+6,by+BAR_H+QUANT_H\/2+4);\r\n  \/\/ headroom zone label\r\n  if(headPx>26){\r\n    ctx.font='500 '+(headPx>70?'20':headPx>40?'15':'11')+'px -apple-system,sans-serif';\r\n    ctx.fillStyle=C.headText;ctx.textAlign='center';\r\n    ctx.fillText(headroom+' dB',bx+BAR_W\/2,by+headPx\/2+(headPx>40?-6:4));\r\n    if(headPx>40){ctx.font='11px -apple-system,sans-serif';ctx.fillText('headroom \u00fatil',bx+BAR_W\/2,by+headPx\/2+12);}\r\n  }else if(headPx>12){\r\n    ctx.font='10px -apple-system,sans-serif';ctx.fillStyle=C.headText;ctx.textAlign='center';\r\n    ctx.fillText(headroom+' dB \u00fatil',bx+BAR_W\/2,by+headPx\/2+4);\r\n  }\r\n  \/\/ noise zone label\r\n  if(noiseZonePx>26){\r\n    ctx.font='500 '+(noiseZonePx>60?'14':'11')+'px -apple-system,sans-serif';\r\n    ctx.fillStyle=C.ambText;ctx.textAlign='center';\r\n    ctx.fillText(noiseDb+' dB',bx+BAR_W\/2,noiseZoneY+noiseZonePx\/2+(noiseZonePx>40?-5:4));\r\n    if(noiseZonePx>40){ctx.font='10px -apple-system,sans-serif';ctx.fillText('ruido de fondo',bx+BAR_W\/2,noiseZoneY+noiseZonePx\/2+10);}\r\n  }else if(noiseZonePx>12){\r\n    ctx.font='10px -apple-system,sans-serif';ctx.fillStyle=C.ambText;ctx.textAlign='center';\r\n    ctx.fillText('ruido '+noiseDb+' dB',bx+BAR_W\/2,noiseZoneY+noiseZonePx\/2+4);\r\n  }\r\n  \/\/ top-right total annotation\r\n  ctx.font='11px -apple-system,sans-serif';\r\n  ctx.fillStyle=C.tMuted;ctx.textAlign='left';\r\n  ctx.fillText(totalDb+' dB totales ('+bd+' bits \u00d7 6)',bx+BAR_W+12,by+12);\r\n}\r\n\r\nfunction noiseCtx(db){for(const c of NOISE_CONTEXTS)if(db<=c.max)return c;return NOISE_CONTEXTS[NOISE_CONTEXTS.length-1];}\r\nfunction headColor(h){return h<=0?'#f09595':h<15?'#f09595':h<40?'#ef9f27':'#5dcaa5';}\r\n\r\nfunction update(){\r\n  srIdx=parseInt(document.getElementById('sr-slider').value);\r\n  bdIdx=parseInt(document.getElementById('bd-slider').value);\r\n  noiseDb=parseInt(document.getElementById('noise-slider').value);\r\n  const bd=bdOptions[bdIdx],totalDb=bd*6,noiseCeiling=totalDb-noiseDb,headroom=Math.max(0,noiseCeiling);\r\n  const nc=noiseCtx(noiseDb);\r\n  document.getElementById('sr-display').textContent=srLabels[srIdx];\r\n  document.getElementById('bd-display').textContent=bd+' bits';\r\n  document.getElementById('dr-display').textContent=totalDb+' dB';\r\n  document.getElementById('noise-stat').textContent=noiseDb+' dB';\r\n  document.getElementById('noise-label').textContent=noiseDb+' dB';\r\n  const badge=document.getElementById('noise-badge');\r\n  badge.textContent=nc.badge;\r\n  const hi=noiseDb>=55;\r\n  badge.style.background=hi?'rgba(163,45,45,0.2)':'rgba(186,117,23,0.18)';\r\n  badge.style.color=hi?'#f09595':'#ef9f27';\r\n  const hEl=document.getElementById('headroom-display');\r\n  hEl.textContent=headroom+' dB';hEl.style.color=headColor(headroom);\r\n  document.getElementById('sr-label').textContent=srLabels[srIdx];\r\n  document.getElementById('bd-label').textContent=bd+' bits';\r\n  document.getElementById('sr-title').textContent='Frecuencia de muestreo: '+srLabels[srIdx];\r\n  document.getElementById('bd-title').textContent='Bit depth: '+bd+' bits';\r\n  document.getElementById('sr-info').textContent=srInfos[srIdx];\r\n  document.getElementById('bd-info').textContent=bdInfos[bdIdx];\r\n  document.getElementById('dr-info-title').textContent=bd+' bits \u2192 '+totalDb+' dB totales';\r\n  document.getElementById('dr-info-text').textContent=\r\n    'El ruido de fondo de '+noiseDb+' dB ocupa la parte inferior del rango. '+\r\n    'La l\u00ednea de ruido queda en -'+noiseCeiling+' dB. '+\r\n    (headroom<=0?'El ruido supera el rango disponible: la se\u00f1al queda completamente enmascarada.':\r\n    'Quedan '+headroom+' dB de headroom \u00fatil hasta saturar en 0 dB. '+\r\n    (headroom<15?'Margen cr\u00edtico.':headroom<40?'Margen moderado.':headroom<80?'Margen profesional c\u00f3modo.':'Margen amplio, ideal para masterizaci\u00f3n.'));\r\n  const tot=totalDb,PX=2.4,QUANT_H=14,TOP_PAD=42,BOT_PAD=24;\r\n  document.getElementById('dr-canvas').style.height=(TOP_PAD+Math.round(tot*PX)+QUANT_H+BOT_PAD)+'px';\r\n  drawWave();drawDR();\r\n}\r\n\r\ndocument.addEventListener('DOMContentLoaded',function(){\r\n  \/\/ Diagnostic: check all required elements exist\r\n  var missing=[];\r\n  ['main-canvas','dr-canvas','sr-slider','bd-slider','noise-slider',\r\n   'sr-display','bd-display','dr-display','noise-stat','headroom-display'].forEach(function(id){\r\n    if(!document.getElementById(id)) missing.push(id);\r\n  });\r\n  if(missing.length>0){\r\n    console.error('ADC Lab: elementos faltantes \u2192',missing.join(', '));\r\n    \/\/ Show visible error on page\r\n    var err=document.createElement('div');\r\n    err.style.cssText='background:#f09595;color:#1a1a18;padding:1rem;border-radius:8px;font-family:monospace;font-size:12px;margin:1rem 0;';\r\n    err.textContent='Error: elementos no encontrados: '+missing.join(', ')+'. Verifique que el HTML est\u00e1 completo.';\r\n    document.body.appendChild(err);\r\n    return;\r\n  }\r\n  console.log('ADC Lab: todos los elementos encontrados, iniciando...');\r\n  document.getElementById('sr-slider').addEventListener('input',update);\r\n  document.getElementById('bd-slider').addEventListener('input',update);\r\n  document.getElementById('noise-slider').addEventListener('input',update);\r\n  window.addEventListener('resize',()=>{drawWave();drawDR();});\r\n  update();\r\n  console.log('ADC Lab: gr\u00e1ficos inicializados correctamente.');\r\n});\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\/\/ LABORATORIO DE CONVERSI\u00d3N ADC\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\r\nconst CH_COLORS=['#85b7eb','#ef9f27','#5dcaa5'];\r\nvar srSimMode='filter';\r\n\r\nconst rec={\r\n  channels:[\r\n    {buf:null,pos:0,active:false,loop:true,gainDb:0,muted:false,\r\n     scriptNode:null,meterDb:-Infinity,peakDbRaw:-Infinity,\r\n     peakHold:-Infinity,peakHoldFrames:0}\r\n  ],\r\n  playing:false, ctx:null, masterGain:null, clipper:null,\r\n  rafId:null, clipHold:0, specAnalyser:null\r\n};\r\n\r\nvar recDecodeCtx=null;\r\nfunction getRecDecodeCtx(){\r\n  if(!recDecodeCtx||recDecodeCtx.state==='closed')\r\n    recDecodeCtx=new(window.AudioContext||window.webkitAudioContext)();\r\n  if(recDecodeCtx.state==='suspended') recDecodeCtx.resume();\r\n  return recDecodeCtx;\r\n}\r\n\r\nfunction recDetectSR(raw){\r\n  if(raw[0]===0x52&&raw[1]===0x49&&raw[2]===0x46&&raw[3]===0x46&&\r\n     raw[8]===0x57&&raw[9]===0x41&&raw[10]===0x56&&raw[11]===0x45)\r\n    return raw[24]|(raw[25]<<8)|(raw[26]<<16)|(raw[27]<<24);\r\n  return 0;\r\n}\r\n\r\nfunction recLoad(idx,file){\r\n  if(!file) return;\r\n  const c=rec.channels[idx];\r\n  document.getElementById('rec-ch'+idx+'-file').value='';\r\n  const nameEl=document.getElementById('rec-ch'+idx+'-name');\r\n  nameEl.textContent='Cargando\u2026';\r\n  const reader=new FileReader();\r\n  reader.onload=function(e){\r\n    const buf=e.target.result;\r\n    const raw=new Uint8Array(buf);\r\n    const sr=recDetectSR(raw);\r\n    const ctx=getRecDecodeCtx();\r\n    ctx.decodeAudioData(buf.slice(0),\r\n      function(decoded){\r\n        c.buf=decoded; c.pos=0; c.active=false;\r\n        const fileSR=(sr>4000&&sr<400000)?sr:decoded.sampleRate;\r\n        const dur=Math.round(decoded.duration);\r\n        nameEl.textContent=file.name.length>22?file.name.slice(0,19)+'\u2026':file.name;\r\n        document.getElementById('rec-ch'+idx+'-info').textContent=\r\n          dur+'s \u00b7 '+(fileSR>=48000?'48,0':'44,1')+' kHz';\r\n        const dropEl=document.getElementById('rec-ch'+idx+'-drop');\r\n        dropEl.style.borderColor='rgba(133,183,235,0.5)';\r\n        dropEl.style.animation='none';\r\n        dropEl.style.color='#85b7eb';\r\n        const iconEl=dropEl.querySelector('div');\r\n        if(iconEl){iconEl.style.animation='none';iconEl.style.filter='none';iconEl.style.transform='none';}\r\n        document.getElementById('rec-ch'+idx+'-drop').style.color='#85b7eb';\r\n        recRefreshActiveBtn(idx,false);\r\n      },\r\n      function(){nameEl.textContent='Error \u2014 prob\u00e1 MP3 o WAV';}\r\n    );\r\n  };\r\n  reader.readAsArrayBuffer(file);\r\n}\r\n\r\nfunction recFaderToDb(pos){\r\n  const p=parseInt(pos);\r\n  if(p<=0) return -Infinity;\r\n  if(p>=100) return 0;\r\n  return parseFloat((60*Math.log10(p\/100)).toFixed(1));\r\n}\r\n\r\nfunction recUpdateGain(idx,val){\r\n  const c=rec.channels[idx];\r\n  if(c.muted) return;\r\n  const db=recFaderToDb(val);\r\n  c.gainDb=db;\r\n  const label=document.getElementById('rec-ch'+idx+'-gainval');\r\n  if(label) label.textContent=isFinite(db)?db+' dB':'\u2212\u221e dB';\r\n}\r\n\r\nfunction recToggleMute(idx){\r\n  const c=rec.channels[idx];\r\n  c.muted=!c.muted;\r\n  if(c.muted){c.gainDb=-Infinity;}\r\n  else{const sl=document.getElementById('rec-ch'+idx+'-gain');recUpdateGain(idx,sl?sl.value:100);}\r\n  const btn=document.getElementById('rec-ch'+idx+'-mute');\r\n  const lbl=document.getElementById('rec-ch'+idx+'-gainval');\r\n  if(c.muted){\r\n    if(btn){btn.style.cssText='padding:2px 8px;border-radius:4px;border:0.5px solid rgba(240,100,100,0.6);background:rgba(240,100,100,0.2);color:#f09595;font-size:9px;cursor:pointer;font-family:inherit;';}\r\n    btn.textContent='MUTED';\r\n    if(lbl)lbl.style.opacity='0.3';\r\n  } else {\r\n    if(btn){btn.style.cssText='padding:2px 8px;border-radius:4px;border:0.5px solid rgba(255,255,255,0.2);background:transparent;color:rgba(255,255,255,0.35);font-size:9px;cursor:pointer;font-family:inherit;';}\r\n    btn.textContent='MUTE';\r\n    if(lbl)lbl.style.opacity='1';\r\n  }\r\n}\r\n\r\nfunction recRefreshActiveBtn(idx,active){\r\n  const btn=document.getElementById('rec-ch'+idx+'-active');\r\n  if(!btn) return;\r\n  if(active){\r\n    btn.textContent='\u23f9 Detener';\r\n    btn.style.borderColor='rgba(240,149,149,0.5)';\r\n    btn.style.background='rgba(240,149,149,0.12)';\r\n    btn.style.color='#f09595';\r\n  } else {\r\n    btn.textContent='\u25b6 Activar';\r\n    btn.style.borderColor='rgba(255,255,255,0.18)';\r\n    btn.style.background='transparent';\r\n    btn.style.color='rgba(255,255,255,0.35)';\r\n  }\r\n}\r\n\r\nfunction recToggleActive(idx){\r\n  const c=rec.channels[idx];\r\n  if(!c.buf){\r\n    const d=document.getElementById('rec-ch'+idx+'-drop');\r\n    d.style.borderColor='#f09595';\r\n    setTimeout(function(){d.style.borderColor='rgba(133,183,235,0.2)';},600);\r\n    return;\r\n  }\r\n  c.active=!c.active;\r\n  recRefreshActiveBtn(idx,c.active);\r\n  if(c.active){\r\n    const ctx=recEnsureCtx();\r\n    if(ctx.state==='suspended'){ctx.resume().then(function(){recStartChannel(idx);});}\r\n    else{recStartChannel(idx);}\r\n  } else {\r\n    recStopChannel(idx);\r\n  }\r\n}\r\n\r\nfunction recToggleLoop(idx){\r\n  rec.channels[idx].loop=!rec.channels[idx].loop;\r\n  const btn=document.getElementById('rec-ch'+idx+'-loop');\r\n  const on=rec.channels[idx].loop;\r\n  btn.textContent=on?'\u27f3 Loop: ON':'\u2014 Loop: OFF';\r\n  btn.style.background=on?'rgba(93,202,165,0.08)':'transparent';\r\n  btn.style.borderColor=on?'rgba(93,202,165,0.4)':'rgba(255,255,255,0.15)';\r\n  btn.style.color=on?'#5dcaa5':'rgba(255,255,255,0.3)';\r\n}\r\n\r\nfunction recEnsureCtx(){\r\n  if(!rec.ctx||rec.ctx.state==='closed'){\r\n    rec.ctx=new(window.AudioContext||window.webkitAudioContext)();\r\n    rec.masterGain=rec.ctx.createGain(); rec.masterGain.gain.value=1;\r\n    rec.clipper=rec.ctx.createWaveShaper();\r\n    const n=4096,cv=new Float32Array(n);\r\n    for(let i=0;i<n;i++){const x=i*2\/(n-1)-1;cv[i]=Math.max(-1,Math.min(1,x));}\r\n    rec.clipper.curve=cv; rec.clipper.oversample='4x';\r\n    rec.masterGain.connect(rec.clipper);\r\n    rec.clipper.connect(rec.ctx.destination);\r\n  }\r\n  if(rec.ctx.state==='suspended') rec.ctx.resume();\r\n  return rec.ctx;\r\n}\r\n\r\nfunction recStartChannel(idx){\r\n  const c=rec.channels[idx];\r\n  if(!c.buf) return;\r\n  recStopChannel(idx);\r\n  const ctx=recEnsureCtx();\r\n  const BUF=2048;\r\n  const data=c.buf.getChannelData(0);\r\n  const len=data.length;\r\n  const nSR=ctx.sampleRate;\r\n\r\n  \/\/ AnalyserNode for spectrum \u2014 taps post-processing signal\r\n  const analyser=ctx.createAnalyser();\r\n  analyser.fftSize=4096;\r\n  analyser.smoothingTimeConstant=0.8;\r\n  rec.specAnalyser=analyser;\r\n\r\n  var lp1={b0:1,b1:0,b2:0,a1:0,a2:0,x1:0,x2:0,y1:0,y2:0};\r\n  var lp2={b0:1,b1:0,b2:0,a1:0,a2:0,x1:0,x2:0,y1:0,y2:0};\r\n  var holdPhase=0, holdVal=0;\r\n\r\n  function calcBW(fc,sr){\r\n    const k=Math.tan(Math.PI*fc\/sr),k2=k*k,sq=Math.SQRT2;\r\n    const d=k2+sq*k+1;\r\n    return{b0:k2\/d,b1:2*k2\/d,b2:k2\/d,a1:2*(k2-1)\/d,a2:(k2-sq*k+1)\/d,x1:0,x2:0,y1:0,y2:0};\r\n  }\r\n  function bq(f,x){\r\n    const y=f.b0*x+f.b1*f.x1+f.b2*f.x2-f.a1*f.y1-f.a2*f.y2;\r\n    f.x2=f.x1;f.x1=x;f.y2=f.y1;f.y1=y;return isNaN(y)?0:y;\r\n  }\r\n\r\n  const sp=ctx.createScriptProcessor(BUF,0,1);\r\n  sp.onaudioprocess=function(e){\r\n    const out=e.outputBuffer.getChannelData(0);\r\n    const mainBd=bdOptions[bdIdx];\r\n    const mainSr=srOptions[srIdx];\r\n    const gainLin=isFinite(c.gainDb)?Math.pow(10,c.gainDb\/20):0;\r\n    let rmsSum=0,peak=0;\r\n    for(let k=0;k<BUF;k++){\r\n      if(c.pos>=len){\r\n        if(c.loop){c.pos=0;}\r\n        else{out[k]=0;continue;}\r\n      }\r\n      let s=data[c.pos++]*gainLin;\r\n      \/\/ 1. Sample rate simulation\r\n      if(mainSr<nSR){\r\n        if(srSimMode==='filter'){\r\n          const fc=Math.max(40,mainSr*0.45);\r\n          const stage=calcBW(fc,nSR);\r\n          lp1=Object.assign({},stage,{x1:lp1.x1,x2:lp1.x2,y1:lp1.y1,y2:lp1.y2});\r\n          lp2=Object.assign({},stage,{x1:lp2.x1,x2:lp2.x2,y1:lp2.y1,y2:lp2.y2});\r\n          s=bq(lp1,s); s=bq(lp2,s);\r\n        } else {\r\n          holdPhase+=1;\r\n          if(holdPhase>=nSR\/mainSr){holdPhase-=nSR\/mainSr;holdVal=s;}\r\n          s=holdVal;\r\n        }\r\n      } else {\r\n        lp1={b0:1,b1:0,b2:0,a1:0,a2:0,x1:0,x2:0,y1:0,y2:0};\r\n        lp2={b0:1,b1:0,b2:0,a1:0,a2:0,x1:0,x2:0,y1:0,y2:0};\r\n        holdPhase=0;\r\n      }\r\n      \/\/ 2. Bit depth quantization\r\n      if(mainBd<32){\r\n        const levels=Math.pow(2,mainBd),step=2\/levels;\r\n        const useDither=(mainBd>=16);\r\n        const dither=useDither?((Math.random()+Math.random()-1)*step*0.5):0;\r\n        s=Math.round((s+dither)\/step)*step;\r\n        s=Math.max(-(1-step),Math.min(1-step,s));\r\n      }\r\n      const absS=Math.abs(s);\r\n      rmsSum+=s*s;\r\n      if(absS>peak)peak=absS;\r\n      out[k]=Math.max(-1,Math.min(1,s));\r\n    }\r\n    const rms=Math.sqrt(rmsSum\/BUF);\r\n    c.meterDb=rms>0.000031?20*Math.log10(rms):-Infinity;\r\n    c.peakDbRaw=peak>0.000031?20*Math.log10(peak):-Infinity;\r\n  };\r\n\r\n  \/\/ Chain: SP \u2192 Analyser (spectrum) \u2192 Master (clipper \u2192 destination)\r\n  sp.connect(analyser);\r\n  analyser.connect(rec.masterGain);\r\n  c.scriptNode=sp;\r\n  c.ctxStartTime=ctx.currentTime;\r\n}\r\n\r\nfunction recStopChannel(idx){\r\n  const c=rec.channels[idx];\r\n  try{if(c.scriptNode)c.scriptNode.disconnect();}catch(e){}\r\n  c.scriptNode=null;\r\n  c.meterDb=-Infinity; c.peakDbRaw=-Infinity;\r\n  rec.specAnalyser=null;\r\n}\r\n\r\n\/\/ \u2500\u2500 VU Meter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nfunction recDrawMeters(){\r\n  const mainBd=bdOptions[bdIdx];\r\n  const mainRange=mainBd*6;\r\n  const FIXED_RANGE=192, CLIP_ZONE=6, TOTAL_H_DB=FIXED_RANGE+CLIP_ZONE;\r\n\r\n  const infoEl=document.getElementById('rec-bd-info');\r\n  if(infoEl) infoEl.textContent=mainBd+' bit \u00b7 '+mainRange+' dB \u00b7 '+srLabels[srIdx];\r\n\r\n  let anyClip=false;\r\n  rec.channels.forEach(function(c,i){\r\n    const cv=document.getElementById('rec-ch'+i+'-meter');\r\n    if(!cv) return;\r\n    const W=cv.offsetWidth||200;\r\n    cv.width=W; cv.height=260;\r\n    const H=260,ctx2=cv.getContext('2d');\r\n    ctx2.clearRect(0,0,W,H);\r\n    const LBL=34,BX=LBL+2,BW=W-BX-3;\r\n    function dbToY(db){return Math.round(((CLIP_ZONE-db)\/TOTAL_H_DB)*H);}\r\n    const y0=dbToY(0);\r\n    const yFloor=dbToY(-mainRange);\r\n\r\n    \/\/ Backgrounds\r\n    ctx2.fillStyle='rgba(200,50,50,0.15)'; ctx2.fillRect(BX,0,BW,y0);\r\n    ctx2.fillStyle='#0e100e';              ctx2.fillRect(BX,y0,BW,yFloor-y0);\r\n    ctx2.fillStyle='rgba(255,255,255,0.02)';ctx2.fillRect(BX,yFloor,BW,H-yFloor);\r\n\r\n    \/\/ Grid + labels\r\n    ctx2.font='8px -apple-system,sans-serif'; ctx2.textAlign='right';\r\n    for(let db=CLIP_ZONE;db>=-FIXED_RANGE;db-=6){\r\n      const y=dbToY(db); if(y<0||y>H+1) continue;\r\n      const iz=db===0,ip=db>0,inM=db>=-mainRange;\r\n      ctx2.strokeStyle=iz?'rgba(255,255,255,0.5)':ip?'rgba(240,80,80,0.35)':inM?'rgba(255,255,255,0.08)':'rgba(255,255,255,0.025)';\r\n      ctx2.lineWidth=iz?1.5:0.5; ctx2.setLineDash(iz?[]:[2,4]);\r\n      ctx2.beginPath();ctx2.moveTo(BX,y);ctx2.lineTo(BX+BW,y);ctx2.stroke();\r\n      ctx2.setLineDash([]);\r\n      if(db%12===0||iz||db===CLIP_ZONE||db===-6){\r\n        ctx2.fillStyle=ip?'rgba(240,100,100,0.8)':iz?'rgba(255,255,255,0.8)':inM?'rgba(255,255,255,0.35)':'rgba(255,255,255,0.1)';\r\n        ctx2.fillText((db>0?'+':'')+db,LBL-1,y+3);\r\n      }\r\n    }\r\n\r\n    \/\/ Floor line\r\n    ctx2.strokeStyle='#85b7eb'; ctx2.lineWidth=2; ctx2.setLineDash([]);\r\n    ctx2.beginPath();ctx2.moveTo(BX,yFloor);ctx2.lineTo(BX+BW,yFloor);ctx2.stroke();\r\n    ctx2.fillStyle='#85b7eb'; ctx2.font='bold 8px -apple-system,sans-serif'; ctx2.textAlign='left';\r\n    ctx2.fillText('-'+mainRange+' dB',BX+2,yFloor-2);\r\n    ctx2.textAlign='right'; ctx2.font='8px -apple-system,sans-serif';\r\n\r\n    \/\/ Quant noise floor\r\n    const quantDb=-(mainRange-3);\r\n    const yQuant=dbToY(quantDb);\r\n    if(yQuant>y0&&yQuant<yFloor){\r\n      ctx2.strokeStyle='rgba(93,202,165,0.4)'; ctx2.lineWidth=1; ctx2.setLineDash([3,4]);\r\n      ctx2.beginPath();ctx2.moveTo(BX,yQuant);ctx2.lineTo(BX+BW,yQuant);ctx2.stroke();\r\n      ctx2.setLineDash([]);\r\n      ctx2.fillStyle='rgba(93,202,165,0.55)'; ctx2.font='7px -apple-system,sans-serif'; ctx2.textAlign='left';\r\n      ctx2.fillText('\u2248 piso cuant.',BX+2,yQuant-1);\r\n      ctx2.textAlign='right'; ctx2.font='8px -apple-system,sans-serif';\r\n    }\r\n\r\n    \/\/ Level\r\n    const dbRMS=(c.active&&isFinite(c.meterDb))?c.meterDb:-Infinity;\r\n    if(dbRMS>c.peakHold){c.peakHold=dbRMS;c.peakHoldFrames=120;}\r\n    else if(c.peakHoldFrames>0){c.peakHoldFrames--;}\r\n    else{c.peakHold=Math.max(-(FIXED_RANGE+3),c.peakHold-0.2);}\r\n\r\n    \/\/ Bar\r\n    if(isFinite(dbRMS)){\r\n      const dbC=Math.max(-mainRange,Math.min(CLIP_ZONE+0.5,dbRMS));\r\n      const yTop=dbToY(dbC),yBot=yFloor,bH=yBot-yTop;\r\n      if(bH>0){\r\n        const isClip=dbRMS>0;\r\n        if(isClip){\r\n          const g1=ctx2.createLinearGradient(0,y0,0,yBot);\r\n          g1.addColorStop(0,CH_COLORS[i]); g1.addColorStop(1,CH_COLORS[i]+'55');\r\n          ctx2.fillStyle=g1; ctx2.fillRect(BX,y0,BW,yBot-y0);\r\n          ctx2.fillStyle='rgba(240,60,60,0.92)'; ctx2.fillRect(BX,yTop,BW,y0-yTop);\r\n          anyClip=true;\r\n        } else {\r\n          const headroom=(dbRMS+mainRange)\/mainRange;\r\n          const g2=ctx2.createLinearGradient(0,yTop,0,yBot);\r\n          if(headroom>0.85){g2.addColorStop(0,'#ef9f27');g2.addColorStop(0.15,CH_COLORS[i]);}\r\n          else{g2.addColorStop(0,CH_COLORS[i]);}\r\n          g2.addColorStop(1,CH_COLORS[i]+'22');\r\n          ctx2.fillStyle=g2; ctx2.fillRect(BX,yTop,BW,bH);\r\n        }\r\n      }\r\n    }\r\n\r\n    \/\/ Peak hold line\r\n    if(isFinite(c.peakHold)&&c.peakHold>-(FIXED_RANGE+3)){\r\n      const yp=dbToY(Math.min(CLIP_ZONE,c.peakHold));\r\n      if(yp>=0&&yp<=H){\r\n        ctx2.strokeStyle=c.peakHold>=0?'#f09595':'rgba(255,255,255,0.85)';\r\n        ctx2.lineWidth=2; ctx2.setLineDash([]);\r\n        ctx2.beginPath();ctx2.moveTo(BX,yp);ctx2.lineTo(BX+BW,yp);ctx2.stroke();\r\n      }\r\n    }\r\n\r\n    \/\/ Readout\r\n    const readout=isFinite(dbRMS)?(dbRMS>=0?'+':'')+dbRMS.toFixed(1)+' dBFS':'\u2212\u221e dBFS';\r\n    ctx2.fillStyle='rgba(14,16,14,0.9)'; ctx2.fillRect(0,H-18,W,18);\r\n    ctx2.fillStyle=dbRMS>=0?'#f09595':CH_COLORS[i];\r\n    ctx2.font='bold 10px -apple-system,sans-serif'; ctx2.textAlign='center';\r\n    ctx2.fillText(readout,W\/2,H-5);\r\n  });\r\n\r\n  \/\/ Clip LED\r\n  if(anyClip){\r\n    rec.clipHold=60;\r\n    document.getElementById('rec-clip-led').style.background='#f09595';\r\n    document.getElementById('rec-clip-txt').textContent='\u00a1SATURACI\u00d3N!';\r\n  } else if(rec.clipHold>0){\r\n    rec.clipHold--;\r\n    if(rec.clipHold===0){\r\n      document.getElementById('rec-clip-led').style.background='rgba(255,255,255,0.15)';\r\n      document.getElementById('rec-clip-txt').textContent='Sin saturaci\u00f3n';\r\n    }\r\n  }\r\n}\r\n\r\n\/\/ \u2500\u2500 Spectrum Analyser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nfunction drawSpectrum(){\r\n  const cv=document.getElementById('rec-spectrum');\r\n  if(!cv) return;\r\n  const W=cv.offsetWidth||400;\r\n  cv.width=W; cv.height=260;\r\n  const H=260, ctx2=cv.getContext('2d');\r\n  ctx2.fillStyle='#0a0c0a'; ctx2.fillRect(0,0,W,H);\r\n\r\n  const nyquist=srOptions[srIdx]\/2;  \/\/ Nyquist frequency = SR\/2\r\n  const maxFreq=22050;               \/\/ X-axis always shows up to 22050 Hz (log scale)\r\n  const minFreq=20;\r\n\r\n  \/\/ Log-scale X helper: frequency \u2192 pixel\r\n  function freqToX(f){\r\n    return Math.round((Math.log10(f\/minFreq)\/Math.log10(maxFreq\/minFreq))*W);\r\n  }\r\n\r\n  \/\/ Grid lines at decade\/half-decade frequencies\r\n  const gridFreqs=[50,100,200,500,1000,2000,5000,10000,20000];\r\n  ctx2.strokeStyle='rgba(255,255,255,0.06)'; ctx2.lineWidth=0.5;\r\n  ctx2.font='8px -apple-system,sans-serif'; ctx2.textAlign='center';\r\n  gridFreqs.forEach(f=>{\r\n    const x=freqToX(f);\r\n    ctx2.beginPath();ctx2.moveTo(x,0);ctx2.lineTo(x,H);ctx2.stroke();\r\n    ctx2.fillStyle='rgba(255,255,255,0.2)';\r\n    const label=f>=1000?(f\/1000)+'k':f+'';\r\n    ctx2.fillText(label,x,H-3);\r\n  });\r\n\r\n  \/\/ Horizontal dB grid\r\n  for(let db=0;db>=-80;db-=20){\r\n    const y=Math.round(H*db\/(-90));\r\n    ctx2.strokeStyle='rgba(255,255,255,0.05)'; ctx2.lineWidth=0.5;\r\n    ctx2.beginPath();ctx2.moveTo(0,y);ctx2.lineTo(W,y);ctx2.stroke();\r\n    ctx2.fillStyle='rgba(255,255,255,0.2)'; ctx2.textAlign='right';\r\n    ctx2.fillText(db+' dB',W-2,y-2);\r\n  }\r\n\r\n  const nyqX=freqToX(Math.min(nyquist,maxFreq));\r\n\r\n  \/\/ Draw spectrum bars from analyser\r\n  if(rec.specAnalyser&&rec.channels[0].active){\r\n    const bufLen=rec.specAnalyser.frequencyBinCount;\r\n    const freqData=new Float32Array(bufLen);\r\n    rec.specAnalyser.getFloatFrequencyData(freqData);\r\n    const sampleRate=rec.ctx?rec.ctx.sampleRate:44100;\r\n\r\n    function drawPass(x0,x1,fill0,fill1,stroke){\r\n      ctx2.save();\r\n      ctx2.beginPath();ctx2.rect(x0,0,x1-x0,H);ctx2.clip();\r\n      const g=ctx2.createLinearGradient(0,0,0,H);\r\n      g.addColorStop(0,fill0);g.addColorStop(1,fill1);\r\n      ctx2.fillStyle=g;ctx2.beginPath();ctx2.moveTo(x0,H);\r\n      for(let i=1;i<bufLen;i++){\r\n        const freq=(i\/bufLen)*(sampleRate\/2);\r\n        if(freq<minFreq||freq>maxFreq)continue;\r\n        ctx2.lineTo(freqToX(freq),Math.round(H*Math.max(-90,freqData[i])\/(-90)));\r\n      }\r\n      ctx2.lineTo(x1,H);ctx2.closePath();ctx2.fill();\r\n      ctx2.strokeStyle=stroke;ctx2.lineWidth=1.5;ctx2.beginPath();\r\n      let first=true;\r\n      for(let i=1;i<bufLen;i++){\r\n        const freq=(i\/bufLen)*(sampleRate\/2);\r\n        if(freq<minFreq||freq>maxFreq)continue;\r\n        const x=freqToX(freq),y=Math.round(H*Math.max(-90,freqData[i])\/(-90));\r\n        if(first){ctx2.moveTo(x,y);first=false;}else{ctx2.lineTo(x,y);}\r\n      }\r\n      ctx2.stroke();ctx2.restore();\r\n    }\r\n\r\n    \/\/ Azul \u2014 frecuencias por debajo de Nyquist (captadas correctamente)\r\n    drawPass(0,nyqX,'rgba(133,183,235,0.85)','rgba(133,183,235,0.08)','rgba(133,183,235,0.95)');\r\n    \/\/ Rojo \u2014 frecuencias por encima de Nyquist (cortadas)\r\n    if(nyqX<W) drawPass(nyqX,W,'rgba(240,100,100,0.75)','rgba(240,100,100,0.05)','rgba(240,100,100,0.9)');\r\n\r\n  } else {\r\n    \/\/ No signal \u2014 show flat noise floor\r\n    ctx2.strokeStyle='rgba(133,183,235,0.15)'; ctx2.lineWidth=1;\r\n    ctx2.beginPath();ctx2.moveTo(0,H-10);ctx2.lineTo(W,H-10);ctx2.stroke();\r\n    ctx2.fillStyle='rgba(255,255,255,0.2)'; ctx2.textAlign='center'; ctx2.font='11px -apple-system,sans-serif';\r\n    ctx2.fillText('Activ\u00e1 el audio para ver el espectro',W\/2,H\/2);\r\n  }\r\n\r\n  \/\/ Nyquist line \u2014 marks the SR\/2 cutoff\r\n  if(nyqX>0&&nyqX<W){\r\n    \/\/ Shaded region above Nyquist (these frequencies are cut\/aliased)\r\n    ctx2.fillStyle='rgba(240,80,80,0.12)';\r\n    ctx2.fillRect(nyqX,0,W-nyqX,H-14);\r\n    \/\/ Nyquist line\r\n    ctx2.strokeStyle='rgba(240,100,100,0.85)'; ctx2.lineWidth=2; ctx2.setLineDash([]);\r\n    ctx2.beginPath();ctx2.moveTo(nyqX,0);ctx2.lineTo(nyqX,H-14);ctx2.stroke();\r\n    \/\/ Label\r\n    ctx2.fillStyle='#f09595'; ctx2.textAlign='left'; ctx2.font='bold 9px -apple-system,sans-serif';\r\n    const nyqLabel=nyquist>=1000?(nyquist\/1000).toFixed(1)+'k Hz':nyquist+' Hz';\r\n    const labelX=Math.min(nyqX+3,W-60);\r\n    ctx2.fillText('Nyquist: '+nyqLabel,labelX,12);\r\n    \/\/ \"zona cortada\" label\r\n    if(W-nyqX>50){\r\n      ctx2.fillStyle='rgba(240,100,100,0.5)'; ctx2.textAlign='center'; ctx2.font='9px -apple-system,sans-serif';\r\n      ctx2.fillText('zona cortada',(nyqX+W)\/2,H\/2);\r\n    }\r\n  }\r\n}\r\n\r\nfunction recLoop(){\r\n  recDrawMeters();\r\n  drawSpectrum();\r\n  rec.rafId=requestAnimationFrame(recLoop);\r\n}\r\ndocument.addEventListener('DOMContentLoaded',function(){rec.rafId=requestAnimationFrame(recLoop);});\r\n\r\n<\/script>\r\n<\/body>\r\n<\/html>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-420","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/marcellolaquale.com\/index.php\/wp-json\/wp\/v2\/pages\/420","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/marcellolaquale.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/marcellolaquale.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/marcellolaquale.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/marcellolaquale.com\/index.php\/wp-json\/wp\/v2\/comments?post=420"}],"version-history":[{"count":32,"href":"https:\/\/marcellolaquale.com\/index.php\/wp-json\/wp\/v2\/pages\/420\/revisions"}],"predecessor-version":[{"id":501,"href":"https:\/\/marcellolaquale.com\/index.php\/wp-json\/wp\/v2\/pages\/420\/revisions\/501"}],"wp:attachment":[{"href":"https:\/\/marcellolaquale.com\/index.php\/wp-json\/wp\/v2\/media?parent=420"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}