# fur-elise.js 鋼琴 https://www.moonshot.ai/about ## 操作步驟 1. **開啟目標網頁** 在瀏覽器中進入 Moonshot AI 關於頁面(確保網頁已完全載入,且能看到鋼琴畫面)。 2. **複製腳本代碼** 移動到本頁面最下面,將裡面所有的 JavaScript 程式碼(從第 1 行的 `(() => {` 一直到最後一行的 `})();`)完整複製起來。 3. **開啟開發者控制台 (Console)** - 回到 Moonshot AI 網頁,按下鍵盤上的 **F12** 鍵,或使用快速鍵 - Windows: `Ctrl + Shift + J` - Mac: `Cmd + Option + J` - 點選頂部的 **Console (主控台)** 標籤。 4. **貼上並執行** - 將剛剛複製的程式碼貼到 Console 最下方的輸入游標處,然後按下 **Enter** 鍵。 5. **開始聆聽** - 腳本執行後,網頁會自動捲動到鋼琴區塊,並開始模擬按鍵自動彈奏音樂。 6. **停止播放** - 若想提早停下來,只需在同一個 Console 視窗中輸入 `furElise.stop()` 並再次按下 **Enter** 即可停止。  --- ## 初次執行 腳本已在網頁背景建立好專屬的控制指令,你只需要呼叫啟動函數即可。 ```js furElise.start() ``` --- ## 進階玩法(自訂速度與次數) 腳本支援自訂播放參數。你可以在括號內加上設定值,例如: - **加快節奏並重複 3 次** ```js furElise.start({bpm: 90, repeats: 3}) ``` - **放慢節奏只聽 1 次** ```js furElise.start({bpm: 60, repeats: 1}) ``` --- 全部代碼 ``` (() => { "use strict"; const previousFurElise = window.furElise; if (previousFurElise && typeof previousFurElise.stop === "function") previousFurElise.stop(); if ( window.pagePianoPiece && window.pagePianoPiece !== previousFurElise && typeof window.pagePianoPiece.stop === "function" ) { window.pagePianoPiece.stop(); } const KEY_BY_NOTE = { C4: "a", "C#4": "w", D4: "s", "D#4": "e", E4: "d", F4: "f", "F#4": "t", G4: "g", "G#4": "y", A4: "h", "A#4": "u", B4: "j", C5: "k", "C#5": "o", D5: "l", "D#5": "p", E5: ";" }; const SIXTEENTH = 0.25; const EIGHTH = 0.5; const QUARTER = 1; const REPEAT_GAP = 0.5; const phraseEvents = []; let cursor = 0; const append = (entries) => { for (const [note, spacing] of entries) { const hold = spacing === SIXTEENTH ? 0.14 : spacing === EIGHTH ? 0.32 : 0.62; phraseEvents.push({ beat: cursor, note, duration: hold }); cursor += spacing; } }; // The opening turn acts as a pickup into each longer A-minor arrival. append([ ["E5", SIXTEENTH], ["D#5", SIXTEENTH], ["E5", SIXTEENTH], ["D#5", SIXTEENTH], ["E5", SIXTEENTH], ["B4", SIXTEENTH], ["D5", SIXTEENTH], ["C5", SIXTEENTH], ["A4", EIGHTH] ]); // Low arpeggio notes are kept sequential so the page instrument remains a single melodic line. append([ ["C4", SIXTEENTH], ["E4", SIXTEENTH], ["A4", SIXTEENTH], ["B4", EIGHTH], ["E4", SIXTEENTH], ["G#4", SIXTEENTH], ["B4", SIXTEENTH], ["C5", EIGHTH] ]); // The second statement includes the familiar E4 breath before the turn returns. append([ ["E4", EIGHTH], ["E5", SIXTEENTH], ["D#5", SIXTEENTH], ["E5", SIXTEENTH], ["D#5", SIXTEENTH], ["E5", SIXTEENTH], ["B4", SIXTEENTH], ["D5", SIXTEENTH], ["C5", SIXTEENTH], ["A4", EIGHTH] ]); append([ ["C4", SIXTEENTH], ["E4", SIXTEENTH], ["A4", SIXTEENTH], ["B4", EIGHTH], ["E4", SIXTEENTH], ["C5", SIXTEENTH], ["B4", EIGHTH], ["A4", QUARTER] ]); const PHRASE_BEATS = cursor; const REQUIRED_NOTES = [...new Set(phraseEvents.map((event) => event.note))]; let playing = false; let scheduler = null; let timers = new Set(); let active = new Map(); let generations = Object.create(null); let runId = 0; let startedAt = 0; let currentBpm = 72; let currentRepeats = 2; let currentDurationMs = 0; const send = (type, key) => { window.dispatchEvent(new KeyboardEvent(type, { key, bubbles: true })); }; const releaseAll = () => { for (const { key } of active.values()) send("keyup", key); active.clear(); }; const stop = (silent = false) => { runId++; playing = false; if (scheduler !== null) clearInterval(scheduler); scheduler = null; for (const timer of timers) clearTimeout(timer); timers.clear(); releaseAll(); if (!silent) console.info("Für Elise stopped; all held keys released."); }; const validatePiano = () => { const piano = document.querySelector('[aria-label="Interactive piano"]'); if (!piano) { console.error('Für Elise: missing [aria-label="Interactive piano"].'); return null; } const labels = new Set(Array.from( piano.querySelectorAll("[aria-label]"), (element) => element.getAttribute("aria-label") )); const missing = REQUIRED_NOTES.filter((note) => !Array.from(labels).some((label) => label && label.startsWith(`${note} piano key (`)) ); if (missing.length) { console.error(`Für Elise: missing required keys: ${missing.join(", ")}.`); return null; } return piano; }; const start = (options = {}) => { stop(true); const piano = validatePiano(); if (!piano) return false; const bpm = Number(options.bpm ?? 72); const repeats = Number(options.repeats ?? 2); if (!Number.isFinite(bpm) || bpm <= 0 || !Number.isFinite(repeats) || repeats <= 0) { console.error("Für Elise: bpm and repeats must be positive numbers."); return false; } currentBpm = bpm; currentRepeats = Math.max(1, Math.floor(repeats)); const beatMs = 60000 / currentBpm; const cycleBeats = PHRASE_BEATS + REPEAT_GAP; currentDurationMs = (PHRASE_BEATS * currentRepeats + REPEAT_GAP * (currentRepeats - 1)) * beatMs; const events = []; for (let repeat = 0; repeat < currentRepeats; repeat++) { const offset = repeat * cycleBeats; phraseEvents.forEach((event) => events.push({ ...event, beat: event.beat + offset })); } const thisRun = ++runId; let eventCursor = 0; playing = true; startedAt = performance.now() + 80; piano.scrollIntoView({ behavior: "smooth", block: "center" }); const later = (callback, delay) => { const timer = setTimeout(() => { timers.delete(timer); callback(); }, Math.max(0, delay)); timers.add(timer); }; const press = (event) => { if (!playing || runId !== thisRun) return; const key = KEY_BY_NOTE[event.note]; const previous = active.get(event.note); if (previous) send("keyup", previous.key); const token = (generations[event.note] || 0) + 1; generations[event.note] = token; send("keydown", key); active.set(event.note, { key, token }); const endAt = startedAt + (event.beat + event.duration) * beatMs; later(() => { const held = active.get(event.note); if (!playing || runId !== thisRun || !held || held.token !== token) return; send("keyup", key); active.delete(event.note); }, endAt - performance.now()); }; const scheduleAhead = () => { if (!playing || runId !== thisRun) return; const now = performance.now(); const horizon = now + 120; while (eventCursor < events.length) { const event = events[eventCursor]; const due = startedAt + event.beat * beatMs; if (due > horizon) break; eventCursor++; if (now - due > 200) continue; later(() => { if (performance.now() - due <= 200) press(event); }, due - performance.now()); } if (now >= startedAt + currentDurationMs + 100) { playing = false; clearInterval(scheduler); scheduler = null; for (const timer of timers) clearTimeout(timer); timers.clear(); releaseAll(); console.info("Für Elise finished."); } }; scheduler = setInterval(scheduleAhead, 25); scheduleAhead(); console.info( `Für Elise / 致愛麗絲 | ${currentBpm} BPM | ${currentRepeats} repeats | ` + `${(currentDurationMs / 1000).toFixed(1)}s. Stop with furElise.stop().` ); return true; }; const status = () => ({ playing, bpm: currentBpm, repeats: currentRepeats, elapsedSeconds: playing ? Math.max(0, (performance.now() - startedAt) / 1000) : 0, durationSeconds: currentDurationMs / 1000, heldNotes: Array.from(active.keys()) }); window.furElise = { start, stop, status }; window.pagePianoPiece = window.furElise; start(); })(); ```