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> |