<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio Visualizer</title>
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
<style>
:root {
--primary-color: #A81C07;
--primary-dark: #7a1405;
--primary-light: #d42509;
--background: #0f0f0f;
--text: #f0f0f0;
--text-secondary: #a0a0a0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: var(--background);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
line-height: 1.6;
}
.container {
width: 100%;
max-width: 800px;
text-align: center;
background: rgba(15, 15, 15, 0.8);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 2.5rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(168, 28, 7, 0.2);
}
h1 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 1.5rem;
background: linear-gradient(90deg, var(--primary-light), var(--primary-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.description {
color: var(--text-secondary);
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.controls {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.95rem;
}
.btn:hover {
background: var(--primary-light);
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0);
}
.btn-secondary {
background: transparent;
color: var(--text);
border: 1px solid rgba(168, 28, 7, 0.5);
}
.btn-secondary:hover {
background: rgba(168, 28, 7, 0.1);
border-color: var(--primary-color);
}
.visualizer-container {
width: 100%;
height: 300px;
background: rgba(15, 15, 15, 0.5);
border-radius: 12px;
margin: 2rem 0;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.visualizer {
display: flex;
align-items: end;
justify-content: center;
gap: 0.2rem;
height: 100%;
width: 100%;
padding: 1rem;
}
.bar {
width: 6px;
background: linear-gradient(to top, var(--primary-color), var(--primary-light));
border-radius: 4px 4px 0 0;
transition: height 0.1s linear;
box-shadow: 0 0 8px rgba(168, 28, 7, 0.6);
}
.bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 10px;
background: rgba(255, 255, 255, 0.3);
border-radius: 4px 4px 0 0;
}
.audio-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(168, 28, 7, 0.2);
}
.audio-source {
display: flex;
align-items: center;
gap: 0.75rem;
color: var(--text-secondary);
font-size: 0.9rem;
}
.status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.status-active {
color: #4ade80;
}
.status-inactive {
color: #f87171;
}
.footer {
margin-top: 2rem;
color: var(--text-secondary);
font-size: 0.85rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.footer a {
color: var(--primary-light);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
@media (max-width: 600px) {
.container {
padding: 1.5rem;
}
h1 {
font-size: 1.8rem;
}
.visualizer-container {
height: 200px;
}
.controls {
flex-direction: column;
align-items: center;
}
}
/* Animation for inactive state */
@keyframes pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.8; }
}
.bar.inactive {
animation: pulse 1.5s ease-in-out infinite;
}
</style>
</head>
<body>
<div class="container">
<h1>Audio Visualizer</h1>
<p class="description">
A minimal audio visualization tool that transforms sound waves into dynamic visual patterns.
Use your microphone or upload an audio file to see the visualization in action.
</p>
<div class="controls">
<button id="microphoneBtn" class="btn">
<ion-icon name="mic-outline"></ion-icon>
Use Microphone
</button>
<button id="uploadBtn" class="btn btn-secondary">
<ion-icon name="cloud-upload-outline"></ion-icon>
Upload Audio
</button>
<input type="file" id="audioUpload" accept="audio/*" style="display: none;">
</div>
<div class="visualizer-container">
<div class="visualizer" id="visualizer">
<!-- Bars will be generated by JavaScript -->
</div>
</div>
<div class="audio-info">
<div class="audio-source">
<ion-icon name="musical-notes-outline"></ion-icon>
<span id="sourceText">No audio source</span>
</div>
<div class="status">
<ion-icon name="radio-button-off-outline" id="statusIcon"></ion-icon>
<span id="statusText">Inactive</span>
</div>
</div>
<div class="footer">
<ion-icon name="build-outline"></ion-icon>
<span>Built with Web Audio API | </span>
<a href="#" id="githubLink">View on GitHub</a>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM elements
const visualizer = document.getElementById('visualizer');
const microphoneBtn = document.getElementById('microphoneBtn');
const uploadBtn = document.getElementById('uploadBtn');
const audioUpload = document.getElementById('audioUpload');
const sourceText = document.getElementById('sourceText');
const statusText = document.getElementById('statusText');
const statusIcon = document.getElementById('statusIcon');
const githubLink = document.getElementById('githubLink');
// Audio context and analyzer
let audioContext;
let analyzer;
let dataArray;
let source;
let stream;
let animationId;
let isVisualizing = false;
// Create visualizer bars
function createVisualizerBars(count = 64) {
visualizer.innerHTML = '';
for (let i = 0; i < count; i++) {
const bar = document.createElement('div');
bar.className = 'bar inactive';
bar.style.height = '5%';
bar.style.transition = `height 0.1s linear ${i * 0.002}s`;
visualizer.appendChild(bar);
}
}
// Initialize audio context
function initAudioContext() {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyzer = audioContext.createAnalyser();
analyzer.fftSize = 128;
const bufferLength = analyzer.frequencyBinCount;
dataArray = new Uint8Array(bufferLength);
}
// Update visualizer
function updateVisualizer() {
if (!isVisualizing) return;
analyzer.getByteFrequencyData(dataArray);
const bars = visualizer.querySelectorAll('.bar');
const barCount = bars.length;
const dataStep = Math.floor(dataArray.length / barCount);
bars.forEach((bar, index) => {
const dataIndex = index * dataStep;
const value = dataArray[dataIndex] || 0;
const percentage = (value / 255) * 100;
bar.style.height = `${percentage}%`;
bar.classList.remove('inactive');
});
animationId = requestAnimationFrame(updateVisualizer);
}
// Start visualization
function startVisualization() {
if (isVisualizing) return;
isVisualizing = true;
updateVisualizer();
statusText.textContent = 'Active';
statusText.className = 'status-active';
statusIcon.name = 'radio-button-on-outline';
statusIcon.className = 'status-active';
}
// Stop visualization
function stopVisualization() {
isVisualizing = false;
if (animationId) {
cancelAnimationFrame(animationId);
}
// Reset bars to inactive state
const bars = visualizer.querySelectorAll('.bar');
bars.forEach(bar => {
bar.style.height = '5%';
bar.classList.add('inactive');
});
statusText.textContent = 'Inactive';
statusText.className = 'status-inactive';
statusIcon.name = 'radio-button-off-outline';
statusIcon.className = 'status-inactive';
}
// Handle microphone access
microphoneBtn.addEventListener('click', async function() {
try {
if (!audioContext) initAudioContext();
if (stream) {
// Stop current stream
stream.getTracks().forEach(track => track.stop());
if (source) source.disconnect();
stopVisualization();
sourceText.textContent = 'No audio source';
return;
}
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
source = audioContext.createMediaStreamSource(stream);
source.connect(analyzer);
sourceText.textContent = 'Microphone';
startVisualization();
} catch (err) {
console.error('Error accessing microphone:', err);
alert('Could not access microphone. Please ensure you have granted permission.');
}
});
// Handle file upload
uploadBtn.addEventListener('click', function() {
audioUpload.click();
});
audioUpload.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
if (!audioContext) initAudioContext();
// Stop any existing visualization
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
if (source) {
source.disconnect();
}
stopVisualization();
const reader = new FileReader();
reader.onload = function(e) {
audioContext.decodeAudioData(e.target.result)
.then(buffer => {
source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(analyzer);
source.connect(audioContext.destination);
source.start();
sourceText.textContent = file.name.substring(0, 20) + (file.name.length > 20 ? '...' : '');
startVisualization();
// Stop when audio ends
source.onended = function() {
stopVisualization();
sourceText.textContent = 'Audio ended';
};
})
.catch(err => {
console.error('Error decoding audio:', err);
alert('Error decoding audio file. Please try another file.');
});
};
reader.readAsArrayBuffer(file);
});
// Initialize
createVisualizerBars(64);
githubLink.href = 'https://github.com';
// Clean up on page unload
window.addEventListener('beforeunload', function() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
if (audioContext && audioContext.state !== 'closed') {
audioContext.close();
}
if (animationId) {
cancelAnimationFrame(animationId);
}
});
});
</script>
</body>
</html>
No comments yet. Be the first!