Spaces:
Running
Running
| // =========================== | |
| // 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 = "β’ μ 체μ μΌλ‘ μ’μ μλμμ΅λλ€. μ€λ λΆμ‘±νλ ꡬκ°μ λ€μ ν λ² μ§μ€ν΄μ μ°μ΅ν΄λ³΄μΈμ!"; | |
| } |