Skip to content

Instantly share code, notes, and snippets.

@camAtGitHub
Forked from chris373/MicViz.html
Last active May 21, 2025 05:08
Show Gist options
  • Select an option

  • Save camAtGitHub/1cf6159aec5dc6eff89a44227be0f372 to your computer and use it in GitHub Desktop.

Select an option

Save camAtGitHub/1cf6159aec5dc6eff89a44227be0f372 to your computer and use it in GitHub Desktop.
Microphone level visualizer - updated APIs and ghosting peak levels
<!DOCTYPE html>
<html>
<head>
<title>Audio Visualizer</title>
</head>
<body>
<svg preserveAspectRatio="none" id="visualizer" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<mask id="mask">
<g id="maskGroup"></g>
</mask>
<linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#ff0a0a;stop-opacity:1" />
<stop offset="20%" style="stop-color:#f1ff0a;stop-opacity:1" />
<stop offset="90%" style="stop-color:#d923b9;stop-opacity:1" />
<stop offset="100%" style="stop-color:#050d61;stop-opacity:1" />
</linearGradient>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="url(#gradient)" mask="url(#mask)"></rect>
<g id="ghostGroup"></g>
</svg>
<div id="permission-container">
<p id="status-message">Please allow microphone access to start the visualizer.</p>
<button id="allow-microphone">Allow Microphone</button>
</div>
<div id="mic-info" style="display: none;"></div>
<style>
html, body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
background-color: #222;
font-size: 0;
}
svg {
display: block;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
position: absolute;
}
#permission-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
padding: 20px;
border-radius: 10px;
text-align: center;
color: white;
font-family: sans-serif;
font-size: 16px;
}
#permission-container button {
padding: 10px 20px;
background-color: #48b1f4;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
#mic-info {
position: absolute;
top: 60%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
padding: 10px;
border-radius: 5px;
color: white;
font-family: sans-serif;
font-size: 14px;
text-align: center;
}
path {
stroke-linecap: square;
stroke: white;
stroke-width: 0.5px;
}
.ghost {
stroke: white;
stroke-width: 0.5px;
opacity: 0;
transition: opacity 2s ease-out;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
var visualizer = document.getElementById('visualizer');
var mask = visualizer.querySelector('#mask');
var ghostGroup = visualizer.querySelector('#ghostGroup');
var permissionContainer = document.getElementById('permission-container');
var statusMessage = document.getElementById('status-message');
var allowButton = document.getElementById('allow-microphone');
var micInfo = document.getElementById('mic-info');
var ghostTimeouts = new Array(128).fill(null); // Track timeouts for fading
const NOISE_THRESHOLD = 5; // Ignore amplitudes below this value
allowButton.addEventListener('click', function() {
navigator.mediaDevices.getUserMedia({audio: true})
.then(soundAllowed)
.catch(soundNotAllowed);
});
async function soundAllowed(stream) {
permissionContainer.style.display = 'none';
// Get the active microphone
const devices = await navigator.mediaDevices.enumerateDevices();
const micDevice = devices.find(device => device.kind === 'audioinput' && device.label);
micInfo.textContent = `Using microphone: ${micDevice ? micDevice.label : 'Unknown'}`;
micInfo.style.display = 'block';
var audioContent = new AudioContext();
var audioStream = audioContent.createMediaStreamSource(stream);
var analyser = audioContent.createAnalyser();
audioStream.connect(analyser);
analyser.fftSize = 1024;
var frequencyArray = new Uint8Array(analyser.frequencyBinCount);
var numBins = 128;
visualizer.setAttribute('viewBox', '0 0 ' + numBins + ' 255');
var paths = [];
var ghostPaths = [];
var ghostLevels = new Array(numBins).fill(0);
for (var i = 0; i < numBins; i++) {
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('stroke-dasharray', '4,1');
mask.appendChild(path);
paths.push(path);
var ghostPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
ghostPath.setAttribute('stroke-dasharray', '4,1');
ghostPath.classList.add('ghost');
ghostGroup.appendChild(ghostPath);
ghostPaths.push(ghostPath);
}
function doDraw() {
requestAnimationFrame(doDraw);
analyser.getByteTimeDomainData(frequencyArray);
for (let i = 0; i < numBins; i++) {
var value = frequencyArray[i];
var adjustedLength = value - 127;
paths[i].setAttribute('d', 'M ' + i + ',127 l 0,' + adjustedLength);
var absAdjusted = Math.abs(adjustedLength);
if (absAdjusted > ghostLevels[i] && absAdjusted > NOISE_THRESHOLD) {
ghostLevels[i] = absAdjusted;
ghostPaths[i].setAttribute('d', 'M ' + i + ',127 l 0,' + adjustedLength);
ghostPaths[i].style.opacity = 0.3;
clearTimeout(ghostTimeouts[i]); // Clear previous timeout
ghostTimeouts[i] = setTimeout(() => {
ghostPaths[i].style.opacity = 0; // Fade out after 2 seconds
ghostLevels[i] = 0; // Reset peak level
}, 2000);
}
}
}
doDraw();
}
function soundNotAllowed(error) {
statusMessage.textContent = 'Microphone access denied or error: ' + error.message;
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment