import json import mido import subprocess nanoKontrol2_mapping: dict[int, str] = { 0: "Slider_1", 1: "Slider_2", 2: "Slider_3", 3: "Slider_4", 4: "Slider_5", 5: "Slider_6", 6: "Slider_7", 7: "Slider_8", 16: "Knob_1", 17: "Knob_2", 18: "Knob_3", 19: "Knob_4", 20: "Knob_5", 21: "Knob_6", 22: "Knob_7", 23: "Knob_8", 32: "Solo_1", 33: "Solo_2", 34: "Solo_3", 35: "Solo_4", 36: "Solo_5", 37: "Solo_6", 38: "Solo_7", 39: "Solo_8", 41: "Play", 42: "Stop", 43: "Prev", 44: "Next", 45: "Record", 46: "Cycle", 48: "Mute_1", 49: "Mute_2", 50: "Mute_3", 51: "Mute_4", 52: "Mute_5", 53: "Mute_6", 54: "Mute_7", 55: "Mute_8", 58: "Track_prev", 59: "Track_next", 60: "Marker_set", 61: "Marker_prev", 62: "Marker_next", 64: "Record_1", 65: "Record_2", 66: "Record_3", 67: "Record_4", 68: "Record_5", 69: "Record_6", 70: "Record_7", 71: "Record_8", } audiomixer_mapping = { "Slider_1": "mic1.output", "Solo_1": "mic1.output", "Mute_1": "mic1.output", "Record_1": "mic1.output", "Slider_2": "mic2.output", "Solo_2": "mic2.output", "Mute_2": "mic2.output", "Record_2": "mic2.output", "Slider_3": "system.output", "Solo_3": "system.output", "Mute_3": "system.output", "Record_3": "system.output", "Slider_4": "discord.output", "Solo_4": "discord.output", "Mute_4": "discord.output", "Record_4": "discord.output", "Slider_5": "music.output", "Solo_5": "music.output", "Mute_5": "music.output", "Record_5": "music.output", "Slider_6": "music.output", "Solo_6": "music.output", "Mute_6": "music.output", "Record_6": "music.output", "Slider_7": "music.output", "Solo_7": "music.output", "Mute_7": "music.output", "Record_7": "music.output", "Slider_8": "browser.output", "Solo_8": "browser.output", "Mute_8": "browser.output", "Record_8": "browser.output", "Knob_1": "alsa_output.usb-Focusrite_Scarlett_2i2_USB_Y8T4J6E095585D-00.Direct__Direct__sink", "Knob_2": "alsa_output.pci-0000_10_00.4.analog-stereo", "Knob_3": "obs_system.output", "Knob_4": "obs_discord.output", "Knob_5": "obs_music.output", "Knob_8": "obs_browser.output", "Prev": "XF86Previous", "Next": "XF86Next", } sliders = [ "Slider_1", "Slider_2", "Slider_3", "Slider_4", "Slider_5", "Slider_6", "Slider_7", "Slider_8", "Knob_1", "Knob_2", "Knob_3", "Knob_4", "Knob_5", "Knob_6", "Knob_7", "Knob_8", ] buttons = [ "Solo_1", "Mute_1", "Record_1", "Solo_2", "Mute_2", "Record_2", "Solo_3", "Mute_3", "Record_3", "Solo_4", "Mute_4", "Record_4", "Solo_5", "Mute_5", "Record_5", "Solo_6", "Mute_6", "Record_6", "Solo_7", "Mute_7", "Record_7", "Solo_8", "Mute_8", "Record_8", ] pw_nodes = [] pw_json = subprocess.run("pw-dump", capture_output=True, text=True) pw_data = json.loads(pw_json.stdout) for node in pw_data: if node["type"] == "PipeWire:Interface:Node": pw_nodes.append(node) def find_id(device: str) -> str: device_id: str = "" for pw_node in pw_nodes: if pw_node["info"]["props"]["node.name"] == device: device_id = str(pw_node["id"]) # else: # raise NotImplementedError return device_id def set_mute(toggle: bool, device: str) -> None: device_id = find_id(device) if toggle: subprocess.Popen(["/usr/bin/wpctl", "set-mute", device_id, "1"]) else: subprocess.Popen(["/usr/bin/wpctl", "set-mute", device_id, "0"]) def set_volume(volume: int, device: str) -> None: device_id = find_id(device) if volume > 0: volume = round((volume / 127), 2) subprocess.Popen(["/usr/bin/wpctl", "set-volume", device_id, str(volume)]) def main(): # TODO threading for MIDI values; with mido.open_input("nanoKONTROL2:nanoKONTROL2 _ CTRL 28:0") as mixer: for update in mixer: midi_input = nanoKontrol2_mapping[update.control] if midi_input == "Record": if update.value == 127: set_mute(toggle=True, device=audiomixer_mapping["Knob_2"]) else: set_mute(toggle=False, device=audiomixer_mapping["Knob_2"]) if midi_input in sliders: set_volume( volume=update.value, device=audiomixer_mapping[nanoKontrol2_mapping[update.control]], ) if midi_input in buttons: if midi_input.startswith("Mute") and update.value == 127: set_mute(toggle=True, device=audiomixer_mapping[midi_input]) elif midi_input.startswith("Mute") and update.value == 0: set_mute(toggle=False, device=audiomixer_mapping[midi_input]) # TODO parsing return to dict to split inputs # TODO define mixers in config files # TODO pipewire integration # TODO user config for audio streams # TODO running as a daemon # TODO systemctl unit file # TODO evaluating update time of script. 1ms? 0.1ms? if __name__ == "__main__": main()