Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Neon Noise Monitor</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto:wght@300;400;500&display=swap'); | |
| :root { | |
| --primary: #00f0ff; | |
| --secondary: #ff00f0; | |
| --tertiary: #f0ff00; | |
| --bg-dark: #0a0a1a; | |
| --bg-light: #1a1a2e; | |
| --bg-darker: #050510; | |
| } | |
| body { | |
| font-family: 'Roboto', sans-serif; | |
| background-color: var(--bg-dark); | |
| color: white; | |
| overflow-x: hidden; | |
| background-image: | |
| radial-gradient(circle at 10% 20%, rgba(0, 240, 255, 0.05) 0%, transparent 20%), | |
| radial-gradient(circle at 90% 80%, rgba(255, 0, 240, 0.05) 0%, transparent 20%); | |
| } | |
| .orbitron { | |
| font-family: 'Orbitron', sans-serif; | |
| } | |
| .glow { | |
| text-shadow: 0 0 10px var(--primary), 0 0 20px var(--primary); | |
| } | |
| .glow-secondary { | |
| text-shadow: 0 0 10px var(--secondary), 0 0 20px var(--secondary); | |
| } | |
| .glow-tertiary { | |
| text-shadow: 0 0 10px var(--tertiary), 0 0 20px var(--tertiary); | |
| } | |
| .btn-glow { | |
| box-shadow: 0 0 15px var(--primary); | |
| transition: all 0.3s ease; | |
| } | |
| .btn-glow:hover { | |
| box-shadow: 0 0 25px var(--primary), 0 0 15px var(--primary); | |
| transform: translateY(-2px); | |
| } | |
| .btn-glow-secondary { | |
| box-shadow: 0 0 15px var(--secondary); | |
| transition: all 0.3s ease; | |
| } | |
| .btn-glow-secondary:hover { | |
| box-shadow: 0 0 25px var(--secondary), 0 0 15px var(--secondary); | |
| transform: translateY(-2px); | |
| } | |
| .btn-glow-tertiary { | |
| box-shadow: 0 0 15px var(--tertiary); | |
| transition: all 0.3s ease; | |
| } | |
| .btn-glow-tertiary:hover { | |
| box-shadow: 0 0 25px var(--tertiary), 0 0 15px var(--tertiary); | |
| transform: translateY(-2px); | |
| } | |
| .border-glow { | |
| border: 1px solid var(--primary); | |
| box-shadow: 0 0 10px var(--primary), inset 0 0 10px var(--primary); | |
| background: linear-gradient(135deg, rgba(0, 240, 255, 0.1), rgba(0, 240, 255, 0.05)); | |
| } | |
| .border-glow-secondary { | |
| border: 1px solid var(--secondary); | |
| box-shadow: 0 0 10px var(--secondary), inset 0 0 10px var(--secondary); | |
| background: linear-gradient(135deg, rgba(255, 0, 240, 0.1), rgba(255, 0, 240, 0.05)); | |
| } | |
| .pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 0.7; } | |
| 50% { opacity: 1; } | |
| 100% { opacity: 0.7; } | |
| } | |
| .visualizer-bar { | |
| background: linear-gradient(to top, var(--primary), var(--secondary)); | |
| width: 4px; | |
| margin: 0 2px; | |
| border-radius: 2px; | |
| transition: height 0.1s ease-out; | |
| } | |
| .noise-level-indicator { | |
| height: 10px; | |
| background: linear-gradient(to right, #00ff00, #ffff00, #ff0000); | |
| border-radius: 5px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .noise-level-indicator::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(90deg, | |
| rgba(0, 255, 0, 0.6) 0%, | |
| rgba(255, 255, 0, 0.6) 50%, | |
| rgba(255, 0, 0, 0.6) 100%); | |
| animation: shine 3s infinite linear; | |
| } | |
| @keyframes shine { | |
| 0% { transform: translateX(-100%); } | |
| 100% { transform: translateX(100%); } | |
| } | |
| .theme-selector input[type="radio"]:checked + label { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 10px var(--primary); | |
| } | |
| .peak-marker { | |
| position: absolute; | |
| width: 2px; | |
| background-color: var(--secondary); | |
| top: 0; | |
| bottom: 0; | |
| z-index: 10; | |
| } | |
| .peak-value { | |
| position: absolute; | |
| top: -20px; | |
| transform: translateX(-50%); | |
| font-size: 10px; | |
| color: var(--secondary); | |
| } | |
| .cyberpunk-box { | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .cyberpunk-box::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: linear-gradient( | |
| to bottom right, | |
| rgba(0, 240, 255, 0) 0%, | |
| rgba(0, 240, 255, 0) 30%, | |
| rgba(0, 240, 255, 0.3) 45%, | |
| rgba(0, 240, 255, 0) 60%, | |
| rgba(0, 240, 255, 0) 100% | |
| ); | |
| transform: rotate(30deg); | |
| animation: shine-overlay 6s infinite linear; | |
| } | |
| @keyframes shine-overlay { | |
| 0% { transform: translateX(-100%) rotate(30deg); } | |
| 100% { transform: translateX(100%) rotate(30deg); } | |
| } | |
| .device-selector { | |
| background-color: var(--bg-light); | |
| border: 1px solid rgba(0, 240, 255, 0.3); | |
| color: white; | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| font-size: 14px; | |
| width: 100%; | |
| transition: all 0.3s ease; | |
| } | |
| .device-selector:hover { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 10px var(--primary); | |
| } | |
| .device-selector:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 15px var(--primary); | |
| } | |
| .holographic-effect { | |
| position: relative; | |
| } | |
| .holographic-effect::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient( | |
| 135deg, | |
| rgba(0, 240, 255, 0.05) 0%, | |
| rgba(255, 0, 240, 0.05) 50%, | |
| rgba(0, 240, 255, 0.05) 100% | |
| ); | |
| pointer-events: none; | |
| z-index: -1; | |
| } | |
| .scanlines { | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .scanlines::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient( | |
| to bottom, | |
| transparent 0%, | |
| rgba(0, 240, 255, 0.05) 50%, | |
| transparent 100% | |
| ); | |
| background-size: 100% 2px; | |
| pointer-events: none; | |
| z-index: 1; | |
| } | |
| .grid-pattern { | |
| background-image: | |
| linear-gradient(rgba(0, 240, 255, 0.1) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0, 240, 255, 0.1) 1px, transparent 1px); | |
| background-size: 20px 20px; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen flex flex-col grid-pattern"> | |
| <header class="container mx-auto px-4 py-8 relative"> | |
| <div class="cyberpunk-box absolute inset-0 opacity-20"></div> | |
| <h1 class="orbitron text-5xl md:text-6xl font-bold text-center mb-2 glow tracking-wider"> | |
| <span class="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">NEON</span> | |
| <span class="text-transparent bg-clip-text bg-gradient-to-r from-purple-500 to-pink-500">NOISE</span> | |
| <span class="text-transparent bg-clip-text bg-gradient-to-r from-pink-500 to-yellow-400">MONITOR</span> | |
| </h1> | |
| <p class="text-center text-gray-400 font-light tracking-wider">REAL-TIME AMBIENT NOISE LEVEL VISUALIZATION SYSTEM</p> | |
| </header> | |
| <main class="flex-grow container mx-auto px-4 py-6"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Left Panel --> | |
| <div class="lg:col-span-1 space-y-6"> | |
| <!-- Microphone Status --> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-xl p-6 border-glow relative overflow-hidden scanlines"> | |
| <div class="absolute inset-0 bg-gradient-to-br from-blue-900/20 to-purple-900/20 opacity-30"></div> | |
| <h2 class="orbitron text-xl mb-4 flex items-center relative z-10"> | |
| <i class="fas fa-microphone mr-3 text-purple-400"></i> | |
| <span class="tracking-wider">MICROPHONE STATUS</span> | |
| </h2> | |
| <div class="flex items-center justify-between relative z-10"> | |
| <span id="micStatus" class="text-gray-300">Click start to begin</span> | |
| <div id="micStatusIndicator" class="w-4 h-4 rounded-full bg-gray-600"></div> | |
| </div> | |
| <!-- Microphone Selector --> | |
| <div class="mt-4 relative z-10"> | |
| <label for="microphoneSelect" class="block text-sm text-gray-400 mb-2 tracking-wider">SELECT INPUT DEVICE</label> | |
| <select id="microphoneSelect" class="device-selector"> | |
| <option value="">Default Microphone</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Current Session --> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-xl p-6 border-glow-secondary relative overflow-hidden scanlines"> | |
| <div class="absolute inset-0 bg-gradient-to-br from-purple-900/20 to-pink-900/20 opacity-30"></div> | |
| <h2 class="orbitron text-xl mb-4 flex items-center relative z-10"> | |
| <i class="fas fa-clock mr-3 text-blue-400"></i> | |
| <span class="tracking-wider">CURRENT SESSION</span> | |
| </h2> | |
| <div class="text-center relative z-10"> | |
| <div id="sessionTimer" class="orbitron text-3xl mb-2 glow">00:00:00</div> | |
| <div class="text-sm text-gray-400 tracking-wider">MONITORING DURATION</div> | |
| </div> | |
| </div> | |
| <!-- Controls --> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-xl p-6 border-glow relative overflow-hidden scanlines"> | |
| <div class="absolute inset-0 bg-gradient-to-br from-blue-900/20 to-cyan-900/20 opacity-30"></div> | |
| <h2 class="orbitron text-xl mb-4 flex items-center relative z-10"> | |
| <i class="fas fa-sliders-h mr-3 text-green-400"></i> | |
| <span class="tracking-wider">CONTROLS</span> | |
| </h2> | |
| <div class="flex flex-col space-y-4 relative z-10"> | |
| <button id="startBtn" class="btn-glow orbitron py-3 px-6 bg-blue-600 hover:bg-blue-700 rounded-lg font-bold transition-all tracking-wider"> | |
| <i class="fas fa-play mr-2"></i> START MONITORING | |
| </button> | |
| <button id="stopBtn" disabled class="btn-glow-secondary orbitron py-3 px-6 bg-purple-600 hover:bg-purple-700 rounded-lg font-bold transition-all tracking-wider"> | |
| <i class="fas fa-stop mr-2"></i> STOP | |
| </button> | |
| <button id="resetBtn" class="btn-glow-tertiary orbitron py-3 px-6 bg-yellow-600 hover:bg-yellow-700 rounded-lg font-bold transition-all tracking-wider"> | |
| <i class="fas fa-redo mr-2"></i> RESET DATA | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Threshold Settings --> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-xl p-6 border-glow-secondary relative overflow-hidden scanlines"> | |
| <div class="absolute inset-0 bg-gradient-to-br from-purple-900/20 to-pink-900/20 opacity-30"></div> | |
| <h2 class="orbitron text-xl mb-4 flex items-center relative z-10"> | |
| <i class="fas fa-bell mr-3 text-yellow-400"></i> | |
| <span class="tracking-wider">NOISE THRESHOLD</span> | |
| </h2> | |
| <div class="space-y-4 relative z-10"> | |
| <div> | |
| <label for="thresholdSlider" class="block text-sm text-gray-400 mb-2 tracking-wider">ALERT LEVEL (0-100)</label> | |
| <input type="range" id="thresholdSlider" min="0" max="100" value="70" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-purple-500"> | |
| <div class="flex justify-between text-xs text-gray-400 mt-1 tracking-wider"> | |
| <span>0</span> | |
| <span id="thresholdValue">70</span> | |
| <span>100</span> | |
| </div> | |
| </div> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="enableThreshold" class="mr-2 w-4 h-4 accent-purple-500" checked> | |
| <label for="enableThreshold" class="text-sm tracking-wider">ENABLE THRESHOLD ALERTS</label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Panel --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <!-- Real-time Noise Level --> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-xl p-6 border-glow relative overflow-hidden scanlines"> | |
| <div class="absolute inset-0 bg-gradient-to-br from-blue-900/20 to-cyan-900/20 opacity-30"></div> | |
| <h2 class="orbitron text-xl mb-4 flex items-center relative z-10"> | |
| <i class="fas fa-volume-up mr-3 text-red-400"></i> | |
| <span class="tracking-wider">REAL-TIME NOISE LEVEL</span> | |
| </h2> | |
| <div class="text-center mb-6 relative z-10"> | |
| <div id="noiseLevel" class="orbitron text-6xl md:text-8xl font-bold mb-2 glow pulse">--</div> | |
| <div id="noiseDescription" class="text-xl text-gray-400 tracking-wider">MICROPHONE INACTIVE</div> | |
| </div> | |
| <!-- Visualizer --> | |
| <div class="flex justify-center items-end h-32 mb-4 relative z-10" id="audioVisualizer"> | |
| <!-- Bars will be added dynamically --> | |
| </div> | |
| <!-- Noise Level Indicator --> | |
| <div class="noise-level-indicator mb-2 relative z-10"> | |
| <div id="noiseLevelBar" class="h-full bg-green-500 rounded-lg" style="width: 0%"></div> | |
| </div> | |
| <div class="flex justify-between text-xs text-gray-400 tracking-wider relative z-10"> | |
| <span>QUIET</span> | |
| <span>MODERATE</span> | |
| <span>LOUD</span> | |
| </div> | |
| <!-- Stats --> | |
| <div class="grid grid-cols-2 gap-4 mt-6 relative z-10"> | |
| <div class="bg-gray-800 bg-opacity-50 rounded-lg p-4 border border-gray-700 hover:border-blue-400 transition-all"> | |
| <div class="text-sm text-gray-400 mb-1 tracking-wider">PEAK LEVEL</div> | |
| <div id="peakLevel" class="orbitron text-2xl glow-secondary">--</div> | |
| </div> | |
| <div class="bg-gray-800 bg-opacity-50 rounded-lg p-4 border border-gray-700 hover:border-purple-400 transition-all"> | |
| <div class="text-sm text-gray-400 mb-1 tracking-wider">AVERAGE</div> | |
| <div id="averageLevel" class="orbitron text-2xl">--</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Historical Data --> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-xl p-6 border-glow-secondary relative overflow-hidden scanlines"> | |
| <div class="absolute inset-0 bg-gradient-to-br from-purple-900/20 to-pink-900/20 opacity-30"></div> | |
| <h2 class="orbitron text-xl mb-4 flex items-center relative z-10"> | |
| <i class="fas fa-chart-line mr-3 text-green-400"></i> | |
| <span class="tracking-wider">HISTORICAL DATA</span> | |
| </h2> | |
| <div class="relative h-64 z-10"> | |
| <canvas id="noiseChart"></canvas> | |
| </div> | |
| <div class="flex justify-between mt-4 relative z-10"> | |
| <div class="text-sm text-gray-400 tracking-wider">LAST 4 HOURS OF DATA</div> | |
| <div class="flex space-x-2"> | |
| <button id="smoothBtn" class="text-xs px-3 py-1 bg-gray-700 rounded hover:bg-gray-600 tracking-wider"> | |
| <i class="fas fa-wave-square mr-1"></i> SMOOTH | |
| </button> | |
| <button id="rawBtn" class="text-xs px-3 py-1 bg-gray-700 rounded hover:bg-gray-600 tracking-wider"> | |
| <i class="fas fa-chart-bar mr-1"></i> RAW | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Peak Values Dashboard --> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-xl p-6 border-glow relative overflow-hidden scanlines"> | |
| <div class="absolute inset-0 bg-gradient-to-br from-blue-900/20 to-cyan-900/20 opacity-30"></div> | |
| <h2 class="orbitron text-xl mb-4 flex items-center relative z-10"> | |
| <i class="fas fa-mountain mr-3 text-purple-400"></i> | |
| <span class="tracking-wider">PEAK VALUES (LAST 4 HOURS)</span> | |
| </h2> | |
| <div class="relative h-64 z-10"> | |
| <canvas id="peakChart"></canvas> | |
| </div> | |
| <div class="grid grid-cols-3 gap-4 mt-4 relative z-10"> | |
| <div class="bg-gray-800 bg-opacity-50 rounded-lg p-3 border border-gray-700 hover:border-blue-400 transition-all"> | |
| <div class="text-sm text-gray-400 mb-1 tracking-wider">HIGHEST PEAK</div> | |
| <div id="highestPeak" class="orbitron text-xl glow-secondary">--</div> | |
| </div> | |
| <div class="bg-gray-800 bg-opacity-50 rounded-lg p-3 border border-gray-700 hover:border-purple-400 transition-all"> | |
| <div class="text-sm text-gray-400 mb-1 tracking-wider">AVG PEAKS</div> | |
| <div id="avgPeaks" class="orbitron text-xl">--</div> | |
| </div> | |
| <div class="bg-gray-800 bg-opacity-50 rounded-lg p-3 border border-gray-700 hover:border-yellow-400 transition-all"> | |
| <div class="text-sm text-gray-400 mb-1 tracking-wider">THRESHOLD BREACHES</div> | |
| <div id="thresholdBreaches" class="orbitron text-xl">--</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <footer class="container mx-auto px-4 py-6 text-center text-gray-500 text-sm tracking-wider"> | |
| <p class="border-t border-gray-800 pt-4">NEON NOISE MONITOR v2.0 | CYBERPUNK EDITION | USES WEB AUDIO API | DATA STAYS IN YOUR BROWSER</p> | |
| </footer> | |
| <script> | |
| // DOM Elements | |
| const startBtn = document.getElementById('startBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const noiseLevel = document.getElementById('noiseLevel'); | |
| const noiseDescription = document.getElementById('noiseDescription'); | |
| const noiseLevelBar = document.getElementById('noiseLevelBar'); | |
| const micStatus = document.getElementById('micStatus'); | |
| const micStatusIndicator = document.getElementById('micStatusIndicator'); | |
| const sessionTimer = document.getElementById('sessionTimer'); | |
| const peakLevel = document.getElementById('peakLevel'); | |
| const averageLevel = document.getElementById('averageLevel'); | |
| const audioVisualizer = document.getElementById('audioVisualizer'); | |
| const thresholdSlider = document.getElementById('thresholdSlider'); | |
| const thresholdValue = document.getElementById('thresholdValue'); | |
| const enableThreshold = document.getElementById('enableThreshold'); | |
| const smoothBtn = document.getElementById('smoothBtn'); | |
| const rawBtn = document.getElementById('rawBtn'); | |
| const highestPeak = document.getElementById('highestPeak'); | |
| const avgPeaks = document.getElementById('avgPeaks'); | |
| const thresholdBreaches = document.getElementById('thresholdBreaches'); | |
| const microphoneSelect = document.getElementById('microphoneSelect'); | |
| // Audio variables | |
| let audioContext; | |
| let analyser; | |
| let microphone; | |
| let javascriptNode; | |
| let isMonitoring = false; | |
| let sessionStartTime = 0; | |
| let timerInterval; | |
| let currentStream; | |
| // Data variables | |
| let noiseData = []; | |
| let peakNoise = 0; | |
| let sumNoise = 0; | |
| let countNoise = 0; | |
| let chart; | |
| let peakChart; | |
| let chartType = 'line'; // 'line' or 'bar' | |
| let threshold = 70; | |
| let thresholdEnabled = true; | |
| let historicalPeaks = []; | |
| let thresholdBreachCount = 0; | |
| // Initialize Charts | |
| function initChart() { | |
| const ctx = document.getElementById('noiseChart').getContext('2d'); | |
| chart = new Chart(ctx, { | |
| type: chartType, | |
| data: { | |
| labels: Array(60).fill('').map((_, i) => `${i*4}m`).reverse(), | |
| datasets: [{ | |
| label: 'Noise Level', | |
| data: Array(60).fill(0), | |
| borderColor: '#00f0ff', | |
| backgroundColor: 'rgba(0, 240, 255, 0.2)', | |
| borderWidth: 2, | |
| pointRadius: 0, | |
| tension: 0.1, | |
| fill: true | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| max: 100, | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)' | |
| }, | |
| ticks: { | |
| color: 'rgba(255, 255, 255, 0.7)' | |
| } | |
| }, | |
| x: { | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)' | |
| }, | |
| ticks: { | |
| color: 'rgba(255, 255, 255, 0.7)' | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| enabled: true, | |
| mode: 'index', | |
| intersect: false, | |
| callbacks: { | |
| label: function(context) { | |
| return `Noise: ${context.parsed.y}%`; | |
| } | |
| } | |
| } | |
| }, | |
| animation: { | |
| duration: 0 | |
| } | |
| } | |
| }); | |
| } | |
| function initPeakChart() { | |
| const ctx = document.getElementById('peakChart').getContext('2d'); | |
| peakChart = new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels: Array(12).fill('').map((_, i) => `${i*20}m`).reverse(), | |
| datasets: [{ | |
| label: 'Peak Values', | |
| data: Array(12).fill(0), | |
| backgroundColor: 'rgba(255, 0, 240, 0.6)', | |
| borderColor: 'rgba(255, 0, 240, 1)', | |
| borderWidth: 1 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| max: 100, | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)' | |
| }, | |
| ticks: { | |
| color: 'rgba(255, 255, 255, 0.7)' | |
| } | |
| }, | |
| x: { | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)' | |
| }, | |
| ticks: { | |
| color: 'rgba(255, 255, 255, 0.7)' | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| label: function(context) { | |
| return `Peak: ${context.parsed.y}%`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Initialize Audio Visualizer Bars | |
| function initVisualizer() { | |
| audioVisualizer.innerHTML = ''; | |
| for (let i = 0; i < 32; i++) { | |
| const bar = document.createElement('div'); | |
| bar.className = 'visualizer-bar'; | |
| bar.style.height = '2px'; | |
| audioVisualizer.appendChild(bar); | |
| } | |
| } | |
| // Update Chart with new data | |
| function updateChart(value) { | |
| if (chart) { | |
| // Shift all data left and add new value | |
| const data = chart.data.datasets[0].data; | |
| data.shift(); | |
| data.push(value); | |
| // Update chart | |
| chart.update(); | |
| } | |
| } | |
| // Update Peak Chart with historical data | |
| function updatePeakChart() { | |
| if (!peakChart) return; | |
| // Get peaks from localStorage | |
| const storedPeaks = JSON.parse(localStorage.getItem('noiseMonitorPeaks')) || []; | |
| const recentPeaks = storedPeaks.slice(-12); // Last 12 peaks (4 hours) | |
| // Fill with zeros if not enough data | |
| while (recentPeaks.length < 12) { | |
| recentPeaks.unshift(0); | |
| } | |
| // Update chart data | |
| peakChart.data.datasets[0].data = recentPeaks; | |
| peakChart.update(); | |
| // Update stats | |
| if (recentPeaks.length > 0) { | |
| const maxPeak = Math.max(...recentPeaks.filter(p => p > 0)); | |
| const avgPeak = recentPeaks.filter(p => p > 0).reduce((a, b) => a + b, 0) / | |
| recentPeaks.filter(p => p > 0).length || 0; | |
| highestPeak.textContent = maxPeak > 0 ? maxPeak : '--'; | |
| avgPeaks.textContent = avgPeak > 0 ? Math.round(avgPeak) : '--'; | |
| // Update threshold breaches | |
| const breaches = storedPeaks.filter(p => p > threshold).length; | |
| thresholdBreaches.textContent = breaches; | |
| } | |
| } | |
| // Store peak value | |
| function storePeakValue(value) { | |
| if (value < 10) return; // Don't store very low values | |
| // Get existing peaks from localStorage | |
| const storedPeaks = JSON.parse(localStorage.getItem('noiseMonitorPeaks')) || []; | |
| // Add new peak (one per minute) | |
| const now = new Date(); | |
| const lastPeakTime = storedPeaks.length > 0 ? | |
| new Date(storedPeaks[storedPeaks.length - 1].timestamp) : null; | |
| if (!lastPeakTime || (now - lastPeakTime) > 60000) { // 1 minute | |
| storedPeaks.push({ | |
| value: value, | |
| timestamp: now.toISOString() | |
| }); | |
| // Keep only last 24 hours of data (1440 minutes) | |
| if (storedPeaks.length > 240) { // 4 hours | |
| storedPeaks.shift(); | |
| } | |
| localStorage.setItem('noiseMonitorPeaks', JSON.stringify(storedPeaks)); | |
| updatePeakChart(); | |
| } | |
| } | |
| // Update Visualizer | |
| function updateVisualizer(frequencyData) { | |
| const bars = audioVisualizer.querySelectorAll('.visualizer-bar'); | |
| bars.forEach((bar, i) => { | |
| const value = frequencyData[i] / 255; | |
| const height = value * 100; | |
| bar.style.height = `${height}px`; | |
| bar.style.opacity = 0.1 + (value * 0.9); | |
| }); | |
| } | |
| // Get noise description based on level | |
| function getNoiseDescription(level) { | |
| if (level < 20) return 'SILENT'; | |
| if (level < 40) return 'VERY QUIET'; | |
| if (level < 60) return 'MODERATE'; | |
| if (level < 80) return 'LOUD'; | |
| return 'VERY LOUD'; | |
| } | |
| // Get color based on noise level | |
| function getNoiseColor(level) { | |
| if (level < 30) return '#00ff00'; // Green | |
| if (level < 60) return '#ffff00'; // Yellow | |
| if (level < 80) return '#ff9900'; // Orange | |
| return '#ff0000'; // Red | |
| } | |
| // Populate microphone devices | |
| async function populateMicrophoneDevices() { | |
| try { | |
| // Get all audio devices | |
| const devices = await navigator.mediaDevices.enumerateDevices(); | |
| const audioInputs = devices.filter(device => device.kind === 'audioinput'); | |
| // Clear existing options | |
| microphoneSelect.innerHTML = ''; | |
| // Add default option | |
| const defaultOption = document.createElement('option'); | |
| defaultOption.value = ''; | |
| defaultOption.textContent = 'Default Microphone'; | |
| microphoneSelect.appendChild(defaultOption); | |
| // Add all available microphones | |
| audioInputs.forEach(device => { | |
| const option = document.createElement('option'); | |
| option.value = device.deviceId; | |
| option.textContent = device.label || `Microphone ${microphoneSelect.length + 1}`; | |
| microphoneSelect.appendChild(option); | |
| }); | |
| return true; | |
| } catch (error) { | |
| console.error('Error enumerating devices:', error); | |
| return false; | |
| } | |
| } | |
| // Start monitoring | |
| async function startMonitoring() { | |
| try { | |
| // Get selected device ID | |
| const deviceId = microphoneSelect.value; | |
| const constraints = deviceId ? { audio: { deviceId: { exact: deviceId } } } : { audio: true }; | |
| // Request microphone access | |
| const stream = await navigator.mediaDevices.getUserMedia(constraints); | |
| currentStream = stream; | |
| // Create audio context | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 64; | |
| // Create microphone source | |
| microphone = audioContext.createMediaStreamSource(stream); | |
| microphone.connect(analyser); | |
| // Create script processor for audio processing | |
| javascriptNode = audioContext.createScriptProcessor(2048, 1, 1); | |
| analyser.connect(javascriptNode); | |
| javascriptNode.connect(audioContext.destination); | |
| // Set up audio processing | |
| javascriptNode.onaudioprocess = function() { | |
| if (!isMonitoring) return; | |
| // Get frequency data for visualizer | |
| const frequencyData = new Uint8Array(analyser.frequencyBinCount); | |
| analyser.getByteFrequencyData(frequencyData); | |
| // Calculate overall volume level | |
| const array = new Uint8Array(analyser.frequencyBinCount); | |
| analyser.getByteTimeDomainData(array); | |
| let values = 0; | |
| for (let i = 0; i < array.length; i++) { | |
| values += Math.abs(array[i] - 128); | |
| } | |
| const average = values / array.length; | |
| const level = Math.min(Math.round(average * 2), 100); // Scale to 0-100 | |
| // Update UI | |
| requestAnimationFrame(() => { | |
| noiseLevel.textContent = level; | |
| noiseLevel.style.color = getNoiseColor(level); | |
| noiseDescription.textContent = getNoiseDescription(level); | |
| noiseLevelBar.style.width = `${level}%`; | |
| noiseLevelBar.style.backgroundColor = getNoiseColor(level); | |
| // Update visualizer | |
| updateVisualizer(frequencyData); | |
| // Update stats | |
| if (level > peakNoise) { | |
| peakNoise = level; | |
| peakLevel.textContent = peakNoise; | |
| peakLevel.style.color = getNoiseColor(peakNoise); | |
| storePeakValue(peakNoise); | |
| } | |
| sumNoise += level; | |
| countNoise++; | |
| const avg = Math.round(sumNoise / countNoise); | |
| averageLevel.textContent = avg; | |
| averageLevel.style.color = getNoiseColor(avg); | |
| // Store data | |
| noiseData.push({ | |
| time: Date.now(), | |
| level: level | |
| }); | |
| // Keep only last 4 hours of data (approx) | |
| if (noiseData.length > 240) { // 4 hours at 1 sample per minute | |
| noiseData.shift(); | |
| } | |
| // Update chart every second | |
| updateChart(level); | |
| // Check threshold | |
| if (thresholdEnabled && level > threshold) { | |
| noiseLevel.classList.add('animate-pulse'); | |
| setTimeout(() => { | |
| noiseLevel.classList.remove('animate-pulse'); | |
| }, 1000); | |
| // Count threshold breaches | |
| thresholdBreachCount++; | |
| thresholdBreaches.textContent = thresholdBreachCount; | |
| } | |
| }); | |
| }; | |
| // Update UI | |
| isMonitoring = true; | |
| startBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| micStatus.textContent = 'Microphone active'; | |
| micStatusIndicator.className = 'w-4 h-4 rounded-full bg-green-500 pulse'; | |
| // Start session timer | |
| sessionStartTime = Date.now(); | |
| updateTimer(); | |
| timerInterval = setInterval(updateTimer, 1000); | |
| // Initialize visualizer bars | |
| initVisualizer(); | |
| } catch (error) { | |
| console.error('Error accessing microphone:', error); | |
| micStatus.textContent = 'Microphone access denied'; | |
| micStatusIndicator.className = 'w-4 h-4 rounded-full bg-red-500'; | |
| } | |
| } | |
| // Stop monitoring | |
| function stopMonitoring() { | |
| if (javascriptNode) { | |
| javascriptNode.disconnect(); | |
| javascriptNode = null; | |
| } | |
| if (microphone) { | |
| microphone.disconnect(); | |
| microphone = null; | |
| } | |
| if (analyser) { | |
| analyser.disconnect(); | |
| analyser = null; | |
| } | |
| if (audioContext) { | |
| audioContext.close(); | |
| audioContext = null; | |
| } | |
| if (currentStream) { | |
| currentStream.getTracks().forEach(track => track.stop()); | |
| currentStream = null; | |
| } | |
| // Update UI | |
| isMonitoring = false; | |
| startBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| micStatus.textContent = 'Microphone inactive'; | |
| micStatusIndicator.className = 'w-4 h-4 rounded-full bg-gray-600'; | |
| // Stop timer | |
| clearInterval(timerInterval); | |
| } | |
| // Reset data | |
| function resetData() { | |
| noiseData = []; | |
| peakNoise = 0; | |
| sumNoise = 0; | |
| countNoise = 0; | |
| thresholdBreachCount = 0; | |
| peakLevel.textContent = '--'; | |
| averageLevel.textContent = '--'; | |
| thresholdBreaches.textContent = '--'; | |
| // Reset charts | |
| if (chart) { | |
| chart.data.datasets[0].data = Array(60).fill(0); | |
| chart.update(); | |
| } | |
| if (peakChart) { | |
| peakChart.data.datasets[0].data = Array(12).fill(0); | |
| peakChart.update(); | |
| } | |
| // Clear localStorage peaks | |
| localStorage.removeItem('noiseMonitorPeaks'); | |
| // Reset stats | |
| highestPeak.textContent = '--'; | |
| avgPeaks.textContent = '--'; | |
| // Reset timer if not monitoring | |
| if (!isMonitoring) { | |
| sessionTimer.textContent = '00:00:00'; | |
| } | |
| } | |
| // Update session timer | |
| function updateTimer() { | |
| const elapsed = Date.now() - sessionStartTime; | |
| const seconds = Math.floor(elapsed / 1000) % 60; | |
| const minutes = Math.floor(elapsed / (1000 * 60)) % 60; | |
| const hours = Math.floor(elapsed / (1000 * 60 * 60)); | |
| sessionTimer.textContent = | |
| `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
| } | |
| // Event Listeners | |
| startBtn.addEventListener('click', startMonitoring); | |
| stopBtn.addEventListener('click', stopMonitoring); | |
| resetBtn.addEventListener('click', resetData); | |
| thresholdSlider.addEventListener('input', function() { | |
| threshold = parseInt(this.value); | |
| thresholdValue.textContent = threshold; | |
| }); | |
| enableThreshold.addEventListener('change', function() { | |
| thresholdEnabled = this.checked; | |
| }); | |
| smoothBtn.addEventListener('click', function() { | |
| chartType = 'line'; | |
| if (chart) { | |
| chart.config.type = 'line'; | |
| chart.data.datasets[0].tension = 0.1; | |
| chart.update(); | |
| } | |
| }); | |
| rawBtn.addEventListener('click', function() { | |
| chartType = 'bar'; | |
| if (chart) { | |
| chart.config.type = 'bar'; | |
| chart.update(); | |
| } | |
| }); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initChart(); | |
| initPeakChart(); | |
| initVisualizer(); | |
| // Set threshold value | |
| thresholdValue.textContent = thresholdSlider.value; | |
| // Load historical peaks | |
| updatePeakChart(); | |
| // Populate microphone devices | |
| populateMicrophoneDevices(); | |
| // Update device list when devices change | |
| navigator.mediaDevices.addEventListener('devicechange', populateMicrophoneDevices); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=markburn/neon-noice-monitor" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |