Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Audio Studio Pro | YouTube-Style 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> | |
| :root { | |
| --yt-red: #ff0000; | |
| --yt-dark: #0f0f0f; | |
| --yt-gray: #272727; | |
| --yt-light-gray: #3f3f3f; | |
| } | |
| .waveform { | |
| background: linear-gradient(90deg, #ff0000 0%, #cc0000 50%, #990000 100%); | |
| height: 100px; | |
| border-radius: 0.75rem; | |
| position: relative; | |
| overflow: hidden; | |
| box-shadow: 0 4px 20px rgba(255, 0, 0, 0.3); | |
| } | |
| .waveform::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(90deg, | |
| rgba(255,255,255,0.15) 0%, | |
| rgba(255,255,255,0.25) 50%, | |
| rgba(255,255,255,0.15) 100%); | |
| animation: wave 1.8s linear infinite; | |
| transform: translateX(-100%); | |
| } | |
| @keyframes wave { | |
| 100% { | |
| transform: translateX(100%); | |
| } | |
| } | |
| .recording-animation { | |
| animation: youtube-pulse 1.2s infinite; | |
| } | |
| @keyframes youtube-pulse { | |
| 0% { | |
| box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.8); | |
| transform: scale(1); | |
| } | |
| 50% { | |
| transform: scale(1.05); | |
| } | |
| 100% { | |
| box-shadow: 0 0 0 15px rgba(255, 0, 0, 0); | |
| transform: scale(1); | |
| } | |
| } | |
| .format-option:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 15px 30px rgba(255, 0, 0, 0.3); | |
| background: var(--yt-gray) ; | |
| } | |
| .format-option { | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| border: 1px solid var(--yt-light-gray); | |
| } | |
| .device-option:hover { | |
| background-color: var(--yt-gray) ; | |
| } | |
| .device-option { | |
| transition: all 0.3s ease; | |
| } | |
| .visualizer { | |
| width: 100%; | |
| height: 100px; | |
| background: var(--yt-dark); | |
| border-radius: 0.75rem; | |
| position: relative; | |
| overflow: hidden; | |
| box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); | |
| } | |
| .bar { | |
| position: absolute; | |
| bottom: 0; | |
| width: 6px; | |
| background: linear-gradient(to top, #ff0000, #cc0000); | |
| border-radius: 3px 3px 0 0; | |
| transition: height 0.08s ease; | |
| } | |
| .youtube-nav { | |
| background: var(--yt-dark); | |
| border-bottom: 1px solid var(--yt-light-gray); | |
| } | |
| .youtube-sidebar { | |
| background: var(--yt-dark); | |
| border-right: 1px solid var(--yt-light-gray); | |
| } | |
| .youtube-card { | |
| background: var(--yt-gray); | |
| border-radius: 1rem; | |
| border: 1px solid var(--yt-light-gray); | |
| box-shadow: 0 4px 25px rgba(0,0,0,0.4); | |
| transition: all 0.3s ease; | |
| } | |
| .youtube-card:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 8px 35px rgba(255, 0, 0, 0.2); | |
| } | |
| .gradient-text { | |
| background: linear-gradient(135deg, #ff0000, #cc0000); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .live-indicator { | |
| background: var(--yt-red); | |
| color: white; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-size: 0.75rem; | |
| font-weight: bold; | |
| animation: live-pulse 2s infinite; | |
| } | |
| @keyframes live-pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.7; } | |
| } | |
| .yt-button { | |
| background: var(--yt-gray); | |
| border: 1px solid var(--yt-light-gray); | |
| transition: all 0.3s ease; | |
| } | |
| .yt-button:hover { | |
| background: var(--yt-light-gray); | |
| } | |
| .yt-button-primary { | |
| background: var(--yt-red); | |
| border: 1px solid var(--yt-red); | |
| } | |
| .yt-button-primary:hover { | |
| background: #cc0000; | |
| border-color: #cc0000; | |
| } | |
| .progress-bar { | |
| height: 4px; | |
| background: var(--yt-light-gray); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: var(--yt-red); | |
| border-radius: 2px; | |
| transition: width 0.1s linear; | |
| } | |
| .recording-item { | |
| background: var(--yt-gray); | |
| border: 1px solid var(--yt-light-gray); | |
| border-radius: 0.75rem; | |
| transition: all 0.3s ease; | |
| } | |
| .recording-item:hover { | |
| background: #2a2a2a; | |
| border-color: #4a4a4a; | |
| } | |
| .sidebar-item { | |
| transition: all 0.2s ease; | |
| } | |
| .sidebar-item:hover { | |
| background: var(--yt-light-gray); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-[#0f0f0f] min-h-screen text-white"> | |
| <!-- YouTube-style Navigation --> | |
| <nav class="youtube-nav sticky top-0 z-50"> | |
| <div class="container mx-auto px-4 py-3"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center gap-6"> | |
| <div class="flex items-center gap-2"> | |
| <i class="fas fa-bars text-xl cursor-pointer"></i> | |
| <div class="flex items-center gap-2"> | |
| <i class="fab fa-youtube text-2xl text-red-500"></i> | |
| <span class="text-xl font-bold">Audio Studio Pro</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex-1 max-w-2xl mx-8"> | |
| <div class="relative"> | |
| <input type="text" placeholder="Search recordings..." class="w-full bg-[#121212] border border-[#303030] rounded-full py-2 px-4 text-sm focus:outline-none focus:border-[#3ea6ff] transition-all"> | |
| <i class="fas fa-search absolute right-3 top-2.5 text-gray-400"></i> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <div class="flex items-center gap-2"> | |
| <span class="live-indicator flex items-center gap-1"> | |
| <i class="fas fa-circle text-xs"></i> | |
| LIVE | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </nav> | |
| <div class="flex"> | |
| <!-- YouTube-style Sidebar --> | |
| <div class="youtube-sidebar w-64 min-h-screen hidden md:block"> | |
| <div class="p-4 space-y-2"> | |
| <div class="sidebar-item p-3 rounded-lg bg-[#272727] cursor-pointer"> | |
| <div class="flex items-center gap-3"> | |
| <i class="fas fa-home text-red-500"></i> | |
| <span>Home</span> | |
| </div> | |
| <div class="sidebar-item p-3 rounded-lg cursor-pointer"> | |
| <div class="flex items-center gap-3"> | |
| <i class="fas fa-fire text-red-500"></i> | |
| <span>Trending</span> | |
| </div> | |
| <div class="sidebar-item p-3 rounded-lg cursor-pointer"> | |
| <div class="flex items-center gap-3"> | |
| <i class="fas fa-microphone text-red-500"></i> | |
| <span>Recordings</span> | |
| </div> | |
| <div class="sidebar-item p-3 rounded-lg cursor-pointer"> | |
| <div class="flex items-center gap-3"> | |
| <i class="fas fa-music text-red-500"></i> | |
| <span>Music</span> | |
| </div> | |
| <div class="sidebar-item p-3 rounded-lg cursor-pointer"> | |
| <div class="flex items-center gap-3"> | |
| <i class="fas fa-cog text-red-500"></i> | |
| <span>Settings</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="flex-1"> | |
| <div class="container mx-auto px-4 py-6"> | |
| <div class="max-w-5xl mx-auto"> | |
| <!-- Header Section --> | |
| <div class="flex items-center justify-between mb-8"> | |
| <div> | |
| <h1 class="text-3xl font-bold gradient-text mb-2">Audio Studio Pro</h1> | |
| <p class="text-gray-400">Professional recording studio with YouTube-inspired interface</p> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <div class="flex items-center gap-2 text-sm text-gray-400"> | |
| <i class="fas fa-users"></i> | |
| <span>1.2K active</span> | |
| </div> | |
| </div> | |
| <!-- Main Recording Card --> | |
| <div class="youtube-card overflow-hidden mb-8"> | |
| <div class="p-6"> | |
| <!-- Live Status Header --> | |
| <div class="flex items-center justify-between mb-6"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div> | |
| <span class="text-red-500 font-semibold">RECORDING STUDIO</div> | |
| <div class="live-indicator flex items-center gap-2"> | |
| <i class="fas fa-circle text-xs"></i> | |
| LIVE INPUT | |
| </div> | |
| </div> | |
| <!-- Device Selection Section --> | |
| <div class="mb-6"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <h3 class="text-lg font-semibold text-white">Audio Input Devices</h3> | |
| <button id="refreshDevices" class="yt-button px-3 py-1 rounded-lg text-sm"> | |
| <i class="fas fa-sync-alt mr-1"></i> | |
| Refresh | |
| </button> | |
| </div> | |
| <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-300 mb-1">Audio Input</label> | |
| <select id="audioSource" class="w-full bg-[#272727] border border-[#3f3f3f] rounded-lg py-2 px-3 text-white focus:outline-none focus:border-[#ff0000] transition-all"> | |
| <option value="">Default Device</option> | |
| </select> | |
| </div> | |
| <div class="flex-1"> | |
| <label for="audioQuality" class="block text-sm font-medium text-gray-300 mb-1">Quality Preset</label> | |
| <select id="audioQuality" class="w-full bg-[#272727] border border-[#3f3f3f] rounded-lg py-2 px-3 text-white focus:outline-none focus:border-[#ff0000] transition-all"> | |
| <option value="high">Studio (24-bit/96kHz)</option> | |
| <option value="medium" selected>Professional (16-bit/48kHz)</option> | |
| <option value="low">Podcast (16-bit/44.1kHz)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Advanced Settings --> | |
| <div class="mb-4"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h4 class="text-sm font-medium text-gray-300">Advanced Settings</h4> | |
| <button id="toggleAdvanced" class="yt-button px-3 py-1 rounded-lg text-sm"> | |
| <i class="fas fa-sliders-h mr-1"></i> | |
| Show Advanced | |
| </button> | |
| </div> | |
| <div id="advancedSettings" class="hidden grid grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <label for="noiseReduction" class="block text-sm text-gray-400 mb-1">Noise Reduction</label> | |
| <select id="noiseReduction" class="w-full bg-[#272727] border border-[#3f3f3f] rounded-lg py-2 px-3 text-white focus:outline-none focus:border-[#ff0000] transition-all"> | |
| <option value="high">High</option> | |
| <option value="medium" selected>Medium</option> | |
| <option value="low">Low</option> | |
| <option value="off">Off</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="autoGain" class="block text-sm text-gray-400 mb-1">Auto Gain</label> | |
| <select id="autoGain" class="w-full bg-[#272727] border border-[#3f3f3f] rounded-lg py-2 px-3 text-white focus:outline-none focus:border-[#ff0000] transition-all"> | |
| <option value="on" selected>On</option> | |
| <option value="off">Off</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="visualizer mb-4" id="visualizer"> | |
| <!-- Audio bars will be inserted here by JavaScript --> | |
| </div> | |
| <div class="flex items-center justify-between text-sm"> | |
| <div id="deviceStatus" class="text-gray-400"> | |
| <i class="fas fa-microphone-slash mr-1"></i> No device selected | |
| </div> | |
| <div class="text-gray-400"> | |
| <i class="fas fa-database mr-1"></i> | |
| <span id="fileSize">0 MB</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Waveform Display --> | |
| <div class="waveform mb-6" id="waveform"></div> | |
| <!-- Control Buttons --> | |
| <div class="flex flex-col sm:flex-row justify-center items-center gap-4 mb-6"> | |
| <button id="recordBtn" class="yt-button-primary font-bold py-3 px-8 rounded-full flex items-center gap-2 transition-all"> | |
| <i class="fas fa-record-vinyl"></i> Start Recording | |
| </button> | |
| <button id="stopBtn" disabled class="yt-button font-bold py-3 px-8 rounded-full flex items-center gap-2 transition-all"> | |
| <i class="fas fa-stop"></i> Stop | |
| </button> | |
| <button id="playBtn" disabled class="yt-button font-bold py-3 px-8 rounded-full flex items-center gap-2 transition-all"> | |
| <i class="fas fa-play"></i> Preview | |
| </button> | |
| </div> | |
| <!-- Progress and Timer --> | |
| <div class="mb-4"> | |
| <div class="progress-bar mb-2"> | |
| <div id="progressFill" class="progress-fill" style="width: 0%"></div> | |
| <div class="flex justify-between items-center"> | |
| <span id="timer" class="text-gray-400">00:00</span> | |
| <span class="text-gray-400"> | |
| <i class="fas fa-volume-up mr-1"></i> | |
| <span id="volumeLevel">50%</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Download Section --> | |
| <div class="bg-[#1f1f1f] p-6 border-t border-[#3f3f3f]"> | |
| <h3 class="text-lg font-semibold text-white mb-4">Export & Download</h3> | |
| <div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6"> | |
| <div class="format-option bg-[#272727] p-4 rounded-lg text-center cursor-pointer" data-format="wav"> | |
| <i class="fas fa-file-waveform text-3xl text-red-500 mb-2"></i> | |
| <p class="font-medium">WAV</p> | |
| <p class="text-sm text-gray-400">Lossless Quality</p> | |
| </div> | |
| <div class="format-option bg-[#272727] p-4 rounded-lg text-center cursor-pointer" data-format="mp3"> | |
| <i class="fas fa-file-audio text-3xl text-red-400 mb-2"></i> | |
| <p class="font-medium">MP3</p> | |
| <p class="text-sm text-gray-400">Universal Format</p> | |
| </div> | |
| <div class="format-option bg-[#272727] p-4 rounded-lg text-center cursor-pointer" data-format="ogg"> | |
| <i class="fas fa-file-audio text-3xl text-red-300 mb-2"></i> | |
| <p class="font-medium">OGG</p> | |
| <p class="text-sm text-gray-400">Web Streaming</p> | |
| </div> | |
| <div class="format-option bg-[#272727] p-4 rounded-lg text-center cursor-pointer" data-format="aac"> | |
| <i class="fas fa-file-audio text-3xl text-red-200 mb-2"></i> | |
| <p class="font-medium">AAC</p> | |
| <p class="text-sm text-gray-400">Mobile Devices</p> | |
| </div> | |
| </div> | |
| <div class="flex gap-4"> | |
| <button id="downloadBtn" disabled class="flex-1 yt-button-primary font-bold py-3 px-4 rounded-lg flex items-center justify-center gap-2 transition-all"> | |
| <i class="fas fa-download"></i> Download Selected Format | |
| </button> | |
| <button id="shareBtn" disabled class="yt-button px-4 py-3 rounded-lg flex items-center gap-2"> | |
| <i class="fas fa-share-alt"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Recording History Section --> | |
| <div class="youtube-card mt-8"> | |
| <div class="p-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-semibold text-white">Recording Library</h3> | |
| <div class="flex items-center gap-2 text-sm text-gray-400"> | |
| <i class="fas fa-history"></i> | |
| <span>Recent Recordings</span> | |
| </div> | |
| </div> | |
| <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 in your library</p> | |
| </div> | |
| </div> | |
| </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 shareBtn = document.getElementById('shareBtn'); | |
| 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'); | |
| const refreshDevicesBtn = document.getElementById('refreshDevices'); | |
| const toggleAdvancedBtn = document.getElementById('toggleAdvanced'); | |
| const advancedSettings = document.getElementById('advancedSettings'); | |
| const progressFill = document.getElementById('progressFill'); | |
| const fileSize = document.getElementById('fileSize'); | |
| const volumeLevel = document.getElementById('volumeLevel'); | |
| const mobileRecordBtn = document.getElementById('mobileRecordBtn'); | |
| let selectedFormat = 'wav'; // Default format | |
| let isAdvancedVisible = false; | |
| // Initialize features | |
| function initFeatures() { | |
| // Toggle advanced settings | |
| toggleAdvancedBtn.addEventListener('click', function() { | |
| isAdvancedVisible = !isAdvancedVisible; | |
| if (isAdvancedVisible) { | |
| advancedSettings.classList.remove('hidden'); | |
| this.innerHTML = '<i class="fas fa-times mr-1"></i> Hide Advanced'; | |
| } else { | |
| advancedSettings.classList.add('hidden'); | |
| this.innerHTML = '<i class="fas fa-sliders-h mr-1"></i> Show Advanced'; | |
| } | |
| }); | |
| // Refresh devices | |
| refreshDevicesBtn.addEventListener('click', function() { | |
| this.classList.add('animate-spin'); | |
| getAudioDevices().then(success => { | |
| setTimeout(() => { | |
| this.classList.remove('animate-spin'); | |
| // Trigger device selection update | |
| if (audioSourceSelect.value) { | |
| audioSourceSelect.dispatchEvent(new Event('change')); | |
| }, 500); | |
| }); | |
| // Mobile record button | |
| if (mobileRecordBtn) { | |
| mobileRecordBtn.addEventListener('click', function() { | |
| recordBtn.click(); | |
| }); | |
| } | |
| // Volume control simulation | |
| let volume = 50; | |
| setInterval(() => { | |
| if (audioElement && !audioElement.paused) { | |
| volume = Math.min(100, Math.max(0, volume + (Math.random() * 4 - 2)); | |
| volumeLevel.textContent = `${Math.round(volume)}%`; | |
| } | |
| }, 1000); | |
| } | |
| // 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-red-500')); | |
| this.classList.add('ring-2', 'ring-red-500'); | |
| selectedFormat = this.dataset.format; | |
| }); | |
| }); | |
| // Set first format as selected by default | |
| formatOptions[0].classList.add('ring-2', 'ring-red-500'); | |
| // Initialize features | |
| initFeatures(); | |
| 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 Device</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; | |
| } | |
| } | |
| // Simulate file size calculation | |
| function updateFileSize(duration) { | |
| const baseSize = duration * 0.1; // MB per minute | |
| fileSize.textContent = `${baseSize.toFixed(1)} MB estimated'; | |
| } | |
| // 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 = 96000; | |
| constraints.audio.channelCount = 2; | |
| constraints.audio.sampleSize = 24; | |
| constraints.audio.bitrate = 320000; | |
| } else if (quality === 'medium') { | |
| constraints.audio.sampleRate = 48000; | |
| constraints.audio.channelCount = 1; | |
| constraints.audio.sampleSize = 16; | |
| constraints.audio.bitrate = 192000; | |
| } else { // low | |
| constraints.audio.sampleRate = 44100; | |
| constraints.audio.channelCount = 1; | |
| constraints.audio.sampleSize = 16; | |
| constraints.audio.bitrate = 128000; | |
| } | |
| return constraints; | |
| } | |
| // Enhanced visualization with YouTube colors | |
| 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 = 512; | |
| // 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 with YouTube red gradient | |
| 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}%`; | |
| // Dynamic color based on amplitude | |
| const hue = 0; // Red | |
| const saturation = 100; | |
| const lightness = 40 + (height * 0.3); | |
| bars[i].style.background = `linear-gradient(to top, hsl(${hue}, ${saturation}%, ${lightness}%), hsl(${hue}, ${saturation}%, ${Math.max(20, lightness - 20)}%)`; | |
| } | |
| } | |
| // Start animation | |
| animate(); | |
| // Update device status with YouTube style | |
| deviceStatus.innerHTML = `<i class="fas fa-microphone mr-1"></i> <span class="text-red-400">${audioSourceSelect.options[audioSourceSelect.selectedIndex].text}</span>`; | |
| } | |
| // 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); | |
| updateFileSize(0); | |
| } 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')); | |
| } | |
| }); | |
| } | |
| }); | |
| // Enhanced recording with progress tracking | |
| 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, download, and share buttons | |
| playBtn.disabled = false; | |
| downloadBtn.disabled = false; | |
| shareBtn.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 and progress | |
| seconds = 0; | |
| updateTimer(); | |
| updateProgress(); | |
| timerInterval = setInterval(() => { | |
| seconds++; | |
| updateTimer(); | |
| updateProgress(); | |
| updateFileSize(seconds / 60); | |
| }, 1000); | |
| // Update UI with YouTube styling | |
| recordBtn.disabled = true; | |
| recordBtn.classList.remove('yt-button-primary'); | |
| recordBtn.classList.add('bg-gray-600', 'cursor-not-allowed'); | |
| stopBtn.disabled = false; | |
| stopBtn.classList.remove('yt-button'); | |
| stopBtn.classList.add('yt-button-primary'); | |
| // Add recording animation | |
| recordBtn.classList.add('recording-animation'); | |
| // Update mobile button if exists | |
| if (mobileRecordBtn) { | |
| mobileRecordBtn.classList.add('recording-animation'); | |
| } | |
| } catch (error) { | |
| console.error('Error accessing microphone:', error); | |
| alert('Could not access microphone. Please check permissions.'); | |
| } | |
| }); | |
| // Enhanced stop recording | |
| stopBtn.addEventListener('click', function() { | |
| if (mediaRecorder && mediaRecorder.state !== 'inactive') { | |
| mediaRecorder.stop(); | |
| // Stop all tracks in the stream | |
| stopStream(currentStream); | |
| // Clear timer and progress | |
| clearInterval(timerInterval); | |
| progressFill.style.width = '0%'; | |
| // Update UI | |
| recordBtn.disabled = false; | |
| recordBtn.classList.remove('bg-gray-600', 'cursor-not-allowed'); | |
| recordBtn.classList.add('yt-button-primary'); | |
| stopBtn.disabled = true; | |
| stopBtn.classList.remove('yt-button-primary'); | |
| stopBtn.classList.add('yt-button'); | |
| // Remove recording animation | |
| recordBtn.classList.remove('recording-animation'); | |
| // Update mobile button if exists | |
| if (mobileRecordBtn) { | |
| mobileRecordBtn.classList.remove('recording-animation'); | |
| } | |
| } | |
| }); | |
| // Enhanced play with progress tracking | |
| playBtn.addEventListener('click', function() { | |
| if (audioElement) { | |
| if (audioElement.paused) { | |
| audioElement.play(); | |
| playBtn.innerHTML = '<i class="fas fa-pause"></i> Pause'; | |
| // Update progress as audio plays | |
| const updatePlayProgress = setInterval(() => { | |
| if (audioElement.paused || audioElement.ended) { | |
| clearInterval(updatePlayProgress); | |
| playBtn.innerHTML = '<i class="fas fa-play"></i> Preview'; | |
| progressFill.style.width = '0%'; | |
| } else { | |
| const progress = (audioElement.currentTime / audioElement.duration) * 100; | |
| progressFill.style.width = `${progress}%`; | |
| }, 100); | |
| } else { | |
| audioElement.pause(); | |
| playBtn.innerHTML = '<i class="fas fa-play"></i> Preview'; | |
| } | |
| } | |
| }); | |
| // Enhanced download | |
| 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); | |
| }, 100); | |
| } | |
| }); | |
| // Share functionality | |
| shareBtn.addEventListener('click', function() { | |
| if (audioBlob) { | |
| if (navigator.share) { | |
| navigator.share({ | |
| title: 'My Recording', | |
| text: 'Check out this recording I made!', | |
| files: [new File([audioBlob], `recording_${new Date().toISOString().slice(0, 19).replace(/[:T-]/g, '_')}.${selectedFormat}` | |
| }).catch(console.error); | |
| } else { | |
| alert('Sharing not supported in this browser. Download the file instead.'); | |
| } | |
| } | |
| }); | |
| // Update timer display | |
| function updateTimer() { | |
| const minutes = Math.floor(seconds / 60); | |
| const remainingSeconds = seconds % 60; | |
| timer.textContent = `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; | |
| } | |
| // Update progress bar | |
| function updateProgress() { | |
| const progress = (seconds / 300) * 100; // 5 minute max | |
| progressFill.style.width = `${Math.min(100, progress)}%`; | |
| } | |
| // 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')}`; | |
| } | |
| // Enhanced recording history with YouTube cards | |
| function addRecordingToList(url) { | |
| // Remove placeholder if it exists | |
| if (recordingsList.querySelector('.text-center')) { | |
| recordingsList.innerHTML = ''; | |
| } | |
| const recordingItem = document.createElement('div'); | |
| recordingItem.className = 'recording-item p-4 flex items-center justify-between'; | |
| recordingItem.innerHTML = ` | |
| <div class="flex items-center gap-4"> | |
| <div class="w-12 h-12 bg-red-500 rounded-lg flex items-center justify-center"> | |
| <i class="fas fa-wave-square"></i> | |
| </div> | |
| <div class="flex-1"> | |
| <p class="font-medium text-white">Recording ${recordingsList.children.length + 1}</p> | |
| <p class="text-sm text-gray-400">${new Date().toLocaleString()}</p> | |
| <p class="text-xs text-gray-500">${audioSourceSelect.options[audioSourceSelect.selectedIndex].text}</p> | |
| </div> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button class="play-history-btn yt-button p-2 rounded-lg"> | |
| <i class="fas fa-play text-xs"></i> | |
| </button> | |
| <button class="download-history-btn yt-button p-2 rounded-lg"> | |
| <i class="fas fa-download text-xs"></i> | |
| </button> | |
| <button class="share-history-btn yt-button p-2 rounded-lg"> | |
| <i class="fas fa-share-alt text-xs"></i> | |
| </button> | |
| </div> | |
| </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 shareBtn = recordingItem.querySelector('.share-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); | |
| }); | |
| shareBtn.addEventListener('click', function() { | |
| if (navigator.share) { | |
| navigator.share({ | |
| title: `Recording ${recordingsList.children.length}`, | |
| text: 'Check out this recording I made with Audio Studio Pro!', | |
| }); | |
| } | |
| }); | |
| // Reset play button when audio ends | |
| audio.addEventListener('ended', function() { | |
| playBtn.innerHTML = '<i class="fas fa-play text-xs"></i>'; | |
| }); | |
| } | |
| }); | |
| </script> | |
| <!-- Footer with credits --> | |
| <div class="mt-12 border-t border-[#3f3f3f] pt-6"> | |
| <div class="flex flex-col sm:flex-row justify-between items-center text-sm text-gray-400"> | |
| <div class="flex items-center gap-4"> | |
| <span>Powered by</span> | |
| <a href="https://huggingface.co/terastudio" target="_blank" class="hover:text-white transition-colors"> | |
| <i class="fas fa-brain mr-1"></i> | |
| TeraStudio AI | |
| </a> | |
| </div> | |
| <div class="flex items-center gap-4 mt-4 sm:mt-0"> | |
| <span>Made with</span> | |
| <a href="https://enzostvs-deepsite.hf.space" target="_blank" class="hover:text-white transition-colors"> | |
| <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);"> | |
| DeepSite | |
| </a> | |
| <span>•</span> | |
| <a href="https://enzostvs-deepsite.hf.space?remix=NeoPy/audio-recorder" target="_blank" class="hover:text-white transition-colors"> | |
| 🧬 Remix Project | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Floating Action Button for Mobile --> | |
| <div class="fixed bottom-6 right-6 md:hidden"> | |
| <button id="mobileRecordBtn" class="yt-button-primary w-14 h-14 rounded-full flex items-center justify-center shadow-lg"> | |
| <i class="fas fa-microphone"></i> | |
| </button> | |
| </div> | |
| </body> | |
| </html> |