diff --git a/mixer.py b/mixer.py index 981edf9..e8c36d5 100755 --- a/mixer.py +++ b/mixer.py @@ -5,16 +5,46 @@ import socket import mido from enum import Enum +from dataclasses import dataclass # For Allen & Heath Qu MIDI protocol documentation see: # https://www.allen-heath.com/content/uploads/2023/06/Qu_MIDI_Protocol_V1.9.pdf class Mixer: + @dataclass + class SystemState: + class QuModel(Enum): + QU16 = 1 + QU24 = 2 + QU32 = 3 + QUPAC = 4 + QUSB = 5 + + def __str__(self): + labels = { + 1: "Qu-16", + 2: "Qu-24", + 3: "Qu-32", + 4: "Qu-Pac", + 5: "Qu-SB", + } + + return labels[self.value] + + sys_ex_header : list + midi_channel : int + id : int + model : QuModel + major_ver : int + minor_ver : int + def __init__(self, ip): self.MIXER_PORT = 51325 self.sock = socket.create_connection((ip, self.MIXER_PORT)) self.mido_parser = mido.Parser() + self.qu_midi_channel = self.get_system_state().midi_channel + self.daw_midi_channel = self.qu_midi_channel + 1 ALLEN_HEATH_ID = [0x00, 0x00, 0x1A] QU_MIXER = [0x50, 0x11] @@ -61,7 +91,7 @@ class Mixer: def get_system_state(self): msg_id = self.SysExMessageId.GET_SYSTEM_STATE_REQUEST.value - i_pad_flag = 0x01 + i_pad_flag = 0x00 data = self.SYSEX_ALL_CALL + [msg_id, i_pad_flag] @@ -73,37 +103,18 @@ class Mixer: response = self.recv_sys_ex(self.SysExMessageId.GET_SYSTEM_STATE_RESPONSE) - class QuModel(Enum): - QU16 = 1 - QU24 = 2 - QU32 = 3 - QUPAC = 4 - QUSB = 5 - - def __str__(self): - labels = { - 1: "Qu-16", - 2: "Qu-24", - 3: "Qu-32", - 4: "Qu-Pac", - 5: "Qu-SB", - } - - return labels[self.value] - - sysex_header = response.data[:8] - - midi_channel = int(response.data[7]) - id = int(response.data[8]) i_pad_flag = int(response.data[9]) - major_ver = int(response.data[10]) - minor_ver = int(response.data[11]) - print(f"sysex_header: {sysex_header}") - print(f"MIDI channel: {midi_channel}") - print(f"ID: 0x{id:02X}") - print(f"Model: {QuModel(i_pad_flag)}") - print(f"Firmware Version: {major_ver}.{minor_ver}") + state = self.SystemState( + sys_ex_header=response.data[:8], + midi_channel=int(response.data[7]), + id=int(response.data[8]), + model=self.SystemState.QuModel(i_pad_flag), + major_ver=int(response.data[10]), + minor_ver = int(response.data[11]) + ) + + return state def get_name_from_qu(self, channel_no, name_to_set): msg_id = self.SysExMessageId.GET_NAME_FROM_QU_REQUEST.value @@ -146,11 +157,17 @@ class Mixer: def shutdown(self): self.nrpn_parameter_control(midi_ch=0, mixer_ch=0, id=0x5F, va=0x00, vx=0x00) - def set_bank_1(self): - msb = mido.Message("control_change", channel=0, control=0x00, value=0x00) - lsb = mido.Message("control_change", channel=0, control=0x20, value=0x00) + def set_bank(self, bank_no): + if bank_no < 1 or bank_no > 16384: + raise ValueError(f"bank_no param must be between 0 and 16384, got {bank_no}") - msg_bytes = bytes(msb.bytes() + lsb.bytes()) + msb_val = ((bank_no - 1) >> 7) & 0xFF + lsb_val = (bank_no - 1) & 0x7F + + control_change_msb = mido.Message("control_change", channel=self.qu_midi_channel, control=0x00, value=msb_val) + control_change_lsb = mido.Message("control_change", channel=self.qu_midi_channel, control=0x20, value=lsb_val) + + msg_bytes = bytes(control_change_msb.bytes() + control_change_lsb.bytes()) print(" ".join(f"{b:02X}" for b in msg_bytes)) self.sock.sendall(msg_bytes) @@ -158,8 +175,8 @@ class Mixer: def scene_recall(self, scene_id): print(f"scene_recall: scene_id={scene_id}") - self.set_bank_1() - msb = mido.Message("program_change", channel=0, program=scene_id) + self.set_bank(1) + msb = mido.Message("program_change", channel=self.qu_midi_channel, program=scene_id) msg_bytes = bytes(msb.bytes()) @@ -206,7 +223,15 @@ def main(): match args.command: case "get_name_from_qu": mixer.get_name_from_qu(args.channel_id, args.name) - case "get_system_state": mixer.get_system_state() + case "get_system_state": + state = mixer.get_system_state() + + print(f"sysex_header: {state.sys_ex_header}") + print(f"MIDI channel: {state.midi_channel}") + print(f"ID: 0x{state.id:02X}") + print(f"Model: {state.model}") + print(f"Firmware Version: {state.major_ver}.{state.minor_ver}") + case "shutdown": mixer.shutdown() case "scene_recall": mixer.scene_recall(args.scene_number) case "scene_recall_default": mixer.scene_recall(0)