Skip to content
LAM
Read Home Blog
Make Projects HTML Tools Games
Touch grass Notes Resume Links
Home Blog HTML Projects
Tools Games Notes Resume Links
Back AI Virtual Piano Fun
Download Open
Show description 195 chars · Fun

AI Virtual Piano

AI Virtual Piano



















Choose a song:

None
Twinkle Twinkle Little Star
Mary Had a Little Lamb
Ode to Joy
Jingle Bells
Happy Birthday






Generate Song

AI Virtual Piano

27,578 bytes · HTML source
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Virtual Piano</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.js"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
    <style>
        /* --- General Body & Layout --- */
        body {
            font-family: 'Inter', sans-serif;
            overflow: hidden;
            display: flex;
            justify-content: center;
        }

        main {
            width: 100%;
            max-width: 1024px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: flex-end;
            height: 100vh;
        }

        /* --- Piano Keyboard --- */
        .piano-container {
            display: flex;
            position: relative;
            height: 200px;
            margin-bottom: 20px;
            background: #222;
            padding: 10px;
            border-radius: 10px;
            box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
        }

        .key {
            border: 1px solid #4a4a4a;
            box-sizing: border-box;
            cursor: pointer;
            transition: background-color 0.1s ease;
            display: flex;
            justify-content: center;
            align-items: flex-end;
            padding-bottom: 8px;
            font-weight: 500;
            border-radius: 0 0 5px 5px;
            position: relative;
        }

        .white-key {
            width: 60px;
            height: 100%;
            background-color: white;
            z-index: 1;
            color: #333;
        }

        .black-key {
            width: 38px;
            height: 60%;
            background-color: #333;
            position: absolute;
            z-index: 2;
            color: white;
            border-width: 1px 2px 5px;
            border-color: #555;
            border-radius: 0 0 4px 4px;
        }

        .key.active {
            background-color: #60a5fa;
            border-color: #2563eb;
        }
        
        /* --- Colored Note Labels --- */
        .note-label {
            width: 28px;
            height: 28px;
            border-radius: 50%;
            display: flex;
            justify-content: center;
            align-items: center;
            font-weight: bold;
            font-size: 16px;
            position: absolute;
            top: 10px;
            left: 50%;
            transform: translateX(-50%);
            color: #1f2937;
            box-shadow: 0 1px 3px rgba(0,0,0,0.2);
            border: 1px solid rgba(0,0,0,0.1);
            user-select: none;
        }
        .note-label-C { background-color: #fef08a; } .black-key .note-label-C { background-color: #eab308; }
        .note-label-D { background-color: #fdba74; } .black-key .note-label-D { background-color: #f97316; }
        .note-label-E { background-color: #fca5a5; }
        .note-label-F { background-color: #86efac; } .black-key .note-label-F { background-color: #22c55e; }
        .note-label-G { background-color: #93c5fd; } .black-key .note-label-G { background-color: #3b82f6; }
        .note-label-A { background-color: #d8b4fe; } .black-key .note-label-A { background-color: #a855f7; }
        .note-label-B { background-color: #f9a8d4; }

        .black-key .note-label { color: #f9fafb; border-color: rgba(255,255,255,0.2); }

        /* --- Chord Legend Widget --- */
        #chord-legend {
            position: fixed;
            right: 20px;
            top: 50%;
            transform: translateY(-50%);
            width: 280px; /* Increased default width */
            min-width: 220px;
            min-height: 200px;
            background-color: #111827;
            color: #d1d5db;
            border: 2px solid #000;
            border-radius: 12px;
            box-shadow: 0 10px 20px rgba(0,0,0,0.3);
            z-index: 100;
            display: flex;
            flex-direction: column;
        }
        #chord-legend-header {
            padding: 8px 16px;
            cursor: move;
            background-color: #1f2937;
            border-bottom: 1px solid #374151;
            border-radius: 10px 10px 0 0;
            user-select: none;
        }
        .chord-list {
            flex-grow: 1; /* Allow list to fill space */
            overflow-y: auto;
            padding: 16px;
        }
        .chord-item { margin-bottom: 12px; }
        .chord-name { font-weight: bold; color: #fff; font-size: 1.1rem; }
        .chord-keys { font-family: monospace; background-color: #374151; padding: 4px 8px; border-radius: 6px; display: inline-block; margin-top: 4px; }
        #resize-handle {
            width: 20px;
            height: 20px;
            position: absolute;
            bottom: 0;
            right: 0;
            cursor: nwse-resize;
        }
        /* Themed Scrollbar */
        .chord-list::-webkit-scrollbar { width: 8px; }
        .chord-list::-webkit-scrollbar-track { background: #1f2937; border-radius: 4px;}
        .chord-list::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 4px; }
        .chord-list::-webkit-scrollbar-thumb:hover { background: #6b7280; }

        /* --- Note Falling Animation --- */
        .falling-note-area { width: 100%; flex-grow: 1; position: relative; }
        .falling-note-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
        .falling-note { position: absolute; background-color: #3b82f6; border-radius: 4px; width: 58px; height: 20px; }
        .black-key-note { background-color: #1e40af; width: 36px; }
        .play-line { position: absolute; bottom: 0; width: 100%; height: 2px; background-color: #ef4444; z-index: 5; }
        
        /* --- UI Controls & Loader --- */
        .controls { padding: 20px; }
        .loader { border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid #3498db; width: 24px; height: 24px; animation: spin 1.5s linear infinite; }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
    </style>
</head>
<body class="bg-gray-100 h-screen">

    <main>
        <div id="falling-note-area" class="falling-note-area">
             <div class="falling-note-container"></div>
             <div class="play-line"></div>
        </div>
        <div class="piano-container" id="piano"></div>
        <div class="controls flex flex-col items-center space-y-4">
            <div class="flex items-center space-x-4">
                <label for="song-select" class="text-lg font-medium text-gray-700">Choose a song:</label>
                <select id="song-select" class="p-2 rounded-lg border-2 border-gray-300 focus:border-blue-500 focus:ring focus:ring-blue-200">
                    <option value="none">None</option>
                    <option value="twinkle">Twinkle Twinkle Little Star</option>
                    <option value="mary">Mary Had a Little Lamb</option>
                    <option value="ode">Ode to Joy</option>
                    <option value="jingle">Jingle Bells</option>
                    <option value="happy_birthday">Happy Birthday</option>
                </select>
            </div>
            <div class="flex items-center space-x-2">
                 <input type="text" id="song-input" placeholder="Or type a song name here..." class="p-2 w-64 rounded-lg border-2 border-gray-300 focus:border-blue-500 focus:ring focus:ring-blue-200">
                 <button id="generate-btn" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center w-40">
                     <span id="generate-btn-text">Generate Song</span>
                     <div id="loader" class="loader hidden ml-2"></div>
                </button>
            </div>
            <div id="error-message" class="text-red-500 font-medium h-5"></div>
        </div>
    </main>

    <div id="chord-legend"></div>

    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const piano = document.getElementById('piano');
            const songSelect = document.getElementById('song-select');
            const fallingNoteContainer = document.querySelector('.falling-note-container');
            const generateBtn = document.getElementById('generate-btn');
            const generateBtnText = document.getElementById('generate-btn-text');
            const songInput = document.getElementById('song-input');
            const loader = document.getElementById('loader');
            const errorMessage = document.getElementById('error-message');
            const chordLegend = document.getElementById('chord-legend');

            const synth = new Tone.PolySynth(Tone.Synth).toDestination();

            const notes = [
                { note: 'C4', key: 'a' }, { note: 'C#4', key: 'w' }, { note: 'D4', key: 's' }, { note: 'D#4', key: 'e' },
                { note: 'E4', key: 'd' }, { note: 'F4', key: 'f' }, { note: 'F#4', key: 't' }, { note: 'G4', key: 'g' },
                { note: 'G#4', key: 'y' }, { note: 'A4', key: 'h' }, { note: 'A#4', key: 'u' }, { note: 'B4', key: 'j' },
                { note: 'C5', key: 'k' }, { note: 'C#5', key: 'o' }, { note: 'D5', key: 'l' }, { note: 'D#5', key: 'p' },
                { note: 'E5', key: ';' }
            ];

            const keyMap = {};
            const noteToKeyMap = {};
            notes.forEach(({ note, key }) => { keyMap[key] = note; noteToKeyMap[note] = key; });

            const noteColorMap = {
                'C': 'note-label-C', 'D': 'note-label-D', 'E': 'note-label-E',
                'F': 'note-label-F', 'G': 'note-label-G', 'A': 'note-label-A', 'B': 'note-label-B'
            };

            // --- Create Piano Keys ---
            const keyElements = {};
            let whiteKeyCount = 0;
            notes.forEach(({ note, key }) => {
                if (!note.includes('#')) {
                    const keyElement = document.createElement('div');
                    keyElement.className = 'key white-key';
                    keyElement.dataset.note = note;
                    keyElement.innerHTML = `<span>${key.toUpperCase()}</span>`;
                    const noteName = note.charAt(0);
                    const colorClass = noteColorMap[noteName];
                    if (colorClass) {
                        const noteLabel = document.createElement('div');
                        noteLabel.className = `note-label ${colorClass}`;
                        noteLabel.textContent = noteName;
                        keyElement.appendChild(noteLabel);
                    }
                    piano.appendChild(keyElement);
                    keyElements[note] = keyElement;
                    whiteKeyCount++;
                }
            });
            const whiteKeyWidth = 60;
            piano.style.width = `${whiteKeyCount * whiteKeyWidth}px`;
            let whiteKeyIndex = 0;
            notes.forEach(({ note, key }) => {
                if (note.includes('#')) {
                    const keyElement = document.createElement('div');
                    keyElement.className = 'key black-key';
                    keyElement.dataset.note = note;
                    keyElement.innerHTML = `<span>${key.toUpperCase()}</span>`;
                    keyElement.style.left = `${(whiteKeyIndex * whiteKeyWidth) - 19}px`;
                    const noteName = note.charAt(0);
                    const colorClass = noteColorMap[noteName];
                    if (colorClass) {
                        const noteLabel = document.createElement('div');
                        noteLabel.className = `note-label ${colorClass}`;
                        noteLabel.textContent = noteName;
                        keyElement.appendChild(noteLabel);
                    }
                    piano.appendChild(keyElement);
                    keyElements[note] = keyElement;
                } else {
                    whiteKeyIndex++;
                }
            });

            // --- Chord Data and Legend Population ---
            const chords = [
                { name: 'C', major: ['C4', 'E4', 'G4'], minor: ['C4', 'D#4', 'G4'] },
                { name: 'C#', major: ['C#4', 'F4', 'G#4'], minor: ['C#4', 'E4', 'G#4'] },
                { name: 'D', major: ['D4', 'F#4', 'A4'], minor: ['D4', 'F4', 'A4'] },
                { name: 'D#', major: ['D#4', 'G4', 'A#4'], minor: ['D#4', 'F#4', 'A#4'] },
                { name: 'E', major: ['E4', 'G#4', 'B4'], minor: ['E4', 'G4', 'B4'] },
                { name: 'F', major: ['F4', 'A4', 'C5'], minor: ['F4', 'G#4', 'C5'] },
                { name: 'F#', major: ['F#4', 'A#4', 'C#5'], minor: ['F#4', 'A4', 'C#5'] },
                { name: 'G', major: ['G4', 'B4', 'D5'], minor: ['G4', 'A#4', 'D5'] },
                { name: 'G#', major: ['G#4', 'C5', 'D#5'], minor: ['G#4', 'B4', 'D#5'] },
                { name: 'A', major: ['A4', 'C#5', 'E5'], minor: ['A4', 'C5', 'E5'] },
                { name: 'A#', minor: ['A#4', 'C#5', 'E5'] }, // Major not available on this keyboard
                { name: 'B', minor: ['B4', 'D5', 'F#4'] }, // Major not available on this keyboard
            ];

            function populateChordLegend() {
                let legendHTML = `
                    <div id="chord-legend-header">
                        <h2 class="text-xl font-bold text-white">Chord Legend</h2>
                    </div>
                    <div class="chord-list">
                `;
                chords.forEach(chord => {
                    const majorKeys = chord.major?.map(n => noteToKeyMap[n]?.toUpperCase()).filter(Boolean).join(' + ');
                    const minorKeys = chord.minor?.map(n => noteToKeyMap[n]?.toUpperCase()).filter(Boolean).join(' + ');
                    
                    legendHTML += `<div class="chord-item"><div class="chord-name">${chord.name}</div>`;
                    if (majorKeys) legendHTML += `<div>Major: <span class="chord-keys">${majorKeys}</span></div>`;
                    if (minorKeys) legendHTML += `<div class="mt-1">Minor: <span class="chord-keys">${minorKeys}</span></div>`;
                    legendHTML += `</div>`;
                });
                legendHTML += '</div><div id="resize-handle"></div>';
                chordLegend.innerHTML = legendHTML;
                makeDraggableAndResizable();
            }
            populateChordLegend();

            // --- Draggable and Resizable Logic ---
            function makeDraggableAndResizable() {
                const header = document.getElementById('chord-legend-header');
                const resizeHandle = document.getElementById('resize-handle');
                let isDragging = false, isResizing = false;
                let offsetX, offsetY, startX, startY, startWidth, startHeight;

                header.addEventListener('mousedown', (e) => {
                    isDragging = true;
                    offsetX = e.clientX - chordLegend.offsetLeft;
                    offsetY = e.clientY - chordLegend.offsetTop;
                    chordLegend.style.transition = 'none';
                });

                resizeHandle.addEventListener('mousedown', (e) => {
                    e.preventDefault();
                    isResizing = true;
                    startX = e.clientX;
                    startY = e.clientY;
                    startWidth = parseInt(document.defaultView.getComputedStyle(chordLegend).width, 10);
                    startHeight = parseInt(document.defaultView.getComputedStyle(chordLegend).height, 10);
                });

                document.addEventListener('mousemove', (e) => {
                    if (isDragging) {
                        chordLegend.style.left = `${e.clientX - offsetX}px`;
                        chordLegend.style.top = `${e.clientY - offsetY}px`;
                    }
                    if (isResizing) {
                        const newWidth = startWidth + e.clientX - startX;
                        const newHeight = startHeight + e.clientY - startY;
                        chordLegend.style.width = `${newWidth}px`;
                        chordLegend.style.height = `${newHeight}px`;
                    }
                });

                document.addEventListener('mouseup', () => {
                    isDragging = false;
                    isResizing = false;
                    chordLegend.style.transition = '';
                });
            }

            // --- Song Data ---
            const songs = {
                twinkle: [ { note: 'C4', time: 0 }, { note: 'C4', time: 0.5 }, { note: 'G4', time: 1 }, { note: 'G4', time: 1.5 }, { note: 'A4', time: 2 }, { note: 'A4', time: 2.5 }, { note: 'G4', time: 3 }, { note: 'F4', time: 4 }, { note: 'F4', time: 4.5 }, { note: 'E4', time: 5 }, { note: 'E4', time: 5.5 }, { note: 'D4', time: 6 }, { note: 'D4', time: 6.5 }, { note: 'C4', time: 7 } ],
                mary: [ { note: 'E4', time: 0 }, { note: 'D4', time: 0.5 }, { note: 'C4', time: 1 }, { note: 'D4', time: 1.5 }, { note: 'E4', time: 2 }, { note: 'E4', time: 2.5 }, { note: 'E4', time: 3 }, { note: 'D4', time: 4 }, { note: 'D4', time: 4.5 }, { note: 'D4', time: 5 }, { note: 'E4', time: 6 }, { note: 'G4', time: 6.5 }, { note: 'G4', time: 7 } ],
                ode: [ { note: 'E4', time: 0 }, { note: 'E4', time: 0.5 }, { note: 'F4', time: 1 }, { note: 'G4', time: 1.5 }, { note: 'G4', time: 2 }, { note: 'F4', time: 2.5 }, { note: 'E4', time: 3 }, { note: 'D4', time: 3.5 }, { note: 'C4', time: 4 }, { note: 'C4', time: 4.5 }, { note: 'D4', time: 5 }, { note: 'E4', time: 5.5 }, { note: 'E4', time: 6.4 }, { note: 'D4', time: 7 }, { note: 'D4', time: 7.5 } ],
                jingle: [ { note: 'E4', time: 0 }, { note: 'E4', time: 0.5 }, { note: 'E4', time: 1 }, { note: 'E4', time: 2 }, { note: 'E4', time: 2.5 }, { note: 'E4', time: 3 }, { note: 'E4', time: 4 }, { note: 'G4', time: 4.5 }, { note: 'C4', time: 5 }, { note: 'D4', time: 5.5 }, { note: 'E4', time: 6 } ],
                happy_birthday: [ { note: 'C4', time: 0 }, { note: 'C4', time: 0.5 }, { note: 'D4', time: 1 }, { note: 'C4', time: 1.5 }, { note: 'F4', time: 2 }, { note: 'E4', time: 2.5 }, { note: 'C4', time: 4 }, { note: 'C4', time: 4.5 }, { note: 'D4', time: 5 }, { note: 'C4', time: 5.5 }, { note: 'G4', time: 6 }, { note: 'F4', time: 6.5 } ]
            };

            let currentSong = null;
            let startTime = 0;
            let animationFrameId = null;
            const activeNotes = new Set();

            // --- Note Playing Logic (Attack/Release) ---
            function startNote(note) {
                if (!note || !keyElements[note] || activeNotes.has(note)) return;
                synth.triggerAttack(note);
                activeNotes.add(note);
                keyElements[note].classList.add('active');
            }

            function stopNote(note) {
                if (!note || !activeNotes.has(note)) return;
                synth.triggerRelease(note);
                activeNotes.delete(note);
                keyElements[note].classList.remove('active');
            }

            // --- Event Handlers for User Interaction ---
            piano.addEventListener('mousedown', (e) => {
                const key = e.target.closest('.key');
                if (key) startNote(key.dataset.note);
            });
            piano.addEventListener('mouseup', (e) => {
                const key = e.target.closest('.key');
                if (key) stopNote(key.dataset.note);
            });
            piano.addEventListener('mouseleave', () => {
                activeNotes.forEach(note => stopNote(note));
            });
            document.addEventListener('keydown', (e) => {
                if (e.repeat || songInput === document.activeElement) return;
                const note = keyMap[e.key];
                if (note) startNote(note);
            });
            document.addEventListener('keyup', (e) => {
                const note = keyMap[e.key];
                if (note) stopNote(note);
            });

            songSelect.addEventListener('change', (e) => handleSongSelection(e.target.value));

            function handleSongSelection(songName) {
                if (animationFrameId) cancelAnimationFrame(animationFrameId);
                fallingNoteContainer.innerHTML = '';
                if (songName !== 'none' && songs[songName]) {
                    currentSong = songs[songName];
                    startSongAnimation();
                } else {
                    currentSong = null;
                }
            }

            // --- Animation Logic ---
            function startSongAnimation() {
                startTime = performance.now();
                const pianoRect = piano.getBoundingClientRect();
                const fallingAreaRect = document.getElementById('falling-note-area').getBoundingClientRect();
                const pianoLeftOffset = pianoRect.left - fallingAreaRect.left;
                currentSong.forEach(noteData => {
                    const keyElement = keyElements[noteData.note];
                    if (!keyElement) return;
                    const noteElement = document.createElement('div');
                    noteElement.className = 'falling-note';
                    if (keyElement.classList.contains('black-key')) {
                        noteElement.classList.add('black-key-note');
                    }
                    noteElement.style.left = `${keyElement.offsetLeft + pianoLeftOffset + 1}px`;
                    noteElement.dataset.time = noteData.time;
                    noteElement.dataset.note = noteData.note;
                    noteElement.dataset.played = 'false';
                    fallingNoteContainer.appendChild(noteElement);
                });
                animateFallingNotes();
            }

            function animateFallingNotes() {
                const currentTime = (performance.now() - startTime) / 1000;
                const fallSpeed = 150; 
                const notesToAnimate = fallingNoteContainer.querySelectorAll('.falling-note');
                let remainingNotes = false;
                notesToAnimate.forEach(noteElement => {
                    const noteTime = parseFloat(noteElement.dataset.time);
                    const timeDifference = noteTime - currentTime;
                    if (timeDifference > -5) {
                        remainingNotes = true;
                        const yPos = fallingNoteContainer.clientHeight - (timeDifference * fallSpeed);
                        noteElement.style.transform = `translateY(${yPos}px)`;
                        if (yPos >= fallingNoteContainer.clientHeight && noteElement.dataset.played === 'false') {
                            const noteToPlay = noteElement.dataset.note;
                            synth.triggerAttackRelease(noteToPlay, '8n', Tone.now());
                            const keyEl = keyElements[noteToPlay];
                            if(keyEl) {
                                keyEl.classList.add('active');
                                setTimeout(() => {
                                    if(!activeNotes.has(noteToPlay)) keyEl.classList.remove('active');
                                }, 200);
                            }
                            noteElement.dataset.played = 'true';
                            noteElement.style.opacity = '0.3';
                        }
                    } else {
                        if (noteElement.parentElement) noteElement.parentElement.removeChild(noteElement);
                    }
                });
                if (remainingNotes) animationFrameId = requestAnimationFrame(animateFallingNotes);
            }
            
            // --- AI Song Generation ---
            generateBtn.addEventListener('click', async () => {
                const songTitle = songInput.value.trim();
                if (!songTitle) { errorMessage.textContent = 'Please enter a song title.'; return; }
                errorMessage.textContent = '';
                loader.classList.remove('hidden');
                generateBtnText.textContent = '';
                generateBtn.disabled = true;
                songInput.disabled = true;
                try {
                    const availableNotes = notes.map(n => n.note).join(', ');
                    const prompt = `Generate the melody for "${songTitle}" as a JSON array. Each object needs "note" (from: ${availableNotes}) and "time" (seconds). Example: [{"note": "C4", "time": 0}, ...]. Output only the raw JSON array.`;
                    const payload = { contents: [{ role: "user", parts: [{ text: prompt }] }], generationConfig: { responseMimeType: "application/json", responseSchema: { type: "ARRAY", items: { type: "OBJECT", properties: { "note": { "type": "STRING" }, "time": { "type": "NUMBER" } }, required: ["note", "time"] } } } };
                    const apiKey = "";
                    const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`;
                    const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
                    if (!response.ok) throw new Error(`API error: ${response.statusText}`);
                    const result = await response.json();
                    if (result.candidates?.[0]?.content?.parts?.[0]) {
                        const generatedSong = JSON.parse(result.candidates[0].content.parts[0].text);
                        const songId = songTitle.toLowerCase().replace(/\s+/g, '_');
                        songs[songId] = generatedSong;
                        const option = document.createElement('option');
                        option.value = songId;
                        option.textContent = songTitle;
                        songSelect.appendChild(option);
                        songSelect.value = songId;
                        handleSongSelection(songId);
                        songInput.value = '';
                    } else {
                        throw new Error("AI did not return a valid song structure.");
                    }
                } catch (error) {
                    console.error('Error generating song:', error);
                    errorMessage.textContent = 'Could not generate song. Please try again.';
                } finally {
                    loader.classList.add('hidden');
                    generateBtnText.textContent = 'Generate Song';
                    generateBtn.disabled = false;
                    songInput.disabled = false;
                }
            });
        });
    </script>
</body>
</html>