Play Heartopia's in-game piano with a real MIDI keyboard.
This is a small Python bridge that listens for MIDI note events from a USB MIDI controller and sends the matching QWERTY keystrokes that Heartopia's piano expects. Plug in your keyboard, run the script, focus the piano UI in Heartopia, and play normally — middle C on your MIDI plays middle C in-game, sharps and all.
MIDI keyboard midi_piano.py Heartopia
┌──────────────┐ ┌─────────────────┐ ┌─────────────┐
│ press C4 │ ───► │ MIDI 60 → 'z' │ ───► │ plays DO │
└──────────────┘ │ send keystroke │ │ (middle C) │
└─────────────────┘ └─────────────┘
The bridge:
- Reads MIDI note-on / note-off events with
pygame.midi. - Looks up the QWERTY key Heartopia expects for that MIDI note.
- Synthesizes a global key press / release with
pynputso the game receives it as normal typing.
- Windows (works on macOS / Linux too in principle, but Heartopia is Windows)
- Python 3.10+ — 3.12 is the most well-tested. 3.13 works with the current
pygame.midibackend. - A USB MIDI keyboard that Windows recognizes
- Heartopia installed and running
git clone https://github.com/sp0oby/heartopia-midi.git
cd heartopia-midi
pip install -r requirements.txtDependencies:
pygame>=2.5.0— MIDI input (and has Python 3.13 wheels, unlikepython-rtmidi)pynput>=1.7.6— synthetic keystroke injection
-
Plug in your MIDI keyboard. Wait a couple seconds for Windows to recognize it.
-
Run the bridge:
python midi_piano.py
You should see:
Using MIDI input: <your keyboard name> Focus Heartopia's piano UI, then play. Ctrl+C to quit. -
Launch Heartopia, open the piano, click the piano UI so it's focused.
-
Play your MIDI keyboard. Notes play in-game in real time.
-
Ctrl+C in the terminal to stop cleanly (all held keys are released).
Want to confirm the bridge works before launching Heartopia?
- Start the script.
- Open Notepad and click into the blank area.
- Press middle C on your MIDI keyboard — the letter
zshould appear in Notepad.
If letters appear in Notepad, the bridge is fine and any issue is on Heartopia's side. If not, see Troubleshooting.
All knobs live at the top of midi_piano.py.
Shifts every incoming MIDI note up or down by N octaves before mapping. Useful for compact MIDI controllers (25/49-key) whose default range doesn't sit naturally inside Heartopia's C3 → C6.
| Value | Effect |
|---|---|
-2 |
Drops 2 octaves |
-1 |
Drops 1 octave |
0 |
No shift (default) |
+1 |
Raises 1 octave |
+2 |
Raises 2 octaves |
You can also just use the OCTAVE +/− buttons on the MIDI keyboard itself — that's usually the easier approach for live playing.
DEBUG = True # prints every incoming MIDI note to the terminal
DEBUG = False # silentLeave it on while you dial in OCTAVE_SHIFT, then turn it off for clean play.
Heartopia covers a full chromatic 3-octave range. The bridge maps each MIDI note in C3–C6 to the QWERTY key the game expects.
| Note | MIDI | Key | Note | MIDI | Key | |
|---|---|---|---|---|---|---|
| C5 | 72 | Q |
C♯5 | 73 | 2 |
|
| D5 | 74 | W |
D♯5 | 75 | 3 |
|
| E5 | 76 | E |
||||
| F5 | 77 | R |
F♯5 | 78 | 5 |
|
| G5 | 79 | T |
G♯5 | 80 | 6 |
|
| A5 | 81 | Y |
A♯5 | 82 | 7 |
|
| B5 | 83 | U |
||||
| C6 | 84 | I |
| Note | MIDI | Key | Note | MIDI | Key | |
|---|---|---|---|---|---|---|
| C4 | 60 | Z |
C♯4 | 61 | S |
|
| D4 | 62 | X |
D♯4 | 63 | D |
|
| E4 | 64 | C |
||||
| F4 | 65 | V |
F♯4 | 66 | G |
|
| G4 | 67 | B |
G♯4 | 68 | H |
|
| A4 | 69 | N |
A♯4 | 70 | J |
|
| B4 | 71 | M |
| Note | MIDI | Key | Note | MIDI | Key | |
|---|---|---|---|---|---|---|
| C3 | 48 | , |
C♯3 | 49 | L |
|
| D3 | 50 | . |
D♯3 | 51 | ; |
|
| E3 | 52 | / |
||||
| F3 | 53 | O |
F♯3 | 54 | 0 |
|
| G3 | 55 | P |
G♯3 | 56 | - |
|
| A3 | 57 | [ |
A♯3 | 58 | = |
|
| B3 | 59 | ] |
Notes outside MIDI 48–84 are dropped silently. Use OCTAVE_SHIFT (or your controller's octave buttons) to bring them into range.
Windows doesn't see your keyboard. Check:
- Is the USB cable a data cable, not power-only?
- Does the MIDI keyboard show up in Device Manager under "Sound, video and game controllers"?
- Some controllers need a vendor driver before they enumerate over MIDI — check the manufacturer's site.
If you're on Python 3.13 and previously installed python-rtmidi, that package's C extension crashes silently on 3.13. This project uses pygame.midi instead specifically to avoid that. Make sure your requirements.txt is the current one and reinstall:
pip install -r requirements.txtSome Unity / Unreal games use raw or DirectInput and ignore pynput's synthetic keystrokes. The fix is to swap the input backend to pydirectinput, which sends scancode-level events most games accept. Open an issue if you hit this and I'll add a switch.
Already handled. The bridge ignores repeat note_on events for a key it's already holding — some MIDI controllers re-trigger on aftertouch and this used to cause doubles.
- Is the piano UI in Heartopia focused? Click on it.
- Run with
DEBUG = Trueand play a key. Ifkey='z'(or whatever) prints, the bridge is working — it's a game-side focus issue. - If
key=Noneprints, your note is outside the C3–C6 range. AdjustOCTAVE_SHIFT.
MIT.