Created
September 8, 2025 03:22
-
-
Save jamiew/5758b1b2f01759e0c95cecc178a7fb80 to your computer and use it in GitHub Desktop.
megavoice by Claude - can't use directly on claude.ai becaues no mic permissions allowed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MEGA VOICE ULTRA™</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Arial Black', sans-serif; | |
| background: linear-gradient(135deg, #0a0a0a 0%, #1a0033 100%); | |
| min-height: 100vh; | |
| color: #fff; | |
| overflow-x: hidden; | |
| position: relative; | |
| } | |
| /* Animated background */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: | |
| radial-gradient(circle at 20% 50%, rgba(255, 0, 128, 0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 80%, rgba(0, 255, 255, 0.1) 0%, transparent 50%); | |
| animation: pulse 10s infinite alternate; | |
| pointer-events: none; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 0.5; } | |
| 100% { opacity: 1; } | |
| } | |
| .container { | |
| max-width: 500px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| /* Header */ | |
| .header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| position: relative; | |
| } | |
| .logo { | |
| font-size: 2.5rem; | |
| background: linear-gradient(45deg, #ff00ff, #00ffff, #ffff00); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-shadow: 0 0 30px rgba(255, 0, 255, 0.5); | |
| letter-spacing: -2px; | |
| animation: glow 2s ease-in-out infinite alternate; | |
| } | |
| @keyframes glow { | |
| 0% { filter: brightness(1) drop-shadow(0 0 20px rgba(255, 0, 255, 0.5)); } | |
| 100% { filter: brightness(1.2) drop-shadow(0 0 40px rgba(0, 255, 255, 0.8)); } | |
| } | |
| .tagline { | |
| font-size: 0.8rem; | |
| color: #ff00ff; | |
| margin-top: 5px; | |
| letter-spacing: 3px; | |
| text-transform: uppercase; | |
| } | |
| /* Main controls */ | |
| .main-controls { | |
| background: rgba(20, 20, 40, 0.9); | |
| border-radius: 20px; | |
| padding: 30px; | |
| backdrop-filter: blur(10px); | |
| border: 2px solid rgba(255, 0, 255, 0.3); | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); | |
| margin-bottom: 20px; | |
| } | |
| /* Record button */ | |
| .record-btn { | |
| width: 120px; | |
| height: 120px; | |
| margin: 0 auto 30px; | |
| display: block; | |
| border-radius: 50%; | |
| border: 4px solid #ff00ff; | |
| background: radial-gradient(circle, #ff0080, #8000ff); | |
| cursor: pointer; | |
| position: relative; | |
| transition: all 0.3s; | |
| box-shadow: 0 0 30px rgba(255, 0, 255, 0.5); | |
| } | |
| .record-btn:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 0 50px rgba(255, 0, 255, 0.8); | |
| } | |
| .record-btn.recording { | |
| animation: recordPulse 1s infinite; | |
| background: radial-gradient(circle, #ff0000, #ff0080); | |
| border-color: #ff0000; | |
| } | |
| @keyframes recordPulse { | |
| 0% { transform: scale(1); box-shadow: 0 0 30px rgba(255, 0, 0, 0.5); } | |
| 50% { transform: scale(1.1); box-shadow: 0 0 60px rgba(255, 0, 0, 0.8); } | |
| 100% { transform: scale(1); box-shadow: 0 0 30px rgba(255, 0, 0, 0.5); } | |
| } | |
| .record-icon { | |
| width: 50px; | |
| height: 50px; | |
| background: #fff; | |
| border-radius: 50%; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| transition: all 0.3s; | |
| } | |
| .record-btn.recording .record-icon { | |
| border-radius: 10px; | |
| width: 40px; | |
| height: 40px; | |
| } | |
| .record-label { | |
| text-align: center; | |
| font-size: 0.9rem; | |
| color: #00ffff; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| } | |
| /* Effects controls */ | |
| .effects-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 15px; | |
| margin: 30px 0; | |
| } | |
| .effect-btn { | |
| padding: 15px; | |
| background: linear-gradient(135deg, rgba(255, 0, 255, 0.2), rgba(0, 255, 255, 0.2)); | |
| border: 2px solid rgba(255, 0, 255, 0.5); | |
| border-radius: 10px; | |
| color: #fff; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| text-align: center; | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .effect-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 20px rgba(255, 0, 255, 0.5); | |
| border-color: #00ffff; | |
| } | |
| .effect-btn.active { | |
| background: linear-gradient(135deg, rgba(255, 0, 255, 0.5), rgba(0, 255, 255, 0.5)); | |
| border-color: #00ffff; | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); | |
| } | |
| /* Privacy toggle */ | |
| .privacy-toggle { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 15px; | |
| margin: 20px 0; | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| width: 60px; | |
| height: 30px; | |
| background: rgba(255, 0, 255, 0.3); | |
| border-radius: 15px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .toggle-switch.active { | |
| background: linear-gradient(90deg, #ff00ff, #00ffff); | |
| } | |
| .toggle-slider { | |
| position: absolute; | |
| top: 3px; | |
| left: 3px; | |
| width: 24px; | |
| height: 24px; | |
| background: #fff; | |
| border-radius: 50%; | |
| transition: all 0.3s; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); | |
| } | |
| .toggle-switch.active .toggle-slider { | |
| left: 33px; | |
| } | |
| .privacy-label { | |
| color: #00ffff; | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| /* Recordings list */ | |
| .recordings-section { | |
| margin-top: 30px; | |
| } | |
| .section-title { | |
| font-size: 1.2rem; | |
| color: #ff00ff; | |
| margin-bottom: 15px; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| text-align: center; | |
| } | |
| .recording-item { | |
| background: rgba(20, 20, 40, 0.8); | |
| border: 1px solid rgba(255, 0, 255, 0.3); | |
| border-radius: 10px; | |
| padding: 15px; | |
| margin-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| transition: all 0.3s; | |
| } | |
| .recording-item:hover { | |
| border-color: #00ffff; | |
| box-shadow: 0 5px 20px rgba(0, 255, 255, 0.3); | |
| } | |
| .play-btn { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #ff00ff, #00ffff); | |
| border: none; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.3s; | |
| flex-shrink: 0; | |
| } | |
| .play-btn:hover { | |
| transform: scale(1.1); | |
| box-shadow: 0 0 20px rgba(255, 0, 255, 0.5); | |
| } | |
| .play-icon { | |
| width: 0; | |
| height: 0; | |
| border-left: 12px solid #fff; | |
| border-top: 8px solid transparent; | |
| border-bottom: 8px solid transparent; | |
| margin-left: 3px; | |
| } | |
| .pause-icon { | |
| width: 12px; | |
| height: 16px; | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .pause-icon::before, | |
| .pause-icon::after { | |
| content: ''; | |
| width: 4px; | |
| height: 100%; | |
| background: #fff; | |
| } | |
| .recording-info { | |
| flex: 1; | |
| } | |
| .recording-title { | |
| font-size: 0.9rem; | |
| color: #fff; | |
| margin-bottom: 5px; | |
| } | |
| .recording-meta { | |
| font-size: 0.75rem; | |
| color: #888; | |
| } | |
| .delete-btn { | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 50%; | |
| background: rgba(255, 0, 0, 0.3); | |
| border: 1px solid rgba(255, 0, 0, 0.5); | |
| color: #ff6666; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| font-size: 1.2rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .delete-btn:hover { | |
| background: rgba(255, 0, 0, 0.5); | |
| transform: scale(1.1); | |
| } | |
| /* Tab buttons */ | |
| .tabs { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| justify-content: center; | |
| } | |
| .tab-btn { | |
| padding: 10px 20px; | |
| background: rgba(255, 0, 255, 0.2); | |
| border: 2px solid rgba(255, 0, 255, 0.5); | |
| border-radius: 20px; | |
| color: #fff; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| text-transform: uppercase; | |
| font-size: 0.9rem; | |
| letter-spacing: 1px; | |
| } | |
| .tab-btn.active { | |
| background: linear-gradient(135deg, #ff00ff, #00ffff); | |
| border-color: #00ffff; | |
| box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); | |
| } | |
| /* Loading spinner */ | |
| .processing { | |
| display: none; | |
| text-align: center; | |
| padding: 20px; | |
| color: #00ffff; | |
| font-size: 1.1rem; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| } | |
| .processing.active { | |
| display: block; | |
| } | |
| .spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 3px solid rgba(255, 0, 255, 0.3); | |
| border-top-color: #ff00ff; | |
| border-radius: 50%; | |
| margin: 20px auto; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* Empty state */ | |
| .empty-state { | |
| text-align: center; | |
| padding: 40px; | |
| color: #666; | |
| font-size: 0.9rem; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 480px) { | |
| .logo { | |
| font-size: 2rem; | |
| } | |
| .record-btn { | |
| width: 100px; | |
| height: 100px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header class="header"> | |
| <h1 class="logo">MEGA VOICE ULTRA™</h1> | |
| <p class="tagline">Transform Your Voice Into PURE EPICNESS</p> | |
| </header> | |
| <div class="main-controls"> | |
| <button class="record-btn" id="recordBtn"> | |
| <div class="record-icon"></div> | |
| </button> | |
| <p class="record-label" id="recordLabel">TAP TO RECORD</p> | |
| <div class="processing" id="processing"> | |
| <div class="spinner"></div> | |
| TRANSFORMING TO EPIC... | |
| </div> | |
| <div class="effects-grid"> | |
| <button class="effect-btn active" data-effect="stadium">🏟️ STADIUM</button> | |
| <button class="effect-btn" data-effect="radio">📻 RADIO DJ</button> | |
| <button class="effect-btn" data-effect="echo">🔊 MEGA ECHO</button> | |
| <button class="effect-btn" data-effect="robot">🤖 ROBO VOICE</button> | |
| </div> | |
| <div class="privacy-toggle"> | |
| <span class="privacy-label">PRIVATE</span> | |
| <div class="toggle-switch" id="privacyToggle"> | |
| <div class="toggle-slider"></div> | |
| </div> | |
| <span class="privacy-label">PUBLIC</span> | |
| </div> | |
| </div> | |
| <div class="recordings-section"> | |
| <div class="tabs"> | |
| <button class="tab-btn active" data-tab="my">MY RECORDINGS</button> | |
| <button class="tab-btn" data-tab="public">PUBLIC FEED</button> | |
| </div> | |
| <div id="recordingsList"> | |
| <div class="empty-state">No recordings yet. Hit that record button!</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize audio context | |
| let audioContext; | |
| let mediaRecorder; | |
| let recordedChunks = []; | |
| let isRecording = false; | |
| let currentEffect = 'stadium'; | |
| let isPublic = false; | |
| let currentTab = 'my'; | |
| let recordings = []; | |
| let publicRecordings = []; | |
| let currentlyPlaying = null; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadRecordings(); | |
| setupEventListeners(); | |
| }); | |
| function setupEventListeners() { | |
| // Record button | |
| document.getElementById('recordBtn').addEventListener('click', toggleRecording); | |
| // Effect buttons | |
| document.querySelectorAll('.effect-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| document.querySelectorAll('.effect-btn').forEach(b => b.classList.remove('active')); | |
| e.target.classList.add('active'); | |
| currentEffect = e.target.dataset.effect; | |
| }); | |
| }); | |
| // Privacy toggle | |
| document.getElementById('privacyToggle').addEventListener('click', () => { | |
| const toggle = document.getElementById('privacyToggle'); | |
| toggle.classList.toggle('active'); | |
| isPublic = toggle.classList.contains('active'); | |
| }); | |
| // Tab buttons | |
| document.querySelectorAll('.tab-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); | |
| e.target.classList.add('active'); | |
| currentTab = e.target.dataset.tab; | |
| displayRecordings(); | |
| }); | |
| }); | |
| } | |
| async function toggleRecording() { | |
| if (!isRecording) { | |
| await startRecording(); | |
| } else { | |
| stopRecording(); | |
| } | |
| } | |
| async function startRecording() { | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| // Initialize audio context if needed | |
| if (!audioContext) { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| mediaRecorder = new MediaRecorder(stream); | |
| recordedChunks = []; | |
| mediaRecorder.ondataavailable = (event) => { | |
| if (event.data.size > 0) { | |
| recordedChunks.push(event.data); | |
| } | |
| }; | |
| mediaRecorder.onstop = async () => { | |
| const blob = new Blob(recordedChunks, { type: 'audio/webm' }); | |
| await processAudio(blob); | |
| }; | |
| mediaRecorder.start(); | |
| isRecording = true; | |
| // Update UI | |
| document.getElementById('recordBtn').classList.add('recording'); | |
| document.getElementById('recordLabel').textContent = 'RECORDING...'; | |
| } catch (err) { | |
| console.error('Error accessing microphone:', err); | |
| alert('Please allow microphone access to use MEGA VOICE ULTRA!'); | |
| } | |
| } | |
| function stopRecording() { | |
| if (mediaRecorder && mediaRecorder.state !== 'inactive') { | |
| mediaRecorder.stop(); | |
| mediaRecorder.stream.getTracks().forEach(track => track.stop()); | |
| isRecording = false; | |
| // Update UI | |
| document.getElementById('recordBtn').classList.remove('recording'); | |
| document.getElementById('recordLabel').textContent = 'TAP TO RECORD'; | |
| } | |
| } | |
| async function processAudio(blob) { | |
| // Show processing UI | |
| document.getElementById('processing').classList.add('active'); | |
| // Convert blob to audio buffer | |
| const arrayBuffer = await blob.arrayBuffer(); | |
| const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); | |
| // Apply effects | |
| const processedBuffer = await applyEffects(audioBuffer); | |
| // Convert back to blob | |
| const processedBlob = await audioBufferToBlob(processedBuffer); | |
| // Create recording object | |
| const recording = { | |
| id: Date.now(), | |
| timestamp: new Date().toISOString(), | |
| effect: currentEffect, | |
| isPublic: isPublic, | |
| audioUrl: URL.createObjectURL(processedBlob), | |
| audioBlob: processedBlob | |
| }; | |
| // Save recording | |
| recordings.unshift(recording); | |
| saveRecordings(); | |
| // If public, add to public feed (simulated) | |
| if (isPublic) { | |
| publicRecordings.unshift({ | |
| ...recording, | |
| username: 'Anonymous User' | |
| }); | |
| } | |
| // Hide processing UI | |
| document.getElementById('processing').classList.remove('active'); | |
| // Update display | |
| displayRecordings(); | |
| } | |
| async function applyEffects(audioBuffer) { | |
| const offlineContext = new OfflineAudioContext( | |
| audioBuffer.numberOfChannels, | |
| audioBuffer.length, | |
| audioBuffer.sampleRate | |
| ); | |
| const source = offlineContext.createBufferSource(); | |
| source.buffer = audioBuffer; | |
| let lastNode = source; | |
| // Apply effects based on selection | |
| switch (currentEffect) { | |
| case 'stadium': | |
| // Stadium effect: heavy reverb + delay | |
| const convolver = offlineContext.createConvolver(); | |
| const reverbBuffer = createReverbImpulse(offlineContext, 4, 2); | |
| convolver.buffer = reverbBuffer; | |
| const delay = offlineContext.createDelay(5); | |
| delay.delayTime.value = 0.5; | |
| const feedback = offlineContext.createGain(); | |
| feedback.gain.value = 0.6; | |
| lastNode.connect(convolver); | |
| lastNode.connect(delay); | |
| delay.connect(feedback); | |
| feedback.connect(delay); | |
| const mixer = offlineContext.createGain(); | |
| convolver.connect(mixer); | |
| delay.connect(mixer); | |
| lastNode = mixer; | |
| break; | |
| case 'radio': | |
| // Radio DJ effect: compression + EQ + slight reverb | |
| const compressor = offlineContext.createDynamicsCompressor(); | |
| compressor.threshold.value = -20; | |
| compressor.ratio.value = 12; | |
| const lowShelf = offlineContext.createBiquadFilter(); | |
| lowShelf.type = 'lowshelf'; | |
| lowShelf.frequency.value = 320; | |
| lowShelf.gain.value = 6; | |
| const highShelf = offlineContext.createBiquadFilter(); | |
| highShelf.type = 'highshelf'; | |
| highShelf.frequency.value = 3200; | |
| highShelf.gain.value = 8; | |
| lastNode.connect(compressor); | |
| compressor.connect(lowShelf); | |
| lowShelf.connect(highShelf); | |
| lastNode = highShelf; | |
| break; | |
| case 'echo': | |
| // Mega echo effect: multiple delays | |
| const delays = []; | |
| const gains = []; | |
| for (let i = 0; i < 5; i++) { | |
| delays[i] = offlineContext.createDelay(5); | |
| delays[i].delayTime.value = (i + 1) * 0.3; | |
| gains[i] = offlineContext.createGain(); | |
| gains[i].gain.value = Math.pow(0.7, i + 1); | |
| lastNode.connect(delays[i]); | |
| delays[i].connect(gains[i]); | |
| } | |
| const echoMixer = offlineContext.createGain(); | |
| lastNode.connect(echoMixer); | |
| gains.forEach(gain => gain.connect(echoMixer)); | |
| lastNode = echoMixer; | |
| break; | |
| case 'robot': | |
| // Robot voice: pitch shift + distortion | |
| const oscillator = offlineContext.createOscillator(); | |
| oscillator.frequency.value = 50; | |
| oscillator.type = 'sawtooth'; | |
| const oscillatorGain = offlineContext.createGain(); | |
| oscillatorGain.gain.value = 0.005; | |
| const distortion = offlineContext.createWaveShaper(); | |
| distortion.curve = makeDistortionCurve(400); | |
| distortion.oversample = '4x'; | |
| oscillator.connect(oscillatorGain); | |
| oscillatorGain.connect(distortion.gain); | |
| lastNode.connect(distortion); | |
| oscillator.start(); | |
| lastNode = distortion; | |
| break; | |
| } | |
| // Connect to destination | |
| lastNode.connect(offlineContext.destination); | |
| source.start(); | |
| // Render | |
| return await offlineContext.startRendering(); | |
| } | |
| function createReverbImpulse(context, duration, decay) { | |
| const length = context.sampleRate * duration; | |
| const impulse = context.createBuffer(2, length, context.sampleRate); | |
| for (let channel = 0; channel < 2; channel++) { | |
| const channelData = impulse.getChannelData(channel); | |
| for (let i = 0; i < length; i++) { | |
| channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay); | |
| } | |
| } | |
| return impulse; | |
| } | |
| function makeDistortionCurve(amount) { | |
| const samples = 44100; | |
| const curve = new Float32Array(samples); | |
| const deg = Math.PI / 180; | |
| for (let i = 0; i < samples; i++) { | |
| const x = (i * 2) / samples - 1; | |
| curve[i] = ((3 + amount) * x * 20 * deg) / (Math.PI + amount * Math.abs(x)); | |
| } | |
| return curve; | |
| } | |
| async function audioBufferToBlob(audioBuffer) { | |
| // Create a new offline context to export | |
| const offlineContext = new OfflineAudioContext( | |
| audioBuffer.numberOfChannels, | |
| audioBuffer.length, | |
| audioBuffer.sampleRate | |
| ); | |
| const source = offlineContext.createBufferSource(); | |
| source.buffer = audioBuffer; | |
| source.connect(offlineContext.destination); | |
| source.start(); | |
| const renderedBuffer = await offlineContext.startRendering(); | |
| // Convert to WAV (simplified version) | |
| const interleaved = interleave(renderedBuffer); | |
| const dataView = encodeWAV(interleaved, renderedBuffer.sampleRate); | |
| const blob = new Blob([dataView], { type: 'audio/wav' }); | |
| return blob; | |
| } | |
| function interleave(audioBuffer) { | |
| const length = audioBuffer.length * audioBuffer.numberOfChannels; | |
| const result = new Float32Array(length); | |
| let index = 0; | |
| for (let i = 0; i < audioBuffer.length; i++) { | |
| for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) { | |
| result[index++] = audioBuffer.getChannelData(channel)[i]; | |
| } | |
| } | |
| return result; | |
| } | |
| function encodeWAV(samples, sampleRate) { | |
| const buffer = new ArrayBuffer(44 + samples.length * 2); | |
| const view = new DataView(buffer); | |
| // WAV header | |
| const writeString = (offset, string) => { | |
| for (let i = 0; i < string.length; i++) { | |
| view.setUint8(offset + i, string.charCodeAt(i)); | |
| } | |
| }; | |
| writeString(0, 'RIFF'); | |
| view.setUint32(4, 36 + samples.length * 2, true); | |
| writeString(8, 'WAVE'); | |
| writeString(12, 'fmt '); | |
| view.setUint32(16, 16, true); | |
| view.setUint16(20, 1, true); | |
| view.setUint16(22, 2, true); | |
| view.setUint32(24, sampleRate, true); | |
| view.setUint32(28, sampleRate * 4, true); | |
| view.setUint16(32, 4, true); | |
| view.setUint16(34, 16, true); | |
| writeString(36, 'data'); | |
| view.setUint32(40, samples.length * 2, true); | |
| // Convert float samples to 16-bit PCM | |
| let offset = 44; | |
| for (let i = 0; i < samples.length; i++) { | |
| const sample = Math.max(-1, Math.min(1, samples[i])); | |
| view.setInt16(offset, sample * 0x7FFF, true); | |
| offset += 2; | |
| } | |
| return view; | |
| } | |
| function saveRecordings() { | |
| // In a real app, we'd use IndexedDB for audio blobs | |
| // For this demo, we'll store metadata only | |
| const recordingsData = recordings.map(r => ({ | |
| id: r.id, | |
| timestamp: r.timestamp, | |
| effect: r.effect, | |
| isPublic: r.isPublic | |
| })); | |
| // Store metadata in memory (localStorage can't handle audio blobs) | |
| // In production, use IndexedDB or a backend service | |
| } | |
| function loadRecordings() { | |
| // Load from storage (simplified for demo) | |
| displayRecordings(); | |
| } | |
| function displayRecordings() { | |
| const container = document.getElementById('recordingsList'); | |
| const recordingsToShow = currentTab === 'my' ? recordings : publicRecordings; | |
| if (recordingsToShow.length === 0) { | |
| container.innerHTML = '<div class="empty-state">No recordings yet. Hit that record button!</div>'; | |
| return; | |
| } | |
| container.innerHTML = recordingsToShow.map(recording => ` | |
| <div class="recording-item" data-id="${recording.id}"> | |
| <button class="play-btn" onclick="playRecording(${recording.id})"> | |
| <div class="play-icon"></div> | |
| </button> | |
| <div class="recording-info"> | |
| <div class="recording-title"> | |
| ${recording.effect.toUpperCase()} VOICE | |
| ${recording.isPublic ? '🌍' : '🔒'} | |
| </div> | |
| <div class="recording-meta"> | |
| ${new Date(recording.timestamp).toLocaleString()} | |
| ${recording.username ? `• ${recording.username}` : ''} | |
| </div> | |
| </div> | |
| ${currentTab === 'my' ? ` | |
| <button class="delete-btn" onclick="deleteRecording(${recording.id})">×</button> | |
| ` : ''} | |
| </div> | |
| `).join(''); | |
| } | |
| function playRecording(id) { | |
| const recording = [...recordings, ...publicRecordings].find(r => r.id === id); | |
| if (!recording) return; | |
| if (currentlyPlaying) { | |
| currentlyPlaying.pause(); | |
| currentlyPlaying = null; | |
| } | |
| const audio = new Audio(recording.audioUrl); | |
| audio.play(); | |
| currentlyPlaying = audio; | |
| // Update play button UI | |
| const playBtn = document.querySelector(`[data-id="${id}"] .play-btn`); | |
| playBtn.innerHTML = '<div class="pause-icon"></div>'; | |
| audio.onended = () => { | |
| playBtn.innerHTML = '<div class="play-icon"></div>'; | |
| currentlyPlaying = null; | |
| }; | |
| } | |
| function deleteRecording(id) { | |
| recordings = recordings.filter(r => r.id !== id); | |
| saveRecordings(); | |
| displayRecordings(); | |
| } | |
| // Add some demo public recordings | |
| setTimeout(() => { | |
| publicRecordings = [ | |
| { | |
| id: 1, | |
| timestamp: new Date(Date.now() - 3600000).toISOString(), | |
| effect: 'stadium', | |
| isPublic: true, | |
| username: 'MegaVoicer2000', | |
| audioUrl: '#' | |
| }, | |
| { | |
| id: 2, | |
| timestamp: new Date(Date.now() - 7200000).toISOString(), | |
| effect: 'robot', | |
| isPublic: true, | |
| username: 'EpicAnnouncer', | |
| audioUrl: '#' | |
| } | |
| ]; | |
| }, 100); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment