| |
| let audioContext = null; |
| let isPlaying = false; |
| let currentMode = 'spatial'; |
| let rampMode = 'none'; |
| let rampDuration = 60; |
| let activeNoiseType = null; |
|
|
| |
| let binauralNodes = { |
| leftOsc: null, |
| rightOsc: null, |
| leftGain: null, |
| rightGain: null, |
| merger: null, |
| masterGain: null, |
| modulator: null |
| }; |
|
|
| 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 |
| }; |
|
|
| |
| function initAudioContext() { |
| if (!audioContext) { |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| setupVisualizer(); |
| } |
| if (audioContext.state === 'suspended') { |
| audioContext.resume(); |
| } |
| } |
|
|
| |
| function setupVisualizer() { |
| const canvas = document.getElementById('audioVisualizer'); |
| const ctx = canvas.getContext('2d'); |
| |
| |
| 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; |
| |
| |
| const gradient = ctx.createLinearGradient(0, height, 0, height - barHeight); |
| gradient.addColorStop(0, 'rgba(6, 182, 212, 0.8)'); |
| gradient.addColorStop(0.5, 'rgba(168, 85, 247, 0.8)'); |
| gradient.addColorStop(1, 'rgba(236, 72, 153, 0.8)'); |
| |
| ctx.fillStyle = gradient; |
| ctx.fillRect(x, height - barHeight, barWidth, barHeight); |
| |
| |
| ctx.shadowBlur = 10; |
| ctx.shadowColor = 'rgba(236, 72, 153, 0.5)'; |
| |
| x += barWidth + 1; |
| } |
| ctx.shadowBlur = 0; |
| } else { |
| |
| 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); |
| } |
|
|
| |
| function createBinauralBeats() { |
| const carrier = parseFloat(document.getElementById('carrierSlider').value); |
| const beat = parseFloat(document.getElementById('beatSlider').value); |
| |
| |
| stopBinaural(); |
| |
| |
| binauralNodes.masterGain = audioContext.createGain(); |
| binauralNodes.masterGain.connect(audioContext.destination); |
| binauralNodes.masterGain.connect(visualizer.analyser); |
| |
| if (currentMode === 'spatial') { |
| |
| binauralNodes.leftOsc = audioContext.createOscillator(); |
| binauralNodes.rightOsc = audioContext.createOscillator(); |
| binauralNodes.leftGain = audioContext.createGain(); |
| binauralNodes.rightGain = audioContext.createGain(); |
| binauralNodes.merger = audioContext.createChannelMerger(2); |
| |
| |
| binauralNodes.leftOsc.frequency.value = carrier; |
| binauralNodes.leftOsc.connect(binauralNodes.leftGain); |
| binauralNodes.leftGain.connect(binauralNodes.merger, 0, 0); |
| |
| |
| 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 { |
| |
| binauralNodes.leftOsc = audioContext.createOscillator(); |
| binauralNodes.modulator = audioContext.createOscillator(); |
| binauralNodes.modulatorGain = audioContext.createGain(); |
| |
| |
| binauralNodes.leftOsc.frequency.value = carrier; |
| |
| |
| binauralNodes.modulator.frequency.value = beat; |
| binauralNodes.modulatorGain.gain.value = 0.5; |
| |
| |
| binauralNodes.modulator.connect(binauralNodes.modulatorGain.gain); |
| |
| |
| const constantOffset = audioContext.createGain(); |
| constantOffset.gain.value = 0.5; |
| binauralNodes.leftOsc.connect(constantOffset); |
| constantOffset.connect(binauralNodes.modulatorGain); |
| |
| |
| 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(); |
| } |
| |
| |
| 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(); |
| } |
| } |
|
|
| |
| function createNoise(type) { |
| stopNoise(); |
| activeNoiseType = type; |
| |
| const bufferSize = 2 * audioContext.sampleRate; |
| const numChannels = 2; |
| const buffer = audioContext.createBuffer(numChannels, bufferSize, audioContext.sampleRate); |
| |
| |
| for (let channel = 0; channel < numChannels; channel++) { |
| const data = buffer.getChannelData(channel); |
| |
| if (type === 'white') { |
| |
| for (let i = 0; i < bufferSize; i++) { |
| data[i] = Math.random() * 2 - 1; |
| } |
| } else if (type === 'pink') { |
| |
| 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; |
| b6 = white * 0.115926; |
| } |
| } else if (type === 'brown') { |
| |
| 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; |
| } |
| } |
| } |
| |
| |
| 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(); |
| |
| |
| noiseNodes.leftSource.buffer = buffer; |
| noiseNodes.rightSource.buffer = buffer; |
| noiseNodes.leftSource.loop = true; |
| noiseNodes.rightSource.loop = true; |
| |
| |
| const spatialWidth = parseInt(document.getElementById('spatialWidthSlider').value) / 100; |
| const panValue = spatialWidth; |
| |
| |
| noiseNodes.leftPanner.pan.value = -panValue; |
| noiseNodes.leftSource.connect(noiseNodes.leftGain); |
| noiseNodes.leftGain.connect(noiseNodes.leftPanner); |
| noiseNodes.leftPanner.connect(noiseNodes.masterGain); |
| |
| |
| noiseNodes.rightPanner.pan.value = panValue; |
| noiseNodes.rightSource.connect(noiseNodes.rightGain); |
| noiseNodes.rightGain.connect(noiseNodes.rightPanner); |
| noiseNodes.rightPanner.connect(noiseNodes.masterGain); |
| |
| |
| if (spatialWidth > 0.5) { |
| const delay = audioContext.createDelay(); |
| delay.delayTime.value = 0.01; |
| noiseNodes.rightPanner.disconnect(); |
| noiseNodes.rightPanner.connect(delay); |
| delay.connect(noiseNodes.masterGain); |
| } |
| |
| noiseNodes.masterGain.connect(audioContext.destination); |
| noiseNodes.masterGain.connect(visualizer.analyser); |
| |
| |
| const volume = parseInt(document.getElementById('noiseVolumeSlider').value) / 100; |
| noiseNodes.masterGain.gain.value = volume; |
| |
| noiseNodes.leftSource.start(); |
| noiseNodes.rightSource.start(); |
| |
| |
| |
| noiseNodes.rightSource.playbackRate.value = 0.999; |
| |
| 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(); |
| } |
|
|
| |
| 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); |
| |
| 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 = '<i data-lucide="pause" class="w-6 h-6"></i><span>Stop Binaural</span>'; |
| 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 = '<i data-lucide="play" class="w-6 h-6"></i><span>Generate Binaural Beat</span>'; |
| 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(); |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| 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'; |
| }); |
| |
| |
| 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'); |
| } |
| }); |
| }); |
| }); |
| |
| |
| 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) { |
| |
| createNoise(activeNoiseType); |
| } |
| }); |
| |
| |
| 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'; |
| |
| |
| btn.style.transform = 'scale(0.95)'; |
| setTimeout(() => btn.style.transform = '', 100); |
| }); |
| }); |
| |
| |
| document.getElementById('playBinauralBtn').addEventListener('click', () => { |
| initAudioContext(); |
| if (isPlaying) { |
| stopBinaural(); |
| isPlaying = false; |
| } else { |
| createBinauralBeats(); |
| isPlaying = true; |
| } |
| updatePlayButton(); |
| }); |
| |
| |
| 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); |
| |
| |
| initParticles(); |
| }); |
|
|
| |
| 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(); |
| }); |
| |
| |
| 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(); |
| } |
|
|
| |
| window.addEventListener('beforeunload', () => { |
| stopAll(); |
| if (audioContext) { |
| audioContext.close(); |
| } |
| }); |