Skip to content

HLammers/multi-midi

Repository files navigation

multi-midi

© 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:

Other Documentation Documents:


How to Use multi-midi

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.

1. Installation

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).

2. Basic Setup

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.

3. Receiving MIDI Messages

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)

4. Sending MIDI Messages

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)

5. Asynchronous Event Loop

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())

6. Advanced: Message Filtering & MIDI Real-Time Routing

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))

Example Project

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.


Further Customization

  • 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 (see log.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.

Overview of multi-midi Library Modules

Main Modules

midi_manager.pyPrimary 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.pyMid-level multi-port asynchronous USB MIDI 1.0 device implementation for MicroPython on RP2.
singleton.pySingleton decorator to restricts a class to a single instance.

Modules With Utility Functions and Helper Classes

midi_test_data.pyDummy data generators for testing MIDI data streams.
midi_monitor.pyAsynchronous MIDI monitor based on the Log class.
log.pyConfigurable asynchronous logger using second core as context for writing to REPL, file and/or screen.
display.pyFrameBuffer based asynchronous display driver implementation for ILI9225 on RP2040/RP2350 based boards (using DMA).
context.pyContext manager to asynchronously run functions or methods on the second core (using _threads).
timed_function.pyDecorator to time a function and log the result using the Log class (if available).

Overview of Key Classes and Module-Level Functions

MIDI Manager Classes (midi_manager.py)

These are the main classes which are relevant when using multi-midi.

MidiManagerSingleton asynchronous MIDI I/O manager for multi-port UART, PIO and USB MIDI.
InPortUARTSingle port handler for hardware UART based MIDI IN port.
Use MidiPort.add_uart_in() to set up (do not instance directly).
InPortPIOSingle port handler for PIO UART based MIDI IN port.
Use MidiPort.add_pio_in() to set up (do not instance directly).
InPortUSBSingle port handler for USB MIDI virtual cable based MIDI IN port.
Use MidiPort.add_usb_in() to set up (do not instance directly).
OutPortUARTSingle port handler for hardware UART based MIDI OUT port.
Use MidiPort.add_uart_out() to set up (do not instance directly).
OutPortPIOSingle port handler for PIO UART based MIDI OUT port.
Use MidiPort.add_pio_out() to set up (do not instance directly).
OutPortUSBSingle port handler for USB MIDI virtual cable based MIDI OUT port.
Use MidiPort.add_usb_out() to set up (do not instance directly).
MidiFilterMIDI 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.

DummyStreamReaderUARTDummy stream reader for testing purposes, simulating data coming from a hardware MIDI IN port.
DummyReaderUSBDummy data generator simulating data coming from a USB MIDI IN port.
DummyBufferStreamDummy 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.

MidiMonitorSingleton 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.

ContextSingleton 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.

DisplayFrameBuffer based asynchronous display driver implementation for ILI9225 on RP2040/RP2350 based boards (using DMA).

Timed Function Decorator (timed_function.py)

timed_functionDecorator 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.

About

MicroPython library for multi-port UART, PIO and USB MIDI on RP2040/RP2350 based boards

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages