Blog Music

MIDI Responsive Qt

Challenge: get MIDI keyboard to update Qt controls for some live jam experimentation. Task list:

  • Use a particular channel for a controller. In future I could make a hardware device that changes channel on a button press, say.
  • Respond to dials changing to update Qt controls. Should be as simple as binding MIDI signal to Qt slot.

I went around the houses a little on this, so rather than adding to my current Midi looper project I’ve made a stand-alone Qt app that contains a number of dials that reflect the potentiometer values on my mini controller keyboard:

The code uses rtmidi and I shepherd the events through a Qt.QueuedConnection. This was precautionary as I thought rtmidi’s set_callback would be on a different thread. After some experimentation it appears to be the same as the Qt GUI thread, but I’m suspicious of that so I’ve left the code in.

Midi surface control test. Show dials for control keyboard and update Qt UI.
Made with reference to:
import rtmidi
import sys
from PyQt5.QtWidgets import (QApplication, QDial, QHBoxLayout, QWidget)
from PyQt5.QtCore import (pyqtSignal, QObject, Qt)

def get_port_by_name(port_name, client):
    for i in range(client.get_port_count()):
        if port_name == client.get_port_name(i):
            return client.open_port(i)

    # Report available ports if not found
    msg = f'Unknown port {port_name}'
    for i in range (client.get_port_count()):
        msg += f'\n - {client.get_port_name(i)}'
    raise Exception(msg)

def status_channel(status):
    return status & 0xf

def is_control_change(status):
    return (status & 0xb0) == 0xb0

class MidiReceiver(QObject):
    # Internal signal for handing over MIDI events in thread-safe manner
    _queue_message = pyqtSignal(int, int, int, int)

    Qt signal for a received MIDI message. Signature is:
        int: status
        int: data1
        int: data2
        int: event time in ms
    process_message = pyqtSignal(int, int, int, int)

    def __init__(self, midi_in_port):
        self._midi_in = get_port_by_name(midi_in_port, rtmidi.MidiIn())
        self._midi_in.set_callback(self._midi_in_callback, self)
        self._time_ms = 0
        self._queue_message.connect(self._thread_safe_queue_message, Qt.QueuedConnection)

    def _thread_safe_queue_message(self, status, data1, data2, time):
        self.process_message.emit(status, data1, data2, time)

    def _midi_in_callback(msg_time_ms_tup, self):
        self._time_ms += int(msg_time_ms_tup[1] * 1000)
        msg = msg_time_ms_tup[0]
        status = msg[0]
        data1 = msg[1]
        data2 = msg[2]
        self._queue_message.emit(status, data1, data2, self._time_ms)

class MainWindow(QWidget):
    def __init__(self):
        self.setWindowTitle('Midi control test')
        self.midi_control = MidiReceiver('Midilink 0')

        control_ids = [74, 71, 81, 91, 16, 80, 19, 2]
        self.dial_map = {}

        layout = QHBoxLayout()
        for control_id in control_ids:
            dial = QDial()
            self.dial_map[control_id] = dial

    def process_message(self, status, data1, data2, _time_ms):
        if is_control_change(status):
            if data1 in self.dial_map:

if __name__ == "__main__":
    app = QApplication(sys.argv)
    main_window = MainWindow()

Leave a Reply

Your email address will not be published. Required fields are marked *