Skip to content

Instantly share code, notes, and snippets.

@jamiew
Created September 8, 2025 03:22
Show Gist options
  • Select an option

  • Save jamiew/5758b1b2f01759e0c95cecc178a7fb80 to your computer and use it in GitHub Desktop.

Select an option

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
<!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