pp / index.html
kimhyunwoo's picture
Update index.html
8381e0f verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ํ”ผ์น˜ ๊ฐ์ง€ ๋ฐ ํ”ผ์•„๋…ธ ํ•ฉ์„ฑ</title>
<style>
body {
font-family: sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh; /* ํ™”๋ฉด ์ „์ฒด ๋†’์ด ์‚ฌ์šฉ */
margin: 0;
background-color: #f4f4f4;
padding: 20px; /* ํŒจ๋”ฉ ์ถ”๊ฐ€ */
box-sizing: border-box; /* ํŒจ๋”ฉ์ด ์ „์ฒด ํฌ๊ธฐ์— ํฌํ•จ๋˜๋„๋ก */
}
h1 {
margin-top: 0;
text-align: center;
}
#controls {
margin-bottom: 20px;
display: flex; /* ๋ฒ„ํŠผ ์ •๋ ฌ */
gap: 10px; /* ๋ฒ„ํŠผ ๊ฐ„ ๊ฐ„๊ฒฉ */
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 5px;
transition: background-color 0.3s ease;
}
#startButton {
background-color: #28a745; /* ๋…น์ƒ‰ */
color: white;
}
#startButton:disabled {
background-color: #94d3a2;
cursor: not-allowed;
}
#stopButton {
background-color: #dc3545; /* ๋นจ๊ฐ„์ƒ‰ */
color: white;
}
#stopButton:disabled {
background-color: #f0a9b0;
cursor: not-allowed;
}
#status {
font-size: 1.1em;
color: #555;
min-height: 1.5em; /* ๋†’์ด ํ™•๋ณด */
}
#pitch-display {
margin-top: 10px;
font-size: 1.2em;
font-weight: bold;
color: #007bff;
min-height: 1.5em; /* ๋†’์ด ํ™•๋ณด */
}
</style>
</head>
<body>
<h1>๋ง ๋˜๋Š” ๋…ธ๋ž˜ ํ”ผ์น˜ ๊ฐ์ง€ ๋ฐ ํ•ฉ์„ฑ</h1>
<p>๋งˆ์ดํฌ์— ๋Œ€๊ณ  ์†Œ๋ฆฌ๋ฅผ ๋‚ด์„ธ์š”. ๊ฐ์ง€๋œ ํ”ผ์น˜๋ฅผ ํ”ผ์•„๋…ธ ์†Œ๋ฆฌ๋กœ ์žฌ์ƒํ•ฉ๋‹ˆ๋‹ค.</p>
<div id="controls">
<button id="startButton" disabled>Start</button>
<button id="stopButton" disabled>Stop</button>
</div>
<div id="status">Loading libraries...</div>
<div id="pitch-display"></div>
<!-- TensorFlow.js ๋ฐ ml5.js ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋“œ -->
<!-- ml5.js ํŠน์ • ์•ˆ์ • ๋ฒ„์ „ ์‚ฌ์šฉ (@0.12.2) -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/ml5.min.js"></script>
<script>
let audioContext;
let micStream;
let pitchDetector;
let currentOscillator = null; // ํ˜„์žฌ ์žฌ์ƒ ์ค‘์ธ ์˜ค์‹ค๋ ˆ์ดํ„ฐ
let gainNode; // ๋ณผ๋ฅจ ์กฐ์ ˆ ๋ฐ ๋ถ€๋“œ๋Ÿฌ์šด ์‹œ์ž‘/์ •์ง€์šฉ
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
const statusDisplay = document.getElementById('status');
const pitchDisplay = document.getElementById('pitch-display');
// CREPE ๋ชจ๋ธ URL - ml5์—์„œ ์ œ๊ณตํ•˜๋Š” ํ”ผ์น˜ ๊ฐ์ง€ ๋ชจ๋ธ
const CREPE_MODEL_URL = 'https://cdn.jsdelivr.net/gh/ml5js/ml5-data@master/models/pitch/crepe/';
const MIDI_NOTE_A4 = 69; // A4๋Š” MIDI ๋…ธํŠธ 69
const FREQUENCY_A4 = 440; // A4๋Š” 440Hz
// Hz๋ฅผ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด MIDI ๋…ธํŠธ ๋ฒˆํ˜ธ๋กœ ๋ณ€ํ™˜ (์–‘์žํ™”)
function hzToMidi(hz) {
if (hz === 0) return null;
// MIDI ๋…ธํŠธ ๋ฒˆํ˜ธ ๊ณ„์‚ฐ (C0 = 12)
const midi = Math.round(12 * Math.log2(hz / 16.35) + 12); // 16.35Hz๋Š” C0์˜ ์ฃผํŒŒ์ˆ˜
// MIDI ๋…ธํŠธ ๋ฒ”์œ„ ์ œํ•œ (์‚ฌ๋žŒ ๋ชฉ์†Œ๋ฆฌ/์•…๊ธฐ ๋ฒ”์œ„)
return Math.max(24, Math.min(100, midi)); // C1 (24) to C7 (84) ์ฏค์œผ๋กœ ์ œํ•œ
// return midi; // ์ œํ•œ ์—†๋Š” ๋ฒ„์ „
}
// MIDI ๋…ธํŠธ ๋ฒˆํ˜ธ๋ฅผ ํ•ด๋‹น ํ‘œ์ค€ ์ฃผํŒŒ์ˆ˜(Hz)๋กœ ๋ณ€ํ™˜
function midiToHz(midi) {
if (midi === null) return 0;
// MIDI 12 = C0, 69 = A4
return 440 * Math.pow(2, (midi - MIDI_NOTE_A4) / 12);
}
// MIDI ๋…ธํŠธ ๋ฒˆํ˜ธ๋ฅผ ์Œ์•…์  ๋…ธํŠธ ์ด๋ฆ„์œผ๋กœ ๋ณ€ํ™˜ (์˜ˆ: 60 -> C4)
function midiToNoteName(midiNote) {
const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
if (midiNote === null || midiNote < 0 || midiNote > 127) {
return "Invalid Note";
}
const octave = Math.floor(midiNote / 12) - 1; // MIDI 0-11์€ ์˜ฅํƒ€๋ธŒ -1, MIDI 12-23์€ ์˜ฅํƒ€๋ธŒ 0 ๋“ฑ
const noteIndex = midiNote % 12;
return noteNames[noteIndex] + octave;
}
// ํ”ผ์น˜ ๊ฐ์ง€ ๋ชจ๋ธ ๋กœ๋“œ
async function loadModel() {
try {
statusDisplay.textContent = 'Loading pitch detection model...';
// ml5 ๊ฐ์ฒด๊ฐ€ ์ œ๋Œ€๋กœ ๋กœ๋“œ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
if (typeof ml5 === 'undefined') {
throw new Error('ml5 library is not loaded. Check script tag or network.');
}
// ml5.pitch ํ•จ์ˆ˜๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
if (typeof ml5.pitch !== 'function') {
console.error('ml5 object:', ml5); // ๋””๋ฒ„๊น…์„ ์œ„ํ•ด ml5 ๊ฐ์ฒด ์ถœ๋ ฅ
throw new Error('ml5.pitch is not a function. ml5 library might be incomplete or incorrect version.');
}
// ml5.pitch ๋ชจ๋ธ ์ดˆ๊ธฐํ™”
// ml5 ์˜ˆ์ œ์—์„œ๋Š” MediaStream ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ์ „๋‹ฌํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค.
pitchDetector = await ml5.pitch(CREPE_MODEL_URL, audioContext, micStream, () => {
statusDisplay.textContent = 'Model loaded. Click Start.';
startButton.disabled = false;
});
} catch (error) {
statusDisplay.textContent = `Error loading model: ${error.message}`;
console.error("Error loading model:", error);
startButton.disabled = true; // ๋ชจ๋ธ ๋กœ๋“œ ์‹คํŒจ ์‹œ ์‹œ์ž‘ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™”
stopButton.disabled = true;
}
}
// ์˜ค์‹ค๋ ˆ์ดํ„ฐ (์†Œ๋ฆฌ) ์‹œ์ž‘/์—…๋ฐ์ดํŠธ/์ •์ง€
function startOrUpdateOscillator(hz) {
const currentTime = audioContext.currentTime;
// ํ”ผ์น˜๊ฐ€ ์œ ํšจํ•˜๊ณ  (0๋ณด๋‹ค ํฌ๊ณ  ๋„ˆ๋ฌด ํฌ์ง€ ์•Š์€ ๊ฐ’), ํ˜„์žฌ ์žฌ์ƒ ์ค‘์ธ ์˜ค์‹ค๋ ˆ์ดํ„ฐ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ฃผํŒŒ์ˆ˜๊ฐ€ ํฌ๊ฒŒ ๋ฐ”๋€Œ๋ฉด ์ƒˆ ์˜ค์‹ค๋ ˆ์ดํ„ฐ ์‹œ์ž‘
if (hz > 50 && hz < 10000) { // ํ˜„์‹ค์ ์ธ ํ”ผ์น˜ ๋ฒ”์œ„ ์ œํ•œ (50Hz ~ 10kHz)
// MIDI ๋…ธํŠธ ๋ฒˆํ˜ธ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์–‘์žํ™”
const midiNote = hzToMidi(hz);
if (midiNote !== null) {
// ํ•ด๋‹น MIDI ๋…ธํŠธ์˜ ํ‘œ์ค€ ์ฃผํŒŒ์ˆ˜ ๊ณ„์‚ฐ
const targetHz = midiToHz(midiNote);
// ํ˜„์žฌ ์˜ค์‹ค๋ ˆ์ดํ„ฐ๊ฐ€ ์—†๊ฑฐ๋‚˜, ๋ชฉํ‘œ ์ฃผํŒŒ์ˆ˜๊ฐ€ ํ˜„์žฌ ์˜ค์‹ค๋ ˆ์ดํ„ฐ ์ฃผํŒŒ์ˆ˜์™€ ๋‹ค๋ฅด๋ฉด (์•ฝ๊ฐ„์˜ ์˜ค์ฐจ ํ—ˆ์šฉ)
// ์•ฝ 0.5Hz ์ฐจ์ด ์ด์ƒ ๋‚˜๋ฉด ์ƒˆ ๋…ธํŠธ ์‹œ์ž‘์œผ๋กœ ๊ฐ„์ฃผ
if (!currentOscillator || Math.abs(currentOscillator.frequency.value - targetHz) > 0.5) {
// ์ด์ „ ์˜ค์‹ค๋ ˆ์ดํ„ฐ ์ •์ง€ (๋ถ€๋“œ๋Ÿฝ๊ฒŒ)
if (currentOscillator) {
// ์ด๋ฏธ ์˜ˆ์•ฝ๋œ ๊ฒŒ์ธ ๋ณ€๊ฒฝ์ด ์žˆ๋‹ค๋ฉด ์ทจ์†Œ
gainNode.gain.cancelScheduledValues(currentTime);
// ํ˜„์žฌ ๋ณผ๋ฅจ์—์„œ 0์œผ๋กœ ์งง๊ฒŒ ๊ฐ์‡ 
gainNode.gain.linearRampToValueAtTime(0, currentTime + 0.05); // ์งง์€ ๊ฐ์‡  (50ms)
// ๊ฐ์‡ ๊ฐ€ ์™„๋ฃŒ๋œ ํ›„ ์˜ค์‹ค๋ ˆ์ดํ„ฐ ์ •์ง€ ์˜ˆ์•ฝ
currentOscillator.stop(currentTime + 0.06); // ๊ฐ์‡  ์‹œ๊ฐ„๋ณด๋‹ค ์•ฝ๊ฐ„ ๊ธธ๊ฒŒ ์„ค์ •
// ์—ฐ๊ฒฐ ํ•ด์ œ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€)
currentOscillator.disconnect();
gainNode.disconnect(); // ๊ฒŒ์ธ ๋…ธ๋“œ๋„ ํ•ด์ œ
currentOscillator = null; // ์ฐธ์กฐ ์ œ๊ฑฐ
}
// ์ƒˆ๋กœ์šด ์˜ค์‹ค๋ ˆ์ดํ„ฐ ๋ฐ ๊ฒŒ์ธ ๋…ธ๋“œ ์ƒ์„ฑ
currentOscillator = audioContext.createOscillator();
gainNode = audioContext.createGain();
currentOscillator.type = 'sine'; // 'sine', 'square', 'sawtooth', 'triangle'
currentOscillator.frequency.setValueAtTime(targetHz, currentTime); // ์ฃผํŒŒ์ˆ˜ ์„ค์ •
// ์˜ค์‹ค๋ ˆ์ดํ„ฐ -> ๊ฒŒ์ธ ๋…ธ๋“œ -> ์˜ค๋””์˜ค ์ถœ๋ ฅ ์—ฐ๊ฒฐ
currentOscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// ๋ณผ๋ฅจ์„ 0์—์„œ ์‹œ์ž‘ํ•˜์—ฌ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์˜ฌ๋ฆผ (Attack)
gainNode.gain.setValueAtTime(0, currentTime);
gainNode.gain.linearRampToValueAtTime(0.3, currentTime + 0.02); // ๋น ๋ฅด๊ฒŒ ๋ณผ๋ฅจ ์˜ฌ๋ฆผ (max 0.3)
// ์˜ค์‹ค๋ ˆ์ดํ„ฐ ์‹œ์ž‘
currentOscillator.start(currentTime);
// ํ”ผ์น˜/๋…ธํŠธ ์ •๋ณด ํ‘œ์‹œ
const noteName = midiToNoteName(midiNote); // MIDI ๋…ธํŠธ ๋ฒˆํ˜ธ๋ฅผ C4, D#5 ๋“ฑ์œผ๋กœ ๋ณ€ํ™˜
pitchDisplay.textContent = `Pitch: ${hz.toFixed(1)} Hz -> ${noteName}`; // ๊ฐ์ง€๋œ Hz์™€ ์–‘์žํ™”๋œ ๋…ธํŠธ ํ‘œ์‹œ
// ์˜ค์‹ค๋ ˆ์ดํ„ฐ๊ฐ€ ๋๋‚˜๋ฉด ์ •๋ฆฌ (stop()์— ์˜ํ•ด ๋ช…์‹œ์ ์œผ๋กœ ๋ฉˆ์ท„์„ ๋•Œ ํ˜ธ์ถœ๋จ)
currentOscillator.onended = () => {
//console.log("Oscillator ended:", targetHz); // ๋””๋ฒ„๊น…์šฉ
// ์˜ค์‹ค๋ ˆ์ดํ„ฐ๊ฐ€ ์ •์ง€๋˜๋ฉด currentOscillator ์ฐธ์กฐ๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ์ƒˆ๋กœ์šด ์†Œ๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•จ
if (currentOscillator && currentOscillator.frequency.value === targetHz) {
currentOscillator = null;
//console.log("Oscillator reference cleared."); // ๋””๋ฒ„๊น…์šฉ
}
};
} else {
// ์ด๋ฏธ ๊ฐ™์€ ๋…ธํŠธ๊ฐ€ ์žฌ์ƒ ์ค‘์ด๋ฉด ์ฃผํŒŒ์ˆ˜๋งŒ ๋ฏธ์„ธ ์—…๋ฐ์ดํŠธ (์„ ํƒ ์‚ฌํ•ญ, ์ƒ๋žต ๊ฐ€๋Šฅ)
// currentOscillator.frequency.setValueAtTime(targetHz, currentTime); // ๋„ˆ๋ฌด ์ž์ฃผ ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์„ ์ฒ˜๋ฆฌ
}
} else {
// MIDI ๋…ธํŠธ ๋ณ€ํ™˜ ์‹คํŒจ (์˜ˆ: ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚œ ์ฃผํŒŒ์ˆ˜)
stopOscillator(); // ์†Œ๋ฆฌ ๋ฉˆ์ถค
pitchDisplay.textContent = `Pitch: ${hz.toFixed(1)} Hz (๋ฒ”์œ„ ๋ฒ—์–ด๋‚จ)`;
}
} else {
// ํ”ผ์น˜๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Œ (0 ๋˜๋Š” ๋„ˆ๋ฌด ๋‚ฎ๊ฑฐ๋‚˜ ๋†’์€ ๊ฐ’) -> ์†Œ๋ฆฌ ๋ฉˆ์ถค
stopOscillator();
pitchDisplay.textContent = `Pitch: Detecting...`;
}
}
// ์˜ค์‹ค๋ ˆ์ดํ„ฐ ์ •์ง€ (๋ถ€๋“œ๋Ÿฝ๊ฒŒ)
function stopOscillator() {
if (currentOscillator) {
const currentTime = audioContext.currentTime;
gainNode.gain.cancelScheduledValues(currentTime);
gainNode.gain.linearRampToValueAtTime(0, currentTime + 0.1); // ์งง์€ ๊ฐ์‡  (100ms)
currentOscillator.stop(currentTime + 0.11); // ๊ฐ์‡  ์‹œ๊ฐ„๋ณด๋‹ค ์•ฝ๊ฐ„ ๊ธธ๊ฒŒ ์„ค์ •
currentOscillator.disconnect(); // ์—ฐ๊ฒฐ ํ•ด์ œ
gainNode.disconnect(); // ๊ฒŒ์ธ ๋…ธ๋“œ๋„ ํ•ด์ œ
currentOscillator = null; // ์ฐธ์กฐ ์ œ๊ฑฐ
pitchDisplay.textContent = `Pitch: Listening...`; // ์†Œ๋ฆฌ ๋ฉˆ์ถค ํ›„ ๋ฉ”์‹œ์ง€
}
}
// ๋งˆ์ดํฌ์—์„œ ํ”ผ์น˜ ๊ฐ์ง€ ์‹œ์ž‘
async function startPitchDetection() {
statusDisplay.textContent = 'Listening...';
startButton.disabled = true;
stopButton.disabled = false;
pitchDisplay.textContent = 'Pitch: Listening...'; // ์ดˆ๊ธฐ ์ƒํƒœ ํ‘œ์‹œ
// ml5 pitch.start() ํ•จ์ˆ˜๋Š” ์˜ค๋””์˜ค ์ŠคํŠธ๋ฆผ์„ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ฝœ๋ฐฑ์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
// pitch ๊ฐ์ฒด๋Š” { hz: ..., confidence: ... } ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค.
pitchDetector.start((error, pitch) => {
if (error) {
console.error("Pitch detection error:", error);
statusDisplay.textContent = `Pitch detection error: ${error.message}`;
stopPitchDetection(); // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์ •์ง€
return;
}
// ml5 CREPE ๋ชจ๋ธ์€ ์‹ ๋ขฐ๋„๊ฐ€ ๋‚ฎ์œผ๋ฉด pitch = null์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝํ–ฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
// ๋”ฐ๋ผ์„œ pitch ๊ฐ์ฒด๊ฐ€ ์œ ํšจํ•œ์ง€ ๋จผ์ € ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
if (pitch && pitch.hz) {
// pitch.hz ๊ฐ’์ด 0์ด ์•„๋‹Œ ์œ ํšจํ•œ ์ฃผํŒŒ์ˆ˜์ธ์ง€ ํ™•์ธ
startOrUpdateOscillator(pitch.hz);
} else {
// ํ”ผ์น˜ ๊ฐ์ง€ ์‹คํŒจ (null์ด ๋ฐ˜ํ™˜๋˜์—ˆ๊ฑฐ๋‚˜ hz=0) -> ์†Œ๋ฆฌ ๋ฉˆ์ถค
stopOscillator();
}
});
}
// ํ”ผ์น˜ ๊ฐ์ง€ ์ •์ง€
function stopPitchDetection() {
if (pitchDetector) {
pitchDetector.stop(); // ml5 ํ”ผ์น˜ ๊ฐ์ง€ ์ค‘์ง€
}
stopOscillator(); // ์žฌ์ƒ ์ค‘์ธ ์†Œ๋ฆฌ ์ •์ง€
statusDisplay.textContent = 'Model loaded. Click Start.'; // ์ƒํƒœ ๋ณต์›
startButton.disabled = false; // ๋ฒ„ํŠผ ์ƒํƒœ ๋ณต์›
stopButton.disabled = true;
pitchDisplay.textContent = ''; // ํ”ผ์น˜ ํ‘œ์‹œ ์ดˆ๊ธฐํ™”
}
// ์ดˆ๊ธฐ ์„ค์ •
async function init() {
try {
statusDisplay.textContent = 'Requesting microphone access...';
// ๋งˆ์ดํฌ ์ŠคํŠธ๋ฆผ ๊ฐ€์ ธ์˜ค๊ธฐ
// ์ด ๊ณผ์ •์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋งˆ์ดํฌ ์ ‘๊ทผ ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๋Š” ํŒ์—…์ด ๋œน๋‹ˆ๋‹ค.
micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
statusDisplay.textContent = 'Microphone access granted. Creating audio context.';
// ์˜ค๋””์˜ค ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ (getUserMedia ์„ฑ๊ณต ํ›„ ์ƒ์„ฑํ•˜์—ฌ Chrome ์ •์ฑ… ๋งŒ์กฑ)
audioContext = new (window.AudioContext || window.webkitAudioContext)();
statusDisplay.textContent = 'Audio context created. Loading pitch detection model...';
// ml5 pitch ๋ชจ๋ธ ๋กœ๋“œ (์ŠคํŠธ๋ฆผ ์ •๋ณด ํ•„์š”)
// loadModel ํ•จ์ˆ˜๋Š” ๋น„๋™๊ธฐ์ ์œผ๋กœ ์™„๋ฃŒ๋˜๋ฉฐ, ์™„๋ฃŒ ํ›„ ๋ฒ„ํŠผ์„ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.
await loadModel();
} catch (error) {
statusDisplay.textContent = `Error accessing microphone or initializing: ${error.message}`;
console.error("Initialization error:", error);
startButton.disabled = true; // ์ดˆ๊ธฐํ™” ์‹คํŒจ ์‹œ ์‹œ์ž‘ ๋ถˆ๊ฐ€
stopButton.disabled = true;
pitchDisplay.textContent = 'Error.';
}
}
// ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ •
startButton.addEventListener('click', startPitchDetection);
stopButton.addEventListener('click', stopPitchDetection);
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ดˆ๊ธฐํ™” ์‹œ์ž‘
window.onload = init;
// ml5 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋กœ๋“œ๋œ ํ›„ ๋ฐ”๋กœ ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋Š” ์ถ”๊ฐ€ ์ฒดํฌ (ํ˜น์‹œ ๋ชจ๋ฅผ ์ง€์—ฐ ๋Œ€๋น„)
// ์‚ฌ์‹ค window.onload ์‹œ์ ์—๋Š” script ํƒœ๊ทธ๊ฐ€ ๋ชจ๋‘ ์‹คํ–‰๋˜์—ˆ์–ด์•ผ ํ•˜๋ฏ€๋กœ ๋ถˆํ•„์š”ํ•  ์ˆ˜ ์žˆ์œผ๋‚˜, ์•ˆ์ „์„ ์œ„ํ•ด ๋‚จ๊ฒจ๋‘ 
if (typeof ml5 === 'undefined') {
console.warn('ml5 library is still undefined after script tag. Check network or source.');
} else {
console.log('ml5 library detected after script tag load.');
}
</script>
</body>
</html>