Created
May 31, 2025 18:28
-
-
Save kellyja1/43f8635c7f146574dcd3f20fb6e3e4ea to your computer and use it in GitHub Desktop.
Sound meter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="flex flex-col items-center p-8 bg-white rounded-2xl shadow-xl w-full" style="max-width: 336px;"> | |
<h1 id="voiceOMeterTitle" class="voice-o-meter-title inactive"> | |
<svg class="microphone-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> | |
<path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 4c0-1.66-1.34-3-3-3S9 2.34 9 4v7c0 1.66 1.34 3 3 3zm5.2-3c0 3.53-2.93 6.43-6.2 6.43S4.8 14.53 4.8 11H3c0 4.56 3.68 8.37 8 8.94V23h2v-3.06c4.32-.57 8-4.38 8-8.94h-1.8z"/> | |
</svg> | |
Voice-O-Meter | |
</h1> | |
<div id="statusBox" class="status-box bg-gray-300"> | |
</div> | |
<!-- Calibration Controls --> | |
<div class="calibration-controls"> | |
<div class="calibration-status"> | |
<span id="calibrationStatus">Ready to calibrate</span> | |
</div> | |
<div class="calibration-progress"> | |
<div id="calibrationProgressBar" class="calibration-progress-bar"></div> | |
</div> | |
<div> | |
<button id="calibrateButton" class="calibration-button">Calibrate Quiet</button> | |
<button id="resetCalibrationButton" class="calibration-button">Reset</button> | |
</div> | |
</div> | |
<p class="noise-zone-title">Noise Zone</p> | |
<div class="relative flex flex-col items-center mb-4"> | |
<div class="meter-and-zones-container"> | |
<div class="noise-bar-container"> | |
<div id="currentLevelMarker" class="current-level-marker"></div> | |
</div> | |
<div class="zones-container"> | |
<div class="zone-label mca-quiet-zone" style="height: 21.05%; bottom: 0%;"> | |
<span>MCA QUIET</span> | |
<span class="range-text">(0-20 dB)</span> | |
</div> | |
<div class="zone-label solo-zone" style="height: 21.05%; bottom: 21.05%;"> | |
<span>SOLO</span> | |
<span class="range-text">(20-40 dB)</span> | |
</div> | |
<div class="zone-label group-zone" style="height: 31.58%; bottom: 42.10%;"> | |
<span>GROUP</span> | |
<span class="range-text">(40-70 dB)</span> | |
</div> | |
</div> | |
</div> | |
<p id="decibelValue" class="text-4xl font-extrabold text-gray-900 mt-4">0 dB</p> | |
<p class="text-sm text-gray-500 mt-2">Current Noise Level</p> | |
<p id="loudestDecibelValue" class="text-xl font-semibold text-gray-700 mt-4">Loudest: 0 dB</p> | |
<p id="averageDecibelValue" class="text-xl font-semibold text-gray-700 mt-2">Average: 0 dB</p> | |
</div> | |
<div class="flex space-x-4 mb-8"> | |
<button id="btnMcaQuiet" class="zone-button active" data-zone="MCA_QUIET">MCA QUIET</button> | |
<button id="btnSolo" class="zone-button" data-zone="SOLO">SOLO</button> | |
<button id="btnGroup" class="zone-button" data-zone="GROUP">GROUP</button> | |
</div> | |
<div class="w-full max-w-xs mb-8"> | |
<label for="sensitivitySlider" class="block text-lg font-medium text-gray-700 mb-3 text-center">Sensitivity</label> | |
<input type="range" id="sensitivitySlider" min="0.1" max="1.0" value="0.5" step="0.01" class="w-full"> | |
<div class="flex justify-between text-sm text-gray-500 mt-2"> | |
<span>Less Sensitive</span> | |
<span>More Sensitive</span> | |
</div> | |
</div> | |
<div id="statusMessage" class="text-red-600 text-center font-medium hidden"></div> | |
</div> | |
<div id="messageBox" class="message-box"> | |
<p id="messageText" class="text-lg text-gray-800 mb-4"></p> | |
<button id="messageButton" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition duration-200 ease-in-out shadow-md">OK</button> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// In-memory data persistence | |
const memoryStorage = { | |
sensitivity: 0.5, | |
activeZone: 'MCA_QUIET', | |
calibrationOffset: 11, | |
isCalibrated: false | |
}; | |
// Global variables | |
let audioContext; | |
let analyser; | |
let microphone; | |
let dataArray; | |
let animationFrameId; | |
let microphoneAccessGranted = false; | |
// DOM elements | |
const currentLevelMarker = document.getElementById('currentLevelMarker'); | |
const decibelValueDisplay = document.getElementById('decibelValue'); | |
const loudestDecibelValueDisplay = document.getElementById('loudestDecibelValue'); | |
const averageDecibelValueDisplay = document.getElementById('averageDecibelValue'); | |
const sensitivitySlider = document.getElementById('sensitivitySlider'); | |
const statusMessage = document.getElementById('statusMessage'); | |
const messageBox = document.getElementById('messageBox'); | |
const messageButton = document.getElementById('messageButton'); | |
const statusBox = document.getElementById('statusBox'); | |
const btnMcaQuiet = document.getElementById('btnMcaQuiet'); | |
const btnSolo = document.getElementById('btnSolo'); | |
const btnGroup = document.getElementById('btnGroup'); | |
const voiceOMeterTitle = document.getElementById('voiceOMeterTitle'); | |
const calibrateButton = document.getElementById('calibrateButton'); | |
const resetCalibrationButton = document.getElementById('resetCalibrationButton'); | |
const calibrationStatus = document.getElementById('calibrationStatus'); | |
const calibrationProgressBar = document.getElementById('calibrationProgressBar'); | |
// Settings | |
let sensitivity = 0.5; | |
const HISTORY_SIZE = 3; // Shortened moving average | |
let decibelHistory = []; | |
let loudestDecibel = 0; | |
const minDisplayDb = 0; | |
const maxDisplayDb = 95; | |
const displayRange = maxDisplayDb - minDisplayDb; | |
let CALIBRATION_OFFSET_DB = 11; | |
let isCalibrated = false; | |
// Calibration variables | |
let isCalibrating = false; | |
let calibrationSamples = []; | |
const CALIBRATION_DURATION = 3000; | |
const CALIBRATION_SAMPLE_RATE = 100; | |
const noiseZones = { | |
MCA_QUIET: { min: 0, max: 20 }, | |
SOLO: { min: 20, max: 40 }, | |
GROUP: { min: 40, max: 70 } | |
}; | |
let activeZone = 'MCA_QUIET'; | |
// Functions | |
function saveSettings() { | |
memoryStorage.sensitivity = sensitivity; | |
memoryStorage.activeZone = activeZone; | |
memoryStorage.calibrationOffset = CALIBRATION_OFFSET_DB; | |
memoryStorage.isCalibrated = isCalibrated; | |
console.log('Settings saved to memory:', memoryStorage); | |
} | |
function loadSettings() { | |
sensitivity = memoryStorage.sensitivity; | |
activeZone = memoryStorage.activeZone; | |
CALIBRATION_OFFSET_DB = memoryStorage.calibrationOffset; | |
isCalibrated = memoryStorage.isCalibrated; | |
if (sensitivitySlider) sensitivitySlider.value = sensitivity; | |
// Update active zone button | |
if (btnMcaQuiet) btnMcaQuiet.classList.remove('active'); | |
if (btnSolo) btnSolo.classList.remove('active'); | |
if (btnGroup) btnGroup.classList.remove('active'); | |
if (activeZone === 'MCA_QUIET' && btnMcaQuiet) btnMcaQuiet.classList.add('active'); | |
else if (activeZone === 'SOLO' && btnSolo) btnSolo.classList.add('active'); | |
else if (activeZone === 'GROUP' && btnGroup) btnGroup.classList.add('active'); | |
updateCalibrationStatus(); | |
console.log('Settings loaded from memory:', memoryStorage); | |
} | |
function showMessageBox(message, buttonText = "OK", onButtonClick = null) { | |
const messageText = document.getElementById('messageText'); | |
if (messageText) messageText.textContent = message; | |
if (messageButton) messageButton.textContent = buttonText; | |
if (messageBox) messageBox.style.display = 'block'; | |
if (messageButton) { | |
messageButton.onclick = () => { | |
if (messageBox) messageBox.style.display = 'none'; | |
if (onButtonClick) onButtonClick(); | |
}; | |
} | |
} | |
function calculateRMS(data) { | |
let sumSquares = 0; | |
for (let i = 0; i < data.length; i++) { | |
const normalizedValue = (data[i] / 128) - 1; | |
sumSquares += normalizedValue * normalizedValue; | |
} | |
return Math.sqrt(sumSquares / data.length); | |
} | |
function updateCalibrationStatus() { | |
if (!calibrationStatus) return; | |
if (isCalibrated) { | |
calibrationStatus.textContent = `Calibrated (offset: ${CALIBRATION_OFFSET_DB.toFixed(1)} dB)`; | |
if (calibrateButton) calibrateButton.textContent = 'Re-calibrate'; | |
} else { | |
calibrationStatus.textContent = 'Not calibrated - may be inaccurate'; | |
if (calibrateButton) calibrateButton.textContent = 'Calibrate Quiet'; | |
} | |
} | |
function startCalibration() { | |
if (!microphoneAccessGranted) { | |
showMessageBox('Please start the microphone first by clicking the Voice-O-Meter title.'); | |
return; | |
} | |
isCalibrating = true; | |
calibrationSamples = []; | |
if (calibrateButton) calibrateButton.disabled = true; | |
if (calibrationStatus) calibrationStatus.textContent = 'Calibrating... Stay quiet!'; | |
if (calibrationProgressBar) calibrationProgressBar.style.width = '0%'; | |
const startTime = Date.now(); | |
const calibrationInterval = setInterval(() => { | |
const elapsed = Date.now() - startTime; | |
const progress = Math.min(100, (elapsed / CALIBRATION_DURATION) * 100); | |
if (calibrationProgressBar) calibrationProgressBar.style.width = `${progress}%`; | |
if (elapsed >= CALIBRATION_DURATION) { | |
clearInterval(calibrationInterval); | |
finishCalibration(); | |
} else { | |
if (analyser && dataArray) { | |
analyser.getByteFrequencyData(dataArray); | |
const rms = calculateRMS(dataArray); | |
const scaledRms = Math.min(1, rms * sensitivity); | |
const logRms = Math.log10(scaledRms + 0.001); | |
let rawDb = minDisplayDb + ((logRms - (-3)) / (0 - (-3))) * displayRange; | |
calibrationSamples.push(rawDb); | |
} | |
} | |
}, CALIBRATION_SAMPLE_RATE); | |
} | |
function finishCalibration() { | |
isCalibrating = false; | |
if (calibrateButton) calibrateButton.disabled = false; | |
if (calibrationSamples.length > 0) { | |
const averageQuietLevel = calibrationSamples.reduce((sum, val) => sum + val, 0) / calibrationSamples.length; | |
CALIBRATION_OFFSET_DB = averageQuietLevel - 2; | |
isCalibrated = true; | |
if (calibrationStatus) { | |
calibrationStatus.textContent = `Calibrated! Quiet level: ${(averageQuietLevel - CALIBRATION_OFFSET_DB).toFixed(1)} dB`; | |
} | |
showMessageBox(`Calibration complete! Your quiet room baseline is now set to ${(averageQuietLevel - CALIBRATION_OFFSET_DB).toFixed(1)} dB.`); | |
saveSettings(); | |
} else { | |
if (calibrationStatus) calibrationStatus.textContent = 'Calibration failed - try again'; | |
showMessageBox('Calibration failed. Please ensure the microphone is working and try again.'); | |
} | |
updateCalibrationStatus(); | |
if (calibrationProgressBar) calibrationProgressBar.style.width = '0%'; | |
} | |
function resetCalibration() { | |
CALIBRATION_OFFSET_DB = 11; | |
isCalibrated = false; | |
updateCalibrationStatus(); | |
saveSettings(); | |
showMessageBox('Calibration reset to default values.'); | |
} | |
function updateNoiseLevel() { | |
if (!microphoneAccessGranted || !analyser || !dataArray) { | |
return; | |
} | |
analyser.getByteFrequencyData(dataArray); | |
const rms = calculateRMS(dataArray); | |
const scaledRms = Math.min(1, rms * sensitivity); | |
const logRms = Math.log10(scaledRms + 0.001); | |
let currentDb = minDisplayDb + ((logRms - (-3)) / (0 - (-3))) * displayRange; | |
currentDb -= CALIBRATION_OFFSET_DB; | |
currentDb = Math.max(minDisplayDb, Math.min(maxDisplayDb, currentDb)); | |
decibelHistory.push(currentDb); | |
if (decibelHistory.length > HISTORY_SIZE) { | |
decibelHistory.shift(); | |
} | |
const averagedDecibels = decibelHistory.reduce((sum, val) => sum + val, 0) / decibelHistory.length; | |
if (decibelValueDisplay) decibelValueDisplay.textContent = `${Math.round(averagedDecibels)} dB`; | |
const markerPosition = ((averagedDecibels - minDisplayDb) / displayRange) * 100; | |
if (currentLevelMarker) { | |
currentLevelMarker.style.bottom = `${Math.min(100, Math.max(0, markerPosition))}%`; | |
} | |
loudestDecibel = Math.max(loudestDecibel, averagedDecibels); | |
if (loudestDecibelValueDisplay) { | |
loudestDecibelValueDisplay.textContent = `Loudest: ${Math.round(loudestDecibel)} dB`; | |
} | |
if (averageDecibelValueDisplay) { | |
averageDecibelValueDisplay.textContent = `Average: ${Math.round(averagedDecibels)} dB`; | |
} | |
const zone = noiseZones[activeZone]; | |
let statusColor = ''; | |
let emoji = ''; | |
let shouldBlink = false; | |
const levelRelativeToZoneStart = averagedDecibels - zone.min; | |
const zoneRange = zone.max - zone.min; | |
let percentageOfZone = (zoneRange > 0) ? (levelRelativeToZoneStart / zoneRange) * 100 : 0; | |
if (averagedDecibels > zone.max) { | |
statusColor = 'red'; | |
emoji = '😞'; | |
shouldBlink = true; | |
} else if (percentageOfZone >= 90) { | |
statusColor = 'yellow'; | |
emoji = '😮'; | |
} else { | |
statusColor = 'green'; | |
emoji = '😊'; | |
} | |
if (statusBox) { | |
statusBox.style.backgroundColor = statusColor; | |
statusBox.innerHTML = emoji; | |
if (shouldBlink) { | |
statusBox.classList.add('blink-red'); | |
} else { | |
statusBox.classList.remove('blink-red'); | |
} | |
} | |
animationFrameId = requestAnimationFrame(updateNoiseLevel); | |
} | |
async function startMicrophone() { | |
console.log('startMicrophone function called.'); | |
try { | |
console.log('Attempting to get user media...'); | |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
console.log('Microphone access granted!'); | |
if (statusMessage) statusMessage.classList.add('hidden'); | |
microphoneAccessGranted = true; | |
if (voiceOMeterTitle) { | |
voiceOMeterTitle.classList.remove('inactive'); | |
voiceOMeterTitle.style.cursor = 'default'; | |
} | |
const microphoneIcon = voiceOMeterTitle ? voiceOMeterTitle.querySelector('.microphone-icon') : null; | |
if (microphoneIcon) { | |
microphoneIcon.style.fill = 'black'; | |
} | |
audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
microphone = audioContext.createMediaStreamSource(stream); | |
analyser = audioContext.createAnalyser(); | |
analyser.fftSize = 2048; | |
dataArray = new Uint8Array(analyser.frequencyBinCount); | |
microphone.connect(analyser); | |
updateNoiseLevel(); | |
} catch (err) { | |
console.error('Error accessing microphone:', err); | |
if (statusMessage) statusMessage.classList.remove('hidden'); | |
let errorMessage = 'Microphone access denied or not available.'; | |
if (err.name === 'NotAllowedError') { | |
errorMessage = 'Microphone access was denied. Please allow microphone access and try again.'; | |
} else if (err.name === 'NotFoundError') { | |
errorMessage = 'No microphone found. Please ensure a microphone is connected and enabled.'; | |
} else if (err.name === 'NotReadableError') { | |
errorMessage = 'Could not access the microphone due to hardware or system issues.'; | |
} else if (err.name === 'SecurityError') { | |
errorMessage = 'Microphone access could not be granted due to security policies.'; | |
} else if (err.name === 'AbortError') { | |
errorMessage = 'Microphone access was aborted.'; | |
} else { | |
errorMessage = `An unknown error occurred: ${err.message}`; | |
} | |
if (statusMessage) statusMessage.textContent = errorMessage; | |
showMessageBox(errorMessage, "OK"); | |
if (voiceOMeterTitle) { | |
voiceOMeterTitle.classList.add('inactive'); | |
voiceOMeterTitle.style.cursor = 'pointer'; | |
} | |
const microphoneIcon = voiceOMeterTitle ? voiceOMeterTitle.querySelector('.microphone-icon') : null; | |
if (microphoneIcon) { | |
microphoneIcon.style.fill = 'gray'; | |
} | |
} | |
} | |
function handleZoneButtonClick(event) { | |
if (btnMcaQuiet) btnMcaQuiet.classList.remove('active'); | |
if (btnSolo) btnSolo.classList.remove('active'); | |
if (btnGroup) btnGroup.classList.remove('active'); | |
event.target.classList.add('active'); | |
activeZone = event.target.dataset.zone; | |
saveSettings(); | |
} | |
// Event listeners | |
document.addEventListener('DOMContentLoaded', () => { | |
loadSettings(); | |
if (voiceOMeterTitle) voiceOMeterTitle.style.cursor = 'pointer'; | |
if (sensitivitySlider) { | |
sensitivitySlider.addEventListener('input', (event) => { | |
sensitivity = parseFloat(event.target.value); | |
saveSettings(); | |
}); | |
} | |
if (btnMcaQuiet) btnMcaQuiet.addEventListener('click', handleZoneButtonClick); | |
if (btnSolo) btnSolo.addEventListener('click', handleZoneButtonClick); | |
if (btnGroup) btnGroup.addEventListener('click', handleZoneButtonClick); | |
if (calibrateButton) calibrateButton.addEventListener('click', startCalibration); | |
if (resetCalibrationButton) resetCalibrationButton.addEventListener('click', resetCalibration); | |
if (voiceOMeterTitle) { | |
voiceOMeterTitle.addEventListener('click', () => { | |
console.log('Voice-O-Meter title clicked.'); | |
if (!microphoneAccessGranted) { | |
startMicrophone(); | |
} | |
}); | |
} | |
}); | |
// Cleanup | |
window.addEventListener('beforeunload', () => { | |
if (animationFrameId) { | |
cancelAnimationFrame(animationFrameId); | |
} | |
if (audioContext && audioContext.state !== 'closed') { | |
audioContext.close(); | |
} | |
if (microphone) { | |
microphone.disconnect(); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import url('https://fonts.googleapis.com/css2?family=Bangers&display=swap'); | |
/* Custom styles for the noise bar and overall layout */ | |
body { | |
font-family: 'Inter', sans-serif; | |
background-color: #f0f4f8; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
min-height: 100vh; | |
margin: 0; | |
overflow: hidden; | |
} | |
.meter-and-zones-container { | |
display: flex; | |
align-items: flex-end; | |
gap: 10px; | |
} | |
.noise-bar-container { | |
width: 60px; | |
height: 300px; | |
background: linear-gradient(to top, #4ade80, #facc15, #ef4444); | |
border-radius: 15px; | |
overflow: hidden; | |
position: relative; | |
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.06); | |
border: 4px solid black; | |
} | |
.current-level-marker { | |
width: 100%; | |
height: 8px; | |
background-color: #ffffff; | |
position: absolute; | |
left: 0; | |
bottom: 0; | |
transition: bottom 0.1s ease-out; | |
z-index: 10; | |
box-shadow: 0 0 12px rgba(0, 0, 0, 0.7); | |
} | |
.zones-container { | |
width: 100px; | |
height: 300px; | |
position: relative; | |
display: flex; | |
flex-direction: column-reverse; | |
justify-content: flex-start; | |
pointer-events: none; | |
} | |
.zone-label { | |
position: absolute; | |
left: 0; | |
width: 100%; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
font-weight: 900; | |
color: white; | |
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; | |
border-radius: 5px; | |
padding: 5px 0; | |
box-sizing: border-box; | |
transition: all 0.1s ease-out; | |
font-size: 16px; | |
border: 4px solid black; | |
} | |
.zone-label span.range-text { | |
font-size: 14.4px; | |
font-weight: normal; | |
margin-top: 2px; | |
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; | |
} | |
.mca-quiet-zone { | |
background-color: rgba(67, 160, 71, 0.7); | |
height: 21.05%; | |
bottom: 0%; | |
} | |
.solo-zone { | |
background-color: rgba(0, 0, 255, 0.7); | |
height: 21.05%; | |
bottom: 21.05%; | |
} | |
.group-zone { | |
background-color: rgba(128, 0, 128, 0.7); | |
height: 31.58%; | |
bottom: 42.10%; | |
} | |
input[type="range"] { | |
-webkit-appearance: none; | |
appearance: none; | |
width: 100%; | |
height: 8px; | |
background: #cbd5e1; | |
outline: none; | |
opacity: 0.7; | |
transition: opacity .15s ease-in-out; | |
border-radius: 5px; | |
} | |
input[type="range"]::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
appearance: none; | |
width: 24px; | |
height: 24px; | |
background: #3b82f6; | |
cursor: pointer; | |
border-radius: 50%; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |
} | |
input[type="range"]::-moz-range-thumb { | |
width: 24px; | |
height: 24px; | |
background: #3b82f6; | |
cursor: pointer; | |
border-radius: 50%; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |
border: none; | |
} | |
.message-box { | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background-color: #fff; | |
padding: 24px; | |
border-radius: 12px; | |
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); | |
z-index: 1000; | |
text-align: center; | |
display: none; | |
font-size: 16px; | |
} | |
.status-box { | |
width: 307.2px; | |
height: 80px; | |
border: 4px solid black; | |
border-radius: 8px; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
font-size: 48px; | |
margin-bottom: 16px; | |
} | |
@keyframes blink { | |
0%, 100% { opacity: 1; } | |
50% { opacity: 0.5; } | |
} | |
.blink-red { | |
animation: blink 1s linear infinite; | |
} | |
.zone-button { | |
padding: 12px 24px; | |
border-radius: 8px; | |
font-weight: 600; | |
transition: all 0.2s ease-in-out; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
background-color: #e2e8f0; | |
color: #4a5568; | |
border: 2px solid #cbd5e1; | |
font-size: 12.8px; | |
cursor: pointer; | |
} | |
.zone-button.active { | |
background-color: #3b82f6; | |
color: white; | |
border-color: #2563eb; | |
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.2); | |
transform: translateY(-2px); | |
} | |
.zone-button:hover:not(.active) { | |
background-color: #cbd5e1; | |
} | |
.noise-zone-title { | |
font-size: 24px; | |
font-weight: bold; | |
color: black; | |
text-align: center; | |
margin-bottom: 8px; | |
} | |
#statusMessage { | |
font-size: 16px; | |
text-align: center; | |
margin-top: 8px; | |
} | |
.voice-o-meter-title { | |
font-family: 'Bangers', cursive; | |
font-size: 64px; | |
color: red; | |
text-shadow: -6px -6px 0 #000, 6px -6px 0 #000, -6px 6px 0 #000, 6px 6px 0 #000; | |
margin-bottom: 16px; | |
text-align: center; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
gap: 10px; | |
} | |
.microphone-icon { | |
width: 96px; | |
height: 96px; | |
fill: black; | |
stroke: none; | |
} | |
.voice-o-meter-title.inactive { | |
color: gray; | |
cursor: pointer; | |
} | |
.voice-o-meter-title.inactive .microphone-icon { | |
fill: gray; | |
} | |
.calibration-button { | |
padding: 8px 16px; | |
border-radius: 6px; | |
font-weight: 500; | |
transition: all 0.2s ease-in-out; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
background-color: #f59e0b; | |
color: white; | |
border: none; | |
font-size: 12px; | |
cursor: pointer; | |
margin: 4px; | |
} | |
.calibration-button:hover { | |
background-color: #d97706; | |
transform: translateY(-1px); | |
} | |
.calibration-button:disabled { | |
background-color: #9ca3af; | |
cursor: not-allowed; | |
transform: none; | |
} | |
.calibration-controls { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
gap: 8px; | |
margin-bottom: 16px; | |
padding: 12px; | |
border: 2px solid #e5e7eb; | |
border-radius: 8px; | |
background-color: #f9fafb; | |
} | |
.calibration-status { | |
font-size: 14px; | |
font-weight: medium; | |
color: #374151; | |
text-align: center; | |
} | |
.calibration-progress { | |
width: 100%; | |
height: 6px; | |
background-color: #e5e7eb; | |
border-radius: 3px; | |
overflow: hidden; | |
} | |
.calibration-progress-bar { | |
height: 100%; | |
background-color: #3b82f6; | |
width: 0%; | |
transition: width 0.1s ease-out; | |
} | |
.hidden { | |
display: none; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment