Dr. J's Classical Guitar Practice Suite
Portal App

Classical Guitar Practice Hub

://drjclassicalguitar.com

Today 0m
Week Total 0m
Streak 0d 🔥

Pro Metronome

Manual Mode

Increases rhythm speed automatically by +2 BPM every 30 seconds to optimize velocity adaptation without microtension.

Performance Deadlines

Target Composition / Event Target Date Add Milestone Saved Practice Workflows ☕ Daily Warm-Up (20 mins) Giuliani Arpeggios & Segovia Linear Scales 🏛️ Repertoire Polish (45 mins) Old Repertoire Maintenance & Phrasing Stamina 🎓 Deep Practice Cycle (90 mins) Isolated Friction Points & Speed Escalation cycles Active Practice Tracker Pedagogical Target Focus Techniques (Arpeggios, Slurs, Tremolo) Scales (Segovia, Diatonic patterns, Speed) New Repertoire (Sight-reading, Memorization) Old Repertoire (Polishing, Memory Refresh) Sight Reading & Applied Analysis 00:00:00 Start Reset Or Overwrite Duration Manually Max Structural Clean Tempo (BPM) Microjournaling (Breakthroughs or Friction measures) Archive Session to Log Ledger Technique Checklist Reset All Historian Charts Day-by-Day Focus Density (Mins) Pedagogical Split (Balance Check) History Ledger CSV Email Dr. J Studio Destination: Date Focus Vol BPM Reset Profile Ledger // System Data State Context let timerInterval = null; let elapsedSeconds = 0; let isRunning = false; let practiceData = []; let milestones = []; // Audio Metronome Subsystem Context Thread let audioCtx = null; let metroInterval = null; let isMetroPlaying = false; let currentBeat = 0; let nextClickTime = 0.0; const lookahead = 25.0; const scheduleAheadTime = 0.1; // Speed Trainer Automation Context Thread let speedTrainerInterval = null; let isSpeedTrainerActive = false; // Chart Registry Instantiations let dailyChartInst = null; let categoryChartInst = null; // Life Cycle Bootloader Hook window.addEventListener('DOMContentLoaded', () => { // Synchronize Icon Vectors lucide.createIcons(); // Load Practice Data Ledger Cache const savedData = localStorage.getItem('drj_guitar_practice_logs'); if (savedData) { practiceData = JSON.parse(savedData); } else { practiceData = [ { date: '2026-10-25', type: 'Techniques', minutes: 20, bpm: 84, notes: 'Giuliani Right Hand variations' }, { date: '2026-10-26', type: 'Scales', minutes: 15, bpm: 92, notes: 'Segovia C major' }, { date: '2026-10-27', type: 'New Repertoire', minutes: 45, bpm: 60, notes: 'Learning Bach Chaconne' }, { date: '2026-10-28', type: 'Old Repertoire', minutes: 30, bpm: 110, notes: 'Villa-Lobos Prelude 1' }, { date: '2026-10-30', type: 'Techniques', minutes: 25, bpm: 90, notes: 'Left hand independence' } ]; localStorage.setItem('drj_guitar_practice_logs', JSON.stringify(practiceData)); } // Sync configured target configuration address const savedEmail = localStorage.getItem('drj_teacher_email'); if (savedEmail) { document.getElementById('teacherEmail').value = savedEmail; } // Synchronize Milestone Timelines const savedMilestones = localStorage.getItem('drj_guitar_milestones'); if (savedMilestones) { milestones = JSON.parse(savedMilestones); } else { milestones = [ { id: 1, title: "Spring Recital Assessment Run", date: "2027-04-15" }, { id: 2, title: "Studio Performance Masterclass Critique", date: "2026-12-05" } ]; localStorage.setItem('drj_guitar_milestones', JSON.stringify(milestones)); } initCharts(); updateUI(); renderMilestones(); updateVisualIndicators(); }); // Event hooks monitoring config target addresses document.getElementById('teacherEmail').addEventListener('change', (e) => { localStorage.setItem('drj_teacher_email', e.target.value); }); // CSV DATA SERIALIZATION FRAMEWORK function downloadCSV() { if (practiceData.length === 0) { alert("The ledger registry profile is empty. Log a session to build a profile record."); return; } let csvRows = ["Date,Category,Duration_Minutes,Metronome_BPM,Session_Progress_Notes"]; practiceData.forEach(item => { let quoteCleanedNotes = item.notes.replace(/"/g, '""'); csvRows.push("${item.date}","${item.type}",${item.minutes},"${item.bpm}","${quoteCleanedNotes}"); }); const blob = new Blob([csvRows.join("\n")], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const hiddenAnchor = document.createElement("a"); hiddenAnchor.setAttribute("href", url); hiddenAnchor.setAttribute("download", "DrJ_Studio_Guitar_Practice_History.csv"); document.body.appendChild(hiddenAnchor); hiddenAnchor.click(); document.body.removeChild(hiddenAnchor); } // MAIL DISTRIBUTION HYPERLINK FORMATTER function emailHistory() { if (practiceData.length === 0) { alert("The ledger configuration registry history is empty."); return; } const activeDestination = document.getElementById('teacherEmail').value || 'lessons@drjclassicalguitar.com'; const subjectHeader = encodeURIComponent("Classical Guitar Studio Practice Log Update"); let messageContext = "Hello Dr. J,\n\nHere is my recent tactical guitar practice session updates summary submitted via the studio dashboard module:\n\n"; messageContext += "========================================================\n"; messageContext += "DATE | TYPE METRIC | DURATION | TARGET CEP | NOTES\n"; messageContext += "========================================================\n"; const reverseTruncatedLogs = [...practiceData].reverse().slice(0, 15); reverseTruncatedLogs.forEach(i => { let padType = i.type.padEnd(14, ' '); let padVol = (i.minutes + "m").padEnd(8, ' '); let padBpm = (i.bpm + " bpm").padEnd(10, ' '); messageContext += ${i.date} | ${padType} | ${padVol} | ${padBpm} | ${i.notes}\n; }); if (practiceData.length > 15) { messageContext += \n...and ${practiceData.length - 15} additional historic sequence tracks archived on device caching layers.; } messageContext += "\n========================================================\n\nSent from the Guitar Practice Suite App."; window.location.href = mailto:${activeDestination}?subject=${subjectHeader}&body=${encodeURIComponent(messageContext)}; } // AUDIO CONTEXT CORE CLOCK MODULES function initAudio() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } } function adjustBpm(delta) { const el = document.getElementById('metroBpm'); let updated = parseInt(el.value) + delta; updated = Math.max(40, Math.min(250, updated)); el.value = updated; updateMetroTempo(); } function updateMetroTempo() { document.getElementById('metronomeBpm').value = document.getElementById('metroBpm').value; } function toggleSpeedTrainerMode() { const check = document.getElementById('speedTrainerToggle'); const status = document.getElementById('trainerStatus'); isSpeedTrainerActive = check.checked; if (isSpeedTrainerActive) { status.innerText = 'Trainer Armed'; status.className = "text-[11px] font-semibold tracking-wide uppercase px-2 py-0.5 rounded-full bg-amber-50 text-amber-700 border border-amber-200 animate-pulse"; } else { status.innerText = 'Manual Mode'; status.className = "text-[11px] font-medium tracking-wide uppercase px-2 py-0.5 rounded-full bg-stone-100 text-stone-500 border border-stone-200"; clearInterval(speedTrainerInterval); } } function startSpeedTrainerTimer() { if (!isSpeedTrainerActive) return; document.getElementById('trainerStatus').innerText = 'Trainer Live 🚀'; speedTrainerInterval = setInterval(() => { const bpmBox = document.getElementById('metroBpm'); let cur = parseInt(bpmBox.value); if (cur < 250) { bpmBox.value = cur + 2; updateMetroTempo(); } }, 30000); } function stopSpeedTrainerTimer() { clearInterval(speedTrainerInterval); toggleSpeedTrainerMode(); } function scheduler() { while (nextClickTime < audioCtx.currentTime + scheduleAheadTime) { scheduleClick(currentBeat, nextClickTime); advanceBeat(); } } function scheduleClick(beatNumber, time) { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); // Frequency assignments contrasting metric subdivisions osc.frequency.setValueAtTime(beatNumber === 0 ? 987.77 : 523.25, time); gain.gain.setValueAtTime(0.6, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.04); osc.start(time); osc.stop(time + 0.04); const totalBeats = parseInt(document.getElementById('metroBeats').value); setTimeout(() => { triggerVisualFlash(beatNumber, totalBeats); }, (time - audioCtx.currentTime) * 1000); } function advanceBeat() { const speedMetric = parseInt(document.getElementById('metroBpm').value); nextClickTime += (60.0 / speedMetric); currentBeat = (currentBeat + 1) % parseInt(document.getElementById('metroBeats').value); } function toggleMetronome() { initAudio(); const btn = document.getElementById('metroPlayBtn'); if (!isMetroPlaying) { if (audioCtx.state === 'suspended') { audioCtx.resume(); } isMetroPlaying = true; currentBeat = 0; nextClickTime = audioCtx.currentTime + 0.02; metroInterval = setInterval(scheduler, lookahead); btn.innerHTML = Stop Metronome; btn.className = "w-full bg-gradient-to-r from-red-700 to-red-800 hover:from-red-600 hover:to-red-700 text-white font-semibold p-3.5 rounded-2xl transition duration-300 shadow-md tracking-wide text-sm flex items-center justify-center gap-2"; lucide.createIcons(); startSpeedTrainerTimer(); } else { isMetroPlaying = false; clearInterval(metroInterval); btn.innerHTML = Start Metronome; btn.className = "w-full bg-gradient-to-r from-amber-800 to-amber-900 hover:from-amber-700 hover:to-amber-800 text-white font-semibold p-3.5 rounded-2xl transition duration-300 shadow-md tracking-wide text-sm flex items-center justify-center gap-2"; lucide.createIcons(); clearFlashIndicators(); stopSpeedTrainerTimer(); } } function updateVisualIndicators() { const limit = parseInt(document.getElementById('metroBeats').value); for (let i = 1; i <= 4; i++) { document.getElementById(metroFlash${i}).style.display = (i <= limit) ? 'block' : 'none'; } } document.getElementById('metroBeats').addEventListener('change', updateVisualIndicators); function triggerVisualFlash(beatIndex, total) { clearFlashIndicators(); const node = document.getElementById(metroFlash${beatIndex + 1}); if (node) { if (beatIndex === 0) { node.className = "w-4 h-4 rounded-full bg-amber-500 shadow-md shadow-amber-500/50 border border-amber-400 scale-125 transition-all duration-75"; } else { node.className = "w-4 h-4 rounded-full bg-stone-700 shadow-sm border border-stone-600 scale-110 transition-all duration-75"; } } } function clearFlashIndicators() { for (let i = 1; i <= 4; i++) { document.getElementById(metroFlash${i}).className = "w-4 h-4 rounded-full bg-stone-300 border border-stone-200 shadow-inner transition-all duration-200"; } } // PERFORMANCE MATRIX DATA PARSERS function addMilestone() { const tBox = document.getElementById('eventTitle'); const dBox = document.getElementById('eventDate'); if (!tBox.value || !dBox.value) { alert("Please add composition description tags and target dates."); return; } milestones.push({ id: Date.now(), title: tBox.value, date: dBox.value }); localStorage.setItem('drj_guitar_milestones', JSON.stringify(milestones)); tBox.value = ''; dBox.value = ''; renderMilestones(); } function deleteMilestone(id) { milestones = milestones.filter(m => m.id !== id); localStorage.setItem('drj_guitar_milestones', JSON.stringify(milestones)); renderMilestones(); } function renderMilestones() { const root = document.getElementById('milestoneContainer'); root.innerHTML = ''; if (milestones.length === 0) { root.innerHTML =

No active target schedules configured.

; return; } const sorted = [...milestones].sort((a,b) => new Date(a.date) - new Date(b.date)); sorted.forEach(m => { const targetTime = new Date(m.date + 'T00:00:00'); const today = new Date(); today.setHours(0,0,0,0); const remainderDays = Math.ceil((targetTime.getTime() - today.getTime()) / (1000 * 3600 * 24)); let badge = ''; if (remainderDays < 0) { badge = Archive; } else if (remainderDays <= 14) { badge = ${remainderDays} days out; } else { badge = ${remainderDays} days left; } const div = document.createElement('div'); div.className = "py-2.5 flex items-center justify-between gap-2 first:pt-0"; div.innerHTML =
${m.title} ${m.date}
${badge}
; root.appendChild(div); }); lucide.createIcons(); } // CHRONOMETER ENGINE CONTROL SETS function toggleTimer() { const btn = document.getElementById('startBtn'); if (!isRunning) { isRunning = true; btn.innerHTML = Pause; btn.className = "px-5 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-xl text-xs font-semibold shadow-sm transition duration-300 flex items-center gap-1"; timerInterval = setInterval(() => { elapsedSeconds++; updateTimerDisplay(); }, 1000); } else { isRunning = false; btn.innerHTML = Start; btn.className = "px-5 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-semibold shadow-sm transition duration-300 flex items-center gap-1"; clearInterval(timerInterval); } lucide.createIcons(); } function resetTimer() { isRunning = false; clearInterval(timerInterval); elapsedSeconds = 0; updateTimerDisplay(); const btn = document.getElementById('startBtn'); btn.innerHTML = Start; btn.className = "px-5 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-semibold shadow-sm transition duration-300 flex items-center gap-1"; lucide.createIcons(); } function updateTimerDisplay() { const hrs = String(Math.floor(elapsedSeconds / 3600)).padStart(2, '0'); const mins = String(Math.floor((elapsedSeconds % 3600) / 60)).padStart(2, '0'); const secs = String(elapsedSeconds % 60).padStart(2, '0'); document.getElementById('timerDisplay').innerText = ${hrs}:${mins}:${secs}; } function loadRoutine(routineKey) { const typeSelect = document.getElementById('practiceType'); const notesArea = document.getElementById('practiceNotes'); const manualIn = document.getElementById('manualMinutes'); if (routineKey === 'warmup') { typeSelect.value = 'Techniques'; manualIn.value = 20; notesArea.value = '[Daily Warm-Up] Microalignment check: focus entirely on stroke follow-through relaxation.'; } else if (routineKey === 'repertoire') { typeSelect.value = 'Old Repertoire'; manualIn.value = 45; notesArea.value = '[Repertoire Maintenance] Structural evaluation: check dynamic contour stability over complete performance cycles.'; } else if (routineKey === 'complete') { typeSelect.value = 'New Repertoire'; manualIn.value = 90; notesArea.value = '[Deep Practice Cycle] Analytical isolated breakdown targeting localized synchronization thresholds.'; } } function saveSession() { let loggedMinutes = 0; const manualVal = parseInt(document.getElementById('manualMinutes').value); if (manualVal && manualVal > 0) { loggedMinutes = manualVal; } else if (elapsedSeconds > 0) { loggedMinutes = Math.max(1, Math.round(elapsedSeconds / 60)); } else { alert("Chronometer tracking data is empty. Initialize timer loops before logging."); return; } const activeLog = { date: new Date().toISOString().split('T')[0], type: document.getElementById('practiceType').value, minutes: loggedMinutes, bpm: parseInt(document.getElementById('metronomeBpm').value) || '-', notes: document.getElementById('practiceNotes').value || 'Session logging complete.' }; practiceData.push(activeLog); localStorage.setItem('drj_guitar_practice_logs', JSON.stringify(practiceData)); resetTimer(); document.getElementById('manualMinutes').value = ''; document.getElementById('metronomeBpm').value = ''; document.getElementById('practiceNotes').value = ''; updateUI(); updateCharts(); } function clearLogs() { if (confirm("This action permanently flushes storage configuration tracking memory. Proceed?")) { practiceData = []; localStorage.removeItem('drj_guitar_practice_logs'); updateUI(); updateCharts(); } } function resetChecklist() { document.querySelectorAll('.tech-check').forEach(box => box.checked = false); } function updateUI() { const rightNow = new Date().toISOString().split('T')[0]; let rawToday = 0; let rawWeek = 0; const thresholdWeek = new Date(); thresholdWeek.setDate(thresholdWeek.getDate() - 7); practiceData.forEach(i => { if (i.date === rightNow) rawToday += i.minutes; if (new Date(i.date) >= thresholdWeek) rawWeek += i.minutes; }); document.getElementById('todayTotal').innerText = ${rawToday}m; document.getElementById('weekTotal').innerText = ${rawWeek}m; document.getElementById('streakCount').innerHTML = ${[...new Set(practiceData.map(i => i.date))].length}d 🔥; const tbody = document.getElementById('ledgerBody'); tbody.innerHTML = ''; [...practiceData].reverse().forEach(item => { const tr = document.createElement('tr'); tr.className = "hover:bg-stone-50 transition border-b border-stone-100 last:border-0"; tr.innerHTML = ` ${item.date.slice(5)} ${item.type.replace(' Repertoire','')} ${item.minutes}m ${item.bpm} `; tbody.appendChild(tr); }); } function initCharts() { const ctxDaily = document.getElementById('dailyChart').getContext('2d'); dailyChartInst = new Chart(ctxDaily, { type: 'line', data: { labels: [], datasets: [{ label: 'Minutes', data: [], borderColor: '#78350f', borderWidth: 2.5, pointBackgroundColor: '#78350f', backgroundColor: 'rgba(120, 53, 15, 0.04)', fill: true, tension: 0.35 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { grid: { display: false } }, y: { grid: { color: '#f3f0ea' }, border: { dash: [5, 5] }, min: 0 } }, plugins: { legend: { display: false } } } }); const ctxCat = document.getElementById('categoryChart').getContext('2d'); categoryChartInst = new Chart(ctxCat, { type: 'doughnut', data: { labels: ['Techniques', 'Scales', 'New Repertoire', 'Old Repertoire', 'Sight Reading'], datasets: [{ data: [], backgroundColor: ['#78350f', '#b45309', '#d97706', '#f59e0b', '#a8a29e'], borderStrokeWidth: 0, hoverOffset: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, cutout: '75%' } }); updateCharts(); } function updateCharts() { if (!dailyChartInst || !categoryChartInst) return; const chronologicalTail = [...practiceData].sort((a,b) => new Date(a.date) - new Date(b.date)).slice(-7); dailyChartInst.data.labels = chronologicalTail.map(l => l.date.slice(5)); dailyChartInst.data.datasets[0].data = chronologicalTail.map(l => l.minutes); dailyChartInst.update(); const matrix = { 'Techniques': 0, 'Scales': 0, 'New Repertoire': 0, 'Old Repertoire': 0, 'Sight Reading': 0 }; practiceData.forEach(item => { if (matrix[item.type] !== undefined) matrix[item.type] += item.minutes; }); categoryChartInst.data.datasets[0].data = [matrix['Techniques'], matrix['Scales'], matrix['New Repertoire'], matrix['Old Repertoire'], matrix['Sight Reading']]; categoryChartInst.update(); }