Skip to content

bledev.midi: BLE-MIDI as a MIDI port, with a portable codec - #85

Merged
bdbarnett merged 5 commits into
mainfrom
bledev-midi
Sep 25, 2026
Merged

bdbarnett merged 5 commits into
mainfrom
bledev-midi

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

The first profile after bledev v1 (docs/ble.md §7 in the workspace repo). Stacked on bledev-host (#81, itself on #78), and it carries #84's three commits (the reconnect and REPL-output fixes), because midi.connect() uses #84's retry. Merge #84 first and this diff shrinks to the MIDI commits.

What you can do with it

import bledev.midi as midi
port = await midi.serve(ble, name="synth")    # on the board
port = await midi.connect(ble, name="synth")  # on a laptop or another board
port.write(b"\x90\x3c\x64")
n = port.read(buf)                            # plain MIDI bytes, never blocks

The port is usbif's MidiPort contract (and a usbif.MidiPort subclass when usbif is installed), so a loop written for a USB MIDI controller, feeding usbif.MidiParser, runs unchanged on a BLE one. await port.receive() also gives you the sender's timestamp. It's the standard BLE-MIDI service and characteristic, so Macs, phones and controllers speak it too.

The codec (bledev.midi_codec) is pure Python with no imports: 13-bit timestamps and their rollover, running status (accepted across packets, sent only within one), SysEx split at the packet size, real-time slipped into a SysEx. Its 31 vectors in tests/bledev_midi_vectors.json were worked out by hand from the packet format, not captured from the code, and both interpreters read the same file.

Gates, run 2026-09-24

  • Codec: 66/66 on CPython and unix MicroPython (vectors plus a seeded round trip of 24,000 messages at random packet sizes). Five planted codec faults (missed rollover, running status lost between packets, a skipped SysEx byte, a header without its timestamp bits, real-time ending a SysEx) each fail.
  • Fake radio: 47/47 contract checks on both interpreters, including 1,000 mixed messages each way; a planted bit flip in one packet in 50 fails. Full suite 547 tests OK.
  • Board to board (LCD-7 serving, T-Embed connecting), default and 7.5 ms intervals, and from the laptop: 1,000 mixed messages (notes, CC, pitch bend, program change, clock, a 300-byte SysEx every hundred) each way, complete, in order and intact. Planted on the air, a flipped bit going up, a dropped packet going down and a flipped bit going down each FAIL.

Latency BLE adds, one way (half a note-on's round trip through an echo, 400 notes at random moments):

Link Median p90 p99 Max
S3 to S3, default interval 44.3 ms 44.3 49.8 69.3
S3 to S3, 7.5 ms 10.8 ms 13.6 15.9 21.1
Laptop to S3, Windows' defaults 52.3 ms 60.0 110.7 118.1
Laptop to S3, throughput parameters 13.4 ms 16.7 23.1 24.7

Players feel about 10 ms, so BLE by itself uses that budget even at the fastest interval. The codec costs 0.5 ms a note on the S3, about 1 ms of the 10.8.

Windows as a MIDI host: once paired, Windows lists the board as bledev-midi (Bluetooth MIDI IN/OUT) through WinRT's MIDI device interfaces. winmm doesn't see it, and this build's Windows MIDI Services has no Bluetooth transport, so a winmm DAW can't open it. serve() now sets the GAP name; before, Windows called it MPY ESP32.

Not done:

  • Pairing and bonding aren't in bledev: the Windows check bonded by hand (ble.config(bond=True) plus aioble's security), then unpaired.
  • How the 10.8 ms splits between connection events and asyncio scheduling isn't measured; that needs a raw GATT echo at 7.5 ms beside it.
  • The DAW loop harness (docs/ble.md §7) hasn't been pointed at BLE MIDI; with no winmm port it can't be yet.

flush() could be re-entered by the retry timer's callback, which sent one
chunk twice and dropped the next: a 16,890-character print came back corrupt
five times out of five. And re-arming a one-shot machine.Timer while its
alarm was being dispatched panicked the S3 in esp_timer's task. One periodic
timer, never re-armed, and a re-entry guard. The REPL gate checks a long
print.
About one reconnect in fifty from Windows came up dead: bleak's connect
returned in 70-440 ms (a real one takes 1.7 s or more) with the device
reported connected and the session active, the board never saw a
connection, and the first GATT operation hung until Windows gave up nine
seconds later. connect_and_set_up() bounds the setup and tries again; nus
uses it. tests/bledev_board/reconnect_loop.py is the rapid-reconnect gate.
midi_codec is the BLE-MIDI 1.0 packet format in pure Python with no imports:
13-bit timestamps and their rollover, running status (kept across packets
when decoding, used only inside one when encoding), SysEx split at the
packet size, and real-time messages slipped into a SysEx. Its vectors
(tests/bledev_midi_vectors.json) were worked out by hand and run on CPython
and unix MicroPython, with five planted codec faults that must fail.

midi.serve() and midi.connect() hand out a Port that is usbif's MidiPort:
plain MIDI bytes through read(buf) and write(data), so a loop written for a
USB controller runs on a BLE one. When usbif is installed the Port subclasses
usbif.MidiPort. serve() sets the GAP name, which is what a paired host calls
the port.
…easured

1,000 mixed messages each way, complete, in order and intact, board to board
and from the laptop; planted drops and bit flips fail. Half a note-on's round
trip: 10.8 ms median (p99 15.9) between two S3s at a 7.5 ms interval, 44.3 ms
at the default, 13.4 ms from Windows with throughput parameters. Windows lists
a paired board as a MIDI device through WinRT, but not through winmm.
Base automatically changed from bledev-host to main September 25, 2026 05:37
@bdbarnett
bdbarnett merged commit 1b3f125 into main Sep 25, 2026
3 checks passed
@bdbarnett
bdbarnett deleted the bledev-midi branch September 25, 2026 05:38
@Psychlist1972

Copy link
Copy Markdown

JFYI

The Windows MIDI Services updates going into Windows 11 25h2 and higher in November include native BLE MIDI 1.0, and will have BLE MIDI 2.0 as a fast-follow after the spec is ratified.

We make the BLE devices available to WinRT MIDI 1.0 and WinMM just like any other MIDI Port. The apps don't need to know they are BLE or do anything special.

Device compatibility tracking here:
microsoft/MIDI#1173

Pete
Microsoft

@bdbarnett

Copy link
Copy Markdown
Contributor Author

JFYI

The Windows MIDI Services updates going into Windows 11 25h2 and higher in November include native BLE MIDI 1.0, and will have BLE MIDI 2.0 as a fast-follow after the spec is ratified.

We make the BLE devices available to WinRT MIDI 1.0 and WinMM just like any other MIDI Port. The apps don't need to know they are BLE or do anything special.

Device compatibility tracking here: microsoft/MIDI#1173

Pete Microsoft

Thanks Pete. I plan to resume work on BLE next week, and your work is coming at the perfect time! I'll post feedback on your issue 1173 when I have data to report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants