Last active
January 22, 2026 15:09
-
-
Save ProjectEli/5ac7abb6e0941b69f0b83f35867d47d4 to your computer and use it in GitHub Desktop.
Audio visualizer for EDM genre musics
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Kick Visualizer (Desktop Audio) v4 by ProjectEli</title> | |
| <style> | |
| body { margin: 0; background: #111; overflow: hidden; display: flex; justify-content: center; align-items: center; height: 100vh; color: #888; font-family: 'Segoe UI', sans-serif; } | |
| canvas { width: 100%; height: 100%; } | |
| #overlay { position: absolute; z-index: 10; cursor: pointer; text-align: center; color: #fff; background: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; } | |
| .grid-label { position: absolute; bottom: 5px; font-size: 10px; color: #666; pointer-events: none; border-left: 1px solid #333; padding-left: 3px; height: 10px;} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="overlay"> | |
| <h1>Click to Start</h1> | |
| <p style="font-size: 0.8em; color: #ccc;">팝업에서 <strong>[시스템 오디오 공유]</strong>를<br>반드시 체크해주세요.</p> | |
| </div> | |
| <canvas id="canvas"></canvas> | |
| <div id="grid-container"></div> | |
| <script> | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const gridContainer = document.getElementById('grid-container'); | |
| let audioContext, analyser, dataArray; | |
| const FFT_SIZE = 4096; | |
| const SMOOTHING = 0.0; | |
| const MIN_FREQ = 0; | |
| const MAX_FREQ = 1000; | |
| const FREQ_BIN_INTERVAL = 100; | |
| // --- Bar Styles --- | |
| const BAR_TOTAL_WIDTH = 8; | |
| const BAR_RATIO = 0.5; | |
| const DRAW_WIDTH = Math.max(1, Math.floor(BAR_TOTAL_WIDTH * BAR_RATIO)); | |
| // --- dB Ranges --- | |
| const DB_MIN = -50; | |
| const DB_MAX = -20; | |
| function resize() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| drawGrid(); | |
| } | |
| window.addEventListener('resize', resize); | |
| function createRange(start, end, step) { | |
| let rangeArray = []; | |
| for (let i = start; i <= end; i += step) { | |
| rangeArray.push(i); | |
| } | |
| return rangeArray; | |
| } | |
| async function initAudio() { | |
| try { | |
| // [변경됨] 시스템 오디오 캡처 요청 (getDisplayMedia) | |
| // 비디오 트랙은 필수 요청이지만 사용하지 않음 | |
| const stream = await navigator.mediaDevices.getDisplayMedia({ | |
| video: true, | |
| audio: { | |
| echoCancellation: false, // 킥 드럼 왜곡 방지 | |
| noiseSuppression: false, // 노이즈 제거 끄기 (저음 보호) | |
| autoGainControl: false, // 볼륨 자동 조절 끄기 | |
| sampleRate: 48000 | |
| } | |
| }); | |
| // 오디오 트랙 확인 | |
| const audioTrack = stream.getAudioTracks()[0]; | |
| if (!audioTrack) { | |
| alert("오디오 공유가 체크되지 않았습니다. 새로고침 후 다시 시도하며 '오디오 공유'를 체크해주세요."); | |
| stream.getTracks().forEach(track => track.stop()); // 스트림 종료 | |
| return; | |
| } | |
| // 오디오 컨텍스트 설정 | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = FFT_SIZE; | |
| analyser.smoothingTimeConstant = SMOOTHING; | |
| analyser.minDecibels = DB_MIN; | |
| analyser.maxDecibels = DB_MAX; | |
| const source = audioContext.createMediaStreamSource(stream); | |
| source.connect(analyser); | |
| // [선택 사항] 캡처한 소리를 스피커로도 듣고 싶으면 주석 해제 (하울링 주의) | |
| // source.connect(audioContext.destination); | |
| const bufferLength = analyser.frequencyBinCount; | |
| dataArray = new Float32Array(bufferLength); | |
| // 화면 공유가 중지되었을 때 처리 | |
| audioTrack.onended = () => { | |
| alert("오디오 공유가 중지되었습니다."); | |
| document.getElementById('overlay').style.display = 'block'; | |
| }; | |
| document.getElementById('overlay').style.display = 'none'; | |
| resize(); | |
| render(); | |
| } catch (e) { | |
| console.error(e); | |
| // 사용자가 취소했거나 권한 문제 발생 시 | |
| if(e.name !== 'NotAllowedError') { | |
| alert("오디오 캡처 중 오류 발생: " + e.message); | |
| } | |
| } | |
| } | |
| function getLinearPos(freq) { | |
| return (freq - MIN_FREQ) / (MAX_FREQ - MIN_FREQ); | |
| } | |
| function getFreqFromPos(pos) { | |
| return MIN_FREQ + (pos * (MAX_FREQ - MIN_FREQ)); | |
| } | |
| function drawGrid() { | |
| gridContainer.innerHTML = ''; | |
| const freqs = createRange(50, MAX_FREQ, FREQ_BIN_INTERVAL); | |
| freqs.forEach(f => { | |
| const pos = getLinearPos(f); | |
| if(pos >= 0 && pos <= 1) { | |
| const label = document.createElement('div'); | |
| label.className = 'grid-label'; | |
| label.innerText = f + 'Hz'; | |
| label.style.left = (pos * 100) + '%'; | |
| gridContainer.appendChild(label); | |
| } | |
| }); | |
| } | |
| // 선형 보간 함수 | |
| function getInterpolatedDb(array, floatIndex) { | |
| const index1 = Math.floor(floatIndex); | |
| const index2 = index1 + 1; | |
| const t = floatIndex - index1; | |
| if (index1 >= array.length) return array[array.length - 1]; | |
| if (index2 >= array.length) return array[index1]; | |
| const v1 = array[index1]; | |
| const v2 = array[index2]; | |
| return v1 * (1 - t) + v2 * t; | |
| } | |
| function render() { | |
| requestAnimationFrame(render); | |
| analyser.getFloatFrequencyData(dataArray); | |
| ctx.fillStyle = '#111'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.strokeStyle = '#222'; | |
| ctx.lineWidth = 1; | |
| ctx.beginPath(); | |
| const gridFreqs = createRange(MIN_FREQ, MAX_FREQ, FREQ_BIN_INTERVAL); | |
| gridFreqs.forEach(f => { | |
| const x = Math.floor(getLinearPos(f) * canvas.width); | |
| ctx.moveTo(x, 0); | |
| ctx.lineTo(x, canvas.height); | |
| }); | |
| ctx.stroke(); | |
| ctx.fillStyle = '#FFFFFF'; | |
| const resolution = audioContext.sampleRate / FFT_SIZE; | |
| for (let x = 0; x < canvas.width; x += BAR_TOTAL_WIDTH) { | |
| const pos = (x + DRAW_WIDTH / 2) / canvas.width; | |
| const targetFreq = getFreqFromPos(pos); | |
| const floatIndex = targetFreq / resolution; | |
| let dbValue = getInterpolatedDb(dataArray, floatIndex); | |
| let barHeightRatio = (dbValue - DB_MIN) / (DB_MAX - DB_MIN); | |
| if (barHeightRatio < 0) barHeightRatio = 0; | |
| if (barHeightRatio > 1) barHeightRatio = 1; | |
| const barHeight = barHeightRatio * canvas.height; | |
| ctx.fillRect(x, canvas.height - barHeight, DRAW_WIDTH, barHeight); | |
| } | |
| } | |
| document.getElementById('overlay').addEventListener('click', initAudio); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment