Spaces:
Running
Running
<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> |