Spaces:
Running
Running
| <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> |