Spaces:
Running
Running
Update index.html
Browse files- index.html +274 -19
index.html
CHANGED
@@ -1,19 +1,274 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="ko">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>ํผ์น ๊ฐ์ง ๋ฐ ํผ์๋
ธ ํฉ์ฑ</title>
|
7 |
+
<style>
|
8 |
+
body {
|
9 |
+
font-family: sans-serif;
|
10 |
+
display: flex;
|
11 |
+
flex-direction: column;
|
12 |
+
align-items: center;
|
13 |
+
justify-content: center;
|
14 |
+
height: 100vh;
|
15 |
+
margin: 0;
|
16 |
+
background-color: #f4f4f4;
|
17 |
+
}
|
18 |
+
#controls {
|
19 |
+
margin-bottom: 20px;
|
20 |
+
}
|
21 |
+
button {
|
22 |
+
padding: 10px 20px;
|
23 |
+
font-size: 16px;
|
24 |
+
margin: 0 5px;
|
25 |
+
cursor: pointer;
|
26 |
+
}
|
27 |
+
#status {
|
28 |
+
font-size: 1.1em;
|
29 |
+
color: #333;
|
30 |
+
}
|
31 |
+
#pitch-display {
|
32 |
+
margin-top: 10px;
|
33 |
+
font-size: 1.2em;
|
34 |
+
font-weight: bold;
|
35 |
+
color: #007bff;
|
36 |
+
}
|
37 |
+
</style>
|
38 |
+
</head>
|
39 |
+
<body>
|
40 |
+
|
41 |
+
<h1>๋ง ๋๋ ๋
ธ๋ ํผ์น ๊ฐ์ง ๋ฐ ํฉ์ฑ</h1>
|
42 |
+
<p>๋ง์ดํฌ์ ๋๊ณ ์๋ฆฌ๋ฅผ ๋ด์ธ์. ๊ฐ์ง๋ ํผ์น๋ฅผ ํผ์๋
ธ ์๋ฆฌ๋ก ์ฌ์ํฉ๋๋ค.</p>
|
43 |
+
|
44 |
+
<div id="controls">
|
45 |
+
<button id="startButton" disabled>Start</button>
|
46 |
+
<button id="stopButton" disabled>Stop</button>
|
47 |
+
</div>
|
48 |
+
|
49 |
+
<div id="status">Loading model...</div>
|
50 |
+
<div id="pitch-display"></div>
|
51 |
+
|
52 |
+
<!-- TensorFlow.js ๋ฐ ml5.js ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ก๋ -->
|
53 |
+
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script>
|
54 |
+
<script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script>
|
55 |
+
|
56 |
+
<script>
|
57 |
+
let audioContext;
|
58 |
+
let micStream;
|
59 |
+
let pitchDetector;
|
60 |
+
let processingNode;
|
61 |
+
let currentOscillator = null; // ํ์ฌ ์ฌ์ ์ค์ธ ์ค์ค๋ ์ดํฐ
|
62 |
+
let gainNode; // ๋ณผ๋ฅจ ์กฐ์ ๋ฐ ๋ถ๋๋ฌ์ด ์์/์ ์ง์ฉ
|
63 |
+
|
64 |
+
const startButton = document.getElementById('startButton');
|
65 |
+
const stopButton = document.getElementById('stopButton');
|
66 |
+
const statusDisplay = document.getElementById('status');
|
67 |
+
const pitchDisplay = document.getElementById('pitch-display');
|
68 |
+
|
69 |
+
const CREPE_MODEL_URL = 'https://cdn.jsdelivr.net/gh/ml5js/ml5-data@master/models/pitch/crepe/';
|
70 |
+
const PITCH_CONFIDENCE_THRESHOLD = 0.9; // ํผ์น๋ก ์ธ์ ํ ์ต์ ์ ๋ขฐ๋ (CREPE ๋ชจ๋ธ์ null๋ก ๋ฐํํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์)
|
71 |
+
const MIDI_NOTE_A4 = 69; // A4๋ MIDI ๋
ธํธ 69
|
72 |
+
const FREQUENCY_A4 = 440; // A4๋ 440Hz
|
73 |
+
|
74 |
+
// Hz๋ฅผ ๊ฐ์ฅ ๊ฐ๊น์ด MIDI ๋
ธํธ ๋ฒํธ๋ก ๋ณํ
|
75 |
+
function hzToMidi(hz) {
|
76 |
+
if (hz === 0) return null;
|
77 |
+
return Math.round(MIDI_NOTE_A4 + 12 * Math.log2(hz / FREQUENCY_A4));
|
78 |
+
}
|
79 |
+
|
80 |
+
// MIDI ๋
ธํธ ๋ฒํธ๋ฅผ ํด๋น ํ์ค ์ฃผํ์(Hz)๋ก ๋ณํ
|
81 |
+
function midiToHz(midi) {
|
82 |
+
if (midi === null) return 0;
|
83 |
+
// MIDI ๋
ธํธ ๋ฒ์ ์ ํ (๋๋ฌด ๋๊ฑฐ๋ ๋ฎ์ ์๋ฆฌ ๋ฐฉ์ง)
|
84 |
+
midi = Math.max(24, Math.min(108, midi)); // C1 (24) to C8 (108) roughly
|
85 |
+
return FREQUENCY_A4 * Math.pow(2, (midi - MIDI_NOTE_A4) / 12);
|
86 |
+
}
|
87 |
+
|
88 |
+
// ํผ์น ๊ฐ์ง ๋ชจ๋ธ ๋ก๋
|
89 |
+
async function loadModel() {
|
90 |
+
try {
|
91 |
+
statusDisplay.textContent = 'Loading model...';
|
92 |
+
pitchDetector = await ml5.pitch(CREPE_MODEL_URL, audioContext, micStream.getAudioTracks()[0], () => {
|
93 |
+
statusDisplay.textContent = 'Model loaded. Click Start.';
|
94 |
+
startButton.disabled = false;
|
95 |
+
});
|
96 |
+
} catch (error) {
|
97 |
+
statusDisplay.textContent = `Error loading model: ${error.message}`;
|
98 |
+
console.error("Error loading model:", error);
|
99 |
+
}
|
100 |
+
}
|
101 |
+
|
102 |
+
// ์ค์ค๋ ์ดํฐ (์๋ฆฌ) ์์/์
๋ฐ์ดํธ/์ ์ง
|
103 |
+
function startOrUpdateOscillator(hz) {
|
104 |
+
const currentTime = audioContext.currentTime;
|
105 |
+
|
106 |
+
// ํผ์น๊ฐ ์ ํจํ๊ณ , ํ์ฌ ์ฌ์ ์ค์ธ ์ค์ค๋ ์ดํฐ๊ฐ ์๊ฑฐ๋ ์ฃผํ์๊ฐ ํฌ๊ฒ ๋ฐ๋๋ฉด ์ ์ค์ค๋ ์ดํฐ ์์
|
107 |
+
if (hz > 0) {
|
108 |
+
// MIDI ๋
ธํธ ๋ฒํธ๋ก ๋ณํํ์ฌ ์์ํ
|
109 |
+
const midiNote = hzToMidi(hz);
|
110 |
+
if (midiNote !== null) {
|
111 |
+
// ํด๋น MIDI ๋
ธํธ์ ํ์ค ์ฃผํ์ ๊ณ์ฐ
|
112 |
+
const targetHz = midiToHz(midiNote);
|
113 |
+
|
114 |
+
// ํ์ฌ ์ค์ค๋ ์ดํฐ๊ฐ ์๊ฑฐ๋, ๏ฟฝ๏ฟฝ๏ฟฝํ ์ฃผํ์๊ฐ ํ์ฌ ์ค์ค๋ ์ดํฐ ์ฃผํ์์ ๋ค๋ฅด๋ฉด
|
115 |
+
// (์ฝ๊ฐ์ ์ค์ฐจ ํ์ฉ)
|
116 |
+
if (!currentOscillator || Math.abs(currentOscillator.frequency.value - targetHz) > 0.5) {
|
117 |
+
|
118 |
+
// ์ด์ ์ค์ค๋ ์ดํฐ ์ ์ง (๋ถ๋๋ฝ๊ฒ)
|
119 |
+
if (currentOscillator) {
|
120 |
+
gainNode.gain.cancelScheduledValues(currentTime);
|
121 |
+
gainNode.gain.linearRampToValueAtTime(0, currentTime + 0.05); // ์งง์ ๊ฐ์
|
122 |
+
currentOscillator.stop(currentTime + 0.06); // ๊ฐ์ ํ ์ ์ง
|
123 |
+
currentOscillator.disconnect();
|
124 |
+
}
|
125 |
+
|
126 |
+
// ์๋ก์ด ์ค์ค๋ ์ดํฐ ์์ฑ ๋ฐ ์์
|
127 |
+
currentOscillator = audioContext.createOscillator();
|
128 |
+
gainNode = audioContext.createGain(); // ์ ๊ฒ์ธ ๋
ธ๋ (์ ํ์ฌํญ, ๊ธฐ์กด ๊ฒ ์ฌ์ฉ๋ ๊ฐ๋ฅ)
|
129 |
+
|
130 |
+
currentOscillator.type = 'sine'; // 'sine', 'square', 'sawtooth', 'triangle'
|
131 |
+
currentOscillator.frequency.setValueAtTime(targetHz, currentTime); // ์ฃผํ์ ์ค์
|
132 |
+
|
133 |
+
currentOscillator.connect(gainNode);
|
134 |
+
gainNode.connect(audioContext.destination);
|
135 |
+
|
136 |
+
gainNode.gain.setValueAtTime(0, currentTime);
|
137 |
+
gainNode.linearRampToValueAtTime(0.5, currentTime + 0.01); // ์งง์ ๊ณต๊ฒฉ (์ต๋ ๋ณผ๋ฅจ 0.5)
|
138 |
+
|
139 |
+
currentOscillator.start(currentTime);
|
140 |
+
|
141 |
+
// ํผ์น/๋
ธํธ ์ ๋ณด ํ์
|
142 |
+
const noteName = music21NoteName(midiNote); // MIDI ๋
ธํธ ๋ฒํธ๋ฅผ C4, D#5 ๋ฑ์ผ๋ก ๋ณํ
|
143 |
+
pitchDisplay.textContent = `Pitch: ${hz.toFixed(2)} Hz (${noteName})`;
|
144 |
+
|
145 |
+
// ์ค์ค๋ ์ดํฐ๊ฐ ๋๋๋ฉด ์ ๋ฆฌ
|
146 |
+
currentOscillator.onended = () => {
|
147 |
+
// console.log("Oscillator ended");
|
148 |
+
if (currentOscillator && currentOscillator.frequency.value === targetHz) {
|
149 |
+
// ์๋์ ์ผ๋ก ๋ฉ์ถ ๊ฒฝ์ฐ currentOscillator๊ฐ null์ด ์๋ ์ ์์
|
150 |
+
// ํ์ง๋ง ํด๋น ์ฃผํ์์ ์ค์ค๋ ์ดํฐ๊ฐ ๋๋ฌ๋ค๋ฉด null๋ก ์ค์
|
151 |
+
currentOscillator = null;
|
152 |
+
}
|
153 |
+
};
|
154 |
+
|
155 |
+
|
156 |
+
} else {
|
157 |
+
// ์ด๋ฏธ ๊ฐ์ ๋
ธํธ๊ฐ ์ฌ์ ์ค์ด๋ฉด ์ฃผํ์๋ง ๋ฏธ์ธ ์
๋ฐ์ดํธ (์ ํ ์ฌํญ, ์๋ต ๊ฐ๋ฅ)
|
158 |
+
currentOscillator.frequency.setValueAtTime(targetHz, currentTime);
|
159 |
+
}
|
160 |
+
} else {
|
161 |
+
// MIDI ๋
ธํธ ๋ณํ ์คํจ (์: ๋๋ฌด ๋ฎ๊ฑฐ๋ ๋์ ์ฃผํ์)
|
162 |
+
stopOscillator(); // ์๋ฆฌ ๋ฉ์ถค
|
163 |
+
pitchDisplay.textContent = `Pitch: ${hz.toFixed(2)} Hz (๊ฐ์ง ์ค๋ฅ)`;
|
164 |
+
}
|
165 |
+
|
166 |
+
} else {
|
167 |
+
// ํผ์น๊ฐ ์ ํจํ์ง ์์ (0 ๋๋ null) -> ์๋ฆฌ ๋ฉ์ถค
|
168 |
+
stopOscillator();
|
169 |
+
pitchDisplay.textContent = `Pitch: Detecting...`;
|
170 |
+
}
|
171 |
+
}
|
172 |
+
|
173 |
+
|
174 |
+
// MIDI ๋
ธํธ ๋ฒํธ๋ฅผ ์์
์ ๋
ธํธ ์ด๋ฆ์ผ๋ก ๋ณํ (์: 60 -> C4)
|
175 |
+
function music21NoteName(midiNote) {
|
176 |
+
const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
177 |
+
if (midiNote === null || midiNote < 0 || midiNote > 127) {
|
178 |
+
return "Invalid Note";
|
179 |
+
}
|
180 |
+
const octave = Math.floor(midiNote / 12) - 1; // MIDI 0-11์ ์ฅํ๋ธ -1, MIDI 12-23์ ์ฅํ๋ธ 0 ๋ฑ
|
181 |
+
const noteIndex = midiNote % 12;
|
182 |
+
return noteNames[noteIndex] + octave;
|
183 |
+
}
|
184 |
+
|
185 |
+
|
186 |
+
// ์ค์ค๋ ์ดํฐ ์ ์ง
|
187 |
+
function stopOscillator() {
|
188 |
+
if (currentOscillator) {
|
189 |
+
const currentTime = audioContext.currentTime;
|
190 |
+
gainNode.gain.cancelScheduledValues(currentTime);
|
191 |
+
gainNode.gain.linearRampToValueAtTime(0, currentTime + 0.1); // ์งง์ ๊ฐ์
|
192 |
+
currentOscillator.stop(currentTime + 0.11); // ๊ฐ์ ํ ์ ์ง
|
193 |
+
currentOscillator.disconnect(); // ์ฐ๊ฒฐ ํด์
|
194 |
+
currentOscillator = null; // ์ฐธ์กฐ ์ ๊ฑฐ
|
195 |
+
pitchDisplay.textContent = `Pitch: Listening...`;
|
196 |
+
}
|
197 |
+
}
|
198 |
+
|
199 |
+
|
200 |
+
// ๋ง์ดํฌ์์ ํผ์น ๊ฐ์ง ์์
|
201 |
+
async function startPitchDetection() {
|
202 |
+
statusDisplay.textContent = 'Listening...';
|
203 |
+
startButton.disabled = true;
|
204 |
+
stopButton.disabled = false;
|
205 |
+
|
206 |
+
// ml5 pitch.start() ํจ์๋ ๋ฒํผ๋ฅผ ์ง์ ์ฒ๋ฆฌํ์ฌ ์ฝ๋ฐฑ์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํฉ๋๋ค.
|
207 |
+
pitchDetector.start((error, pitch) => {
|
208 |
+
if (error) {
|
209 |
+
console.error("Pitch detection error:", error);
|
210 |
+
statusDisplay.textContent = `Pitch detection error: ${error.message}`;
|
211 |
+
stopPitchDetection(); // ์ค๋ฅ ๋ฐ์ ์ ์ ์ง
|
212 |
+
return;
|
213 |
+
}
|
214 |
+
|
215 |
+
if (pitch) {
|
216 |
+
// pitch ๊ฐ์ฒด์ Hz ๊ฐ์ด ํฌํจ๋์ด ์์ต๋๋ค. CREPE ๋ชจ๋ธ์ ์ ๋ขฐ๋๊ฐ ๋ฎ์ผ๋ฉด pitch=null์ ๋ฐํํ๋ ๊ฒฝํฅ์ด ์์ต๋๋ค.
|
217 |
+
// ๋ณ๋์ ์ ๋ขฐ๋ ์๊ณ๊ฐ ๊ฒ์ฌ๋ณด๋ค๋ null ์ฒดํฌ๊ฐ ๋ ์ผ๋ฐ์ ์
๋๋ค.
|
218 |
+
startOrUpdateOscillator(pitch.hz);
|
219 |
+
} else {
|
220 |
+
// ํผ์น ๊ฐ์ง ์คํจ (๋ถํ์คํ๊ฑฐ๋ ์๋ฆฌ๊ฐ ์๋ ๊ฒฝ์ฐ)
|
221 |
+
stopOscillator();
|
222 |
+
pitchDisplay.textContent = `Pitch: Detecting...`;
|
223 |
+
}
|
224 |
+
});
|
225 |
+
}
|
226 |
+
|
227 |
+
// ํผ์น ๊ฐ์ง ์ ์ง
|
228 |
+
function stopPitchDetection() {
|
229 |
+
if (pitchDetector) {
|
230 |
+
pitchDetector.stop();
|
231 |
+
}
|
232 |
+
stopOscillator(); // ์ฌ์ ์ค์ธ ์๋ฆฌ ์ ์ง
|
233 |
+
statusDisplay.textContent = 'Model loaded. Click Start.';
|
234 |
+
startButton.disabled = false;
|
235 |
+
stopButton.disabled = true;
|
236 |
+
pitchDisplay.textContent = '';
|
237 |
+
}
|
238 |
+
|
239 |
+
// ์ด๊ธฐ ์ค์
|
240 |
+
async function init() {
|
241 |
+
try {
|
242 |
+
// ์ค๋์ค ์ปจํ
์คํธ ์์ฑ (ํฌ๋กฌ ์ ์ฑ
์ผ๋ก ์ธํด ์ฌ์ฉ์ ์ธํฐ๋์
ํ์)
|
243 |
+
// ์ฌ์ฉ์๊ฐ Start ๋ฒํผ์ ๋๋ฅผ ๋ ์์ฑํ๋๋ก ๋ณ๊ฒฝ
|
244 |
+
// audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
245 |
+
|
246 |
+
// ๋ง์ดํฌ ์คํธ๋ฆผ ๊ฐ์ ธ์ค๊ธฐ
|
247 |
+
statusDisplay.textContent = 'Requesting microphone access...';
|
248 |
+
micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
249 |
+
|
250 |
+
// ์ค๋์ค ์ปจํ
์คํธ ์์ฑ (getUserMedia ์ฑ๊ณต ํ)
|
251 |
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
252 |
+
|
253 |
+
// ml5 pitch ๋ชจ๋ธ ๋ก๋ (์คํธ๋ฆผ ์ ๋ณด ํ์)
|
254 |
+
loadModel(); // loadModel ํจ์ ์์์ pitchDetector.start()๋ ๋ชจ๋ธ ๋ก๋ ์๋ฃ ํ์ ํธ์ถ๋จ
|
255 |
+
|
256 |
+
} catch (error) {
|
257 |
+
statusDisplay.textContent = `Error accessing microphone: ${error.message}`;
|
258 |
+
console.error("Error accessing microphone:", error);
|
259 |
+
startButton.disabled = true; // ๋ง์ดํฌ ์ ๊ทผ ์คํจ ์ ์์ ๋ถ๊ฐ
|
260 |
+
stopButton.disabled = true;
|
261 |
+
}
|
262 |
+
}
|
263 |
+
|
264 |
+
// ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ค์
|
265 |
+
startButton.addEventListener('click', startPitchDetection);
|
266 |
+
stopButton.addEventListener('click', stopPitchDetection);
|
267 |
+
|
268 |
+
// ํ์ด์ง ๋ก๋ ์ ์ด๊ธฐํ ์์
|
269 |
+
window.onload = init;
|
270 |
+
|
271 |
+
</script>
|
272 |
+
|
273 |
+
</body>
|
274 |
+
</html>
|