brain-tuner / index.html
Jorgy72's picture
Add 2 files
ca709b1 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spatial Binaural Beats Generator</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary-color: #4a6fa5;
--secondary-color: #166088;
--accent-color: #4fc3f7;
--dark-color: #0a2463;
--light-color: #e8f1f2;
--success-color: #4caf50;
--warning-color: #ff9800;
--danger-color: #f44336;
--pattern-color: #9c27b0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #0a2463, #166088);
color: var(--light-color);
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: rgba(10, 36, 99, 0.5);
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
background: linear-gradient(90deg, var(--accent-color), #ffffff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.8;
}
.app-container {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 20px;
flex: 1;
}
@media (max-width: 992px) {
.app-container {
grid-template-columns: 1fr;
}
}
.control-panel {
background: rgba(22, 96, 136, 0.5);
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.visualization {
background: rgba(22, 96, 136, 0.5);
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
}
.control-group {
margin-bottom: 25px;
}
.control-group h3 {
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.slider-container {
margin-bottom: 15px;
}
.slider-container label {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.9rem;
}
.slider-container input[type="range"] {
width: 100%;
-webkit-appearance: none;
height: 10px;
border-radius: 5px;
background: var(--light-color);
outline: none;
}
.slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--accent-color);
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.value-display {
display: inline-block;
min-width: 40px;
text-align: right;
}
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
background: var(--primary-color);
color: white;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background: var(--primary-color);
}
.btn-danger {
background: var(--danger-color);
}
.btn-success {
background: var(--success-color);
}
.btn-pattern {
background: var(--pattern-color);
}
.btn-controls {
display: flex;
gap: 15px;
}
canvas {
background: rgba(10, 36, 99, 0.3);
border-radius: 10px;
width: 100%;
flex: 1;
}
.position-control {
position: relative;
width: 300px;
height: 300px;
margin: 30px auto;
background: rgba(10, 36, 99, 0.3);
border-radius: 50%;
border: 2px solid var(--accent-color);
overflow: hidden;
}
.position-indicator {
position: absolute;
width: 20px;
height: 20px;
background: var(--accent-color);
border-radius: 50%;
transform: translate(-50%, -50%);
cursor: grab;
box-shadow: 0 0 10px var(--accent-color);
transition: left 0.1s linear, top 0.1s linear;
z-index: 10;
}
.position-indicator:active {
cursor: grabbing;
}
.position-trail {
position: absolute;
width: 4px;
height: 4px;
background: rgba(79, 195, 247, 0.5);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 5;
}
.presets {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 20px;
}
.pattern-controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 15px;
}
.preset-btn, .pattern-btn {
padding: 8px;
border-radius: 6px;
background: rgba(74, 111, 165, 0.5);
border: 1px solid var(--accent-color);
color: var(--light-color);
cursor: pointer;
transition: all 0.2s ease;
}
.pattern-btn {
border-color: var(--pattern-color);
}
.preset-btn:hover {
background: var(--primary-color);
}
.pattern-btn:hover {
background: var(--pattern-color);
}
.audio-visualizer {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.footer {
text-align: center;
margin-top: 30px;
padding: 20px;
font-size: 0.9rem;
opacity: 0.7;
}
.tooltip {
position: relative;
display: inline-block;
margin-left: 5px;
cursor: pointer;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 200px;
background-color: var(--dark-color);
color: var(--light-color);
text-align: center;
border-radius: 6px;
padding: 8px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
font-size: 0.8rem;
line-height: 1.4;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
/* Animation for the binaural effect */
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 5px var(--accent-color);
}
50% {
box-shadow: 0 0 20px var(--accent-color);
}
}
.binaural-active {
animation: pulse 2s infinite;
}
/* Warning message */
.warning {
background-color: var(--warning-color);
color: white;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.pattern-active {
position: relative;
overflow: hidden;
}
.pattern-active::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(156, 39, 176, 0.2);
pointer-events: none;
}
.pattern-speed-control {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.pattern-speed-control input[type="range"] {
flex: 1;
}
</style>
</head>
<body>
<header>
<h1>Spatial Binaural Beats Generator</h1>
<p class="subtitle">Experience binaural beats with 3D spatial audio positioning and geometric patterns</p>
</header>
<div id="audioWarning" class="warning" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
<span>Press the Start button and allow audio when prompted. Use headphones for best experience.</span>
</div>
<div class="app-container">
<div class="control-panel">
<div class="control-group">
<h3><i class="fas fa-sliders-h"></i> Base Frequency</h3>
<div class="slider-container">
<label for="baseFrequency">
<span>Frequency: <span id="baseFrequencyValue" class="value-display">200</span> Hz</span>
<span class="tooltip">
<i class="fas fa-info-circle"></i>
<span class="tooltip-text">The carrier frequency that will be played in both ears. The difference between ears creates the binaural beat effect.</span>
</span>
</label>
<input type="range" id="baseFrequency" min="100" max="800" value="200" step="1">
</div>
</div>
<div class="control-group">
<h3><i class="fas fa-brain"></i> Binaural Beat</h3>
<div class="slider-container">
<label for="beatFrequency">
<span>Beat Frequency: <span id="beatFrequencyValue" class="value-display">10</span> Hz</span>
<span class="tooltip">
<i class="fas fa-info-circle"></i>
<span class="tooltip-text">The frequency difference between left and right ears. This creates the binaural beat effect (0.1-30Hz is most effective).</span>
</span>
</label>
<input type="range" id="beatFrequency" min="0.1" max="30" value="10" step="0.1">
</div>
<div class="slider-container">
<label for="beatVolume">
<span>Beat Volume: <span id="beatVolumeValue" class="value-display">0.7</span></span>
</label>
<input type="range" id="beatVolume" min="0" max="1" value="0.7" step="0.01">
</div>
</div>
<div class="control-group">
<h3><i class="fas fa-volume-up"></i> Spatial Audio</h3>
<div class="position-control" id="positionControl">
<div class="position-indicator" id="positionIndicator"></div>
</div>
<div class="slider-container">
<label for="distance">
<span>Distance: <span id="distanceValue" class="value-display">1</span> m</span>
<span class="tooltip">
<i class="fas fa-info-circle"></i>
<span class="tooltip-text">Distance from the sound source. Closer sounds will be louder and have more high frequencies.</span>
</span>
</label>
<input type="range" id="distance" min="0.1" max="10" value="1" step="0.1">
</div>
<div class="slider-container">
<label for="spatialVolume">
<span>Spatial Volume: <span id="spatialVolumeValue" class="value-display">0.7</span></span>
</label>
<input type="range" id="spatialVolume" min="0" max="1" value="0.7" step="0.01">
</div>
</div>
<div class="control-group">
<h3><i class="fas fa-project-diagram"></i> Geometric Patterns</h3>
<div class="pattern-controls">
<button class="pattern-btn" id="patternCircle"><i class="fas fa-circle"></i> Circle</button>
<button class="pattern-btn" id="patternSpiral"><i class="fas fa-spinner"></i> Spiral</button>
<button class="pattern-btn" id="patternPyramid"><i class="fas fa-mountain"></i> Pyramid</button>
<button class="pattern-btn" id="patternFigure8"><i class="fas fa-infinity"></i> Figure 8</button>
<button class="pattern-btn" id="patternRandom"><i class="fas fa-random"></i> Random</button>
<button class="pattern-btn" id="patternOff"><i class="fas fa-power-off"></i> Off</button>
</div>
<div class="pattern-speed-control">
<label for="patternSpeed">Speed:</label>
<input type="range" id="patternSpeed" min="0.1" max="3" value="1" step="0.1">
<span id="patternSpeedValue" class="value-display">1.0x</span>
</div>
</div>
<div class="control-group">
<h3><i class="fas fa-prescription-bottle"></i> Presets</h3>
<div class="presets">
<button class="preset-btn" data-base="200" data-beat="10">Focus (10Hz)</button>
<button class="preset-btn" data-base="220" data-beat="15">Creativity (15Hz)</button>
<button class="preset-btn" data-base="180" data-beat="6">Relaxation (6Hz)</button>
<button class="preset-btn" data-base="150" data-beat="4">Meditation (4Hz)</button>
<button class="preset-btn" data-base="180" data-beat="2">Deep Sleep (2Hz)</button>
<button class="preset-btn" data-base="300" data-beat="40">Awakening (40Hz)</button>
</div>
</div>
<div class="btn-controls">
<button id="toggleBtn" class="btn btn-success">
<i class="fas fa-play"></i> Start
</button>
<button id="stopBtn" class="btn btn-danger">
<i class="fas fa-stop"></i> Stop
</button>
<button id="patternPlayBtn" class="btn btn-pattern" style="display: none;">
<i class="fas fa-play"></i> Play Pattern
</button>
</div>
</div>
<div class="visualization">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3><i class="fas fa-chart-line"></i> Audio Analysis</h3>
<div style="display: flex; gap: 10px;">
<button id="clearTrailsBtn" class="btn" style="padding: 5px 10px; font-size: 0.8rem;">
<i class="fas fa-eraser"></i> Clear Trails
</button>
</div>
</div>
<div class="audio-visualizer">
<canvas id="waveformCanvas"></canvas>
</div>
<div class="audio-visualizer">
<canvas id="frequencyCanvas"></canvas>
</div>
</div>
</div>
<div class="footer">
<p>Spatial Binaural Beats Generator | Use headphones for best experience</p>
</div>
<script>
// Audio context setup
let audioContext;
let leftOscillator, rightOscillator;
let leftGain, rightGain;
let pannerLeft, pannerRight;
let analyserLeft, analyserRight;
let dataArrayLeft, dataArrayRight;
let isPlaying = false;
let animationId;
let analyserInitialized = false;
let audioStarted = false;
let patternInterval;
let activePattern = null;
let trailPoints = [];
let trailCanvas, trailCtx;
// Pattern parameters
const patterns = {
none: { name: "None", func: null },
circle: {
name: "Circle",
func: (t, speed) => {
const radius = 0.9;
const x = radius * Math.cos(t * speed);
const y = radius * Math.sin(t * speed);
return { x, y, z: -1 };
}
},
spiral: {
name: "Spiral",
func: (t, speed) => {
const radius = 0.8 * (0.5 + 0.5 * Math.sin(t * speed * 0.3));
const x = radius * Math.cos(t * speed);
const y = radius * Math.sin(t * speed);
return { x, y, z: -1 };
}
},
pyramid: {
name: "Pyramid",
func: (t, speed) => {
const sides = 4;
const angle = ((Math.floor(t * speed / (Math.PI/2)) % sides) + (t * speed % (Math.PI/2))/(Math.PI/2)) * (Math.PI*2/sides);
const x = 0.9 * Math.cos(angle);
const y = 0.9 * Math.sin(angle);
return { x, y, z: -1 + Math.sin(t * speed * 0.5) * 0.5 };
}
},
figure8: {
name: "Figure 8",
func: (t, speed) => {
const x = 0.8 * Math.sin(t * speed * 0.5);
const y = 0.8 * Math.sin(t * speed);
return { x, y, z: -1 };
}
},
random: {
name: "Random",
func: (t, speed) => {
if (t % 1 < 0.02) { // Change direction every ~second
return {
x: (Math.random() - 0.5) * 1.6,
y: (Math.random() - 0.5) * 1.6,
z: -1 + Math.random() * 0.5
};
}
// Continue moving in the same direction
const lastPoint = trailPoints[trailPoints.length - 1] || { x: 0, y: 0, z: -1 };
return {
x: lastPoint.x,
y: lastPoint.y,
z: lastPoint.z
};
}
}
};
// DOM elements
const baseFrequencySlider = document.getElementById('baseFrequency');
const baseFrequencyValue = document.getElementById('baseFrequencyValue');
const beatFrequencySlider = document.getElementById('beatFrequency');
const beatFrequencyValue = document.getElementById('beatFrequencyValue');
const beatVolumeSlider = document.getElementById('beatVolume');
const beatVolumeValue = document.getElementById('beatVolumeValue');
const distanceSlider = document.getElementById('distance');
const distanceValue = document.getElementById('distanceValue');
const spatialVolumeSlider = document.getElementById('spatialVolume');
const spatialVolumeValue = document.getElementById('spatialVolumeValue');
const toggleBtn = document.getElementById('toggleBtn');
const stopBtn = document.getElementById('stopBtn');
const positionControl = document.getElementById('positionControl');
const positionIndicator = document.getElementById('positionIndicator');
const presetButtons = document.querySelectorAll('.preset-btn');
const waveformCanvas = document.getElementById('waveformCanvas');
const frequencyCanvas = document.getElementById('frequencyCanvas');
const waveformCtx = waveformCanvas.getContext('2d');
const frequencyCtx = frequencyCanvas.getContext('2d');
const audioWarning = document.getElementById('audioWarning');
const patternSpeedSlider = document.getElementById('patternSpeed');
const patternSpeedValue = document.getElementById('patternSpeedValue');
const clearTrailsBtn = document.getElementById('clearTrailsBtn');
const patternPlayBtn = document.getElementById('patternPlayBtn');
// Pattern buttons
const patternButtons = {
circle: document.getElementById('patternCircle'),
spiral: document.getElementById('patternSpiral'),
pyramid: document.getElementById('patternPyramid'),
figure8: document.getElementById('patternFigure8'),
random: document.getElementById('patternRandom'),
off: document.getElementById('patternOff')
};
// Create trail canvas overlay
function createTrailCanvas() {
trailCanvas = document.createElement('canvas');
trailCanvas.style.position = 'absolute';
trailCanvas.style.top = '0';
trailCanvas.style.left = '0';
trailCanvas.style.pointerEvents = 'none';
trailCanvas.width = positionControl.offsetWidth;
trailCanvas.height = positionControl.offsetHeight;
positionControl.appendChild(trailCanvas);
trailCtx = trailCanvas.getContext('2d');
}
// Clear trail points
function clearTrails() {
trailPoints = [];
if (trailCanvas && trailCtx) {
trailCtx.clearRect(0, 0, trailCanvas.width, trailCanvas.height);
}
}
// Show warning message
function showAudioWarning() {
audioWarning.style.display = 'flex';
setTimeout(() => {
audioWarning.style.opacity = '1';
}, 10);
}
// Initialize audio context on first user interaction
function initAudioContext() {
if (!audioContext) {
try {
// Create audio context
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Setup the audio nodes
setupAudioNodes();
// Some browsers require a resume after creation
if (audioContext.state === 'suspended') {
audioContext.resume().then(() => {
console.log('AudioContext resumed successfully');
}).catch(err => {
console.error('Error resuming AudioContext:', err);
showAudioWarning();
});
}
// Show warning if context is suspended after creation
if (audioContext.state === 'suspended') {
showAudioWarning();
}
} catch (error) {
console.error('Error initializing audio context:', error);
showAudioWarning();
}
}
}
// Setup audio nodes
function setupAudioNodes() {
try {
// Create oscillators
leftOscillator = audioContext.createOscillator();
rightOscillator = audioContext.createOscillator();
// Set oscillator types to sine waves for smooth binaural beats
leftOscillator.type = 'sine';
rightOscillator.type = 'sine';
// Create gain nodes
leftGain = audioContext.createGain();
rightGain = audioContext.createGain();
// Create panners for spatial audio
pannerLeft = audioContext.createPanner();
pannerRight = audioContext.createPanner();
// Create analysers for visualization
analyserLeft = audioContext.createAnalyser();
analyserRight = audioContext.createAnalyser();
analyserLeft.fftSize = 2048;
analyserRight.fftSize = 2048;
dataArrayLeft = new Uint8Array(analyserLeft.frequencyBinCount);
dataArrayRight = new Uint8Array(analyserRight.frequencyBinCount);
// Connect nodes
leftOscillator.connect(leftGain);
rightOscillator.connect(rightGain);
leftGain.connect(pannerLeft);
rightGain.connect(pannerRight);
pannerLeft.connect(analyserLeft);
pannerRight.connect(analyserRight);
analyserLeft.connect(audioContext.destination);
analyserRight.connect(audioContext.destination);
// Set initial values
updateAudioParameters();
// Start oscillators
leftOscillator.start();
rightOscillator.start();
analyserInitialized = true;
} catch (error) {
console.error('Error setting up audio nodes:', error);
showAudioWarning();
}
}
// Update audio parameters based on UI controls
function updateAudioParameters() {
if (!audioContext || !audioStarted) return;
try {
// Base frequency - slightly different for each ear to create binaural beats
const baseFrequency = parseFloat(baseFrequencySlider.value);
const beatFrequency = parseFloat(beatFrequencySlider.value);
leftOscillator.frequency.setValueAtTime(baseFrequency - (beatFrequency / 2), audioContext.currentTime);
rightOscillator.frequency.setValueAtTime(baseFrequency + (beatFrequency / 2), audioContext.currentTime);
// Beat volume
const beatVolume = parseFloat(beatVolumeSlider.value);
leftGain.gain.setValueAtTime(beatVolume, audioContext.currentTime);
rightGain.gain.setValueAtTime(beatVolume, audioContext.currentTime);
// Spatial audio
updateSpatialPosition();
// Spatial volume
const spatialVolume = parseFloat(spatialVolumeSlider.value);
// Panner nodes handle some volume based on position, but we can adjust overall level
pannerLeft.setDistanceModel('linear');
pannerLeft.refDistance = 1;
pannerLeft.maxDistance = 10;
pannerLeft.rolloffFactor = 1;
pannerRight.setDistanceModel('linear');
pannerRight.refDistance = 1;
pannerRight.maxDistance = 10;
pannerRight.rolloffFactor = 1;
} catch (error) {
console.error('Error updating audio parameters:', error);
}
}
// Update spatial positioning
function updateSpatialPosition() {
if (!audioContext || !audioStarted) return;
try {
const distance = parseFloat(distanceSlider.value);
const x = parseFloat(positionIndicator.dataset.x) || 0;
const y = parseFloat(positionIndicator.dataset.y) || 0;
const z = Math.min(0, -(distance * 0.5)); // Position slightly behind to simulate natural listening
// Add trail point (if not already tracking this position)
const lastPoint = trailPoints[trailPoints.length - 1];
if (!lastPoint || lastPoint.x !== x || lastPoint.y !== y || lastPoint.z !== z) {
trailPoints.push({ x, y, z, timestamp: Date.now() });
drawTrail();
}
// Set positions with slight separation for stereo effect
pannerLeft.positionX.setValueAtTime(x - 0.1, audioContext.currentTime);
pannerLeft.positionY.setValueAtTime(y, audioContext.currentTime);
pannerLeft.positionZ.setValueAtTime(z, audioContext.currentTime);
pannerRight.positionX.setValueAtTime(x + 0.1, audioContext.currentTime);
pannerRight.positionY.setValueAtTime(y, audioContext.currentTime);
pannerRight.positionZ.setValueAtTime(z, audioContext.currentTime);
// Adjust distance - this affects volume and high frequency attenuation
pannerLeft.refDistance = distance;
pannerRight.refDistance = distance;
} catch (error) {
console.error('Error updating spatial position:', error);
}
}
// Draw trail of movement
function drawTrail() {
if (!trailCanvas || !trailCtx) return;
// Clear and redraw all trail points
trailCtx.clearRect(0, 0, trailCanvas.width, trailCanvas.height);
const centerX = trailCanvas.width / 2;
const centerY = trailCanvas.height / 2;
const radius = Math.min(trailCanvas.width, trailCanvas.height) / 2;
trailCtx.strokeStyle = 'rgba(79, 195, 247, 0.3)';
trailCtx.lineWidth = 1;
trailCtx.beginPath();
for (let i = 0; i < trailPoints.length; i++) {
const point = trailPoints[i];
const px = centerX + point.x * radius;
const py = centerY + point.y * radius;
if (i === 0) {
trailCtx.moveTo(px, py);
} else {
trailCtx.lineTo(px, py);
}
}
trailCtx.stroke();
}
// Visualization functions
function drawWaveform() {
if (!analyserInitialized || !isPlaying) return;
try {
analyserLeft.getByteTimeDomainData(dataArrayLeft);
analyserRight.getByteTimeDomainData(dataArrayRight);
waveformCanvas.width = waveformCanvas.clientWidth;
waveformCanvas.height = waveformCanvas.clientHeight;
const width = waveformCanvas.width;
const height = waveformCanvas.height;
waveformCtx.clearRect(0, 0, width, height);
// Draw left channel (top half)
waveformCtx.strokeStyle = '#4fc3f7';
waveformCtx.lineWidth = 2;
waveformCtx.beginPath();
const sliceWidthLeft = width / analyserLeft.frequencyBinCount;
let x = 0;
for (let i = 0; i < analyserLeft.frequencyBinCount; i++) {
const v = dataArrayLeft[i] / 128.0;
const y = v * (height / 4);
if (i === 0) {
waveformCtx.moveTo(x, y + (height / 4));
} else {
waveformCtx.lineTo(x, y + (height / 4));
}
x += sliceWidthLeft;
}
waveformCtx.stroke();
// Draw right channel (bottom half)
waveformCtx.strokeStyle = '#f44336';
waveformCtx.beginPath();
x = 0;
for (let i = 0; i < analyserRight.frequencyBinCount; i++) {
const v = dataArrayRight[i] / 128.0;
const y = v * (height / 4);
if (i === 0) {
waveformCtx.moveTo(x, y + (3 * height / 4));
} else {
waveformCtx.lineTo(x, y + (3 * height / 4));
}
x += sliceWidthLeft;
}
waveformCtx.stroke();
// Draw center line
waveformCtx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
waveformCtx.lineWidth = 1;
waveformCtx.beginPath();
waveformCtx.moveTo(0, height / 2);
waveformCtx.lineTo(width, height / 2);
waveformCtx.stroke();
// Add labels
waveformCtx.fillStyle = 'white';
waveformCtx.font = '12px Arial';
waveformCtx.fillText('Left Channel', 10, 20);
waveformCtx.fillText('Right Channel', 10, height - 10);
} catch (error) {
console.error('Error drawing waveform:', error);
}
}
function drawFrequency() {
if (!analyserInitialized || !isPlaying) return;
try {
analyserLeft.getByteFrequencyData(dataArrayLeft);
analyserRight.getByteFrequencyData(dataArrayRight);
frequencyCanvas.width = frequencyCanvas.clientWidth;
frequencyCanvas.height = frequencyCanvas.clientHeight;
const width = frequencyCanvas.width;
const height = frequencyCanvas.height;
frequencyCtx.clearRect(0, 0, width, height);
// Draw left channel (blue)
const barWidthLeft = width / analyserLeft.frequencyBinCount;
let x = 0;
for (let i = 0; i < analyserLeft.frequencyBinCount; i++) {
const v = dataArrayLeft[i] / 255;
const barHeight = v * height;
frequencyCtx.fillStyle = `rgba(79, 195, 247, ${v})`;
frequencyCtx.fillRect(x, height - barHeight, barWidthLeft, barHeight);
x += barWidthLeft;
}
// Draw right channel (red, semi-transparent over blue)
const barWidthRight = width / analyserRight.frequencyBinCount;
x = 0;
for (let i = 0; i < analyserRight.frequencyBinCount; i++) {
const v = dataArrayRight[i] / 255;
const barHeight = v * height;
frequencyCtx.fillStyle = `rgba(244, 67, 54, ${v})`;
frequencyCtx.fillRect(x, height - barHeight, barWidthRight, barHeight);
x += barWidthRight;
}
// Add frequency markers
frequencyCtx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
frequencyCtx.lineWidth = 0.5;
frequencyCtx.font = '10px Arial';
frequencyCtx.fillStyle = 'white';
const freqLabels = [100, 200, 500, 1000, 2000, 4000, 8000];
const sampleRate = audioContext.sampleRate;
freqLabels.forEach(freq => {
const pos = (freq / (sampleRate / 2)) * width;
frequencyCtx.beginPath();
frequencyCtx.moveTo(pos, 0);
frequencyCtx.lineTo(pos, height);
frequencyCtx.stroke();
frequencyCtx.fillText(`${freq}Hz`, pos + 3, height - 5);
});
} catch (error) {
console.error('Error drawing frequency:', error);
}
}
function animate() {
drawWaveform();
drawFrequency();
animationId = requestAnimationFrame(animate);
}
// Start geometric pattern animation
function startPattern(patternName) {
stopPattern(); // Stop any existing pattern
if (!patternName || patternName === 'none') {
activePattern = null;
patternPlayBtn.style.display = 'none';
return;
}
activePattern = patternName;
const pattern = patterns[patternName];
let t = 0;
const speed = parseFloat(patternSpeedSlider.value);
patternPlayBtn.style.display = 'flex';
patternPlayBtn.innerHTML = `<i class="fas ${patternName === 'none' ? 'fa-play' : 'fa-pause'}"></i> ${pattern.name}`;
// Highlight active pattern button
Object.values(patternButtons).forEach(btn => btn.classList.remove('pattern-active'));
if (patternName !== 'none') {
patternButtons[patternName].classList.add('pattern-active');
}
patternInterval = setInterval(() => {
t += 0.05;
const pos = pattern.func(t, speed);
// Update position indicator data
positionIndicator.dataset.x = pos.x.toFixed(4);
positionIndicator.dataset.y = pos.y.toFixed(4);
// Update visual position
const controlRect = positionControl.getBoundingClientRect();
const centerX = controlRect.width / 2;
const centerY = controlRect.height / 2;
positionIndicator.style.left = `${50 + pos.x * 50}%`;
positionIndicator.style.top = `${50 + pos.y * 50}%`;
// Update audio
updateAudioParameters();
}, 50);
}
// Stop geometric pattern animation
function stopPattern() {
if (patternInterval) {
clearInterval(patternInterval);
patternInterval = null;
}
Object.values(patternButtons).forEach(btn => btn.classList.remove('pattern-active'));
}
// Toggle current pattern play/pause
function togglePattern() {
if (patternInterval) {
stopPattern();
patternPlayBtn.innerHTML = `<i class="fas fa-play"></i> Play ${patterns[activePattern].name}`;
} else if (activePattern) {
startPattern(activePattern);
patternPlayBtn.innerHTML = `<i class="fas fa-pause"></i> Pause ${patterns[activePattern].name}`;
}
}
// Position control interaction
function setupPositionControl() {
createTrailCanvas();
const controlRect = positionControl.getBoundingClientRect();
const centerX = controlRect.width / 2;
const centerY = controlRect.height / 2;
// Initialize position indicator in the center
positionIndicator.dataset.x = '0';
positionIndicator.dataset.y = '0';
positionIndicator.style.left = '50%';
positionIndicator.style.top = '50%';
// Set up drag interaction
let isDragging = false;
positionIndicator.addEventListener('mousedown', (e) => {
isDragging = true;
stopPattern(); // Stop any active pattern when user moves manually
activePattern = null;
patternPlayBtn.style.display = 'none';
e.stopPropagation();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const controlRect = positionControl.getBoundingClientRect();
const centerX = controlRect.left + controlRect.width / 2;
const centerY = controlRect.top + controlRect.height / 2;
// Calculate position relative to center
const x = (e.clientX - centerX) / (controlRect.width / 2);
const y = (e.clientY - centerY) / (controlRect.height / 2);
// Constrain to circular boundary
const radius = Math.sqrt(x * x + y * y);
if (radius > 1) {
const angle = Math.atan2(y, x);
positionIndicator.dataset.x = Math.cos(angle).toFixed(2);
positionIndicator.dataset.y = Math.sin(angle).toFixed(2);
} else {
positionIndicator.dataset.x = x.toFixed(2);
positionIndicator.dataset.y = y.toFixed(2);
}
// Update visual position
positionIndicator.style.left = `${50 + x * 50}%`;
positionIndicator.style.top = `${50 + y * 50}%`;
// Update audio
if (isPlaying && audioStarted) {
updateAudioParameters();
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
// Also allow clicking anywhere in the control
positionControl.addEventListener('click', (e) => {
stopPattern(); // Stop any active pattern when user moves manually
activePattern = null;
patternPlayBtn.style.display = 'none';
const controlRect = positionControl.getBoundingClientRect();
const centerX = controlRect.width / 2;
const centerY = controlRect.height / 2;
const x = (e.offsetX - centerX) / centerX;
const y = (e.offsetY - centerY) / centerY;
// Constrain to circular boundary
const radius = Math.sqrt(x * x + y * y);
if (radius > 1) {
const angle = Math.atan2(y, x);
positionIndicator.dataset.x = Math.cos(angle).toFixed(2);
positionIndicator.dataset.y = Math.sin(angle).toFixed(2);
positionIndicator.style.left = `${50 + Math.cos(angle) * 50}%`;
positionIndicator.style.top = `${50 + Math.sin(angle) * 50}%`;
} else {
positionIndicator.dataset.x = x.toFixed(2);
positionIndicator.dataset.y = y.toFixed(2);
positionIndicator.style.left = `${(x + 1) * 50}%`;
positionIndicator.style.top = `${(y + 1) * 50}%`;
}
// Update audio
if (isPlaying && audioStarted) {
updateAudioParameters();
}
});
}
// Event listeners for UI controls
baseFrequencySlider.addEventListener('input', function() {
baseFrequencyValue.textContent = this.value;
if (isPlaying && audioStarted) updateAudioParameters();
});
beatFrequencySlider.addEventListener('input', function() {
beatFrequencyValue.textContent = parseFloat(this.value).toFixed(1);
if (isPlaying && audioStarted) updateAudioParameters();
});
beatVolumeSlider.addEventListener('input', function() {
beatVolumeValue.textContent = parseFloat(this.value).toFixed(2);
if (isPlaying && audioStarted) updateAudioParameters();
});
distanceSlider.addEventListener('input', function() {
distanceValue.textContent = parseFloat(this.value).toFixed(1);
if (isPlaying && audioStarted) updateAudioParameters();
});
spatialVolumeSlider.addEventListener('input', function() {
spatialVolumeValue.textContent = parseFloat(this.value).toFixed(2);
if (isPlaying && audioStarted) updateAudioParameters();
});
// Pattern speed control
patternSpeedSlider.addEventListener('input', function() {
patternSpeedValue.textContent = parseFloat(this.value).toFixed(1) + 'x';
if (patternInterval && activePattern) {
startPattern(activePattern); // Restart with new speed
}
});
// Clear trails button
clearTrailsBtn.addEventListener('click', clearTrails);
// Pattern buttons
Object.entries(patternButtons).forEach(([patternName, button]) => {
button.addEventListener('click', () => {
if (patternName === 'off') {
stopPattern();
activePattern = null;
patternPlayBtn.style.display = 'none';
} else {
startPattern(patternName);
}
});
});
// Pattern play/pause button
patternPlayBtn.addEventListener('click', togglePattern);
// Toggle playback
toggleBtn.addEventListener('click', async function() {
try {
if (!audioStarted) {
// First click - initialize audio
initAudioContext();
// Wait for audio to be ready
await audioContext.resume();
audioStarted = true;
}
if (isPlaying) {
// Pause audio
await audioContext.suspend();
toggleBtn.innerHTML = '<i class="fas fa-play"></i> Start';
toggleBtn.classList.remove('binaural-active');
cancelAnimationFrame(animationId);
isPlaying = false;
} else {
// Start or resume audio
await audioContext.resume();
toggleBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
toggleBtn.classList.add('binaural-active');
animate();
isPlaying = true;
}
} catch (error) {
console.error('Error toggling playback:', error);
showAudioWarning();
}
});
// Stop button - completely stops and resets
stopBtn.addEventListener('click', function() {
if (audioContext) {
audioContext.suspend();
cancelAnimationFrame(animationId);
isPlaying = false;
audioStarted = false;
toggleBtn.innerHTML = '<i class="fas fa-play"></i> Start';
toggleBtn.classList.remove('binaural-active');
// Clear visualizations
waveformCtx.clearRect(0, 0, waveformCanvas.width, waveformCanvas.height);
frequencyCtx.clearRect(0, 0, frequencyCanvas.width, frequencyCanvas.height);
// Stop pattern
stopPattern();
activePattern = null;
patternPlayBtn.style.display = 'none';
// Show warning that audio needs to be started again
showAudioWarning();
}
});
// Preset buttons
presetButtons.forEach(btn => {
btn.addEventListener('click', function() {
const base = parseFloat(this.dataset.base);
const beat = parseFloat(this.dataset.beat);
baseFrequencySlider.value = base;
beatFrequencySlider.value = beat;
baseFrequencyValue.textContent = base;
beatFrequencyValue.textContent = beat.toFixed(1);
// Reset position to center
positionIndicator.dataset.x = '0';
positionIndicator.dataset.y = '0';
positionIndicator.style.left = '50%';
positionIndicator.style.top = '50%';
distanceSlider.value = '1';
distanceValue.textContent = '1';
if (isPlaying && audioStarted) {
updateAudioParameters();
}
});
});
// Initialize position control
setupPositionControl();
// Handle window resize
window.addEventListener('resize', function() {
if (trailCanvas) {
trailCanvas.width = positionControl.offsetWidth;
trailCanvas.height = positionControl.offsetHeight;
drawTrail();
}
if (isPlaying) {
drawWaveform();
drawFrequency();
}
});
// Initialize on any user interaction
document.addEventListener('click', function() {
initAudioContext();
}, { once: true });
// Add a help message for mobile users
if (/Mobi|Android/i.test(navigator.userAgent)) {
showAudioWarning();
}
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: absolute; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">This website has been generated by <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body>
</html>