anycoder-b8a7dcdc / index.html
idgmatrix's picture
Upload folder using huggingface_hub
8f656f4 verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passive Sonar Audio Synthesizer</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--sonar-primary: #00ffaa;
--sonar-secondary: #008f60;
--sonar-bg: #021016;
--panel-bg: rgba(2, 25, 35, 0.85);
--text-muted: #6fa8a1;
--accent-alert: #ff4d4d;
--font-main: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--sonar-bg);
color: var(--sonar-primary);
font-family: var(--font-main);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background-image:
radial-gradient(circle at 50% 50%, rgba(0, 255, 170, 0.05) 0%, transparent 60%),
linear-gradient(0deg, rgba(0,0,0,0.8) 0%, transparent 100%);
}
/* Header */
header {
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(0, 255, 170, 0.3);
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
z-index: 10;
}
.brand {
font-size: 1.2rem;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 10px;
}
.brand i {
animation: pulse 2s infinite;
}
.anycoder-link {
color: var(--text-muted);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s;
border: 1px solid rgba(111, 168, 161, 0.3);
padding: 5px 12px;
border-radius: 20px;
}
.anycoder-link:hover {
color: var(--sonar-primary);
border-color: var(--sonar-primary);
background: rgba(0, 255, 170, 0.1);
}
/* Main Layout */
main {
flex: 1;
display: grid;
grid-template-columns: 300px 1fr 300px;
gap: 1rem;
padding: 1rem;
overflow: hidden;
}
/* Panels */
.panel {
background: var(--panel-bg);
border: 1px solid rgba(0, 255, 170, 0.2);
border-radius: 8px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
overflow-y: auto;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
backdrop-filter: blur(10px);
}
.panel-title {
font-size: 1rem;
text-transform: uppercase;
border-bottom: 1px solid var(--sonar-secondary);
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 10px;
}
/* Visualizer Area */
.visualizer-container {
grid-column: 2;
display: flex;
flex-direction: column;
position: relative;
border: 1px solid var(--sonar-secondary);
border-radius: 8px;
background: #000;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.sonar-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
background: conic-gradient(from 0deg, transparent 0deg, transparent 280deg, rgba(0, 255, 170, 0.1) 360deg);
border-radius: 50%;
animation: spin 4s linear infinite;
pointer-events: none;
opacity: 0.3;
mix-blend-mode: screen;
}
/* Controls */
.control-group {
margin-bottom: 1rem;
}
.control-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 0.3rem;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
background: rgba(0, 255, 170, 0.2);
border-radius: 3px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--sonar-primary);
cursor: pointer;
box-shadow: 0 0 10px var(--sonar-primary);
margin-top: -5px;
}
input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
height: 6px;
cursor: pointer;
}
/* Buttons */
.btn {
background: transparent;
border: 1px solid var(--sonar-primary);
color: var(--sonar-primary);
padding: 10px 20px;
font-family: inherit;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 4px;
font-weight: bold;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:hover {
background: rgba(0, 255, 170, 0.2);
box-shadow: 0 0 15px rgba(0, 255, 170, 0.4);
}
.btn.active {
background: var(--sonar-primary);
color: #000;
}
.btn-trigger {
background: rgba(0, 255, 170, 0.1);
margin-top: 5px;
}
/* Start Overlay */
#start-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(2, 16, 22, 0.95);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 100;
backdrop-filter: blur(10px);
}
#start-overlay h1 {
font-size: 3rem;
margin-bottom: 1rem;
text-shadow: 0 0 20px var(--sonar-primary);
}
#start-overlay p {
color: var(--text-muted);
margin-bottom: 2rem;
max-width: 600px;
text-align: center;
line-height: 1.6;
}
#start-btn {
font-size: 1.5rem;
padding: 15px 40px;
border: 2px solid var(--sonar-primary);
}
/* Responsive */
@media (max-width: 1024px) {
main {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr;
}
.visualizer-container {
grid-column: 1 / -1;
grid-row: 1;
height: 300px;
}
}
@media (max-width: 768px) {
main {
grid-template-columns: 1fr;
overflow-y: auto;
}
.visualizer-container {
height: 250px;
}
body {
overflow: auto;
}
}
/* Animations */
@keyframes spin {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes pulse {
0% { opacity: 0.6; text-shadow: 0 0 5px var(--sonar-primary); }
50% { opacity: 1; text-shadow: 0 0 20px var(--sonar-primary); }
100% { opacity: 0.6; text-shadow: 0 0 5px var(--sonar-primary); }
}
.status-led {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #333;
display: inline-block;
margin-right: 8px;
box-shadow: inset 0 0 2px rgba(0,0,0,0.5);
}
.status-led.on {
background-color: var(--sonar-primary);
box-shadow: 0 0 8px var(--sonar-primary);
}
</style>
</head>
<body>
<!-- Audio Context Start Overlay -->
<div id="start-overlay">
<h1><i class="fa-solid fa-water"></i> PASSIVE SONAR SYNTH</h1>
<p>
ํŒจ์‹œ๋ธŒ ์†Œ๋‚˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์œ„ํ•œ ๊ณ ํ’ˆ์งˆ ์ˆ˜์ค‘ ์Œ์› ํ•ฉ์„ฑ๊ธฐ์ž…๋‹ˆ๋‹ค.<br>
Web Audio API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ณ ๋ž˜ ์†Œ๋ฆฌ, ์„ ๋ฐ• ํ”„๋กœํŽ ๋Ÿฌ ์†Œ์Œ, ๋ฐ”๋‹ค ๋ฐฐ๊ฒฝ์Œ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
</p>
<button id="start-btn" class="btn">
<i class="fa-solid fa-power-off"></i> ์‹œ์Šคํ…œ ๊ฐ€๋™ (Start System)
</button>
</div>
<header>
<div class="brand">
<i class="fa-solid fa-radar"></i>
<span>Sonar Audio Lab</span>
</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a>
</header>
<main>
<!-- Left Panel: Environment & Biologic -->
<div class="panel">
<div class="panel-title">
<i class="fa-solid fa-globe-americas"></i> ํ™˜๊ฒฝ (Environment)
</div>
<!-- Ocean Ambience -->
<div class="control-group">
<div class="control-label">
<span><span class="status-led" id="led-ocean"></span>ํ•ด์ € ๋ฐฐ๊ฒฝ์Œ (Ambience)</span>
<span id="val-ocean-vol">0%</span>
</div>
<input type="range" id="ocean-vol" min="0" max="1" step="0.01" value="0">
<button class="btn btn-trigger" id="toggle-ocean" style="margin-top:5px; font-size: 0.8rem;">
ON / OFF
</button>
</div>
<div class="panel-title" style="margin-top: 1rem;">
<i class="fa-solid fa-fish"></i> ์ƒ๋ฌผ (Biologic)
</div>
<!-- Whale Synth -->
<div class="control-group">
<div class="control-label">
<span>๊ณ ๋ž˜ ์†Œ๋ฆฌ ํฌ๊ธฐ (Whale Volume)</span>
<span id="val-whale-vol">80%</span>
</div>
<input type="range" id="whale-vol" min="0" max="1" step="0.01" value="0.8">
</div>
<div class="control-group">
<div class="control-label">
<span>ํ˜ธ์ถœ ๋นˆ๋„ (Call Pitch)</span>
<span id="val-whale-pitch">Low</span>
</div>
<input type="range" id="whale-pitch" min="100" max="600" step="10" value="300">
</div>
<button class="btn" id="btn-whale-call">
<i class="fa-solid fa-bullhorn"></i> ๊ณ ๋ž˜ ํ˜ธ์ถœ (Trigger Call)
</button>
<div class="control-group" style="margin-top:10px;">
<div class="control-label">
<span>ํด๋ฆญ์Œ (Clicks)</span>
</div>
<button class="btn btn-trigger" id="btn-whale-click">
<i class="fa-regular fa-circle-dot"></i> ํด๋ฆญ์Œ ๋ฐœ์ƒ
</button>
</div>
</div>
<!-- Center: Visualizer -->
<div class="visualizer-container">
<canvas id="scope"></canvas>
<div class="sonar-overlay"></div>
<div style="position: absolute; bottom: 10px; left: 10px; font-size: 0.8rem; color: var(--sonar-primary);">
LOFARGRAM PREVIEW
</div>
</div>
<!-- Right Panel: Mechanical Target -->
<div class="panel">
<div class="panel-title">
<i class="fa-solid fa-ship"></i> ํ‘œ์  ์†Œ์Œ (Target Noise)
</div>
<div class="control-group">
<button class="btn" id="toggle-ship">
<i class="fa-solid fa-power-off"></i> ์—”์ง„ ์‹œ๋™ (Engine Start)
</button>
</div>
<!-- Propeller RPM -->
<div class="control-group">
<div class="control-label">
<span>ํ”„๋กœํŽ ๋Ÿฌ ์†๋„ (RPM)</span>
<span id="val-rpm">Stopped</span>
</div>
<input type="range" id="ship-rpm" min="0" max="15" step="0.1" value="0">
</div>
<!-- Engine Tone -->
<div class="control-group">
<div class="control-label">
<span>์—”์ง„ ํ†ค (Engine Tone)</span>
<span id="val-engine-tone">Low</span>
</div>
<input type="range" id="engine-tone" min="50" max="200" step="1" value="80">
</div>
<!-- Cavitation -->
<div class="control-group">
<div class="control-label">
<span>๊ณต๋™ ํ˜„์ƒ (Cavitation Noise)</span>
<span id="val-cavitation">50%</span>
</div>
<input type="range" id="cavitation-vol" min="0" max="1" step="0.01" value="0.5">
</div>
<!-- Distance Filter -->
<div class="control-group" style="margin-top: 1rem; border-top: 1px solid var(--sonar-secondary); padding-top: 1rem;">
<div class="control-label">
<span>๊ฑฐ๋ฆฌ ๊ฐ์‡  (Distance Filter)</span>
<span id="val-distance">Close</span>
</div>
<input type="range" id="distance-filter" min="100" max="2000" step="10" value="2000">
<p style="font-size: 0.7rem; color: var(--text-muted); margin-top: 5px;">
๋‚ฎ์„์ˆ˜๋ก ๋ฉ€๋ฆฌ ์žˆ๋Š” ์†Œ๋ฆฌ (Low Pass)
</p>
</div>
</div>
</main>
<script>
/**
* Sonar Audio Synthesizer Logic
* Uses Web Audio API to generate sounds procedurally.
*/
// Audio Context
let audioCtx;
let masterGain;
let analyser;
let isSystemOn = false;
// Visualization
const canvas = document.getElementById('scope');
const canvasCtx = canvas.getContext('2d');
// State
let oceanNode = null;
let shipNodes = null;
// --- Initialization ---
const initAudio = () => {
if (audioCtx) return;
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext();
// Master Gain (Volume)
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.8;
// Analyser for Visualization
analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
masterGain.connect(analyser);
analyser.connect(audioCtx.destination);
drawVisualizer();
isSystemOn = true;
};
document.getElementById('start-btn').addEventListener('click', () => {
initAudio();
if(audioCtx.state === 'suspended') audioCtx.resume();
document.getElementById('start-overlay').style.opacity = '0';
setTimeout(() => {
document.getElementById('start-overlay').style.display = 'none';
}, 500);
});
// --- Helper: Create Noise Buffer ---
function createNoiseBuffer() {
const bufferSize = audioCtx.sampleRate * 2; // 2 seconds
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1; // White noise
}
return buffer;
}
// --- Sound Module: Ocean Ambience ---
// Pinkish noise with heavy lowpass filter
let isOceanOn = false;
function toggleOcean() {
if (!audioCtx) return;
if (isOceanOn) {
// Stop
if (oceanNode) {
oceanNode.source.stop();
oceanNode.source.disconnect();
oceanNode = null;
}
isOceanOn = false;
document.getElementById('led-ocean').classList.remove('on');
} else {
// Start
const buffer = createNoiseBuffer();
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.loop = true;
// Filter to make it sound "underwater" (Pink/Brown noise approx)
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 400;
const gain = audioCtx.createGain();
gain.gain.value = document.getElementById('ocean-vol').value;
source.connect(filter);
filter.connect(gain);
gain.connect(masterGain);
oceanNode = { source, gain, filter };
source.start();
isOceanOn = true;
document.getElementById('led-ocean').classList.add('on');
}
}
document.getElementById('toggle-ocean').addEventListener('click', toggleOcean);
document.getElementById('ocean-vol').addEventListener('input', (e) => {
document.getElementById('val-ocean-vol').innerText = Math.round(e.target.value * 100) + '%';
if (oceanNode) oceanNode.gain.gain.setTargetAtTime(e.target.value, audioCtx.currentTime, 0.1);
});
// --- Sound Module: Whale Synth ---
// Sine waves with pitch bending and reverb/delay
function playWhaleCall() {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const basePitch = parseInt(document.getElementById('whale-pitch').value);
const volume = parseFloat(document.getElementById('whale-vol').value);
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
const filter = audioCtx.createBiquadFilter(); // Smooth out edges
// Simple Reverb using Delay
const delay = audioCtx.createDelay();
const delayGain = audioCtx.createGain();
osc.type = 'sine';
// Pitch Envelope (The "Moan")
osc.frequency.setValueAtTime(basePitch, t);
osc.frequency.exponentialRampToValueAtTime(basePitch * 1.5, t + 1.5); // Pitch up
osc.frequency.exponentialRampToValueAtTime(basePitch * 0.8, t + 3.0); // Pitch down
// Amplitude Envelope
gain.gain.setValueAtTime(0, t);
gain.gain.linearRampToValueAtTime(volume, t + 0.5); // Attack
gain.gain.linearRampToValueAtTime(volume * 0.8, t + 2.0); // Sustain-ish
gain.gain.exponentialRampToValueAtTime(0.01, t + 4.0); // Release
// Filter
filter.type = 'lowpass';
filter.frequency.value = 1500;
// Delay settings (Echo)
delay.delayTime.value = 0.4;
delayGain.gain.value = 0.4;
// Connections
osc.connect(filter);
filter.connect(gain);
// Dry path
gain.connect(masterGain);
// Wet path (Echo)
gain.connect(delay);
delay.connect(delayGain);
delayGain.connect(delay); // Feedback loop
delayGain.connect(masterGain);
osc.start(t);
osc.stop(t + 5.0);
}
function playWhaleClick() {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const volume = parseFloat(document.getElementById('whale-vol').value);
// Create a burst of high frequency noise/sine
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(3000, t);
osc.frequency.exponentialRampToValueAtTime(100, t + 0.05);
gain.gain.setValueAtTime(volume, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.05);
osc.connect(gain);
gain.connect(masterGain);
osc.start(t);
osc.stop(t + 0.1);
}
document.getElementById('btn-whale-call').addEventListener('click', playWhaleCall);
document.getElementById('btn-whale-click').addEventListener('click', () => {
// Play a sequence of clicks
let count = 0;
const interval = setInterval(() => {
playWhaleClick();
count++;
if(count > 5) clearInterval(interval);
}, 150); // rapid clicks
});
document.getElementById('whale-pitch').addEventListener('input', (e) => {
document.getElementById('val-whale-pitch').innerText = e.target.value + 'Hz';
});
document.getElementById('whale-vol').addEventListener('input', (e) => {
document.getElementById('val-whale-vol').innerText = Math.round(e.target.value * 100) + '%';
});
// --- Sound Module: Ship Propeller ---
// Complex: Engine drone (Oscillator) + Cavitation (Modulated Noise)
let isShipOn = false;
function updateShipParams() {
if (!shipNodes) return;
const rpm = parseFloat(document.getElementById('ship-rpm').value);
const tone = parseFloat(document.getElementById('engine-tone').value);
const cavVol = parseFloat(document.getElementById('cavitation-vol').value);
const distFreq = parseFloat(document.getElementById('distance-filter').value);
// Update Engine Tone
shipNodes.engineOsc.frequency.setTargetAtTime(tone, audioCtx.currentTime, 0.2);
// Update RPM (LFO speed)
// If RPM is 0, stop sound effectively
const lfoFreq = rpm;
shipNodes.lfo.frequency.setTargetAtTime(lfoFreq, audioCtx.currentTime, 0.5);
// Update Volumes based on RPM (faster = louder generally)
const engineBaseVol = rpm > 0 ? 0.3 : 0;
shipNodes.engineGain.gain.setTargetAtTime(engineBaseVol, audioCtx.currentTime, 0.5);
const cavBaseVol = rpm > 0 ? cavVol : 0;
shipNodes.cavitationMasterGain.gain.setTargetAtTime(cavBaseVol, audioCtx.currentTime, 0.5);
// Update Distance Filter
shipNodes.masterFilter.frequency.setTargetAtTime(distFreq, audioCtx.currentTime, 0.2);
}
function toggleShip() {
if (!audioCtx) return;
const btn = document.getElementById('toggle-ship');
if (isShipOn) {
// Stop
shipNodes.engineOsc.stop();
shipNodes.lfo.stop();
shipNodes.noiseSource.stop();
// Clean disconnect
shipNodes.masterFilter.disconnect();
shipNodes = null;
isShipOn = false;
btn.classList.remove('active');
btn.innerHTML = '<i class="fa-solid fa-power-off"></i> ์—”์ง„ ์‹œ๋™ (Engine Start)';
} else {
// Start
const t = audioCtx.currentTime;
// 1. Master Filter (Distance simulation)
const masterFilter = audioCtx.createBiquadFilter();
masterFilter.type = 'lowpass';
masterFilter.frequency.value = 2000;
masterFilter.connect(masterGain);
// 2. LFO (The RPM Rhythm)
const lfo = audioCtx.createOscillator();
lfo.type = 'sine';
lfo.frequency.value = 0; // Start at 0
// 3. Engine Drone (Low Sawtooth)
const engineOsc = audioCtx.createOscillator();
engineOsc.type = 'sawtooth';
engineOsc.frequency.value = 100;
const engineGain = audioCtx.createGain();
engineGain.gain.value = 0;
// Engine LFO modulation (Engine throbs slightly)
const engineLfoGain = audioCtx.createGain();
engineLfoGain.gain.value = 0.3; // Depth of modulation
lfo.connect(engineLfoGain);
engineLfoGain.connect(engineGain.gain); // AM Synthesis
engineOsc.connect(engineGain);
engineGain.connect(masterFilter);
// 4. Cavitation (White Noise modulated by LFO)
const noiseBuffer = createNoiseBuffer();
const noiseSource = audioCtx.createBufferSource();
noiseSource.buffer = noiseBuffer;
noiseSource.loop = true;
const noiseFilter = audioCtx.createBiquadFilter();
noiseFilter.type = 'bandpass';
noiseFilter.frequency.value = 800;
noiseFilter.Q.value = 1;
const cavitationGain = audioCtx.createGain();
cavitationGain.gain.value = 0; // Controlled by LFO
// To map LFO (-1 to 1) to Gain (0 to 1), we need a bias
// But simple AM is fine here. We want the swoosh-swoosh.
const cavLfoGain = audioCtx.createGain();
cavLfoGain.gain.value = 0.8; // Depth
const cavitationMasterGain = audioCtx.createGain();
cavitationMasterGain.gain.value = 0; // Overall volume
lfo.connect(cavLfoGain);
cavLfoGain.connect(cavitationGain.gain);
noiseSource.connect(noiseFilter);
noiseFilter.connect(cavitationGain);
cavitationGain.connect(cavitationMasterGain);
cavitationMasterGain.connect(masterFilter);
// Start everything
lfo.start(t);
engineOsc.start(t);
noiseSource.start(t);
shipNodes = {
masterFilter, lfo, engineOsc, engineGain,
noiseSource, cavitationMasterGain
};
isShipOn = true;
btn.classList.add('active');
btn.innerHTML = '<i class="fa-solid fa-stop"></i> ์—”์ง„ ์ •์ง€ (Engine Stop)';
// Apply initial slider values
updateShipParams();
}
}
document.getElementById('toggle-ship').addEventListener('click', toggleShip);
// Ship Controls Listeners
['ship-rpm', 'engine-tone', 'cavitation-vol', 'distance-filter'].forEach(id => {
document.getElementById(id).addEventListener('input', (e) => {
// Update UI labels
if(id === 'ship-rpm') document.getElementById('val-rpm').innerText = e.target.value + ' Hz';
if(id === 'engine-tone') document.getElementById('val-engine-tone').innerText = e.target.value + ' Hz';
if(id === 'cavitation-vol') document.getElementById('val-cavitation').innerText = Math.round(e.target.value*100) + '%';
if(id === 'distance-filter') document.getElementById('val-distance').innerText = e.target.value + ' Hz';
updateShipParams();
});
});
// --- Visualizer Logic ---
function drawVisualizer() {
requestAnimationFrame(drawVisualizer);
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteTimeDomainData(dataArray);
canvasCtx.fillStyle = 'rgba(0, 15, 20, 0.2)'; // Trail effect
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = '#00ffaa';
canvasCtx.shadowBlur = 5;
canvasCtx.shadowColor = '#00ffaa';
canvasCtx.beginPath();
const sliceWidth = canvas.width * 1.0 / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = v * canvas.height / 2;
if (i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(canvas.width, canvas.height / 2);
canvasCtx.stroke();
}
// Resize Canvas
function resizeCanvas() {
const container = document.querySelector('.visualizer-container');
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
</script>
</body>
</html>