3D-Audio-visuals / index.html
yokoha's picture
Update index.html
a32ef17 verified
<!DOCTYPE html>
<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>