Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Advanced Audio Recorder</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.waveform { | |
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%); | |
height: 100px; | |
border-radius: 0.5rem; | |
position: relative; | |
overflow: hidden; | |
} | |
.waveform::after { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background: linear-gradient(90deg, | |
rgba(255,255,255,0.1) 0%, | |
rgba(255,255,255,0.3) 50%, | |
rgba(255,255,255,0.1) 100%); | |
animation: wave 2s linear infinite; | |
transform: translateX(-100%); | |
} | |
@keyframes wave { | |
100% { | |
transform: translateX(100%); | |
} | |
} | |
.recording-animation { | |
animation: pulse 1.5s infinite; | |
} | |
@keyframes pulse { | |
0% { | |
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); | |
} | |
70% { | |
box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); | |
} | |
100% { | |
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); | |
} | |
} | |
.format-option:hover { | |
transform: translateY(-3px); | |
box-shadow: 0 10px 20px rgba(0,0,0,0.1); | |
} | |
.format-option { | |
transition: all 0.3s ease; | |
} | |
.device-option:hover { | |
background-color: #f3f4f6; | |
} | |
.device-option { | |
transition: all 0.2s ease; | |
} | |
.visualizer { | |
width: 100%; | |
height: 80px; | |
background-color: #f3f4f6; | |
border-radius: 0.5rem; | |
position: relative; | |
overflow: hidden; | |
} | |
.bar { | |
position: absolute; | |
bottom: 0; | |
width: 8px; | |
background-color: #3b82f6; | |
border-radius: 4px 4px 0 0; | |
transition: height 0.1s ease; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 min-h-screen"> | |
<div class="container mx-auto px-4 py-8"> | |
<div class="max-w-3xl mx-auto"> | |
<div class="text-center mb-8"> | |
<h1 class="text-4xl font-bold text-gray-800 mb-2">Advanced Audio Recorder</h1> | |
<p class="text-gray-600">Select your input device, record, and download in multiple formats</p> | |
</div> | |
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8"> | |
<div class="p-6"> | |
<!-- Device Selection Section --> | |
<div class="mb-6"> | |
<h3 class="text-lg font-semibold text-gray-800 mb-3">Select Input Device</h3> | |
<div class="flex flex-col sm:flex-row gap-4 mb-4"> | |
<div class="flex-1"> | |
<label for="audioSource" class="block text-sm font-medium text-gray-700 mb-1">Audio Input</label> | |
<select id="audioSource" class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 py-2 px-3 border"> | |
<option value="">Default</option> | |
</select> | |
</div> | |
<div class="flex-1"> | |
<label for="audioQuality" class="block text-sm font-medium text-gray-700 mb-1">Quality</label> | |
<select id="audioQuality" class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 py-2 px-3 border"> | |
<option value="high">High (CD Quality)</option> | |
<option value="medium" selected>Medium (Good Quality)</option> | |
<option value="low">Low (Voice Only)</option> | |
</select> | |
</div> | |
</div> | |
<div class="visualizer mb-4" id="visualizer"> | |
<!-- Audio bars will be inserted here by JavaScript --> | |
</div> | |
<div class="text-center text-sm text-gray-500" id="deviceStatus"> | |
<i class="fas fa-microphone-slash mr-1"></i> No device selected | |
</div> | |
</div> | |
<div class="waveform mb-6" id="waveform"></div> | |
<div class="flex flex-col sm:flex-row justify-center items-center gap-4 mb-6"> | |
<button id="recordBtn" class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-6 rounded-full flex items-center gap-2 transition-all"> | |
<i class="fas fa-microphone"></i> Start Recording | |
</button> | |
<button id="stopBtn" disabled class="bg-gray-300 text-gray-600 font-bold py-3 px-6 rounded-full flex items-center gap-2 transition-all"> | |
<i class="fas fa-stop"></i> Stop | |
</button> | |
<button id="playBtn" disabled class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-full flex items-center gap-2 transition-all"> | |
<i class="fas fa-play"></i> Play | |
</button> | |
</div> | |
<div class="text-center text-gray-500 mb-4"> | |
<span id="timer">00:00</span> | |
</div> | |
</div> | |
<div class="bg-gray-50 p-6 border-t border-gray-200"> | |
<h3 class="text-lg font-semibold text-gray-800 mb-4">Download Options</h3> | |
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4"> | |
<div class="format-option bg-white p-4 rounded-lg border border-gray-200 text-center cursor-pointer" data-format="wav"> | |
<i class="fas fa-file-audio text-3xl text-blue-500 mb-2"></i> | |
<p class="font-medium">WAV</p> | |
<p class="text-sm text-gray-500">High quality</p> | |
</div> | |
<div class="format-option bg-white p-4 rounded-lg border border-gray-200 text-center cursor-pointer" data-format="mp3"> | |
<i class="fas fa-file-audio text-3xl text-purple-500 mb-2"></i> | |
<p class="font-medium">MP3</p> | |
<p class="text-sm text-gray-500">Compressed</p> | |
</div> | |
<div class="format-option bg-white p-4 rounded-lg border border-gray-200 text-center cursor-pointer" data-format="ogg"> | |
<i class="fas fa-file-audio text-3xl text-green-500 mb-2"></i> | |
<p class="font-medium">OGG</p> | |
<p class="text-sm text-gray-500">Web optimized</p> | |
</div> | |
<div class="format-option bg-white p-4 rounded-lg border border-gray-200 text-center cursor-pointer" data-format="aac"> | |
<i class="fas fa-file-audio text-3xl text-red-500 mb-2"></i> | |
<p class="font-medium">AAC</p> | |
<p class="text-sm text-gray-500">Mobile friendly</p> | |
</div> | |
</div> | |
<button id="downloadBtn" disabled class="mt-6 w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-4 rounded-lg flex items-center justify-center gap-2 transition-all"> | |
<i class="fas fa-download"></i> Download Audio | |
</button> | |
</div> | |
</div> | |
<div class="bg-white rounded-xl shadow-lg overflow-hidden"> | |
<div class="p-6"> | |
<h3 class="text-lg font-semibold text-gray-800 mb-4">Recording History</h3> | |
<div id="recordingsList" class="space-y-3"> | |
<div class="text-center text-gray-500 py-8"> | |
<i class="fas fa-microphone-slash text-3xl mb-2"></i> | |
<p>No recordings yet</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Audio recording variables | |
let mediaRecorder; | |
let audioChunks = []; | |
let audioBlob; | |
let audioUrl; | |
let audioElement; | |
let timerInterval; | |
let seconds = 0; | |
let audioContext; | |
let analyser; | |
let microphone; | |
let dataArray; | |
let animationId; | |
let currentStream; | |
// DOM elements | |
const recordBtn = document.getElementById('recordBtn'); | |
const stopBtn = document.getElementById('stopBtn'); | |
const playBtn = document.getElementById('playBtn'); | |
const downloadBtn = document.getElementById('downloadBtn'); | |
const timer = document.getElementById('timer'); | |
const recordingsList = document.getElementById('recordingsList'); | |
const formatOptions = document.querySelectorAll('.format-option'); | |
const audioSourceSelect = document.getElementById('audioSource'); | |
const audioQualitySelect = document.getElementById('audioQuality'); | |
const visualizer = document.getElementById('visualizer'); | |
const deviceStatus = document.getElementById('deviceStatus'); | |
let selectedFormat = 'wav'; // Default format | |
// Create audio bars for visualizer | |
const barCount = 40; | |
for (let i = 0; i < barCount; i++) { | |
const bar = document.createElement('div'); | |
bar.className = 'bar'; | |
bar.style.left = `${i * (100 / barCount)}%`; | |
bar.style.height = '0%'; | |
visualizer.appendChild(bar); | |
} | |
const bars = document.querySelectorAll('.bar'); | |
// Format selection | |
formatOptions.forEach(option => { | |
option.addEventListener('click', function() { | |
formatOptions.forEach(opt => opt.classList.remove('ring-2', 'ring-indigo-500')); | |
this.classList.add('ring-2', 'ring-indigo-500'); | |
selectedFormat = this.dataset.format; | |
}); | |
}); | |
// Set first format as selected by default | |
formatOptions[0].classList.add('ring-2', 'ring-indigo-500'); | |
// Get available audio devices | |
async function getAudioDevices() { | |
try { | |
// First get permission by accessing media | |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
stopStream(stream); // We don't need this stream, just needed for permission | |
// Now enumerate devices | |
const devices = await navigator.mediaDevices.enumerateDevices(); | |
const audioInputs = devices.filter(device => device.kind === 'audioinput'); | |
// Clear existing options | |
audioSourceSelect.innerHTML = '<option value="">Default</option>'; | |
// Add each audio input device as an option | |
audioInputs.forEach(device => { | |
const option = document.createElement('option'); | |
option.value = device.deviceId; | |
option.text = device.label || `Microphone ${audioSourceSelect.length}`; | |
audioSourceSelect.appendChild(option); | |
}); | |
return true; | |
} catch (error) { | |
console.error('Error getting audio devices:', error); | |
deviceStatus.innerHTML = '<i class="fas fa-exclamation-triangle mr-1"></i> Could not access microphone devices'; | |
return false; | |
} | |
} | |
// Stop a media stream | |
function stopStream(stream) { | |
if (stream) { | |
stream.getTracks().forEach(track => track.stop()); | |
} | |
} | |
// Visualize audio input | |
function visualizeAudio(stream) { | |
// Create audio context if it doesn't exist | |
if (!audioContext) { | |
audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
} | |
// Create analyser node | |
analyser = audioContext.createAnalyser(); | |
analyser.fftSize = 256; | |
// Create microphone source | |
microphone = audioContext.createMediaStreamSource(stream); | |
microphone.connect(analyser); | |
// Create data array for analysis | |
const bufferLength = analyser.frequencyBinCount; | |
dataArray = new Uint8Array(bufferLength); | |
// Start animation loop | |
function animate() { | |
animationId = requestAnimationFrame(animate); | |
analyser.getByteFrequencyData(dataArray); | |
// Update bars | |
for (let i = 0; i < bars.length; i++) { | |
// Scale the index to cover the full frequency range | |
const scaledIndex = Math.floor(i * (bufferLength / bars.length)); | |
const value = dataArray[scaledIndex]; | |
const height = value / 2.55; // Convert 0-255 to 0-100 | |
bars[i].style.height = `${height}%`; | |
bars[i].style.backgroundColor = `hsl(${200 + (height * 1.5)}, 100%, 50%)`; | |
} | |
} | |
// Start animation | |
animate(); | |
// Update device status | |
deviceStatus.innerHTML = `<i class="fas fa-microphone mr-1"></i> ${audioSourceSelect.options[audioSourceSelect.selectedIndex].text}`; | |
} | |
// Stop visualization | |
function stopVisualization() { | |
if (animationId) { | |
cancelAnimationFrame(animationId); | |
animationId = null; | |
} | |
// Reset bars | |
bars.forEach(bar => { | |
bar.style.height = '0%'; | |
bar.style.backgroundColor = '#3b82f6'; | |
}); | |
// Update device status | |
deviceStatus.innerHTML = '<i class="fas fa-microphone-slash mr-1"></i> No active input'; | |
} | |
// Get audio constraints based on selected quality | |
function getAudioConstraints() { | |
const deviceId = audioSourceSelect.value; | |
const quality = audioQualitySelect.value; | |
let constraints = { | |
audio: { | |
deviceId: deviceId ? { exact: deviceId } : undefined, | |
echoCancellation: true, | |
noiseSuppression: true, | |
autoGainControl: true | |
} | |
}; | |
// Adjust constraints based on quality | |
if (quality === 'high') { | |
constraints.audio.sampleRate = 48000; | |
constraints.audio.channelCount = 2; | |
constraints.audio.sampleSize = 16; | |
constraints.audio.bitrate = 256000; | |
} else if (quality === 'medium') { | |
constraints.audio.sampleRate = 44100; | |
constraints.audio.channelCount = 1; | |
constraints.audio.sampleSize = 16; | |
constraints.audio.bitrate = 128000; | |
} else { // low | |
constraints.audio.sampleRate = 22050; | |
constraints.audio.channelCount = 1; | |
constraints.audio.sampleSize = 8; | |
constraints.audio.bitrate = 64000; | |
} | |
return constraints; | |
} | |
// Initialize device selection | |
getAudioDevices().then(success => { | |
if (success) { | |
// When device selection changes, update the visualization | |
audioSourceSelect.addEventListener('change', async function() { | |
// Stop any existing visualization | |
stopVisualization(); | |
if (currentStream) { | |
stopStream(currentStream); | |
} | |
// If a device is selected, start visualization | |
if (this.value) { | |
try { | |
const constraints = getAudioConstraints(); | |
currentStream = await navigator.mediaDevices.getUserMedia(constraints); | |
visualizeAudio(currentStream); | |
} catch (error) { | |
console.error('Error accessing selected device:', error); | |
deviceStatus.innerHTML = '<i class="fas fa-exclamation-triangle mr-1"></i> Could not access selected device'; | |
} | |
} | |
}); | |
// Also update when quality changes | |
audioQualitySelect.addEventListener('change', function() { | |
if (audioSourceSelect.value) { | |
// Trigger change event to restart with new quality | |
audioSourceSelect.dispatchEvent(new Event('change')); | |
} | |
}); | |
} | |
}); | |
// Request permission to use microphone | |
recordBtn.addEventListener('click', async function() { | |
try { | |
const constraints = getAudioConstraints(); | |
const stream = await navigator.mediaDevices.getUserMedia(constraints); | |
currentStream = stream; | |
// Stop visualization if it's running | |
stopVisualization(); | |
mediaRecorder = new MediaRecorder(stream); | |
audioChunks = []; | |
mediaRecorder.ondataavailable = function(event) { | |
audioChunks.push(event.data); | |
}; | |
mediaRecorder.onstop = function() { | |
audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); | |
audioUrl = URL.createObjectURL(audioBlob); | |
// Create audio element for playback | |
audioElement = new Audio(audioUrl); | |
// Enable play and download buttons | |
playBtn.disabled = false; | |
downloadBtn.disabled = false; | |
// Add to recordings list | |
addRecordingToList(audioUrl); | |
// Restart visualization if a device was selected | |
if (audioSourceSelect.value) { | |
visualizeAudio(currentStream); | |
} | |
}; | |
mediaRecorder.start(10); // Collect data every 10ms | |
// Start timer | |
seconds = 0; | |
updateTimer(); | |
timerInterval = setInterval(updateTimer, 1000); | |
// Update UI | |
recordBtn.disabled = true; | |
recordBtn.classList.remove('bg-red-500', 'hover:bg-red-600'); | |
recordBtn.classList.add('bg-gray-300', 'text-gray-600'); | |
stopBtn.disabled = false; | |
stopBtn.classList.remove('bg-gray-300', 'text-gray-600'); | |
stopBtn.classList.add('bg-gray-800', 'hover:bg-gray-900', 'text-white'); | |
// Add recording animation | |
recordBtn.classList.add('recording-animation'); | |
} catch (error) { | |
console.error('Error accessing microphone:', error); | |
alert('Could not access microphone. Please check permissions.'); | |
} | |
}); | |
// Stop recording | |
stopBtn.addEventListener('click', function() { | |
if (mediaRecorder && mediaRecorder.state !== 'inactive') { | |
mediaRecorder.stop(); | |
// Stop all tracks in the stream | |
stopStream(currentStream); | |
// Clear timer | |
clearInterval(timerInterval); | |
// Update UI | |
recordBtn.disabled = false; | |
recordBtn.classList.remove('bg-gray-300', 'text-gray-600'); | |
recordBtn.classList.add('bg-red-500', 'hover:bg-red-600'); | |
stopBtn.disabled = true; | |
stopBtn.classList.remove('bg-gray-800', 'hover:bg-gray-900', 'text-white'); | |
stopBtn.classList.add('bg-gray-300', 'text-gray-600'); | |
// Remove recording animation | |
recordBtn.classList.remove('recording-animation'); | |
} | |
}); | |
// Play recording | |
playBtn.addEventListener('click', function() { | |
if (audioElement) { | |
if (audioElement.paused) { | |
audioElement.play(); | |
playBtn.innerHTML = '<i class="fas fa-pause"></i> Pause'; | |
} else { | |
audioElement.pause(); | |
playBtn.innerHTML = '<i class="fas fa-play"></i> Play'; | |
} | |
} | |
}); | |
// Download recording | |
downloadBtn.addEventListener('click', function() { | |
if (audioBlob) { | |
const a = document.createElement('a'); | |
a.style.display = 'none'; | |
a.href = audioUrl; | |
a.download = `recording_${new Date().toISOString().slice(0, 19).replace(/[:T-]/g, '_')}.${selectedFormat}`; | |
document.body.appendChild(a); | |
a.click(); | |
setTimeout(() => { | |
document.body.removeChild(a); | |
window.URL.revokeObjectURL(audioUrl); | |
}, 100); | |
} | |
}); | |
// Update timer display | |
function updateTimer() { | |
seconds++; | |
const minutes = Math.floor(seconds / 60); | |
const remainingSeconds = seconds % 60; | |
timer.textContent = `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; | |
} | |
// Add recording to history list | |
function addRecordingToList(url) { | |
// Remove placeholder if it exists | |
if (recordingsList.querySelector('.text-center')) { | |
recordingsList.innerHTML = ''; | |
} | |
const recordingItem = document.createElement('div'); | |
recordingItem.className = 'bg-gray-50 p-4 rounded-lg border border-gray-200 flex items-center justify-between'; | |
recordingItem.innerHTML = ` | |
<div class="flex items-center gap-3"> | |
<i class="fas fa-microphone text-indigo-500"></i> | |
<div> | |
<p class="font-medium">Recording ${recordingsList.children.length + 1}</p> | |
<p class="text-sm text-gray-500">${new Date().toLocaleString()}</p> | |
<p class="text-xs text-gray-400">${audioSourceSelect.options[audioSourceSelect.selectedIndex].text}</p> | |
</div> | |
</div> | |
<div class="flex gap-2"> | |
<button class="play-history-btn bg-blue-500 hover:bg-blue-600 text-white p-2 rounded-full"> | |
<i class="fas fa-play text-xs"></i> | |
</button> | |
<button class="download-history-btn bg-indigo-500 hover:bg-indigo-600 text-white p-2 rounded-full"> | |
<i class="fas fa-download text-xs"></i> | |
</button> | |
</div> | |
`; | |
recordingsList.prepend(recordingItem); | |
// Add event listeners to the new buttons | |
const playBtn = recordingItem.querySelector('.play-history-btn'); | |
const downloadBtn = recordingItem.querySelector('.download-history-btn'); | |
const audio = new Audio(url); | |
playBtn.addEventListener('click', function() { | |
if (audio.paused) { | |
// Stop all other audio elements first | |
document.querySelectorAll('audio').forEach(a => { | |
if (a !== audio) a.pause(); | |
}); | |
audio.play(); | |
playBtn.innerHTML = '<i class="fas fa-pause text-xs"></i>'; | |
} else { | |
audio.pause(); | |
playBtn.innerHTML = '<i class="fas fa-play text-xs"></i>'; | |
} | |
}); | |
downloadBtn.addEventListener('click', function() { | |
const a = document.createElement('a'); | |
a.style.display = 'none'; | |
a.href = url; | |
a.download = `recording_${new Date().toISOString().slice(0, 19).replace(/[:T-]/g, '_')}.${selectedFormat}`; | |
document.body.appendChild(a); | |
a.click(); | |
setTimeout(() => { | |
document.body.removeChild(a); | |
}, 100); | |
}); | |
// Reset play button when audio ends | |
audio.addEventListener('ended', function() { | |
playBtn.innerHTML = '<i class="fas fa-play text-xs"></i>'; | |
}); | |
} | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=NeoPy/audio-recorder" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |