diff --git a/UI/digit_core.py b/UI/digit_core.py
index 45ee460..d02ca01 100644
--- a/UI/digit_core.py
+++ b/UI/digit_core.py
@@ -1,35 +1,9 @@
 ##################### DIGIT #######
-##################### DIGIT #######
-##################### DIGIT #######
-# 26 total effect slots? 
-# delay 1-4
-# reverb
-# cab
-# effects 1-4 per delay & reverb
-
-#    
-
-
-# from pluginsmanager.jack.jack_client import JackClient
-# client = JackClient()
-# sys_effect = SystemEffect('system', ['capture_1', 'capture_2', "capture_3, capture_4"],
-#         ['playback_1', 'playback_2', 'playback_3', 'playback_4'])
-import sys, time, json, os.path, os, subprocess, queue
-from collections import defaultdict
+import sys, time, json, os.path, os, subprocess, queue, threading
+from multiprocessing import Process, Queue
 ##### Hardware backend
 # --------------------------------------------------------------------------------------------------------
-import pedal_hardware, start_jconvolver
-os.environ["QT_IM_MODULE"] = "qtvirtualkeyboard"
-###### UI
-# --------------------------------------------------------------------------------------------------------
-from PySide2.QtGui import QGuiApplication
-from PySide2.QtCore import QObject, QUrl, Slot, QStringListModel, Property, Signal
-from PySide2.QtQml import QQmlApplicationEngine
-from PySide2.QtGui import QIcon
-# compiled QML files, compile with pyside2-rcc
-import qml.qml
-import icons.icons#, imagine_assets
-import resource_rc
+import pedal_hardware, digit_frontend
 ######## Carla
 # --------------------------------------------------------------------------------------------------------
 sys.path.append('/git_repos/Carla/source/frontend')
@@ -51,6 +25,8 @@ def signalHandler(sig, frame):
     if sig in (SIGINT, SIGTERM):
         gCarla.term = True
 
+os.sched_setaffinity(0, (0, 1, 3))
+
 pedal_hardware.add_hardware_listeners()
 # --------------------------------------------------------------------------------------------------------
 # Poly Globals
@@ -84,398 +60,161 @@ pluginMap = {}
 #
 parameterMap = {}
 current_connections = {} # these are group port group port to connection id pairs #TODO remove stale
-current_connection_pairs_poly = set()
-current_midi_connection_pairs_poly = set()
-current_ir_file = None
 pending_connect = queue.Queue()
+ui_messages = Queue()
+core_messages = Queue()
 
-# --------------------------------------------------------------------------------------------------------
-source_ports = ["sigmoid1:Output", "delay2:out0","delay3:out0", "delay4:out0",
-    "postreverb:Out Left", "postreverb:Out Right", "system:capture_2", "system:capture_4",
-    "system:capture_3", "system:capture_5", "postcab:Out"]
-available_port_models = dict({(k, QStringListModel()) for k in source_ports})
-used_port_models = dict({(k, QStringListModel()) for k in available_port_models.keys()})
-# XXX temp, until I fix bypassing
-
+def send_ui_message(command, args):
+    ui_messages.put((command, args))
 
-def insert_row(model, row):
-    j = len(model.stringList())
-    model.insertRows(j, 1)
-    model.setData(model.index(j), row)
 
-def remove_row(model, row):
-    i = model.stringList().index(row)
-    model.removeRows(i, 1)
+# --------------------------------------------------------------------------------------------------------
 
 def set_active(effect, is_active):
     print(effect, " active state is ", bool(is_active))
     host.set_drywet(pluginMap[effect], is_active) # full wet if active else full dry
-    plugin_state[effect].value = is_active
-
-def clamp(v, min_value, max_value):
-    return max(min(v, max_value), min_value)
-
-class PolyEncoder(QObject):
-    # name, min, max, value
-    def __init__(self, starteffect="", startparameter=""):
-        QObject.__init__(self)
-        self.effectval = starteffect
-        self.parameterval = startparameter
-        self.speed = 1
-        self.value = 1
-
-    def readEffect(self):
-        return self.effectval
-
-    def setEffect(self,val):
-        self.effectval = val
-        self.effect_changed.emit()
-
-    @Signal
-    def effect_changed(self):
-        pass
-
-    effect = Property(str, readEffect, setEffect, notify=effect_changed)
-
-    def readParameter(self):
-        return self.parameterval
-
-    def setParameter(self,val):
-        self.parameterval = val
-        self.parameter_changed.emit()
-
-    @Signal
-    def parameter_changed(self):
-        pass
-
-    parameter = Property(str, readParameter, setParameter, notify=parameter_changed)
-
-class PolyBool(QObject):
-    # name, min, max, value
-    def __init__(self, startval=False):
-        QObject.__init__(self)
-        self.valueval = startval
-
-    def readValue(self):
-        return self.valueval
-
-    def setValue(self,val):
-        self.valueval = val
-        self.value_changed.emit()
-
-    @Signal
-    def value_changed(self):
-        pass
-
-    value = Property(bool, readValue, setValue, notify=value_changed)
-
-class PolyValue(QObject):
-    # name, min, max, value
-    def __init__(self, startname="", startval=0, startmin=0, startmax=1, curve_type="lin"):
-        QObject.__init__(self)
-        self.nameval = startname
-        self.valueval = startval
-        self.rminval = startmin
-        self.rmax = startmax
-        self.assigned_cc = None
-
-    def readValue(self):
-        return self.valueval
-
-    def setValue(self,val):
-        # clamp values
-        self.valueval = clamp(val, self.rmin, self.rmax)
-        self.value_changed.emit()
-        print("setting value", val)
-
-    @Signal
-    def value_changed(self):
-        pass
-
-    value = Property(float, readValue, setValue, notify=value_changed)
-
-    def readName(self):
-        return self.nameval
-
-    def setName(self,val):
-        self.nameval = val
-        self.name_changed.emit()
-
-    @Signal
-    def name_changed(self):
-        pass
-
-    name = Property(str, readName, setName, notify=name_changed)
-
-    def readRMin(self):
-        return self.rminval
-
-    def setRMin(self,val):
-        self.rminval = val
-        self.rmin_changed.emit()
-
-    @Signal
-    def rmin_changed(self):
-        pass
-
-    rmin = Property(float, readRMin, setRMin, notify=rmin_changed)
-
-    def readRMax(self):
-        return self.rmaxval
-
-    def setRMax(self,val):
-        self.rmaxval = val
-        self.rmax_changed.emit()
-
-    @Signal
-    def rmax_changed(self):
-        pass
-
-    rmax = Property(float, readRMax, setRMax, notify=rmax_changed)
-
-def time_to_tempo_text(v):
-    if v >= 1:
-        return (1, "1")
-    elif v > 0.75:
-        return (0.75, "1/2.")
-    elif v > 0.5:
-        return (0.5, "1/2")
-    elif v > 0.33:
-        return (0.33, "1/2t")
-    elif v > 0.25:
-        return (0.25, "1/4")
-    elif v > 0.125:
-        return (0.125, "1/8")
-    elif v > 1/16.0:
-        return (1/16.0, "1/16")
-    elif v > 1/32.0:
-        return (1/32.0, "1/32")
-    else: #hif v > 1/64.0:
-        return (1/64.0, "1/64")
-    # lv2:scalePoint [rdfs:label "Dotted1/2note"; rdf:value 1];
-    # lv2:scalePoint [rdfs:label "1/2note"; rdf:value 2];
-    # lv2:scalePoint [rdfs:label "1/2notetriplets"; rdf:value 3];
-    # lv2:scalePoint [rdfs:label "Dotted1/4note"; rdf:value 4];
-    # lv2:scalePoint [rdfs:label "1/4note"; rdf:value 5];
-    # lv2:scalePoint [rdfs:label "1/4notetriplets"; rdf:value 6];
-    # lv2:scalePoint [rdfs:label "Dotted1/8note"; rdf:value 7];
-    # lv2:scalePoint [rdfs:label "1/8note"; rdf:value 8];
-    # lv2:scalePoint [rdfs:label "1/8notetriplets"; rdf:value 9];
-    # lv2:scalePoint [rdfs:label "Dotted1/16note"; rdf:value 10];
-    # lv2:scalePoint [rdfs:label "1/16note"; rdf:value 11];
-    # lv2:scalePoint [rdfs:label "1/16notetriplets"; rdf:value 12];
-    # lv2:scalePoint [rdfs:label "Dotted1/32note"; rdf:value 13];
-    # lv2:scalePoint [rdfs:label "1/32note"; rdf:value 14];
-    # lv2:scalePoint [rdfs:label "1/32notetriplets"; rdf:value 15];
-    # lv2:scalePoint [rdfs:label "Dotted1/64note"; rdf:value 16];
-    # lv2:scalePoint [rdfs:label "1/64note"; rdf:value 17];
-    # lv2:scalePoint [rdfs:label "1/64notetriplets"; rdf:value 18];
-
-class Knobs(QObject):
-    """Basically all functions for QML to call"""
-
-    def __init__(self):
-        QObject.__init__(self)
-        self.waitingval = ""
-
-    @Slot(str, str, 'double')
-    def ui_knob_change(self, effect_name, parameter, value):
-        # print(x, y, z)
-        if effect_name in pluginMap:
-            effect_parameter_data[effect_name][parameter].value = value
-            if parameter == "carla_level":
-                host.set_volume(pluginMap[effect_name], value)
-            else:
-                host.set_parameter_value(pluginMap[effect_name],
-                        parameterMap[effect_name][parameter], value)
-        else:
-            print("effect not found")
-
-    @Slot(str, str, str)
-    def ui_add_connection(self, effect, source_port, x, midi=False):
-        effect_source = effect + ":" + source_port
-        if not midi:
-            remove_row(available_port_models[effect_source], x)
-            insert_row(used_port_models[effect_source], x)
-        current_connection_pairs_poly.add((effect_source, x))
-        # print("portMap is", portMap)
-
-        host.patchbay_connect(patchbay_external, portMap[effect]["group"],
-                portMap[effect]["ports"][source_port],
-                portMap[output_port_names[x][0]]["group"],
-                portMap[output_port_names[x][0]]["ports"][output_port_names[x][1]])
-        print(x)
-
-    @Slot(str, str, str)
-    def ui_remove_connection(self, effect, source_port, x):
-        effect_source = effect + ":" + source_port
-        remove_row(used_port_models[effect_source], x)
-        insert_row(available_port_models[effect_source], x)
-        current_connection_pairs_poly.remove((effect_source, x))
-
-        host.patchbay_disconnect(patchbay_external, current_connections[(portMap[effect]["group"],
-                portMap[effect]["ports"][source_port],
-                portMap[output_port_names[x][0]]["group"],
-                portMap[output_port_names[x][0]]["ports"][output_port_names[x][1]])])
-        print(x)
-
-    @Slot(str)
-    def toggle_enabled(self, effect):
-        print("toggling", effect)
-        # host.set_active(pluginMap[effect], not bool(host.get_internal_parameter_value(pluginMap[effect], PARAMETER_ACTIVE)))
-        # active = host.get_internal_parameter_value(pluginMap[effect], PARAMETER_ACTIVE))
-        is_active = not plugin_state[effect].value
-        set_active(effect, is_active)
-
-    @Slot(bool, str)
-    def update_ir(self, is_reverb, ir_file):
-        print("updating ir", ir_file)
-        global current_ir_file
-        current_ir_file = ir_file[7:] # strip file:// prefix
-        # cause call file callback
-        # by calling show GUI
-        if is_reverb:
-            # kill existing jconvolver
-            # write jconvolver file
-            # start jconvolver
-            if is_loading["reverb"].value:
-                return
-            is_loading["reverb"].value = True
-            effect_parameter_data["reverb"]["ir"].name = ir_file
-            # host.show_custom_ui(pluginMap["reverb"], True)
-            start_jconvolver.generate_reverb_conf(current_ir_file)
-            # host.set_program(pluginMap["reverb"], 0)
-        else:
-            if is_loading["cab"].value:
-                return
-            is_loading["cab"].value = True
-            effect_parameter_data["cab"]["ir"].name = ir_file
-            start_jconvolver.generate_cab_conf(current_ir_file)
+    plugin_state[effect] = is_active
 
 
-
-    @Slot(str, str)
-    def map_parameter(self, effect_name, parameter):
-        if self.waiting == "left" or self.waiting == "right":
-            # mapping and encoder
-            set_knob_current_effect(self.waiting, effect_name, parameter)
+def knob_change(effect_name, parameter, value):
+    # print(x, y, z)
+    if effect_name in pluginMap:
+        if parameter == "carla_level":
+            host.set_volume(pluginMap[effect_name], value)
         else:
-            # we're mapping to LFO
-            host.set_parameter_midi_cc(pluginMap[effect_name], parameterMap[effect_name][parameter],
-                    effect_parameter_data[self.waiting]["cc_num"].value)
-            # connect ports
-            host.patchbay_connect(patchbay_external, portMap[self.waiting]["group"],
-                    portMap[self.waiting]["ports"]["events-out"],
-                    portMap[effect_name]["group"],
-                    portMap[effect_name]["ports"]["events-in"])
-            effect_parameter_data[effect_name][parameter].assigned_cc = effect_parameter_data[self.waiting]["cc_num"].value
-            current_midi_connection_pairs_poly.add(((self.waiting, "events-out"), (effect_name, "events-in")))
-        self.waiting = ""
-
-    @Slot(str, str)
-    def unmap_parameter(self, effect_name, parameter):
-            host.set_parameter_midi_cc(pluginMap[effect_name], parameterMap[effect_name][parameter],
-                    0)
-            effect_parameter_data[effect_name][parameter].assigned_cc = None
-
-    @Slot(str, str, int)
-    def map_parameter_cc(self, effect_name, parameter, cc):
+            host.set_parameter_value(pluginMap[effect_name],
+                    parameterMap[effect_name][parameter], value)
+    else:
+        print("effect not found")
+
+def add_connection(effect, source_port, x):
+    host.patchbay_connect(patchbay_external, portMap[effect]["group"],
+            portMap[effect]["ports"][source_port],
+            portMap[output_port_names[x][0]]["group"],
+            portMap[output_port_names[x][0]]["ports"][output_port_names[x][1]])
+
+def add_connection_pair(source_pair, target_pair):
+    host.patchbay_connect(patchbay_external, portMap[source_pair[0]]["group"],
+            portMap[source_pair[0]]["ports"][source_pair[1]],
+            portMap[target_pair[0]]["group"],
+            portMap[target_pair[0]]["ports"][target_pair[1]])
+
+def remove_connection(effect, source_port, x):
+    host.patchbay_disconnect(patchbay_external, current_connections[(portMap[effect]["group"],
+            portMap[effect]["ports"][source_port],
+            portMap[output_port_names[x][0]]["group"],
+            portMap[output_port_names[x][0]]["ports"][output_port_names[x][1]])])
+
+def toggle_enabled(effect):
+    is_active = not plugin_state[effect]
+    set_active(effect, is_active)
+
+# def update_ir(is_reverb, ir_file):
+#     print("updating ir", ir_file)
+#     current_ir_file = ir_file[7:] # strip file:// prefix
+#     # cause call file callback
+#     # by calling show GUI
+#     if is_reverb:
+#         # kill existing jconvolver
+#         # write jconvolver file
+#         # start jconvolver
+#         if is_loading["reverb"]
+#             return
+#         is_loading["reverb"] = True
+#         to_ui_update_value("reverb", "ir", ir_file)
+#         # host.show_custom_ui(pluginMap["reverb"], True)
+#         start_jconvolver.generate_reverb_conf(current_ir_file)
+#         # host.set_program(pluginMap["reverb"], 0)
+#     else:
+#         if is_loading["cab"]
+#             return
+#         is_loading["cab"] = True
+#         to_ui_update_value("cab", "ir", ir_file)
+#         start_jconvolver.generate_cab_conf(current_ir_file)
+
+
+
+def map_parameter(source, effect_name, parameter, cc_num=0):
+    if source == "left" or source == "right":
+        # mapping and encoder
+        set_knob_current_effect(source, effect_name, parameter)
+    else:
+        # we're mapping to LFO
         host.set_parameter_midi_cc(pluginMap[effect_name], parameterMap[effect_name][parameter],
-                cc)
-        host.set_parameter_midi_channel(pluginMap[effect_name], parameterMap[effect_name][parameter], midi_channel.value)
+                cc_num)
+        host.set_parameter_midi_channel(pluginMap[effect_name], parameterMap[effect_name][parameter], midi_channel)
         # connect ports
-        # ("ttymidi", "MIDI_in")
-        host.patchbay_connect(patchbay_external, portMap["ttymidi"]["group"],
-                portMap["ttymidi"]["ports"]["MIDI_in"],
+        host.patchbay_connect(patchbay_external, portMap[source]["group"],
+                portMap[source]["ports"]["events-out"],
                 portMap[effect_name]["group"],
                 portMap[effect_name]["ports"]["events-in"])
-        effect_parameter_data[effect_name][parameter].assigned_cc = cc
-        current_midi_connection_pairs_poly.add((("ttymidi", "MIDI_in"), (effect_name, "events-in")))
 
-    @Slot(str)
-    def set_waiting(self, knob):
-        print("waiting", knob)
-        self.waiting = knob
 
-    def readWaiting(self):
-        return self.waitingval
+def unmap_parameter(effect_name, parameter):
+    host.set_parameter_midi_cc(pluginMap[effect_name], parameterMap[effect_name][parameter],
+                0)
 
-    def setWaiting(self,val):
-        self.waitingval = val
-        self.waiting_changed.emit()
+def map_parameter_cc(effect_name, parameter, cc, connect_ports=True):
+    host.set_parameter_midi_cc(pluginMap[effect_name], parameterMap[effect_name][parameter],
+            cc)
+    host.set_parameter_midi_channel(pluginMap[effect_name], parameterMap[effect_name][parameter], midi_channel)
+    # connect ports
+    # ("ttymidi", "MIDI_in")
+    if connect_ports:
+        host.patchbay_connect(patchbay_external, portMap["ttymidi"]["group"],
+                portMap["ttymidi"]["ports"]["MIDI_in"],
+                portMap[effect_name]["group"],
+                portMap[effect_name]["ports"]["events-in"])
 
-    @Signal
-    def waiting_changed(self):
-        pass
 
-    waiting = Property(str, readWaiting, setWaiting, notify=waiting_changed)
-
-    @Slot(str)
-    def ui_save_preset(self, preset_name):
-        print("saving", preset_name)
-        # TODO add folders
-        outfile = "/presets/"+preset_name+".json"
-        current_preset.name = preset_name
-        save_preset(outfile)
-
-    @Slot(str)
-    def ui_load_preset_by_name(self, preset_file):
-        print("loading", preset_file)
-        outfile = preset_file[7:] # strip file:// prefix
-        load_preset(outfile)
-        update_counter.value+=1
-
-    @Slot()
-    def ui_copy_irs(self):
-        print("copy irs from USB")
-        # could convert any that aren't 48khz.
-        # instead we just only copy ones that are
-        command_reverb = """cd /media/reverbs; find . -iname "*.wav" -type f -exec sh -c 'test $(soxi -r "$0") = "48000"' {} \; -print0 | xargs -0 cp --target-directory=/audio/reverbs --parents"""
-        command_cab = """cd /media/cabs; find . -iname "*.wav" -type f -exec sh -c 'test $(soxi -r "$0") = "48000"' {} \; -print0 | xargs -0 cp --target-directory=/audio/cabs --parents"""
-        # copy all wavs in /usb/reverbs and /usr/cabs to /audio/reverbs and /audio/cabs
-        command_status[0].value = -1
-        command_status[1].value = -1
-        command_status[0].value = subprocess.call(command_reverb, shell=True)
-        command_status[1].value = subprocess.call(command_cab, shell=True)
-
-    @Slot()
-    def import_presets(self):
-        print("copy presets from USB")
-        # could convert any that aren't 48khz.
-        # instead we just only copy ones that are
-        command = """cd /media/presets; find . -iname "*.json" -type f -print0 | xargs -0 cp --target-directory=/presets --parents"""
-        command_status[0].value = subprocess.call(command, shell=True)
-
-    @Slot()
-    def export_presets(self):
-        print("copy presets to USB")
-        # could convert any that aren't 48khz.
-        # instead we just only copy ones that are
-        command = """cd /presets; mkdir -p /media/presets; find . -iname "*.json" -type f -print0 | xargs -0 cp --target-directory=/media/presets --parents"""
-        command_status[0].value = subprocess.call(command, shell=True)
-
-    @Slot()
-    def ui_update_firmware(self):
-        print("Updating firmware")
-        # dpkg the debs in the folder
-        command = """sudo dpkg -i /media/*.deb"""
-        command_status[0].value = subprocess.call(command, shell=True)
-
-    @Slot(bool)
-    def enable_ableton_link(self, enable):
-        extra = ":link:" if enable else ""
-        host.transportExtra = extra
-        host.set_engine_option(ENGINE_OPTION_TRANSPORT_MODE,
-                                    host.transportMode,
-                                    host.transportExtra)
-
-    @Slot(int)
-    def set_channel(self, channel):
-        for effect, parameters in effect_parameter_data.items():
-            for param_name, p_value in parameters.items():
-                if p_value.assigned_cc is not None:
-                    host.set_parameter_midi_channel(pluginMap[effect], parameterMap[effect][param_name], channel)
-        midi_channel.value = channel
+# @Slot()
+# def ui_copy_irs(self):
+#     print("copy irs from USB")
+#     # could convert any that aren't 48khz.
+#     # instead we just only copy ones that are
+#     command_reverb = """cd /media/reverbs; find . -iname "*.wav" -type f -exec sh -c 'test $(soxi -r "$0") = "48000"' {} \; -print0 | xargs -0 cp --target-directory=/audio/reverbs --parents"""
+#     command_cab = """cd /media/cabs; find . -iname "*.wav" -type f -exec sh -c 'test $(soxi -r "$0") = "48000"' {} \; -print0 | xargs -0 cp --target-directory=/audio/cabs --parents"""
+#     # copy all wavs in /usb/reverbs and /usr/cabs to /audio/reverbs and /audio/cabs
+#     command_status[0].value = -1
+#     command_status[1].value = -1
+#     command_status[0].value = subprocess.call(command_reverb, shell=True)
+#     command_status[1].value = subprocess.call(command_cab, shell=True)
+
+# @Slot()
+# def import_presets(self):
+#     print("copy presets from USB")
+#     # could convert any that aren't 48khz.
+#     # instead we just only copy ones that are
+#     command = """cd /media/presets; find . -iname "*.json" -type f -print0 | xargs -0 cp --target-directory=/presets --parents"""
+#     command_status[0].value = subprocess.call(command, shell=True)
+
+# @Slot()
+# def export_presets(self):
+#     print("copy presets to USB")
+#     # could convert any that aren't 48khz.
+#     # instead we just only copy ones that are
+#     command = """cd /presets; mkdir -p /media/presets; find . -iname "*.json" -type f -print0 | xargs -0 cp --target-directory=/media/presets --parents"""
+#     command_status[0].value = subprocess.call(command, shell=True)
+
+# @Slot()
+# def ui_update_firmware(self):
+#     print("Updating firmware")
+#     # dpkg the debs in the folder
+#     command = """sudo dpkg -i /media/*.deb"""
+#     command_status[0].value = subprocess.call(command, shell=True)
+
+def enable_ableton_link(enable):
+    extra = ":link:" if enable else ""
+    host.transportExtra = extra
+    host.set_engine_option(ENGINE_OPTION_TRANSPORT_MODE,
+                                host.transportMode,
+                                host.transportExtra)
+
+def set_channel(channel, effect_params):
+    for effect, parameter in effect_params:
+        host.set_parameter_midi_channel(pluginMap[effect], parameterMap[effect][parameter], channel)
+    global midi_channel
+    midi_channel = channel
 
 def engineCallback(host, action, pluginId, value1, value2, value3, valuef, valueStr):
     valueStr = charPtrToString(valueStr)
@@ -483,16 +222,8 @@ def engineCallback(host, action, pluginId, value1, value2, value3, valuef, value
         print("patchbay port added", pluginId, value1, value2, valueStr)
         if pluginId in invPortMap:
             portMap[invPortMap[pluginId]]["ports"][valueStr] = value1
-            for k, model in available_port_models.items():
-                if (invPortMap[pluginId], valueStr) in inv_output_port_names:
-                    if (((k.split(":")[0] == invPortMap[pluginId]) and
-                            (invPortMap[pluginId] != "system"))) or \
-                                    ((k.split(":")[0] == "sigmoid1") and \
-                                    (invPortMap[pluginId] == "delay1")):
-                        pass # don't allow effect to self connect
-                    else:
-                        # print("adding port:", invPortMap[pluginId], k)
-                        insert_row(model, inv_output_port_names[(invPortMap[pluginId], valueStr)])
+            if (invPortMap[pluginId], valueStr) in inv_output_port_names:
+                send_ui_message("add_port", (inv_output_port_names[(invPortMap[pluginId], valueStr)], ))
             if "jconvolver" in  portMap and pluginId == portMap["jconvolver"]["group"]:
                 # auto connect jconvolver ports
                 print("auto connect jconvolver", pluginId, value1, value2, valueStr)
@@ -593,7 +324,7 @@ def engineCallback(host, action, pluginId, value1, value2, value3, valuef, value
     #     host.PatchbayClientRenamedCallback.emit(pluginId, valueStr)
     # elif action == ENGINE_CALLBACK_PATCHBAY_PORT_ADDED:
     #     print("patchbay port added", pluginId, value1, value2, valueStr)
-        # def slot_handlePatchbayPortAddedCallback(self, clientId, portId, portFlags, portName):
+        # def slot_handlePatchbayPortAddedCallback(clientId, portId, portFlags, portName):
         # host.PatchbayPortAddedCallback.emit(pluginId, value1, value2, valueStr)
     # elif action == ENGINE_CALLBACK_PATCHBAY_PORT_REMOVED:
     #     host.PatchbayPortRemovedCallback.emit(pluginId, value1)
@@ -622,112 +353,6 @@ def engineCallback(host, action, pluginId, value1, value2, value3, valuef, value
     # elif action == ENGINE_CALLBACK_QUIT:
     #     host.QuitCallback.emit()
 
-def fileCallback(ptr, action, isDir, title, filter):
-    title  = charPtrToString(title)
-    filter = charPtrToString(filter)
-    global current_ir_file
-    global fileRet
-
-    # if action == FILE_CALLBACK_OPEN:
-    #     ret, ok = QFileDialog.getOpenFileName(gCarla.gui, title, "", filter) #, QFileDialog.ShowDirsOnly if isD ir else 0x0)
-    # elif action == FILE_CALLBACK_SAVE:
-    #     ret, ok = QFileDialog.getSaveFileName(gCarla.gui, title, "", filter, QFileDialog.ShowDirsOnly if isDir else 0x0)
-    # else:
-    #     ret, ok = ("", "")
-    # check if a file is selected, and if we're setting reverb or cab IR
-
-    if not current_ir_file:
-        return None
-
-    # FIXME
-    fileRet = c_char_p(current_ir_file.encode("utf-8"))
-    retval  = cast(byref(fileRet), POINTER(c_uintptr))
-    return retval.contents.value
-
-def save_preset(filename):
-    # write all effect parameters
-    output = {"effects":{}}
-    output["midi_map"] = {}
-    for effect, parameters in effect_parameter_data.items():
-        output["effects"][effect] = {}
-        # output["midi_map"][effect] = {}
-        for param_name, p_value in parameters.items():
-            if param_name == "ir":
-                output["effects"][effect][param_name] = p_value.name
-            else:
-                output["effects"][effect][param_name] = p_value.value
-            if p_value.assigned_cc is not None:
-                if effect not in output["midi_map"]:
-                    output["midi_map"][effect] = {}
-                output["midi_map"][effect][param_name] = p_value.assigned_cc
-    # write enabled state
-    output["state"] = {k:v.value for k,v in plugin_state.items()}
-    # write connections
-    output["connections"] = tuple(current_connection_pairs_poly)
-    output["midi_connections"] = tuple(current_midi_connection_pairs_poly)
-    # write knob / midi mapping XXX
-    output["knobs"] = {k:[v.effect, v.parameter] for k,v in knob_map.items()}
-    # write bpm
-    output["bpm"] = current_bpm.value
-    with open(filename, "w") as f:
-        json.dump(output, f)
-
-def load_preset(filename):
-    preset = {}
-    with open(filename) as f:
-        preset = json.load(f)
-    current_preset.name = os.path.splitext(os.path.basename(filename))[0]
-    # read all effect parameters
-    for effect_name, effect_value in preset["effects"].items():
-        for parameter_name, parameter_value in effect_value.items():
-            # update changed
-            if parameter_name == "ir":
-                if effect_parameter_data[effect_name][parameter_name].name != parameter_value:
-                    knobs.update_ir(effect_name == "reverb", parameter_value)
-            else:
-                if effect_parameter_data[effect_name][parameter_name].value != parameter_value:
-                    print("loading parameter", effect_name, parameter_name, parameter_value)
-                    knobs.ui_knob_change(effect_name, parameter_name, parameter_value)
-                # remove all existing MIDI mapping
-                if effect_parameter_data[effect_name][parameter_name].assigned_cc is not None:
-                    knobs.unmap_parameter(effect_name, parameter_name)
-    for effect_name, effect_value in preset["midi_map"].items():
-        for parameter_name, parameter_value in effect_value.items():
-            host.set_parameter_midi_cc(pluginMap[effect_name],
-                    parameterMap[effect_name][parameter_name], value)
-            effect_parameter_data[effect_name][parameter_name].assigned_cc = value
-            host.set_parameter_midi_channel(pluginMap[effect_name], parameterMap[effect][parameter_name], midi_channel.value)
-    # read enabled state
-    for effect, is_active in preset["state"].items():
-        if effect == "global":
-            pass
-        else:
-            set_active(effect, is_active)
-    # read connections
-    preset_connections = set([tuple(a) for a in preset["connections"]])
-    # remove connections that aren't in the new preset
-    for source_port, target_port in (current_connection_pairs_poly-preset_connections):
-        effect, source_p = source_port.split(":")
-        knobs.ui_remove_connection(effect, source_p, target_port)
-    # add connections that are in the new preset but not the old
-    for source_port, target_port in (preset_connections - current_connection_pairs_poly):
-        effect, source_p = source_port.split(":")
-        knobs.ui_add_connection(effect, source_p, target_port)
-    midi_connections = set([tuple(a) for a in preset["midi_connections"]])
-    for source_pair, target_pair in midi_connections:
-        host.patchbay_connect(patchbay_external, portMap[source_pair[0]]["group"],
-                portMap[source_pair[0]]["ports"][source_pair[1]],
-                portMap[target_pair[0]]["group"],
-                portMap[target_pair[0]]["ports"][target_pair[1]])
-    global current_midi_connection_pairs_poly
-    current_midi_connection_pairs_poly = midi_connections
-    # read knob mapping
-    for knob, mapping in preset["knobs"].items():
-        set_knob_current_effect(knob, mapping[0], mapping[1])
-    # read bpm
-    if current_bpm.value != preset["bpm"]:
-        current_bpm.value = preset["bpm"]
-        host.transport_bpm(preset["bpm"])
 
 def next_preset():
     # need to have some kind of mapping for all presets so you can assign numbers to them
@@ -735,7 +360,7 @@ def next_preset():
 
 def auto_connect_ports():
     try:
-        while True:
+        while not gCarla.term:
             valueStr = pending_connect.get(block=False)
             if valueStr == "OutL":
                 # connect to "postreverb:In Left" 
@@ -750,7 +375,7 @@ def auto_connect_ports():
                         portMap["postreverb"]["group"],
                         portMap["postreverb"]["ports"]["In Right"])
                 # if we had the loading screen up, we are now loaded
-                is_loading["reverb"].value = False
+                send_ui_message("is_loading", ("reverb", ))
             elif valueStr == "ReverbIn":
                 # connect to "postreverb:In Left" 
                 host.patchbay_connect(patchbay_external,
@@ -772,7 +397,7 @@ def auto_connect_ports():
                         portMap["postcab"]["group"],
                         portMap["postcab"]["ports"]["In"])
                 # if we had the loading screen up, we are now loaded
-                is_loading["cab"].value = False
+                send_ui_message("is_loading", ("cab", ))
     except queue.Empty:
         pass
 
@@ -782,7 +407,6 @@ host = CarlaHostDLL("/git_repos/Carla/bin/libcarla_standalone2.so", False)
 host.set_engine_option(ENGINE_OPTION_PATH_BINARIES, 0, binaryDir)
 # host.set_engine_callback(lambda h,a,p,v1,v2,v3,vs: engineCallback(host,a,p,v1,v2,v3,vs))
 host.set_engine_callback(lambda h,a,p,v1,v2,v3,vf,vs: engineCallback(host,a,p,v1,v2,v3,vf,vs))
-host.set_file_callback(fileCallback)
 
 if not host.engine_init("JACK", "PolyCarla"):
     print("Engine failed to initialize, possible reasons:\n%s" % host.get_last_error())
@@ -826,207 +450,62 @@ host.add_plugin(BINARY_NATIVE, PLUGIN_LV2, None, "mclk", "http://gareus.org/oss/
 signal(SIGINT,  signalHandler)
 signal(SIGTERM, signalHandler)
 
-# class Knobs(QObject):
-#     """Output stuff on the console."""
-#     @Slot(str, str, 'double')
-#     def ui_knob_change(self, effect_name, parameter_id, value):
-#         # print(x, y)
-#         host.set_parameter_value(effects[effect_name], parameter_id, value)
-
-###
-
-knob_map = {"left": PolyEncoder("delay1", "Delay_1"), "right": PolyEncoder("delay1", "Feedback_4")}
-lfos = []
-
-
-for n in range(1):
-    lfos.append({})
-    lfos[n]["num_points"] = PolyValue("num_points", 1, 1, 16)
-    lfos[n]["channel"] = PolyValue("channel", 1, 1, 16)
-    lfos[n]["cc_num"] = PolyValue("cc_num", 102+n, 0, 127)
-    for i in range(1,17):
-        lfos[n]["time"+str(i)] = PolyValue("time"+str(i), 0, 0, 1)
-        lfos[n]["value"+str(i)] = PolyValue("value"+str(i), 0, 0, 1)
-        lfos[n]["style"+str(i)] = PolyValue("style"+str(i), 0, 0, 5)
-
-# this is not great
-
-effect_parameter_data = {"delay1": {"BPM_0" : PolyValue("BPM_0", 120.000000, 30.000000, 300.000000),
-        "Delay_1" : PolyValue("Time", 0.500000, 0.001000, 1.000000),
-        "Warp_2" : PolyValue("Warp", 0.000000, -1.000000, 1.000000),
-        "DelayT60_3" : PolyValue("Glide", 0.500000, 0.000000, 100.000000),
-        "Feedback_4" : PolyValue("Feedback", 0.300000, 0.000000, 1.000000),
-        "Amp_5" : PolyValue("Level", 0.500000, 0.000000, 1.000000),
-        "FeedbackSm_6" : PolyValue("Tone", 0.000000, 0.000000, 1.000000),
-        "EnableEcho_7" : PolyValue("EnableEcho_7", 1.000000, 0.000000, 1.000000),
-        "carla_level": PolyValue("level", 1, 0, 1)},
-    "delay2": {"BPM_0" : PolyValue("BPM_0", 120.000000, 30.000000, 300.000000),
-            "Delay_1" : PolyValue("Time", 0.500000, 0.001000, 1.000000),
-            "Warp_2" : PolyValue("Warp", 0.000000, -1.000000, 1.000000),
-            "DelayT60_3" : PolyValue("Glide", 0.500000, 0.000000, 100.000000),
-            "Feedback_4" : PolyValue("Feedback", 0.300000, 0.000000, 1.000000),
-            "Amp_5" : PolyValue("Level", 0.500000, 0.000000, 1.000000),
-            "FeedbackSm_6" : PolyValue("Tone", 0.000000, 0.000000, 1.000000),
-            "EnableEcho_7" : PolyValue("EnableEcho_7", 1.000000, 0.000000, 1.000000),
-            "carla_level": PolyValue("level", 1, 0, 1)},
-    "delay3": {"BPM_0" : PolyValue("BPM_0", 120.000000, 30.000000, 300.000000),
-            "Delay_1" : PolyValue("Time", 0.500000, 0.001000, 1.000000),
-            "Warp_2" : PolyValue("Warp", 0.000000, -1.000000, 1.000000),
-            "DelayT60_3" : PolyValue("Glide", 0.500000, 0.000000, 100.000000),
-            "Feedback_4" : PolyValue("Feedback", 0.300000, 0.000000, 1.000000),
-            "Amp_5" : PolyValue("Level", 0.500000, 0.000000, 1.000000),
-            "FeedbackSm_6" : PolyValue("Tone", 0.000000, 0.000000, 1.000000),
-            "EnableEcho_7" : PolyValue("EnableEcho_7", 1.000000, 0.000000, 1.000000),
-            "carla_level": PolyValue("level", 1, 0, 1)},
-    "delay4": {"BPM_0" : PolyValue("BPM_0", 120.000000, 30.000000, 300.000000),
-            "Delay_1" : PolyValue("Time", 0.500000, 0.001000, 1.000000),
-            "Warp_2" : PolyValue("Warp", 0.000000, -1.000000, 1.000000),
-            "DelayT60_3" : PolyValue("Glide", 0.500000, 0.000000, 100.000000),
-            "Feedback_4" : PolyValue("Feedback", 0.300000, 0.000000, 1.000000),
-            "Amp_5" : PolyValue("Level", 0.500000, 0.000000, 1.000000),
-            "FeedbackSm_6" : PolyValue("Tone", 0.000000, 0.000000, 1.000000),
-            "EnableEcho_7" : PolyValue("EnableEcho_7", 1.000000, 0.000000, 1.000000),
-            "carla_level": PolyValue("level", 1, 0, 1)},
-    "reverb": {"gain": PolyValue("gain", 0, -90, 24), "ir": PolyValue("/audio/reverbs/emt_140_dark_1.wav", 0, 0, 1),
-        "carla_level": PolyValue("level", 1, 0, 1)},
-    "postreverb": {"routing": PolyValue("gain", 6, 0, 6), "carla_level": PolyValue("level", 1, 0, 1)},
-    "mixer": {"mix_1_1": PolyValue("mix 1,1", 1, 0, 1), "mix_1_2": PolyValue("mix 1,2", 0, 0, 1),
-        "mix_1_3": PolyValue("mix 1,3", 0, 0, 1),"mix_1_4": PolyValue("mix 1,4", 0, 0, 1),
-        "mix_2_1": PolyValue("mix 2,1", 0, 0, 1),"mix_2_2": PolyValue("mix 2,2", 1, 0, 1),
-        "mix_2_3": PolyValue("mix 2,3", 0, 0, 1),"mix_2_4": PolyValue("mix 2,4", 0, 0, 1),
-        "mix_3_1": PolyValue("mix 3,1", 0, 0, 1),"mix_3_2": PolyValue("mix 3,2", 0, 0, 1),
-        "mix_3_3": PolyValue("mix 3,3", 1, 0, 1),"mix_3_4": PolyValue("mix 3,4", 0, 0, 1),
-        "mix_4_1": PolyValue("mix 4,1", 0, 0, 1),"mix_4_2": PolyValue("mix 4,2", 0, 0, 1),
-        "mix_4_3": PolyValue("mix 4,3", 0, 0, 1),"mix_4_4": PolyValue("mix 4,4", 1, 0, 1)
-        },
-    "tape1": {"drive": PolyValue("drive", 5, 0, 10), "blend": PolyValue("tape vs tube", 10, -10, 10)},
-    # "filter1": {"freq": PolyValue("cutoff", 440, 20, 15000, "log"), "res": PolyValue("resonance", 0, 0, 0.8)},
-    "sigmoid1": {"Pregain": PolyValue("pre gain", 0, -90, 20), "Postgain": PolyValue("post gain", 0, -90, 20)},
-    "reverse1": {"fragment": PolyValue("fragment", 1000, 100, 1600),
-        "wet": PolyValue("wet", 0, -90, 20),
-        "dry": PolyValue("dry", 0, -90, 20)},
-    # "reverse2": {"fragment": PolyValue("fragment", 1000, 100, 1600),
-    #     "wet": PolyValue("wet", 0, -90, 20),
-    #     "dry": PolyValue("dry", 0, -90, 20)},
-    "eq2": {
-        "enable": PolyValue("Enable", 1.000000, 0.000000, 1.0),
-        "gain": PolyValue("Gain", 0.000000, -18.000000, 18.000000),
-        "HighPass": PolyValue("Highpass", 0.000000, 0.000000, 1.000000),
-        "HPfreq": PolyValue("Highpass Frequency", 20.000000, 5.000000, 1250.000000),
-        "HPQ": PolyValue("HighPass Resonance", 0.700000, 0.000000, 1.400000),
-        "LowPass": PolyValue("Lowpass", 0.000000, 0.000000, 1.000000),
-        "LPfreq": PolyValue("Lowpass Frequency", 20000.000000, 500.000000, 20000.000000),
-        "LPQ": PolyValue("LowPass Resonance", 1.000000, 0.000000, 1.400000),
-        "LSsec": PolyValue("Lowshelf", 1.000000, 0.000000, 1.000000),
-        "LSfreq": PolyValue("Lowshelf Frequency", 80.000000, 25.000000, 400.000000),
-        "LSq": PolyValue("Lowshelf Bandwidth", 1.000000, 0.062500, 4.000000),
-        "LSgain": PolyValue("Lowshelf Gain", 0.000000, -18.000000, 18.000000),
-        "sec1": PolyValue("Section 1", 1.000000, 0.000000, 1.000000),
-        "freq1": PolyValue("Frequency 1", 160.000000, 20.000000, 2000.000000),
-        "q1": PolyValue("Bandwidth 1", 0.600000, 0.062500, 4.000000),
-        "gain1": PolyValue("Gain 1", 0.000000, -18.000000, 18.000000),
-        "sec2": PolyValue("Section 2", 1.000000, 0.000000, 1.000000),
-        "freq2": PolyValue("Frequency 2", 397.000000, 40.000000, 4000.000000),
-        "q2": PolyValue("Bandwidth 2", 0.600000, 0.062500, 4.000000),
-        "gain2": PolyValue("Gain 2", 0.000000, -18.000000, 18.000000),
-        "sec3": PolyValue("Section 3", 1.000000, 0.000000, 1.000000),
-        "freq3": PolyValue("Frequency 3", 1250.000000, 100.000000, 10000.000000),
-        "q3": PolyValue("Bandwidth 3", 0.600000, 0.062500, 4.000000),
-        "gain3": PolyValue("Gain 3", 0.000000, -18.000000, 18.000000),
-        "sec4": PolyValue("Section 4", 1.000000, 0.000000, 1.000000),
-        "freq4": PolyValue("Frequency 4", 2500.000000, 200.000000, 20000.000000),
-        "q4": PolyValue("Bandwidth 4", 0.600000, 0.062500, 4.000000),
-        "gain4": PolyValue("Gain 4", 0.000000, -18.000000, 18.000000),
-        "HSsec": PolyValue("Highshelf", 1.000000, 0.000000, 1.000000),
-        "HSfreq": PolyValue("Highshelf Frequency", 8000.000000, 1000.000000, 16000.000000),
-        "HSq": PolyValue("Highshelf Bandwidth", 1.000000, 0.062500, 4.000000),
-        "HSgain": PolyValue("Highshelf Gain", 0.000000, -18.000000, 18.000000)},
-    "cab": {"gain": PolyValue("gain", 0, -90, 24), "ir": PolyValue("/audio/cabs/1x12cab.wav", 0, 0, 1),
-        "carla_level": PolyValue("level", 1, 0, 1)},
-    "postcab": {"gain": PolyValue("gain", 0, -90, 24), "carla_level": PolyValue("level", 1, 0, 1)},
-    "lfo1": lfos[0],
-    # "lfo2": lfos[1],
-    # "lfo3": lfos[2],
-    # "lfo4": lfos[3],
-    "mclk": {"carla_level": PolyValue("level", 1, 0, 1)},
-    }
+class Encoder():
+    # name, min, max, value
+    def __init__(self, starteffect="", startparameter=""):
+        self.effect = starteffect
+        self.parameter = startparameter
+        self.speed = 1
+        self.rmin = 0
+        self.rmax = 1
+
+
+knob_map = {"left": Encoder("delay1", "Delay_1"), "right": Encoder("delay1", "Feedback_4")}
 
 all_effects = [("delay1", True), ("delay2", True), ("delay3", True),
         ("delay4", True), ("reverb", True), ("postreverb", True), ("mixer", True),
         ("tape1", False), ("reverse1", False),
         ("sigmoid1", False), ("eq2", True), ("cab", True), ("postcab", True)]
-plugin_state = dict({(k, PolyBool(initial)) for k, initial in all_effects})
-plugin_state["global"] = PolyBool(True)
-
-app = QGuiApplication(sys.argv)
-QIcon.setThemeName("digit")
-# Instantiate the Python object.
-knobs = Knobs()
-current_bpm = PolyValue("BPM", 120, 30, 250) # bit of a hack
-global_bypass = PolyValue("BPM", 120, 30, 250) # bit of a hack
-current_preset = PolyValue("Default Preset", 0, 0, 1)
-update_counter = PolyValue("update counter", 0, 0, 100000)
-command_status = [PolyValue("command status", -1, -10, 100000), PolyValue("command status", -1, -10, 100000)]
-delay_num_bars = PolyValue("Num bars", 1, 1, 16)
-midi_channel = PolyValue("channel", 1, 1, 16)
-is_loading = {"reverb":PolyBool(False), "cab":PolyBool(False)}
-
-qmlEngine = QQmlApplicationEngine()
-# Expose the object to QML.
-context = qmlEngine.rootContext()
-for k, v in available_port_models.items():
-    context.setContextProperty(k.replace(" ", "_").replace(":", "_")+"AvailablePorts", v)
-for k, v in used_port_models.items():
-    context.setContextProperty(k.replace(" ", "_").replace(":", "_")+"UsedPorts", v)
-context.setContextProperty("knobs", knobs)
-context.setContextProperty("polyValues", effect_parameter_data)
-context.setContextProperty("knobMap", knob_map)
-context.setContextProperty("currentBPM", current_bpm)
-context.setContextProperty("pluginState", plugin_state)
-context.setContextProperty("currentPreset", current_preset)
-context.setContextProperty("updateCounter", update_counter)
-context.setContextProperty("commandStatus", command_status)
-context.setContextProperty("delayNumBars", delay_num_bars)
-context.setContextProperty("midiChannel", midi_channel)
-context.setContextProperty("isLoading", is_loading)
-
-# engine.load(QUrl("qrc:/qml/digit.qml"))
-qmlEngine.load(QUrl("qml/digit.qml"))
+plugin_state = dict({(k, initial) for k, initial in all_effects})
+plugin_state["global"] = True
+
+current_bpm = 120
+current_preset = "Default Preset"
+command_status = [-1, -1]
+midi_channel = 1
+# is_loading = {"reverb":False, "cab":False}
 
 mixer_is_connected = False
 effects_are_connected = False
 knobs_are_initial_mapped  = False
 
-######### UI is setup
+# def translate_range(value, leftMin, leftMax, rightMin, rightMax):
+#     # Figure out how 'wide' each range is
+#     leftSpan = leftMax - leftMin
+#     rightSpan = rightMax - rightMin
 
+#     # Convert the left range into a 0-1 range (float)
+#     valueScaled = float(value - leftMin) / float(leftSpan)
 
+#     # Convert the 0-1 range into a value in the right range.
+#     return rightMin + (valueScaled * rightSpan)
 
-def translate_range(value, leftMin, leftMax, rightMin, rightMax):
-    # Figure out how 'wide' each range is
-    leftSpan = leftMax - leftMin
-    rightSpan = rightMax - rightMin
+# def map_ui_parameter_lv2_value(effect, parameter, in_min, in_max, value):
+#     # map UI or hardware range to effect range
+#     # in_min / in_max are what the knob / ui generates
+#     #   from numpy import interp
+#     #  interp(256,[1,512],[5,10])
+#     out_min = effect_parameter_data[effect][parameter].rmin
+#     out_max = effect_parameter_data[effect][parameter].rmax
+#     return translate_range(value, in_min, in_max, out_min, out_max)
 
-    # Convert the left range into a 0-1 range (float)
-    valueScaled = float(value - leftMin) / float(leftSpan)
+# def map_lv2_value_to_ui_knob(effect, parameter, in_min, in_max, value):
+#     # map UI or hardware range to effect range
+#     # in_min / in_max are what the knob / ui generates
+#     out_min = effect_parameter_data[effect][parameter].rmin
+#     out_max = effect_parameter_data[effect][parameter].rmax
+#     return translate_range(value, out_min, out_max, in_min, in_max)
 
-    # Convert the 0-1 range into a value in the right range.
-    return rightMin + (valueScaled * rightSpan)
-
-def map_ui_parameter_lv2_value(effect, parameter, in_min, in_max, value):
-    # map UI or hardware range to effect range
-    # in_min / in_max are what the knob / ui generates
-    #   from numpy import interp
-    #  interp(256,[1,512],[5,10])
-    out_min = effect_parameter_data[effect][parameter].rmin
-    out_max = effect_parameter_data[effect][parameter].rmax
-    return translate_range(value, in_min, in_max, out_min, out_max)
-
-def map_lv2_value_to_ui_knob(effect, parameter, in_min, in_max, value):
-    # map UI or hardware range to effect range
-    # in_min / in_max are what the knob / ui generates
-    out_min = effect_parameter_data[effect][parameter].rmin
-    out_max = effect_parameter_data[effect][parameter].rmax
-    return translate_range(value, out_min, out_max, in_min, in_max)
 
 def set_knob_current_effect(knob, effect, parameter):
     # get current value and update encoder / cache.
@@ -1042,17 +521,28 @@ def handle_encoder_change(is_left, change):
         knob = "left"
     knob_effect = knob_map[knob].effect
     knob_parameter = knob_map[knob].parameter
-    value = effect_parameter_data[knob_effect][knob_parameter].value
+    value = host.get_current_parameter_value (pluginMap[knob_effect],
+                    parameterMap[knob_effect][knob_parameter])
     # base speed * speed multiplier
-    base_speed = (abs(effect_parameter_data[knob_effect][knob_parameter].rmin) + abs(effect_parameter_data[knob_effect][knob_parameter].rmax)) / normal_speed
+    base_speed = (abs(knob_map[knob].rmin) + abs(knob_map[knob].rmax)) / normal_speed
     value = value + (change * knob_map[knob].speed * base_speed)
     print("knob value is", value)
     # knob change handles clamping
-    knobs.ui_knob_change(knob_effect, knob_parameter, value)
+    knob_change(knob_effect, knob_parameter, value)
+    send_ui_message("value_change", (knob_effect, knob_parameter, value))
 
 def update_delay_bpms():
     for effect_name in ["delay1", "delay2", "delay3", "delay4"]:
-        knobs.ui_knob_change(effect_name, "BPM_0", current_bpm.value)
+        knob_change(effect_name, "BPM_0", current_bpm)
+
+def set_bpm(bpm):
+    global current_bpm
+    current_bpm = bpm
+    update_delay_bpms()
+    host.transport_bpm(bpm)
+    send_ui_message("bpm_change", (bpm, ))
+    print("setting tempo", bpm)
+
 
 start_tap_time = None
 ## tap callback is called by hardware button from the GPIO checking thread
@@ -1067,27 +557,61 @@ def handle_tap():
         bpm = 60 / d
         if bpm > 30 and bpm < 250:
             # set host BPM
-            host.transport_bpm(bpm)
-            print("setting tempo", bpm)
-            current_bpm.value = bpm
-            update_delay_bpms()
+            set_bpm(bpm)
 
     # record start time
     start_tap_time = current_tap
 
 def handle_next():
     # disable delay
-    # plugin_state["global"].value = not plugin_state["global"].value
-    knobs.toggle_enabled("delay1")
+    toggle_enabled("delay1")
+    toggle_enabled("delay2")
+    toggle_enabled("delay3")
+    toggle_enabled("delay4")
+    send_ui_message("set_plugin_state", ("delay1", plugin_state["delay1"]))
+    send_ui_message("set_plugin_state", ("delay2", plugin_state["delay2"]))
+    send_ui_message("set_plugin_state", ("delay3", plugin_state["delay3"]))
+    send_ui_message("set_plugin_state", ("delay4", plugin_state["delay4"]))
 
 def handle_bypass():
     # global bypass
-    plugin_state["global"].value = not plugin_state["global"].value
-    if plugin_state["global"].value:
+    plugin_state["global"] = not plugin_state["global"]
+    send_ui_message("set_plugin_state", ("global", plugin_state["global"] ))
+    if plugin_state["global"]:
         pedal_hardware.effect_on()
     else:
         pedal_hardware.effect_off()
 
+def process_core_messages():
+    # pop from queue
+    try:
+        while not gCarla.term:
+            m = core_messages.get(block=False)
+            if m[0] == "add_connection_pair":
+                add_connecton_pair(*m[1])
+            elif m[0] == "add_connection":
+                add_connection(*m[1])
+            elif m[0] == "map_parameter":
+                map_parameter(*m[1])
+            elif m[0] == "map_parameter_cc":
+                map_parameter_cc(*m[1])
+            elif m[0] == "unmap_parameter":
+                unmap_parameter(*m[1])
+            elif m[0] == "set_bpm":
+                set_bpm(*m[1])
+            elif m[0] == "remove_connection":
+                remove_connection(*m[1])
+            elif m[0] == "knob_change":
+                knob_change(*m[1])
+            elif m[0] == "toggle_enabled":
+                toggle_enabled(*m[1])
+            elif m[0] == "set_active":
+                set_active(*m[1])
+            elif m[0] == "set_channel":
+                set_channel(*m[1])
+    except queue.Empty:
+        pass
+
 # def connect_ports(source_effect, source_port, target_effect, target_port):
 #     host.patchbay_connect(patchbay_external, portMap["mixer"]["group"],
 #             portMap["mixer"]["ports"][source_port],
@@ -1101,11 +625,13 @@ pedal_hardware.bypass_callback = handle_bypass
 pedal_hardware.encoder_change_callback = handle_encoder_change
 patchbay_external = False
 
+p = Process(name="digit_frontend.py", target=digit_frontend.ui_worker, args=(ui_messages, core_messages))
+p.start()
+
 while host.is_engine_running() and not gCarla.term:
     # print("engine is idle")
     host.engine_idle()
     # print("processing GUI events in CALLBACK")
-    app.processEvents()
     sleep(0.01)
     # wait until the last of our plugins is added, then connect them if they haven't been connected yet
     # default routing is input 1 to delay 1 to reverb to cab to out
@@ -1153,51 +679,40 @@ while host.is_engine_running() and not gCarla.term:
             knobs_are_initial_mapped = True
     else:
         pedal_hardware.process_input()
+        process_core_messages()
         auto_connect_ports()
 
 pedal_hardware.EXIT_THREADS = True
+host.set_engine_about_to_close()
+send_ui_message("exit", ("exit", ))
+p.terminate()
+p.join()
+
+while True:
+    try:
+        ui_messages.get(block=False)
+    except queue.Empty:
+        break
+
+while True:
+    try:
+        core_messages.get(block=False)
+    except queue.Empty:
+        break
+
+ui_messages.close()
+ui_messages.join_thread()
+core_messages.close()
+core_messages.join_thread()
+print("exiting core")
 
 if not gCarla.term:
     print("Engine closed abruptely")
+else:
+    print("Normal exit")
 
 if not host.engine_close():
     print("Engine failed to close, possible reasons:\n%s" % host.get_last_error())
+else:
+    print("Engine Closed")
 exit(1)
-
-## on ui knob change, set value in lv2
-# def ui_knob_change(value):
-    # knob is zero to 1, map to pedal range 
-    #fuzz.params[0].minimum / fuzz.params[0].maximum
-    #fuzz.params[0].value
-
-# 4 out, delay, reverb, cab
-# 4 in, delay, reverb, cab
-
-#
-# fuzz.toggle()
-# or
-# fuzz.active = not fuzz.active
-
-# mixer points
-# effects on wet path dry option later
-# delay wet to other delay
-# delay wet to [reverb .... 
-# delay post to
-# one effect is send?
-
-
-# delay 1 wet to delay 2,3,4. Reverb, output 1,2,3,4
-# collapsable mixer? only 1->1 shown?
-# delay 1 post to delay 2,3,4. Reverb, output 1,2,3,4
-
-
-# tape / tube, filter, eq, flange, bit crush
-
-
-# cv mixer
-
-
-# calculate tap tempo then
-# set transport state
-
-
