Source code for pico_synth_sandbox.midi

# pico_synth_sandbox/midi.py
# 2023 Cooper Dalrymple - me@dcdalrymple.com
# GPL v3 License

from pico_synth_sandbox.tasks import Task
from pico_synth_sandbox import clamp
import os, time
import usb_midi
import adafruit_midi
from adafruit_midi.note_on import NoteOn
from adafruit_midi.note_off import NoteOff
from adafruit_midi.control_change import ControlChange
from adafruit_midi.pitch_bend import PitchBend
from adafruit_midi.program_change import ProgramChange
from adafruit_midi.midi_message import MIDIUnknownEvent

[docs]class Midi(Task): """Send and receive both hardware UART and USB MIDI messages using :class:`adafruit_midi.MIDI`. UART can be enabled with the `MIDI_UART` variable and USB can be enabled with the `MIDI_USB` variable in `settings.toml`. The midi channel is limited to a single value for both input and output and is determined by the `MIDI_CHANNEL` variable in `settings.toml` with a range of 0-15. However, the channel can be changed once a :class:`pico_synth_sandbox.midi.Midi` object is created by calling the `set_channel` function. By default, the onboard led will be used to indicate incoming midi messages. At the moment, this feature cannot be disabled. """ def __init__(self, board): self._channel = None self._thru = False self._note_on = None self._note_off = None self._control_change = None self._pitch_bend = None self._program_change = None if os.getenv("MIDI_UART", 0) > 0: self._uart = board.get_uart() self._uart_midi = adafruit_midi.MIDI( midi_in=self._uart, midi_out=self._uart, debug=False ) else: self._uart_midi = None if os.getenv("MIDI_USB", 0) > 0: self._usb_midi = adafruit_midi.MIDI( midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], debug=False ) else: self._usb_midi = None self._led = board.get_led() self._led.value = False self._led_duration = 0.01 self._led_last = time.monotonic() Task.__init__(self, update_frequency=100)
[docs] def set_note_on(self, callback): """Set the callback method you would like to be called when a `adafruit_midi.note_on.NoteOn` message is received. :param callback: The callback method. Must have 2 parameters for note value and velocity (0.0-1.0). Ie: `def note_on(notenum, velocity):`. :type callback: function """ self._note_on = callback
[docs] def set_note_off(self, callback): """Set the callback method you would like to be called when a `adafruit_midi.note_off.NoteOff` message is received. :param callback: The callback method. Must have 1 parameter for the note value. Ie: `def note_off(notenum):`. :type callback: function """ self._note_off = callback
[docs] def set_control_change(self, callback): """Set the callback method you would like to be called when a `adafruit_midi.control_change.ControlChange` message is received. :param callback: The callback method. Must have 2 parameters for control number and control value (0.0-1.0). Ie: `def control_change(control, value):`. :type callback: function """ self._control_change = callback
[docs] def set_pitch_bend(self, callback): """Set the callback method you would like to be called when a `adafruit_midi.pitch_bend.PitchBend` message is received. :param callback: The callback method. Must have 1 parameter for the pitch bend value (-1.0-1.0). Ie: `def pitch_bend(value):`. :type callback: function """ self._pitch_bend = callback
[docs] def set_program_change(self, callback): """Set the callback method you would like to be called when a `adafruit_midi.program_change.ProgramChange` message is received. :param callback: The callback method. Must have 1 parameter for the patch number requested. Ie: `def program_change(patch):`. :type callback: function """ self._program_change = callback
[docs] def set_channel(self, value): """Set the midi channel for messages to be received and sent from. :param value: The desired channel from 0 to 16. 0 will accept all midi messages. :type value: int """ if value == 0 or value is None: value = None else: value = clamp(value - 1, 0, 15) self._channel = value
def get_channel(self): return 0 if self._channel is None else self._channel + 1
[docs] def set_thru(self, value): """Set whether you would like to forward incoming midi messages through the enabled outputs automatically. :param value: Whether or not you would like to enable midi thru. :type value: bool """ self._thru = value
def get_thru(self): return self._thru def _process_message(self, msg): if not msg: return if self._channel is None or msg.channel == self._channel: if isinstance(msg, NoteOn): if msg.velocity > 0.0: if self._note_on: self._note_on(msg.note, msg.velocity / 127.0) elif self._note_off: self._note_off(msg.note) elif isinstance(msg, NoteOff): if self._note_off: self._note_off(msg.note) elif isinstance(msg, ControlChange): if self._control_change: self._control_change(msg.control, msg.value / 127.0) elif isinstance(msg, PitchBend): if self._pitch_bend: self._pitch_bend((msg.pitch_bend - 8192) / 8192) elif isinstance(msg, ProgramChange): if self._program_change: self._program_change(msg.patch) if self._thru and not isinstance(msg, MIDIUnknownEvent): if self._uart_midi: self._uart_midi.send(msg) if self._usb_midi: self._usb_midi.send(msg) self._trigger_led() def _process_messages(self, midi, limit=32): while limit>0: msg = midi.receive() if not msg: break self._process_message(msg) limit = limit - 1
[docs] async def update(self): """Process any incoming midi messages from the enabled midi devices. Will trigger any pre-defined callbacks if the appropriate messages are received. """ if self._uart_midi: self._process_messages(self._uart_midi) if self._usb_midi: self._process_messages(self._usb_midi) if self._led.value and time.monotonic() - self._led_last > self._led_duration: self._led.value = False
def _trigger_led(self): self._led.value = True self._led_last = time.monotonic()
[docs] def send_message(self, msg): """Send an :class:`adafruit_midi.midi_message.MIDIMessage` message through the enabled midi outputs. :param msg: The message you would like to trasmit :type msg: adafruit_midi.midi_message.MIDIMessage """ if self._uart_midi: self._uart_midi.send(msg) if self._usb_midi: self._usb_midi.send(msg) self._trigger_led()
[docs] def send_note_on(self, notenum, velocity=1.0, channel=None): """Send an :class:`adafruit_midi.note_on.NoteOn` message through the enabled midi outputs. :param notenum: The value of the midi note to send. :type notenum: int :param velocity: The velocity of the note from 0.0 through 1.0. :type velocity: float :param channel: The midi channel to transmit the message on. :type channel: int """ self.send_message(NoteOn( notenum, int(clamp(velocity) * 127.0) if type(velocity) is float else velocity, channel=channel if not channel is None else self._channel ))
[docs] def send_note_off(self, notenum, channel=None): """Send an :class:`adafruit_midi.note_off.NoteOff` message through the enabled midi outputs. :param notenum: The value of the midi note to send. :type notenum: int :param channel: The midi channel to transmit the message on. :type channel: int """ self.send_message(NoteOff( notenum, channel=channel if not channel is None else self._channel ))
[docs] def send_control_change(self, control, value, channel=None): """Send an :class:`adafruit_midi.control_change.ControlChange` message through the enabled midi outputs. :param control: The number of the midi control to send. :type control: int :param value: The value to set of the desired control from 0.0 through 1.0. :type value: float :param channel: The midi channel to transmit the message on. :type channel: int """ self.send_message(ControlChange( control, int(clamp(value) * 127.0) if type(value) is float else value, channel=channel if not channel is None else self._channel ))
[docs] def send_program_change(self, patch, channel=None): """Send an :class:`adafruit_midi.program_change.ProgramChange` message through the enabled midi outputs. :param patch: The program/patch you would like to change to from 0 through 127. :type patch: int :param channel: The midi channel to transmit the message on. :type channel: int """ self.send_message(ProgramChange( clamp(patch, 0, 127), channel=channel if not channel is None else self._channel ))