// Audio Context and State let audioContext = null; let isPlaying = false; let currentMode = 'spatial'; // 'spatial' or 'monaural' let rampMode = 'none'; // 'none', 'up', 'down' let rampDuration = 60; let activeNoiseType = null; // Nodes let binauralNodes = { leftOsc: null, rightOsc: null, leftGain: null, rightGain: null, merger: null, masterGain: null, modulator: null // For monaural }; let noiseNodes = { leftBuffer: null, rightBuffer: null, leftSource: null, rightSource: null, leftGain: null, rightGain: null, leftPanner: null, rightPanner: null, masterGain: null, filter: null }; let visualizer = { analyser: null, canvas: null, ctx: null, animationId: null }; // Initialize Audio Context function initAudioContext() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); setupVisualizer(); } if (audioContext.state === 'suspended') { audioContext.resume(); } } // Setup Visualizer function setupVisualizer() { const canvas = document.getElementById('audioVisualizer'); const ctx = canvas.getContext('2d'); // Handle high DPI displays const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.scale(dpr, dpr); visualizer.canvas = canvas; visualizer.ctx = ctx; visualizer.analyser = audioContext.createAnalyser(); visualizer.analyser.fftSize = 2048; visualizer.analyser.smoothingTimeConstant = 0.8; drawVisualizer(); } function drawVisualizer() { if (!visualizer.ctx) return; const { ctx, canvas, analyser } = visualizer; const width = canvas.width / (window.devicePixelRatio || 1); const height = canvas.height / (window.devicePixelRatio || 1); ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; ctx.fillRect(0, 0, width, height); if (isPlaying && analyser) { const bufferLength = analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); analyser.getByteFrequencyData(dataArray); const barWidth = (width / bufferLength) * 2.5; let barHeight; let x = 0; for (let i = 0; i < bufferLength; i++) { barHeight = (dataArray[i] / 255) * height * 0.8; // Create gradient for bars const gradient = ctx.createLinearGradient(0, height, 0, height - barHeight); gradient.addColorStop(0, 'rgba(6, 182, 212, 0.8)'); // Cyan gradient.addColorStop(0.5, 'rgba(168, 85, 247, 0.8)'); // Purple gradient.addColorStop(1, 'rgba(236, 72, 153, 0.8)'); // Pink ctx.fillStyle = gradient; ctx.fillRect(x, height - barHeight, barWidth, barHeight); // Add glow effect ctx.shadowBlur = 10; ctx.shadowColor = 'rgba(236, 72, 153, 0.5)'; x += barWidth + 1; } ctx.shadowBlur = 0; } else { // Idle animation const time = Date.now() * 0.001; ctx.strokeStyle = 'rgba(6, 182, 212, 0.3)'; ctx.lineWidth = 2; ctx.beginPath(); for (let x = 0; x < width; x++) { const y = height / 2 + Math.sin(x * 0.01 + time) * 20 * Math.sin(time * 0.5); if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } visualizer.animationId = requestAnimationFrame(drawVisualizer); } // Binaural Beat Generation function createBinauralBeats() { const carrier = parseFloat(document.getElementById('carrierSlider').value); const beat = parseFloat(document.getElementById('beatSlider').value); // Cleanup existing stopBinaural(); // Create nodes binauralNodes.masterGain = audioContext.createGain(); binauralNodes.masterGain.connect(audioContext.destination); binauralNodes.masterGain.connect(visualizer.analyser); if (currentMode === 'spatial') { // Spatial/Binaural mode: Different frequencies per ear binauralNodes.leftOsc = audioContext.createOscillator(); binauralNodes.rightOsc = audioContext.createOscillator(); binauralNodes.leftGain = audioContext.createGain(); binauralNodes.rightGain = audioContext.createGain(); binauralNodes.merger = audioContext.createChannelMerger(2); // Left ear: carrier binauralNodes.leftOsc.frequency.value = carrier; binauralNodes.leftOsc.connect(binauralNodes.leftGain); binauralNodes.leftGain.connect(binauralNodes.merger, 0, 0); // Right ear: carrier + beat binauralNodes.rightOsc.frequency.value = carrier + beat; binauralNodes.rightOsc.connect(binauralNodes.rightGain); binauralNodes.rightGain.connect(binauralNodes.merger, 0, 1); binauralNodes.merger.connect(binauralNodes.masterGain); binauralNodes.leftOsc.start(); binauralNodes.rightOsc.start(); } else { // Monaural mode: Amplitude modulation at beat frequency binauralNodes.leftOsc = audioContext.createOscillator(); binauralNodes.modulator = audioContext.createOscillator(); binauralNodes.modulatorGain = audioContext.createGain(); // Carrier tone binauralNodes.leftOsc.frequency.value = carrier; // Modulator for amplitude binauralNodes.modulator.frequency.value = beat; binauralNodes.modulatorGain.gain.value = 0.5; // Modulation depth // Connect modulator to gain binauralNodes.modulator.connect(binauralNodes.modulatorGain.gain); // Create constant offset for gain const constantOffset = audioContext.createGain(); constantOffset.gain.value = 0.5; binauralNodes.leftOsc.connect(constantOffset); constantOffset.connect(binauralNodes.modulatorGain); // Connect to both channels equally (monaural) binauralNodes.merger = audioContext.createChannelMerger(2); binauralNodes.modulatorGain.connect(binauralNodes.merger, 0, 0); binauralNodes.modulatorGain.connect(binauralNodes.merger, 0, 1); binauralNodes.merger.connect(binauralNodes.masterGain); binauralNodes.leftOsc.start(); binauralNodes.modulator.start(); } // Apply ramping applyRamp(binauralNodes.masterGain.gain, 0.3); } function stopBinaural() { if (binauralNodes.leftOsc) { try { binauralNodes.leftOsc.stop(); } catch(e) {} binauralNodes.leftOsc = null; } if (binauralNodes.rightOsc) { try { binauralNodes.rightOsc.stop(); } catch(e) {} binauralNodes.rightOsc = null; } if (binauralNodes.modulator) { try { binauralNodes.modulator.stop(); } catch(e) {} binauralNodes.modulator = null; } if (binauralNodes.masterGain) { binauralNodes.masterGain.disconnect(); } } // Noise Generation (Always Spatial) function createNoise(type) { stopNoise(); activeNoiseType = type; const bufferSize = 2 * audioContext.sampleRate; // 2 seconds buffer const numChannels = 2; // Stereo const buffer = audioContext.createBuffer(numChannels, bufferSize, audioContext.sampleRate); // Generate noise data for (let channel = 0; channel < numChannels; channel++) { const data = buffer.getChannelData(channel); if (type === 'white') { // White noise: random -1 to 1 for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; } } else if (type === 'pink') { // Pink noise: 1/f using Paul Kellet's method let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0; for (let i = 0; i < bufferSize; i++) { const white = Math.random() * 2 - 1; b0 = 0.99886 * b0 + white * 0.0555179; b1 = 0.99332 * b1 + white * 0.0750759; b2 = 0.96900 * b2 + white * 0.1538520; b3 = 0.86650 * b3 + white * 0.3104856; b4 = 0.55000 * b4 + white * 0.5329522; b5 = -0.7616 * b5 - white * 0.0168980; data[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362; data[i] *= 0.11; // Normalize b6 = white * 0.115926; } } else if (type === 'brown') { // Brown noise: 1/f² (integration of white noise) let lastOut = 0; for (let i = 0; i < bufferSize; i++) { const white = Math.random() * 2 - 1; data[i] = (lastOut + (0.02 * white)) / 1.02; lastOut = data[i]; data[i] *= 3.5; // Normalize } } } // Create spatial setup noiseNodes.masterGain = audioContext.createGain(); noiseNodes.leftSource = audioContext.createBufferSource(); noiseNodes.rightSource = audioContext.createBufferSource(); noiseNodes.leftGain = audioContext.createGain(); noiseNodes.rightGain = audioContext.createGain(); noiseNodes.leftPanner = audioContext.createStereoPanner(); noiseNodes.rightPanner = audioContext.createStereoPanner(); // Set up looping noiseNodes.leftSource.buffer = buffer; noiseNodes.rightSource.buffer = buffer; noiseNodes.leftSource.loop = true; noiseNodes.rightSource.loop = true; // Get spatial width const spatialWidth = parseInt(document.getElementById('spatialWidthSlider').value) / 100; const panValue = spatialWidth; // 0 = center, 1 = full side // Left channel fully left noiseNodes.leftPanner.pan.value = -panValue; noiseNodes.leftSource.connect(noiseNodes.leftGain); noiseNodes.leftGain.connect(noiseNodes.leftPanner); noiseNodes.leftPanner.connect(noiseNodes.masterGain); // Right channel fully right (with slight offset for true spatial feel) noiseNodes.rightPanner.pan.value = panValue; noiseNodes.rightSource.connect(noiseNodes.rightGain); noiseNodes.rightGain.connect(noiseNodes.rightPanner); noiseNodes.rightPanner.connect(noiseNodes.masterGain); // Add slight delay to right channel for enhanced spatialization if (spatialWidth > 0.5) { const delay = audioContext.createDelay(); delay.delayTime.value = 0.01; // 10ms delay noiseNodes.rightPanner.disconnect(); noiseNodes.rightPanner.connect(delay); delay.connect(noiseNodes.masterGain); } noiseNodes.masterGain.connect(audioContext.destination); noiseNodes.masterGain.connect(visualizer.analyser); // Set volume const volume = parseInt(document.getElementById('noiseVolumeSlider').value) / 100; noiseNodes.masterGain.gain.value = volume; noiseNodes.leftSource.start(); noiseNodes.rightSource.start(); // Add slight variation between channels for true spatial effect // By starting at slightly different offsets noiseNodes.rightSource.playbackRate.value = 0.999; // Slight pitch difference creates beating updateNoiseButtonStates(); } function stopNoise() { if (noiseNodes.leftSource) { try { noiseNodes.leftSource.stop(); } catch(e) {} noiseNodes.leftSource = null; } if (noiseNodes.rightSource) { try { noiseNodes.rightSource.stop(); } catch(e) {} noiseNodes.rightSource = null; } if (noiseNodes.masterGain) { noiseNodes.masterGain.disconnect(); } activeNoiseType = null; updateNoiseButtonStates(); } function updateNoiseButtonStates() { const buttons = ['pinkNoiseBtn', 'brownNoiseBtn', 'whiteNoiseBtn']; const types = ['pink', 'brown', 'white']; buttons.forEach((id, index) => { const btn = document.getElementById(id); if (activeNoiseType === types[index]) { btn.classList.add('playing'); btn.querySelector('i[data-lucide="play-circle"]').setAttribute('data-lucide', 'stop-circle'); } else { btn.classList.remove('playing'); btn.querySelector('i[data-lucide="stop-circle"]')?.setAttribute('data-lucide', 'play-circle'); } }); lucide.createIcons(); } // Ramping Functionality function applyRamp(gainNode, targetValue) { const now = audioContext.currentTime; if (rampMode === 'none') { gainNode.setValueAtTime(targetValue, now); } else if (rampMode === 'up') { gainNode.setValueAtTime(0.001, now); gainNode.exponentialRampToValueAtTime(targetValue, now + rampDuration); } else if (rampMode === 'down') { gainNode.setValueAtTime(targetValue, now); gainNode.exponentialRampToValueAtTime(0.001, now + rampDuration); // Stop after ramp down setTimeout(() => { stopAll(); }, rampDuration * 1000); } } function stopAll() { stopBinaural(); stopNoise(); isPlaying = false; updatePlayButton(); } function updatePlayButton() { const btn = document.getElementById('playBinauralBtn'); const stopBtn = document.getElementById('stopAllBtn'); if (isPlaying) { btn.innerHTML = 'Stop Binaural'; btn.classList.remove('from-cyan-400', 'via-purple-500', 'to-pink-500'); btn.classList.add('from-red-400', 'via-red-500', 'to-rose-600'); stopBtn.disabled = false; stopBtn.classList.remove('opacity-50', 'cursor-not-allowed'); } else { btn.innerHTML = 'Generate Binaural Beat'; btn.classList.add('from-cyan-400', 'via-purple-500', 'to-pink-500'); btn.classList.remove('from-red-400', 'via-red-500', 'to-rose-600'); stopBtn.disabled = true; stopBtn.classList.add('opacity-50', 'cursor-not-allowed'); } lucide.createIcons(); } // Event Listeners document.addEventListener('DOMContentLoaded', () => { // Mode selection document.getElementById('spatialBtn').addEventListener('click', () => { currentMode = 'spatial'; document.getElementById('spatialBtn').classList.add('active', 'from-cyan-500', 'to-blue-500', 'text-white'); document.getElementById('spatialBtn').classList.remove('bg-white/10', 'text-purple-200'); document.getElementById('monauralBtn').classList.remove('active', 'from-purple-500', 'to-pink-500', 'text-white'); document.getElementById('monauralBtn').classList.add('bg-white/10', 'text-purple-200'); document.getElementById('modeDescription').textContent = 'Different frequencies in each ear create the beat perception'; }); document.getElementById('monauralBtn').addEventListener('click', () => { currentMode = 'monaural'; document.getElementById('monauralBtn').classList.add('active', 'from-purple-500', 'to-pink-500', 'text-white'); document.getElementById('monauralBtn').classList.remove('bg-white/10', 'text-purple-200'); document.getElementById('spatialBtn').classList.remove('active', 'from-cyan-500', 'to-blue-500', 'text-white'); document.getElementById('spatialBtn').classList.add('bg-white/10', 'text-purple-200'); document.getElementById('modeDescription').textContent = 'Same frequency in both ears with amplitude modulation'; }); // Ramp selection const rampButtons = { 'rampNone': 'none', 'rampUp': 'up', 'rampDown': 'down' }; Object.keys(rampButtons).forEach(id => { document.getElementById(id).addEventListener('click', () => { rampMode = rampButtons[id]; Object.keys(rampButtons).forEach(btnId => { const btn = document.getElementById(btnId); if (btnId === id) { btn.classList.add('active', 'from-purple-500', 'to-pink-500', 'text-white'); btn.classList.remove('bg-white/10', 'text-purple-200'); } else { btn.classList.remove('active', 'from-purple-500', 'to-pink-500', 'text-white'); btn.classList.add('bg-white/10', 'text-purple-200'); } }); }); }); // Sliders document.getElementById('carrierSlider').addEventListener('input', (e) => { document.getElementById('carrierValue').textContent = e.target.value + ' Hz'; if (isPlaying && currentMode === 'spatial') { const beat = parseFloat(document.getElementById('beatSlider').value); binauralNodes.leftOsc.frequency.setValueAtTime(parseFloat(e.target.value), audioContext.currentTime); binauralNodes.rightOsc.frequency.setValueAtTime(parseFloat(e.target.value) + beat, audioContext.currentTime); } else if (isPlaying) { binauralNodes.leftOsc.frequency.setValueAtTime(parseFloat(e.target.value), audioContext.currentTime); } }); document.getElementById('beatSlider').addEventListener('input', (e) => { document.getElementById('beatValue').textContent = e.target.value + ' Hz'; if (isPlaying) { const carrier = parseFloat(document.getElementById('carrierSlider').value); const beat = parseFloat(e.target.value); if (currentMode === 'spatial') { binauralNodes.rightOsc.frequency.setValueAtTime(carrier + beat, audioContext.currentTime); } else { binauralNodes.modulator.frequency.setValueAtTime(beat, audioContext.currentTime); } } }); document.getElementById('rampDuration').addEventListener('input', (e) => { rampDuration = parseInt(e.target.value); document.getElementById('rampDurationValue').textContent = rampDuration + 's'; }); document.getElementById('noiseVolumeSlider').addEventListener('input', (e) => { const value = e.target.value; document.getElementById('noiseVolumeValue').textContent = value + '%'; if (noiseNodes.masterGain) { noiseNodes.masterGain.gain.setValueAtTime(value / 100, audioContext.currentTime); } }); document.getElementById('spatialWidthSlider').addEventListener('input', (e) => { const value = parseInt(e.target.value); const label = value < 30 ? 'Narrow' : value < 70 ? 'Medium' : 'Wide'; document.getElementById('spatialWidthValue').textContent = label; if (activeNoiseType) { // Recreate noise with new spatial settings createNoise(activeNoiseType); } }); // Brainwave presets document.querySelectorAll('.brainwave-btn').forEach(btn => { btn.addEventListener('click', () => { const carrier = btn.dataset.carrier; const beat = btn.dataset.beat; document.getElementById('carrierSlider').value = carrier; document.getElementById('beatSlider').value = beat; document.getElementById('carrierValue').textContent = carrier + ' Hz'; document.getElementById('beatValue').textContent = beat + ' Hz'; // Visual feedback btn.style.transform = 'scale(0.95)'; setTimeout(() => btn.style.transform = '', 100); }); }); // Play buttons document.getElementById('playBinauralBtn').addEventListener('click', () => { initAudioContext(); if (isPlaying) { stopBinaural(); isPlaying = false; } else { createBinauralBeats(); isPlaying = true; } updatePlayButton(); }); // Noise buttons (toggle behavior) document.getElementById('pinkNoiseBtn').addEventListener('click', () => { initAudioContext(); if (activeNoiseType === 'pink') { stopNoise(); } else { createNoise('pink'); } }); document.getElementById('brownNoiseBtn').addEventListener('click', () => { initAudioContext(); if (activeNoiseType === 'brown') { stopNoise(); } else { createNoise('brown'); } }); document.getElementById('whiteNoiseBtn').addEventListener('click', () => { initAudioContext(); if (activeNoiseType === 'white') { stopNoise(); } else { createNoise('white'); } }); document.getElementById('stopAllBtn').addEventListener('click', stopAll); // Particle background animation initParticles(); }); // Particle System function initParticles() { const canvas = document.getElementById('particleCanvas'); const ctx = canvas.getContext('2d'); function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } resize(); window.addEventListener('resize', resize); const particles = []; const particleCount = 50; for (let i = 0; i < particleCount; i++) { particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, radius: Math.random() * 3 + 1, vx: (Math.random() - 0.5) * 0.5, vy: (Math.random() - 0.5) * 0.5, color: `hsla(${Math.random() * 60 + 240}, 70%, 60%, ${Math.random() * 0.3})` }); } function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x > canvas.width) p.vx *= -1; if (p.y < 0 || p.y > canvas.height) p.vy *= -1; ctx.beginPath(); ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); ctx.fillStyle = p.color; ctx.fill(); }); // Connect nearby particles particles.forEach((p1, i) => { particles.slice(i + 1).forEach(p2 => { const dx = p1.x - p2.x; const dy = p1.y - p2.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 100) { ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.strokeStyle = `rgba(147, 51, 234, ${0.1 * (1 - distance / 100)})`; ctx.stroke(); } }); }); requestAnimationFrame(animate); } animate(); } // Cleanup on page unload window.addEventListener('beforeunload', () => { stopAll(); if (audioContext) { audioContext.close(); } });