Skip to content

Instantly share code, notes, and snippets.

@ProjectEli
Last active January 22, 2026 15:09
Show Gist options
  • Select an option

  • Save ProjectEli/5ac7abb6e0941b69f0b83f35867d47d4 to your computer and use it in GitHub Desktop.

Select an option

Save ProjectEli/5ac7abb6e0941b69f0b83f35867d47d4 to your computer and use it in GitHub Desktop.
Audio visualizer for EDM genre musics
<!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