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: - https://www.songstuff.com/recording/article/midi_message_format/ - https://github.com/pyqt/examples/ """ 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): super().__init__() 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) @staticmethod 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): super().__init__() self.setWindowTitle('Midi control test') self.midi_control = MidiReceiver('Midilink 0') self.midi_control.process_message.connect(self.process_message) control_ids = [74, 71, 81, 91, 16, 80, 19, 2] self.dial_map = {} layout = QHBoxLayout() for control_id in control_ids: dial = QDial() dial.setMaximum(127) layout.addWidget(dial) self.dial_map[control_id] = dial self.setLayout(layout) def process_message(self, status, data1, data2, _time_ms): if is_control_change(status): if data1 in self.dial_map: self.dial_map[data1].setValue(data2) if __name__ == "__main__": app = QApplication(sys.argv) main_window = MainWindow() main_window.show() sys.exit(app.exec_())