oneshotlm
Compare all models on this prompt →

Piano synth · deepseek-v4-pro

Using the pre-provided tone.min.js (global Tone), build a playable piano keyboard controlled by mouse and computer keys that synthesizes notes with an ADSR envelope and visual key feedback.

⛨ sandboxed Open in new tab ↗
transcript · 10 messages
user

Using the pre-provided tone.min.js (global Tone), build a playable piano keyboard controlled by mouse and computer keys that synthesizes notes with an ADSR envelope and visual key feedback.

assistant
thinking
The user wants me to build a playable piano keyboard web app using Tone.js (already provided as tone.min.js). Let me create a single-page HTML file with:

1. A piano keyboard UI with white and black keys
2. Mouse interaction (click to play)
3. Computer keyboard mapping (keys to notes)
4. ADSR envelope for note synthesis
5. Visual feedback when keys are pressed

Let me first check what's in the current directory.
bash {"command":"ls /workspace/"}
toolResult

tone.min.js

assistant
thinking
Good, tone.min.js is present. Now I'll build a comprehensive piano keyboard app. Let me think about the design:

1. **Synthesizer**: Use Tone.Synth or Tone.MonoSynth with ADSR envelope. Tone.Synth has an envelope with attack, decay, sustain, release built in, but I could also build one from scratch using Tone.Oscillator + Tone.AmplitudeEnvelope. Let me use Tone.Synth for simplicity since it already includes ADSR controls. Actually, Tone.Synth is essentially an oscillator + envelope. Let me use Tone.Synth with configurable ADSR.

2. **Keyboard layout**: I'll map computer keys to piano notes. A common mapping:
   - White keys: A, S, D, F, G, H, J, K, L (for C4 through E5 or similar)
   - Black keys: W, E, T, Y, U, O, P (for C#4 through A#4 etc.)
   
   Actually, let me use a more intuitive 2-row mapping:
   - Lower row (white keys): Z, X, C, V, B, N, M, comma, period, /
   - Upper row (black keys): S, D, G, H, J, L, ;
   
   Or the classic mapping:
   - White: A, S, D, F, G, H, J, K, L, ; (C4 to E5, 10 white keys)
   - Black: W, E, , T, Y, U, , O, P (between those white keys, 7 black keys)
   
   Let me do a standard 2-octave keyboard (with one extra C):
   - Octave starting at C4
   - White keys: A=C4, S=D4, D=E4, F=F4, G=G4, H=A4, J=B4, K=C5, L=D5, ;=E5
   - Black keys: W=C#4, E=D#4, T=F#4, Y=G#4, U=A#4, O=C#5, P=D#5

3. **UI**: I'll draw the piano keys with CSS/HTML. White keys in a row, black keys positioned absolutely on top.

4. **Visual feedback**: Add a CSS class when a key is pressed/playing, with a color change or animation.

5. **ADSR controls**: Add sliders for Attack, Decay, Sustain, Release so users can adjust them in real-time.

Let me build this now.
write {"file_path":"/workspace/index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport
toolResult

Validation failed for tool "write": - path: must have required properties path Received arguments: { "file_path": "/workspace/index.html", "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Piano Keyboard — ADSR Synth</title>\n<style>\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\nbody {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n background: #1a1a2e;\n font-family: 'Segoe UI', system-ui, sans-serif;\n user-select: none;\n -webkit-user-select: none;\n gap: 28px;\n padding: 20px;\n}\n\nh1 {\n color: #e0e0e0;\n font-weight: 400;\n letter-spacing: 2px;\n font-size: 1.6rem;\n text-transform: uppercase;\n}\n\n/* ── Piano container ── */\n.piano-wrap {\n position: relative;\n background: #2b2b3d;\n border-radius: 12px;\n padding: 28px 32px 20px;\n box-shadow: 0 20px 60px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);\n}\n\n.piano {\n position: relative;\n display: flex;\n width: fit-content;\n}\n\n/* ── White keys ── */\n.key-white {\n width: 56px;\n height: 220px;\n background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 4%, #f5f5f5 8%, #fff 40%, #eee 96%, #ccc 100%);\n border: 1px solid #888;\n border-radius: 0 0 8px 8px;\n cursor: pointer;\n position: relative;\n z-index: 1;\n transition: background 0.08s, box-shadow 0.08s, transform 0.08s;\n box-shadow: 0 3px 6px rgba(0,0,0,0.3), inset 0 -2px 4px rgba(0,0,0,0.1);\n}\n\n.key-white:active,\n.key-white.active {\n background: linear-gradient(180deg, #a0d2ff 0%, #7bb8f0 40%, #d0e8ff 70%, #c0d8f0 100%);\n box-shadow: 0 0px 2px rgba(0,0,0,0.3), inset 0 3px 6px rgba(0,0,0,0.2);\n transform: translateY(2px);\n}\n\n.key-white:hover:not(.active) {\n background: linear-gradient(180deg, #fafafa 0%, #f0f0f0 4%, #fff 40%, #f2f2f2 96%, #d5d5d5 100%);\n}\n\n/* key label */\n.label {\n position: absolute;\n bottom: 14px;\n left: 50%;\n transform: translateX(-50%);\n font-size: 0.65rem;\n color: #888;\n pointer-events: none;\n letter-spacing: 1px;\n font-weight: 500;\n}\n\n.key-white.active .label { color: #336699; }\n\n/* ── Black keys ── */\n.key-black {\n width: 36px;\n height: 140px;\n background: linear-gradient(180deg, #3a3a3a 0%, #1a1a1a 50%, #000 95%, #111 100%);\n border: 1px solid #111;\n border-top: none;\n border-radius: 0 0 6px 6px;\n cursor: pointer;\n position: absolute;\n z-index: 2;\n top: 0;\n box-shadow: 0 4px 8px rgba(0,0,0,0.55), inset 0 -2px 3px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.04);\n transition: background 0.08s, box-shadow 0.08s, transform 0.08s, height 0.08s;\n}\n\n.key-black:active,\n.key-black.active {\n background: linear-gradient(180deg, #3a6090 0%, #1a3a60 50%, #0a2a50 95%, #1a3a60 100%);\n box-shadow: 0 1px 3px rgba(0,0,0,0.5), inset 0 3px 5px rgba(0,0,0,0.4);\n transform: translateY(2px);\n height: 136px;\n}\n\n.key-black:hover:not(.active) {\n background: linear-gradient(180deg, #4a4a4a 0%, #2a2a2a 50%, #151515 95%, #222 100%);\n}\n\n.key-black .label {\n bottom: 10px;\n color: #999;\n font-size: 0.55rem;\n}\n.key-black.active .label { color: #aaccff; }\n\n/* ── Shortcuts panel ── */\n.shortcuts {\n color: #888;\n font-size: 0.75rem;\n text-align: center;\n letter-spacing: 0.5px;\n line-height: 1.6;\n}\n.shortcuts span {\n display: inline-block;\n background: #2b2b3d;\n border: 1px solid #444;\n border-radius: 4px;\n padding: 1px 7px;\n margin: 0 1px;\n font-family: monospace;\n font-size: 0.8rem;\n color: #ccc;\n}\n\n/* ── ADSR controls ── */\n.adsr-panel {\n background: #2b2b3d;\n border-radius: 12px;\n padding: 18px 28px;\n display: flex;\n gap: 30px;\n flex-wrap: wrap;\n justify-content: center;\n box-shadow: 0 8px 24px rgba(0,0,0,0.3);\n}\n\n.adsr-knob {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 6px;\n}\n\n.adsr-knob label {\n color: #aaa;\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n}\n\n.adsr-knob input[type=\"range\"] {\n -webkit-appearance: none;\n width: 80px;\n height: 6px;\n border-radius: 3px;\n background: #444;\n outline: none;\n cursor: pointer;\n}\n.adsr-knob input[type=\"range\"]::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: #7bb8f0;\n border: 2px solid #fff;\n cursor: pointer;\n}\n\n.adsr-knob .val {\n color: #e0e0e0;\n font-size: 0.8rem;\n font-family: monospace;\n min-width: 36px;\n text-align: center;\n}\n\n/* ── Waveform selector ── */\n.wave-select {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.wave-select select {\n background: #3a3a4a;\n color: #e0e0e0;\n border: 1px solid #555;\n border-radius: 6px;\n padding: 4px 10px;\n font-size: 0.75rem;\n cursor: pointer;\n outline: none;\n}\n.wave-select label {\n color: #aaa;\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n}\n\n/* ── Octave shift ── */\n.octave-shift {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.octave-shift button {\n background: #3a3a4a;\n color: #e0e0e0;\n border: 1px solid #555;\n border-radius: 6px;\n padding: 4px 14px;\n cursor: pointer;\n font-size: 0.85rem;\n transition: background 0.15s;\n}\n.octave-shift button:hover {\n background: #4a4a5a;\n}\n.octave-shift .octave-label {\n color: #e0e0e0;\n font-size: 0.8rem;\n font-family: monospace;\n min-width: 30px;\n text-align: center;\n}\n.octave-shift > label {\n color: #aaa;\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n}\n</style>\n</head>\n<body>\n\n<h1>🎹 Synthesizer Piano</h1>\n\n<div class=\"piano-wrap\">\n <div class=\"piano\" id=\"piano\"></div>\n</div>\n\n<div class=\"shortcuts\" id=\"shortcuts\"></div>\n\n<div class=\"adsr-panel\">\n <div class=\"adsr-knob\">\n <label>Attack</label>\n <input type=\"range\" id=\"attack\" min=\"0\" max=\"100\" value=\"5\" step=\"1\">\n <span class=\"val\" id=\"attackVal\">0.05s</span>\n </div>\n <div class=\"adsr-knob\">\n <label>Decay</label>\n <input type=\"range\" id=\"decay\" min=\"0\" max=\"100\" value=\"20\" step=\"1\">\n <span class=\"val\" id=\"decayVal\">0.20s</span>\n </div>\n <div class=\"adsr-knob\">\n <label>Sustain</label>\n <input type=\"range\" id=\"sustain\" min=\"0\" max=\"100\" value=\"50\" step=\"1\">\n <span class=\"val\" id=\"sustainVal\">0.50</span>\n </div>\n <div class=\"adsr-knob\">\n <label>Release</label>\n <input type=\"range\" id=\"release\" min=\"0\" max=\"200\" value=\"60\" step=\"1\">\n <span class=\"val\" id=\"releaseVal\">0.60s</span>\n </div>\n <div class=\"wave-select\">\n <label>Wave</label>\n <select id=\"waveform\">\n <option value=\"sine\">Sine</option>\n <option value=\"triangle\" selected>Triangle</option>\n <option value=\"sawtooth\">Sawtooth</option>\n <option value=\"square\">Square</option>\n </select>\n </div>\n <div class=\"octave-shift\">\n <label>Octave</label>\n <button id=\"octDown\">−</button>\n <span class=\"octave-label\" id=\"octaveVal\">C4</span>\n <button id=\"octUp\">+</button>\n </div>\n</div>\n\n<script src=\"tone.min.js\"></script>\n<script>\n// ──────────────────────────────────────────────\n// Piano keyboard layout\n// ──────────────────────────────────────────────\n\n// Two octaves + 1: C4 (MIDI 60) → E6 (MIDI 76), 15 white keys, 10 black keys\nconst WHITE_NOTES = ['C','D','E','F','G','A','B'];\nconst WHITE_OFFSETS = [0,2,4,5,7,9,11]; // semitone offset in octave\nconst BLACK_OFFSETS = [1,3,6,8,10]; // C# D# F# G# A#\n\n// Build note lists for two octaves (starting at octave 4)\nfunction buildNotes(startOctave, numOctaves) {\n const white = [];\n const black = [];\n for (let o = startOctave; o < startOctave + numOctaves; o++) {\n WHITE_OFFSETS.forEach(off => {\n white.push({ name: WHITE_NOTES[WHITE_OFFSETS.indexOf(off)], octave: o, midi: (o+1)*12 + off });\n });\n BLACK_OFFSETS.forEach(off => {\n const idx = BLACK_OFFSETS.indexOf(off);\n const names = ['C#','D#','F#','G#','A#'];\n black.push({ name: names[idx], octave: o, midi: (o+1)*12 + off });\n });\n }\n // plus final C\n white.push({ name: 'C', octave: startOctave + numOctaves, midi: (startOctave + numOctaves + 1) * 12 });\n return { white, black };\n}\n\nlet baseOctave = 4;\nlet notes = buildNotes(baseOctave, 2); // C4 → C6\n\n// Keyboard map: white row + black row\n// White: A S D F G H J K L ; ' (also Z X C V B N M , . / as alternative lower row)\n// Let's use QWERTY home row style:\nconst KEY_WHITE = ['a','s','d','f','g','h','j','k','l',';',\"'\",'z','x','c','v'];\nconst KEY_BLACK = ['w','e','','t','y','u','','o','p','', '','','r','',''];\n\n// Build key→midi map\nconst keyToMidi = {};\nnotes.white.forEach((n, i) => {\n if (KEY_WHITE[i]) keyToMidi[KEY_WHITE[i]] = n.midi;\n});\nnotes.black.forEach((n, i) => {\n if (KEY_BLACK[i]) keyToMidi[KEY_BLACK[i]] = n.midi;\n});\n\n// ──────────────────────────────────────────────\n// Build piano DOM\n// ──────────────────────────────────────────────\n\nconst pianoEl = document.getElementById('piano');\nlet whiteKeyEls = [];\nlet blackKeyEls = [];\n\nfunction renderPiano() {\n pianoEl.innerHTML = '';\n\n // White keys\n notes.white.forEach((n, i) => {\n const el = document.createElement('div');\n el.className = 'key-white';\n el.dataset.midi = n.midi;\n el.dataset.note = n.name + n.octave;\n el.innerHTML = `<span class=\"label\">${KEY_WHITE[i] ? KEY_WHITE[i].toUpperCase() : ''}</span>`;\n pianoEl.appendChild(el);\n whiteKeyEls.push(el);\n });\n\n // Black keys — absolutely positioned\n // Position black keys between white keys. The gap pattern per octave:\n // C [C#] D [D#] E F [F#] G [G#] A [A#] B C\n // Inter-key gaps where black keys exist: after index 0,1, 3,4,5. In a 7-note octave that's after 0,1,3,4,5\n // For 2 octaves: after 0,1, 3,4,5, 7,8, 10,11,12 → black keys at white indices: 0,1,3,4,5,7,8,10,11,12\n const blackPositions = [0, 1, 3, 4, 5, 7, 8, 10, 11, 12];\n\n blackKeyEls = [];\n notes.black.forEach((n, i) => {\n const el = document.createElement('div');\n el.className = 'key-black';\n el.dataset.midi = n.midi;\n el.dataset.note = n.name + n.octave;\n el.innerHTML = `<span class=\"label\">${KEY_BLACK[i] ? KEY_BLACK[i].toUpperCase() : ''}</span>`;\n // Position relative to white key index\n const whiteIdx = blackPositions[i];\n if (whiteIdx !== undefined) {\n el.style.left = ((whiteIdx + 1) * 56 - 18) + 'px'; // center over gap\n }\n pianoEl.appendChild(el);\n blackKeyEls.push(el);\n });\n}\n\nrenderPiano();\n\n// ──────────────────────────────────────────────\n// Shortcuts hint\n// ──────────────────────────────────────────────\nconst shortcutsEl = document.getElementById('shortcuts');\n(function renderShortcuts() {\n let html = 'White keys: ';\n notes.white.forEach((n, i) => {\n if (KEY_WHITE[i]) html += `<span>${KEY_WHITE[i].toUpperCase()}</span> `;\n });\n html += '<br>Black keys: ';\n notes.black.forEach((n, i) => {\n if (KEY_BLACK[i]) html += `<span>${KEY_BLACK[i].toUpperCase()}</span> `;\n });\n html += '<br>Click keys or press keyboard letters';\n shortcutsEl.innerHTML = html;\n})();\n\n// ──────────────────────────────────────────────\n// Tone.js Synth with ADSR\n// ──────────────────────────────────────────────\n\nconst synth = new Tone.PolySynth(Tone.Synth, {\n oscillator: { type: 'triangle' },\n envelope: {\n attack: 0.05,\n decay: 0.2,\n sustain: 0.5,\n release: 0.6\n }\n}).toDestination();\n\n// ──────────────────────────────────────────────\n// ADSR controls\n// ──────────────────────────────────────────────\n\nconst attackSlider = document.getElementById('attack');\nconst decaySlider = document.getElementById('decay');\nconst sustainSlider = document.getElementById('sustain');\nconst releaseSlider = document.getElementById('release');\nconst attackVal = document.getElementById('attackVal');\nconst decayVal = document.getElementById('decayVal');\nconst sustainVal = document.getElementById('sustainVal');\nconst releaseVal = document.getElementById('releaseVal');\nconst waveformSelect = document.getElementById('waveform');\n\nfunction updateEnvelope() {\n const a = attackSlider.value / 100;\n const d = decaySlider.value / 100;\n const s = sustainSlider.value / 100;\n const r = releaseSlider.value / 100;\n synth.set({ envelope: { attack: a, decay: d, sustain: s, release: r } });\n attackVal.textContent = a.toFixed(2) + 's';\n decayVal.textContent = d.toFixed(2) + 's';\n sustainVal.textContent = s.toFixed(2);\n releaseVal.textContent = r.toFixed(2) + 's';\n}\n\nattackSlider.addEventListener('input', updateEnvelope);\ndecaySlider.addEventListener('input', updateEnvelope);\nsustainSlider.addEventListener('input', updateEnvelope);\nreleaseSlider.addEventListener('input', updateEnvelope);\n\nwaveformSelect.addEventListener('change', () => {\n synth.set({ oscillator: { type: waveformSelect.value } });\n});\n\n// Octave shift\nconst octUpBtn = document.getElementById('octUp');\nconst octDownBtn = document.getElementById('octDown');\nconst octaveValEl = document.getElementById('octaveVal');\n\nfunction shiftOctave(delta) {\n const newBase = baseOctave + delta;\n if (newBase < 1 || newBase > 6) return; // clamp\n baseOctave = newBase;\n notes = buildNotes(baseOctave, 2);\n\n // Rebuild key→midi map\n for (const k in keyToMidi) delete keyToMidi[k];\n notes.white.forEach((n, i) => { if (KEY_WHITE[i]) keyToMidi[KEY_WHITE[i]] = n.midi; });\n notes.black.forEach((n, i) => { if (KEY_BLACK[i]) keyToMidi[KEY_BLACK[i]] = n.midi; });\n\n // Rerender\n whiteKeyEls = [];\n blackKeyEls = [];\n activeElements.clear();\n renderPiano();\n bindClickEvents();\n (function renderShortcuts() {\n let html = 'White keys: ';\n notes.white.forEach((n, i) => {\n if (KEY_WHITE[i]) html += `<span>${KEY_WHITE[i].toUpperCase()}</span> `;\n });\n html += '<br>Black keys: ';\n notes.black.forEach((n, i) => {\n if (KEY_BLACK[i]) html += `<span>${KEY_BLACK[i].toUpperCase()}</span> `;\n });\n html += '<br>Click keys or press keyboard letters';\n shortcutsEl.innerHTML = html;\n })();\n\n octaveValEl.textContent = 'C' + baseOctave;\n}\n\noctUpBtn.addEventListener('click', () => shiftOctave(1));\noctDownBtn.addEventListener('click', () => shiftOctave(-1));\n\n// ──────────────────────────────────────────────\n// Play a note with visual feedback\n// ──────────────────────────────────────────────\n\nconst activeElements = new Map(); // midi → DOM element\n\nfunction getElByMidi(midi) {\n return document.querySelector(`.key-white[data-midi=\"${midi}\"], .key-black[data-midi=\"${midi}\"]`);\n}\n\nfunction noteOn(midi, velocity = 0.8) {\n if (activeElements.has(midi)) return; // already held\n const freq = Tone.Midi(midi).toFrequency();\n synth.triggerAttack(freq, undefined, velocity);\n const el = getElByMidi(midi);\n if (el) {\n el.classList.add('active');\n activeElements.set(midi, el);\n }\n}\n\nfunction noteOff(midi) {\n const freq = Tone.Midi(midi).toFrequency();\n synth.triggerRelease(freq);\n const el = activeElements.get(midi);\n if (el) {\n el.classList.remove('active');\n activeElements.delete(midi);\n }\n}\n\n// ──────────────────────────────────────────────\n// Mouse events on piano keys\n// ──────────────────────────────────────────────\n\nfunction bindClickEvents() {\n const allKeys = document.querySelectorAll('.key-white, .key-black');\n allKeys.forEach(el => {\n el.addEventListener('mousedown', (e) => {\n e.preventDefault();\n const midi = parseInt(el.dataset.midi);\n noteOn(midi);\n });\n el.addEventListener('mouseup', (e) => {\n e.preventDefault();\n const midi = parseInt(el.dataset.midi);\n noteOff(midi);\n });\n el.addEventListener('mouseleave', (e) => {\n const midi = parseInt(el.dataset.midi);\n if (activeElements.has(midi)) noteOff(midi);\n });\n });\n}\n\nbindClickEvents();\n\n// ──────────────────────────────────────────────\n// Keyboard events\n// ──────────────────────────────────────────────\n\nconst heldKeys = new Set(); // track physical keys held\n\ndocument.addEventListener('keydown', (e) => {\n if (e.repeat) return;\n const key = e.key.toLowerCase();\n if (heldKeys.has(key)) return;\n const midi = keyToMidi[key];\n if (midi !== undefined) {\n e.preventDefault();\n heldKeys.add(key);\n noteOn(midi);\n }\n});\n\ndocument.addEventListener('keyup', (e) => {\n const key = e.key.toLowerCase();\n heldKeys.delete(key);\n const midi = keyToMidi[key];\n if (midi !== undefined) {\n e.preventDefault();\n noteOff(midi);\n }\n});\n\n// ──────────────────────────────────────────────\n// Init Tone on first interaction\n// ──────────────────────────────────────────────\n\ndocument.addEventListener('click', async () => {\n if (Tone.context.state !== 'running') await Tone.start();\n}, { once: true });\n\ndocument.addEventListener('keydown', async () => {\n if (Tone.context.state !== 'running') await Tone.start();\n}, { once: true });\n\n// Set initial values\nupdateEnvelope();\noctaveValEl.textContent = 'C' + baseOctave;\n</script>\n</body>\n</html>" }

assistant
thinking
The write tool expects `path` not `file_path`. Let me fix that.
write /workspace/index.html (15657 bytes)
toolResult

Successfully wrote 15657 bytes to /workspace/index.html

assistant
thinking
The file was written successfully. Let me verify it's there and check the tone.min.js is also present.
bash {"command":"ls -lh /workspace/index.html /workspace/tone.min.js"}
toolResult

-rw-r--r-- 1 node node 17K Jul 10 00:31 /workspace/index.html -rw-r--r-- 1 node node 341K Jul 10 00:27 /workspace/tone.min.js

assistant
thinking
Both files are present. Let me give a summary of what was built.

Done. Both files are in place: | File | Size | |---|---| | `index.html` | 17 KB | | `tone.min.js` | 341 KB (pre-provided) | ### What's built **`/workspace/index.html`** — a self-contained piano keyboard app: **Synth engine** — `Tone.PolySynth` wrapping `Tone.Synth` with a full ADSR envelope (`attack`, `decay`, `sustain`, `release`) and 4 oscillator shapes (sine, triangle, sawtooth, square). All configurable live via sliders. **Keyboard** — 15 white keys + 10 black keys spanning C4 → C6 (two octaves plus a top C). Keys are rendered with HTML/CSS, black keys absolutely positioned over the gaps between whites. Each key is labeled with its computer-key mapping. **Mouse control** — click/tap any key to play it; release or drag off to stop. Pressed keys show a blue highlight and depress 2px for tactile feedback. **Computer keys** — | Row | Keys | |---|---| | White | `A S D F G H J K L ; ' Z X C V` | | Black | `W E T Y U O P` | Keyboard input uses `e.repeat` filtering and a `heldKeys` Set to prevent re-triggers on held keys, and `Tone.start()` is called on first interaction to comply with browser autoplay policy. **Controls panel** — sliders for Attack (0–1s), Decay (0–1s), Sustain (0–1), Release (0–2s), a waveform dropdown, and ± octave shift buttons with range clamping (C1–C6). All changes apply instantly to the running synth.

TAKE IT TO CODE

A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.

RUN

modeldeepseek-v4-pro
vendorDeepSeek
harnesspi-agent
generated2026-07-10

EVALUATION

3/5 partial

Piano UI with ADSR controls renders well; drag highlights a key but click/keyboard inputs show no visible feedback.

Vision grade · anthropic/claude-sonnet-5

MEASUREMENTS

Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.

idle motion0.0%
on click0.0%
on drag1.6%
on wheel1.6%
on enter + space0.0%
on arrow keys0.0%
on w/a/s/d0.0%
frame spread70.3 / 255
console errors0
js errors none

METRICS

runtime240.2s
tokens in21k
tokens out12k
cached35k
cost / run$0.0198