TL;DR: I bought a Roland FP-90X piano partly because it had Bluetooth MIDI. Pairing it with my Windows 11 PC succeeded. Notes never made it to my DAW. Notes I sent from the PC never made it to the piano. Three completely different bugs were stacked on top of each other. I shipped a free, MIT-licensed open-source bridge (Avalonia, .NET 10) that handles all of them: github.com/mayerwin/Perfect-Bluetooth-MIDI-For-Windows. This post is the debugging story.
The setup
I have a Roland FP-90X (a digital piano with Bluetooth MIDI built in) and a Windows 11 PC. I wanted the obvious workflow:
- Pair the piano with the PC over Bluetooth.
- Open my DAW (or any Web MIDI app in Chrome).
- Pick the piano as a MIDI input/output.
- Play.
Windows happily showed the piano as a Bluetooth device. Device Manager listed it. The Bluetooth icon was solid. Nothing red, nothing yellow. And yet the DAW saw nothing.
Sending notes the other way (PC to piano) had an equally infuriating symptom: every NoteOn I sent at the GATT layer was acknowledged. ATT-layer success. The piano just didn't make any sound.
A few hours of binary search later, I had separated this into three distinct bugs.
Bug 1: Windows only exposes BLE-MIDI through WinRT
This is the one most people hit first.
Windows has multiple MIDI APIs:
- The legacy Multimedia Extensions (winmm) API, which is what almost every DAW on Windows still uses. It dates back to the 90s.
- The WinRT MIDI API (
Windows.Devices.Midi), introduced for UWP apps starting with Windows 10. - The brand-new Windows MIDI Services stack (
Microsoft.Windows.Devices.Midi2), which is the future and is currently rolling out in Windows 11 24H2 and 25H2.
Microsoft has explicitly stated that BLE-MIDI is exposed only via WinRT, not via winmm. So when your DAW polls winmm for MIDI ports, the BLE keyboard you just paired is invisible to it. Pairing succeeded. The MIDI just isn't reachable.
Most of the existing workarounds for this stack a couple of programs:
- MIDIberry acts as a WinRT-aware BLE-MIDI client.
- loopMIDI creates a virtual winmm port pair.
- You configure MIDIberry to send to one end of the loopMIDI pair, and tell your DAW to listen on the other.
It works, but it's fragile. The 20-second-disconnect bug is a particularly common gripe. The symptom is well-documented on Microsoft Q&A, and the community-diagnosed root cause (which Microsoft hasn't officially confirmed) is that Windows keeps polling for a battery service the device doesn't expose, the BLE link gets renegotiated, and MIDI silently drops until you reconnect. Korg's BLE-MIDI driver is another common workaround, but it's a winmm-shim driver that has aged poorly on Windows 11.
I wanted a single-app solution. Conveniently, the new Windows MIDI Services stack ships with a feature called loopback endpoints: you create a pair of MIDI ports inside WMS, anything written to one comes out the other, and any winmm/WinRT/WMS app can see them as normal MIDI ports. Bridge BLE-MIDI to one side of the loopback, and the DAW sees a regular MIDI device on the other.
So that's what the app does:
[BLE keyboard] --WinRT BLE-MIDI--> [my app] --WMS loopback--> [your DAW]
The BLE side uses Windows.Devices.Midi.MidiInPort. The WMS side uses Microsoft.Windows.Devices.Midi2. One process, no virtual port plumbing for the user.
That solved direction one (piano to PC). Direction two (PC to piano) still didn't work.
Bug 2: ATT Success means nothing
I started writing NoteOn messages from the app to the piano. I could see them go out. The Bluetooth GATT write returned Success. The piano didn't make a sound.
Before going deeper I needed a sanity check that the BLE-MIDI link from PC to piano was even possible in principle. So I tried Roland's own Piano App, which lets you play full songs through the piano over Bluetooth. Single-song playback worked, the piano was clearly happy to receive MIDI from this PC over Bluetooth. (Accompaniments in the same app didn't, which looked like a bug in the Roland app rather than the link itself.) That was enough to rule out the BLE link as the culprit and pin the failure on something specific to what I was sending.
I assumed I was sending malformed BLE-MIDI packets. I checked the timestamp encoding. I checked the running-status handling. I tried WriteWithoutResponse. I tried WriteWithResponse (some BLE-MIDI firmware silently drops one or the other). I poked at the proprietary ISSC characteristic on the device. Every variant ATT-acked, every variant produced silence.
So the bytes were almost certainly reaching the piano. Something above the GATT layer was choosing not to play them.
Bug 3: The Roland that silently receives on the wrong channel
After ruling out pairing, encryption, write-mode, and proprietary characteristics, the only obvious lever I had left was the MIDI channel itself. A NoteOn that's GATT-acked but produces no sound is exactly what you'd see if the synth engine was listening on a different channel from the one I was sending on.
The FP-90X has a panel setting called Transmit Channel. By default it's 1. The manual says it's both the channel the piano transmits on and (implicitly) the channel it listens on. So I'd been sending on channel 1, like a reasonable person.
It turns out the FP-90X actually receives on channel 4 by default, regardless of what its panel says. I have no idea why. The transmit channel and receive channel are not the same setting on this piano, even though only one of them is exposed in the UI. Notes I sent on channel 1 were being received at the GATT layer, ATT-acked, and then silently dropped by the synth engine because they weren't on the channel the engine was listening to.
I haven't personally verified this on other Roland FP-X pianos, but I wouldn't be surprised if some of them, or other manufacturers' pianos, have similar quirks.
This was the most painful bug because there was zero feedback. Every layer below "the synth makes a sound" reported success. The fix had to live up at the application layer.
I added a Detect button. When you click it, the app:
- Picks each MIDI channel from 1 to 16 in turn.
- On each channel, plays N test notes ascending (e.g. 5 notes for channel 5).
- You count the notes you actually heard.
- The number you heard is the receive channel.
It saves the result keyed by the device's BLE MAC address, so you only ever do this once per piano.
Total time to discover the right channel: about 75 seconds. Total time to figure out you needed to do it in the first place: a regrettable number of evenings.
Bug 4 (honourable mention): pairing and encryption
While debugging Bug 3, I tripped over a fourth class of issue. Some BLE-MIDI devices won't accept MIDI until the link is fully encrypted. They'll let you connect and subscribe to notifications, but writes get silently dropped until the bond completes. The fix is to pair proactively before enabling notifications, rather than waiting for Windows to "lazy bond" later.
A few firmware revisions also only respond to one of WriteWithResponse and WriteWithoutResponse. Without correlating to a working device, you'd never know which.
Both behaviors exist purely because the BLE-MIDI spec doesn't dictate either, so the manufacturers diverged.
What the app actually does
To recap, here's what I had to handle, in approximate order of pain:
- Bridge WinRT BLE-MIDI to Windows MIDI Services loopback, so DAWs see a normal port without needing MIDIberry + loopMIDI.
- Pair proactively before enabling notifications, so encryption-required devices don't drop writes.
- Prefer
WriteWithResponse, fall back toWriteWithoutResponseonly if necessary. - Detect the actual receive MIDI channel of the piano, persist per BLE MAC.
- Cleanly unpair on exit so your phone (or another PC) can reconnect to the piano without first kicking the bonded entry on the piano side.
- Surface a verbose diagnostics log with status-byte names and hex, plus the full GATT service/characteristic tree on connect, so when something doesn't work the user can post a log and someone can actually help.
It's MIT, .NET 10, Avalonia. Single self-contained ~21 MB exe.
- Site (with screenshots): mayerwin.github.io/Perfect-Bluetooth-MIDI-For-Windows
- Source: github.com/mayerwin/Perfect-Bluetooth-MIDI-For-Windows
What I'd love to learn
I've personally only tested with my FP-90X. The library is generic, and similar pianos (FP-30X, FP-60X, FP-E50, RP701, F701) almost certainly have the same channel-mismatch quirk. Other BLE-MIDI keyboards (WIDI Master, CME, Yamaha MD-BT01, Korg microKey Air, ROLI Seaboard) should work because the protocol layer is shared, but each device has its own tiny bag of behavior.
If you have one of those and a Windows 11 PC, I'd be very grateful for an issue or a PR with what you saw. Especially:
- Whether the Detect flow finds the right channel.
- Whether your device needs encryption or
WriteWithResponse. - Whether the app's Save log… output captures enough to debug your case.
Microsoft has said BLE-MIDI is on the Windows MIDI Services roadmap as a first-class transport. When that ships, this app will become unnecessary, which is the goal. Until then, hopefully it saves someone else the evenings I lost.
Thanks for reading. If you're stuck on Bluetooth MIDI on Windows, I hope this is useful.
Top comments (0)