Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Audio Spectrum Analyzer</title> | |
| <style> | |
| :root { | |
| --primary-color: #1a1a2e; | |
| --secondary-color: #16213e; | |
| --accent-color: #0f3460; | |
| --text-color: #e7e7e7; | |
| --highlight-color: #4cc9f0; | |
| --gradient-1: #4361ee; | |
| --gradient-2: #3a0ca3; | |
| --gradient-3: #7209b7; | |
| --gradient-4: #f72585; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| body { | |
| background-color: var(--primary-color); | |
| color: var(--text-color); | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 100vh; | |
| } | |
| header { | |
| background-color: var(--secondary-color); | |
| padding: 1rem; | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); | |
| z-index: 10; | |
| } | |
| .title { | |
| text-align: center; | |
| font-size: 1.8rem; | |
| font-weight: 600; | |
| color: var(--highlight-color); | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| } | |
| .canvas-container { | |
| flex: 1; | |
| position: relative; | |
| } | |
| #visualizer { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .control-panel { | |
| position: absolute; | |
| top: 1rem; | |
| right: 1rem; | |
| background-color: rgba(22, 33, 62, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-radius: 12px; | |
| padding: 1.2rem; | |
| width: 280px; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); | |
| z-index: 100; | |
| transition: transform 0.3s ease; | |
| } | |
| .control-panel.collapsed { | |
| transform: translateX(calc(100% - 50px)); | |
| } | |
| .toggle-panel { | |
| position: absolute; | |
| left: 10px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| background-color: var(--accent-color); | |
| border: none; | |
| color: var(--text-color); | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.2rem; | |
| transition: background-color 0.2s ease; | |
| } | |
| .toggle-panel:hover { | |
| background-color: var(--highlight-color); | |
| } | |
| h2 { | |
| margin-bottom: 1rem; | |
| font-size: 1.3rem; | |
| color: var(--highlight-color); | |
| text-align: center; | |
| } | |
| .control-group { | |
| margin-bottom: 1.2rem; | |
| } | |
| .control-label { | |
| display: block; | |
| margin-bottom: 0.5rem; | |
| font-weight: 500; | |
| font-size: 0.9rem; | |
| color: var(--text-color); | |
| } | |
| .btn { | |
| background-color: var(--accent-color); | |
| color: var(--text-color); | |
| border: none; | |
| border-radius: 8px; | |
| padding: 0.7rem 1rem; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| width: 100%; | |
| } | |
| .btn:hover { | |
| background-color: var(--highlight-color); | |
| color: var(--primary-color); | |
| } | |
| .btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .btn-record { | |
| background-color: #e63946; | |
| } | |
| .btn-record:hover { | |
| background-color: #ff6b6b; | |
| } | |
| .btn-record.recording { | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| background-color: #e63946; | |
| } | |
| 50% { | |
| background-color: #ff6b6b; | |
| } | |
| 100% { | |
| background-color: #e63946; | |
| } | |
| } | |
| .range-slider { | |
| width: 100%; | |
| margin: 0.5rem 0; | |
| } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .slider-value { | |
| width: 50px; | |
| text-align: center; | |
| font-size: 0.9rem; | |
| color: var(--highlight-color); | |
| } | |
| .radio-group { | |
| display: flex; | |
| gap: 0.8rem; | |
| margin-top: 0.5rem; | |
| } | |
| .radio-option { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.3rem; | |
| cursor: pointer; | |
| } | |
| .radio-option input { | |
| cursor: pointer; | |
| } | |
| .status-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| margin-top: 1rem; | |
| padding: 0.5rem; | |
| border-radius: 8px; | |
| background-color: rgba(15, 52, 96, 0.5); | |
| } | |
| .status-dot { | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| background-color: #6c757d; | |
| } | |
| .status-dot.active { | |
| background-color: #4cc9f0; | |
| box-shadow: 0 0 8px #4cc9f0; | |
| animation: glow 1.5s infinite alternate; | |
| } | |
| @keyframes glow { | |
| from { | |
| box-shadow: 0 0 5px #4cc9f0; | |
| } | |
| to { | |
| box-shadow: 0 0 12px #4cc9f0; | |
| } | |
| } | |
| .status-text { | |
| font-size: 0.9rem; | |
| } | |
| @media (max-width: 768px) { | |
| .control-panel { | |
| width: 250px; | |
| } | |
| .title { | |
| font-size: 1.5rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1 class="title">3D Audio Spectrum Analyzer</h1> | |
| </header> | |
| <div class="canvas-container"> | |
| <canvas id="visualizer"></canvas> | |
| </div> | |
| <div class="control-panel"> | |
| <button class="toggle-panel">≡</button> | |
| <h2>Control Panel</h2> | |
| <div class="control-group"> | |
| <button id="startBtn" class="btn btn-record"> | |
| <span class="btn-icon">◉</span> Start Microphone | |
| </button> | |
| </div> | |
| <div class="control-group"> | |
| <label class="control-label">Color Scheme</label> | |
| <div class="radio-group"> | |
| <label class="radio-option"> | |
| <input type="radio" name="colorScheme" value="blue" checked> | |
| <span>Blue</span> | |
| </label> | |
| <label class="radio-option"> | |
| <input type="radio" name="colorScheme" value="red"> | |
| <span>Red</span> | |
| </label> | |
| <label class="radio-option"> | |
| <input type="radio" name="colorScheme" value="rainbow"> | |
| <span>Rainbow</span> | |
| </label> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label class="control-label">Frequency Range</label> | |
| <div class="slider-container"> | |
| <input type="range" class="range-slider" id="frequencyRange" min="0" max="100" value="100"> | |
| <span class="slider-value" id="frequencyValue">100%</span> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label class="control-label">Sensitivity</label> | |
| <div class="slider-container"> | |
| <input type="range" class="range-slider" id="sensitivityRange" min="1" max="10" value="5"> | |
| <span class="slider-value" id="sensitivityValue">5</span> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label class="control-label">3D Effect Depth</label> | |
| <div class="slider-container"> | |
| <input type="range" class="range-slider" id="depthRange" min="1" max="10" value="5"> | |
| <span class="slider-value" id="depthValue">5</span> | |
| </div> | |
| </div> | |
| <div class="status-indicator"> | |
| <div class="status-dot" id="statusDot"></div> | |
| <div class="status-text" id="statusText">Waiting for microphone...</div> | |
| </div> | |
| </div> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> | |
| <script> | |
| class AudioVisualizer { | |
| constructor() { | |
| // DOM elements | |
| this.canvas = document.getElementById('visualizer'); | |
| this.startBtn = document.getElementById('startBtn'); | |
| this.statusDot = document.getElementById('statusDot'); | |
| this.statusText = document.getElementById('statusText'); | |
| this.frequencyRange = document.getElementById('frequencyRange'); | |
| this.frequencyValue = document.getElementById('frequencyValue'); | |
| this.sensitivityRange = document.getElementById('sensitivityRange'); | |
| this.sensitivityValue = document.getElementById('sensitivityValue'); | |
| this.depthRange = document.getElementById('depthRange'); | |
| this.depthValue = document.getElementById('depthValue'); | |
| this.togglePanel = document.querySelector('.toggle-panel'); | |
| this.controlPanel = document.querySelector('.control-panel'); | |
| this.colorOptions = document.querySelectorAll('input[name="colorScheme"]'); | |
| // Audio context and analyzer | |
| this.audioContext = null; | |
| this.analyser = null; | |
| this.dataArray = null; | |
| this.source = null; | |
| this.isRecording = false; | |
| // Three.js variables | |
| this.scene = null; | |
| this.camera = null; | |
| this.renderer = null; | |
| this.terrain = null; | |
| this.colorScheme = 'blue'; | |
| this.maxFrequencyPercent = 100; | |
| this.sensitivity = 5; | |
| this.depth = 5; | |
| // Grid size for the 3D visualization | |
| this.gridSize = 64; | |
| this.vertices = []; | |
| this.heights = []; | |
| this.init(); | |
| } | |
| init() { | |
| // Initialize Three.js | |
| this.initThree(); | |
| // Initialize UI controls | |
| this.initControls(); | |
| // Start render loop | |
| this.animate(); | |
| // Handle window resize | |
| window.addEventListener('resize', () => this.onWindowResize()); | |
| } | |
| initThree() { | |
| // Create scene | |
| this.scene = new THREE.Scene(); | |
| // Create camera | |
| this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| this.camera.position.set(0, 25, 50); | |
| this.camera.lookAt(0, 0, 0); | |
| // Create renderer | |
| this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas, antialias: true }); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| this.renderer.setClearColor(0x1a1a2e); | |
| // Add ambient light | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); | |
| this.scene.add(ambientLight); | |
| // Add directional light | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(1, 1, 1); | |
| this.scene.add(directionalLight); | |
| // Create the terrain mesh | |
| this.createTerrain(); | |
| } | |
| createTerrain() { | |
| // Create grid geometry | |
| const geometry = new THREE.PlaneGeometry(60, 60, this.gridSize - 1, this.gridSize - 1); | |
| geometry.rotateX(-Math.PI / 2); | |
| // Store original vertices | |
| this.vertices = geometry.attributes.position.array; | |
| this.heights = new Array(this.vertices.length / 3).fill(0); | |
| // Create material | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: 0x4cc9f0, | |
| wireframe: false, | |
| flatShading: true, | |
| metalness: 0.3, | |
| roughness: 0.7, | |
| vertexColors: true | |
| }); | |
| // Create vertex colors | |
| const count = geometry.attributes.position.count; | |
| const colors = new Float32Array(count * 3); | |
| for (let i = 0; i < count; i++) { | |
| const x = geometry.attributes.position.array[i * 3]; | |
| const z = geometry.attributes.position.array[i * 3 + 2]; | |
| const distance = Math.sqrt(x * x + z * z); | |
| const normalizedDistance = Math.min(1, distance / 30); | |
| // Default blue color scheme | |
| colors[i * 3] = 0.2 + normalizedDistance * 0.2; // R | |
| colors[i * 3 + 1] = 0.5 + normalizedDistance * 0.3; // G | |
| colors[i * 3 + 2] = 0.8; // B | |
| } | |
| geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| // Create and add mesh | |
| this.terrain = new THREE.Mesh(geometry, material); | |
| this.terrain.position.y = -10; | |
| this.scene.add(this.terrain); | |
| } | |
| updateTerrainColors() { | |
| const colors = this.terrain.geometry.attributes.color.array; | |
| const count = this.terrain.geometry.attributes.position.count; | |
| for (let i = 0; i < count; i++) { | |
| const x = this.terrain.geometry.attributes.position.array[i * 3]; | |
| const z = this.terrain.geometry.attributes.position.array[i * 3 + 2]; | |
| const distance = Math.sqrt(x * x + z * z); | |
| const normalizedDistance = Math.min(1, distance / 30); | |
| const height = this.heights[i] / 10; // Normalized height | |
| // Apply different color schemes | |
| if (this.colorScheme === 'blue') { | |
| colors[i * 3] = 0.2 + normalizedDistance * 0.2; // R | |
| colors[i * 3 + 1] = 0.5 + normalizedDistance * 0.3; // G | |
| colors[i * 3 + 2] = 0.8 - height * 0.3; // B | |
| } else if (this.colorScheme === 'red') { | |
| colors[i * 3] = 0.8 - height * 0.3; // R | |
| colors[i * 3 + 1] = 0.2 + normalizedDistance * 0.2; // G | |
| colors[i * 3 + 2] = 0.3 + normalizedDistance * 0.3; // B | |
| } else if (this.colorScheme === 'rainbow') { | |
| // Rainbow color scheme | |
| const hue = (normalizedDistance + height) * 360; | |
| const saturation = 0.8; | |
| const lightness = 0.5 + height * 0.3; | |
| // Convert HSL to RGB | |
| const c = (1 - Math.abs(2 * lightness - 1)) * saturation; | |
| const x = c * (1 - Math.abs((hue / 60) % 2 - 1)); | |
| const m = lightness - c / 2; | |
| let r, g, b; | |
| if (hue < 60) { | |
| [r, g, b] = [c, x, 0]; | |
| } else if (hue < 120) { | |
| [r, g, b] = [x, c, 0]; | |
| } else if (hue < 180) { | |
| [r, g, b] = [0, c, x]; | |
| } else if (hue < 240) { | |
| [r, g, b] = [0, x, c]; | |
| } else if (hue < 300) { | |
| [r, g, b] = [x, 0, c]; | |
| } else { | |
| [r, g, b] = [c, 0, x]; | |
| } | |
| colors[i * 3] = r + m; | |
| colors[i * 3 + 1] = g + m; | |
| colors[i * 3 + 2] = b + m; | |
| } | |
| } | |
| this.terrain.geometry.attributes.color.needsUpdate = true; | |
| } | |
| animate() { | |
| requestAnimationFrame(() => this.animate()); | |
| // Update terrain based on audio data | |
| if (this.isRecording && this.dataArray) { | |
| this.updateTerrainGeometry(); | |
| } else { | |
| // Idle animation when not recording | |
| this.idleAnimation(); | |
| } | |
| // Render scene | |
| this.renderer.render(this.scene, this.camera); | |
| } | |
| updateTerrainGeometry() { | |
| // Get frequency data | |
| this.analyser.getByteFrequencyData(this.dataArray); | |
| // Calculate the number of frequency bins to use based on the frequency range slider | |
| const maxBinIndex = Math.floor(this.dataArray.length * (this.maxFrequencyPercent / 100)); | |
| // Update vertices based on frequency data | |
| for (let i = 0; i < this.gridSize; i++) { | |
| for (let j = 0; j < this.gridSize; j++) { | |
| const index = i * this.gridSize + j; | |
| const vertexIndex = index * 3 + 1; // Y component | |
| // Map grid position to frequency bin | |
| const binIndex = Math.floor((i * this.gridSize + j) * maxBinIndex / (this.gridSize * this.gridSize)); | |
| // Get amplitude from frequency data | |
| const amplitude = this.dataArray[binIndex] / 255.0; | |
| // Apply sensitivity multiplier | |
| const heightValue = amplitude * (this.sensitivity * 2); | |
| // Apply depth effect | |
| const distanceFromCenter = Math.sqrt( | |
| Math.pow((i - this.gridSize / 2) / (this.gridSize / 2), 2) + | |
| Math.pow((j - this.gridSize / 2) / (this.gridSize / 2), 2) | |
| ); | |
| // Apply distance falloff based on depth setting | |
| const falloff = Math.max(0, 1 - distanceFromCenter * (1 - this.depth / 10)); | |
| // Calculate new height | |
| this.heights[index] = heightValue * 10 * falloff; | |
| // Update vertex position | |
| this.vertices[vertexIndex] = this.heights[index]; | |
| } | |
| } | |
| // Update colors | |
| this.updateTerrainColors(); | |
| // Update geometry | |
| this.terrain.geometry.attributes.position.needsUpdate = true; | |
| } | |
| idleAnimation() { | |
| // Simple idle wave animation | |
| const time = Date.now() * 0.001; | |
| for (let i = 0; i < this.gridSize; i++) { | |
| for (let j = 0; j < this.gridSize; j++) { | |
| const index = i * this.gridSize + j; | |
| const vertexIndex = index * 3 + 1; // Y component | |
| const x = (i - this.gridSize / 2) / 5; | |
| const z = (j - this.gridSize / 2) / 5; | |
| // Generate a gentle wave pattern | |
| const height = Math.sin(x + time) * Math.cos(z + time) * 0.5; | |
| // Store height for color calculations | |
| this.heights[index] = height * 3; | |
| // Update vertex position | |
| this.vertices[vertexIndex] = height * 3; | |
| } | |
| } | |
| // Update colors | |
| this.updateTerrainColors(); | |
| // Update geometry | |
| this.terrain.geometry.attributes.position.needsUpdate = true; | |
| } | |
| onWindowResize() { | |
| // Update camera aspect ratio | |
| this.camera.aspect = window.innerWidth / window.innerHeight; | |
| this.camera.updateProjectionMatrix(); | |
| // Update renderer size | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| async startMicrophone() { | |
| try { | |
| // Request microphone access | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| // Create audio context | |
| this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| // Create analyzer | |
| this.analyser = this.audioContext.createAnalyser(); | |
| this.analyser.fftSize = 2048; | |
| this.analyser.smoothingTimeConstant = 0.85; | |
| // Create buffer for frequency data | |
| this.dataArray = new Uint8Array(this.analyser.frequencyBinCount); | |
| // Connect microphone to analyzer | |
| this.source = this.audioContext.createMediaStreamSource(stream); | |
| this.source.connect(this.analyser); | |
| // Update UI | |
| this.isRecording = true; | |
| this.updateUIState(); | |
| } catch (error) { | |
| console.error('Error accessing microphone:', error); | |
| this.statusText.textContent = 'Microphone access denied'; | |
| } | |
| } | |
| stopMicrophone() { | |
| if (this.source) { | |
| this.source.disconnect(); | |
| this.source = null; | |
| } | |
| if (this.audioContext) { | |
| this.audioContext.close().then(() => { | |
| this.audioContext = null; | |
| this.analyser = null; | |
| this.dataArray = null; | |
| this.isRecording = false; | |
| this.updateUIState(); | |
| }); | |
| } else { | |
| this.isRecording = false; | |
| this.updateUIState(); | |
| } | |
| } | |
| updateUIState() { | |
| if (this.isRecording) { | |
| this.startBtn.innerHTML = '<span class="btn-icon">■</span> Stop Microphone'; | |
| this.startBtn.classList.add('recording'); | |
| this.statusDot.classList.add('active'); | |
| this.statusText.textContent = 'Microphone active'; | |
| } else { | |
| this.startBtn.innerHTML = '<span class="btn-icon">◉</span> Start Microphone'; | |
| this.startBtn.classList.remove('recording'); | |
| this.statusDot.classList.remove('active'); | |
| this.statusText.textContent = 'Microphone inactive'; | |
| } | |
| } | |
| initControls() { | |
| // Start/stop button | |
| this.startBtn.addEventListener('click', () => { | |
| if (this.isRecording) { | |
| this.stopMicrophone(); | |
| } else { | |
| this.startMicrophone(); | |
| } | |
| }); | |
| // Frequency range slider | |
| this.frequencyRange.addEventListener('input', (e) => { | |
| this.maxFrequencyPercent = parseInt(e.target.value); | |
| this.frequencyValue.textContent = `${this.maxFrequencyPercent}%`; | |
| }); | |
| // Sensitivity slider | |
| this.sensitivityRange.addEventListener('input', (e) => { | |
| this.sensitivity = parseInt(e.target.value); | |
| this.sensitivityValue.textContent = this.sensitivity; | |
| }); | |
| // Depth slider | |
| this.depthRange.addEventListener('input', (e) => { | |
| this.depth = parseInt(e.target.value); | |
| this.depthValue.textContent = this.depth; | |
| }); | |
| // Color scheme radio buttons | |
| this.colorOptions.forEach(option => { | |
| option.addEventListener('change', (e) => { | |
| this.colorScheme = e.target.value; | |
| this.updateTerrainColors(); | |
| }); | |
| }); | |
| // Toggle panel button | |
| this.togglePanel.addEventListener('click', () => { | |
| this.controlPanel.classList.toggle('collapsed'); | |
| this.togglePanel.textContent = this.controlPanel.classList.contains('collapsed') ? '≫' : '≡'; | |
| }); | |
| } | |
| } | |
| // Initialize the visualizer when the page loads | |
| window.addEventListener('DOMContentLoaded', () => { | |
| new AudioVisualizer(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |