<!DOCTYPE html>
<html lang="es" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ROCK FM 104.5 • Señal en Prueba</title>
<!-- Tailwind CSS para diseño moderno y responsivo -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Google Fonts: Inter y Space Grotesk para estética audaz y técnica -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Space Grotesk', 'sans-serif'],
},
colors: {
obsidian: '#08080A',
charcoal: '#121216',
ember: '#FF4500', // Naranja válvula incandescente
crimson: '#E11D48', // Rojo neón estudio
},
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'spin-slow': 'spin 12s linear infinite',
}
}
}
}
</script>
<style>
/* Estilos personalizados para estética de equipo de audio analógico/digital */
:root {
--accent-glow: rgba(255, 69, 0, 0.35);
--accent-color: #FF4500;
}
body {
background-color: #08080A;
background-image:
radial-gradient(circle at 50% 0%, rgba(255, 69, 0, 0.08) 0%, transparent 55%),
radial-gradient(circle at 85% 90%, rgba(225, 29, 72, 0.05) 0%, transparent 45%);
color: #F4F4F5;
overflow-x: hidden;
}
/* Textura sutil de grano analógico */
.noise-overlay {
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.025;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
z-index: 50;
}
/* Control deslizante de volumen minimalista */
input[type="range"] {
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
height: 4px;
border-radius: 999px;
background: #27272A;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
margin-top: -5px;
background-color: #FF4500;
height: 14px;
width: 14px;
border-radius: 50%;
box-shadow: 0 0 10px rgba(255, 69, 0, 0.7);
transition: transform 0.15s ease, background-color 0.15s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.25);
background-color: #FF6A33;
}
/* Resplandor del botón principal */
.btn-glow {
box-shadow: 0 0 30px -5px rgba(255, 69, 0, 0.4), inset 0 1px 1px rgba(255, 255, 255, 0.2);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.btn-glow:hover {
box-shadow: 0 0 45px 0px rgba(255, 69, 0, 0.65), inset 0 1px 1px rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.btn-glow:active {
transform: translateY(0px) scale(0.97);
}
/* Barras del ecualizador */
.eq-bar {
transition: height 0.08s ease-out;
border-radius: 999px;
}
</style>
</head>
<body class="min-h-screen flex flex-col justify-between antialiased selection:bg-ember selection:text-white">
<!-- Textura sutil de estudio -->
<div class="noise-overlay"></div>
<!-- =====================================================================
CONFIGURACIÓN DE STREAMING (ICECAST / SHOUTCAST)
INSTRUCCIONES PARA EL ADMINISTRADOR:
Reemplaza el atributo `src=""` dentro de la etiqueta <audio> inferior
con la URL directa de tu servidor de streaming.
Ejemplo: src="https://tu-servidor-icecast.com:8000/stream"
Si se deja vacío, el reproductor usará un sintetizador ambiental de
prueba incorporado para demostrar el funcionamiento al instante.
====================================================================== -->
<audio id="radio-stream" preload="none" crossorigin="anonymous" src=""></audio>
<!-- CABECERA MINIMALISTA -->
<header class="w-full max-w-4xl mx-auto px-6 pt-8 pb-4 flex items-center justify-between">
<div class="flex items-center gap-2.5">
<!-- Icono SVG minimalista de dial / vinilo -->
<div class="w-8 h-8 rounded-full border border-zinc-800 bg-zinc-900/80 flex items-center justify-center">
<div class="w-2.5 h-2.5 rounded-full bg-ember shadow-[0_0_8px_#FF4500]"></div>
</div>
<span class="font-display text-xs tracking-[0.25em] text-zinc-400 uppercase">Frecuencia Modulada</span>
</div>
<!-- Insignia pulsante de Testing -->
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-zinc-900/90 border border-zinc-800/80 backdrop-blur-sm">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-ember opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-ember"></span>
</span>
<span class="text-[11px] font-medium tracking-[0.15em] text-zinc-300 uppercase">Señal en Prueba • Testing</span>
</div>
</header>
<!-- CONTENIDO PRINCIPAL (REPRODUCTOR CENTRAL) -->
<main class="w-full max-w-xl mx-auto px-6 my-auto py-8 flex flex-col items-center text-center">
<!-- Identidad de la Emisora -->
<div class="mb-8">
<p class="text-xs font-medium tracking-[0.35em] text-ember uppercase mb-2">Transmisión Experimental</p>
<h1 class="font-display text-5xl sm:text-7xl font-bold tracking-tight text-white">
ROCK <span class="text-transparent bg-clip-text bg-gradient-to-r from-ember to-crimson">104.5</span>
</h1>
<p class="mt-3 text-sm sm:text-base text-zinc-400 font-light max-w-md mx-auto leading-relaxed">
Calibrando transmisores y procesadores de audio. Muy pronto encendemos la programación oficial sin filtros.
</p>
</div>
<!-- Tarjeta del Reproductor Minimalista -->
<div class="w-full rounded-3xl bg-charcoal/90 border border-zinc-800/80 p-7 sm:p-9 shadow-2xl backdrop-blur-md relative overflow-hidden">
<!-- Reflejo superior estilo chasis metálico -->
<div class="absolute top-0 inset-x-0 h-px bg-gradient-to-r from-transparent via-zinc-700/50 to-transparent"></div>
<!-- Estado "Ahora Sonando" -->
<div class="flex flex-col items-center mb-7">
<span id="status-badge" class="text-[11px] font-semibold tracking-[0.2em] uppercase text-zinc-500 mb-1.5 transition-colors">
Listo para conectar
</span>
<h2 id="now-playing" class="text-base sm:text-lg font-medium text-zinc-100 tracking-tight">
Transmisión de prueba — Rock Clásico & Alternativo
</h2>
</div>
<!-- Visualizador de Ondas / Ecualizador Minimalista -->
<div id="visualizer" class="h-14 flex items-center justify-center gap-1.5 sm:gap-2 mb-8 px-4" aria-hidden="true">
<!-- 20 barras generadas dinámicamente por JS -->
</div>
<!-- Controles Principales -->
<div class="flex flex-col sm:flex-row items-center justify-between gap-6 pt-2 border-t border-zinc-800/60">
<!-- Control de Volumen Izquierdo -->
<div class="flex items-center gap-3 w-full sm:w-36 order-2 sm:order-1 justify-center sm:justify-start">
<button id="mute-btn" type="button" aria-label="Silenciar" class="text-zinc-400 hover:text-white transition-colors p-1.5 rounded-lg">
<!-- Icono Volumen Activo -->
<svg id="icon-vol-on" class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5L6 9H2v6h4l5 4V5zM15.54 8.46a5 5 0 010 7.07M19.07 4.93a10 10 0 010 14.14"/>
</svg>
<!-- Icono Mute -->
<svg id="icon-vol-off" class="w-5 h-5 hidden text-crimson" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5L6 9H2v6h4l5 4V5zM23 9l-6 6M17 9l6 6"/>
</svg>
</button>
<input id="volume-slider" type="range" min="0" max="1" step="0.01" value="0.8" aria-label="Control de volumen" class="w-24">
</div>
<!-- Botón Central Play / Pause -->
<div class="order-1 sm:order-2">
<button id="play-btn" type="button" aria-label="Reproducir o pausar radio" class="btn-glow w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-gradient-to-br from-ember to-crimson text-white flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-ember/50 focus:ring-offset-2 focus:ring-offset-charcoal">
<!-- Icono Play -->
<svg id="icon-play" class="w-7 h-7 sm:w-8 sm:h-8 ml-1" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
<!-- Icono Pause -->
<svg id="icon-pause" class="w-7 h-7 sm:w-8 sm:h-8 hidden" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
</button>
</div>
<!-- Indicador de Frecuencia / Modo -->
<div class="w-full sm:w-36 order-3 flex items-center justify-center sm:justify-end">
<span id="stream-mode-label" class="text-[11px] font-mono text-zinc-500 bg-zinc-900/90 px-2.5 py-1 rounded-md border border-zinc-800">
TEST TONE / DEMO
</span>
</div>
</div>
</div>
<!-- Acción Secundaria: Reporte de Señal -->
<div class="mt-8 flex flex-col sm:flex-row items-center gap-3">
<button id="open-report-modal" type="button" class="group inline-flex items-center gap-2.5 px-5 py-2.5 rounded-full bg-zinc-900 hover:bg-zinc-800/90 border border-zinc-800 hover:border-zinc-700 text-xs sm:text-sm font-medium text-zinc-300 hover:text-white transition-all">
<svg class="w-4 h-4 text-ember transition-transform group-hover:scale-110" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"/>
</svg>
<span>Reportar calidad de señal</span>
</button>
</div>
</main>
<!-- PIE DE PÁGINA ULTRA LIMPIO -->
<footer class="w-full max-w-4xl mx-auto px-6 py-6 border-t border-zinc-900/80 flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-zinc-500">
<!-- Estado técnico del servidor -->
<div class="flex items-center gap-2 font-mono">
<span class="w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_8px_#10b981]"></span>
<span>Servidor: <strong class="text-zinc-300 font-normal">Online</strong></span>
<span class="text-zinc-700">•</span>
<span>Bitrate: <strong class="text-zinc-300 font-normal">128kbps AAC</strong></span>
</div>
<!-- Redes Sociales Minimalistas (SVG) -->
<div class="flex items-center gap-5">
<a href="#" aria-label="Instagram" class="text-zinc-500 hover:text-ember transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24">
<rect width="20" height="20" x="2" y="2" rx="5" ry="5"/>
<path d="M16 11.37A4 4 0 1112.63 8 4 4 0 0116 11.37zM17.5 6.5h.01"/>
</svg>
</a>
<a href="#" aria-label="X (Twitter)" class="text-zinc-500 hover:text-ember transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
<a href="#" aria-label="WhatsApp" class="text-zinc-500 hover:text-ember transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z"/>
</svg>
</a>
</div>
</footer>
<!-- MODAL DE REPORTE DE SEÑAL / FEEDBACK -->
<div id="report-modal" class="fixed inset-0 z-40 hidden items-center justify-center p-4 bg-black/80 backdrop-blur-sm transition-opacity">
<div class="w-full max-w-md rounded-2xl bg-charcoal border border-zinc-800 p-6 shadow-2xl relative">
<div class="flex items-center justify-between mb-4">
<h3 class="font-display text-lg font-bold text-white">Reporte de Recepción</h3>
<button id="close-report-modal" type="button" aria-label="Cerrar ventana" class="text-zinc-500 hover:text-white p-1 rounded-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<p class="text-xs text-zinc-400 mb-5 leading-relaxed">
Ayúdanos a calibrar el alcance de 104.5 FM y nuestro servidor digital. Cuéntanos desde dónde nos escuchas y cómo llega el audio.
</p>
<form id="report-form" class="space-y-4">
<div>
<label for="listener-zone" class="block text-xs font-medium text-zinc-400 mb-1.5">Zona / Ciudad o Dispositivo</label>
<input id="listener-zone" type="text" required placeholder="Ej. Zona Norte / Auto / Web móvil" class="w-full rounded-xl bg-zinc-900 border border-zinc-800 px-3.5 py-2.5 text-sm text-white placeholder-zinc-600 focus:border-ember focus:outline-none">
</div>
<div>
<label class="block text-xs font-medium text-zinc-400 mb-1.5">Calidad de Audio</label>
<div class="grid grid-cols-3 gap-2">
<label class="cursor-pointer">
<input type="radio" name="quality" value="Nítida / Potente" class="peer sr-only" checked>
<div class="text-center text-xs py-2 px-2 rounded-xl bg-zinc-900 border border-zinc-800 text-zinc-400 peer-checked:border-ember peer-checked:text-white peer-checked:bg-ember/10 transition-all">
Nítida
</div>
</label>
<label class="cursor-pointer">
<input type="radio" name="quality" value="Con cortes leves" class="peer sr-only">
<div class="text-center text-xs py-2 px-2 rounded-xl bg-zinc-900 border border-zinc-800 text-zinc-400 peer-checked:border-ember peer-checked:text-white peer-checked:bg-ember/10 transition-all">
Cortes leves
</div>
</label>
<label class="cursor-pointer">
<input type="radio" name="quality" value="Baja / Saturada" class="peer sr-only">
<div class="text-center text-xs py-2 px-2 rounded-xl bg-zinc-900 border border-zinc-800 text-zinc-400 peer-checked:border-ember peer-checked:text-white peer-checked:bg-ember/10 transition-all">
Saturada
</div>
</label>
</div>
</div>
<div>
<label for="listener-notes" class="block text-xs font-medium text-zinc-400 mb-1.5">Observaciones (opcional)</label>
<textarea id="listener-notes" rows="2" placeholder="¿Qué banda te gustaría escuchar al aire?" class="w-full rounded-xl bg-zinc-900 border border-zinc-800 px-3.5 py-2 text-sm text-white placeholder-zinc-600 focus:border-ember focus:outline-none resize-none"></textarea>
</div>
<div class="pt-2 flex items-center justify-end gap-3">
<button type="button" id="cancel-report-btn" class="px-4 py-2 rounded-xl text-xs font-medium text-zinc-400 hover:text-white transition-colors">
Cancelar
</button>
<button type="submit" class="px-5 py-2.5 rounded-xl bg-ember hover:bg-orange-500 text-xs font-semibold text-white shadow-lg shadow-ember/25 transition-all">
Enviar Reporte
</button>
</div>
</form>
<!-- Mensaje de confirmación interno (sin alert) -->
<div id="report-success" class="hidden py-6 text-center">
<div class="w-12 h-12 rounded-full bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 flex items-center justify-center mx-auto mb-3">
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
</div>
<h4 class="font-display text-base font-bold text-white mb-1">¡Reporte recibido en cabina!</h4>
<p class="text-xs text-zinc-400">Gracias por ayudarnos a afinar el sonido de ROCK 104.5.</p>
</div>
</div>
</div>
<!-- LÓGICA DE AUDIO, VISUALIZADOR Y MODAL -->
<script>
document.addEventListener('DOMContentLoaded', () => {
// Referencias del DOM
const audioEl = document.getElementById('radio-stream');
const playBtn = document.getElementById('play-btn');
const iconPlay = document.getElementById('icon-play');
const iconPause = document.getElementById('icon-pause');
const volumeSlider = document.getElementById('volume-slider');
const muteBtn = document.getElementById('mute-btn');
const iconVolOn = document.getElementById('icon-vol-on');
const iconVolOff = document.getElementById('icon-vol-off');
const visualizerContainer = document.getElementById('visualizer');
const statusBadge = document.getElementById('status-badge');
const streamModeLabel = document.getElementById('stream-mode-label');
// Estado de reproducción
let isPlaying = false;
let isMuted = false;
let previousVolume = 0.8;
let animationFrameId = null;
// Variables para Web Audio API (Sintetizador de prueba cuando no hay URL externa)
let audioCtx = null;
let demoGainNode = null;
let demoOscillators = [];
let analyser = null;
let dataArray = null;
// Determinar si hay una URL real de Icecast/Shoutcast configurada
const hasStreamUrl = Boolean(audioEl.getAttribute('src') && audioEl.getAttribute('src').trim() !== '');
if (hasStreamUrl) {
streamModeLabel.textContent = 'LIVE STREAM';
streamModeLabel.classList.replace('text-zinc-500', 'text-emerald-400');
}
// 1. Generar barras del ecualizador minimalista
const BAR_COUNT = 22;
const bars = [];
for (let i = 0; i < BAR_COUNT; i++) {
const bar = document.createElement('div');
bar.className = 'eq-bar w-1 sm:w-1.5 bg-zinc-700';
bar.style.height = '6px';
visualizerContainer.appendChild(bar);
bars.push(bar);
}
// 2. Inicializar sintetizador de acorde Rock/Drone cálido para modo Demo
function startDemoAudio() {
if (!audioCtx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
// Nodo de ganancia maestro
demoGainNode = audioCtx.createGain();
demoGainNode.gain.setValueAtTime(0.001, audioCtx.currentTime);
demoGainNode.gain.exponentialRampToValueAtTime(parseFloat(volumeSlider.value) * 0.12, audioCtx.currentTime + 0.8);
// Filtro paso bajo cálido (estilo amplificador valvular)
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(420, audioCtx.currentTime);
// Analizador para mover las barras al ritmo real del audio
analyser = audioCtx.createAnalyser();
analyser.fftSize = 64;
dataArray = new Uint8Array(analyser.frequencyBinCount);
// Acorde de quinta de Rock profundo (E2 + B2 + E3) con leve desafinación analógica
const frequencies = [82.41, 123.47, 164.81];
demoOscillators = frequencies.map((freq, idx) => {
const osc = audioCtx.createOscillator();
osc.type = idx === 0 ? 'sawtooth' : 'triangle';
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
osc.detune.setValueAtTime((idx - 1) * 4, audioCtx.currentTime);
osc.connect(filter);
osc.start();
return osc;
});
filter.connect(demoGainNode);
demoGainNode.connect(analyser);
analyser.connect(audioCtx.destination);
}
function stopDemoAudio() {
if (demoGainNode && audioCtx) {
demoGainNode.gain.setTargetAtTime(0.0001, audioCtx.currentTime, 0.15);
setTimeout(() => {
demoOscillators.forEach(osc => {
try { osc.stop(); osc.disconnect(); } catch (e) {}
});
demoOscillators = [];
}, 200);
}
}
// 3. Animación del ecualizador
function renderVisualizer() {
if (!isPlaying) return;
if (analyser && dataArray && !hasStreamUrl) {
analyser.getByteFrequencyData(dataArray);
}
const time = Date.now() * 0.006;
bars.forEach((bar, index) => {
// Curva simétrica desde el centro hacia los extremos
const centerDistance = Math.abs(index - (BAR_COUNT - 1) / 2) / ((BAR_COUNT - 1) / 2);
const envelope = 1 - Math.pow(centerDistance, 1.6) * 0.7;
let intensity = 0;
if (analyser && dataArray && !hasStreamUrl) {
const bin = Math.min(index % 12, dataArray.length - 1);
const audioVal = dataArray[bin] / 255;
const wave = (Math.sin(time + index * 0.55) + 1) * 0.5;
intensity = (audioVal * 0.5 + wave * 0.5) * envelope;
} else {
// Movimiento orgánico cuando se reproduce stream externo (evita bloqueo CORS de AudioContext)
const wave1 = Math.sin(time * 1.4 + index * 0.5);
const wave2 = Math.cos(time * 2.3 - index * 0.35);
const pseudoBeat = Math.abs(wave1 * 0.6 + wave2 * 0.4);
intensity = pseudoBeat * envelope;
}
const heightPx = Math.max(6, Math.round(intensity * 52));
bar.style.height = `${heightPx}px`;
// Color dinámico según intensidad
if (heightPx > 34) {
bar.style.backgroundColor = '#FF4500';
bar.style.boxShadow = '0 0 10px rgba(255, 69, 0, 0.6)';
} else if (heightPx > 18) {
bar.style.backgroundColor = '#E11D48';
bar.style.boxShadow = 'none';
} else {
bar.style.backgroundColor = '#52525B';
bar.style.boxShadow = 'none';
}
});
animationFrameId = requestAnimationFrame(renderVisualizer);
}
function resetVisualizer() {
cancelAnimationFrame(animationFrameId);
bars.forEach(bar => {
bar.style.height = '6px';
bar.style.backgroundColor = '#3F3F46';
bar.style.boxShadow = 'none';
});
}
// 4. Alternar Play / Pause
async function togglePlay() {
if (!isPlaying) {
try {
statusBadge.textContent = 'Conectando señal...';
statusBadge.classList.replace('text-zinc-500', 'text-ember');
if (hasStreamUrl) {
audioEl.volume = isMuted ? 0 : parseFloat(volumeSlider.value);
await audioEl.play();
} else {
startDemoAudio();
}
isPlaying = true;
iconPlay.classList.add('hidden');
iconPause.classList.remove('hidden');
statusBadge.textContent = 'Al aire • Señal Activa';
renderVisualizer();
} catch (err) {
statusBadge.textContent = 'Error de conexión • Reintentar';
isPlaying = false;
}
} else {
if (hasStreamUrl) {
audioEl.pause();
} else {
stopDemoAudio();
}
isPlaying = false;
iconPause.classList.add('hidden');
iconPlay.classList.remove('hidden');
statusBadge.textContent = 'Transmisión en pausa';
statusBadge.classList.replace('text-ember', 'text-zinc-500');
resetVisualizer();
}
}
playBtn.addEventListener('click', togglePlay);
// 5. Control de Volumen y Mute
function updateVolume(val) {
const vol = parseFloat(val);
audioEl.volume = vol;
if (demoGainNode && audioCtx) {
demoGainNode.gain.setTargetAtTime(vol * 0.12, audioCtx.currentTime, 0.05);
}
if (vol === 0) {
isMuted = true;
iconVolOn.classList.add('hidden');
iconVolOff.classList.remove('hidden');
} else {
isMuted = false;
previousVolume = vol;
iconVolOff.classList.add('hidden');
iconVolOn.classList.remove('hidden');
}
}
volumeSlider.addEventListener('input', (e) => updateVolume(e.target.value));
muteBtn.addEventListener('click', () => {
if (!isMuted) {
volumeSlider.value = 0;
updateVolume(0);
} else {
const restore = previousVolume > 0.05 ? previousVolume : 0.8;
volumeSlider.value = restore;
updateVolume(restore);
}
});
// 6. Lógica del Modal de Reporte de Señal
const reportModal = document.getElementById('report-modal');
const openModalBtn = document.getElementById('open-report-modal');
const closeModalBtn = document.getElementById('close-report-modal');
const cancelReportBtn = document.getElementById('cancel-report-btn');
const reportForm = document.getElementById('report-form');
const reportSuccess = document.getElementById('report-success');
function openModal() {
reportForm.classList.remove('hidden');
reportSuccess.classList.add('hidden');
reportModal.classList.remove('hidden');
reportModal.classList.add('flex');
}
function closeModal() {
reportModal.classList.add('hidden');
reportModal.classList.remove('flex');
}
openModalBtn.addEventListener('click', openModal);
closeModalBtn.addEventListener('click', closeModal);
cancelReportBtn.addEventListener('click', closeModal);
reportModal.addEventListener('click', (e) => {
if (e.target === reportModal) closeModal();
});
reportForm.addEventListener('submit', (e) => {
e.preventDefault();
reportForm.classList.add('hidden');
reportSuccess.classList.remove('hidden');
setTimeout(() => {
closeModal();
reportForm.reset();
}, 2200);
});
});
</script>
</body>
</html>
888888888
mmmmmmmmm
ooooooooo
nnnnnnnnn
ttttttttt
uuuuuuuuu
eeeeeeeee
sssssssss
wwwwwwwww
hhhhhhhhh
rrrrrrrrr
fffffffff
iiiiiiiii
ddddddddd
aaaaaaaaa
yyyyyyyyy
888
mmm
ooo
nnn
ttt
uuu
eee
sss
www
hhh
rrr
fff
iii
888
mmm
ooo
nnn
ttt
uuu
eee
sss
www
hhh
rrr
fff
iii
ddd
aaa
yyy
month
88
88
day
88888
88888
UTC
88
88
hour
:
88
88
minute
:
88
88
second
am
pm