The MidiWarp Story
Inspiration
It started with a simple problem: I wanted to map my keyboard's pitch bend wheel to CC11 (Expression) so I could control dynamics in my DAW without buying new hardware. Straightforward enough... except every solution I found either required deep DAW-specific configuration, or pointed me to Bome MIDI Translator, the de facto standard tool for this kind of MIDI remapping.
Bome works. But it costs €79 for the version that lets you write custom scripts. And the interface looks like it shipped in 2005.
That felt wrong. MIDI transformation is fundamentally just logic, if this event comes in, send this event out. That should be free, fast, and approachable. So we built MidiWarp.
What We Learned
Building MidiWarp taught us a lot about the gap between how MIDI looks on paper and how it actually behaves at runtime.
MIDI is messier than it seems. Midi is clean on paper. It's a handful of message types, a handful of bytes. In practice however, you're dealing with OS-level audio services, virtual port drivers, and timing-sensitive polling loops. Getting events to flow from a physical keyboard through a Python process and back out to a DAW without dropouts or latency required careful threading design.
The scripting model matters more than the scripting language. Our first instinct was to build a custom DSL. We scrapped it. Plain Python with a clean context API turned out to be more powerful and far easier to reason about. The insight was that the variables are the interface, not the syntax. If a user can write:
if event_type == 'note_on' and note == C5:
note = D5
...they don't need to learn anything new. The math of a pitch-to-CC mapping is just linear interpolation:
$$ \text{cc_val} = \max\left(0,\ \left\lfloor \frac{\text{pitch}}{8191} \times 127 \right\rfloor\right) $$
That's it. No DSL required.
Shipping is a feature. A tool that only works on the developer's machine isn't a tool. We spent real time on the startup sequence: detecting loopMIDI, creating the virtual port in the registry, restarting the Windows MIDI service. This way, a user who has never heard of a virtual MIDI driver can install MidiWarp and be playing in under a minute.
How We Built It
MidiWarp is a Python desktop app built on pywebview, which lets us use a standard HTML/CSS/JS frontend rendered in a native Windows window. The UI communicates with Python through pywebview's JS bridge.
The MIDI pipeline is:
$$ \text{Physical Keyboard} \xrightarrow{\text{rtmidi}} \text{MidiWarp Engine} \xrightarrow{\text{scripts}} \text{loopMIDI virtual port} \xrightarrow{} \text{DAW} $$
A background thread polls the input port at 1ms intervals. Each incoming message is run through the enabled script pipeline. Each script gets a mutable context of human-readable variables and can mutate them, emit additional messages, or suppress the original entirely. The result is forwarded to the loopMIDI output port.
The script editor, event feed, waveform display, and AI generation feature (powered by the Gemini API) all live in the HTML frontend and communicate with the Python backend through async API calls.
The bundled installer was built with Inno Setup, and the app is packaged with PyInstaller using --uac-admin so the Windows MIDI service restart on launch works without prompting mid-session.
Challenges
The Windows MIDI stack is fragile. After creating a new loopMIDI port in the registry, the Windows MIDI service needs to be restarted before it recognizes the new port, and loopMIDI itself needs to be restarted to load it. Getting this sequence right, in the right order, with proper polling to confirm each step completed before moving to the next, took more iteration than expected.
pywebview's drag and resize behavior on Windows. Frameless windows look great but give up all the native window management for free. -webkit-app-region: drag doesn't work with pywebview's EdgeChromium backend on Windows when easy_drag=False, so we implemented manual drag tracking using screenX/Y deltas and win32gui for the hide-to-tray behavior.
Making scripting approachable without dumbing it down. The balance between "simple enough for a non-programmer" and "powerful enough for real use cases" is genuinely hard. Our answer was the mutation model. Essentially scripts that read and write plain variables feel like configuration, but they're running real Python, so there's no ceiling on what they can do.
Built With
- css
- google-gemini-api
- html
- inno
- javascript
- loopmidi
- mido
- psutil
- pyinstaller
- python
- python-rtmidi
- pywebview
- win32gui
- winreg
Log in or sign up for Devpost to join the conversation.