
CLASES DE PIANO PARTICULARES
MAKSIM BADENAS
<!doctype html>
<!--
Página web de enseñanza de piano (single-file)
- HTML/CSS/JS en un archivo
- Teclado interactivo (2 octavas) usando WebAudio
- Lecciones, ejercicios, reproductor de ejemplo, y pequeño quiz
- Guarda progreso en localStorage
Autor: ChatGPT
-->
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aprende Piano — Lecciones Interactivas</title>
<style>
:root{
--bg:#0f1724; /* oscuro */
--card:#0b1220;
--accent:#7c3aed;
--muted:#94a3b8;
--white:#e6eef8;
}
*{box-sizing:border-box;font-family:Inter, system-ui, -apple-system, Roboto, 'Segoe UI', Arial;}
body{margin:0;background:linear-gradient(180deg,var(--bg),#071022);color:var(--white);-webkit-font-smoothing:antialiased}
header{display:flex;align-items:center;gap:16px;padding:20px;border-bottom:1px solid rgba(255,255,255,0.03)}
header h1{margin:0;font-size:20px}
.container{max-width:1100px;margin:28px auto;padding:0 16px}
.grid{display:grid;grid-template-columns:1fr 360px;gap:20px}
.card{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));padding:18px;border-radius:12px;box-shadow:0 6px 24px rgba(2,6,23,0.6)}
nav .btn{display:inline-block;padding:8px 12px;border-radius:8px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.02);cursor:pointer}
/* Teclado */
.keyboard{user-select:none;margin-top:12px;display:flex;height:180px;position:relative}
.key{flex:1;border:1px solid rgba(0,0,0,0.25);margin:0 1px;border-radius:6px;display:flex;align-items:flex-end;justify-content:center;position:relative;background:linear-gradient(180deg,#fff,#f2f2f2);color:#111;font-weight:600;box-shadow:0 6px 18px rgba(2,6,23,0.6)}
.key.black{background:linear-gradient(180deg,#111,#0b0b0b);color:#fff;flex:0 0 56px;margin:0 -28px 0 -28px;height:110px;z-index:2;border-radius:0 0 6px 6px}
.key span{padding:6px;font-size:12px}
.keyboard-wrapper{position:relative}
/* Layout small screens */
@media (max-width:900px){.grid{grid-template-columns:1fr}.keyboard{height:140px}}
button.primary{background:linear-gradient(90deg,var(--accent),#4c1d95);color:white;border:none;padding:10px 12px;border-radius:10px;cursor:pointer}
.lesson-list{display:flex;flex-direction:column;gap:10px}
.lesson{padding:10px;border-radius:8px;background:rgba(255,255,255,0.02);cursor:pointer}
.lesson.selected{outline:2px solid rgba(124,58,237,0.3)}
.controls{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
.note-display{font-weight:700;font-size:18px;color:var(--accent)}
.quiz .option{padding:8px;border-radius:8px;background:rgba(255,255,255,0.02);cursor:pointer;margin-top:8px}
footer{padding:20px;text-align:center;color:var(--muted)}
/* small legend on keys */
.key .label{background:rgba(0,0,0,0.06);padding:4px;border-radius:6px;margin-bottom:8px;font-size:12px}
.meter{height:8px;background:rgba(255,255,255,0.03);border-radius:20px;overflow:hidden}
.meter > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),#2ea4f2);width:0%}
</style>
</head>
<body>
<header>
<h1>Aprende Piano</h1>
<div style="margin-left:auto;display:flex;gap:8px;align-items:center">
<div class="note-display" id="currentNote">—</div>
<button class="btn" id="toggleSound">🔊 Sonido ON</button>
</div>
</header>
<main class="container">
<div class="grid">
<section>
<div class="card">
<h2>Teclado interactivo</h2>
<p style="color:var(--muted);margin:6px 0 12px">Haz clic en las teclas o usa el teclado (a, s, d, f, g, h, j para la escala blanca). Presiona Shift para las negras.</p>
<div class="keyboard-wrapper">
<div id="keyboard" class="keyboard" aria-label="Teclado virtual">
<!-- Keys created por JS -->
</div>
</div>
<div class="controls">
<button class="primary" id="playC">Reproducir C Mayor</button>
<button class="btn" id="playExercise">Ejercicio: Do-Re-Mi</button>
<button class="btn" id="recordBtn">🎙 Grabar práctica</button>
<button class="btn" id="downloadBtn">⬇ Descargar grabación</button>
</div>
<div style="margin-top:12px">
<div style="display:flex;gap:8px;align-items:center">
<div style="flex:1">
<small style="color:var(--muted)">Progreso lección actual</small>
<div class="meter" aria-hidden="true"><i id="progressBar"></i></div>
</div>
<div id="progressPct" style="width:60px;text-align:right">0%</div>
</div>
</div>
</div>
<div class="card" style="margin-top:18px">
<h3>Lecciones</h3>
<div class="lesson-list" id="lessons">
<!-- Lista por JS -->
</div>
</div>
<div class="card" style="margin-top:18px">
<h3>Mini-quiz: Reconoce la nota</h3>
<div class="quiz" id="quiz">
<p style="color:var(--muted)">Escucha la nota y elige la opción correcta.</p>
<div style="display:flex;gap:8px;align-items:center">
<button class="btn" id="quizPlay">🔊 Reproducir nota</button>
<div id="quizResult" style="margin-left:auto;color:var(--muted)"></div>
</div>
<div id="quizOptions" style="margin-top:10px"></div>
</div>
</div>
</section>
<aside>
<div class="card">
<h3>Información</h3>
<p style="color:var(--muted)">Sigue estas lecciones para aprender posición de manos, lectura de notas y ritmos básicos. Guarda tu progreso en el navegador.</p>
<hr style="border:none;border-top:1px solid rgba(255,255,255,0.03);margin:12px 0"/>
<h4>Control de volumen</h4>
<input id="volume" type="range" min="0" max="1" step="0.01" value="0.6">
<h4 style="margin-top:12px">Configuración</h4>
<label><input type="checkbox" id="showNoteNames" checked> Mostrar nombres en teclas</label>
<div style="margin-top:12px">
<small style="color:var(--muted)">Progreso global</small>
<div class="meter" style="margin-top:6px"><i id="globalProgress"></i></div>
</div>
</div>
<div class="card" style="margin-top:18px">
<h3>Tu cuenta</h3>
<p style="color:var(--muted)">Guardado local: <span id="savedAt">—</span></p>
<button class="btn" id="resetProgress">Restablecer progreso</button>
</div>
</aside>
</div>
</main>
<footer>
<small>Hecho con ❤️ para aprender piano. Modifica las lecciones según tus necesidades.</small>
</footer>
<script>
// ====== Datos y utilidades ======
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioCtx();
let masterGain = ctx.createGain(); masterGain.gain.value = 0.6; masterGain.connect(ctx.destination);
const noteFrequencies = (function(){
// A4 = 440Hz. Creamos mapa para 2 octavas: C4..B5
const map = {};
const A4 = 440; const A4_index = 57; // midi index for A4 (C0=0) we're using formula
function midiToFreq(m){return 440 * Math.pow(2,(m-69)/12)}
const names = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
for(let octave=4; octave<=5; octave++){
for(let i=0;i<12;i++){
const name = names[i]+octave;
const midi = (octave+1)*12 + i; // simple midi math
map[name] = midiToFreq(midi);
}
}
return map;
})();
const whiteKeysOrder = ['C4','D4','E4','F4','G4','A4','B4','C5','D5','E5','F5','G5','A5','B5'];
const blackKeysOrder = ['C#4','D#4',null,'F#4','G#4','A#4',null,'C#5','D#5',null,'F#5','G#5','A#5',null];
// ====== DOM refs ======
const keyboardEl = document.getElementById('keyboard');
const currentNoteEl = document.getElementById('currentNote');
const toggleSound = document.getElementById('toggleSound');
const volume = document.getElementById('volume');
const showNoteNames = document.getElementById('showNoteNames');
const lessonsEl = document.getElementById('lessons');
const progressBar = document.getElementById('progressBar');
const progressPct = document.getElementById('progressPct');
const globalProgress = document.getElementById('globalProgress');
const savedAt = document.getElementById('savedAt');
// Estado
let soundOn = true;
let recording = false;
let recorderChunks = [];
let selectedLesson = 0;
let lessonProgress = {}; // stored per lesson
// Lecciones simples
const lessons = [
{title:'Introducción: Teclas y notas', content:'Aprende la disposición de las teclas: C D E F G A B. Usa el teclado virtual.'},
{title:'Posición de las manos', content:'Pulgar en C, dedos 1..5. Ejercicio de digitación: C-D-E-F-G.'},
{title:'Lectura: Notas en el pentagrama (básico)', content:'Relaciona la posición en pentagrama con las teclas.'},
{title:'Ritmo: Negra y blanca', content:'Practica tocar al compás con un metrónomo sencillo.'}
];
// ====== Construir teclado ======
function buildKeyboard(){
keyboardEl.innerHTML = '';
// We'll create a flexible layout mixing whites and positioned blacks
whiteKeysOrder.forEach((note,i)=>{
const k = document.createElement('div');
k.className = 'key white';
k.dataset.note = note;
k.tabIndex = 0;
const label = document.createElement('span');
label.className='label';
label.textContent = showNoteNames.checked ? note : '';
k.appendChild(label);
const small = document.createElement('span');
small.textContent = note.replace(/[0-9]/g,'');
k.appendChild(small);
keyboardEl.appendChild(k);
// add black key if exists
const b = blackKeysOrder[i];
if(b){
const kb = document.createElement('div');
kb.className = 'key black';
kb.dataset.note = b;
kb.tabIndex = 0;
const blabel = document.createElement('span'); blabel.className='label'; blabel.textContent = showNoteNames.checked ? b : '';
kb.appendChild(blabel);
keyboardEl.appendChild(kb);
}
});
// attach events
document.querySelectorAll('#keyboard .key').forEach(el=>{
el.addEventListener('mousedown',()=>playNote(el.dataset.note));
el.addEventListener('touchstart',(e)=>{e.preventDefault();playNote(el.dataset.note)}, {passive:false});
el.addEventListener('keydown',(e)=>{if(e.key==='Enter') playNote(el.dataset.note)});
});
}
buildKeyboard();
showNoteNames.addEventListener('change',buildKeyboard);
// ====== Reproducción con WebAudio ======
function playNote(note, duration=0.9){
if(!soundOn) return;
const freq = noteFrequencies[note];
if(!freq) return;
currentNoteEl.textContent = note;
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.value = freq;
g.gain.value = 0;
o.connect(g); g.connect(masterGain);
const now = ctx.currentTime;
g.gain.cancelScheduledValues(now);
g.gain.setValueAtTime(0, now);
g.gain.linearRampToValueAtTime(0.8, now+0.02);
g.gain.exponentialRampToValueAtTime(0.0001, now+duration);
o.start(now); o.stop(now+duration+0.02);
// highlight key
const keyEl = document.querySelector(`[data-note="${note}"]`);
if(keyEl){
const prevBg = keyEl.style.boxShadow;
keyEl.style.transform = 'translateY(4px)';
setTimeout(()=>keyEl.style.transform='',300);
}
// for quiz/progress tracking
trackPractice(note);
}
// Keyboard computer mapping (simple)
const keyMap = {
'a':'C4','w':'C#4','s':'D4','e':'D#4','d':'E4','f':'F4','t':'F#4','g':'G4','y':'G#4','h':'A4','u':'A#4','j':'B4','k':'C5',
'o':'C#5','l':'D5','p':'D#5',';':'E5','\'' :'F5'
};
document.addEventListener('keydown', (e)=>{
if(e.repeat) return;
const key = e.key.toLowerCase();
if(keyMap[key]){
playNote(keyMap[key]);
}
});
// Volume control
volume.addEventListener('input', ()=>{masterGain.gain.value = volume.value});
// Toggle sound
toggleSound.addEventListener('click', ()=>{soundOn = !soundOn; toggleSound.textContent = soundOn? '🔊 Sonido ON' : '🔇 Sonido OFF'});
// ====== Lecciones UI ======
function renderLessons(){
lessonsEl.innerHTML='';
lessons.forEach((l,idx)=>{
const el = document.createElement('div');
el.className = 'lesson'+(idx===selectedLesson? ' selected':'');
el.innerHTML = `<strong>${l.title}</strong><div style="color:var(--muted);margin-top:6px">${l.content}</div>`;
el.addEventListener('click', ()=>{selectedLesson=idx; renderLessons(); saveState()});
lessonsEl.appendChild(el);
});
}
renderLessons();
// ====== Tracking y almacenamiento ======
function loadState(){
const s = localStorage.getItem('pianoTutor_v1');
if(s) try{const obj=JSON.parse(s); lessonProgress=obj.lessonProgress||{}; selectedLesson=obj.selectedLesson||0; updateProgressUI(); savedAt.textContent = obj.savedAt||'—';}catch(e){}
renderLessons();
}
function saveState(){
const obj = {lessonProgress, selectedLesson, savedAt: new Date().toLocaleString()};
localStorage.setItem('pianoTutor_v1', JSON.stringify(obj));
savedAt.textContent = obj.savedAt;
updateProgressUI();
}
function updateProgressUI(){
const val = lessonProgress[selectedLesson] || 0;
progressBar.style.width = (val*100)+'%';
progressPct.textContent = Math.round(val*100)+'%';
// global
const total = Object.values(lessonProgress).reduce((a,b)=>a+b,0);
const avg = lessons.length? Math.round((total/lessons.length)*100) : 0;
globalProgress.style.width = avg+'%';
document.getElementById('globalProgress').style.width = avg+'%';
}
function trackPractice(note){
// incrementar progreso simple: cada nota tocada suma un pequeño progreso
lessonProgress[selectedLesson] = Math.min(1, (lessonProgress[selectedLesson]||0) + 0.01);
saveState();
}
document.getElementById('resetProgress').addEventListener('click', ()=>{lessonProgress={}; saveState();});
loadState();
// ====== Ejercicios y reproducción ======
document.getElementById('playC').addEventListener('click', ()=>{
// C major arpeggio
const seq = ['C4','E4','G4','C5'];
playSequence(seq, 0.4);
});
document.getElementById('playExercise').addEventListener('click', ()=>{
playSequence(['C4','D4','E4','C4'],0.5);
});
function playSequence(notes, step){
let t = 0;
notes.forEach(n=>{setTimeout(()=>playNote(n, step-0.05), t*1000); t+=step});
}
// ====== Quiz ======
const quizOptionsEl = document.getElementById('quizOptions');
function newQuiz(){
const notes = Object.keys(noteFrequencies);
const pick = notes[Math.floor(Math.random()*notes.length)];
// create 4 options
const options = new Set([pick]);
while(options.size<4) options.add(notes[Math.floor(Math.random()*notes.length)]);
const arr = Array.from(options).sort(()=>Math.random()-0.5);
quizOptionsEl.innerHTML='';
arr.forEach(opt=>{
const o = document.createElement('div'); o.className='option'; o.textContent = opt; o.tabIndex=0;
o.addEventListener('click', ()=>{
if(opt===pick){document.getElementById('quizResult').textContent='✅ Correcto'; lessonProgress[selectedLesson] = Math.min(1,(lessonProgress[selectedLesson]||0)+0.08); saveState(); }
else document.getElementById('quizResult').textContent='❌ Falló — era '+pick;
});
quizOptionsEl.appendChild(o);
});
document.getElementById('quizPlay').onclick = ()=>playNote(pick);
}
newQuiz();
// ====== Grabación (MediaRecorder sobre destino de WebAudio) ======
// Para grabar el audio generado por WebAudio creamos un MediaStream desde un MediaStreamDestination
const dest = ctx.createMediaStreamDestination();
masterGain.connect(dest);
let mediaRecorder;
try{
mediaRecorder = new MediaRecorder(dest.stream);
mediaRecorder.ondataavailable = e=>{if(e.data.size>0) recorderChunks.push(e.data)};
mediaRecorder.onstop = ()=>{
const blob = new Blob(recorderChunks,{type:'audio/webm'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = 'practica.webm'; a.click();
recorderChunks = [];
}
}catch(e){console.warn('MediaRecorder no soportado',e)}
document.getElementById('recordBtn').addEventListener('click', ()=>{
if(!mediaRecorder) return alert('Grabación no disponible en este navegador');
if(!recording){
recorderChunks=[]; mediaRecorder.start(); recording=true; document.getElementById('recordBtn').textContent='🛑 Detener grabación';
}else{mediaRecorder.stop(); recording=false; document.getElementById('recordBtn').textContent='🎙 Grabar práctica';}
});
document.getElementById('downloadBtn').addEventListener('click', ()=>{
// Si ya hay grabación parada, el onstop gestionará la descarga. Alternativamente generar un breve sample
playSequence(['C4','E4','G4'],0.3);
});
// Estado inicial persistente
saveState();
// Small accessibility: focus trap of keyboard
keyboardEl.addEventListener('keydown', (e)=>{ if(e.key==='ArrowRight' || e.key==='ArrowLeft') e.preventDefault(); });
// Antes de cerrar, guardar
window.addEventListener('beforeunload', saveState);
</script>
</body>
</html>