Skip to content

Latest commit

 

History

History
788 lines (470 loc) · 27.5 KB

File metadata and controls

788 lines (470 loc) · 27.5 KB

multi-midi Reference

Part of the multi-midi documentation

© 2025 Harm Lammers

For questions or contributions open an issue or start a discussion on the GitHub repository.


This document provides a detailed description of the most important interface classes and functions in the multi-midi library.

Table of Contents


Core multi-midi Classes

midi_manager.py: Class MidiManager

Asynchronous MIDI I/O manager for multi-port UART, PIO and USB MIDI.

This class has been defined with the @singleton, so only a single MidiManager class can exist and initiating it again return that first instance.


Class Attributes

out_portslist[OutPortUART|OutPortPIO|OutPortUSB]List with all single OUT port handler instances.

Class Methods

MidiManager.set_usb_strings()

Set USB device’s manufacturer name, product name and/or serial number.

Method Arguments

manufacturer_strstr|bytes|bytearray|None, optionalManufacturer name to be assigned to the USB Device
Defaults to None (copy manufacturer name from MicroPython’s built-in driver)
product_strstr|bytes|bytearray|None, optionalProduct name to be assigned to the USB Device
Defaults to None (copy product name from MicroPython’s built-in driver)
serial_strstr|bytes|bytearray|None, optionalUnique serial number to be assigned to the USB Device
Defaults to None (copy serial number from MicroPython’s built-in driver)

Tip

Use machine.unique_id() to get a byte string with a unique identifier of a board/SoC to use as serial_str.


MidiManager.usb_is_active()

asyncio awaitable which releases once the USB host activated the connection or returns immediately if USB is not initiated.


MidiManager.assign_callbacks()

Assign callback functions to be called when receiving MIDI messages.

Method Arguments

cb_rtCallable, optionalCallback for MIDI Real-Time messages
Signature: f(port_id: int, byte: int) -> None.
Defaults to None
cb_sys_exCallable, optionalCallback for MIDI SysEx messages.
Signature: f(port_id: int, buf: bytearray, num_bytes: int) -> None
Defaults to None
cb_dataCallable, optionalCallback for regular MIDI messages (all except Real-Time and SysEx).
Signature: f(port_id: int, byte_0: int, byte_1: int = 0, byte_2: int = 0) -> None
Defaults to None

MidiManager.add_uart_in()

Set up hardware MIDI IN port based on a standard set of RP2040/RP2350 UART pins, which gets the next available IN port ID assigned.

Only call this function once for a given uart_id number.

Method Arguments

uart_idintUART to be used (0: UART0, 1: UART1).
tx_pinint|None, optionalGPIO number to be used for transmitting UART
Defaults to None (0 for UART0, 4 for UART1)
rx_pinint|None, optionalGPIO number to be used for receiving UART
Defaults to None (1 for UART0, 5 for UART1)

Method Returns

intAssigned IN port ID (attribute in_ports index number).

Note

  • UART TX and RX come in pairs, so it is set up the first time an UART ID is used for either adding an IN port or an OUT port.
  • If you’d like to adjust the TX and RX pins of a UART, you should do so the first time you assign an IN or OUT port to that UART.
  • UART0 can only be mapped to GPIO 0/1, 12/13 and 16/17; UART1 can only be mapped to GPIO 4/5 and 8/9.

MidiManager.add_pio_in()

Set up hardware MIDI IN port based on a RP2040/RP2350 PIO state machine, which gets the next available IN port ID assigned.

Only call this function once for a given pio_id number.

Method Arguments

pio_idintPIO state machine ID to be used (RP2040: 0 to 7, RP2350: 0 to 11).
pinintGPIO number to be used.

Method Returns

intAssigned IN port ID (attribute in_ports index number).

MidiManager.add_usb_in()

Set up USB MIDI IN port based on a ‘virtual cable’, which gets the next available IN port ID assigned.

Method Arguments

cableintUSB virtual IN cable number to be assigned.
port_namestr|bytes|bytearray|None, optionalNames to be shown by the host.
The same name is used for IN and OUT ports with the same virtual cable number (overwrites previously defined name, if not None).
Defaults to None
external_jackbool|None, optionalWhether to set up an ‘external jack’ interface
This setting applies to both IN and OUT ports with the same virtual cable number (overwrites previous setting).
Defaults to None (becomes True if never set)

Method Returns

intAssigned IN port ID (attribute in_ports index number).

Note

  • Avoid skipping cable numbers because the missing ones will be set up as well and show up on the host.
  • The Port name needs to be at least 2 characters long (otherwise a Windows host would fail to recognize the device) and is not shown on a Windows host.

MidiManager.add_uart_out()

Set up hardware MIDI OUT port based on a standard set of RP2040/RP2350 UART pins, which gets the next available OUT port ID assigned.

Only call this function once for a given uart_id number.

Method Arguments

uart_idintUART to be used (0: UART0, 1: UART1).
tx_pinint|None, optionalGPIO number to be used for transmitting UART.
Defaults to None (0 for UART0, 4 for UART1)
rx_pinint|None, optionalGPIO number to be used for receiving UART.
Defaults to None (1 for UART0, 5 for UART1)
running_statusbool, optionalSet whether to apply running status when sending out MIDI data.
Defaults to True

Method Returns

intAssigned OUT port ID (attribute out_ports index number).

Note

  • UART TX and RX come in pairs, so it is set up the first time an UART ID is used for either adding an IN port or an OUT port.
  • If you’d like to adjust the TX and RX pins of a UART, you should do so the first time you assign an IN or OUT port to that UART.
  • UART0 can only be mapped to GPIO 0/1, 12/13 and 16/17; UART1 can only be mapped to GPIO 4/5 and 8/9.

MidiManager.add_pio_out()

Set up hardware MIDI OUT port based on a RP2040/RP2350 PIO state machine, which gets the next available OUT port ID assigned.

Only call this function once for a given pio_id number.

Method Arguments

pio_idintPIO state machine ID to be used (RP2040: 0 to 7, RP2350: 0 to 11).
pinintGPIO number to be used.
running_statusbool, optionalSet whether to apply running status when sending out MIDI data.
Defaults to True

Method Returns

intAssigned OUT port ID (attribute out_ports index number).

MidiManager.add_usb_out()

Set up USB MIDI OUT port based on a ‘virtual cable’, which gets the next available OUT port ID assigned.

Method Arguments

cableintUSB virtual OUT cable number to be assigned.
port_namestr|bytes|bytearray|None, optionalNames to be shown by the host.
The same name is used for IN and OUT ports with the same virtual cable number (overwrites previously defined name, if not None).
Defaults to None
external_jackbool|None, optionalWhether to set up an ‘external jack’ interface.
This setting applies to both IN and OUT ports with the same virtual cable number (overwrites previous setting)
Defaults to None (becomes True if never set)

Method Returns

intAssigned OUT port ID (attribute out_ports index number).

Note

  • Avoid skipping cable numbers because the missing ones will be set up as well and show up on the host.
  • The Port name needs to be at least 2 characters long (otherwise a Windows host would fail to recognize the device) and is not shown on a Windows host.

MidiManager.set_midi_real_time_routing()

Set up fast-track routing of MIDI Real-Time messages to the destination MIDI OUT ports when received from the source MIDI IN port.

Method Arguments

sourceintPort ID of the MIDI IN port to be used as source for MIDI Real-Time routing.
destinationslist|tuplePort IDs of the MIDI OUT ports to be used as destination for MIDI Real-Time routing.

MidiManager.run()

asyncio awaitable to start the midi manager: start the USB driver (if USB MIDI ports were defined) and schedule tasks to run each set up MIDI ports.


MidiManager.deinit()

Deinitialize all processes and tasks related to set up MIDI ports (including the USB Midi singleton if applicable).


midi_manager.py Classes InPortUART, InPortPIO and InPortUSB

Single port handlers for hardware UART, PIO UART or USB MIDI virtual cable based MIDI IN port.

Use MidiPort.add_*_in() to set up (do not instance directly).


Class Methods

InPort*.set_type_filter()

Set whether to block a MIDI message type.

Method Arguments

msg_typeintMIDI message type (or status byte) to be (un)blocked.
For Channel Voice or Channel Common type message the channel part of a status byte is ignored.
blockboolTrue to block, False to unblock.

InPort*.set_all_type_filters()

Set for all MIDI message types whether to be blocked.

Method Arguments

blockboolTrue to block, False to unblock.

InPort*.set_channel_filter()

Set whether to block a MIDI channel.

Method Arguments

channelintMIDI channel (status byte) to be (un)blocked
If a status byte is provided, only its least significant nibble is taken.
blockboolTrue to block, False to unblock.

InPort*.set_all_channel_filters()

Set for all MIDI channels whether to be blocked.

Method Arguments

blockboolTrue to block, False to unblock.

InPort*.set_cc_filter()

Set whether to block a MIDI Control Change (CC) number.

Method Arguments

cc_numberintMIDI Control Change number to be (un)blocked.
blockboolTrue to block, False to unblock.

InPort*.set_all_cc_filters()

Set for all MIDI Control Change (CC) numbers whether to be blocked.

Method Arguments

blockboolTrue to block, False to unblock.

midi_manager.py Classes OutPortUART, OutPortPIO and OutPortUSB

Single port handler for hardware UART, PIO UART or USB MIDI virtual cable based MIDI OUT port

Use MidiPort.add_*_out() to set up (do not instance directly)


Class Methods

OutPort*.write_real_time()

Queue a MIDI Real-Time message to be sent to MIDI OUT port, which will be sent as quick as possible.

Method Arguments

byteintSingle-byte MIDI Real-Time message to be sent.

OutPort*.write_sysex()

Queue a block of MIDI SysEx data to be sent to MIDI OUT port.

Method Arguments

sysex_bytesbytearray|bytes|memoryviewSysEx data buffer from which to be sent.
num_bytesintNumber of bytes to be sent.

OutPort*.write_data()

Queue a MIDI message to be sent to MIDI OUT port.

Do not use for System Real-Time and SysEx messages.

Method Arguments

byte_0intFirst byte of the MIDI message to be sent.
byte_1int, optionalSecond byte of the MIDI message to be sent.
Defaults to 0
byte_2int, optionalThird byte of the MIDI message to be sent.
Defaults to 0

midi_manager.py: Class MidiFilter

MIDI filter supporting message type, channel and CC number filtering.

MidiFilter instances could be used to filter outgoing MIDI messages before sending them to a MIDI OUT port. The classes InPortUART, InPortPIO and InPortUSB inherit this class to offer the option to filter incoming MIDI messages.


Class Methods

MidiFilter.set_type_filter()

Set whether to block a MIDI message type.

Method Arguments

msg_typeintMIDI message type (or status byte) to be (un)blocked.
For Channel Voice or Channel Common type message the channel part of a status byte is ignored.
blockboolTrue to block, False to unblock.

MidiFilter.set_all_type_filters()

Set for all MIDI message types whether to be blocked.

Method Arguments

blockboolTrue to block, False to unblock.

MidiFilter.set_channel_filter()

Set whether to block a MIDI channel.

Method Arguments

channelintMIDI channel (status byte) to be (un)blocked
If a status byte is provided, only its least significant nibble is taken.
blockboolTrue to block, False to unblock.

MidiFilter.set_all_channel_filters()

Set for all MIDI channels whether to be blocked.

Method Arguments

blockboolTrue to block, False to unblock<./td>

MidiFilter.set_cc_filter()

Set whether to block a MIDI Control Change (CC) number.

Method Arguments

cc_numberintMIDI Control Change number to be (un)blocked.
blockboolTrue to block, False to unblock.

MidiFilter.set_all_cc_filters()

Set for all MIDI Control Change (CC) numbers whether to be blocked.

Method Arguments

blockboolTrue to block, False to unblock.

MidiFilter.filter()

Determine whether to block a MIDI message.

Method Arguments

msgbytearray|bytes|memoryviewAt least the first 2-bytes of the MIDI message to be assessed.

Method Returns

boolTrue if the MIDI message is to be blocked, otherwise False.

Singleton Decorator

singleton.py: Decorator @singleton

Decorator function which restricts a class to a single instance: it creates the instance only on first-time instantiation and returns that instance on each consecutive instantiation.


Helper Classes and Utility Functions

log.py: Class Log

asyncio compatible logger which prints the message and can be configured to write to file and/or monitor.

This class has been defined with the @singleton, so only a single Log class can exist and initiating it again return that first instance.

The dependency on the display module is optional: the Log class also works if display.py is missing, but only for logging to file. For logging to display this module is set up to work with the Cybo-Drummer hardware (https://github.com/HLammers/cybo-drummer), which uses an ILI9225 display, connected as defined in the constant definitions below. To make this work with another screen the _DisplayMonitor class needs to be rewritten to use different display drivers.


Class Arguments

to_filebool, optionalWrite logs to log file if True.
Defaults to False
to_monitorbool, optionalWrite logs to display if True.
Defaults to True (which is overruled if the display import failed)
file_namestr, optionalFile name for log file.
Defaults to /log.txt

Class Methods

Log.write()

Write message to logger buffer to be logged synchronously.

Method Arguments

msgstr|bytes|bytearrayMessage to write to logger.

Log.sync_write()

Write message to logger synchronously (flushes to screen immediately and blocks until completed).

Method Arguments

msgstr|bytes|bytearrayMessage to write to logger.

Log.run()

asyncio task to read from log buffer and print, write to file and/or show on display whenever data is available.


Log.deinit()

Stop second core context, switch off display and cancel display monitor task.


midi_test_data.py: Class DummyStreamReaderUART

Dummy stream reader for testing purposes, simulating data coming from a hardware MIDI IN port.

Designed to work as replacement for a micropython.RingIO, asyncio.StreamReader or other stream protocol compatible object from which data is read using a readinto() method.


Class Methods

DummyStreamReaderUART.readinto()

Stream protocol compatible method to read test data into buffer.

Method Arguments

bufbytearrayBuffer to read into.

Method Returns

intnumber of bytes read.

midi_test_data.py: Class DummyReaderUSB

Dummy data generator simulating data coming from a USB MIDI IN port.


Class Methods

DummyReaderUSB.next_packet()

Returns next 4-byte packet of test data.

Method Returns

bytearray(4)USB MIDI packet.

midi_test_data.py: Class DummyBufferStream

Dummy data generator supplying 3-byte MIDI messages with an option to also generate SysEx data blocks.


Class Arguments

include_sysexbool, optionalGenerates also SysEx blocks if set to True.
Defaults to False

Class Methods

DummyBufferStream.next_message()

Returns next 3-byte MIDI message (1-byte and 2-byte messages are appended with zero value bytes) or SysEx block (if enabled).

Method Returns

bytearray3-byte USB MIDI message or variable-length SysEx block (if enabled).

midi_monitor.py: Class MidiMonitor

MIDI monitor which uses the Log singleton class to log MIDI messages.

This class has been defined with the @singleton, so only a single MidiMonitor class can exist and initiating it again return that first instance.


Class Arguments

longboolIf True include HEX representation of the message.
Defaults to True

Class Methods

MidiMonitor.write()

Generate MIDI monitor string and send to logger.

Do not use for SysEx data bytes.

Method Arguments

port_idintPort number.
byte_0intFirst byte of the MIDI message.
byte_1intSecond byte of the MIDI message.
byte_2intThird byte of the MIDI message.

context.py: Class Context

Context manager to asynchronously run functions or methods on the second core (using _threads).

This class has been defined with the @singleton, so only a single Context class can exist and initiating it again return that first instance.


Class Arguments

queue_sizeint, optionalSize of the deque buffer to store tasks to be queued to be run on the second core.
Defaults to 10

Class Methods

Context.assign()

asyncio awaitable to queue function to be run on the second core.

Releases once the function completed running.

Method Arguments

funcCallableFunction to be queued.
*argsPositional arguments to pass to func.
**kwargsKeyword arguments to pass to func.

Method Returns

AnyReturn value(s) from func.

Context.deinit()

Stop second core context.


timed_function.py: Decorator @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.


display.py: Class Display

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

This display driver doesn’t include an option to change the screen orientation, because it is optimized to be used by Cybo-Drummer (https://github.com/HLammers/cybo-drummer), which uses the display in a landscape orientation.

This class inherits all framebuf.FrameBuffer methods.


Class Arguments

spi_idintID of the SPI controller to be used (0: SPI0, 1: SPI1).
baudrateintSCK clock rage (Hz).
rs_pinmachine.PinPin object for the RS (register selection) pin.
rst_pinmachine.PinPin object for the RST (reset) pin.
led_pinmachine.PinPin object for the LED (backlight) pin.
sckmachine.Pin|None, optionalPin object for the CLK (serial clock) pin.
Defaults to None (18 for SPI0, 10 for SPI1)
mosimachine.Pin|None, optionalPin object for the MOSI (master output slave input) / SDI pin.
Defaults to None (19 for SPI0, 11 for SPI1)
misomachine.Pin|None, optionalPin object for the MISO (master input slave output) pin.
Defaults to None (16 for SPI0, 8 for SPI1)
rp2350boolTrue to set up for RP2350 based board, False for RP2040 based board.
Defaults to True (RP2350)

Note

  • This display driver assumes that the CS (chip select) pin is hard-wired to +3.3V.
  • SPI0 can only be mapped to GPIO 2/3/0, 6/7/4 or 18/19/16; SPI1 can only be mapped to GPIO 10/11/8 or 14/15/12.

Class Methods

Display.on()

Turn display on (including backlight).


Display.off()

Turn display off (including backlight).


Display.async_show()

asyncio awaitable to flush display buffer to screen asynchronously.


Display.show()

Flush display buffer to screen (blocks until completed).


Display.deinit()

Turn screen off, close DMA and deinitialize SPI controller.