// 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();
}
});