Ai-Sight-Singing / static /script.js
highupvocal's picture
Update static/script.js
da54b39 verified
Raw
History Blame Contribute Delete
9.11 kB
// ===========================
// Supabase μ΄ˆκΈ°ν™”
// ===========================
alert("script.js λ‘œλ“œλ¨");
const supabaseClient = window.supabase.createClient(
"https://kceyrxbzlzfrzonjmkdu.supabase.co",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtjZXlyeGJ6bHpmcnpvbmpta2R1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjUxODc5OTUsImV4cCI6MjA4MDc2Mzk5NX0.TT5KPhwDHD9cwcpEduWxCwPtT0en72H4V3EZyKkX-KE"
);
// ===========================
// μ „μ—­ λ³€μˆ˜
// ===========================
let currentMelody = [];
let recorder;
let audioChunks = [];
let isRecording = false;
// ===========================
// λ‚œμ΄λ„ 선택 β†’ 문제 생성
// ===========================
function selectLevel(level) {
document.getElementById("status").innerText = `μƒνƒœ: λ‚œμ΄λ„ ${level} 문제 생성 쀑...`;
currentMelody = generateMelody(level);
renderMelody(currentMelody);
document.getElementById("status").innerText = `μƒνƒœ: λ‚œμ΄λ„ ${level} 문제 μ€€λΉ„ μ™„λ£Œ`;
}
// ===========================
// λ‚œμ΄λ„λ³„ λ©œλ‘œλ”” 생성기
// ===========================
function generateMelody(level) {
const notes = [];
let scaleLow = 60; // C4
let scaleHigh = 72; // C5
let allowDotted = false;
let allowSkip = false;
if (level === 1) {
scaleHigh = 67; // G4
allowDotted = false;
allowSkip = false;
}
if (level === 2) {
scaleHigh = 69; // A4
allowDotted = true;
allowSkip = true;
}
if (level === 3) {
scaleHigh = 72; // C5
allowDotted = true;
allowSkip = true;
}
let totalBeats = 8; // 2λ§ˆλ””(4/4)
while (totalBeats > 0) {
let duration = 1; // κΈ°λ³Έ 4λΆ„μŒν‘œ
if (allowDotted && Math.random() < 0.3) {
duration = 1.5; // 점4λΆ„μŒν‘œ
} else if (Math.random() < 0.4) {
duration = 0.5; // 8λΆ„μŒν‘œ
}
if (duration > totalBeats) continue;
let pitch = Math.floor(Math.random() * (scaleHigh - scaleLow + 1)) + scaleLow;
if (!allowSkip && notes.length > 0) {
let prev = notes[notes.length - 1].pitch;
if (Math.abs(prev - pitch) > 3) continue;
}
notes.push({ pitch, duration });
totalBeats -= duration;
}
return notes;
}
// ===========================
// 악보 λ Œλ”λ§ (VexFlow)
// ===========================
function renderMelody(melody) {
const VF = Vex.Flow;
const canvas = document.getElementById("sheetCanvas");
const renderer = new VF.Renderer(canvas, VF.Renderer.Backends.CANVAS);
renderer.resize(750, 200);
const ctx = renderer.getContext();
ctx.clearRect(0, 0, 750, 200);
const stave = new VF.Stave(10, 20, 720);
stave.addClef("treble").addTimeSignature("4/4");
stave.setContext(ctx).draw();
const notes = melody.map(n => {
let dur = "q"; // 4λΆ„μŒν‘œ
if (n.duration === 0.5) dur = "8";
if (n.duration === 1.5) dur = "dq";
return new VF.StaveNote({
clef: "treble",
keys: [midiToNoteName(n.pitch)],
duration: dur
});
});
notes.forEach((note, i) => {
if (melody[i].duration === 1.5) {
note.addDotToAll();
}
});
VF.Formatter.FormatAndDraw(ctx, stave, notes);
}
// MIDI β†’ μŒμ΄λ¦„ λ³€ν™˜
function midiToNoteName(midi) {
const names = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"];
const name = names[midi % 12];
const octave = Math.floor(midi / 12) - 1;
return `${name}/${octave}`;
}
// ===========================
// λ…ΉμŒ κΈ°λŠ₯
// ===========================
async function startRecording() {
const status = document.getElementById("status");
status.innerText = "μƒνƒœ: λ…ΉμŒ 쀑...";
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
recorder = new MediaRecorder(stream);
audioChunks = [];
recorder.ondataavailable = e => audioChunks.push(e.data);
recorder.onstop = sendAudio;
recorder.start();
isRecording = true;
}
function stopRecording() {
if (recorder && isRecording) {
recorder.stop();
isRecording = false;
document.getElementById("status").innerText = "μƒνƒœ: λ…ΉμŒ μ’…λ£Œ, μ—…λ‘œλ“œ 쀑...";
}
}
// ===========================
// Supabase μ—…λ‘œλ“œ
// ===========================
async function sendAudio() {
const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
const fileName = `rec_${Date.now()}.webm`;
const status = document.getElementById("status");
// 1) Supabase μ—…λ‘œλ“œ
const { data, error } = await supabaseClient.storage
.from("recordings")
.upload(fileName, audioBlob, { contentType: "audio/webm" });
if (error) {
status.innerText = "μƒνƒœ: μ—…λ‘œλ“œ μ‹€νŒ¨";
return;
}
const publicURL = `https://kceyrxbzlzfrzonjmkdu.supabase.co/storage/v1/object/public/recordings/${fileName}`;
status.innerText = "μƒνƒœ: 뢄석 쀑...";
// 2) FastAPI 뢄석 μš”μ²­ (ꡬ글 런 μ£Όμ†Œλ‘œ μžλ™ μ—°κ²°λ˜λ„λ‘ μƒλŒ€ 경둜 '/' μ‚¬μš©)
// 2) FastAPI 뢄석 μš”μ²­
try {
const res = await fetch(window.location.origin + "/analyze", { // 경둜 μˆ˜μ •
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: publicURL,
notes: currentMelody.map(n => ({
midi: n.pitch,
quarterLength: n.duration
})) // app.pyκ°€ 이해할 수 μžˆλŠ” ν˜•μ‹μœΌλ‘œ λ³€ν™˜ν•΄μ„œ 전달
})
});
const result = await res.json();
// 3) κ²°κ³Ό λŒ€μ‹œλ³΄λ“œ μ‹œκ°ν™” μ‹€ν–‰
showResultDashboard(result);
status.innerText = "μƒνƒœ: 평가 μ™„λ£Œ";
} catch (e) {
status.innerText = "μƒνƒœ: 뢄석 μ„œλ²„ μ—°κ²° μ‹€νŒ¨";
}
}
// κ²°κ³Όλ₯Ό 화면에 λΏŒλ €μ£ΌλŠ” ν•¨μˆ˜
function showResultDashboard(data) {
// λŒ€μ‹œλ³΄λ“œ λ‚˜νƒ€λ‚΄κΈ°
document.getElementById("resultDashboard").style.display = "block";
// 점수 숫자 μ±„μš°κΈ°
document.getElementById("totalScore").innerText = data.total_score;
document.getElementById("pScore").innerText = data.pitch_score;
document.getElementById("rScore").innerText = data.rhythm_score;
// κ·Έλž˜ν”„ 그리기
drawPitchGraph("pitchCanvas", data.pitch_errors);
drawRhythmGraph("rhythmCanvas", data.rhythm_errors, 80); // BPM 80 κΈ°μ€€
// 3쀄 ν”Όλ“œλ°± μ—…λ°μ΄νŠΈ
updateFeedback(data.pitch_score, data.rhythm_score, data.pitch_errors, data.rhythm_errors);
}
// μŒμ • κ·Έλž˜ν”„ 그리기
function drawPitchGraph(canvasId, centsArray) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext("2d");
const W = canvas.width = canvas.parentElement.clientWidth;
const H = canvas.height = 120;
const centerY = H / 2;
ctx.clearRect(0, 0, W, H);
// κ°€μ΄λ“œ μ˜μ—­ (Β±25 cent)
ctx.fillStyle = "rgba(56, 189, 248, 0.1)";
ctx.fillRect(0, centerY - 20, W, 40);
// 데이터 μ„  그리기
ctx.strokeStyle = "#38bdf8";
ctx.lineWidth = 2;
ctx.beginPath();
const step = W / (centsArray.length - 1);
centsArray.forEach((cent, i) => {
const x = i * step;
const y = centerY - (cent * 2); // μ¦ν­ν•΄μ„œ λ³΄μ—¬μ€Œ
if(i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
});
ctx.stroke();
}
// 리듬 κ·Έλž˜ν”„ 그리기
function drawRhythmGraph(canvasId, msErrors, bpm) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext("2d");
const W = canvas.width = canvas.parentElement.clientWidth;
const H = canvas.height = 100;
const centerY = H / 2;
ctx.clearRect(0, 0, W, H);
ctx.strokeStyle = "rgba(255,255,255,0.2)";
ctx.beginPath(); ctx.moveTo(0, centerY); ctx.lineTo(W, centerY); ctx.stroke();
const step = W / (msErrors.length + 1);
msErrors.forEach((ms, i) => {
const x = (i + 1) * step;
const y = centerY - (ms / 2);
ctx.fillStyle = Math.abs(ms) < 50 ? "#4ade80" : "#fb7185";
ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI*2); ctx.fill();
});
}
// 3쀄 ν”Όλ“œλ°± 둜직
function updateFeedback(p, r, pErr, rErr) {
const f1 = document.getElementById("f1");
const f2 = document.getElementById("f2");
const f3 = document.getElementById("f3");
f1.innerText = p >= 40 ? "β€’ μŒμ •μ΄ 맀우 μ •ν™•ν•©λ‹ˆλ‹€. μ•ˆμ •μ μΈ λ°œμ„±μ„ μœ μ§€ν•˜κ³  κ³„μ‹œλ„€μš”." : "β€’ νŠΉμ • κ΅¬κ°„μ—μ„œ μŒμ •μ΄ λΆˆμ•ˆμ •ν•©λ‹ˆλ‹€. 호흑의 μ••λ ₯을 μΌμ •ν•˜κ²Œ μœ μ§€ν•΄ λ³΄μ„Έμš”.";
f2.innerText = r >= 40 ? "β€’ 리듬감이 ν›Œλ₯­ν•©λ‹ˆλ‹€. λΉ„νŠΈμ˜ 쀑심을 μ •ν™•νžˆ νƒ€κ²©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€." : "β€’ λ°•μžκ°€ 쑰금 μ„œλ‘λ₯΄κ±°λ‚˜(Early) λ°€λ¦¬λŠ”(Late) κ²½ν–₯이 λ³΄μž…λ‹ˆλ‹€.";
f3.innerText = "β€’ μ „μ²΄μ μœΌλ‘œ 쒋은 μ‹œλ„μ˜€μŠ΅λ‹ˆλ‹€. 였늘 λΆ€μ‘±ν–ˆλ˜ ꡬ간을 λ‹€μ‹œ ν•œ 번 μ§‘μ€‘ν•΄μ„œ μ—°μŠ΅ν•΄λ³΄μ„Έμš”!";
}