Inicio

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>
Diseña un sitio como este con WordPress.com
Comenzar