<script src="https://cdn.tailwindcss.com"></script>
<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>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap" rel="stylesheet">
<body class="flex items-center justify-center min-h-screen p-4">
<main class="w-full max-w-4xl mx-auto text-center p-6 md:p-8 rounded-xl shadow-2xl main-container">
<header class="mb-6">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-white">Audio Wave Visualizer</h1>
<p class="text-lg text-gray-400 mt-2">Upload an audio file or use your microphone.</p>
</header>
<div class="canvas-container mb-8">
<canvas id="visualizerCanvas"></canvas>
</div>
<div id="controls" class="flex flex-col sm:flex-row items-center justify-center gap-4">
<button id="uploadBtn" class="btn w-full sm:w-auto">
<ion-icon name="cloud-upload-outline"></ion-icon>
Upload Audio
</button>
<input type="file" id="audioFileInput" accept="audio/*">
<button id="micBtn" class="btn w-full sm:w-auto">
<ion-icon name="mic-outline"></ion-icon>
Use Microphone
</button>
</div>
<div id="playback-controls" class="mt-6 flex items-center justify-center gap-4 hidden">
<button id="playPauseBtn" class="btn !p-3">
<ion-icon name="play-outline"></ion-icon>
</button>
<div id="fileInfo" class="text-gray-400 text-sm truncate max-w-[200px] sm:max-w-xs"></div>
</div>
<p id="status" class="mt-6 text-gray-500 text-sm min-h-[1.25rem]">Select an audio source to begin.</p>
</main>
/* Custom Styles */
:root {
--primary-color: #004225;
--light-green: #52d194;
--bg-color: #050a08;
--text-color: #e0f2e9;
--border-color: rgba(0, 66, 37, 0.5);
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.btn {
background-color: transparent;
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
cursor: pointer;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
.btn:hover {
background-color: var(--primary-color);
border-color: var(--primary-color);
box-shadow: 0 10px 15px -3px rgba(0, 66, 37, 0.3), 0 4px 6px -4px rgba(0, 66, 37, 0.3);
transform: translateY(-2px);
}
.btn:focus, .btn:focus-visible {
outline: 2px solid var(--light-green);
outline-offset: 2px;
}
#visualizerCanvas {
background: linear-gradient(180deg, rgba(0, 66, 37, 0.05) 0%, rgba(0, 0, 0, 0.2) 100%);
border-radius: 0.5rem;
border: 1px solid var(--border-color);
width: 100%;
height: auto;
}
#playPauseBtn ion-icon {
font-size: 1.75rem;
}
/* Hide the default file input */
#audioFileInput {
display: none;
}
.main-container {
background-color: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
}
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const canvas = document.getElementById('visualizerCanvas');
const ctx = canvas.getContext('2d');
const uploadBtn = document.getElementById('uploadBtn');
const audioFileInput = document.getElementById('audioFileInput');
const micBtn = document.getElementById('micBtn');
const playPauseBtn = document.getElementById('playPauseBtn');
const playbackControls = document.getElementById('playback-controls');
const fileInfo = document.getElementById('fileInfo');
const status = document.getElementById('status');
const playPauseIcon = playPauseBtn.querySelector('ion-icon');
// Audio API state
let audioContext;
let analyser;
let source;
let dataArray;
let bufferLength;
let isPlaying = false;
let isInitialized = false;
let isMicActive = false;
let animationFrameId;
// Setup Canvas
function setupCanvas() {
const container = canvas.parentElement;
const dpr = window.devicePixelRatio || 1;
const rect = container.getBoundingClientRect();
const width = rect.width;
const height = width * 0.4; // Maintain aspect ratio
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.scale(dpr, dpr);
drawSilence();
}
window.addEventListener('resize', setupCanvas);
setupCanvas();
function drawSilence() {
const width = canvas.width / (window.devicePixelRatio || 1);
const height = canvas.height / (window.devicePixelRatio || 1);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgba(0, 66, 37, 0.5)';
ctx.beginPath();
const sliceWidth = width / 256;
let x = 0;
ctx.moveTo(x, height / 2);
for(let i = 0; i < 256; i++) {
x += sliceWidth;
ctx.lineTo(x, height / 2);
}
ctx.stroke();
}
function initializeAudioContext() {
if (isInitialized) return;
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
bufferLength = analyser.frequencyBinCount;
dataArray = new Uint8Array(bufferLength);
isInitialized = true;
}
function visualize() {
animationFrameId = requestAnimationFrame(visualize);
analyser.getByteTimeDomainData(dataArray);
const width = canvas.width / (window.devicePixelRatio || 1);
const height = canvas.height / (window.devicePixelRatio || 1);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, '#52d194');
gradient.addColorStop(0.5, '#004225');
gradient.addColorStop(1, '#52d194');
ctx.lineWidth = 2;
ctx.strokeStyle = gradient;
ctx.shadowBlur = 5;
ctx.shadowColor = 'rgba(82, 209, 148, 0.5)';
ctx.beginPath();
const sliceWidth = width * 1.0 / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = v * height / 2;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
x += sliceWidth;
}
ctx.lineTo(width, height / 2);
ctx.stroke();
}
function cleanupSource() {
if (source) {
source.disconnect();
if (source.mediaStream) { // For microphone
source.mediaStream.getTracks().forEach(track => track.stop());
}
source = null;
}
}
function resetState() {
cleanupSource();
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
isPlaying = false;
isMicActive = false;
playbackControls.classList.add('hidden');
micBtn.classList.remove('!bg-red-800', '!border-red-800');
micBtn.innerHTML = '<ion-icon name="mic-outline"></ion-icon> Use Microphone';
playPauseIcon.setAttribute('name', 'play-outline');
drawSilence();
}
// Event Listeners
uploadBtn.addEventListener('click', () => {
initializeAudioContext();
audioFileInput.click();
});
audioFileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
resetState();
const reader = new FileReader();
reader.onload = (e) => {
status.textContent = 'Decoding audio...';
audioContext.decodeAudioData(e.target.result, (buffer) => {
source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(analyser);
analyser.connect(audioContext.destination);
source.onended = () => {
resetState();
status.textContent = 'Track finished. Upload another file.';
};
fileInfo.textContent = file.name;
playbackControls.classList.remove('hidden');
status.textContent = 'Ready to play.';
playPauseBtn.focus();
}, (error) => {
status.textContent = 'Error decoding audio file.';
console.error('Error decoding audio data:', error);
});
};
reader.readAsArrayBuffer(file);
});
micBtn.addEventListener('click', async () => {
if (isMicActive) {
resetState();
status.textContent = 'Microphone off. Select a source.';
return;
}
resetState();
initializeAudioContext();
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
isMicActive = true;
micBtn.classList.add('!bg-red-800', '!border-red-800');
micBtn.innerHTML = '<ion-icon name="mic-off-outline"></ion-icon> Stop Microphone';
status.textContent = 'Listening...';
playbackControls.classList.add('hidden');
if (audioContext.state === 'suspended') {
audioContext.resume();
}
visualize();
} catch (err) {
status.textContent = 'Microphone access denied.';
console.error('Error accessing microphone:', err);
}
});
playPauseBtn.addEventListener('click', () => {
if (!source || isMicActive) return;
if (isPlaying) {
audioContext.suspend().then(() => {
isPlaying = false;
playPauseIcon.setAttribute('name', 'play-outline');
status.textContent = 'Paused.';
cancelAnimationFrame(animationFrameId);
});
} else {
audioContext.resume().then(() => {
// This handles both first play and resume
if (!isPlaying) { // To prevent multiple starts
try {
source.start(0);
} catch (e) {
// This will catch errors if source has already been started.
// This is expected when resuming.
}
}
isPlaying = true;
playPauseIcon.setAttribute('name', 'pause-outline');
status.textContent = 'Playing...';
visualize();
});
}
});
});
No comments yet. Be the first!