← back to Model Arena
data/artifacts/1fd68f0d37b7/gpt.html
541 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Microphone Audio Visualizer</title>
<style>
:root {
--bg: #050709;
--fg: #f5f7fa;
--accent: #4fc3f7;
--accent2: #ff6f61;
--muted: #888;
--canvas-bg: #050709;
}
body.light {
--bg: #f5f7fa;
--fg: #111827;
--accent: #2563eb;
--accent2: #f97316;
--muted: #6b7280;
--canvas-bg: #e5e7eb;
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
background: radial-gradient(circle at top, #111 0, var(--bg) 55%);
color: var(--fg);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
display: flex;
align-items: stretch;
justify-content: center;
}
.app {
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 16px 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.title {
display: flex;
flex-direction: column;
}
h1 {
margin: 0;
font-size: 1.2rem;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.subtitle {
font-size: 0.75rem;
color: var(--muted);
margin-top: 2px;
}
.controls {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
button {
border: none;
border-radius: 999px;
padding: 6px 14px;
font-size: 0.8rem;
cursor: pointer;
background: rgba(255,255,255,0.06);
color: var(--fg);
display: inline-flex;
align-items: center;
gap: 6px;
backdrop-filter: blur(6px);
transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
box-shadow: 0 0 0 rgba(0,0,0,0);
}
button:hover {
background: rgba(255,255,255,0.12);
}
button:active {
transform: translateY(1px) scale(0.99);
box-shadow: 0 0 0 rgba(0,0,0,0);
}
button.primary {
background: linear-gradient(120deg, var(--accent), var(--accent2));
color: #fff;
font-weight: 600;
box-shadow: 0 7px 18px rgba(0,0,0,0.35);
}
button.primary:hover {
filter: brightness(1.05);
}
.pill {
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.08);
padding: 4px 8px;
font-size: 0.7rem;
color: var(--muted);
display: inline-flex;
align-items: center;
gap: 4px;
}
.pill-dot {
width: 7px;
height: 7px;
border-radius: 999px;
background: #f97316;
box-shadow: 0 0 6px rgba(249,115,22,0.9);
}
.status {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8rem;
color: var(--muted);
}
.status-indicator {
width: 9px;
height: 9px;
border-radius: 999px;
background: #fbbf24;
box-shadow: 0 0 4px rgba(251,191,36,0.8);
}
.status-indicator.live {
background: #22c55e;
box-shadow: 0 0 8px rgba(34,197,94,0.9);
}
.status-indicator.error {
background: #ef4444;
box-shadow: 0 0 8px rgba(239,68,68,0.8);
}
main {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.canvas-shell {
position: relative;
border-radius: 18px;
overflow: hidden;
background: radial-gradient(circle at top left, rgba(79,195,247,0.25), transparent 55%),
radial-gradient(circle at bottom right, rgba(244,114,182,0.18), transparent 55%),
var(--canvas-bg);
box-shadow:
0 12px 40px rgba(0,0,0,0.65),
0 0 0 1px rgba(255,255,255,0.04);
min-height: 260px;
}
canvas {
width: 100%;
height: 280px;
display: block;
}
.overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
text-align: center;
padding: 24px;
}
.overlay.hidden {
display: none;
}
.overlay h2 {
margin: 0 0 8px;
font-size: 1rem;
}
.overlay p {
margin: 0;
font-size: 0.8rem;
color: var(--muted);
}
.overlay .hint {
margin-top: 10px;
font-size: 0.75rem;
color: var(--muted);
}
.chip-row {
display: flex;
gap: 6px;
margin-top: 12px;
flex-wrap: wrap;
justify-content: center;
}
.chip {
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.15);
padding: 3px 9px;
font-size: 0.7rem;
color: var(--muted);
backdrop-filter: blur(4px);
background: rgba(15,23,42,0.4);
}
footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.7rem;
color: var(--muted);
gap: 8px;
flex-wrap: wrap;
}
.meters {
display: flex;
align-items: center;
gap: 6px;
}
.vu-shell {
width: 80px;
height: 6px;
border-radius: 999px;
background: rgba(15,23,42,0.8);
overflow: hidden;
}
.vu-fill {
height: 100%;
width: 0%;
border-radius: inherit;
background: linear-gradient(90deg, #22c55e, #eab308, #ef4444);
transition: width 0.05s linear;
}
.theme-toggle {
font-size: 0.75rem;
}
@media (max-width: 640px) {
h1 {
font-size: 1rem;
}
canvas {
height: 220px;
}
.canvas-shell {
min-height: 220px;
}
}
</style>
</head>
<body>
<div class="app">
<header>
<div class="title">
<h1>Mic Audio Visualizer</h1>
<div class="subtitle">Live frequency bars + waveform from your microphone</div>
</div>
<div class="controls">
<button id="micBtn" class="primary" type="button">
<span id="micBtnIcon">🎤</span>
<span id="micBtnLabel">Enable mic</span>
</button>
<button id="themeToggle" class="theme-toggle" type="button" aria-label="Toggle color theme">
🌙 Theme
</button>
<span class="pill">
<span class="pill-dot"></span>
Runs fully offline · No recording
</span>
</div>
</header>
<main>
<div class="canvas-shell">
<canvas id="visualizer" aria-label="Microphone audio visualizer"></canvas>
<div id="overlayIdle" class="overlay">
<h2>Waiting for microphone permission</h2>
<p>Click “Enable mic” above. Your audio never leaves this page.</p>
<div class="chip-row">
<span class="chip">Frequency spectrum</span>
<span class="chip">Waveform</span>
<span class="chip">Live VU meter</span>
</div>
<div class="hint">If you don’t see a prompt, check your browser’s address bar for a mic icon.</div>
</div>
<div id="overlayError" class="overlay hidden">
<h2>Microphone unavailable</h2>
<p id="errorText">Access to the microphone was denied.</p>
<div class="hint">Allow mic access in your browser settings, then reload this page.</div>
</div>
</div>
</main>
<footer>
<div class="status">
<div id="statusIndicator" class="status-indicator"></div>
<span id="statusText">Idle · awaiting permission</span>
</div>
<div class="meters">
<span>Input level</span>
<div class="vu-shell" aria-hidden="true">
<div id="vuFill" class="vu-fill"></div>
</div>
</div>
</footer>
</div>
<script>
(function() {
const micBtn = document.getElementById('micBtn');
const micBtnLabel = document.getElementById('micBtnLabel');
const micBtnIcon = document.getElementById('micBtnIcon');
const themeToggle = document.getElementById('themeToggle');
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
const vuFill = document.getElementById('vuFill');
const canvas = document.getElementById('visualizer');
const overlayIdle = document.getElementById('overlayIdle');
const overlayError = document.getElementById('overlayError');
const errorText = document.getElementById('errorText');
const body = document.body;
let audioCtx = null;
let analyser = null;
let waveformData = null;
let freqData = null;
let sourceNode = null;
let animationId = null;
let micEnabled = false;
function resizeCanvas() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
}
window.addEventListener('resize', resizeCanvas);
function updateStatus(mode, message) {
statusIndicator.classList.remove('live', 'error');
if (mode === 'live') {
statusIndicator.classList.add('live');
} else if (mode === 'error') {
statusIndicator.classList.add('error');
}
statusText.textContent = message;
}
function setOverlay(state, errorMsg) {
if (state === 'idle') {
overlayIdle.classList.remove('hidden');
overlayError.classList.add('hidden');
} else if (state === 'error') {
overlayIdle.classList.add('hidden');
overlayError.classList.remove('hidden');
if (errorMsg) errorText.textContent = errorMsg;
} else if (state === 'none') {
overlayIdle.classList.add('hidden');
overlayError.classList.add('hidden');
}
}
function stopAudio() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
if (sourceNode && sourceNode.mediaStream) {
sourceNode.mediaStream.getTracks().forEach(t => t.stop());
}
if (audioCtx) {
audioCtx.close().catch(() => {});
}
audioCtx = null;
analyser = null;
waveformData = null;
freqData = null;
sourceNode = null;
micEnabled = false;
}
async function initAudio() {
if (micEnabled) return;
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
setOverlay('error', 'getUserMedia is not supported in this browser.');
updateStatus('error', 'Unsupported browser: no microphone API');
return;
}
try {
updateStatus('pending', 'Requesting microphone access…');
micBtn.disabled = true;
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8;
waveformData = new Uint8Array(analyser.fftSize);
freqData = new Uint8Array(analyser.frequencyBinCount);
sourceNode = audioCtx.createMediaStreamSource(stream);
sourceNode.connect(analyser);
micEnabled = true;
setOverlay('none');
updateStatus('live', 'Live · microphone streaming');
micBtnLabel.textContent = 'Mic on';
micBtnIcon.textContent = '✅';
micBtn.disabled = false;
resizeCanvas();
draw();
} catch (err) {
console.error(err);
let msg = 'Access to the microphone was denied.';
if (err && err.name === 'NotFoundError') {
msg = 'No microphone was found on this device.';
} else if (err && err.name === 'NotAllowedError') {
msg = 'Microphone permission was blocked by the browser.';
}
setOverlay('error', msg);
updateStatus('error', 'Error · ' + msg.replace(/\.$/, ''));
micBtn.disabled = false;
}
}
function draw() {
if (!analyser) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
const dpr = window.devicePixelRatio || 1;
analyser.getByteTimeDomainData(waveformData);
analyser.getByteFrequencyData(freqData);
let sumSquares = 0;
for (let i = 0; i < waveformData.length; i++) {
const v = (waveformData[i] - 128) / 128;
sumSquares += v * v;
}
const rms = Math.sqrt(sumSquares / waveformData.length);
const vuPct = Math.min(100, rms * 220);
vuFill.style.width = vuPct.toFixed(1) + '%';
ctx.clearRect(0, 0, width, height);
const gradBg = ctx.createLinearGradient(0, 0, 0, height);
gradBg.addColorStop(0, 'rgba(15,23,42,0.8)');
gradBg.addColorStop(1, 'rgba(15,23,42,0.2)');
ctx.fillStyle = gradBg;
ctx.fillRect(0, 0, width, height);
const barCount = Math.floor(width / (8 * dpr));
const slice = Math.floor(freqData.length / barCount) || 1;
const barWidth = (width / barCount) * 0.7;
for (let i = 0; i < barCount; i++) {
let sum = 0;
for (let j = 0; j < slice; j++) {
const idx = i * slice + j;
if (idx < freqData.length) sum += freqData[idx];
}
const value = sum / slice / 255;
const barHeight = value * height * 0.55;
const x = i * (width / barCount);
const y = height - barHeight - 2 * dpr;
const g = ctx.createLinearGradient(0, y, 0, y + barHeight);
g.addColorStop(0, 'rgba(79,195,247,0.95)');
g.addColorStop(0.5, 'rgba(96,165,250,0.85)');
g.addColorStop(1, 'rgba(248,113,113,0.8)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.roundRect(x, y, barWidth, barHeight, 3 * dpr);
ctx.fill();
}
ctx.lineWidth = 2 * dpr;
const wfGrad = ctx.createLinearGradient(0, 0, width, 0);
wfGrad.addColorStop(0, 'rgba(56,189,248,1)');
wfGrad.addColorStop(0.5, 'rgba(244,114,182,1)');
wfGrad.addColorStop(1, 'rgba(251,191,36,1)');
ctx.strokeStyle = wfGrad;
ctx.beginPath();
const step = Math.max(1, Math.floor(waveformData.length / width));
const midY = height * 0.42;
const amp = height * 0.32;
for (let i = 0, x = 0; i < waveformData.length && x <= width; i += step, x += dpr) {
const v = (waveformData[i] - 128) / 128;
const y = midY + v * amp;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
ctx.save();
ctx.globalAlpha = 0.12;
ctx.lineWidth = 1 * dpr;
ctx.beginPath();
const centerX = width / 2;
ctx.moveTo(centerX, 0);
ctx.lineTo(centerX, height);
ctx.strokeStyle = '#64748b';
ctx.stroke();
ctx.restore();
animationId = requestAnimationFrame(draw);
}
micBtn.addEventListener('click', initAudio);
themeToggle.addEventListener('click', () => {
const light = body.classList.toggle('light');
themeToggle.textContent = light ? '☀️ Theme' : '🌙 Theme';
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// keep stream alive but pause animation
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
} else if (micEnabled && !animationId) {
resizeCanvas();
draw();
}
});
resizeCanvas();
})();
</script>
</body>
</html>