|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Multi-Device Audio Analysis System</title> |
|
<style> |
|
:root { |
|
--primary: #2196F3; |
|
--secondary: #4CAF50; |
|
--bg-dark: #1a1a1a; |
|
--bg-light: #2a2a2a; |
|
--text: #ffffff; |
|
} |
|
|
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
font-family: 'Segoe UI', system-ui, sans-serif; |
|
} |
|
|
|
body { |
|
background: var(--bg-dark); |
|
color: var(--text); |
|
min-height: 100vh; |
|
} |
|
|
|
.app-container { |
|
display: grid; |
|
grid-template-columns: 300px 1fr; |
|
gap: 20px; |
|
padding: 20px; |
|
height: 100vh; |
|
} |
|
|
|
.sidebar { |
|
background: var(--bg-light); |
|
border-radius: 12px; |
|
padding: 20px; |
|
display: flex; |
|
flex-direction: column; |
|
gap: 20px; |
|
} |
|
|
|
.main-content { |
|
display: grid; |
|
grid-template-rows: auto 1fr; |
|
gap: 20px; |
|
} |
|
|
|
.visualization-container { |
|
display: grid; |
|
grid-template-columns: 2fr 1fr; |
|
gap: 20px; |
|
} |
|
|
|
.canvas-container { |
|
background: var(--bg-light); |
|
border-radius: 12px; |
|
padding: 20px; |
|
position: relative; |
|
} |
|
|
|
canvas { |
|
width: 100%; |
|
height: 100%; |
|
background: rgba(0,0,0,0.2); |
|
border-radius: 8px; |
|
} |
|
|
|
.control-panel { |
|
background: var(--bg-light); |
|
border-radius: 12px; |
|
padding: 20px; |
|
} |
|
|
|
.btn { |
|
background: var(--primary); |
|
color: white; |
|
border: none; |
|
padding: 12px 24px; |
|
border-radius: 6px; |
|
cursor: pointer; |
|
transition: opacity 0.2s; |
|
width: 100%; |
|
margin-bottom: 10px; |
|
} |
|
|
|
.btn:hover { |
|
opacity: 0.9; |
|
} |
|
|
|
.btn.secondary { |
|
background: var(--secondary); |
|
} |
|
|
|
.input-group { |
|
margin-bottom: 15px; |
|
} |
|
|
|
.input-group label { |
|
display: block; |
|
margin-bottom: 5px; |
|
color: #888; |
|
} |
|
|
|
input[type="number"], |
|
select { |
|
width: 100%; |
|
padding: 8px; |
|
background: rgba(255,255,255,0.1); |
|
border: 1px solid rgba(255,255,255,0.2); |
|
border-radius: 4px; |
|
color: white; |
|
} |
|
|
|
.device-list { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 10px; |
|
} |
|
|
|
.device-item { |
|
background: rgba(255,255,255,0.1); |
|
padding: 10px; |
|
border-radius: 4px; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
} |
|
|
|
.status-indicator { |
|
width: 10px; |
|
height: 10px; |
|
border-radius: 50%; |
|
background: var(--secondary); |
|
} |
|
|
|
#frequencyDisplay { |
|
position: absolute; |
|
top: 10px; |
|
right: 10px; |
|
background: rgba(0,0,0,0.7); |
|
padding: 5px 10px; |
|
border-radius: 4px; |
|
} |
|
|
|
.preset-list { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); |
|
gap: 10px; |
|
} |
|
|
|
.preset-item { |
|
background: rgba(255,255,255,0.1); |
|
padding: 10px; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
transition: background 0.2s; |
|
} |
|
|
|
.preset-item:hover { |
|
background: rgba(255,255,255,0.2); |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="app-container"> |
|
<aside class="sidebar"> |
|
<div class="control-panel"> |
|
<h3>Device Settings</h3> |
|
<div class="input-group"> |
|
<label>Device Role</label> |
|
<select id="deviceRole"> |
|
<option value="primary">Primary</option> |
|
<option value="left">Left Channel</option> |
|
<option value="right">Right Channel</option> |
|
</select> |
|
</div> |
|
<button class="btn" id="startCapture">Start Capture</button> |
|
<button class="btn secondary" id="calibrate">Calibrate</button> |
|
</div> |
|
|
|
<div class="device-list"> |
|
<h3>Connected Devices</h3> |
|
<div class="device-item"> |
|
<span>This Device</span> |
|
<div class="status-indicator"></div> |
|
</div> |
|
</div> |
|
</aside> |
|
|
|
<main class="main-content"> |
|
<div class="control-panel"> |
|
<h2>Binaural Beat Generator</h2> |
|
<div class="input-group"> |
|
<label>Base Frequency (Hz)</label> |
|
<input type="number" id="baseFreq" value="432" min="20" max="1000"> |
|
</div> |
|
<div class="input-group"> |
|
<label>Beat Frequency (Hz)</label> |
|
<input type="number" id="beatFreq" value="7" min="1" max="40"> |
|
</div> |
|
<button class="btn" id="generateBeat">Generate Beat</button> |
|
</div> |
|
|
|
<div class="visualization-container"> |
|
<div class="canvas-container"> |
|
<canvas id="spectrumAnalyzer"></canvas> |
|
<div id="frequencyDisplay">0 Hz</div> |
|
</div> |
|
<div class="canvas-container"> |
|
<canvas id="roomAnalysis"></canvas> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<script> |
|
class AudioSystem { |
|
constructor() { |
|
this.audioContext = null; |
|
this.analyser = null; |
|
this.gainNode = null; |
|
this.oscillators = {}; |
|
this.isCapturing = false; |
|
this.isGenerating = false; |
|
|
|
this.initialize(); |
|
this.setupEventListeners(); |
|
} |
|
|
|
async initialize() { |
|
try { |
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
this.analyser = this.audioContext.createAnalyser(); |
|
this.gainNode = this.audioContext.createGain(); |
|
|
|
this.analyser.fftSize = 2048; |
|
this.gainNode.connect(this.audioContext.destination); |
|
this.analyser.connect(this.gainNode); |
|
|
|
await this.setupSpectrumVisualizer(); |
|
} catch (error) { |
|
console.error('Audio initialization failed:', error); |
|
} |
|
} |
|
|
|
async startCapture() { |
|
if (this.isCapturing) return; |
|
|
|
try { |
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
const source = this.audioContext.createMediaStreamSource(stream); |
|
source.connect(this.analyser); |
|
this.isCapturing = true; |
|
} catch (error) { |
|
console.error('Capture failed:', error); |
|
} |
|
} |
|
|
|
generateBinauralBeat(baseFreq, beatFreq) { |
|
if (this.isGenerating) this.stopBinauralBeat(); |
|
|
|
const leftOsc = this.audioContext.createOscillator(); |
|
const rightOsc = this.audioContext.createOscillator(); |
|
|
|
leftOsc.frequency.value = baseFreq; |
|
rightOsc.frequency.value = baseFreq + beatFreq; |
|
|
|
const merger = this.audioContext.createChannelMerger(2); |
|
|
|
leftOsc.connect(merger, 0, 0); |
|
rightOsc.connect(merger, 0, 1); |
|
merger.connect(this.gainNode); |
|
|
|
leftOsc.start(); |
|
rightOsc.start(); |
|
|
|
this.oscillators = { left: leftOsc, right: rightOsc }; |
|
this.isGenerating = true; |
|
} |
|
|
|
stopBinauralBeat() { |
|
if (!this.isGenerating) return; |
|
|
|
Object.values(this.oscillators).forEach(osc => osc.stop()); |
|
this.oscillators = {}; |
|
this.isGenerating = false; |
|
} |
|
|
|
async setupSpectrumVisualizer() { |
|
const canvas = document.getElementById('spectrumAnalyzer'); |
|
const ctx = canvas.getContext('2d'); |
|
const bufferLength = this.analyser.frequencyBinCount; |
|
const dataArray = new Uint8Array(bufferLength); |
|
|
|
const draw = () => { |
|
requestAnimationFrame(draw); |
|
|
|
this.analyser.getByteFrequencyData(dataArray); |
|
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
const barWidth = canvas.width / bufferLength; |
|
let x = 0; |
|
|
|
for(let i = 0; i < bufferLength; i++) { |
|
const barHeight = (dataArray[i] / 255) * canvas.height; |
|
|
|
const hue = i / bufferLength * 360; |
|
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`; |
|
|
|
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight); |
|
x += barWidth + 1; |
|
} |
|
}; |
|
|
|
draw(); |
|
} |
|
|
|
setupEventListeners() { |
|
document.getElementById('startCapture').onclick = () => this.startCapture(); |
|
document.getElementById('generateBeat').onclick = () => { |
|
const baseFreq = parseFloat(document.getElementById('baseFreq').value); |
|
const beatFreq = parseFloat(document.getElementById('beatFreq').value); |
|
this.generateBinauralBeat(baseFreq, beatFreq); |
|
}; |
|
} |
|
} |
|
|
|
|
|
const audioSystem = new AudioSystem(); |
|
</script> |
|
</body> |
|
</html><script async data-explicit-opt-in="true" data-cookie-opt-in="true" src="https://vercel.live/_next-live/feedback/feedback.js"></script> |