© 2025 Harm Lammers
For questions or contributions open an issue or start a discussion on the GitHub repository.
multi-midi is a MicroPython library for working with multiple MIDI ports on RP2040/RB2350 based hardware like the Raspberry Pi Pico (2). It supports both hardware MIDI (UART/PIO) and USB MIDI 1.0, allowing applications to handle several MIDI IN and OUT ports at once. The main interface is provided by midi_manager.py
, which manages port configuration, message routing and asynchronous event handling.
USB MIDI support is optional and depends on midi_usb.py
; if only hardware MIDI is needed, this dependency can be omitted. The library uses a singleton pattern (see singleton.py
) to ensure MIDI resources are managed centrally. Additional modules provide utility functions and helper classes. The included example.py
demonstrates usage of midi_manager
and is designed for the Cybo-Drummer hardware, but can be adapted for other hardware setups.
Features:
- Supports multiple MIDI IN and OUT ports (UART, PIO and USB)
- Handles real-time SysEx and standard MIDI messages
- Handles running status for hardware MIDI IN and OUT
- Manages the prioritization of MIDI Real-Time messages
- Configurable MIDI message filtering (message type, channel and CC value)
- Configurable fast-track MIDI Real Time message routing (one source IN port to many destination OUT ports)
- Asynchronous, non-blocking event processing
- Modular structure for integration into custom hardware projects
- Highly optimized code to minimize latency
- Example code for quick testing and adaptation
Important
I’m still considering whether I’ve followed the right approach (using ayncio
asynchronous processing) – see multi-midi Process Map for more on this. There is a chance that this library will be rewritten and restructured in the future.
Table of Contents:
- How to Use multi-midi
- Overview of multi-midi Library Modules
- Overview of Key Classes and Module-Level Functions
Other Documentation Documents:
The core of multi-midi is the midi_manager.py
module, which provides an easy-to-use interface for configuring, routing and processing MIDI data across multiple MIDI IN and OUT ports. This section guides you through integrating midi_manager
into your own MicroPython application for RP2040/RP2350 based hardware.
Copy the multi-midi
library files (midi_manager.py
, midi_usb.py
and singleton.py
) into your MicroPython project Folder.
midi_usb.py
can be omitted if you only need hardware MIDI (UART/PIO).
Import and initialize the MIDI manager in your main application script:
from midi_manager import MidiManager
# Create the singleton MIDI manager instance
midi = MidiManager()
# Set USB device’s manufacturer name, product name and/or serial number
# TIP: use machine.unique_id() to get a byte string with a unique identifier of a board/SoC to use as serial_str
# midi.set_usb_strings('TestMaker', 'TestMIDI', machine.unique_id())
# Configure MIDI IN and OUT ports
midi.add_uart_in(0) # in_ports[0] (UART0, default pins)
midi.add_pio_in(0, 6) # in_ports[1] (PIO 0, GPIO 6)
# To add a USB MIDI IN port (if midi_usb.py is available):
# midi.add_usb_in(0, 'Test Port') # in_ports[2] (virtual cable 0, port name ‘Test Port’, with External Jack)
midi.add_uart_out(0) # out_ports[0] (UART0, default pins)
midi.add_pio_out(4, 10) # out_ports[1] (PIO 4, GPIO 10)
# To add a USB MIDI OUT port (if midi_usb.py is available):
# midi.add_usb_out(0) # out_ports[2] (virtual cable 0, port name ‘Test Port’, with External Jack)
Note
On Internal and External Jacks
One of the building block of a USB MIDI 1.0 setup are so called ‘Jacks’. Each MIDI IN and OUT port is linked to one or a set of Jacks. They come in two flavours: Embedded Jacks are described in the USB 1.0 device class definition as to represent ‘virtual’ ports inside the device (e.g. a software synth), External Jacks are described as to represent physical connectors on the device (DIN, TRS, etc.). USB MIDI 1.0 requires at least Embedded Jacks, optionally linked to External Jacks.
The often found advice to always add both Embedded and External Jacks seems to come from a bug in early versions of iOS, which only worked if both were provided. This has long been resolved (apparently since iOS 7, released in 2013), so that is no longer relevant.
To be fully compliant with the USB MIDI specifications it would be best to use Embedded plus External Jacks for ports which map to MIDI hardware connectors and only Embedded Jacks for all other cases, although functionally it doesn’t matter which one is used.
Set up callbacks to process incoming MIDI messages:
def handle_midi_data(port_id, byte_0, byte_1=0, byte_2=0):
print(f'Received from port {port_id}: {byte_0:02x} {byte_1:02x} {byte_2:02x}')
# Register the callback for all IN ports
midi.asign_callbacks(cb_data=handle_midi_data)
Send MIDI messages to any configured OUT port:
# Send a Note On message (channel 0, note 60, velocity 100) via port 0
port = midi.out_ports[0]
port.write_data(0x90, 60, 100)
Set up an asyncio
main event loop and run it:
# Define asyncio event loop
async def main():
# Start the midi manager
await _midi_manager.run()
# ... other application logic ...
await asyncio.Event().wait() # keep alive (wait eternally)
# Run the asyncio event loop
try:
asyncio.run(main())
You can filter messages by type, channel or CC number, and set up fast-track routing of MIDI Real-Time messages, e.g.:
# Only accept Note On and Note Off on port 0
port = midi.in_ports[0]
port.set_all_type_filters(True) # block all MIDI message types
port.set_type_filter(0x90, False) # do not block Note On
port.set_type_filter(0x80, False) # do not block Note Off
# Filter out channel 16 on port 0
port.set_channel_filter(15, True) # in Python we count from 0, so 16 becomes 15
# Filter out Bank Select messages on port 0
port.set_cc_filter(0, True) # CC 0 is Bank Select MSB
port.set_cc_filter(32, True) # CC 0 is Bank Select LSB
# Route MIDI Real-Time messages from MIDI IN port 0 to MIDI OUT ports 1 and 2
midi.set_midi_real_time_routing(0, (1, 2))
Check out example.py
for a working demonstration using the Cybo-Drummer hardware. It shows how to set up ports, callbacks and the main event loop. You can adapt this example for your hardware setup.
- Use the modular structure to add your own processing logic or hardware abstraction.
- For full documentation of method parameters and available port types, refer to the source code comments in
midi_manager.py
.
Tip
- To avoid increasing the MIDI latency too much, try to make input callbacks and other MIDI handling routines run as short as possible, or at least block the
asyncio
scheduler as briefly as possible. - Be careful with adding too many additional
asyncio
tasks, and if you have to (e.g. for user input or a graphic user interface), try to make these as little blocking as possible. - Use
Context
to off-load heavy tasks to the RP2040/RP2350s second core (seelog.py
for an example of how this is used to off-load file and screen writing tasks) - If possible, disable USB MIDI during testing and debugging, otherwise you can’t use the REPL.
Main Modules
midi_manager.py | Primary interface of the Multi-Midi library: Asynchronous MIDI I/O manager module for multi-port UART, PIO and USB MIDI on RP2040/RP2350 based boards. |
midi_usb.py | Mid-level multi-port asynchronous USB MIDI 1.0 device implementation for MicroPython on RP2. |
singleton.py | Singleton decorator to restricts a class to a single instance. |
Modules With Utility Functions and Helper Classes
midi_test_data.py | Dummy data generators for testing MIDI data streams. |
midi_monitor.py | Asynchronous MIDI monitor based on the Log class. |
log.py | Configurable asynchronous logger using second core as context for writing to REPL, file and/or screen. |
display.py | FrameBuffer based asynchronous display driver implementation for ILI9225 on RP2040/RP2350 based boards (using DMA). |
context.py | Context manager to asynchronously run functions or methods on the second core (using _threads ). |
timed_function.py | Decorator to time a function and log the result using the Log class (if available). |
MIDI Manager Classes (midi_manager.py
)
These are the main classes which are relevant when using multi-midi.
MidiManager | Singleton asynchronous MIDI I/O manager for multi-port UART, PIO and USB MIDI. |
InPortUART | Single port handler for hardware UART based MIDI IN port. Use MidiPort.add_uart_in() to set up (do not instance directly). |
InPortPIO | Single port handler for PIO UART based MIDI IN port. Use MidiPort.add_pio_in() to set up (do not instance directly). |
InPortUSB | Single port handler for USB MIDI virtual cable based MIDI IN port. Use MidiPort.add_usb_in() to set up (do not instance directly). |
OutPortUART | Single port handler for hardware UART based MIDI OUT port. Use MidiPort.add_uart_out() to set up (do not instance directly). |
OutPortPIO | Single port handler for PIO UART based MIDI OUT port. Use MidiPort.add_pio_out() to set up (do not instance directly). |
OutPortUSB | Single port handler for USB MIDI virtual cable based MIDI OUT port. Use MidiPort.add_usb_out() to set up (do not instance directly). |
MidiFilter | MIDI filter supporting message type, channel and CC number filtering. |
Log Class (log.py
)
This is a helper class used by example.py
, but – if available (imported) – also used by MidiManager
, which can be particularly useful for debugging when the USB MIDI driver is enabled (which disables the REPL).
Log | `asyncio`-compatible singleton logger which prints the message and can be configured to write to file and/or monitor. |
MIDI Test Data Classes (midi_test_data.py
)
This is a helper class used by example.py
.
DummyStreamReaderUART | Dummy stream reader for testing purposes, simulating data coming from a hardware MIDI IN port. |
DummyReaderUSB | Dummy data generator simulating data coming from a USB MIDI IN port. |
DummyBufferStream | Dummy data generator supplying 3-byte MIDI messages with an option to also generate SysEx data blocks. |
MIDI Monitor Class (midi_monitor.py
)
This is a helper class used by example.py
.
MidiMonitor | Singleton MIDI monitor which uses the Log singleton class to log MIDI messages. |
Context Class (context.py
)
This is a helper class used by log.py
.
Context | Singleton context manager to asynchronously run functions or methods on the second core (using _threads ). |
Display Class (display.py
)
This is a display driver class used by log.py
.
Display | FrameBuffer based asynchronous display driver implementation for ILI9225 on RP2040/RP2350 based boards (using DMA). |
Timed Function Decorator (timed_function.py
)
timed_function | Decorator function to time a function and log the result using the Log class (if available). Gives the average of each time the function was called. |