Approximately real-time MIDI manipulation for Python.
A light-weight modular framework for prototyping real-time MIDI effects in Python, based on asyncio
and python-rtmidi
.
Note that pure python (and therefore this codebase) is probably not the best choice for time-sensitive operations like MIDI I/O: your mileage may vary.
This package requires Python >= 3.10. Clone the repository and install using pip install .
in the cloned directory.
To instantiate a chain of MIDI effects, including optional MIDI input and output, create a Chain
and add modules to it. E.g.:
from midifx.io import Chain, ReceiveMIDI, SendMIDI
from midifx.effects import Delay, PitchShift
chain = Chain(
ReceiveMIDI(name="MidiFX in"),
Delay(delay=2.0),
PitchShift(amount=12),
SendMIDI(name="MidiFX out"),
)
chain.run()
For a more elaborate example, see demo.py
.
Warning: this package currently combines 'note on' and 'note off' events into 'note' objects before processing, after the 'note off' event is registered. In some cases, this adds an intrinsic latency stemming from the duration of a note.
Create a real-time MIDI effect chain by chaining together I/O modules and effects.
- I/O modules include reading and writing MIDI from a file, and sending and receiving MIDI over a MIDI port.
- A few simple MIDI effects, including delay, pitch shift and velocity changes, are also included.
- New effects should be relatively easy to implement by subclassing
Module
and implementingprocess
.
Modules can be controlled in real-time using Parameter
objects that listen to control change messages and update their current value
within a given range. Similarly, Switch
objects allow for turning effects on and off. See the built-in effects for examples.
More experimentally, modules can also be equipped with randomly evolving parameters and switches. These are randomly updated every time a particular control message is encountered. See again the demo.py
script for how this can work.
from typing import Iterable
from midifx.core import as_parameter, Module, Parameter, Switch
from midifx.note import Message, Note
class MyEffect(Module):
"""Custom MIDI effect. Has one parameter and an on/off attribute, which can be
MIDI-controlled by passing a Parameter and Switch object to the constructor.
These will automatically listen to the relevant control messages.
"""
def __init__(self, parameter: float | Parameter = 2.0, on: bool | Switch = True):
super().__init__("My Effect", on=on)
self.parameter = as_parameter(parameter)
def process(self, messages: Iterable[Message]) -> Iterable[Message]:
"""Receive 0 or 1 messages (e.g. notes or control messages), and return 0 or 1
messages. For most use cases, only notes need actual processing; control message
need not be handled (but you will very probably want to pass them on).
"""
for message in messages:
if isinstance(message, Note):
pass # add your logic here, modifying `message` based on `self.parameter.value`
yield message